October 1998

Low-level wave audio, part 3

by Kent Reisdorph

Sample files are available from our Web site as part of the file oct98.zip. Visit www.zdjournals.com/cpb and click the Source Code hyperlink.

This month, we finish our series on low-level audio; the previous two articles appeared in the July and August 1998 issues of C++Builder Developer's Journal. In this installment, we'll show you how to record wave audio using the low-level audio functions. Those functions include waveInOpen, waveInPrepareHeader, waveInAddBuffer, waveInStart, waveInUnprepareHeader, and waveInClose. Recording audio is only half the story, of course. You'll ultimately have to save the recorded data to disk (at least, in most applications). Our example application shows you how to save data after you record it.

Recording wave data

Recording wave data with the low-level interface is only slightly more complicated than playing wave data, as we showed you in Part 2 of this series. Here are the steps required:
bullet Set the wave format.
bulletOpen the wave input device.
bullet Allocate a buffer to hold the wave data.
bullet Prepare the wave header.
bulletStart recording.
bullet Close the device when recording finishes.
In the following sections, we'll explain each of these steps in detail.

Set the wave format

Before you open the device, you must set the recording parameters (wave format) for the wave data. You do so by filling out an instance of the WAVEFORMATEX structure. Wave data is in pulse code modulation (PCM) format. PCM data has three properties that determine the quality of the recorded sound: the number of bits per sample, the number of samples per second, and the number of channels. You can set the bits-per-sample element of the equation to either 8 or 16. The number of samples per second is usually 8,000; 11,025; 22,050; or 44,100. The number of channels will be either 1 (monaural) or 2 (stereo).

The size of the wave data stored on disk is directly proportional to the quality of the audio. Ten seconds of audio recorded at 8 kHz, mono, and 8 bits per sample will produce a WAV file about 79KB in size. Ten seconds of audio recorded at 44.1 kHz, stereo, and 16 bits per sample, however, will result in a file that is 862KB in size. Obviously, you want the wave audio quality to be good--but be careful of going overboard. In general, a wave format of 22.05 kHz, 8 bits per sample, and mono provides a reasonable compromise between sound quality and wave data size (the Windows system sounds are recorded using that format).

After you've determined the wave format, you need to fill out the WAVEFORMATEX structure. Here's the code:


// class member variable
WAVEFORMATEX WaveFormat;

// later...
WaveFormat.wFormatTag      = 
  WAVE_FORMAT_PCM;
WaveFormat.nChannels       = 1; 
WaveFormat.nSamplesPerSec  = 22050;
WaveFormat.wBitsPerSample  = 8;
WaveFormat.nAvgBytesPerSec = 22050;
WaveFormat.nBlockAlign     = 1;
WaveFormat.cbSize          = 0;

Notice that the wFormatTag member is set to WAVE_FORMAT_PCM. This is the wave format for Windows WAV files (other wave formats are defined in MMREG.H, if you want to take a look). The nChannels, nSamplesPerSec, and nBitsPerSample members are set as described in the preceding paragraphs on PCM formats. The nAvgBytesPerSec member is set to the average number of bytes recorded (or sampled) per second. This value is determined by the following formula:

SamplesPerSecond * Channels

The nBlockAlign data member also requires explanation. You determine the value for the block alignment with this formula:

(Channels * BitsPerSample) / 8

The cbSize data member specifies the number of extra bytes of data stored with the wave format header. This value isn't typically used when recording wave data.

Open the wave input device

Opening the wave input device is nearly identical to opening the wave output device in Part 2 of this series. Before you open the wave device, though, you should query the device to be sure it supports the format you've selected. You do so by calling waveInOpen() with the WAVE_FORMAT_QUERY flag. For example:

int Res = waveInOpen(&WaveHandle, 
  WAVE_MAPPER, &WaveFormat, 0, 0, 
  WAVE_FORMAT_QUERY);
if (Res == WAVERR_BADFORMAT) return;

If the specified wave format (passed in the WaveFormat structure in the third parameter) is compatible with the wave device, waveInOpen() will return 0. If the format is incompatible with the wave device, waveInOpen() will return WAVERR_BADFORMAT. When you query the device, Windows checks the device's capabilities, but doesn't open the device. Querying the device gives you a chance to abort a recording operation if the specified wave format is invalid. The rest of the parameters for waveInOpen() are identical to those of waveOutOpen(), as in Part 2.

Now that you know the requested format is valid, you can open the wave input device. Here's the code:


Res = waveInOpen(
  &WaveHandle, WAVE_MAPPER, &WaveFormat,
  MAKELONG(Handle, 0), 0, CALLBACK_WINDOW);

In this example, WaveHandle is a variable (of type HWAVEIN) that will receive a handle to the device if the device is opened successfully. The WAVE_MAPPER constant tells Windows to use the first available wave input device on the system that can support the specified format (usually the sound card). The fourth and sixth parameters of waveInOpen() tell Windows to send wave input messages to the form's window procedure. The fifth parameter passes any additional data to the application when Windows sends the wave-in messages. We aren't using any additional data, so we pass 0 for this parameter. If waveInOpen() returns 0, you can proceed with the next step: creating the wave input buffer.

 
Note: Checking return values
All of the code in this article assigns the return value from the various wave-input functions to a variable called Res. The code examples don't show our error-checking code, for brevity's sake--but you should always check the return values in your own code and take appropriate action if you encounter an error.

Allocate a buffer for the wave data

Next, you allocate a buffer to hold the wave data. First, you must know how much memory to allocate for the buffer. You can calculate the buffer size based on the number of seconds of data you want to record and the recording format, using this formula:

RecordSeconds * AvgerageBytesPerSecond

For example, if you want to record 10 seconds of data at 22.05 kHz, mono, 8-bit, you'll use this code to allocate the buffer:

// class member variables
char* WaveData;
int BufferSize;

BufferSize = 10 * 22050;
WaveData = new char[BufferSize];

Remember to keep track of your allocations and deallocations so that your program doesn't leak memory.

Prepare the wave header

Now you're ready to prepare the wave input header, a structure that contains the information Windows needs to carry out the recording operation. Specifically, it holds the size of the wave data buffer and a pointer to the buffer itself. (Don't confuse the wave input header with the wave format header.) The wave input header is an instance of a WAVEHDR structure--this structure is used when performing both playback and recording, so you can ignore many of its data members when recording wave data. You need to set these data members: dwBufferLength, dwFlags, and lpBuffer. For example:

//class member variable
WAVEHDR WaveHeader;

WaveHeader.dwBufferLength = BufferSize;
WaveHeader.dwFlags        = 0;
WaveHeader.lpData         = WaveData;

Notice that the dwBufferLength and lpData members are assigned values obtained when we allocated the wave data buffer in the previous step. You must set the dwFlags parameter to 0 before recording. Now that the wave input header is set up, you can prepare it for use with the waveInPrepareHeader() function:

Res = waveInPrepareHeader(WaveHandle, 
  &WaveHeader, sizeof(WAVEHDR));

If waveInPrepareHeader() returns 0, the wave header was successfully prepared and is ready for use. To implement the wave header, call the waveInAddBuffer() function, as follows:

Res = waveInAddBuffer(WaveHandle, 
  &WaveHeader, sizeof(WAVEHDR));

This function adds the buffer specified in the wave header to the list of buffers that will be played. We're using only one buffer in this case, but this step is still required.

Start recording

You're ready to start recording. You do so by calling the waveInStart() function, passing the handle to the wave input device (obtained when we called waveInOpen()). This part is simple:

Res = waveInStart(WaveHandle);

The waveInStart() function starts recording and immediately returns. If waveInStart() returns 0, recording has started and control is immediately returned to the calling application. Put another way, the recording process happens asynchronously--recording starts and your application is free to go about its business while recording is taking place. This gives you the ability to let the user stop the recording in response to a button click or some other event.

Catching wave-in messages

In order to do something useful with the wave data, you need to know when recording has finished. To accomplish this, you'll catch the Windows messages that correspond to wave audio input.

Wave-in messages

In Part 2 of this series, we talked about the wave-out messages Windows sends when playing a wave file. Naturally, there are corresponding messages for wave recording. Table A lists those messages.

Table A: Windows wave-in messages
Message Description
MM_WIM_OPEN Device has been opened.
MM_WIM_CLOSE Device has been closed.
MM_WIM_DATA Recording has finished and the input buffer is being returned to the application.

Of these messages, you're most likely to be concerned with MM_WIM_DATA. In fact, you must respond to this message if you're going to do anything useful with the recorded wave data. MM_WIM_DATA is received when the wave buffer is being returned to the calling application. This can happen as the result of two primary events: Either the wave input buffer has filled up, or the recording operation was interrupted. In either case, this message notifies you that the wave recording operation has finished. At that point, you can save the wave data to a file.

Catching the MM_WIM_DATA message

You catch the MM_WIM_DATA message by implementing a C++Builder message map. The message map looks like this:

BEGIN_MESSAGE_MAP
  MESSAGE_HANDLER(
    MM_WIM_DATA, TMessage, OnWaveMessage)
END_MESSAGE_MAP(TForm)

The OnWaveMessage() message handler will be called when the MM_WIM_DATA message is received. At that point, you'll take appropriate action, such as saving the wave data to a file. Let's look at that next.

The MM_WIM_DATA message handler

Your message handler for the MM_WIM_DATA message needs to do three things:
bulletClose the wave input device.
bullet Save the wave data to disk.
bullet Free the memory allocated for the wave input buffer.
Here's how your OnWaveMessage method will look:

void TMainForm::OnWaveMessage(
  TMessage& msg)
{
  if (msg.Msg == MM_WIM_DATA) {
    // close the wave device
    waveInClose(WaveHandle);
    // save the wave data
    SaveWaveFile();
    // Free the memory for the wave buffer.
    WaveHeader.lpData = 0;
    delete[] WaveData;
    WaveData = 0;
  }
}

This code is straightforward. First, the waveInClose() function closes the wave input device using the wave handle obtained when the device was opened. Next, the SaveWaveFile() function saves the wave data to disk (we'll discuss that next). Finally, the memory allocated for the wave buffer is freed. The MM_WIM_DATA message handler is simple, but it's a vital part of the wave recording operation.

Saving the wave data to disk

We covered saving the wave data to disk in Part 1 of this series when we discussed RIFF files. You need to create a RIFF file, write out the wave format header, and then write the actual wave data to the file. We won't explain each of these steps again here--refer to that article, and to the SaveWaveFile() function in Listing B. Also see "RIFF Update from Part 1" in this issue.

Putting it all together

Listings A and B contain the header file and source code for an example program that records a wave file. Figure A shows the program window.

Figure A: Our sample program lets you record wave files.
[ Figure A ]

The example lets you set the recording length and parameters (the wave format). To begin, click Start Recording. Recording will automatically stop after the specified number of seconds has elapsed (10 seconds by default). To stop recording before the specified time is up, click Stop Recording. After you finish recording, you can play the wave file by clicking Play.

To test this program, you'll need a microphone attached to your sound card. It would be a good idea to record a wave file using Windows Sound Recorder, so you know the sound card and microphone are functioning properly before attempting to run our program.

That's it! Using the tools we've provided in this article series, you're ready and able to control the recording and playback of wave audio data in your applications.

Listing A: RecWaveU.H

//---------------------------------------------
#ifndef RecWaveUH
#define RecWaveUH
//---------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <mmsystem.h>
#include <ExtCtrls.hpp>
//---------------------------------------------
class TMainForm : public TForm
{
__published:  // IDE-managed Components
  TButton *StartBtn;
  TButton *StopBtn;
  TButton *PlayBtn;
  TMemo *Memo1;
  TRadioGroup *ChannelsGroup;
  TRadioGroup *BitsGroup;
  TRadioGroup *SamplesGroup;
  TEdit *SecondsEdit;
  TLabel *Label1;
  void __fastcall PlayBtnClick(TObject *Sender);
  void __fastcall StartBtnClick(TObject*Sender);
  void __fastcall FormCreate(TObject *Sender);
  void __fastcall StopBtnClick(TObject *Sender);
  void __fastcall FormDestroy(TObject *Sender);
private:  // User declarations
private:  // User declarations
  char* WaveData;
  HWAVEIN WaveHandle;
  int DataSize;
  void CheckMMIOError(DWORD code);
  void OnWaveMessage(TMessage& msg);
  void CheckWaveError(DWORD code);
  void SaveWaveFile();
  WAVEHDR WaveHeader;
  WAVEFORMATEX WaveFormat;
public:    // User declarations
  __fastcall TMainForm(TComponent* Owner);
  BEGIN_MESSAGE_MAP
    MESSAGE_HANDLER(MM_WIM_DATA, TMessage, OnWaveMessage)
  END_MESSAGE_MAP(TForm)
};
//---------------------------------------------
extern PACKAGE TMainForm *MainForm;
//---------------------------------------------
#endif
Listing B: RecWaveU.CPP
//---------------------------------------------
#include <vcl.h>
#pragma hdrstop

#include "RecWaveU.h"
//---------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TMainForm *MainForm;
//---------------------------------------------
__fastcall TMainForm::TMainForm
  (TComponent* Owner) : TForm(Owner)
{
}
//---------------------------------------------
void __fastcall 
TMainForm::StartBtnClick(TObject *Sender)
{
  // Get recording parameters from radio group boxes on form.
  int samplesPerSec = (int)SamplesGroup->
    Items->Objects[SamplesGroup->ItemIndex];
  WORD channels = (WORD)(ChannelsGroup->ItemIndex + 1);
  WORD bitsPerSample = WORD(8 * (BitsGroup->ItemIndex + 1));
  DWORD avgBytesPerSec = channels * samplesPerSec;

  // Fill in the WAVEFORMATEX header.
  WaveFormat.wFormatTag      = WAVE_FORMAT_PCM;
  WaveFormat.nChannels       = channels;
  WaveFormat.nSamplesPerSec  = samplesPerSec;
  WaveFormat.nAvgBytesPerSec = avgBytesPerSec;
  WaveFormat.nBlockAlign     = 
    WORD((channels * bitsPerSample) / 8);
  WaveFormat.wBitsPerSample  = bitsPerSample;
  WaveFormat.cbSize          = 0;

  // Query device to see if it supports the selected format.
  int Res = waveInOpen(&WaveHandle, WAVE_MAPPER,
    &WaveFormat, 0, 0, WAVE_FORMAT_QUERY);
  CheckWaveError(Res);
  if (Res == WAVERR_BADFORMAT)
    return;

  // Open device. Wave-in messages go to window proc of form.
  Res = waveInOpen(&WaveHandle, WAVE_MAPPER, &WaveFormat,
    MAKELONG(Handle, 0), 0, CALLBACK_WINDOW);
  CheckWaveError(Res);

  // Allocate buffer for wave data large enough to hold data.
  int seconds = SecondsEdit->Text.ToIntDef(10);
  int bufferSize = seconds * avgBytesPerSec;
  if (WaveData)
    delete[] WaveData;
  WaveData = new char[bufferSize];

  // Set up WaveHeader structure.
  WaveHeader.dwBufferLength = bufferSize;
  WaveHeader.dwFlags        = 0;
  WaveHeader.lpData = WaveData;

  // Prepare the header.
  Res = waveInPrepareHeader(
    WaveHandle, &WaveHeader, sizeof(WAVEHDR));
  CheckWaveError(Res);
  Res = waveInAddBuffer(
    WaveHandle, &WaveHeader, sizeof(WAVEHDR));

  // Error. Free the memory and exit.
  if (Res != 0) {
    waveInUnprepareHeader(
      WaveHandle, &WaveHeader, sizeof(WAVEHDR));
    delete[] WaveData;
    WaveData = 0;
    return;
  }

  // Start recording.
  StopBtn->Enabled = true;
  Res = waveInStart(WaveHandle);
  CheckWaveError(Res);
}
//---------------------------------------------
void __fastcall 
TMainForm::StopBtnClick(TObject *Sender)
{
  // Call waveInReset() to stop recording. Forcec Windows to send 
  // the MM_WIM_DATA message to the application.
  waveInReset(WaveHandle);
}
//---------------------------------------------
void __fastcall 
TMainForm::PlayBtnClick(TObject *Sender)
{
  StartBtn->Enabled = false;
  StopBtn->Enabled = false;
  // Could have used low-level audio functions to play wave file 
  // but PlaySound is much easier.
  PlaySound("test.wav", 0, SND_FILENAME);
  StartBtn->Enabled = true;
  StopBtn->Enabled = true;
}
//---------------------------------------------
void __fastcall 
TMainForm::FormCreate(TObject *Sender)
{
  // Assign the sample rates that correspond to each radio button 
  // in the Samples Per Second radio group.
  SamplesGroup->Items->Objects[0] = (TObject*)8000;
  SamplesGroup->Items->Objects[1] = (TObject*)11025;
  SamplesGroup->Items->Objects[2] = (TObject*)22050;
  SamplesGroup->Items->Objects[3] = (TObject*)44100;
  WaveData = 0;
}
//---------------------------------------------
void __fastcall 
TMainForm::FormDestroy(TObject *Sender)
{
  // Just in case.
  if (WaveData)
    delete[] WaveData;
}
//---------------------------------------------
void TMainForm::OnWaveMessage(TMessage& msg)
{
  // Record buffer is full so free the memory allocated for the 
  // buffer. After that write out the file.
  if (msg.Msg == MM_WIM_DATA) {
    waveInClose(WaveHandle);
    SaveWaveFile();
    WaveHeader.lpData = 0;
    if (WaveData) {
      delete[] WaveData;
      WaveData = 0;
    }
    StartBtn->Enabled = true;
    StopBtn->Enabled = false;
    PlayBtn->Enabled = true;
  }
}
//---------------------------------------------
void TMainForm::CheckWaveError(DWORD code)
{
  if (code == 0) return;
  char buff[256];
  // Report a wave out error, if one occurred.
  waveInGetErrorText(code, buff, sizeof(buff));
  MessageBox(Handle, buff, "Wave Error", MB_OK);
}

void TMainForm::CheckMMIOError(DWORD code)
{
  // Report an mmio error, if one occurred.
  if (code == 0) return;
  char buff[256];
  wsprintf(buff,
    "MMIO Error. Error Code: %d", code);
  Application->MessageBox(buff, "MMIO Error", 0);
}

void TMainForm::SaveWaveFile()
{
  // Declare the structures we'll need.
  MMCKINFO ChunkInfo;
  MMCKINFO FormatChunkInfo;
  MMCKINFO DataChunkInfo;

  // Open the file.
  HMMIO handle = mmioOpen(
    "test.wav", 0, MMIO_CREATE | MMIO_WRITE);
  if (!handle) {
    MessageBox(0, "Error creating file.", "Error Message", 0);
    return;
  }

  // Create RIFF chunk. First zero out ChunkInfo structure.
  memset(&ChunkInfo, 0, sizeof(MMCKINFO));
  ChunkInfo.fccType = mmioStringToFOURCC("WAVE", 0);
  DWORD Res = mmioCreateChunk(
    handle, &ChunkInfo, MMIO_CREATERIFF);
  CheckMMIOError(Res);

  // Create the format chunk.
  FormatChunkInfo.ckid = mmioStringToFOURCC("fmt ", 0);
  FormatChunkInfo.cksize = sizeof(WAVEFORMATEX);
  Res = mmioCreateChunk(handle, &FormatChunkInfo, 0);
  CheckMMIOError(Res);
  // Write the wave format data.
  mmioWrite(handle, (char*)&WaveFormat, sizeof(WaveFormat));

  // Create the data chunk.
  Res = mmioAscend(handle, &FormatChunkInfo, 0);
  CheckMMIOError(Res);
  DataChunkInfo.ckid = mmioStringToFOURCC("data", 0);
  DataSize = WaveHeader.dwBytesRecorded;
  DataChunkInfo.cksize = DataSize;
  Res = mmioCreateChunk(handle, &DataChunkInfo, 0);
  CheckMMIOError(Res);
  // Write the data.
  mmioWrite(handle, (char*)WaveHeader.lpData, DataSize);
  // Ascend out of the data chunk.
  mmioAscend(handle, &DataChunkInfo, 0);

  // Ascend out of the RIFF chunk (the main chunk). Failure to do 
  // this will result in a file that is unreadable by Windows95
  // Sound Recorder.
  mmioAscend(handle, &ChunkInfo, 0);
  mmioClose(handle, 0);
}