IP-телефония — практические подходы к реализации схемы PC—PC

(Окончание. Начало в № 2-4’2001)

С.В. Захаров

Занятие 4. Управление звуковыми устройствами

   Как считать и записать звуковые данные?

   Чтение данных с ЗУ

   Запись данных в ЗУ

Занятие 4. Управление звуковыми устройствами

Как считать и записать звуковые данные?

Итак, вы уже знаете, как можно достать из реестра необходимую для нас информацию, в частности определить ваш IP-адрес и на основе ее организовать просмотр сети, а также как установить клиентскую и серверную части и организовать передачу данных с выбранным вами абонентом.

Источником данных у нас служил заранее подготовленный буфер, содержащий в себе текстовую информацию. Теперь в качестве источника и получателя данных у нас выступит звуковое устройство (ЗУ), а точнее Waveaudio-устройство. Чтение и запись с/на ЗУ будем производить в виде WAV-данных (оцифрованный звук).

В начало В начало

Чтение данных с ЗУ

Для того чтобы считать данные с звукового устройства, необходимо для системы мультимедиа описать формат считываемых данных, ведь WAV-данные можно представить в различных форматах, отличающихся частотой дискретизации, количеством каналов и разрешением (количеством битов на один отчет).

Прежде всего опишем необходимые для нас переменные и установим формат звуковых данных, для чего инициализируем структуру WAVEFORMATEX  (листинг 1).

HGLOBAL      waveDataBlockIN; 
HGLOBAL     waveDataBlockINtwo; 
  
WAVEFORMATEX     PCMWaveFmtRecord; 
  
char      * pWaveIN; 
char      * pWaveINttwo; 
  
WAVEHDR      WaveHeaderIN; 
WAVEHDR     WaveHeaderINtwo; 
  
PCMWaveFmtRecord.wFormatTag           =1; // РСМ Wave 
PCMWaveFmtRecord.nChannels               =1; // Канал 
PCMWaveFmtRecord.nSamplesPerSec  =22050;// Частота  
PCMWaveFmtRecord.nAvgBytesPerSec=22050;// Байтов в сек.  
PCMWaveFmtRecord.nBlockAlign           =1; //Байтов на отсчет 
PCMWaveFmtRecord.wBitsPerSample  =8;    //Разрядность 
PCMWaveFmtRecord.cbSize                    =0; //Размер доп. инф. 

Листинг 1. Описание переменных и структур

 

Для чтения WAV-данных с ЗУ необходимо выделить память. Для этой цели используется функция GlobalAlloc() (листинг 2), имеющая два параметра. Первый — это флаг, определяющий режим выделения памяти. Мы используем GHND, который позволит Windows произвести инициализацию и перемещения в выделенную область памяти. Второй параметр показывает необходимое нам количество байтов памяти (в нашем примере буфер содержит в себе 1 с  записи). Далее запираем область памяти и получаем указатель на нее. Создав первый блок памяти для записи в него данных, перейдем к созданию второго.

waveDataBlockIN=GlobalAlloc(GHND,22050); 
if (waveDataBlockIN==NULL)      {< СТОП.>};  
char* pWaveIN=(char*)GlobalLock(waveDataBlockIN); 
  
waveDataBlockINtwo=GlobalAlloc(GHND,lbuff); 
if (waveDataBlockINtwo==NULL){ {< СТОП.>}; 
char* pWaveINtwo=(char*)GlobalLock(waveDataBlockINtwo); 

Листинг 2. Выделение памяти для WAV-данных

Далее нам необходимо выполнить следующие шаги:

  1. Открыть устройствоWaveaudio.
  2. Подготовить заголовок первого сегмента.
  3. Передать первый буфер ЗУ.
  4. Подготовить заголовок второго сегмента.
  5. Передать второй буфер ЗУ.
  6. Запустить процесс чтения.

Начнем с открытия открываем Waveaudio-устройства. Для этого воспользуемся функцией waveInOpen() (листинг 3), которая имеет шесть параметров. Первому параметру hWaveIN функция присваивает адрес свободной ссылки на устройство. Эта ссылка используется при всех операциях с устройством. Второй параметр WAVE_MAPPER=-1 указывает, что мы будем открывать первое свободное устройство. Третий — передает адрес структуры с информацией о формате данных. Остальные параметры определяют функцию возврата и необходимую для нее информацию.

if(waveInOpen(&hWaveIN,WAVE_MAPPER, 
          &PCMWaveFmtRecord,0L,0L,0L)!=  
           MMSYSERR_NOERROR)  
{<СТОП>} 
  
WaveHeaderIN.lpData       =pWaveIN; 
WaveHeaderIN.dwBufferLength    =22050; 
WaveHeaderIN.dwFlags     =0L; 
WaveHeaderIN.dwLoops      =0L; 
WaveHeaderIN.dwBytesRecorded =0L; 
  
if(waveInPrepareHeader(hWaveIN,&WaveHeaderIN, 
    sizeof(WaveHeaderIN))!=MMSYSERR_NOERROR) 
{ <СТОП>} 
  
if(waveInAddBuffer(hWaveIN,&WaveHeaderIN, 
    sizeof(WaveHeaderIN))!=MMSYSERR_NOERROR) 
{ <СТОП>} 
  
WaveHeaderINtwo.lpData         =pWaveINtwo;  
WaveHeaderINtwo.dwBufferLength   =22050; 
WaveHeaderINtwo.dwFlags                =0L; 
WaveHeaderINtwo.dwLoops                  =0L; 
WaveHeaderINtwo.dwBytesRecorded =0L; 
  
if(waveInPrepareHeader(hWaveIN,&WaveHeaderINtwo, 
     sizeof(WaveHeaderINtwo))!=MMSYSERR_NOERROR) 
{ <СТОП>} 
  
if(waveInAddBuffer(hWaveIN,&WaveHeaderINtwo 
     sizeof(WaveHeaderINtwo))!=MMSYSERR_NOERROR) 
{ <СТОП>} 
  
if(waveInStart(hWaveIN)!=MMSYSERR_NOERROR) 
{ <СТОП>} 

Листинг 3. Подготовка ЗУ к чтению и чтение данных

Следующий наш шаг — подготовка заголовка сегмента WAVEHDR (эта структура описывает отдельный WAV-буфер) и передача входного буфера ЗУ. Когда буфер будет заполнен, приложение получит уведомление. Выполним те же операции и со вторым сегментом.

Теперь нам остается только запустить процесс чтения waveInStart(). Поступающие данные будут записываться в переданные ЗУ буфера. Помните, что при заполнении всех буферов, ввод данных не прекратится, а будет продолжаться до тех пор, пока не будет выполнена функция остановки waveInReset(). Следовательно, данные будут потеряны. Как же избежать потери данных и организовать их передачу?

Обратите внимание, что я использую два буфера для чтения данных. Пока заполняется один из буферов (в нашем примере — 1 с), можно произвести необходимые манипуляции со вторым. Пример реализации данной схемы представлен на листинге 4.

while (<УСЛОВИЕ ВЫХОДА>) 
{ 
do{} 
while (!(WaveHeaderIN.dwFlags&WHDR_DONE)); 
send(<SOCKET>,WaveHeaderIN.lpData, 
        WaveHeaderIN.dwBytesRecorded, 0);  
WaveHeaderIN.dwFlags=0L; 
waveInPrepareHeader(hWaveIN,&WaveHeaderIN, 
         sizeof(WaveHeaderIN)); 
waveInAddBuffer(hWaveIN,&WaveHeaderIN, 
        sizeof(WaveHeaderIN)); 
  
do{} 
while (!(WaveHeaderINtwo.dwFlags&WHDR_DONE)); 
send(<SOCKET>,WaveHeaderINtwo.lpData, 
       WaveHeaderIN.dwBytesRecorded, 0); 
WaveHeaderINtwo.dwFlags=0L; 
waveInPrepareHeader(hWaveIN,&WaveHeaderINtwo, 
        sizeof(WaveHeaderINtwo)); 
waveInAddBuffer(hWaveIN,&WaveHeaderINtwo, 
         sizeof(WaveHeaderINtwo));  
} 

Листинг 4. Организация непрерывного  чтения данных с ЗУ и его передача удаленному пользователю

Для непрерывного чтения данных организуем цикл, выход из которого осуществляется по команде пользователя. Выполнение цикла начинается c ожидания сообщения от системы мультимедиа о заполнении первого буфера. Заполнив первый буфер, система автоматически начинает заполнять второй. И пока это происходит, считанные в первый буфер данные отправляются удаленному пользователю send(). После завершения передачи данных возвращаем данный сегмент памяти обратно в ЗУ — для последующего повторного использования.

Выполнив эти действия, перейдем в режим ожидания сообщения от системы мультимедиа о заполнении второго буфера, а система в это время заполняет возвращенный нами первый. Второй буфер передаем удаленному пользователю, так же как ранее первый, после чего происходит возврат его ЗУ. Схема повторяется раз за разом  — до тех пор, пока пользователь не прервет ее.

В конце работы с ЗУ не забудьте восстановить начальные данные на листинге 5.

waveInReset(hWaveIN); 
waveInUnprepareHeader(hWaveIN,&WaveHeaderIN, 
          sizeof(WaveHeaderIN));  
waveInUnprepareHeader(hWaveIN,&WaveHeaderINtwo, 
           sizeof(WaveHeaderINtwo));  
waveInClose(hWaveIN); 
GlobalFree(waveDataBlockIN); 
GlobalFree(waveDataBlockINtwo); 

Листинг 5. Восстановление параметров

 

В начало В начало

Запись данных в ЗУ

Последовательность шагов инициализации ЗУ на чтение и запись одинакова.

Вначале опишем необходимые для нас переменные и установим формат звуковых данных, для чего инициализируем структуру WAVEFORMATEX  (листинг 6).

HGLOBAL      MemOut; 
HGLOBAL     MemOuttwo; 
HGLOBAL     waveDataBlockOUT; 
  
WAVEFORMATEX     PCMWaveFmtRecord; 
  
char      * pWaveOut; 
char      * pWaveOuttwo; 
  
WAVEHDR      WaveHeaderOUT; 
WAVEHDR     WaveHeaderOUTtwo; 
  
PCMWaveFmtRecord.wFormatTag        =1; //    РСМ Wave 
PCMWaveFmtRecord.nChannels               =1; // Канал 
PCMWaveFmtRecord.nSamplesPerSec  =22050;// Частота 
PCMWaveFmtRecord.nAvgBytesPerSec=22050;//Байтов в сек. 
PCMWaveFmtRecord.nBlockAlign           =1; //Байтов на отсчет 
PCMWaveFmtRecord.wBitsPerSample  =8; //Разрядность 
PCMWaveFmtRecord.cbSize                    =0; //Размер доп. инф. 

Листинг 6. Описание переменных и структур

Если вы хотите прослушать у себя звуковые данные в том виде, как они передавались, то не допускайте различий в структурах WAVEFORMATEX .

Например, если в функции чтения установлено поле:

PCMWaveFmtRecord.nSamplesPerSec  = 11025;  
PCMWaveFmtRecord.nAvgBytesPerSec=11025;   

а при записи — поле:

PCMWaveFmtRecord.nSamplesPerSec  = 22050;  
PCMWaveFmtRecord.nAvgBytesPerSec=22050;    

то вы вполне сможете оценить всю прелесть ускоренного воспроизведения.

Создадим два блока памяти для WAV-данных, которые впоследствии будут передаваться на воспроизведение ЗУ  (листинг 7).

MemOut =GlobalAlloc(GHND,22050); 
if (MemOut = =NULL) {< СТОП >}; 
char* pWaveOut =(char*)GlobalLock(MemOut); 
  
MemOuttwo =GlobalAlloc(GHND,lbuff); 
if (MemOuttwo = =NULL){ {< СТОП >}; 
char* pWaveOuttwo =(char*)GlobalLock(MemOuttwo); 

Листинг 7. Выделение памяти для WAV-данных

Следующий шаг — инициализируем часть заголовка сегмента WAVEHDR. Это нам будет необходимо при реализации алгоритма передачи буфера на воспроизведение звуковому устройству.

Присвоив полю WaveHeaderOUTtwo.dwFlags значение 3, мы отметили буфер как уже воспроизведенный.

Далее открываем Waveaudio-устройство. Это делается с помощью функции waveOutOpen() (листинг 8), которая имеет шесть параметров, имеющих то же значение, что и для функции waveInOpen().

WaveHeaderOUT.dwFlags       =0L; 
WaveHeaderOUT.dwLoops       =0L; 
WaveHeaderOUT.dwBytesRecorded   =0L; 
  
WaveHeaderOUTtwo.dwFlags        =3; 
WaveHeaderOUTtwo.dwLoops        =0L; 
WaveHeaderOUTtwo.dwBytesRecorded     =0L; 
  
if(waveOutOpen(&hWaveOUT,WAVE_MAPPER, 
            &PCMWaveFmtRecord,0L,0L,0L)!=  
           MMSYSERR_NOERROR)  
{<СТОП>} 

Листинг 8. Открыть устройство на воспроизведение

Для непрерывного воспроизведения данных мы организуем цикл, выход из которого осуществляется по команде пользователя (листинг 9).

while (<УСЛОВИЕ ВЫХОДА>) 
{ 
int mSlide1=0;  // счетчик считанных данных 
int mSlide2=0;  // счетчик считанных данных 
  
UINT rv=1; 
while ((lenBuf-mSlide1!=0)&&(rv!=-1)&&(rv!=0)) 
{ 
rv=recv(<SOCKET>,pWaveOut+mSlide1, 
       lenBuf-mSlide1,0); 
   mSlide1=mSlide1+rv; 
} 
if (rv==SOCKET_ERROR)     <СТОП>; 
  
WaveHeaderOUT.lpData=pWaveOut; 
WaveHeaderOUT.dwBufferLength=mSlide1; 
waveOutPrepareHeader(hWaveOUT,&WaveHeaderOUT, 
          sizeof(WaveHeaderOUT));  
waveOutWrite(hWaveOUT,&WaveHeaderOUT, 
           sizeof(WaveHeaderOUT));  
do{} 
while (!(WaveHeaderOUTtwo.dwFlags&WHDR_DONE)); 
WaveHeaderOUTtwo.dwFlags=0L; 
  
rv=1; 
while ((lenBuf-mSlide2!=0)&&(rv!=-1)&&(rv!=0)) 
{ 
rv=recv(<SOCKET>,pWaveOuttwo+mSlide2, 
        lenBuf-mSlide2,0); 
   mSlide2=mSlide2+rv; 
} 
if (rv==SOCKET_ERROR)     <СТОП>; 
  
WaveHeaderOUTtwo.lpData=pWaveOuttwo; 
WaveHeaderOUTtwo.dwBufferLength=mSlide2; 
waveOutPrepareHeader(hWaveOUT,&WaveHeaderOUTtwo,  
            sizeof(WaveHeaderOUTtwo)); 
waveOutWrite(hWaveOUT,&WaveHeaderOUTtwo, 
         sizeof(WaveHeaderOUTtwo));  
do{} 
while (!(WaveHeaderOUT.dwFlags&WHDR_DONE)); 
WaveHeaderOUT.dwFlags=0L; 
} 

Листинг 9. Функция непрерывного воспроизведения

Первым нашим шагом будет получение от удаленного пользователя данных для воспроизведения. Для получения данных воспользуемся функцией recv(). Полученными данными мы полностью заполняем организованный нами первый буфер pWaveOut. После этого до конца описываем заголовок сегмента WAVEHDR и производим его назначение. Подготовленный буфер, используя функцию waveOutWritе, передаем на воспроизведение ЗУ.

Далее выполнение приостанавливается  do{} while(<Условие>) до тех пор, пока данные из второго буфера не будут полностью воспроизведены. Идея состоит в том, что в любое время количество буферов, переданных на воспроизведение, не должно превышать двух. Таким образом исключается возможность переполнения.

При первом выполнении цикла второй буфер пуст, поэтому остановка в этом месте не произойдет. Затем по той же схеме, как у первого буфера, заполняем второй буфер и отправляем его на воспроизведение. Приостанавливаем выполнение до тех пор, пока данные из первого буфера не будут воспроизведены полностью.

И вот мы в конце цикла. За один проход цикла мы приняли и озвучили две — с WAV-данных установленного формата. А удаленный пользователь за это время передал те же 2 с.

 

Связь установлена!

КомпьютерПресс 5'2001