Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Программирование для начинающих и не только


Информационный Канал Subscribe.Ru

©Gigabyte 2004

Озвучиваем Delphi

Наверняка многие из пользователей ПК работали со звуком посредством того или иного приложения с довольно богатым набором методов его обработки, ровно как и некоторые программисты встраивали звуковые "заставки" в свои программки: Но это "встраивание" зачастую сводилось к простому или циклическому воспроизведению файлов WAV формата, и не давало никаких путей для полета программистской фантазии.

*

Но мы не потерпим никаких рамок, а поэтому откинем всевозможные предопределенные функции типа PlayFile или PlaySound подаренные нам разработчиками ОС и примемся за разработку собственного велосипеда.

С чего начать

Все детали необходимые нашего "велосипеда" содержаться в модуле mmsystem.pas. Здесь можно найти функции на любой вкус, от воспроизведения цифрового аудио звука, до MIDI и записи из микрофона. Но давайте обо всем по порядку. И дабы никого не пугать большим количеством функций выданных на-гора мы продемонстрируем только возможности системы по воспроизведению оцифрованного звука.

Итак для нашей демонстрации нам потребуется заготовка для воспроизведения, которую можно выцепить из любого WAV файла просто убрав оттуда всю системную информацию о частоте дискретизации, количестве каналов и разрядности звука. Не то чтобы она нам не понадобилась, просто мы ее будем использовать в другом месте. Я предлагаю вам воспользоваться заготовкой звука из файла "ding.wav" (22050Hz, Стерео, 16 бит), который можно найти здесь

Ну и для чистоты эксперимента я рекомендую предварительно закрыть все WinAmp-ы, Media Player-ы и другие примочки, которые воспроизводят звук.

За дело

К этому времени вы уже находитесь в Delphi с заготовкой нового приложения. Теперь вы можете разместить на форме кнопку, по нажатию которой и будет происходить задуманное. Собственно звук выводится при помощи функций расположенных в модуле mmsystem и потому для дальнейшей работы нам необходимо включить его в разделUses нашей программы. При воспроизведении звука мы воспользуемся такими функциями:


function waveOutOpen(lphWaveIn: PHWAVEIN; uDeviceID: UINT;
  lpFormatEx: PWaveFormatEx; dwCallback, dwInstance, dwFlags: DWORD): MMRESULT; stdcall;

Эта функция возвращает нам дескриптор устройства воспроизведения, с которым нам предстоит подальшая работа. Параметры этой функции имеют следующие предназначения:
LphWaveInАдрес куда будет возвращаться контекста нашего устройства воспроизведения.
UDeviceIDномер устройства воспроизведения с системе, в нашем случае отставим его равным 0. Что приведет к открытию устройства по умолчанию.
LpFormatExадрес структуры, которая содержит информацию о типе воспроизводимой информации (подробнее см. Ниже).
DwCallbackдескриптор значение которого определяется в зависимости от dwFlags. (см. Таблицу 1).
DwInstanceПользовательская копия данных, которая передается в Callback-механизм. Этот параметр здесь нам не понадобится и должен быть равен 0.
DwFlagsФлаги управления параметром dwCallback (см. Таблицу 1).

По результатам работы функции мы можем оценить правильность наших настройки. Так если результат функции равен константе MMSYSERR_NOERROR (определенной в том же mmsystem.pas), то все прошло удачно и мы можем продолжать работу дальше. Если же результат отличен от указанного выше значения, то произошла одна из ошибок описанных в Таблице 2. Теперь рассмотрим подробнее структуру TWaveFormatEx:


  PWaveFormatEx = ^TWaveFormatEx;
  {$EXTERNALSYM tWAVEFORMATEX}
  tWAVEFORMATEX = packed record
    wFormatTag: Word;         
    nChannels: Word;           
    nSamplesPerSec: DWORD;  
    nAvgBytesPerSec: DWORD; 
    nBlockAlign: Word;      
    wBitsPerSample: Word;   
    cbSize: Word;           
  end;
Здесь:
WFormatTagтип аудиоданных содержит один из флагов WAVE_FORMAT;
NChannelsколичество каналов (1- моно, 2- стерео:..)
NSamplesPerSecondчастота дискретизации. Если wFormatTag = WAVE_FORMAT_PCM, то тут могут содержаться следующие частоты: 8000,11025,22050,44100.
NAvgBytesPerSecсреднее скорость передачи данных. При PCM формате данных (e.g. wFormatTag = WAVE_FORMAT_PCM). Вычисляется как произведение nSamplesPerSec и nBlockAlign.
NBlockAlignминимальный размер блока данных. Должен быть равен произведению nChannelsна wBitsPerSampleразделенных на 8 (количество бит в байте).
WBitsPerSampleпри PCM формате данных дожен быть равен 8 или 16.
CbSizeРазмер данной структуры в байтах ( SizeOf(TWaveFormatEx) ).

Далее после успешного выполнения функции waveInOpen. Нам необходимо проинформировать систему о том, откуда будет воспроизводится звук т.е. задать адрес памяти по которому расположены аудиоданные требуемые для воспроизведения, а также подготовить из для передачи. Для этого мы используем функцию waveOutPrepareHeader:
function waveOutPrepareHeader(hWaveOut: HWAVEOUT; lpWaveOutHdr: PWaveHdr; uSize: UINT): MMRESULT; stdcall;
Здесь в качестве параметров передаются такие данные:
HWaveOut - контекст аудиоустройства (параметра возвращенный при waveOutOpen).
LpWaveOutHdr - указатель на структуру, которая содержит все необходимую дополнительную информацию.
USize - размер структуры lpWaveOutHdr.
Собственно все жизненно важная информация содержится в структуре TWaveHdr:


  PWaveHdr = ^TWaveHdr;
  {$EXTERNALSYM wavehdr_tag}
  wavehdr_tag = record
    lpData: PChar;              
    dwBufferLength: DWORD;      
    dwBytesRecorded: DWORD;     
    dwUser: DWORD;              
    dwFlags: DWORD;             
    dwLoops: DWORD;             
    lpNext: PWaveHdr;           
    reserved: DWORD;            
  end;
  TWaveHdr = wavehdr_tag;
  {$EXTERNALSYM WAVEHDR}
  WAVEHDR = wavehdr_tag;
Здесь:
LpDataуказатель на буффер, где хранятся подготовленные для воспроизведения данные.
DwBufferLengthразмер буфера в байтах
DwBytesRecordedиспользуется при записи данных возвращает объем записанной информации.
DwUserне используется системой.
DwFlagsфлаги воспроизведения. В нашем случае - 0.
DwLoopsколичество повторов данного блока. В нашем случае - 0.
LpNext, reservedзарезервированы для системного пользования.

Здесь интерес для нас представляют только поля lpDataи dwBufferSize, в которые мы и занесем информацию о буфере звуковых данных. Теперь посредством вызова waveOutPrepareBuffer мы подготавливаем к воспроизведению наши данные. А собственно воспроизведение осуществляется при помощи функции waveOutWrite:


 function waveOutWrite(hWaveOut: HWAVEOUT; lpWaveOutHdr: PWaveHdr;
  uSize: UINT): MMRESULT; stdcall;
параметры которой означают то же самое, что и в функции waveOutPrepareBuffer.

После воспроизведения буфера мы как приличные программисты должны закрыть всё открытое, что и делаем посредством функций waveOutUnPrepareHeader и waveOutClose в соответствующем порядке. Далее нам остается только очистить блоки памяти занятые собственно аудиоданными.

Теперь после такого небольшого экскурса в описания функций мы напишем реакцию нашей кнопки на нажатие:


procedure TForm1.Button1Click(Sender: TObject);
var Evt:THandle;
    Fmt:tWAVEFORMATEX;
    Hdr:TWaveHdr;
    WO:HWAVEOUT;
    FS:TFileStream;
    Data:Pointer;
    Size:Integer;
begin
 FS:=TFileStream.Create('Sound',fmOpenRead);
 Size:=FS.Size;
 GetMem(Data,Size);
 try
  FS.ReadBuffer(Data^,Size);
  FS.Free;
  Evt:=CreateEvent(nil,False,False,nil);
  ZeroMemory(@Fmt,SizeOf(Fmt));
  ZeroMemory(@Hdr,SizeOf(Hdr));
  Fmt.wFormatTag:=WAVE_FORMAT_PCM;
  Fmt.nChannels:=2;
  Fmt.nSamplesPerSec:=22050;
  Fmt.wBitsPerSample:=16;
  Fmt.cbSize:=SizeOf(Fmt);
  Fmt.nBlockAlign:=4;
  Fmt.nAvgBytesPerSec:=88200;
  if waveOutOpen(@WO,0,@Fmt,Evt,0,CALLBACK_EVENT)<>MMSYSERR_NOERROR then Halt(1);
  Hdr.lpData:=Data;
  Hdr.dwBufferLength:=Size;
  waveOutPrepareHeader(WO,@Hdr,SizeOf(Hdr));
  ResetEvent(Evt);
  waveOutWrite(WO,@Hdr,SizeOf(Hdr));
  WaitForSingleObject(Evt,Infinite);
  waveOutUnprepareHeader(WO,@Hdr,SizeOf(Hdr));
  waveOutClose(WO);
  CloseHandle(Evt);
 finally
  FreeMem(Data,Size);
 end;
end;

Здесь вам уже почти все знакомо кроме нескольких строк. Так сначала открывает наш подготовленный файл и считываем оттуда аудиоданные в специально выделенную область оперативной памяти, Далее мы создаем объект - событие (строка CreateEvent), который будет ответственным за реализацию задержки достаточной для воспроизведения звука. Далее мы обнуляем все структуры и заполняем их поля соответствующими значениями. После этого мы открываем наше устройство воспроизведения, делаем подготовку аудиоданных и их вывод на динамики. После этого мы через WaitForSingleObject реализуем задержку до окончания проигрывания (если этого не сделать то звука вообще не будет), и под конец закрываем все открытые контексты и очищаем аудио буферы.

Что еще можно сделать

Конечно сегодня никого уже нельзя удивить возможностью встраивания в приложения поддержки воспроизведения звука. Ведь для простого проигрывания файлов у нас есть стандартные функции расположенные все в том же mmsystem.pas. Но если присмотреться поближе, то эти функции предоставляют нам очень ограниченные возможности по воспроизведению звука (никакой суперсовременный формат вы им не воспроизведёте), в то же время приведенный выше код дает возможность работать с "сырым" звуком, без каких либо надстроек. Так к примеру если заменить в вышеприведенном коде ту часть, которая считывает информацию из подготовленного файла в память, на код, который будет заполнять эту область шумами или же другими видами звуковых волн:


Size:=88200;
GetMem(Data,Size);
try
Ch:=Data;
For i:=0 to 22050 do
 begin
  Ch^:=Char(Round(255*Sin(10/pi*i)*Cos(10/pi*i)));
  Inc(Ch);
 end;
то тем самым мы сможем почти мгновенно получить все разнообразие звуков которое только возможно описать математическими формулами. Этот код также может стать наглядным (лучше сказать наушным) представлением какого-то курсового проекта по теории акустики или чего-то другого и у вас не будет необходимости залезать в дебри DirectInput для того чтобы похвастаться результатами своих исследований.

Запись данных

Но кроме возможности воспроизведения всевозможных звуковых волн. Модуль mmsystem.pas содержит, как уже было сказано вначале статьи, и другие полезные вкусности. В частности здесь объявлены функции которые используются при записи звука с разных источников (микрофон, диск, линейный вход:.). И сейчас мы вкратце разберемся с техникой записи звука из микрофонного входа.

Для этого мы сначала как всегда делаем подготовительные работы т.е. выделяем необходимую область памяти, создаем событие и заполняем все структуры точно также, как и в примере с воспроизведением звука. Основное отличие здесь составляет только то, что вместо вызова функции waveOutOpen мы вызываем waveInOpen, где в качестве параметров передаются те же значения:


Fmt.wFormatTag:=WAVE_FORMAT_PCM;
Fmt.nChannels:=1;
Fmt.nSamplesPerSec:=11025;
Fmt.wBitsPerSample:=8;
Fmt.nAvgBytesPerSec:=11025;
Fmt.cbSize:=sizeOf(Fmt);
Fmt.nBlockAlign:=4;
WaveInOpen(@WI,WAVE_MAPPER,@Fmt,Evt,0,CALLBACK_EVENT);

Константа WAVE_MAPPER - определена в mmsystem.pas и ее следует использовать если вы не знаете, с какого именно устройства нужно сделать запись. При указании этого влага система по выставленным в TWaveFormatEx параметрам определит какое устройство наиболее подходит для записи данных в этом формате и откроет его.

Далее как и в примере с воспроизведением звука, нам необходимо соответствующим образом заполнить структуру TWaveHdr, где заполнению в первую очередь подлежит поле lpData. Здесь должен быть указан адрес буфера в который будут помещены аудио данные записанные через микрофон. После этого мы как всегда должны подготовить буфер для записи, а дальше добавить его во внутрисистемный список для записи посредством функции waveInAddBuffer. Для непосредственной записи мы вызываем функцию waveInStart, которая сообщает системе о том что уже пора начать записывать звук. Теперь мы делаем задержку для того чтоб заполнился весь буфер и после этого используя функции waveInStop и waveInReset мы останавливаем запись. Исходный код для всего выше сказанного приведен ниже:


Hdr.lpData:=@Data;
Hdr.dwBufferLength:=SizeOf(Data);
if waveInPrepareHeader(WI,@Hdr,SizeOf(HDR))<>MMSYSERR_NOERROR then
 raise EAbort.Create('Cannot prepare buffer');
waveInAddBuffer(WI,@Hdr,SizeOf(Hdr));
ResetEvent(Event);
if waveInStart(WI)<>MMSYSERR_NOERROR then
 raise EAbort.Create('Cannot start recording');
WaitForSingleObject(Event,Infinite);
waveInReset(WI);
if waveInReset(WI)<>MMSYSERR_NOERROR then
 raise EAbort.Create('Cannot reset recording');
waveInStop(WI);
if waveInStop(WI)<>MMSYSERR_NOERROR then
 raise EAbort.Create('Cannot stop recording');
waveInUnprepareHeader(WI,@HDR,SizeOf(Hdr));

После выполнения этого кода в массиве Data будет содержаться звук, который мы произнесем в микрофон.

И в завершении

В завершении хотелось бы отметить, что данный код для воспроизведения звука уже морально устарел, поскольку уже повсеместно используется технология DirectX, с ее огромными возможностями по выводу звука. Но все же mmsystem.pas все таки приберег несколько "козырных тузов" в рукаве, так я перерыл все файлы посвященные звуку в DirectX, но так и не сумел найти средства по записи звука, или воспроизведении MIDI данных. Может быть я действительно не очень глубоко копался, но так ли иначе сейчас для записи звука из микрофонного входа я пользуюсь функциями предоставляемыми именно этим модулем.

Таблица.1 Соотношения параметров переменных dwCallback и dwFlags
DwCallbackdwFlags
CALLBACK_EVENTВ dwCallback содержится идентификатор события возбуждаемого системой в процессе работы.(Используется нами в примере)
CALLBACK_FUNCTIONВ dwCallback содржится адрес процедуры, которой передается управлении в процессе работы программы
CALLBACK_NULLНет механизма возврата обработанной информации (используется по умолчанию)
CALLBACK_THREADВ dwCallback содержится идентификатор потока, которому передается управление в процессе работы.
CALLBACK_WINDOWdwCallback содержит идентификатор окна, которому передается управление.
Таблица 2. Типические ошибки возникающие при работе со звуком.
ИдентификаторЗначениеОписание
MMSYSERR_NOERROR0Нет ошибки, можно продолжать работу
MMSYSERR_ERROR1Неопознанная ошибка
MMSYSERR_BADDEVICEID2Неправильный идентификатор указывающий на ID устройства
MMSYSERR_NOTENABLED3Невозможно активизировать драйвер
MMSYSERR_ALLOCATED4Устройство уже выделено
MMSYSERR_INVALHANDLE5Неправильный идентификатор устройства
MMSYSERR_NODRIVER6Отсутствует соответствующий драйвер устройства
MMSYSERR_NOMEM7Ошибка при выделении памяти
MMSYSERR_NOTSUPPORTED8Данная функция не поддерживается
MMSYSERR_BADERRNUM9Неправильный код ошибки.
MMSYSERR_INVALFLAG10Указан неправильный флаг
MMSYSERR_INVALPARAM11Функции передан неправильный параметр
MMSYSERR_HANDLEBUSY12Идентификатор используется также и другим обработчиком (например потоком)
MMSYSERR_INVALIDALIAS13Указанный синоним не найден
MMSYSERR_BADDB14Ошибка в реестре
MMSYSERR_KEYNOTFOUND15Ключ реестра не найден
MMSYSERR_READERROR16Невозможно прочитать значение из реестра
MMSYSERR_WRITEERROR17Невозможно записать значение в реестр
MMSYSERR_DELETEERROR18Невозможно удалить значение и реестра
MMSYSERR_VALNOTFOUND19Заданное значение не найдено в реестре
MMSYSERR_NODRIVERCB20Драйвер не поддерживает операцию вызова DriverCallback

Листинг


unit Main;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs,mmsystem, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var Evt:THandle;
    Fmt:tWAVEFORMATEX;
    Hdr:TWaveHdr;
    WO:HWAVEOUT;
    FS:TFileStream;
    Data:Pointer;
    Size:Integer;
    Ch:PChar;i:integer;hz:Byte;
begin
Size:=88200;
GetMem(Data,Size);
hz:=10;
try
Ch:=Data;
For i:=0 to 22050 do
 begin
  Ch^:=Char(Round(255*Sin(hz/pi*i)*Cos(hz/pi*i)));
  Inc(Ch);
 end;
Evt:=CreateEvent(nil,False,False,nil);
ZeroMemory(@Fmt,SizeOf(Fmt));
ZeroMemory(@Hdr,SizeOf(Hdr));
Fmt.wFormatTag:=WAVE_FORMAT_PCM;
Fmt.nChannels:=1;
Fmt.nSamplesPerSec:=22050;
Fmt.wBitsPerSample:=8;
Fmt.cbSize:=SizeOf(Fmt);
Fmt.nBlockAlign:=4;
Fmt.nAvgBytesPerSec:=22050;
if waveOutOpen(@WO,0,@Fmt,Evt,0,CALLBACK_EVENT)<>MMSYSERR_NOERROR then Halt(1);
Hdr.lpData:=Data;
Hdr.dwBufferLength:=Size;
waveOutPrepareHeader(WO,@Hdr,SizeOf(Hdr));
ResetEvent(Evt);
waveOutWrite(WO,@Hdr,SizeOf(Hdr));
WaitForSingleObject(Evt,Infinite);
waveOutUnprepareHeader(WO,@Hdr,SizeOf(Hdr));
waveOutClose(WO);
finally
FreeMem(Data,Size);
end;
end;

end.

http://subscribe.ru/
http://subscribe.ru/feedback/
Подписан адрес:
Код этой рассылки: comp.soft.prog.programmershelp
Отписаться

В избранное