Delphi World - Еще раз о звуке
Delphi World - это проект, являющийся сборником статей и малодокументированных возможностей  по программированию в среде Delphi. Здесь вы найдёте работы по следующим категориям: delphi, delfi, borland, bds, дельфи, делфи, дэльфи, дэлфи, programming, example, программирование, исходные коды, code, исходники, source, sources, сорцы, сорсы, soft, programs, программы, and, how, delphiworld, базы данных, графика, игры, интернет, сети, компоненты, классы, мультимедиа, ос, железо, программа, интерфейс, рабочий стол, синтаксис, технологии, файловая система...
Еще раз о звуке

Оформил: 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 с.

Проект Delphi World © Выпуск 2002 - 2017
Автор проекта: Эксклюзивные курсы программирования