Еще раз о звуке
Оформил: DeeCo
Автор: Сергей Козлов
От автора.
Как я выяснил, жители Королевства интересуются темой ввода/вывода звука,
причем вопросов больше, чем ответов. Меня же некоторое время назад жизнь
заставила разобраться с этой темой, не сказать чтобы досконально, но некоторые
интересные моменты есть :). Так что спрос рождает предложение и оно есть у меня.
Кратко перечислю те вопросы, которые интересуют нас, жителей (из вопросов
Круглого стола):
- …текущие характеристики САМОГО звука (частоту или громкость)
- …получение спектра с помощью FFT,
- …запись в формате MP3.
- …помогите проиграть mp3 и wma файлы с помощью Mutlimedia API WAVEOUT*****
- …определить устройство ввода звука, получить с него звук, отобразить форму
волны, сравнить с образцом и выдать расхождение. Что-то вроде системы
распознавания речи.
- …самый примитивный код, осуществляющий воспроизведение звука с помощью
базовых функций (waveOutOpen,waveOutPrepareHeader и т.д.),
- …регулировать звук воспроизводимого файла из своей программы не могу
- …как програмно регулировать громкости не знаю.
- …функции waveOutWtire и waveInAddBuffer при работе с каким либо callback
механизмом тратят очень много времени на переключение буферов.
- …в CallBack-функции при переключении буферов возникают щелчки в динамиках.
Как от них избавиться?
- …Но как все-таки сначала узнать, установлена ли звуковая карта или нет ?
Итак, на что я попытаюсь ответить:
- как узнать, есть ли устройство вывода/записи звука
- как использовать Multimedia API для вывода/записи звука
- как генерировать звук
- как менять громкость и вообще работать с микшером
- что можно сделать, если есть fullduplex
Чего я не скажу (надеюсь, скажет кто-то другой :)
- Как работать с MP3 файлами.
- Как проводить цифровую обработку сигнала.
- Как работать со звуком в DirectX.
Еще "на берегу" хочу договориться -- HELP или MSDN не переписываю! В
хелпах Delphi все функции описаны -- осталось только найти…
Начинаем.
Для нас важны следующие понятия: PCM, выборка, битовое разрешение, частота
выборки. (см. более полно здесь
)
PCM (импульсно-кодовая модуляция) -- Звук может быть представлен
разными способами, но это самый простой (и, наверное, поэтому наиболее
используемый). Что это такое, можно посмотреть на
сайте, Королевство DELPHI я повторяться не буду.
Sample (выборка) -- значение амплитуды дискретизированного сигнала.
Секунда звучания на компакт-диске содержит 44100 выборок (сэмплов). Имеется в
виду, что выборка содержит в себе реально два значения - для левого и правого
каналов.
sample rate (частота выборки) -- Число выборок в секунду, которое
используется для записи звука. Более высокие частоты соответствуют более
высокому качеству звука, однако потребляют большее количество памяти.
sample size (битовое разрешение) -- определяет количество бит,
используемое для записи единичной выборки на каждом канале. Компьютеры
используют в основном 8 и 16 бит, профессиональное оборудование - 18, 20 и выше.
Несколько слов по поводу "железа". Необходимо четко различать, что звуковая
плата -- это НЕ ОДНО устройство в системе. Есть устройство вывода звука,
записи звука, микшер, синтезатор и т.д. по вкусу. Это важно понимать, т.к.
каждое устройство имеет свой набор функций: waveOut***, waveIn***,
midiOut***, midiIn***, mixer*** и др.
Еще раз повторю: все это РЕАЛЬНО РАЗНЫЕ устройства, упакованные в
одном или нескольких аудиочипах. Кому интересно, посмотрите описание любого
аудиочипа. Например, CS4281
или ES1938
Как узнать, есть ли устройство вывода/записи звука
Для ответа на этот важнейший вопрос ( если устройства нет -- мы ведь ничего
не услышим, правда?) используются следующие функции и структуры API:
- waveOutGetNumDevs -- получить количество аудиоустройств
- waveOutGetDevCaps -- получить свойства аудиоустройства
- TWAVEOUTCAPS -- структура для WaveOutGetDevCaps
Если Вы знаете, что устройство в системе одно, можно поступить так:
procedure TForm1.btnClick(Sender: TObject);
var
WOutCaps: TWAVEOUTCAPS;
begin
// проверка наличия устройства вывода
FillChar(WOutCaps, SizeOf(TWAVEOUTCAPS), #0);
if MMSYSERR_NOERROR <> WaveOutGetDevCaps(0, @WOutCaps, SizeOf(TWAVEOUTCAPS))
then
begin
ShowMessage('Ошибка аудиоустройства');
exit;
end;
end;
Так мы пытаемся узнать характеристики устройства с номером 0 (т.е. первого в
системе) и если его нет, говорим об ошибке. Если у нас несколько звуковых
карточек, используем waveOutGetNumDevs. Характеристики нам понадобятся
позже.
Важно: если хотим узнать, есть ли устройство записи, миксер в системе,
используем WaveIn***, mixer*** и т.д. Ведь этих устройств может и не быть
(USB-колонки). Так что вопрос: "Есть ли звуковая карточка в компьютере?"
не совсем корректен для наших целей, да и не нужен. Вам звук выводить или
карточкой хвалиться?
Как использовать Multimedia API для записи/вывода звука.
Для вывода звука мы используем следующий набор функций и структур API:
- waveOutGetDevCaps -- получить свойства аудиоустройства
- waveOutOpen -- открыть аудиоустройство
- waveOutPrepareHeader -- приготовить буфер вывода для
воспроизведения
- waveOutWrite -- вывести звук (поставить буфер на воспроизведение)
- waveOutReset -- остановить воспроизведение и освободить буферы
- waveOutUnprepareHeader -- вернуть буфер вывода
- WaveOutClose -- закрыть устройство вывода звука
- TWAVEOUTCAPS -- структура для WaveOutGetDevCaps
- TWAVEFORMATEX -- формат звуковых данных
- TWAVEHDR -- формат заголовка буфера вывода.
Как же мы выведем звук?
Во-первых, надо озаботиться способом общения с драйвером. Вариантов
много: сообщения, callback-функции, объекты-события и т.д. По моему опыту,
наиболее "приятно" работать с объектами-событиями, то есть использовать объекты
ядра Events и потоки. Работает без особых проблем, лего управляется, нет
ненужных задержек в очереди сообщений, можно поставить более высокий приоритет
потоку, обрабатывающему звуковые данные. В общем, плюсов много, а главное …
Microsoft рекомендует.
Так, с этим определились, теперь формат звуковых данных. Необходимо заполнить
TWAVEFORMATEX, например, так:
var
wfx: TWAVEFORMATEX;
…
// заполнение структуры формата
FillChar(wfx, Sizeof(TWAVEFORMATEX), #0);
with wfx do
begin
wFormatTag := WAVE_FORMAT_PCM; // используется PCM формат
nChannels := 2; // это стереосигнал
nSamplesPerSec := 44100; // частота дискретизации 44,1 Кгц
wBitsPerSample := 16; // битовое разрешение выборки 16 бит
nBlockAlign := wBitsPerSample div 8 * nChannels;
// число байт в выборке для стереосигнала -- 4 байта
nAvgBytesPerSec := nSamplesPerSec * nBlockAlign;
// число байт в секундном интервале для стереосигнала
cbSize := 0; // не используется
end;
Готово, можно открывать:
var
wfx: TWAVEFORMATEX;
hEvent: THandle;
wfx: TWAVEFORMATEX;
hwo: HWAVEOUT;
…
// открытие устройства
hEvent := CreateEvent(nil, false, false, nil);
if WaveOutOpen(@hwo, 0, @wfx, hEvent, 0, CALLBACK_EVENT) <> MMSYSERR_NOERROR
then
…;
Устройство открыто, теперь (вторым шагом) решим, откуда будем брать
данные для вывода. Для этого выделяем память и готовим буферы вывода. Заметьте,
готовим ДВА буфера для того, чтобы организовать двойную буферизацию -- и
никто никого не ждет…если буфер подходящего размера. В зависимости
от производительности системы он может быть поменьше. ( у меня был минимум
-- 8 кбайт)
Ниже в листинге есть одна особенность -- выделяется память из расчета на
КАЖДЫЙ канал стереозвука -- это нужно для нашего примера, но обычно такое
не требуется.
И еще одна особенность -- умные люди (см. литературу)
рекомендуют выделять только целое количество страниц памяти с учетом грануляции,
что мы и делаем.
var
wfx: TWAVEFORMATEX;
hEvent: THandle;
wfx: TWAVEFORMATEX;
hwo: HWAVEOUT;
si: TSYSTEMINFO;
wh: array[0..1] of TWAVEHDR;
Buf: array[0..1] of PChar;
CnlBuf: array[0..1] of PChar;
…
// выделение памяти под буферы, выравниваются под страницу памяти Windows
GetSystemInfo(si);
buf[0] := VirtualAlloc(nil, (BlockSize * 4 + si.dwPageSize - 1) div
si.dwPagesize * si.dwPageSize,
MEM_RESERVE or MEM_COMMIT, PAGE_READWRITE);
buf[1] := PChar(LongInt(buf[0]) + BlockSize);
// отдельно буферы для генераторов под каждый канал
CnlBuf[0] := PChar(LongInt(Buf[1]) + BlockSize);
CnlBuf[1] := PChar(LongInt(CnlBuf[0]) + BlockSize div 2);
// подготовка 2-х буферов вывода
for I := 0 to 1 do
begin
FillChar(wh[I], sizeof(TWAVEHDR), #0);
wh[I].lpData := buf[I]; // указатель на буфер
wh[I].dwBufferLength := BlockSize; // длина буфера
waveOutPrepareHeader(hwo, @wh[I], sizeof(TWAVEHDR));
// подготовка буферов драйвером
end;
Итак, куда выводить -- есть, откуда выводить -- есть. Третим шагом
осталось определить, что выводить и СДЕЛАТЬ ЭТО (вывести звук). Сначала
мы генерим данные для левого и правого канала раздельно, затем смешиваем и
помещаем в первый буфер вывода. Генерация производится очень просто -- sin.
Смешиваем два буфера в один с помощью процедуры mix -- небольшая процедурка на
ASMе Такой подход я избрал вот почему -- не все же синус по двум каналам
генерить! Можно и музыку разную налево и направо пустить. (это называется
бинуральное слушание, кажется). Заметьте, для генерации каждого нового буфера мы
сохраняем текущее время сигнала, чтобы он был гладкий да шелковистый... И
ПОМНИТЕ, что все это делается в отдельном потоке. Как видите, здесь есть
пространство для творчества (оптимизации), но это оставляю читателям.
// генерация буферов каналов
Generator(CnlBuf[0], Typ[0], Freq[0], Lev[0], BlockSize div 2, tPred[0]);
Generator(CnlBuf[1], Typ[1], Freq[1], Lev[1], BlockSize div 2, tPred[1]);
// смешивание буферов каналов в первый буфер вывода
Mix(buf[0], CnlBuf[0], CnlBuf[1], BlockSize div 2);
И наконец, вот он, ЗВУК!
I := 0;
while not Terminated do
begin
// передача очередного буфера драйверу для проигрывания
waveOutWrite(hwo, @wh[I], sizeof(WAVEHDR));
WaitForSingleObject(hEvent, INFINITE);
I := I xor 1;
// генерация буферов каналов
Generator(CnlBuf[0], Typ[0], Freq[0], Lev[0], BlockSize div 2, tPred[0]);
Generator(CnlBuf[1], Typ[1], Freq[1], Lev[1], BlockSize div 2, tPred[1]);
// смешивание буферов каналов в очередной буфер вывода
Mix(buf[I], CnlBuf[0], CnlBuf[1], BlockSize div 2);
// ожидание конца проигрывания и освобождения предыдущего буфера
end;
Важно: нет необходимости повторно готовить буферы функцией
waveOutPrepareHeader, просто пишите данные в память и играйте… Когда Вы
насладитесь звуком (все это пищание надоест), нужно выключить машинку:
// завершение работы с аудиоустройством
waveOutReset(hwo);
waveOutUnprepareHeader(hwo, @wh[0], sizeof(WAVEHDR));
waveOutUnprepareHeader(hwo, @wh[1], sizeof(WAVEHDR));
// освобождение памяти
VirtualFree(buf[0], 0, MEM_RELEASE);
WaveOutClose(hwo);
И освобождаем наш объект-событие.
CloseHandle(hEvent);
Все, наступила тишина…
Итак, мы разобрались с тремя вопросами:
- как узнать, есть ли устройство вывода звука,
- как сгенерировать звук и
- как вывести звук.
Далее по плану: как менять громкость и вообще работать с микшером и что такое
fullduplex.
Пример программы подготовлен для Delphi5. Скачать — Generator.zip
5.8K
Литература
Гордеев О. В. Программирование звука в Windows. СПб.: БХВ — Санкт-Петербург
1999 384 с.
|