В ходе работы над очередным проектом заказчик выразил пожелание добавить функционал обработки звука в реальном времени. Для этого было необходимо захватывать сигнал со звуковой карты, обрабатывать и отображать в интерфейсе результаты обработки, т.е. нужна realtime работа со звуком на ПК. Решение должно было быть кросс платформенным. Свой велосипед изобретать не было времени и желания. Поэтому был произведен поиск готовых решений для работы со звуком в интернете.
Там присутствовали следующие библиотеки:
Библиотека BASS отпала по причине платной лицензии для коммерческого использования. FMOD, кроме платности, не предоставляла нужный функционал. OpenAL ориентирована на игры. В итоге, выбор пал на PortAudio. К плюсам данной библиотеки можно отнести:
- простой API,
- удобную лицензию,
- возможность работы под основными ОС.
Сборка
Для того чтобы начать работать с библиотекой необходимо собрать ее под нужную ОС. В моем случае это ОС семейства Windows, поэтому для сборки я использую компилятор MinGW. Что нужно сделать?
- Подготовить среду сборки. Для этого нужно скачать компилятор MinGW и среду MSYS. Установить, открыть shell MSYS и проверить работоспособность.
- Скачать исходники библиотеки с официального сайта. Распаковать их в папку с MSYS (для удобства). В моем случае это папка portaudio.
- Собрать библиотеку. Для этого запускаем shell, переходим в папку с PortAudio, даем команду конфигурации ./configure. По умолчанию собирается конфигурация для WMME. Если вам необходима другая конфигурация, используйте флаг -with-winapi. Далее подаем команду make. После завершения сборки бинарные файлы библиотеки находятся в папке lib.
Для работы с библиотекой из вашей программы будут нужны следующие файлы:
- portaudio.h,
- libportaudio.a,
- libportaudio.dll.
Работа
Работать с библиотекой достаточно просто, интерфейс построен на вызове функций. Обмен аудиоданными осуществляется с помощью callback функций. До начала работы необходимо вызвать функцию инициализации библиотеки Pa_Initialize():
//список обнаруженных устройств
vector<DeviceDesc> devices;
bool Init()
{
devices.clear();
curError=Pa_Initialize();
isInitialized=(curError!=paNoError) ? false : true;
return isInitialized;
}
Теперь самое время определить имеющиеся звуковые устройства. Для этого нужно произвести их поиск. Количество устройств возвращает функция Pa_GetDeviceCount(), а функция Pa_GetDeviceInfo() возвращает информацию об устройстве в виде структуры PaDeviceInfo. Ниже приведен пример функции, выполняющей поиск:
//создаем список устройств
bool Enumerate()
{
bool res=false;
if(!isInitialized) return res;
int numDevices = Pa_GetDeviceCount();
if( numDevices < 0 )
{
//устройства не найдены
}
else
{
//заполним список
const PaDeviceInfo *deviceInfo;
for(int i=0; i < numDevices; i++ )
{
deviceInfo = Pa_GetDeviceInfo( i );
//здесь можно поставить фильтрацию чтобы включать в список только подходящие устройства
//сейчас включаем в список все что есть
DeviceDesc device(i,deviceInfo->name,
deviceInfo->defaultHighInputLatency);
devices.push_back(device);
}
if(devices.isEmpty())
{
//нет подходящих устройств
}
else res=true;
}
return res;
}
Сам обмен аудиоданными происходит посредством вызова библиотекой callback функции audioEngineCallback (в примере показан только захват данных с выбранного источника):
static int audioEngineCallback( const void *inputBuffer, void *outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo* timeInfo,
PaStreamCallbackFlags statusFlags,
void *userData )
{
//копируем данные в очередь для последующей обработки
const float* ptr=static_cast(inputBuffer);
int bufSize=(framesPerBuffer>PA_BUFFER_SIZE) ? PA_BUFFER_SIZE : framesPerBuffer;
static AudioData data;
for(int i=0;i<bufSize;i++)
data.samples[i]=ptr[i];
audioDataQueue.push(data);
return paContinue;
}
Для запуска процесса обмена аудиоданными нужно открыть устройство и запустить поток. При этом библиотека portaudio начнет вызывать функцию audioEngineCallback. Пример кода:
bool Open(int index)
{
int devID(0);
if(!isInitialized)
{
return false;
}
//если есть работающий поток закроем его чтобы не мешал
if (stream) Close();
devID=GetDeviceIDByIndex(index);
if(devID<0)
{
return false;
}
//тут настраиваем параметры потока - в этом примере используем только вход
PaStreamParameters inputParameters;
memset(&inputParameters,0,sizeof(inputParameters));
inputParameters.channelCount = PA_NUM_CHANNELS;
inputParameters.device = devID;
inputParameters.sampleFormat = paFloat32;
inputParameters.suggestedLatency = devInfo->defaultLowInputLatency;
curError = Pa_OpenStream(
&stream,
&inputParameters,
NULL,
PA_SAMPLE_RATE,
PA_BUFFER_SIZE,
paNoFlag,
audioEngineCallback,
NULL);
if(curError!=paNoError)
{
stream=NULL;
return false;
}
//начинаем работу
curError=Pa_StartStream(stream);
if(curError!=paNoError)
{
Close();
return false;
}
isActive=true;
return true;
}
Для завершения процесса обмена необходимо остановить поток и закрыть устройство. Пример кода:
void Close()
{
if(!isInitialized||stream==NULL)
{
return;
}
if(isActive)
{
isActive=false;
curError=Pa_StopStream(stream);
}
curError=Pa_CloseStream(stream);
stream=NULL;
}
Если библиотека вам больше не нужна, нужно освободить ресурсы, используя функцию Pa_Terminate():
void Finish()
{
//закрываем поток
if (stream) Close();
//и библиотеку
curError = Pa_Terminate();
}
Полностью исходный код примера работы с библиотекой PortAudio можно скачать по ссылке Пример работы с PortAudio. Если вам нужна работа со звуком на ПК подобного вида обращайтесь к нам.