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.
| Set the wave format. | |
| Open the wave input device. | |
| Allocate a buffer to hold the wave data. | |
| Prepare the wave header. | |
| Start recording. | |
| Close the device when recording finishes. |
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 * ChannelsThe nBlockAlign data member also requires explanation. You determine the value for the block alignment with this formula:
(Channels * BitsPerSample) / 8The 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.
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. |
RecordSeconds * AvgerageBytesPerSecondFor 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.
//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.
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.
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.
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.
| Close the wave input device. | |
| Save the wave data to disk. | |
| Free the memory allocated for the wave input buffer. |
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.
Figure A: Our sample program lets you record wave files.
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);
}