Программирование звука. Пример воспроизведения WAV файла:
Мы уже знаем основные функции воспроизведения звуковых данных, знаем формат WAV файла, пора бы написать реальный пример. Сегодня мы выведем звуковые данные в колонки. Пример будет чисто демонстрационным и работает он только в Win9x. В NT наша прога непокатит.
На рисунке 1 показана форма сегодняшней проги. Я понимаю, что ты не станешь писать всё что я говорю своими руками, но я всё же объясню внутренности примера. Это тебе пригодится.
Рис 1. Форма
Самое главное - подключить модуль mmsystem в раздел uses. В этом модуле объявлены все мультимедиа функции.
А теперь переходим к примеру. Он достаточно большой и его изучение отнимет очень много времени. Если ты сможешь с ним разобратся, то поймёшь достаточно много тонкостей в программировании. Я здесь использовал очень много приёмов, которые мы ещё не обсуждали, но в следующем номере я постараюсь восполнить эту дыру. А начнём мы как всегда с загрузки, а именно с обработчика OnShow:
procedure TSounderForm.FormShow(Sender: TObject);
begin
EnableBut(false);
FillDevice;
RestartPlay:=false;
end;
EnableBut - моя функция, которая делает недоступными кнопки воспроизведения, потому что ещё не загружен звуковой файл. Как только пользователь выберет WAV файл, мы снова сделаем эти кнопки доступными.
FillDevice - моя функция, которая определяет возможности звуковухи. Давай рассмотрим её подробнее:
procedure TSounderForm.FillDevice;
var
pwoc:TWaveOutCaps;// Объявляю переменную типа TWaveOutCaps
begin
//Запросить свойства звуковухи
waveOutGetDevCaps(WAVE_MAPPER, @pwoc, sizeof(pwoc));
// Если звуковая поддерживает изменение звука, то ...
if (pwoc.dwSupport and WAVECAPS_VOLUME)>0 then
begin
// Делаю доступными регуляторы громкости.
VolumeLBar.Enabled:=true;
VolumeRBar.Enabled:=true;
end;
end;
Здесь я использовал пока ещё неизвестную нам функцию waveOutGetDevCaps. Но это только пока. Сейчас мы разберёмся с этим монстром. Она выглядит так:
Для С/С++
MMRESULT waveOutGetDevCaps(
UINT uDeviceID, //Указатель на устройство воспроизведения
LPWAVEOUTCAPS pwoc,//Структура, куда будут записаны данные
UINT cbwoc //Размер структуры
);
Для Delphi
function waveOutGetDevCaps(
uDeviceID: UINT; //Указатель на устройство воспроизведения
lpCaps: PWaveOutCaps; //Структура, куда будут записаны данные
uSize: UINT //Размер структуры
): MMRESULT; stdcall;
Второй параметр - структура WAVEOUTCAPS. Она выглядит так:
Для С/С++
typedef struct {
WORD wMid; //Идентификатор производителя
WORD wPid; //Идентификатор устройства
MMVERSION vDriverVersion; //Версия драйвера
CHAR szPname[MAXPNAMELEN];//Название устройства
DWORD dwFormats; //Поддерживаемые форматы
WORD wChannels; //Количество каналов
WORD wReserved1; //Зарезервировано
DWORD dwSupport; //Свойства
} WAVEOUTCAPS;
Для Delphi
PWaveOutCapsA = ^TWaveOutCapsA;
PWaveOutCapsW = ^TWaveOutCapsW;
PWaveOutCaps = PWaveOutCapsA;
{$EXTERNALSYM tagWAVEOUTCAPSA}
tagWAVEOUTCAPSA = record
wMid: Word; //Идентификатор производителя
wPid: Word; //Идентификатор устройства
vDriverVersion: MMVERSION; //Версия драйвера
szPname: array[0..MAXPNAMELEN-1] of AnsiChar; //Название устройства
dwFormats: DWORD; //Поддерживаемые форматы
wChannels: Word; //Количество каналов
dwSupport: DWORD; //Свойства
end;
Самым интересным для нас является последний параметр - dwSupport . Он может принимать значения:
WAVECAPS_LRVOLUME - поддерживается изменение правого и левого каналов.
WAVECAPS_SYNC - драйвер сихронный и будет блокировать работу пока играет музыка.
WAVECAPS_VOLUME - поддержка громкости.
WAVECAPS_SAMPLEACCURATE - возвращает позицию.
Параметр wChannels может быть 1 (для моно) и 2 (для стерео).
Параметр dwFormats может быть комбинацией следующих значений:
WAVE_FORMAT_1M08 - 11.025 kHz, mono, 8-bit
WAVE_FORMAT_1M16 - 11.025 kHz, mono, 16-bit
WAVE_FORMAT_1S08 - 11.025 kHz, stereo, 8-bit
WAVE_FORMAT_1S16 - 11.025 kHz, stereo, 16-bit
WAVE_FORMAT_2M08 - 22.05 kHz, mono, 8-bit
WAVE_FORMAT_2M16 - 22.05 kHz, mono, 16-bit
WAVE_FORMAT_2S08 - 22.05 kHz, stereo, 8-bit
WAVE_FORMAT_2S16 - 22.05 kHz, stereo, 16-bit
WAVE_FORMAT_4M08 - 44.1 kHz, mono, 8-bit
WAVE_FORMAT_4M16 - 44.1 kHz, mono, 16-bit
WAVE_FORMAT_4S08 - 44.1 kHz, stereo, 8-bit
WAVE_FORMAT_4S16 - 44.1 kHz, stereo, 16-bit
Теперь перейдём к загрузке wav файла. По нажатию кнопки "Открыть" происходит следующее:
procedure TSounderForm.OpenButtonClick(Sender: TObject);
begin
//Сначала, на всякий случай остановим воспроизведение
StopButtonClick(nil);
DataSize:=0; // Обнуляю размер данных
if OpenDialog1.Execute then //Показать окно открытия файла
OpenWaveFile; //Если файл выбран, то открыть его
WavFileName:=OpenDialog1.FileName; //Сохраняем имя файла
//Изменяем заголовок окна.
Caption:='Sounder - '+ExtractFileName(WavFileName);
//Показываю параметры звуковых данных.
if wfx.nChannels=1 then
ChanelsBox.Text:='Моно'
else
ChanelsBox.Text:='Стерео';
FreqBox.Text:=IntToStr(wfx.nSamplesPerSec);
BitsBox.Text:=IntToStr(wfx.wBitsPerSample);
Label7.Caption:=IntToStr(wfx.nAvgBytesPerSec);
end;
Здесь можно разобратся со всем происходящим по коментариям. В принципе, ничего особенного не происходит. Самое интересное будет в процедуре OpenWaveFile. Вот её мы сейчас и будем разглядывать. Если ты прочитал предыдущую статью о программировании звука и статью о формате файла WAV, то и она покажется тебе достаточно лёгкой.
procedure TSounderForm.OpenWaveFile;
var
hFile:THandle;
Str:array [0..255] of char;
TempStr:String;
BytesReaded,Size:DWORD;
begin
//Если мы уже открывали WAV файл, то закрыть предыдущий
if w_hData<>0 then
CloseHandle(w_hData);
//Открыть файл для чтения. Это достаточно продвинутая функция и мы
//познакомимся с ней отдельно.
hFile:=CreateFile(PChar(OpenDialog1.FileName),GENERIC_READ,
FILE_SHARE_READ,nil,OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE,0);
//Если файл открыт нормально, то продолжаем.
if hFile=INVALID_HANDLE_VALUE then exit;
//С TRY мы познакомимся в следующий раз
//Пока скажу, что он работает, как Begin, а вместо end
//нужно ставить finally + операторы + end;
//или except + операторы + end;
try
//Получаем размер файлы с помощью GetFileSize
wFileSize:=GetFileSize(hFile,nil);
try
//Устанавливаем позицию в файле на 8 байт от начала
SetFilePointer(hFile,8,nil,FILE_BEGIN);
//Прочитаем начиная от этой позиции 8 байт
ReadFile(hFile,Str,8,BytesReaded,nil);
Str[7]:=#0;//Обнуляю седьмой байт
TempStr:=Str;//Перегоняю результат в строку
//Если прочитанная строка не равна 'WAVEfmt', то это не WAV
if TempStr<>'WAVEfmt' then
begin
//Вывожу сообщение об ошибке
Application.MessageBox('Ошибка заголовка','Error',
MB_OK+MB_ICONINFORMATION);
CloseHandle(hFile);//Закрываю файл
exit; //Выход из процедуры
end;
//Читаем размер следующего блока
ReadFile(hFile,Size,Sizeof(Size),BytesReaded,nil);
//Обнуляем структуру wfx
ZeroMemory(@wfx,sizeof(wfx));
//Если размер следующего блока равен размеру структуры,
//То читаем структуру. Иначе выдаём ошибку
if Size=sizeof(PCMWAVEFORMAT) then
begin
ReadFile(hFile,wfx,Size,BytesReaded,nil);
end
else
begin
MessageBox(Handle, 'Формат не поддерживается', 'Error',
MB_OK+MB_ICONEXCLAMATION);
exit;
end;
wfx.cbSize:=0;
ReadFile(hFile,Str,4,BytesReaded,nil);
//Читаем размер следующего блока
ReadFile(hFile,DataSize,4,BytesReaded,nil);
//Далее идут звуковые данные, поэтому сохраняем текущую позицию
//в DataOffset, чтобы можно было сразу перейти к данным.
DataOffset:=SetFilePointer(hFile,0,nil,FILE_CURRENT);
//Создаём образ файла в памяти с помощью CreateFilemapping
w_hData:=CreateFilemapping(hFile,nil,PAGE_READONLY,0,
Integer(wFileSize),nil);
//Закрываем файл
CloseHandle(hFile);
hFile:=INVALID_HANDLE_VALUE;
except
Application.MessageBox('Формат не поддерживается','Error',
MB_OK+MB_ICONINFORMATION);
CloseHandle(hFile);
hFile:=INVALID_HANDLE_VALUE;
end;
finally
if hFile<>INVALID_HANDLE_VALUE then CloseHandle(hFile);
if DataSize>0 then
begin
EnableBut(true);
InitSoundControls;
end
else
EnableBut(false);
end;
end;
WAV файл загружен. Теперь переходим к воспроизведению. По нажатию кнопки "Играть" происходит следующее:
procedure TSounderForm.PlayButtonClick(Sender: TObject);
begin
//Если была нажата пауза, то продолжить воспроизведение.
if ((w_Pause)and (w_hPlay<>0)) then
begin
waveOutRestart(w_hPlay);
w_Pause:=not w_Pause;
exit;
end;
//Если данные воспроизводятся, то остановить вывод
if w_hPlay<>0 then StopButtonClick(nil);
//Включить таймер
SoundTimer.Enabled:=true;
w_Pause:=false; w_Restart:=false; w_Stop:=false;
w_PlayPosition:=PositionBar.Position*1000;
w_RestartPosition:=PositionBar.Position*1000;
//Устанавливаю устройство поумолчанию.
w_DeviceID:=Integer(WAVE_MAPPER);
//Запускаю поток воспроизведения данных.
CyDSounder1:=TSoundPlayer.Create(true);
//Устанавливаю высокий приоритет
CyDSounder1.Priority:=tpHigher;
CyDSounder1.Resume;
end;
При запуске потока я вызываю только одну процедуру, которая будет отвечать за вывод звуковых данных:
procedure TSoundPlayer.Execute;
begin
Process;
end;
Давай мельком посмотрим на эту процедуру:
procedure TSoundPlayer.Process;
const
DEFAULT_BLOCKSIZE=64*1024; //Размер 1-го блока
var
BlockSize,ii,shift,size:Integer;
si:TSystemInfo;
curHdr:WAVEHDR;
ofs:Int64;
p:PSoundArray;
begin
BlockSize:=DEFAULT_BLOCKSIZE;
GetSystemInfo(si); //Получаю системную информацию
ZeroMemory(@Header,sizeof(Header)); //Обнуляю заголовок
hEvent:=CreateEvent(nil,false,false,nil);
try
//Открываю звуковуху
WaveOutCheck(waveOutOpen(@SounderForm.w_hPlay, SounderForm.w_DeviceID,
@SounderForm.wfx, hEvent, 0,CALLBACK_EVENT));
//Выделяю память для данных
Header[0].lpData:=VirtualAlloc(nil,round((BlockSize*2+
Integer(si.dwPageSize)-1)/si.dwPageSize*si.dwPageSize),
MEM_RESERVE+MEM_COMMIT,PAGE_READWRITE);
if Header[0].lpData=nil then exit;
//Выделенную память разделяю на два заголовка
Header[1].lpData:=Header[0].lpData+BlockSize;
Header[0].dwBufferLength:=BlockSize;
Header[1].dwBufferLength:=BlockSize;
//Подготавливаю заголовки
waveOutCheck(waveOutPrepareHeader(SounderForm.w_hPlay,
@Header[0],sizeof(waveHDR)));
waveOutCheck(waveOutPrepareHeader(SounderForm.w_hPlay,
@Header[1],sizeof(waveHDR)));
ii:=0;
if SounderForm.DataSize<=BlockSize then ResetEvent(hEvent);
//Запускаю цикл воспроизведения
while SounderForm.w_PlayPositionSounderForm.DataSize-
SounderForm.w_PlayPosition then
BlockSize:=SounderForm.DataSize-SounderForm.w_PlayPosition;
//Расчитываю размер данных
curhdr.dwBufferLength:=BlockSize;
ofs:=SounderForm.w_PlayPosition+SounderForm.DataOffset;
shift:=(ofs mod si.dwAllocationGranularity);
ofs:=ofs-shift;
size:=(BlockSize+ofs+si.dwPageSize-1);
size:=size-(size mod Integer(si.dwPageSize));
if size>SounderForm.wFileSize-ofs then size:=SounderForm.wFileSize-ofs;
//Загружаю из памяти файла звуковые данные
p:=MapViewOfFile(SounderForm.w_hData,FILE_MAP_READ,
ofs shr 32 and $FFFFFFFF,ofs and $FFFFFFFF, Size);
try
//Копирую в заголовок
CopyMemory(curhdr.lpData, @p[shift], min(size-shift,BlockSize));
finally
UnMapViewOfFile(p);
end;
//Воспроизвожу.
waveOutCheck(waveOutWrite(SounderForm.w_hplay,@curhdr,sizeof(waveHDR)));
//Ожидаю окончания воспроизведения
if (WAIT_TIMEOUT=WaitForSingleObject(hEvent,10000)) then
begin
if not SounderForm.w_Pause then
begin
MessageBox(SounderForm.Handle,'Ошибка воспроизведения','Ошибка',
MB_OK+MB_ICONINFORMATION);
break;
end
else
WaitForSingleObject(hEvent,INFINITE);
end;
//Если нажат стоп, то выход
if SounderForm.w_Stop then ClosePlay;
//Если отпущена пауза, то востановить воспроизведение
if SounderForm.w_Restart then
begin
waveOutReset(SounderForm.w_hPlay);
SounderForm.w_Restart:=false;
SounderForm.w_PlayPosition:=SounderForm.w_RestartPosition;
BlockSize:=DEFAULT_BLOCKSIZE;
if SounderForm.DataSize>BlockSize then SetEvent(hEvent);
continue;
end;
//Новая позиция для чтения следующих данных
SounderForm.w_PlayPosition:=SounderForm.w_PlayPosition+BlockSize;
end;
finally
//Ожидаю окончания воспроизведения
WaitForSingleObject(hEvent,20000);
//Закрываю воспроизведение
ClosePlay;
end;
end;
На сегодня хватит. Я просто уверен, что ты не до конца понял, что здесь происходит. Пример получился слишком насыщеный новыми вещами. В следующем месяце я постараюсь объяснить как можно больше из того, что здесь было нового. А пока что наслождайся музыкой.