скрыть

скрыть

  Форум  

Delphi FAQ - Часто задаваемые вопросы

| Базы данных | Графика и Игры | Интернет и Сети | Компоненты и Классы | Мультимедиа |
| ОС и Железо | Программа и Интерфейс | Рабочий стол | Синтаксис | Технологии | Файловая система |



Google  
 

Еще раз о звуке



Автор: Сергей Козлов

От автора.

Как я выяснил, жители Королевства интересуются темой ввода/вывода звука, причем вопросов больше, чем ответов. Меня же некоторое время назад жизнь заставила разобраться с этой темой, не сказать чтобы досконально, но некоторые интересные моменты есть :). Так что спрос рождает предложение и оно есть у меня. Кратко перечислю те вопросы, которые интересуют нас, жителей (из вопросов Круглого стола):

  • …текущие характеристики САМОГО звука (частоту или громкость)
  • …получение спектра с помощью 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 с.






Copyright © 2004-2016 "Delphi Sources". Delphi World FAQ




Группа ВКонтакте   Ссылка на Twitter   Группа на Facebook