Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Прикладное программирование на CC++ с нуля до мультимедийных и сетевых приложений
.pdf
Основы программирования звукозаписи 141
Прежде всего, заметим, что проигрывание звука в наших будущих приложениях на самом глубинном уровне производится для звуковой информации,
представленной в самом простейшем виде. Это — представление отсчетов звукового сигнала в форме импульсно-кодовой модуляции (РСМ). Как мы уже
знаем, основными параметрами импульсно-кодовой модуляции являются частота дискретизации, разрядность оцифровки и количество каналов. Эти и ряд
других характеристик должны быть переданы драйверу звуковой карты (или
другого устройства ввода звука) перед началом записи. Оцифрованный звуковой сигнал со звукового устройства сохраняется в специальном буфере в оперативной памяти. Сформированный буфер может быть превращен в звуковой
файл, записанный на жесткий диск, или непосредственно передан звуковой
карте для воспроизведения. При воспроизведении драйверу устройства также
должны быть переданы характеристики записанного звука и другие параметры. В этой связи в буфер и дисковый файл наряду со звуковыми данными записываются эти параметры.
Для формирования буфера записи в системе Windows API используются
специальные структуры, определения которых для приложений, написанных
на языке C/C++ (в Builder'å è Visual C++), находятся в заголовочном файле
mmsystem.h. Таким образом, разработчику нет необходимости самому создавать описание структур, а достаточно использовать готовые объекты.
Базовой будем называть предопределенную в указанном заголовке структуру PCMWAVEFORMAT:
typedef struct pcmwaveformat_tag {
WAVEFORMAT wf; // структура
WORD wBitsPerSample; // разрядность оцифровки
} PCMWAVEFORMAT;
Структура содержит два члена (поля). Второе поле wBitsPerSample задает
разрядность оцифровки, которая обычно составляет 8 или 16 бит. Первое
поле в свою очередь является предопределенной структурой, также описанной
в заголовке mmsystem.h:
typedef struct waveformat_tag {
WORD wFormatTag; // тип формата
WORD nChannels; // количество каналов (моно или стерео)
DWORD nSamplesPerSec; // частота дискретизации
DWORD nAvgBytesPerSec; // скорость потока данных
WORD nBlockAlign; // выравнивание блока данных
} WAVEFORMAT;
Первое поле задает тип формата звуковых данных. Для импульсно-кодовой модуляции, с которой мы и работаем, в этом поле должна быть задана
системная константа WAVE_FORMAT_PCM. Второе поле содержит 1 для монозаписи или 2 — для стерео. Третье поле — частота дискретизации — должно
содержать частоту оцифровки, выраженную в Герцах. В зависимости от звукового устройства и его драйвера эта величина может принимать стандартные
значения 11 025, 22 050, 44 100 Гц или произвольную величину, например,

142 Глава 4. Запись, обработка и воспроизведение звука
8000 Гц. Предпоследний член это тот самый битрайт, о котором мы говорили
в первой части книги. Напомним, что битрайт определяется как произведение
частоты оцифровки, количества каналов и разрядности оцифровки. Значение
в последнем поле вычисляется по формуле:
nBlockAlign = (nChannels * wBitsPerSample) / 8
Выравнивание необходимо для определения количества байтов в одном
отсчете оцифровки.
Использование двух представленных структур полностью обеспечивает запись звука в буфер оперативной памяти. Конкретная реализация будет показана в следующем разделе (рис. 4.4).
Ðèñ. 4.4
Теперь необходимо познакомиться с сохранением звука в дисковом файле, начав с рассмотрения организации wav-файла. Большинство мультимедийных файлов, в том числе и wav, относятся к так называемому внешнему формату RIFF, структура которого показана на рисунке выше. Файлы этого формата содержат две части: звуковые (или видео) данные и заголовок. Первый
раздел заголовка содержит слово FOURCC. Следующий раздел хранит размер
блока звуковых данных, выраженный в байтах. Область данных может иметь
наряду с непосредственно звуковыми отсчетами сложную структуру. Здесь содержится расшифровка подтипа RIFF-формата. Для wav-файла (следующий
рисунок) расшифровка представляет собой слово «WAVE». Необходимым элементом здесь является также слово «fmt» (с пробелом в конце). Возможны и
другие служебные слова, однако в наших задачах ничего больше в начале части данных не требуется. Далее следует подобласть с описанием формата данных — тех самых характеристик, которые содержатся в описанной выше
структуре PCMWAVEFORMAT. Наконец, после всех описаний в конце области данных находится служебное слово «data», размер непосредственно звуковых данных и байты отсчетов звуковой информации (рис. 4.5).
Для создания RIFF-файла используется структура MMCKINFO èç
mmsystem.h:
typedef struct _MMCKINFO
{
FOURCC ckid;
DWORD cksize;
FOURCC fccType;
DWORD dwDataOffset;
DWORD dwFlags;
} MMCKINFO;

Программы записи и воспроизведения звука 143
Ðèñ. 4.5
При формировании нового файла в первое поле заносится константа
MMIO_CREATERIFF. Второе поле содержит размер фрагмента в байтах, без
учета идентификатора фрагмента, поля длины фрагмента и дополнительных
байтов выравнивания, которые могут находиться в конце фрагмента. Третий
член — тип фрагмента. Здесь должно быть помещено значение WAVE в специальном формате, ÷òî достигается использованием особой функции
mmioFOURCC('W', 'A', 'V', 'E'). Остальные поля нам на первых порах не понадобятся.
Для сохранения записанного звука в файле предварительно в него заносятся все необходимые данные в соответствии с показанными выше двумя рисунками. В последнюю очередь в файл переписываются звуковые данные из
буфера в памяти. Применение структур, описанных выше, позволяет существенно упростить занесение информации в файл, что мы продемонстрируем в
следующем разделе.
Воспроизведение звукового файла требует операции, обратной по отношению к сохранению звука в файле. Создается блок в оперативной памяти,
фактически полностью повторяющий блок памяти для записи звука. В этот
блок переписывается информация из файла. Данные передаются звуковой
карте, которая на этом этапе является устройством вывода. Карта выполняет
свою работу, и в результате мы слышим звук, записанный в файле.
Программы записи и воспроизведения звука
Вооружившись представлениями о методике записи звука, приступим к
разработке первого своего серьезного приложения. Поставим себе следующую
задачу. В соответствии с интерфейсом, представленным на следующем рисунке, при нажатии кнопки START программа должна начать запись сигнала с
микрофона или другого источника, подключенного ко входу звуковой карты.
При нажатии кнопки STOP или при заполнении буфера запись останавливается. Кнопка SAVE инициирует сохранение записанного звукового фрагмента
в дисковый wav-файл. Кнопка PLAY открывает созданный файл и воспроиз
водит записанный фрагмент (рис. 4.6).
-

144 Глава 4. Запись, обработка и воспроизведение звука
Ðèñ. 4.6
В листинге, показанном ниже, приведен полный текст основного модуля
программы. Терпеливому читателю предлагается пройти по этому тексту в сопровождении подробных пояснений, приведенных после листинга.
#include <VCL.h>
#include <windows.h>
#include <mmsystem.h>
#include <windowsx.h>
#include <mmsystem.h>
#include "Unit1.h"
#pragma resource "*.dfm"
TForm1 *Form1;
int diskret=11025, // Частота дискретизации
razrad=16, // Разрядность оцифровки
kanal=1; // Число каналов
long buf=100000; // Размер блока звуковых данных
HWAVEIN hWaveIn; // Указатель устройства ввода
WAVEFORMATEX wfx;
LPWAVEHDR lpHdr = new WAVEHDR;
//————————————————————————————// Запись звуковых данных в буфер
//————————————————————————————void wioRecord(void)
{
lpHdr->lpData = (HPSTR)GlobalAllocPtr(GMEM_MOVEABLE | GMEM_SHARE, buf);
//Получаем память для блока звуковых данных
wfx.wFormatTag = WAVE_FORMAT_PCM;
int nC = wfx.nChannels = kanal;
int BPS = wfx.wBitsPerSample = razrad;
int nSPS = wfx.nSamplesPerSec = diskret;
int nBA = wfx.nBlockAlign = nC*BPS/8;
wfx.nAvgBytesPerSec = nSPS*nBA;
lpHdr->dwBufferLength = buf; //Заполняем заголовок блока
waveInOpen(&hWaveIn, WAVE_MAPPER,&wfx,0L, 0L, 0L);
//Открываем устройство записи

Программы записи и воспроизведения звука 145
waveInPrepareHeader(hWaveIn, lpHdr,sizeof(WAVEHDR));
//Подготавливаем блок
waveInAddBuffer(hWaveIn, lpHdr,sizeof(WAVEHDR));
//Передаем блок устройству
waveInStart(hWaveIn); //Запускаем запись
}
//————————————————————————————// Сохранение записанного звука в wav-файл
//————————————————————————————void wioFileSave(LPSTR szFileName)
{
char szdata[] = "data"; // Стандартное слово в заголовке
HMMIO hFile; // Дескриптор файла
MMCKINFO ck; // Структура для RIFF-файла
char szfmt[] = "fmt "; // Стандартное слово в заголовке
DWORD dwFmtSize = 16; // Размер форматного заголовка
// Создаем новый файл или перезаписываем существующий
hFile = mmioOpen(szFileName, NULL, MMIO_CREATE | MMIO_READWRITE);
// Создаем заголовок wav-файла
ck.ckid = MMIO_CREATERIFF;
ck.cksize = lpHdr->dwBytesRecorded+ sizeof(PCMWAVEFORMAT) + 20;
ck.fccType = mmioFOURCC('W', 'A', 'V', 'E');
mmioCreateChunk(hFile, (LPMMCKINFO)&ck,MMIO_CREATERIFF);
// Записываем фрагмент "fmt "
mmioWrite(hFile, (HPSTR)szfmt, 4);
mmioWrite(hFile, (HPSTR)&dwFmtSize, sizeof(DWORD));
mmioWrite(hFile, (HPSTR)&wfx,16); //sizeof(WAVEFORMAT));
mmioWrite(hFile, (HPSTR)szdata, 4);
mmioWrite(hFile, (HPSTR)&lpHdr->dwBytesRecorded, sizeof(DWORD));
// Записываем данные
mmioWrite(hFile,(HPSTR)lpHdr->lpData,lpHdr->dwBytesRecorded);
mmioClose(hFile, 0); // Закрываем файл
// Удаляем блок из очереди записи
waveInUnprepareHeader(hWaveIn,lpHdr,sizeof(WAVEHDR));
}
//——————————————————————
// Проигрываем файл
//——————————————————————
void FilePlay()
{
HFILE hf; // Дескриптор файла
HGLOBAL hWave; // Адрес в памяти
char *lpbuf; // Указатель на строку с адресом
DWORD dwFileSize; // Размер файла
BOOL rc; // Вспомогательная булева величина
hf = _lopen((LPSTR)"33.wav",OF_READ); // Открываем wav-файл
dwFileSize = _llseek(hf, 0l, FILE_END); // Определяем размер файла
_llseek(hf, 0l, FILE_BEGIN); // Переходим в начало файла
//Заказываем блок памяти, размер которого равен длине файла
hWave = GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE,dwFileSize);
lpbuf = (char *)GlobalLock(hWave); //Фиксируем блок в памяти

146 Глава 4. Запись, обработка и воспроизведение звука
_hread(hf, lpbuf, dwFileSize); //Читаем файл в полученный блок памяти
//Проигрываем звуковой фрагмент, загруженный в память
rc = sndPlaySound((LPCSTR)lpbuf,SND_MEMORY | SND_SYNC);
if(!rc)
{
MessageBeep(MB_ICONHAND);
MessageBox(NULL,"Не могу проиграть файл","SndPlay", MB_OK |
MB_ICONHAND);
}
//Расфиксируем и освобождаем память
GlobalUnlock(hWave);
GlobalFree(hWave);
_lclose(hf); // Закрываем файл
}
//—————————————————————————————————————__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//—————————————————————————————————————-
void __fastcall TForm1::Button1Click(TObject *Sender)
{
wioRecord();
}
//—————————————————————————————————————-
void __fastcall TForm1::Button2Click(TObject *Sender)
{
waveInReset(hWaveIn);
waveInClose(hWaveIn);
}
//—————————————————————————————————————-
void __fastcall TForm1::Button3Click(TObject *Sender)
{
wioFileSave("33.wav");
}
//—————————————————————————————————————-
void __fastcall TForm1::Button4Click(TObject *Sender)
{
FilePlay();
Close();
}
//—————————————————————————————————————-
В первых строках представлены директивы препроцессора, в том числе
предписывающие подключение к проекту некоторых заголовочных файлов.
Дается описание и инициализация глобальных переменных, содержащих частоту дискретизации, разрядность оцифровки, число каналов и размер (в бай-

Программы записи и воспроизведения звука 147
тах) блока звуковых данных. Здесь же создан дескриптор устройства ввода.
Структура wfx предопределенного типа WAVEFORMATEX предназначена для
хранения информации о типе звуковых данных. Указатель lpHdr указывает на
структуру типа WAVEHDR, в которой будет формироваться заголовок блока
звуковых данных.
Запись звука в блок оперативной памяти организована в форме функции
с именем wioRecord. Здесь с помощью функций GlobalAllocPtr распределяется
память для заголовка блока, блока звуковых данных и блока формата. Функции возвращают адреса блоков, которые приводятся к нужному типу указателей. Далее заполняются поля блока формата в структуре. В частности, как это
требуется для записи сигнала с импульсно-кодовой модуляцией, поле
wfx.wFormatTag получает значение константы WAVE_FORMAT_PCM, а выравнивание wfx.nBlockAlign вычисляется по приведенной в предыдущем разделе формулой. В структуру заголовка данных передается значение размера блока данных — глобальная переменная buf. Вызов функции waveInOpen открывает звуковое устройство ввода. Как можно видеть из перечня аргументов
функции, используется дескриптор устройства и блок формата из только что
заполненной структуры.
Блок данных должен быть подготовлен для записи, что делается с помощью функции waveInPrepareHeader, и передан устройству — функция
waveInAddBuffer. Наконец, непосредственно начало записи осуществляется
функцией waveInStart. Запись звука будет продолжаться до полного заполнения блока данных или может быть прервана функцией waveInReset.
Далее в модуле описана функция сохранения записанных в оперативную
память данных в дисковом звуковом файла — wioFileSave. Аргументами функции является строка, содержащая имя будущего файла. В начале тела функции
даны описания и инициализация нескольких локальных переменных. В строках-комментариях указано назначение этих переменных. Обратим внимание
на то, что здесь задается еще одна предопределенная структура MMCKINFO.
Мультимедийные файлы создаются функцией mmioOpen. После формирования файла инициализируются поля MMCKINFO. Подчеркнем то, что запись
данных в мультимедийный файл производится с помощью специализированной мультимедийной функции mmioWrite. Что и как записывается в файл, понятно из комментариев в программе. После помещения в файл звуковых отсчетов он закрывается. При записи звука в память мы подготавливали блок для
записи функцией waveInPrepareHeader. Теперь необходимо «разподготовить»
этот блок с использованием обратной функции waveInUnprepareHeader.
Следующая «самодельная» функция FilePlay в модуле выполняет воспроизведение звукового файла. Описан ряд локальных переменных, назначение
которых понятно из комментариев. Открывается файл с записью. Мы назовем
наш файл 33.wav, что и использовано в функции открытия _lopen. Конструкция _llseek(hf, 0l, FILE_END) «перематывает» файл в конец и возвращает число
прокрученных байтов, то есть размер файла. Далее с помощью той же конструкции файл прокручивается в начало. Определив размер файла, мы заказываем в памяти блок соответствующей длины — GlobalAlloc и фиксируем

148 Глава 4. Запись, обработка и воспроизведение звука
áëîê — GlobalLock. Скопировав содержимое файла в память (_hread), мы тем
самым создаем точно такой же блок, как и при записи с микрофона. Теперь
производится попытка воспроизведения блока функцией sndPlaySound.
Почему мы говорим: производится попытка? Потому, что в ряде случаев
могут возникнуть проблемы — об этом несколькими строками позднее. Булевская величина rc получает значение ИСТИНА (true), если воспроизведение
прошло успешно. В случае обнаружения неполадок срабатывает условная
конструкция, в которой на экран выдается сообщение о невозможности воспроизвести запись. Если все в порядке, работа функции заканчивается расфиксированием и освобождением памяти и закрытием файла.
Упомянутые проблемы могут возникнуть, и это не столь большая редкость, из-за неполной совместимости драйверов звуковых устройств с майкрософтовской системой звукозаписи. В частности, была обнаружена такая ситуация: при использовании некоторых звуковых карт, интегрированных на
материнской плате, представленная программа нормально работала, когда
производилась запись до полного заполнения блока, отведенного под данные.
Когда же запись прерывалась кнопкой STOP до заполнения блока, размер записанных данных определялся не верно — оставался равным 0. При этом и
возникала «авария». К сожалению, положение оказывается практически безвыходным. Остается только заменить звуковую карту или попытаться обновить драйвер устройства.
Первая программа звукозаписи сделана! Поэкспериментируйте с различными наборами разрядности оцифровки и частоты дискретизации и оцените
качество звука при разных вариантах. В представленном приложении изменение указанных величин необходимо делать в тексте программы и после этого
перекомпилировать задачу. Это, конечно, не удобно, но мы пока не стремились к разработке хорошего интерфейса приложения. Далее на оформление
интерфейса обратим большее внимание.
Следующая программа иллюстрирует запись звука в память и воспроизведение его прямо из памяти, минуя этап создания дискового файла. Такой вариант звукозаписи понадобится нам в дальнейшем при рассмотрении программирования IP-телефонии. Наряду с демонстрацией звукозаписи без использования файла, поставим себе задачу создания удобного пользовательского
интерфейса в окне приложения, обеспечивающего выбор параметров качества
записи. Окно программы после запуска должно выглядеть так, как это показано на рис. 4.7.
К оформлению интерфейса мы вернемся позднее, а пока рассмотрим
главный текстовый модуль, листинг которого представлен далее.
//—————————————————————————————————————-
#include <VCL.h>
#include <windows.h>
#include <windowsx.h>
#include <mmsystem.h>
#include "Unit1.h"
#pragma resource "*.dfm"

Программы записи и воспроизведения звука 149
Ðèñ. 4.7
TForm1 *Form1;
WAVEFORMATEX wfx;
LPWAVEHDR lpHdr = new WAVEHDR;
int tau, //Максимальная длительность
diskret, //Частота дискретизации
razrad=8, //Разрядность оцифровки
kanal=1; //Число каналов
long buf; //Размер буфера
HWAVEIN hWaveIn; //Указатель устройства вводя
HWAVEOUT hWaveOut; //Указатель устройства вывода
//————————————————————————————__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
ListBox1->ItemIndex = 0;
}
//—————————————————————————————————————-
void __fastcall TForm1::Button1Click(TObject *Sender)
{
if(RadioGroup1->ItemIndex == 1) kanal = 2;
if(RadioGroup2->ItemIndex == 1) razrad = 16;
tau = StrToInt(Edit1->Text);
switch (ListBox1->ItemIndex)
{
case 0: diskret = 8000; break;
case 1: diskret = 11025; break;
case 2: diskret = 22050; break;
case 3: diskret = 44100; break;
}
buf = kanal*razrad/8*tau*diskret;

150 Глава 4. Запись, обработка и воспроизведение звука
lpHdr->lpData = (HPSTR)GlobalAllocPtr(GMEM_MOVEABLE | GMEM_SHARE, buf);
// Получаем память для блока звуковых данных
wfx.wFormatTag = WAVE_FORMAT_PCM;
int nC = wfx.nChannels = kanal;
int BPS = wfx.wBitsPerSample = razrad;
int nSPS = wfx.nSamplesPerSec = diskret;
int nBA = wfx.nBlockAlign = nC*BPS/8;
wfx.nAvgBytesPerSec = nSPS*nBA;
lpHdr->dwBufferLength = buf; //Заполняем заголовок блока
waveInOpen(&hWaveIn, WAVE_MAPPER,&wfx,0L, 0L, 0L);
//Открываем устройство записи
waveInPrepareHeader(hWaveIn, lpHdr,sizeof(WAVEHDR));
//Подготавливаем блок
waveInAddBuffer(hWaveIn, lpHdr,sizeof(WAVEHDR));
//Передаем блок устройству
waveInStart(hWaveIn); //Запускаем запись
}
//—————————————————————————————————————-
void __fastcall TForm1::Button2Click(TObject *Sender)
{
waveInReset(hWaveIn);
waveInClose(hWaveIn);
waveInUnprepareHeader(hWaveIn, lpHdr,sizeof(WAVEHDR));
lpHdr->dwBufferLength = lpHdr->dwBytesRecorded;
}
//—————————————————————————————————————-
void __fastcall TForm1::Button3Click(TObject *Sender)
{
waveOutOpen(&hWaveOut, WAVE_MAPPER,&wfx,0L,0L,0L);
waveOutPrepareHeader(hWaveOut,lpHdr,sizeof(WAVEHDR));
waveOutWrite(hWaveOut,lpHdr,sizeof(WAVEHDR));
waveOutUnprepareHeader(hWaveOut,lpHdr,sizeof(WAVEHDR));
waveOutClose(&hWaveOut);
}
//—————————————————————————————————————-
void __fastcall TForm1::Button4Click(TObject *Sender)
{
Close();
}
//—————————————————————————————————————-
Ничего принципиально нового по части записи и воспроизведения звука
по сравнению с предыдущей программой здесь нет. В то же время структура
программы существенно изменена организационно. Здесь мы отказались от
оформления отдельных функций записи в память, сохранения в файл (такового здесь вообще нет) и воспроизведения. Все операции по формированию блока в оперативной памяти и записи в него звука сосредоточены в обработчике
нажатия на первую кнопку. В функцию-обработчик нажатия на вторую кноп
-
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
