Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Прикладное программирование на CC++ с нуля до мультимедийных и сетевых приложений

.pdf
Скачиваний:
0
Добавлен:
07.09.2026
Размер:
2 Мб
Скачать
☆
Основы программирования звукозаписи 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();
} //—————————————————————————————————————-
Ничего принципиально нового по части записи и воспроизведения звука по сравнению с предыдущей программой здесь нет. В то же время структура программы существенно изменена организационно. Здесь мы отказались от оформления отдельных функций записи в память, сохранения в файл (таково­го здесь вообще нет) и воспроизведения. Все операции по формированию бло­ка в оперативной памяти и записи в него звука сосредоточены в обработчике нажатия на первую кнопку. В функцию-обработчик нажатия на вторую кноп
-