Как непрерывно воспроизводить звуковые данные? Как написать программу потокового звукового вещания? С подобными вопросами дело имеют многие (и откуда я это знаю…). Да к чему эти интриги? Я же в первую очередь говорю о себе! Может читателю вообще будет не интересно читать этот монолог:) И вообще, будет замечательно если найдется хоть один, кому будет интересна эта статья. Так вот, в первую очередь именно мне было интересно воспроизвести произвольные непрерывные данные на звуковой карте моего лэптопа. В этом изложении я постараюсь вкратце описать, каким образом можно это осуществить. Немного предыстории. Начав искать в Интернете способы вывода звука в звуковую карту, я перепробовал многие библиотеки. В первую очередь я уделял внимание выбору именно кроссплатформенного решения. Ведь не сильно хочется полностью переписывать программу, если вдруг придется изменить операционную систему, на которой эта программа должна работать. В результате мой выбор пал на библиотеку «Portaudio». Эта замечательная библиотека позволяет воспроизводить звуковые данные под управлением таких операционных систем как Винда, Мак и Юних т.е. является кроссплатформенной. Ко всему «Portaudio» можно использовать и в коммерческих проектах.

В качестве примера работы с  «Portaudio» рассмотрим генератор синусоидального сигнала. Для начала нужно где-то достать эту самую библиотеку. Я смог убедится в том, что бинарные файлы этой библиотеки распространять не принято. Но не составит особого труда самому собрать эти файлы из исходных файлов библиотеки. На сегодняшний день (день написания статьи) исходный код можно скачать по адресу http://www.portaudio.com/download.html. Для того, чтобы успешно собрать библиотеку придется попотеть (конечно же, я говорю о сборке библиотеки под “суперудобной” WindowsOS). Для начала придется найти Readme.txt файл по приблизительному пути …\portaudio\build\msvc\. Потом всё это хорошенько почитать и всё получится. Дабы долго не томить читателя излишним изложением бестолковых предложений и словосочетаний, собранные файлы библиотеки я прикрепляю в проект в конце данной статьи. Перейдем к коду. Я решил использовать уже готовые наработки по работе с «Portaudio», которые можно найти в самом проекте библиотеки. Чуть-чуть переделав (упростив) файл примера я получил следующий исходный код:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#include <math.h>
#include <stdio.h>
#include <iostream>
 
#include "portaudio.h"
 
#define SAMPLE_RATE (44100)
#define FREQUENCY   (1000)
 
#ifndef M_PI
#define M_PI    (3.14159265358979323846)
#endif
 
void HandleError(PaError &err)
{
    Pa_Terminate();
 
    printf("An error occured while using the portaudio stream\n");
    printf("Error number: %d\n", err);
    printf("Error message: %s\n", Pa_GetErrorText(err));
 
    exit(err);
}
 
///* This routine will be called by the PortAudio engine when audio is needed.
//** It may called at interrupt level on some machines so don't do anything
//** that could mess up the system like calling malloc() or free().
//*/
static int patestCallback(const void*                     inputBuffer,
                          void*                           outputBuffer,
                          unsigned long                   framesPerBuffer,
                          const PaStreamCallbackTimeInfo* timeInfo,
                          PaStreamCallbackFlags           statusFlags,
                          void*                           userData)
{
    float *out = (float*)outputBuffer;
 
    static unsigned int timeTmp = 0;
    float sampleValTmp = 0;
 
    for(unsigned int i=0; i<framesPerBuffer; i++)
    {
        sampleValTmp = (float)sin(2.0*M_PI*FREQUENCY*(timeTmp++)/SAMPLE_RATE);
 
        *out++ = sampleValTmp; /* left channel */
        *out++ = sampleValTmp; /* right channel */
    }
 
    return 0;
}
 
int main(void)
{
    PaError err;
    PaStream *stream;
    PaStreamParameters outputParameters;
 
    printf("PortAudio Test: output sine wave.\n");
 
    err = Pa_Initialize();
 
    if(err != paNoError)
        HandleError(err);
 
    outputParameters.device = Pa_GetDefaultOutputDevice(); /* default output device */
 
    if(outputParameters.device == paNoDevice)
    {
        printf("Error: No default output device.\n");
 
        HandleError(err);
    }
 
    outputParameters.channelCount = 2;                     /* stereo output */
    outputParameters.sampleFormat = paFloat32;             /* 32 bit floating point output */
    outputParameters.suggestedLatency = Pa_GetDeviceInfo(outputParameters.device)->defaultHighOutputLatency;
    outputParameters.hostApiSpecificStreamInfo = NULL;
 
    err = Pa_OpenStream(&stream,
        NULL,              /* No input. */
        &outputParameters, /* As above. */
        SAMPLE_RATE,
        256,               /* Frames per buffer. */
        paClipOff,         /* No out of range samples expected. */
        patestCallback,
        NULL);
 
    if(err != paNoError)
        HandleError(err);
 
    err = Pa_StartStream(stream);
 
    if(err != paNoError)
        HandleError(err);
 
    printf("Hit ENTER to stop program.\n");
 
    getchar();
 
    err = Pa_CloseStream(stream);
 
    if(err != paNoError)
        HandleError(err);
 
    Pa_Terminate();
 
    printf("Generation finished.\n");
 
    return err;
}

Начинающему не сразу станет ясно, что именно здесь происходит. Поэтому я объясню. В первую очередь нужно инициализировать библиотеку, вызвав функцию Pa_Initialize().

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

Если устройство получено без ошибки, тогда можно приступить к его настройке. Для этого нужно заполнить структуру outputParameters.

Когда всё это сделано, можно переходить к самому интересному, а именно к открытию (Pa_OpenStream(…)) и запуску (Pa_StartStream(…)) потока воспроизведения.

Наверное, на данном этапе не совсем понятно, куда и где мы будем передавать наши звуковые выборки. Тут всё просто… Открывая поток вызовом функции Pa_OpenStream(…) мы передаем в эту функцию указатель на функцию patestCallback(…), которую будет дёргать драйвер аудиоустройства (пусть это звучит так) когда ему нужны новые данные. Так вот, в этой функции мы и будем генерировать и передавать драйверу наши аудиовыборки. Как генерировать и записывать выборки в левый и правый каналы, можно посмотреть тут:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
///* This routine will be called by the PortAudio engine when audio is needed.
//** It may called at interrupt level on some machines so don't do anything
//** that could mess up the system like calling malloc() or free().
//*/
static int patestCallback(const void*                     inputBuffer,
                          void*                           outputBuffer,
                          unsigned long                   framesPerBuffer,
                          const PaStreamCallbackTimeInfo* timeInfo,
                          PaStreamCallbackFlags           statusFlags,
                          void*                           userData)
{
    float *out = (float*)outputBuffer;
 
    static unsigned int timeTmp = 0;
    float sampleValTmp = 0;
 
    for(unsigned int i=0; i<framesPerBuffer; i++)
    {
        sampleValTmp = (float)sin(2.0*M_PI*FREQUENCY*(timeTmp++)/SAMPLE_RATE);
 
        *out++ = sampleValTmp; /* left channel */
        *out++ = sampleValTmp; /* right channel */
    }
 
    return 0;
}

Здесь мы генерируем выборку функцией синуса из стандартной математической библиотеки. Значение timeTmp постоянно увеличивается до тех пор, пока не перешагнет максимальное для себя значение и опять станет нулём. Каналы в буфере чередуются один за другим для каждого семпла. В данном случае мы имеем только 2 канала, поэтому и сдвигаем указатель out на буфер только 2 раза.

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

Вот собственно и всё, что необходимо знать при работе с библиотекой «Portaudio» на начальном этапе!

В завершение, я хотел бы сказать, что это всего-лишь пример и некоторые вещи делать так как в этом проекте нежелательно, например выделять или удалять память внутри функции patestCallback(…). Кроме этого делать громоздкие вычисления тут тоже нежелательно. Конкретно в нашем случае, в случае этого примера я знаю, что за время между соседними вызовами функции patestCallback(…) мы однозначно успеваем выполнить расчет значения для новой выборки. Если же нужно делать более громоздкие вычисления, тогда можно воспользоваться логикой многопоточных приложений и, например, буфером FIFO. Но это уже совсем другая история…

Мне остаётся самое главное — выложить исходный код проекта:). Вот он, дорогия друзья:

Загрузка

Скачать исходный код проекта