1. 介绍
PortAudio是一个免费、跨平台、开源的音频I/O库。看到I/O可能就想到了文件,但是PortAudio操作的I/O不是文件,而是音频设备。它能够简化C/C++的音频程序的设计实现,能够运行在Windows、Macintosh OS X和UNIX之上(Linux的各种版本也不在话下)。使用PortAudio可以在不同的平台上迁移应用程序,比如你可以把你基于PortAudio的应用程序发展一个Android版本啊。
PortAudio的API非常简单,通过一个一个简单的回调函数或者阻塞的读/写接口来录制或者播放声音。PortAudio自带了很多示例程序,比如播放正弦波形的音频信号,处理音频输入,录制回放音频,列举音频设备。
本文以一个录音,播放的例子展示,PortAudio的使用流程。完整代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <memory>
#include "portaudio.h"
using namespace std;
// 回调函数定义
static int audioCallback(const void* inputBuffer, void* outputBuffer,unsigned long framesPerBuffer,const PaStreamCallbackTimeInfo* timeInfo,PaStreamCallbackFlags statusFlags,void* userData)
{// 在此处处理音频数据// 将输入缓冲区中的数据复制到输出缓冲区以回放声音if (inputBuffer != NULL)memcpy(outputBuffer, inputBuffer, framesPerBuffer * sizeof(float));return paContinue;
}int main()
{PaStream* stream;PaError err;// 初始化PortAudio库err = Pa_Initialize();if (err != paNoError) {printf("初始化PortAudio失败: %s\n", Pa_GetErrorText(err));return 1;}// 打开默认的音频输入和输出设备err = Pa_OpenDefaultStream(&stream,1, // 输入通道数1, // 输出通道数paFloat32, // 采样格式44100, // 采样率256, // 缓冲区大小(每个缓冲区的帧数)audioCallback, // 回调函数NULL); // 用户数据if (err != paNoError) {printf("打开音频流失败: %s\n", Pa_GetErrorText(err));return 1;}// 启动音频流err = Pa_StartStream(stream);if (err != paNoError) {printf("启动音频流失败: %s\n", Pa_GetErrorText(err));return 1;}printf("录音已开始,请按 Enter 键停止...\n");getchar();// 停止音频流err = Pa_StopStream(stream);if (err != paNoError) {printf("停止音频流失败: %s\n", Pa_GetErrorText(err));return 1;}// 关闭音频流和PortAudio库err = Pa_CloseStream(stream);if (err != paNoError) {printf("关闭音频流失败: %s\n", Pa_GetErrorText(err));return 1;}Pa_Terminate();printf("录音已停止。\n");return 0;
}
2. 下载安装
2.1 源码编译
源码下载路径为http://www.portaudio.com/download.html
VS上编译的步骤可以参考http://portaudio.com/docs/v19-doxydocs/compile_windows.html
ubuntu下直接使用apt-get install portaudio19-dev即可
2.2 Vcpkg 安装
使用 vcpkg c++ 包管理器安装
vcpkg install portaudio:x64-windows
3. 使用流程
编写一个PortAudio应用,只需要掌握回调函数即可:
- 编写一个回调函数,PortAudio在进行音频处理的时候自动调用
- 初始化PA库,并为I/O打开一个流
- 启动流,PA会在幕后调用回调函数
- 在回调函数中可以从inputBuffer读取音频数据,或者将音频数据写入到outputBuffer
- 回调函数返回1, 或者调用相应函数来停止流
- 关闭流,然后终止PA
除了回调函数,PA还支持阻塞I/O模型,但并不是所有的功能都得到支持。所以推荐使用回调函数。
流程图
3.1 编写回调函数
首先引入PA的头文件
#include "portaudio.h"
回调函数会在两种情况下被调用:PA获取音频数据时和PA需要音频数据作为输出时。
回调函数是一个神奇的地方,因为一些系统在一个特殊的线程中处理回调函数,甚至是通过中断来处理,这不同于程序中的其他代码。如果你想音频能够按时到达Speaker,就得保证回调函数能够快速地执行。不同的平台上,什么 样的操作是安全的,什么样的操作是不安全的,是不一样的。一个通用准则就是,不要做内存的分配释放操作、读写文件、printf,或者其他依赖于OS的不能在一定时间内返回的操作,也包括可能导致上下文切换的操作。
回调函数原型:
typedef int PaStreamCallback( const void *input,void *output,unsigned long frameCount, const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void *userData );
比如我们想把录音的数据,再播放回来就可以这样写这个回调
// 回调函数定义
static int audioCallback(const void* inputBuffer, void* outputBuffer,unsigned long framesPerBuffer,const PaStreamCallbackTimeInfo* timeInfo,PaStreamCallbackFlags statusFlags,void* userData)
{// 在此处处理音频数据// 将输入缓冲区中的数据复制到输出缓冲区以回放声音if (inputBuffer != NULL)memcpy(outputBuffer, inputBuffer, framesPerBuffer * sizeof(float));return paContinue;
}
如果我们想对声音数据做处理,可以这样写
typedef struct
{float left_phase;float right_phase;
}
paTestData;
/* 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 )
{/* Cast data passed through stream to our structure. */paTestData *data = (paTestData*)userData; float *out = (float*)outputBuffer;unsigned int i;(void) inputBuffer; /* Prevent unused variable warning. */for( i=0; i<framesPerBuffer; i++ ){out++ = data->left_phase; /* left */out++ = data->right_phase; /* right *//* Generate simple sawtooth phaser that ranges between -1.0 and 1.0. */data->left_phase += 0.01f;/* When signal reaches top, drop back down. */if( data->left_phase >= 1.0f ) data->left_phase -= 2.0f;/* higher pitch so we can distinguish left and right. */data->right_phase += 0.03f;if( data->right_phase >= 1.0f ) data->right_phase -= 2.0f;}return 0;
}
3.2 Initializing PortAudio
调用 Pa_Initialize() 这将触发对可用设备的扫描,稍后可以查询这些设备。像大部分PA 函数,都将返回paError信息, 如果不是 paNoError 这个表示有错误产生。
auto err = Pa_Terminate();
if( err != paNoError )printf( "PortAudio error: %s\n", Pa_GetErrorText( err ) );
3.3 Opeing Stream Using Defaults
在这一步将打开一个流,就和打开一个文件一样。你可以指定你想输入/输出音频,多少通道,数据格式,采样率等。打开一个“默认”流意味着打开默认的输入和输出设备,这样可以省去获取设备列表并从列表中选择一个的麻烦。(稍后我们将介绍如何做到这一点。)
#define SAMPLE_RATE (44100)
static paTestData data;
.....PaStream *stream;PaError err;/* Open an audio I/O stream. */err = Pa_OpenDefaultStream( &stream,0, /* no input channels */2, /* stereo output */paFloat32, /* 32 bit floating point output */SAMPLE_RATE,256, /* frames per buffer, i.e. the numberof sample frames that PortAudio willrequest from the callback. Many appsmay want to usepaFramesPerBufferUnspecified, whichtells PortAudio to pick the best,possibly changing, buffer size.*/patestCallback, /* this is your callback function */&data ); /*This is a pointer that will be passed toyour callback*/if( err != paNoError ) goto error;
这里的的 data 对应了 Call back 里面的 userData 参数。
上面的这个示例展示了写的stream, 以满足播放的要求。也可以打开一个用于读取的流,进行录音,或者同时进行读取和写入,以实现同时录制和播放甚至实时音频处理。如果您计划同时进行播放和录制,请只打开一个具有有效输入和输出参数的流。
比如,在文章开头提到得录音,播放得初始化
err = Pa_OpenDefaultStream(&stream,1, // 输入通道数1, // 输出通道数paFloat32, // 采样格式44100, // 采样率256, // 缓冲区大小(每个缓冲区的帧数)audioCallback, // 回调函数NULL); // 用户数据if (err != paNoError) {printf("打开音频流失败: %s\n", Pa_GetErrorText(err));return 1;}
Note:
- 一些平台得设备可能只读或者只写
- 尽管多流可以被打开,但是他们很难同步
- 一些平台的设备不支持打开多流
- 使用多个流可能没有经过与其他功能一样全面的测试。
- PortAudio库的调用必须来自同一线程或由用户进行同步。
3.4 Starting, Stoping and Aborting a Stream
PortAudio 当你启动流的将开始播放音频, 当调用 Pa_StartStream() 时, PortAudio 将开始调用你的Callback 函数来执行音频处理。
err = Pa_StartStream( stream );
if( err != paNoError ) goto error;
你可以通过在打开调用时传递的数据结构、全局变量或使用其他进程间通信技术与回调函数进行通信,但请注意,当前台进程最不希望发生中断时可能会调用你的回调函数。因此,避免共享像双向链表这样容易损坏的复杂数据结构,并避免使用诸如互斥锁之类的锁,因为这可能导致你的回调函数阻塞并且丢失音频。这些技术甚至可能在某些平台上导致死锁。
PortAudio将继续调用您的回调函数并处理音频,直到您停止流。这可以通过多种方式完成,但在执行此操作之前,我们希望能看到一些我们的音频经过几秒钟的睡眠进行处理。使用Pa_Sleep()可以轻松实现这一点,许多patests/目录中的示例都是为了这个目的而使用它。请注意,出于各种原因,您不能依赖此函数进行准确的调度,因此您的流可能不会像您期望的那样运行相同的时间,但对于我们的示例来说,这已经足够了。
/* Sleep for several seconds. */
Pa_Sleep(NUM_SECONDS*1000);
现在我们需要停止播放。有几种方法可以做到这一点,其中最简单的方法是调用Pa_StopStream()函数:
err = Pa_StopStream( stream );
if( err != paNoError ) goto error;
Pa_StopStream()函数的设计目的是确保您在回调函数中处理的缓冲区都被播放,这可能会导致一些延迟。或者,您可以调用Pa_AbortStream()函数。在某些平台上,中止流程速度更快,可能会导致部分由回调函数处理的数据不被播放。
停止流的另一种方法是从回调函数返回paComplete或paAbort。paComplete确保最后一个缓冲区被播放,而paAbort尽快停止流。如果您使用此技术停止流程,则需要在再次启动流程之前调用Pa_StopStream()函数。
3.5 Closing a Stream and Terminating PortAudio
当您完成一个流程时,应该关闭它以释放资源:
err = Pa_CloseStream( stream );
if( err != paNoError ) goto error;
在初始化PortAudio时我们已经提到过这一点,但是以防您忘记了,在完成时请确保终止PortAudio:
err = Pa_Terminate( );
if( err != paNoError ) goto error;
4. 参考资料
https://blog.csdn.net/GG_SiMiDa/article/details/77185755
http://files.portaudio.com/docs/v19-doxydocs/terminating_portaudio.html