PortAudio--Cross-platform Open-Source Audio I/O Library
目录
音频相关概念
声音是什么
声音是由物体振动产生的声波。声音作为一种机械波,频率在20 Hz~20 kHz之间的声音是可以被人耳识别的。
音频录制
最简单的音频录制流程为:
设备采集获取模拟信号 --->模数转换 --->存储(播放、传输等).

播放端流程相反:
音频文件 --->数模转换--->播放器播放
模数转换
模拟信号转化为数字信号的流程:

如上图:
- 模拟数据:最原始的连续信号。
- 采样:按固定时间间隔 “抓取” 模拟信号的瞬时值,将连续时间的信号转换为离散时间的信号。
- 量化:把采样得到的连续幅度值,划分到有限个 “量化等级” 中,将连续幅度转换为离散幅度。
- 编码:将量化后的离散整数值,转换为二进制,或其他数字格式,最终生成计算机可存储、处理的数字信号。
这些术语是数字信号处理(Digital Signal Processing, DSP) 领域的核心概念。
简单说:模拟信号 → 采样(时间离散)→ 量化(幅度离散)→ 编码(二进制化)→ 数字信号。
PortAudio
PortAudio 是一个跨平台的开源音频 I/O 库,旨在为不同操作系统提供统一的音频设备访问接口,方便开发者进行音频采集、播放等操作。
其能够在 Windows、macOS、Linux、iOS、Android 等多种操作系统上使用,支持多种音频设备和多种常见的音频数据格式,并且提供了简洁且一致的 API,使得开发者可以相对轻松地实现音频设备的打开、关闭,音频流的启动、停止,以及音频数据的读写等操作。
核心概念与函数
- 音频设备(Audio Device):在 PortAudio 中,每个音频输入或输出设备都被抽象为一个音频设备对象。通过
Pa_GetDeviceCount()函数可以获取系统中音频设备的数量。使用Pa_GetDeviceInfo()函数可以获取指定设备的详细信息。 - 音频流(Audio Stream):音频流用于管理音频数据的传输。要使用音频设备进行音频采集或播放,需要先打开一个音频流。通过
Pa_OpenStream()函数来打开音频流,该函数需要指定输入设备参数、输出设备参数、采样率、帧大小、音频格式等信息。打开音频流后,使用Pa_StartStream()函数启动音频流,开始进行音频数据的传输;使用Pa_ReadStream()函数可以从输入音频流中读取音频数据(用于音频采集),使用Pa_WriteStream()函数可以向输出音频流中写入音频数据(用于音频播放) ;最后,使用Pa_CloseStream()函数关闭音频流,释放相关资源。 - 回调函数(Callback Function):在音频流的操作中,回调函数起着重要作用。例如,在进行音频采集时,可以设置一个回调函数,当音频流有新的音频数据可用时,PortAudio 会自动调用该回调函数,开发者可以在回调函数中处理采集到的音频数据。回调函数的使用使得音频数据的处理可以以异步的方式进行,提高了程序的效率和响应性。
回调函数
PortAudio 的回调函数是其异步音频处理模式的核心,它由 PortAudio 内部的音频驱动线程自动调用,而非用户代码直接触发。其调用时机严格与音频硬件的采样时钟和缓冲区处理逻辑绑定。
1. 音频缓冲区需要填充 / 读取(实时处理)
这是回调函数被调用的最主要、最频繁的原因,是其最核心场景,直接服务于音频的实时输入 / 输出。
PortAudio 采用 “缓冲区” 机制协调软件处理与硬件速度的差异:
- 对于输出流(播放音频):当音频硬件的输出缓冲区中已有的数据即将播放完毕(或已空)时,PortAudio 会立即调用回调函数,要求用户代码填充新的音频数据到输出缓冲区,避免出现 “断音”(underflow)。
- 对于输入流(录制音频):当音频硬件的输入缓冲区已收集到足够多的音频数据(达到预设的缓冲区大小)时,PortAudio 会调用回调函数,将缓冲区中录制好的原始数据传递给用户代码,供后续处理,避免数据溢出(overflow)。
- 对于全双工流(同时输入 + 输出):回调函数会在 “输入缓冲区满” 或 “输出缓冲区空” 的较早触发条件下被调用,此时用户可同时读取输入数据、填充输出数据(例如实时语音处理、音效实时叠加等场景)。
2. 流状态变化时的通知
当音频流的状态发生改变(如启动、停止、出错)时,PortAudio 也会调用回调函数,并通过参数告知用户当前的状态,以便进行对应的处理。常见的状态触发包括:
- 流启动时:部分系统中,音频流刚通过
Pa_StartStream()启动后,会立即调用一次回调函数,用于初始化缓冲区或同步状态。 - 流停止时:当调用
Pa_StopStream()或Pa_AbortStream()后,PortAudio 可能会再调用一次回调函数,传递paComplete或paAbort状态,通知用户流已终止,可进行资源清理。 - 流出错时:若音频硬件发生错误(如设备被占用、采样率不支持、缓冲区溢出 / 下溢),回调函数会被调用,并通过
paError状态传递具体错误码,用户可据此进行错误处理。
回调函数的核心规范(必须遵守)
PortAudio 回调函数的函数签名是固定的,用户不能自定义参数或返回值类型,否则会导致程序崩溃。其原型如下(C/C++ 通用):
typedef int PaStreamCallback(
const void *inputBuffer, // 输入音频数据缓冲区(录制场景有效)
void *outputBuffer, // 输出音频数据缓冲区(播放场景有效)
unsigned long framesPerBuffer, // 本次回调需处理的采样点数(帧大小)
const PaStreamCallbackTimeInfo *timeInfo, // 时间信息(可选)
PaStreamCallbackFlags statusFlags, // 状态标志(如缓冲区溢出/下溢)
void *userData // 用户自定义数据(传递上下文)
);
各参数含义详解:
inputBuffer |
录制场景:指向麦克风等输入设备采集的原始音频数据(格式与流配置一致);无输入时为 NULL。 |
outputBuffer |
播放场景:指向需要填充的输出音频数据缓冲区;无输出时为 NULL。 |
framesPerBuffer |
本次回调需处理的采样点数(即 “帧大小”),由 Pa_OpenStream 配置时指定(或系统自动分配)。 |
timeInfo |
包含音频处理的时间戳信息(如当前缓冲区的播放 / 采集时间),一般用于高精度同步场景(可选忽略)。 |
statusFlags |
状态标志位,如 paInputOverflow(输入缓冲区溢出)、paOutputUnderflow(输出缓冲区下溢),需据此处理错误。 |
userData |
用户自定义数据指针(通过 Pa_OpenStream 传入),用于传递上下文(如缓冲区、配置参数等),避免全局变量。 |
返回值规范(决定流的后续状态)
回调函数的返回值是 PortAudio 控制音频流生命周期的关键,必须返回以下枚举值之一:
paContinue:继续运行,PortAudio 会持续调用回调函数;paComplete:正常终止流,PortAudio 后续不再调用回调,需配合Pa_CloseStream释放资源;paAbort:立即终止流。
回调函数的调用特性
理解调用时机的同时,需明确其底层特性,调用本质是 “硬件驱动的事件通知”,避免误用:
- 线程独立性:回调函数运行在 PortAudio 的内部音频线程中,而非用户的主线程,因此回调函数内需避免阻塞操作(如文件 IO、sleep、锁等待),否则会导致音频卡顿。
- 实时性要求:调用时机由硬件时钟决定,具有严格的实时性,用户需确保回调函数内的代码执行速度足够快(耗时 < 缓冲区对应的时间)。
- 参数驱动:回调函数的输入参数(如输入数据指针、缓冲区大小)会明确告知当前需要处理的任务,用户无需主动查询流状态。
使用流程
1.编写回调函数
首先引入PA的头文件
#include "portaudio.h"
常见使用场景
| 场景 | 回调核心逻辑 |
|---|---|
| 麦克风录制 | 读取 inputBuffer → (可选预处理)→ 写入文件 / 传递给后续模块(如语音识别) |
| 音频播放 | 从文件 / 内存读取数据 → 写入 outputBuffer → 读完返回 paComplete |
| 实时音效处理 | 读取 inputBuffer → 音效算法处理(如混响、均衡)→ 写入 outputBuffer 播放 |
| 全双工(同时录播) | 读取 inputBuffer 处理,同时填充 outputBuffer 播放,需协调两者的同步性 |
麦克风录制场景
#include <portaudio.h>
#include <iostream>
#include <fstream>
// 自定义上下文结构体,用于传递录制相关信息
struct RecordContext {
std::ofstream outFile; // 用于存储录制音频的文件流
bool isRecording; // 录制状态标志
};
// 麦克风录制回调函数
int RecordCallback(
const void *inputBuffer,
void *outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo *timeInfo,
PaStreamCallbackFlags statusFlags,
void *userData
) {
RecordContext* ctx = reinterpret_cast<RecordContext*>(userData);
// 检查状态标志,处理可能的错误
if (statusFlags & paInputOverflow) {
std::cerr << "Input buffer overflow occurred!" << std::endl;
}
if (statusFlags & paError) {
std::cerr << "An error occurred in the audio stream!" << std::endl;
return paAbort;
}
// 如果处于录制状态且有输入音频数据
if (ctx->isRecording && inputBuffer != nullptr) {
// 假设音频数据是32位浮点数格式,将其写入文件
const float* audioData = reinterpret_cast<const float*>(inputBuffer);
ctx->outFile.write(reinterpret_cast<const char*>(audioData), framesPerBuffer * sizeof(float));
}
// 如果停止录制,返回paComplete终止流
return ctx->isRecording? paContinue : paComplete;
}
音频播放场景:
#include <portaudio.h>
#include <iostream>
#include <fstream>
// 自定义上下文结构体,用于传递播放相关信息
struct PlaybackContext {
std::ifstream inFile; // 要播放的音频文件流
bool isPlaying; // 播放状态标志
};
// 音频播放回调函数
int PlaybackCallback(
const void *inputBuffer,
void *outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo *timeInfo,
PaStreamCallbackFlags statusFlags,
void *userData
) {
PlaybackContext* ctx = reinterpret_cast<PlaybackContext*>(userData);
// 检查状态标志,处理可能的错误
if (statusFlags & paOutputUnderflow) {
std::cerr << "Output buffer underflow occurred!" << std::endl;
}
if (statusFlags & paError) {
std::cerr << "An error occurred in the audio stream!" << std::endl;
return paAbort;
}
// 如果处于播放状态且有输出缓冲区
if (ctx->isPlaying && outputBuffer != nullptr) {
float* outData = reinterpret_cast<float*>(outputBuffer);
// 从文件读取音频数据到输出缓冲区
ctx->inFile.read(reinterpret_cast<char*>(outData), framesPerBuffer * sizeof(float));
// 如果读取的帧数少于预期,说明文件已读完,填充剩余部分为0(静音)
if (ctx->inFile.gcount() < framesPerBuffer) {
std::fill(outData + ctx->inFile.gcount(), outData + framesPerBuffer, 0.0f);
ctx->isPlaying = false;
return paComplete;
}
}
return paContinue;
}
实时音效处理场景(以 “音量调节 + 简单混响” 为例)
#include <portaudio.h>
#include <iostream>
#include <vector>
// 简单混响效果器:使用延迟线实现回声
class ReverbEffect {
private:
std::vector<float> delayLine; // 延迟线缓冲区
size_t delayIndex = 0; // 延迟线当前索引
float decayFactor = 0.5f; // 回声衰减因子
public:
ReverbEffect(size_t maxDelaySamples) : delayLine(maxDelaySamples, 0.0f) {}
// 处理单帧音频,添加混响效果
void process(float* audioData, unsigned long framesPerBuffer) {
for (unsigned long i = 0; i < framesPerBuffer; ++i) {
// 当前输入样本
float inputSample = audioData[i];
// 延迟线中取出历史样本(回声)
float delayedSample = delayLine[delayIndex];
// 新样本 = 原始样本 + 衰减后的回声
audioData[i] = inputSample + (delayedSample * decayFactor);
// 将新样本存入延迟线(覆盖旧样本,实现循环)
delayLine[delayIndex] = inputSample;
// 更新延迟线索引(循环)
delayIndex = (delayIndex + 1) % delayLine.size();
}
}
};
// 自定义上下文结构体:存储音效参数和状态
struct EffectContext {
ReverbEffect reverb; // 混响效果器
float volumeGain = 1.2f; // 音量增益(1.2倍)
bool isProcessing = true; // 处理状态标志
};
// 实时音效处理回调函数
int EffectCallback(
const void *inputBuffer,
void *outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo *timeInfo,
PaStreamCallbackFlags statusFlags,
void *userData
) {
EffectContext* ctx = reinterpret_cast<EffectContext*>(userData);
// 1. 处理状态标志(输入/输出错误)
if (statusFlags & (paInputOverflow | paOutputUnderflow)) {
std::cerr << "警告:缓冲区溢出/下溢,可能导致音效失真!\n";
}
if (statusFlags & paError) {
std::cerr << "错误:音频流异常,终止处理!\n";
return paAbort;
}
// 2. 核心逻辑:仅在处理状态下执行
if (ctx->isProcessing && inputBuffer != nullptr && outputBuffer != nullptr) {
// 强转输入/输出数据为 float*(与流配置的 paFloat32 匹配)
const float* inData = reinterpret_cast<const float*>(inputBuffer);
float* outData = reinterpret_cast<float*>(outputBuffer);
// 3. 逐帧处理:音量调节 → 混响效果
for (unsigned long i = 0; i < framesPerBuffer; ++i) {
// 步骤1:音量调节(原始数据 × 增益)
float processedSample = inData[i] * ctx->volumeGain;
// 步骤2:限制音量范围(防止溢出)
if (processedSample > 1.0f) processedSample = 1.0f;
if (processedSample < -1.0f) processedSample = -1.0f;
// 步骤3:混响处理(延迟线回声)
outData[i] = processedSample; // 先暂存,混响会直接修改 outData
}
ctx->reverb.process(outData, framesPerBuffer);
} else {
// 若未处理,填充静音
memset(outputBuffer, 0, framesPerBuffer * sizeof(float));
}
return ctx->isProcessing ? paContinue : paComplete;
}
全双工(同时录播)场景(麦克风输入→实时变调→播放)
#include <portaudio.h>
#include <iostream>
#include <cmath>
// 简单变调效果器:通过插值实现音高调整
class PitchShifter {
private:
float pitchRatio = 1.5f; // 变调比例(1.5倍即升高半音)
float readIndex = 0.0f; // 读取索引(模拟非整数采样)
public:
void setPitchRatio(float ratio) { pitchRatio = ratio; }
// 处理单帧音频,调整音高
void process(float* audioData, unsigned long framesPerBuffer) {
for (unsigned long i = 0; i < framesPerBuffer; ++i) {
// 线性插值:模拟非整数采样点的取值
size_t prevIndex = static_cast<size_t>(readIndex);
size_t nextIndex = (prevIndex + 1) % framesPerBuffer;
float fraction = readIndex - prevIndex;
// 插值计算新样本
audioData[i] = audioData[prevIndex] * (1.0f - fraction) +
audioData[nextIndex] * fraction;
// 更新读取索引(按变调比例步进)
readIndex = fmod(readIndex + pitchRatio, framesPerBuffer);
}
}
};
// 自定义上下文结构体:存储录制和播放的状态
struct DuplexContext {
PitchShifter shifter; // 变调效果器
float* tempBuffer; // 临时缓冲区(存储原始录制数据)
unsigned long bufferSize; // 缓冲区大小
bool isDuplexRunning = true; // 全双工运行状态
};
// 全双工回调函数:同时录制和播放,实时变调
int DuplexCallback(
const void *inputBuffer,
void *outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo *timeInfo,
PaStreamCallbackFlags statusFlags,
void *userData
) {
DuplexContext* ctx = reinterpret_cast<DuplexContext*>(userData);
// 1. 处理状态标志(输入/输出错误)
if (statusFlags & (paInputOverflow | paOutputUnderflow)) {
std::cerr << "警告:全双工模式下缓冲区溢出/下溢!\n";
}
if (statusFlags & paError) {
std::cerr << "错误:音频流异常,终止全双工!\n";
return paAbort;
}
// 2. 核心逻辑:仅在运行状态下执行
if (ctx->isDuplexRunning && inputBuffer != nullptr && outputBuffer != nullptr) {
// 强转输入/输出数据为 float*
const float* inData = reinterpret_cast<const float*>(inputBuffer);
float* outData = reinterpret_cast<float*>(outputBuffer);
// 3. 步骤1:录制原始数据到临时缓冲区
memcpy(ctx->tempBuffer, inData, framesPerBuffer * sizeof(float));
// 4. 步骤2:对临时缓冲区数据变调处理
ctx->shifter.process(ctx->tempBuffer, framesPerBuffer);
// 5. 步骤3:将处理后的数据写入输出缓冲区(播放)
memcpy(outData, ctx->tempBuffer, framesPerBuffer * sizeof(float));
} else {
// 若停止,填充静音
memset(outputBuffer, 0, framesPerBuffer * sizeof(float));
}
return ctx->isDuplexRunning ? paContinue : paComplete;
}
2.初始化 PortAudio 库
在使用任何 PortAudio 功能前,必须先初始化库,为后续的音频设备交互和流操作准备环境。
PaError err = Pa_Initialize();
if (err != paNoError) {
std::cerr << "PortAudio 初始化失败: " << Pa_GetErrorText(err) << std::endl;
return -1; // 初始化失败,直接退出
}
3.配置音频流参数(PaStreamParameters)
根据需求(录制 / 播放 / 全双工),配置输入 / 输出流的参数,包括:
- 音频设备
- 通道数
- 采样格式
- 延迟
示例:麦克风录制的输入参数配置
PaStreamParameters inputParams;
inputParams.device = Pa_GetDefaultInputDevice(); // 使用默认麦克风
inputParams.channelCount = 1; // 单声道
inputParams.sampleFormat = paFloat32; // 32位浮点数格式
inputParams.suggestedLatency = Pa_GetDeviceInfo(inputParams.device)->defaultLowInputLatency;
inputParams.hostApiSpecificStreamInfo = NULL; // 无特定宿主API信息
4.打开音频流(Pa_OpenStream)
将回调函数和上下文结构体注册到音频流中,完成 “流 - 回调 - 数据” 的绑定。
PaStream* stream;
err = Pa_OpenStream(
&stream, // 输出:创建的音频流指针
&inputParams, // 输入参数(录制场景,如麦克风)
NULL, // 输出参数(录制场景为 NULL)
SAMPLE_RATE, // 采样率(如 16000 Hz)
FRAMES_PER_BUFFER, // 每帧采样数(如 512)
paClipOff, // 禁用自动剪辑(避免数据失真)
RecordCallback, // 注册回调函数
&recordCtx // 传递上下文结构体(userData 参数)
);
if (err != paNoError) {
std::cerr << "打开音频流失败: " << Pa_GetErrorText(err) << std::endl;
Pa_Terminate(); // 初始化失败,终止 PortAudio
return -1;
}
5.启动音频流(Pa_StartStream)
启动后,PortAudio 会自动调用回调函数,开始音频的采集或播放。
err = Pa_StartStream(stream);
if (err != paNoError) {
std::cerr << "启动音频流失败: " << Pa_GetErrorText(err) << std::endl;
Pa_CloseStream(stream); // 关闭流
Pa_Terminate();
return -1;
}
6.运行音频逻辑(如等待录制结束、处理数据)
根据业务逻辑,让程序保持运行,直到音频任务完成。
示例:等待麦克风录制 5 秒后停止
std::this_thread::sleep_for(std::chrono::seconds(5)); // 等待 5 秒
recordCtx.isRecording = false; // 修改上下文状态,让回调返回 paComplete
Pa_StopStream(stream); // 停止流(等待回调结束)
7.释放资源(关闭流 + 终止 PortAudio)
音频任务结束后,必须释放资源,避免内存泄漏或设备占用。
Pa_CloseStream(stream); // 关闭音频流
Pa_Terminate(); // 终止 PortAudio 库
8.完整Demo:
#include <portaudio.h>
#include <iostream>
#include <fstream>
#include <thread>
// 录制上下文结构体
struct RecordContext {
std::ofstream outFile;
bool isRecording = true;
};
// 回调函数(前文定义的 RecordCallback)
int RecordCallback(
const void *inputBuffer,
void *outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo *timeInfo,
PaStreamCallbackFlags statusFlags,
void *userData
);
int main() {
// 1. 初始化 PortAudio
PaError err = Pa_Initialize();
if (err != paNoError) {
std::cerr << "Pa_Initialize 失败: " << Pa_GetErrorText(err) << std::endl;
return -1;
}
// 2. 初始化上下文
RecordContext recordCtx;
recordCtx.outFile.open("output.pcm", std::ios::binary);
if (!recordCtx.outFile.is_open()) {
std::cerr << "打开输出文件失败" << std::endl;
Pa_Terminate();
return -1;
}
// 3. 配置输入参数
PaStreamParameters inputParams;
inputParams.device = Pa_GetDefaultInputDevice();
inputParams.channelCount = 1;
inputParams.sampleFormat = paFloat32;
inputParams.suggestedLatency = Pa_GetDeviceInfo(inputParams.device)->defaultLowInputLatency;
inputParams.hostApiSpecificStreamInfo = NULL;
// 4. 打开音频流
PaStream* stream;
err = Pa_OpenStream(
&stream,
&inputParams,
NULL,
16000, // 采样率 16kHz
512, // 每帧 512 采样
paClipOff,
RecordCallback,
&recordCtx
);
if (err != paNoError) {
std::cerr << "Pa_OpenStream 失败: " << Pa_GetErrorText(err) << std::endl;
recordCtx.outFile.close();
Pa_Terminate();
return -1;
}
// 5. 启动流
err = Pa_StartStream(stream);
if (err != paNoError) {
std::cerr << "Pa_StartStream 失败: " << Pa_GetErrorText(err) << std::endl;
Pa_CloseStream(stream);
recordCtx.outFile.close();
Pa_Terminate();
return -1;
}
// 6. 等待 5 秒后停止录制
std::cout << "录制中... 5 秒后停止" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5));
recordCtx.isRecording = false;
Pa_StopStream(stream);
// 7. 释放资源
Pa_CloseStream(stream);
recordCtx.outFile.close();
Pa_Terminate();
std::cout << "录制完成,文件已保存为 output.pcm" << std::endl;
return 0;
}
// 回调函数实现(前文定义的 RecordCallback)
int RecordCallback(
const void *inputBuffer,
void *outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo *timeInfo,
PaStreamCallbackFlags statusFlags,
void *userData
) {
RecordContext* ctx = reinterpret_cast<RecordContext*>(userData);
if (statusFlags & paInputOverflow) {
std::cerr << "输入缓冲区溢出!" << std::endl;
}
if (ctx->isRecording && inputBuffer != nullptr) {
const float* audioData = reinterpret_cast<const float*>(inputBuffer);
ctx->outFile.write(reinterpret_cast<const char*>(audioData), framesPerBuffer * sizeof(float));
}
return ctx->isRecording ? paContinue : paComplete;
}
总结:核心流程是 “编写回调函数→ 初始化 → 配置参数 → 打开流 → 启动流 → 运行逻辑 → 释放资源”。
👾👾👾...
更多推荐



所有评论(0)