Начиная с сегодняшнего номера я начинаю цикл статей про программирование звука. Я покажу тебе, как эффективно выводить звук в колонки и как его записывать. Для воспроизведения и записи звука нам придётся познакомиться как минимум с одним форматом хранения файлов. Для начала это будет WAV, но в разделе форматы файлов я опишу ещё пару. Помимо этого, нам предстоит научиться преобразовывать файлы из одного формата, в другой. Как всегда, для реализации примеров будет использован язык Delphi.
Для понимания технологии программирования звука тебе понадобится немного познакомится с внутренностью работы цифрового звука. Вот именно с этого мы и начнём рассмотрение цифрового звука.
Звуковые данные хранятся в компьютере с помощью метода импульсно-кодовой модуляции. Ты наверно не раз встречал сокращение PCM, она именно это и означает. Расшифровывается PCM как Pulse-Code Modulation. При этом методе аналоговый звук квантуется по времени и амплитуде. При выводе звука на колонки происходит обратное преобразование. Для большего понимания я нарисовал рис. 1, на котором всё представлено удобно для твоего глаза.
Рис 1. Квантование
На графике слева нарисована амплитуда аналогового (ну и словечко) сигнала. Вверх направлена ось амплитуды звука, а вправо - время звучания. Вертикальными штриховыми линиями показано время, в которое произошёл замер амплитуды и сохранение её величины.
На правом рисунке показан результат. Как видишь, кривая звука превратилась в точки. Всё, что между точками потеряно, поэтому, чем чаще происходит замер амплитуды, тем выше качество звука и меньше потерь. Значение 1/время между замерами (дельта Т) называется частотой дискретизации. Для качественного звука, частота дискретизации должна быть вдвое больше чем высшая частота в обрабатываемом звуке.
Анекдот:
Пpогpаммиста спpашивают:
- Как вам yдалось так быстpо выyчить английский язык?!!
- Да, еpyнда какая. Они там почти все слова из С++ взяли.
С физикой мы разобрались. Теперь давай двинем в сторону рукоблудия (никакой пошлости, я имел ввиду программирование). Сколько бы не ругали Windows за его глючность, я его буду хвалить за то, что здесь есть практически всё необходимо программисту, т.е. мне. Так что переходим к делу и начинаем программить.
Для работы со звуковухой используется всё тот же алгоритм, как и с любым другим устройством.
Инициализировать драйвер звуковой.
Установить свои параметры.
Воспроизвести или записать звуковые данные.
Помахать ручкой и закрыть драйвер.
Хочу напомнить, что твоя звуковуха не резиновая. Поэтому она не сможет воспроизводить все файлы подряд. Тебе придутся самому заботиться о выводе на колонки файлов больших размеров. Для этого тебе придётся делать цикл, в котором будешь последовательно отправлять драйверу данные маленькими кусочками. Когда мы будем разбирать пример, я всё это тебе покажу. А сейчас давай познакомимся с основными функциями, необходимыми для воспроизведения звука.
Для сегодняшнего примера тебе понадобится пять основных функции. Все они определены в модуле mmsystem, поэтому тебе придётся подключить его в раздел uses.
lphWaveOut - адрес, по которому будет записан указатель на устройство воспроизведения.
uDeviceID - идентификатор устройства, которое ты хочешь открыть. Если ты поставишь сюда константу WAVE_MAPPER, то откроется устройство по умолчанию.
lpFormat - указатель на структуру типа WAVEFORMATEX, в которой описан формат воспроизводимых звуковых данных.
dwCallback - указатель на функцию семафор. Эта функция будет вызываться, чтобы сообщить тебе о происходящем.
dwFlags - Параметры открываемого устройства. Может принимать следующие значения:
CALLBACK_EVENT - в dwCallback находится событие THandle, через которое будет происходить информирования о ходе воспроизведения.
CALLBACK_THREAD - в dwCallback находится идентификатор потока
CALLBACK_FUNCTION - в dwCallback находится указатель на функцию.
CALLBACK_WINDOW - в dwCallback указатель на окно, которому будут посылаться сообщения.
CALLBACK_NULL - в dwCallback ничего нет.
WAVE_ALLOWSYNC - можно открыть устройство в синхронном режиме.
WAVE_FORMAT_DIRECT - запрещается преобразование данных с помощью ACM драйвера.
WAVE_FORMAT_QUERY - Если ты установишь этот параметр, то реального открытия звуковухи не произойдёт. Функция проверит возможность открытия с заданными тобой параметрами, и если всё ничтяк, то вернёт тебе MMSYSERR_NOERROR. Если твои параметры недопустимы, то вернётся код ошибки. В любом случае реального открытия устройства не произойдёт. Этот флаг можно использовать как тест на допустимость настроек.
Функция может вернуть следующие значения:
MMSYSERR_ALLOCATED - устройство уже открыто
MMSYSERR_BADDEVICEID - указан неправильный идентификатор устройства uDeviceID
MMSYSERR_NODRIVER - драйвер отсутствует
MMSYSERR_NOMEM - не могу выделить память, или она заблокирована
WAVERR_BADFORMAT - указан неправильный формат
WAVERR_SYNC - устройство в синхронно, но оно вызвано без WAVE_ALLOWSYNC флага
Теперь рассмотрим используемую здесь структуру WAVEFORMATEX:
Для С++
typedef struct {
WORD wFormatTag;
WORD nChannels;
DWORD nSamplesPerSec;
DWORD nAvgBytesPerSec;
WORD nBlockAlign;
WORD wBitsPerSample;
WORD cbSize;
} WAVEFORMATEX;
Для Delphi
PWaveFormatEx = ^TWaveFormatEx;
tWAVEFORMATEX = packed record
wFormatTag: Word;
nChannels: Word;
nSamplesPerSec: DWORD;
nAvgBytesPerSec: DWORD;
nBlockAlign: Word;
wBitsPerSample: Word;
cbSize: Word;
end;
wFormatTag - Формат звуковых данных. Мы будем использовать в основном WAVE_FORMAT_PCM.
nChannels - Количество каналов (1- моно, 2 - стерео).
nSamplesPerSec - Частота дискретизации (возможны значения 8000, 11025. 22050 и 44100).
nAvgBytesPerSec - Количество байт в секунду. Для WAVE_FORMAT_PCM это является результатом nSamplesPerSec* nBlockAlign.
nBlockAlign - Выравнивание блока. Для WAVE_FORMAT_PCM равен wBitsPerSample/8* nChannels
wBitsPerSample -Количество бит в одной выборке. Для WAVE_FORMAT_PCM может быть 8 или 16.
cbSize - Размер дополнительной информации, которая располагается после структуры. Если ничего нет, то должен быть 0.
Теперь у нас есть вся информация о том, как открыть звуковое устройство. На первый взгляд уже можно приступить к воспроизведению звуковых данных, но это не так. Теперь нам предстоит подготовить заголовки, которые и будут отправляться драйверу звуковухи. Для этого есть функция waveOutPrepareHeader :
Для С++
MMRESULT waveOutPrepareHeader(
HWAVEOUT hwo,
LPWAVEHDR pwh,
UINT cbwh
);
Для Delphi
function waveOutPrepareHeader(
hWaveOut: HWAVEOUT;
lpWaveOutHdr: PWaveHdr;
uSize: UINT
): MMRESULT; stdcall;
Возвращаемые параметры те же, а вот внутренности давай рассмотрим:
hWaveOut идентификатор устройства воспроизведения. Ты его получил после вызова функции waveOutOpen.
lpWaveOutHdr Указатель на структуру wavehdr_tag. Она уже должны быть специально подготовлена. Как это сделать см. ниже.
uSize Размер структуры wavehdr_tag.
Теперь структура wavehdr_tag.
Для С++
typedef struct {
LPSTR lpData; // address of the waveform buffer
DWORD dwBufferLength; // length, in bytes, of the buffer
DWORD dwBytesRecorded; // see below
DWORD dwUser; // 32 bits of user data
DWORD dwFlags; // see below
DWORD dwLoops; // see below
struct wavehdr_tag far * lpNext; // reserved; must be zero
DWORD reserved; // reserved; must be zero
} WAVEHDR;
Для Delphi
wavehdr_tag = record
lpData: PChar;
dwBufferLength: DWORD;
dwBytesRecorded: DWORD;
dwUser: DWORD;
dwFlags: DWORD;
dwLoops: DWORD;
lpNext: PWaveHdr;
reserved: DWORD;
end;
Что же это за зверь? Посмотрим на его кишки:
lpData Указатель на звуковые данные.
dwBufferLength Размер звуковых данных.
dwBytesRecorded Количество записанных байт. Я думаю ты догадался, что при воспроизведении этот параметр не хляется.
dwUser Любая чушь.
dwFlags Здесь происходит описание заголовка. Возможны значения:
WHDR_BEGINLOOP Звуковые данные являются первыми в цикле.
WHDR_ENDLOOP Звуковые данные являются последними в цикле.
WHDR_DONE Окончание воспроизведение. Этот флаг может выставить только драйвер.
WHDR_QUEUE Данные помещены в очередь. Этот флаг может выставить только драйвер.
WHDR_PREPARED Заголовок инициализирован. Этот флаг может выставить только драйвер.
dwLoops Количество воспроизведений звуковых данных.
lpNext Зарезервировано.
reserved Зарезервировано.
Прежде чем вызывать waveOutPrepareHeader, ты должен заполнить структуру wavehdr_tag. Для этого её нужно сначала наполнить нулями, а затем установить правильные значения в поля указывающие размер и положение звуковых данных. А если необходимы флаги, то они тоже должны быть уже выставлены. После подготовки заголовка с помощью waveOutPrepareHeader, в заголовке нельзя уже ничего менять, кроме: lpData (можно подставить следующие данные), dwBufferLength (можно только уменьшать) и dwFlags (можно добавлять и удалять флаги WHDR_BEGINLOOP и WHDR_ENDLOOP).
После окончания работы с этим заголовком, нужно освободить его с помощью функции waveOutUnprepareHeader.
Теперь уже можно смело отправлять подготовленный заголовок драйверу.
Для С++
MMRESULT waveOutWrite(
HWAVEOUT hwo,
LPWAVEHDR pwh,
UINT cbwh
);
Для Delphi
function waveOutWrite(
hWaveOut: HWAVEOUT;
lpWaveOutHdr: PWaveHdr;
uSize: UINT
): MMRESULT; stdcall;
hWaveOut идентификатор устройства воспроизведения. Ты его получил после вызова функции waveOutOpen.
lpWaveOutHdr это указатель на структуру, которую ты сделал с помощью waveOutPrepareHeader.
uSize Размер структуры wavehdr.
Возвращаемые значения те же, что и при открытии звуковухи.
После вывода звука, нужно вызвать waveOutUnprepareHeader, чтобы очистить заголовки и закрыть устройство. Давай посмотрим на эти функции.
Для С++
MMRESULT waveOutUnprepareHeader(
HWAVEOUT hwo,
LPWAVEHDR pwh,
UINT cbwh
);
Для Delphi
function waveOutUnprepareHeader(
hWaveOut: HWAVEOUT;
lpWaveOutHdr: PWaveHdr;
uSize: UINT
): MMRESULT; stdcall;
Давай рассмотрим все параметры.
hWaveOut идентификатор устройства воспроизведения. Ты его получил после вызова функции waveOutOpen.
lpWaveOutHdr Указатель на структуру wavehdr_tag. Она уже должны быть специально подготовлена. Как это сделать см. ниже.
uSize Размер структуры wavehdr_tag.
Как видишь, параметры те же, что и у функции waveOutPrepareHeader. Теперь нам осталось только закрыть устройство:
Для С++
MMRESULT waveOutClose(
HWAVEOUT hwo
);
Для Delphi
function waveOutClose(
hWaveOut: HWAVEOUT
): MMRESULT; stdcall;
Давай рассмотрим все параметры.
hWaveOut идентификатор устройства воспроизведения. Ты его получил после вызова функции waveOutOpen.
Вот и всё. Пример мы рассмотрим в следующий раз, потому что ещё многое чего надо узнать: формат файла Wav из которого будет происходить загрузка данных и ещё несколько функций улучшающих воспроизведение.