Александр Тарасенко, Санкт-Петербург
Редакция: 23.04.04
e-mail: taralex@yandex.ru
URL: http://junglewin.narod.ru/
Статья посвящана методам работы со звуковыми данными в среде Windows. Рассматриваются вопросы вывода и захвата звука различными методами: с помощью мультимедиа библиотеки, средствами DirectSound и DirectShow. Данный материал предназначен для начинающих. Тем не менее, предполагается, что читатель знаком с техникой программирования в среде Windows. В частности ему известно, что такое сообщения (message), события (event), COM (component object model). С другой стороны, даже если вы не знакомы с этими концепциями - будет повод познакомится. Хочу заметить, что данная статья не претендует на справочное руководство, поэтому для получения полной объективной информации по рассматриваемым вопросам читателям настоятельно рекомендуется обратиться к соответствующим разделам MSDN и DirectX SDK. Дополнительные сведения о модели драйверов, обслуживающих ввод/вывод звука можно найти в DDK.
1. Введение
2. Работа со звуком средствами
стандартной мультимедиа библиотеки
3. Работа с wave
файлами
4. Работа
со звуком средствами DirectSound
4.1 Как воспроизвести
аудиопоток
4.2 Как воспроизвести несколько
аудиопотоков одновременно
4.3 Как захватиь
звук
5. Использование ACM компреccоров
6. Использование
DirectShow
7. Взаимодействие драйверов и компонентов при выводе звука
8.
Исправления
Все современные настольные компьютеры как правило оснащены звуковыми картами и многие разработчики используют звук в своих приложениях. Основными направлениями использования возможностей звуковых карт в программном обеспечении являются:
Любая звуковая плата состоит как правило из следующих частей: многоканальный АЦП - для оцифровывания входного звукового сигнала, многоканальный ЦАП - для восстановления сигнала по его цифровому представлению, синтезатор - позволяет генерировать многотональные сигналы, аппаратные буфера, интерфейс шины обмена данными (PCI, ISA, USB). Кроме того, на плате может быть установлен специализированный процессор производящий звуковые эффекты (например, реверберацию), осуществляющий компрессию (например, декодер MP3). В этом случае говорят, что плата имеет аппаратный декодер. Если плата не оснащена декодерами и производителями эффектов, эти функции берет на себя центральный процессор. В таком случае говорят, что в системе установлен программный декодер. Естественно, при этом снижается общая производительность системы.
Оцифровка и последующее восстановление звука осуществляется в соответствии с теоремой Котельникова (известной также как теорема отсчетов). Согласно этой теореме, сигнал (бесконечный) может быть разложен в ряд (бесконечный) по функциям вида sin(wt)/(wt). Коэффециенты этого разложения называются отсчетами (или сэмплами - на английский манер). w - циклическая частота называемая частотой дискретизации. Так вот теорема Котельникова гласит, что частота дискретизации должна быть не менее, чем в два раза больше граничной частоты спектра сигнала. При этом не происходит потери информации! То есть сигнал может быть АБСОЛЮТНО ТОЧНО восстановлен по своим отсчетам. Проясним это дело на примере со звуком. Известно, что человеческое ухо воспринимает звуковые колебания с частотой не более 20кГц. Из этого следует, что если частота дискретизации будет не менее 40кГц, то мы не заметим операции "дискретизация -восстановление". Вспомним тут частоту дискретизации в формате AudioCD - 44кГц. Теперь становится понятнее, откуда взялась цифра. Другой пример, известно, что человеческий голос по мощности в основном сосредоточен в полосе 300-3500Гц. Соответственно, для качественной передачи голоса необходимо иметь частоту дискретизации не менее 7кГц. Если вы уже имели дело с цифровой обработкой речи, вы знаете, что обычно речь записывается с частотой дискретизации 8кГц.
Одной процедуры дискретизации мало. Для компьютерной обработки звука необходимо получить его цифровое представление. Для этого сигнал квантуют. В отличии от дискретизации при квантовании происходит потеря информации. Эту потерю информации называют шумом квантования. Естественно, что чем больше уровней квантования, тем меньше шум квантования. Считается, что при 16 битном цифрро-аналоговом преобразовании человеческое ухо не слышит шума кванотования.
Совместную дискретизацию и квантование сигнала называют аналого-цифровым преобразованием. Иногда эту опреацию называют также импульсно-кодовой модуляцией (ИКМ или PCM - в английском варианте). Существует несколько вариантов ИКМ, определяемых характеристикой квантователя. На практике используется линейная и логарифмическая ИКМ. Wav файлы как правило представляют собой как раз линейную ИКМ исходного сигнала.
Вооружившись этими фактами, мы теперь молжем вполне ответственно подойти к выбору частоты дискретизации и количеству уровней квантования сигнала. Заметим, что качественное представление звука требует гораздо больших ресурсов (места на диске, размера буферов в памяти, времени на передачу по сети) и не всегда оправдано. Во многих случаях используется достаточно низкая частота дискретизации. Например, речь, оцифрованная при частоте дискретизации 4кГц, звучит вполне разборчиво.
Начнем с записи. Для этого на понадобяться следующие функции:
waveInOpen - открыть аудиоустройство для записи
waveInClose
- закрыть аудиоустройство
waveInPrepareHeader - подготовить буфер для
записи
waveInAddBuffer - отдать буфер
аудиоустройству
waveInUnprepareHeader - освободить записанный
буфер
waveInStart - начать запись
waveInStop - остановить
запись
waveInGetPosition - возвращает информацию о длительности
записанного фрагмента
waveInReset - остановить запись и установить
длительность записанного фрагмента равной нулю
Кроме того понадобятся следующие структуры:
WAVEHDR - заголовок буфера
WAVEFORMATEX - формат записи
Прежде, чем читать материал далее, читателю настойчиво рекомендуется обратиться к соответствующим разделам SDK, где описаны упомянутые типы.
Рассмотрим и прокомментируем кратко порядок действий при записи данных с аудиоустройства. Все операции с аудиоустройством начинаются с попытки его открыть. Процедура "открывания" аудиоустройства выполняется при помощи вызова waveInOpen. Для вызова необходимо указать номер устройства, которое, как предполагается, будет выполнять запись. Узнать количество установленных в системе устройств и выяснить их возможности можно при помощи функций waveInGetNumDevs и waveInGetDevCaps соответственно. Можно указать флаг WAVE_MAPPER. В этом случае система сама подберет устройство, которое способно записать звук в указанном формате. Кроме того, в функции waveInOpen задается способ извещения приложения устройством записи. Всего предусмотренно три механизма таких извещений: передача сообщений указанному окну, вызов процедуры косвенного вызова, установка события (event). Следующий код иллюстрирует процедуру "открывания" аудиоустройства и записи одного буфера:
HWAVEIN hWaveIn;
WAVEFORMATEX WaveFormat;
WaveFormat.wFormatTag = WAVE_FORMAT_PCM;
WaveFormat.nChannels =
1;
WaveFormat.nSamplesPerSec = 16000L;
WaveFormat.nBlockAlign =
2;
WaveFormat.nAvgBytesPerSec =
WaveFormat.nSamplesPerSec*WaveFormat.nBlockAlign;
WaveFormat.wBitsPerSample =
16;
WaveFormat.cbSize = 0;
MMRESULT mmRes =
waveInOpen(&hWaveIn, WAVE_MAPPER, &WaveFormat,
(DWORD)hWnd, 0L, CALLBACK_WINDOW);
В данном случае будет открыто устройство для записи звука со следующими параметрами:
Если по каким-либо причинам устройство не может быть открыто, будет возвращен код ошибки. Для получение информации об ощибке по ее коду можно воспользоваться функцией waveInGetErrorText.
Если устройство было открыто успешно, можно приступать к записи. Для этого сначала нужно подготовить буфер:
WAVEHDR WaveHdr;
ULONG BufferSize =
WaveFormat.nBlockAlign*WaveFormat.nSamplesPerSec*10;
WaveHdr.lpData =
malloc(BufferSize);
WaveHdr.dwBufferLength = BufferSize;
waveInPrepareHeader(hWaveIn, &WaveHdr, sizeof(WAVEHDR));
waveInAddBuffer(hWaveIn, &WaveHdr, sizeof(WAVEHDR));
В данном случае был подготовлен буфер, достаточный для записи 10с звука. Теперь можно начать запись:
waveInStart(hWaveIn);
После того, как буфер будет заполнен данными, аудиоустройство вернет буфер приложению, о чем известит его. В нашем случае приложению будет послано сообщение MM_WIM_DATA, параметром которого является указатель на записанный буфер. После записи, необходимо корректно освободить буфер:
MM_WIM_DATA:
waveInUnprepareHeader(hWaveIn, &WaveHdr,
sizeof(WAVEHDR));
//WaveHdr.lpData - указывают на буфер, где хранять
записанные данные.
//После вызова waveInUnprepareHeader ими можно
распоряжаться
//по собственному усмотрению
free(WaveHdr.lpData);
По окончанию записи следует закрыть аудиоустройство:
waveInClose(hWaveIn);
В рассмотренном случае, когда разер записываемого буфера заранее известен, все достаточно просто. В общем случае, когда размер записываемого буфера заранее не известен, следует предусмотреть циклическое добавление буферов аудиоустройству при помощи waveInAddBuffer. Этот цикл удобно выполнить в виде отдельного потока. В качестве извещений в таком случае логично использовать события. В этом случае поток должен ожидать с помощью WaitForSingleObject возникновения события. После этого проверить все буфера, переданные аудиоустройству, на установку флага WHDR_DONE - буфер записан. Записанный буфер освободить, а устройству "подсунуть" новый.
Отмечу, что буфер будет считаться записанным, если во время записи будет вызвана функция waveInStop. В поле WaveHdr.dwBytesRecorded будет содержаться количество записанных байт. Таким образом, может возникнуть идея использовать один очень большой буфер. Однако, я бы предостерег использовать буфера, размером сопоставимые с размером свободной оперативной памяти. Это может существенно снизить производительноть всей системы. Поэтому, несмотря на большую сложность в реализации, я бы настойчиво советовал использовать первый путь - подбрасывать в аудиоустройство, как в печку, небольшие (фанатизма тоже не надо, размер буфера длительностью 1с мождно считать небольшим) буфера.
Еще одно замечание. При использовании в качестве механихма извещений событий, следует помнить, что события будут устанавливаться во всех тех же случаях, когда посылались бы сообщения окну, т.е. при открытии аудиоустройства (MM_WIM_OPEN), при записи очередного буфера (MM_WIM_DATA), при закрытии аудиоустройства (MM_WIM_DATA). Поэтому, при получении первого события все буфера еще пустые - это событие об открытии аудиоустройства, а не об записи первого буфера. Я это пишу к тому, что не нужно пытаться этот первый буфер освободить :)
Теперь менее подробно рассмотрим обратную процедуру - воспроизведение потока оцифорованного звука:
HWAVEOUT hWaveOut;
WAVEFORMATEX WaveFormat;
WaveFormat.wFormatTag = WAVE_FORMAT_PCM;
WaveFormat.nChannels =
1;
WaveFormat.nSamplesPerSec = 16000L;
WaveFormat.nAvgBytesPerSec =
16000L;
WaveFormat.nBlockAlign = 2;
WaveFormat.wBitsPerSample =
16;
WaveFormat.cbSize = 0;
MMRESULT mmRes =
waveOutOpen(&hWaveOut, WAVE_MAPPER, &WaveFormat,
(DWORD)hWnd, 0L, CALLBACK_WINDOW);
WAVEHDR WaveHdr;
ULONG BufferSize =
WaveFormat.nBlockAlign*WaveFormat.nSamplesPerSec*10;
WaveHdr.lpData =
malloc(BufferSize);
WaveHdr.dwBufferLength = BufferSize;
//Заполняем буфер WaveHdr.lpData данными
waveOutPrepareHeader(hWaveOut, &WaveHdr,
sizeof(WAVEHDR));
waveOutWrite(hWaveOut, &WaveHdr, sizeof(WAVEHDR));
Обработчик сообщения MM_WOM_DONE
MM_WOM_DONE:
waveOutUnprepareHeader(hWaveOut, &WaveHdr,
sizeof(WAVEHDR));
free(WaveHdr.lpData);
waveInClose(hWaveIn);
Для недовольных и ленивых: в следующем разделе будет приведен и рассмотрен код работающей программы. А посвящен будет следующий раздел работе с wave файлами.
В заключении этого раздела кратенько познакомимся с довольно полезными функциями:
auxGetVolume - уровень звука (не в обязательно "в
колонках"!)
auxSetVolume - установить уровень звуа для указанного
устройства
Назначение этих функций понятно. Но вот использование не так просто. Рекомендую тщательно почесть описание в справке.
PlaySound - воспроизвести wave файл из ресурсов или диска.
Вместо имени файла может буть указан алиас для системного звука (типа щелчка при раскрытии окна). В принципе, этой функцией можно обойтись и не читать следующий раздел. Тем не менее, если вы собираетесь воспроизводить несколько файлов одновременно, следующий раздел будет вам полезен (вместе с разделом, посвященном DirectSound).
Wave-файлы (обычно с расширением .wav) имеет формат RIFF - Resource Interchange File Format. Информация в этом формате представляется в виде блоков (chunk). Каждый блок имеет три раздела: идентификатор блока - четыре символа, размер поля данных - четыре байта (таким образом, размер данных не может привышать 4Гб, иначе данные следует разбить на несколько блоков) и непосредственно сами данные. Внутри поля данных могут находится вложенные блоки. Таким образом внутри одного wave-файла может в принципе находиться несколько потоков аудиоданных.
Любой файл в формате RIFF имеет один "старший" блок с индификатором "RIFF". Все остальные блоки являются вложенными. Блок RIFF имеет одно дополнительное поле, в котором указан тип хранимых данных.
Для работы с RIFF файлами в мультимедиа библиотеке существуют несколько функций:
mmioOpen - открыть RIFF файл;
mmioClose - закрыть RIFF
файл;
mmioDescend - открыть вложенный блок;
mmioAscend -
закрыть вложенный блок;
mmioRead - прочитать данные из открытого
блока;
mmioWrite - записать данные в открытый
блок;
mmioCreateChunk - создать новый вложенный блок;
Кроме указанных функций, существует несколько других, но их рассмотрение выходит за рамки изложения. Описание всех функций есть в SDK.
Рассмотрим на примере, как работать с RIFF файлами.
Для начала wave-файл (как и любой другой :) ) следует открыть. Мультмедиа функции для работы с файлами имеют практически те же возможности, что и функции базового API. Так функция mmioOpen позволяет с помощью флагов задать режим доступа (совместный или эксклюзивный), отметить открываемый файл как временный и.т.п. Полный список флагов и их описание можно найтив в SDK. Существующий на диске файл можно открыть так:
HMMIO hMmio = mmioOpen(szFileName, NULL, MMIO_READ | MMIO_ALLOCBUF);
В данном случае мы открыли файл только для чтения с буферезованным досупом. Толк от буферизации будет только если часто и последовательно читаются небольшие порции информации (меньшие размера буфера, который по умолчанию составляет 8К). В данном примере мы попытаемся за раз прочесть все данные, поэтому буферизация нам здесь не нужна. Тем не менее, в большинстве случаев использование буферизованных операций чтения/записи поможет более рационально использовать ресурсы системы. Поэтому мы и установили этот флаг - так "на будущее". Кроме того, если вы отчетливо понимаете, что вы делаете, можно увеличить размер буфера с помощью вызова mmioSetBuffer.
Если файл был открыт удачно (дескриптор не нулевой), читаем RIFF заголовок:
MMCKINFO mmCkInfoRiff;
mmCkInfoRiff.fccType = mmioFOURCC('W', 'A', 'V',
'E');
MMRESULT mmRes = mmioDescend(hMmio, &mmCkInfoRiff, NULL,
MMIO_FINFRIFF);
Если mmRes = MMSYSERR_NOERROR, значит был удачно открыт RIFF заголовок для аудиопотока (тип WAVE).
Аудиоданные располагаются в таком порядке: блок формата (тип 'fmt'), непосредственно данные (тип 'date'). В таком порядке и следует их читать. Для начала прочитаем заголовок блока информации о формате аудиоданных:
MMCKINFO mmCkInfo;
mmCkInfo.ckid = mmioFOURCC('f', 'm', 't', '
');
mmRes = mmioDescend(hMmio, &mmCkInfo, &mmCkInfoRiff,
MMIO_FINDCHUNK);
Если заголовок был прочитан успешно, можно прочитать данные, которые соответствуют структуре WAVEFORMATEX, рассмотренной в предыдущем разделе.
WAVEFORMATEX WaveFormat;
mmioRead(hMmio, (char*)&WaveFormat,
sizeof(WaveFormat) );
После чтения информации о формате аудиоданных, закрываем блок формата и открываем блок данных:
mmRes = mmioAscend(hMmio, &mmCkInfo, 0);
mmCkInfo.ckid =
mmioFOURCC('d', 'a', 't', 'a');
mmRes = mmioDescend(hMmio, &mmCkInfo,
&mmCkInfoRiff, MMIO_FINDCHUNK);
Теперь остается только прочесть данные. Для этогоследует выделить буфер в оперативной памяти.
LPVOID pBuf = VirtualAlloc(NULL, mmCkInfo.cksize, MEM_COMMIT, PAGE_READWRITE
);
if (!pBuf) mmioRead(hMmio, (HPSTR)pBuf, mmCkInfo.cksize);
В случае удачного завершения операции чтения в буфере будут расположены аудиоданные. После этого они могут быть обработаны программой, в том числе выведены в звуковую карту, как это было описано в предыдущем разделе. Заметим, что по окончанию работы следует освободить память с помощью вызова VirtualFree.
Как и в предыдущем разделе, мы рассмотрели простейший сценарий работы с wave-файлом. На практике возможна ситуация, когда wave-файл имеет очень большой размер - сотни мегабайт. В этом случае крайне неудачным решением будут попытка прочесть весь файл за один раз в буфер. В данной ситуации следует создать отдельный поток, который будет читать данные из файла небольшими порциями (функция mmioRead подобно другим функциями для работы с файлами читает данные относительно файлового указателя и может вызываться последовательно несколько раз пока не будет достигнут конец блока), которые будут передаваться звуковой карте для воспроизведения. При таком подходе работа приложения практически не скажется на производительности всей системы.
И еще одно замечание. Функции для работы с файлами в формате RIFF могут оказаться полезными для хранения пользовательских данных, если требуется организовать их хранение в иерархической древовидной стркутуре.
DirectSound является одним из компонентов библиотеки DirectX. Эта билиотека была разработана специально для поддержки высокопроизводительных мультимедиа приложений и включает в себя средства для работы с видеокартами, 3D ускорителями, звуковыми картами и MIDI устройствами, с графическими, аудио и видео компрессорами, устройствами пользовательского ввода, а также сетевыми компонентами системы. Слово Direct указывает на то, что программист получает непосредственный доступ к аппаратуре (в первую очередь видимо имелась в виду библиотека DirectDraw, позволяющая напрямую работать с видеобуферами, в отличие от стандартных средств GDI). На самом деле, естественно, прямой доступ к аппаратуре имеет только драйвер устройства. Он в свою очередь имеет интерфейс, позволяющий библиотеке DirectX работать с этим устройством абстрагируясь от вопросов непосредственного управления аппаратурой. Кроме того, совместными усилиями драйвер устройства и драйвера DirectX создают то, что называется HEL (с одним L :) ) - Hardware Emulation Level. То есть даже если устройство не поддерживает какую-нибудь функцию, DirectX сделает попытку эмулировать ее работу. Например, если видеокарта не имеет 3D ускорителя, вся работа по визуализации сцены будет выполнена программными средствами Direct3D. Еще одной особенностью DirectX является поддержка технологии COM - управление любыми объектами библиотеки осуществляется только через предоставляемые интерфейсы. Это обеспечивает 100% совместимость "снизу-вверх" - приложения, разработанные для старых версий DirectX, будут работать на любых более новых версиях.
Поговорим не много об архитектуре DirectSound. Эта библиотека предоставляет
несколько CОМ объектов, позволяющих работать со звуковой платой. Каждый объект в
соответствии с моделью COM доступен через набор предоставляемых интерфейсов.
Основными объектами являются:
· "устройство воспроизведения", предоставляет интерфейс IDirectSound;
· "устройство захвата звука", предоставляет интерфейс
IDirectSoundCapture;
· Объект "аудиобуфер", предоставляет интерфейс IDirectSoundBuffer для
буферов, предназначенных для воспроизведения звука, и IDirectSoundCaptureBuffer
для буферов, предназначенных для захвата звука;
Работа с библиотекой DirectSound всегда начинается с создания объекта
аудиоустройства. Если в системе установлено различное оборудование, на этом
этапе можно выбрать, с каким устройством будет осуществляться работа. Узнать
информацию об установленных устройствах можно с помощью функции
DirectSoundEnumerate для устройств воспроизведения и DirectSoundCaptureEnumerate
для устройств аудиозахвата.
Для каждого аудиоустройства можно создать один или несколько аудиобуферов.
Эти буфера используются для воспроизведения, захвата, микширования звуковых
потоков.
При работе с устройствами воспроизведения различают первичный и вторичные
буфера. Вторичные буфера содержат данные в формате PCM c различными параметрами
(частота дискретизации, количество каналов и.т.д). Первичный буфер содержит
аудиоданные, воспроизводимые звуковой картой. Перед тем как попасть в первичный
буфер, данные из вторичных буферов подвергаются микшированию, что позволяет
параллельно воспроизводить несколько аудиопотоков. При микшировании данных
производиться автоматическое преобразование форматов данных, содержащихся во
вторичных буферах, к установленному формату первичного буфера.
Рассмотрим порядок действий при воспроизведении звука средствами DirectSound.
Первым делом нужно создать объект аудиоустройства:
HRESULT hRes;
LPDIRECTSOUND pDSound;
if (FAILED( hRes
=
DirectSoundCreate(NULL, &pDSound, NULL) ) ) return hRes;
Для работы с библиотекой DirecrtSound необходимо подключить заголовок <dsound.h>, в котором описаны все используемые типы данных.
Примечание: с каждой версией DirectX выпускается соответствующий SDK, содержащий кроме всего прочего, последние версии библиотек и соответствующие заголовочные файлы. Для доступа к новым возможностям необходимо использовать последние версии интерфейсов. Обычно они отличаются от базовых цифрой в имени типа интерфейса, указывающей на версию соответствующего интерфейса. Например, IDirectSound8 и соответствующий псевдоним LPDIRECTSOUND8. Мы далее будем использовать только базовые функции и интерфейсы, что сделает наш код независимым от версии DirectX.
При вызове функции DirectSoundCreate первым параметром можно указать GUID
(глобально-уникальный идентификатор) требуемого аудиоустройства. Требуемый GUID
можно получить, например, с помощью функции DirectSoundEnumerate. Если указать
NULL (или псевдоним DSDEVID_DefaultPlayback), будет произведена попытка создать
объект, используемый для воспроизведения звука по умолчанию.
После создания объекта аудиоустройства необходимо задать модель
взаимодействия (уровень привилегий обращения к звуковой аппаратуре) при
воспроизведении звука с другими работающими приложениями, иначе вторичные буфера
не будет микшироваться. Для оконных приложений рекомендуется использовать
"нормальный" уровень привилегий (DSSCL_NORMAL). При этом звуковой поток
микшируется в первичном буфере с потоками других приложений. На этом уровне
запрещено менять формат первичного буфера и звук будет воспроизводиться формате
по умолчанию - в 8 битном. Для полноэкранных приложений следует использовать
эксклюзивный режим (DSSCL_EXCLUSIVE).
if (FAILED(hRes =
pDSound->SetCooperativeLevel(hWnd, DSSCL_NORMAL) )
)
return hRes;
Далее приступим к созданию вторичного аудиобуфера. Для начала зададим его
формат:
WAVEFORMATEX waveFmt;
waveFmt.wFormatTag =
WAVE_FORMAT_PCM;
waveFmt.nChannels = 1;
waveFmt.nSamplesPerSec =
8000;
waveFmt.wBitsPerSample = 8;
waveFmt.nBlockAlign =
(WORD)(waveFmt.wBitsPerSample * waveFmt.nChannels /
8);
waveFmt.nAvgBytesPerSec = waveFmt.nSamplesPerSec *
waveFmt.nBlockAlign;
waveFmt.cbSize = 0;
Примечание: если компилятор "ругается" на тип WAVEFORMATEX, включите заголовок <mmsystem.h>
Далее следует заполнить структуру с информацией о создаваемом буфере. В данном случае мы создаем буфер с параметрами, описываемыми структурой waveFmt и размером, достаточным для размещения 4 с звука.
DSBUFFERDESC dsBufDesc;
ZeroMemory(&dsBufDesc, sizeof(dsBufDesc) );
dsBufDesc.dwSize = sizeof(dsBufDesc);
dsBufDesc.dwFlags =
DSBCAPS_CTRLPOSITIONNOTIFY;
dsBufDesc.dwBufferBytes = waveFmt.nAvgBytesPerSec
* 4;
dsBufDesc.dwReserved = 0;
dsBufDesc.lpwfxFormat = &waveFmt;
После этих подготовительных операций можно непосредственно создать сам буфер:
LPDIRECTSOUNDBUFFER pDsBuffer;
if (FAILED(hRes =
pDSound->CreateSoundBuffer(&dsBufDesc, &pDsBuffer, NULL) )
)
return hRes;
И заполнить его данными:
if (FAILED(hRes =
OnBufferLost(pDsBuffer, 440.0) ) ) return hRes;
После создания буфера, его можно воспроизвести. Но для начала необходимо заполнить его данными. Чуть ниже мы рассмотрим процедуру заполнения буфера данными, которую мы на самом деле уже вызывали под именем OnBufferLost. Но перед этим скажем пару слов о "потере буфера". Некоторые методы интерфейса IDirectSoundBuffer возвращают значение DSERR_BUFFERLOST, означающее, что память, выделенная под аудиоданные, была освобождена. Это можно сказать штатная ситуация. Буфера "теряются" вместе с потерей фокуса вашим приложением. Кроме того, буфер может утеряться при нехватке ресурсов (оперативной памяти) системе. После потери буфера его необходимо восстановить (для этого существует специальный метод – IDirectSoundBuffer::Restore() ) и заново заполнить корректными данными. Поэтому рассмотрим процедуру инициализации буфера данными на примере его восстановления:
HRESULT OnBufferLost(LPDIRECTSOUNDBUFFER lpDSBuffer, float flFreq
)
{
HRESULT hRes;
PUCHAR pLockBuf;
DWORD dwBufSize;
WAVEFORMATEX
waveFmt;
do hRes = lpDSBuffer->Restore();
while (hRes ==
DSERR_BUFFERLOST);
if ( FAILED( hRes ) ) return hRes;
if ( FAILED( hRes =
lpDSBuffer->Lock(0, 0, (LPVOID*)&pLockBuf,
&dwBufSize, NULL, 0,
DSBLOCK_ENTIREBUFFER ) )) return hRes;
if ( FAILED( hRes =
lpDSBuffer->GetFormat(&waveFmt,
sizeof(waveFmt), NULL )
) ) return hRes;
for (int i = 0; i < dwBufSize; ++i)
{
pLockBuf[i] = (UCHAR)
(64.0
* (1.0 + cos(2.0*3.1416*i*flFreq/waveFmt.nSamplesPerSec) ) );
}
return lpDSBuffer->Unlock( (LPVOID)pLockBuf, dwBufSize, NULL, 0);
}
Эта функция выполняет следующие действия:
А теперь, когда у нас есть буфер, заполненный данными, самое время его
запустить на воспроизведение! Сделать это не сложно: достаточно вызвать метод
IDirectSoundBuffer::Play. Воспроизведение буфера займет некоторое время. Если
ваша программа должна быть извещена об окончании воспроизведения, мы должны об
этом позаботиться. Библиотека DirectSound предоставляет специальный интерфейс
–IDirectSoundNotify – для реализации механизма извещения. На проигрываемом файле
можно установить несколько точек, при достижении которых будет сгенерировано
извещение. В качестве механизма извещения используются события (Events).
Следующий фрагмент кода иллюстрирует, как использовать механизм извещений от
DirectSound:
LPDIRECTSOUNDNOTIFY lpDsNotify;
DSBPOSITIONNOTIFY PositionNotify;
//Требуем нужный нам интерфейс IDirectSoundNotify
if (FAILED(
hRes =
pDsBuffer->QueryInterface(IID_IDirectSoundNotify,
(LPVOID*)&lpDsNotify)))
return hRes;
PositionNotify.dwOffset = DSBPN_OFFSETSTOP;
PositionNotify.hEventNotify =
CreateEvent(NULL, FALSE, FALSE, NULL);
hRes =
lpDsNotify->SetNotificationPositions(1,
&PositionNotify);
lpDsNotify->Release(); //Освобождаем не нужный более
интерфейс
Замечание: обратите внимание на флаг, установленный при создании буфера: DSBCAPS_CTRLPOSITIONNOTIFY. Без установки этого флага объект аудиобуфер не вернет указатель на интерфейс IDirectSoundNotify.
Мы задали единственное событие, которое будет установлено при проигрывании всего буфера (на что указывает предопределенный макрос DSBPN_OFFSETSTOP). А вот теперь можно и воспроизвести буфер:
while (pDsBuffer->Play(0, 0, 0 ) ==
DSERR_BUFFERLOST)
OnBufferLost(pDsBuffer, 440.0);
//Ожидаем окончания воспроизведения
буфера
WaitForSingleObject(PositionNotify.hEventNotify, INFINITE);
По окончанию работы культурные люди прибирают за собой:
CloseHandle( PositionNotify.hEventNotify
);
pDsBuffer->Release();
pDSound->Release();
Как уже упоминалось выше, перед тем как попасть в первичный буфер, данные из вторичных буферов подвергаются микшированию. Таким образом, чтобы воспроизвести два звуковых потока параллельно, не нужно никаких дополнительных исхищрений: создаете два (более) аудиобуферов (процедура создания рассмотрена довольно подробно) и воспроизводите их, например, так:
pDsBuffer1 -> Play();
pDsBuffer2 -> Play();
pDsBuffer3 ->
Play();
Процедура захвата звука очень похожа на воспроизведение. Последовательность действий такая:
Вот как это могло бы выглядеть:
HRESULT hRes;
//Создаем устройство аудиозахвата
LPDIRECTSOUNDCAPTURE pDSoundCapture;
if ( FAILED (hRes =
DirectSoundCaptureCreate(NULL, &pDSoundCapture,
NULL)
) ) return hRes;
//Задаем параметры захватываемого потока
WAVEFORMATEX
waveFmt;
waveFmt.wFormatTag = WAVE_FORMAT_PCM;
waveFmt.nChannels =
1;
waveFmt.nSamplesPerSec = 8000;
waveFmt.wBitsPerSample =
8;
waveFmt.nBlockAlign = (WORD)
(waveFmt.wBitsPerSample *
waveFmt.nChannels / 8);
waveFmt.nAvgBytesPerSec = waveFmt.nSamplesPerSec *
waveFmt.nBlockAlign;
waveFmt.cbSize = 0;
//Создаем буфер, достаточный для захвата 4 с звука
DSCBUFFERDESC
dscBufDesc;
ZeroMemory(&dscBufDesc, sizeof(dscBufDesc)
);
dscBufDesc.dwSize = sizeof(dscBufDesc);
dscBufDesc.dwFlags =
0;
dscBufDesc.dwBufferBytes = waveFmt.nAvgBytesPerSec *
4;
dscBufDesc.dwReserved = 0;
dscBufDesc.lpwfxFormat = &waveFmt;
LPDIRECTSOUNDCAPTUREBUFFER pDSCBuffer;
if ( FAILED (hRes
=
pDSoundCapture->CreateCaptureBuffer(&dscBufDesc,
&pDSCBuffer,
NULL ) ) ) return hRes;
//Устанавливаем извещение на конец буфера
LPDIRECTSOUNDNOTIFY
lpDsNotify;
if (FAILED(
hRes =
pDSCBuffer->QueryInterface(IID_IDirectSoundNotify,
(LPVOID*)&lpDsNotify)))
return hRes;
DSBPOSITIONNOTIFY PositionNotify;
PositionNotify.dwOffset =
DSBPN_OFFSETSTOP;
PositionNotify.hEventNotify = CreateEvent(NULL, FALSE,
FALSE, NULL);
hRes = lpDsNotify->SetNotificationPositions(1,
&PositionNotify);
lpDsNotify->Release();
//Запускаем процедуру аудиозахвата
pDSCBuffer->Start(0);
//Ждем окончания
аудиозахвата
WaitForSingleObject(PositionNotify.hEventNotify,
INFINITE);
CloseHandle(PositionNotify.hEventNotify);
//Фиксируем аудиобуфер – получаем виртуальный адрес захваченных
данных
PUCHAR pCapBuf;
DWORD dwCapBufSize;
pDSCBuffer->Lock(0, 0,
(LPVOID*)&pCapBuf, &dwCapBufSize, NULL, 0,
DSCBLOCK_ENTIREBUFFER
);
//Выполняем любые действия над данными
for (int i = dwSrcBufSize - 1; i
> 100; i--)
{
//Хочу заметить, это не "эхо", данный код не несет
смысловой нагрузки J
pSrcBuf[i] = (UCHAR)(0.9*pSrcBuf[i] + 0.1*pSrcBuf[i -
100]);
}
//Незабываем разблокировать зафиксированный ранее
буфер
pDSCBuffer->Unlock( (LPVOID)pSrcBuf, dwSrcBufSize, NULL, 0 );
//И в заключении освобождаем
объекты
pDSCBuffer->Release();
pDSoundCapture->Release();
Замечание: При реализации процедуры аудиозахвата мы не возились с потерянными буферами, как при воспроизведении. Приятная новость – буфера, предназначенные для захвата звука, не теряются. Отсюда следует сделать вывод, что не следует создавать такие буфера большими, поскольку система не сможет самостоятельно освободить необходимую для нее память, что может отрицательно сказаться на общей производительности системы.
Это не так просто, как может показаться на первый взгляд. Если вы используете
WindowsXP, то можете воспользоваться для создания аудиоустройства функцией
DirectSoundFullDuplexCreate8. В общем случае придется делать это вручную.