В ходе работы над очередным проектом заказчик выразил пожелание добавить функционал обработки звука в реальном времени. Для этого было необходимо захватывать сигнал со звуковой карты, обрабатывать и отображать в интерфейсе результаты обработки, т.е. нужна realtime работа со звуком на ПК. Решение должно было быть кросс платформенным. Свой велосипед изобретать не было времени и желания. Поэтому был произведен поиск готовых решений для работы со звуком в интернете.

Там присутствовали следующие библиотеки:

Библиотека BASS отпала по причине платной лицензии для коммерческого использования. FMOD, кроме платности, не предоставляла нужный функционал. OpenAL ориентирована на игры. В итоге, выбор пал на PortAudio. К плюсам данной библиотеки можно отнести:

  • простой API,
  • удобную лицензию,
  • возможность работы под основными ОС.

Сборка

Для того чтобы начать работать с библиотекой необходимо собрать ее под нужную ОС. В моем случае это ОС семейства Windows, поэтому для сборки я использую компилятор MinGW. Что нужно сделать?

  1. Подготовить среду сборки. Для этого нужно скачать компилятор MinGW и среду MSYS. Установить, открыть shell MSYS и проверить работоспособность.
  2. Скачать исходники библиотеки с официального сайта. Распаковать их в папку с MSYS (для удобства). В моем случае это папка portaudio.
  3. Собрать библиотеку. Для этого запускаем 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. Если вам нужна работа со звуком на ПК подобного вида обращайтесь к нам.