博客源码下载 : https://download.csdn.net/download/han1202012/89734548
一、PCM 格式简介
1、PCM 简介
PCM , Pulse Code Modulation , 脉冲编码调制 , 使用数字表示模拟信号 , 广泛应用于音频数字化 ;
- 模拟信号 转 数字信号 : PCM 将 模拟信号 转换为 数字信号 , 对模拟信号进行 采样、量化 和 编码 生成 PCM 数据 ;
- 采样 : 在特定 时间间隔 内对模拟信号的幅度进行测量 , 对声音来说就是测量声音的振幅 ;
- 量化 : 将 测量的幅度值 映射 到 离散的数值 上 ;
- 编码 : 将 量化后的值 转换为二进制格式 , 以便进行数字处理和存储 ;
声音 是 模拟信号的一种 , 将声音 通过麦克风 录制成 PCM 数据 , 然后将 PCM 数据传递给扬声器 就可以将声音播放出来 ;
PCM 音频数据没有经过压缩 , 是高保真数据 , 没有任何声音损失 , 一旦转为 aac / mp3 格式 , 就会不可逆的损失部分声音信息 , 如 : 高频信号 / 低频信号 / 时域掩盖信息 / 频域掩盖信息 等 都在音频压缩时被删除 ;
参考 【音视频原理】音频编解码原理 ① ( 声音特性 | 声音本质 | 声音频率 | 声音频率和响度本质分析 | 数字音频 |脉冲编码调制 PCM - 采样振幅值 | 奈奎斯特 Nyguist 采样定理 ) 博客 ;
2、PCM 参数
PCM 数据的参数 :
- 采样率 : 每秒钟对信号进行采样的次数 , 常见的采样率有 44.1 kHz , 48 kHz 等 , 注意区分 采样率 与 声音频率 , 下面有声音频率分析 ;
- 通道数 : 音频信号的通道数量 , 例如 : 单声道 ( Mono ) 、立体声 ( Stereo ) 或 多声道 ( 5.1 环绕声 ) ;
- 位深度 : 每个样本的分辨率 , 通常为 16 位、24 位等 , 位深度决定了音频的动态范围和精确度 ;
3、声音频率 ( 注意与采样率区分 )
声音频率分析 : 声音的频率 就是 声音的 振幅 ;
声音的振幅实际上是 正弦 / 余弦 曲线 , 正弦的周期数就是声音的频率 , 比如 : 128 键钢琴中间的中央 C 音符 Do 频率为 声音频率为 262 Hz , 也就是主频率每秒钟震动 262 次 , 每秒钟有 262 个 正弦 / 余弦 曲线 周期 , 参考 【音频处理】音高 与 频率 对照表 ( 音符频率算法 ) ;
通过 傅里叶变换 , 可以从音频采样数据中分析出 声音频率 , 这就是 时域信息 转 频域信息 ;
4、使用 ffmpeg 获取 PCM 格式数据
PCM 数据没有经过压缩 , 占用很多空间 , 1 分钟的音频数据有 11MB 左右 , 如果压缩成 mp4 或 aac 格式 , 能压缩到 1MB 以内 ;
PCM 数据不容易找到 , 该数据没有任何的 文件头 描述信息 , 文件的第一个字节就是 第一个采样的数值数据 , 播放 PCM 数据时必须知道该音频的 采样率 通道数 采样位数 等参数 ;
这里使用 FFmpeg 命令行工具从视频中提取 PCM 数据 , 下面的命令 , 可以将 mp4 格式的视频中提取 pcm 数据 ;
代码语言:javascript复制ffmpeg -i input.mp4 -codec:a pcm_s16le -ar 44100 -ac 2 -f s16le 44100_16bit_2ch.pcm
-i input.mp4
: 指定输入文件 input2.mp4 , -i 是用于指定输入文件的参数 ;-codec:a pcm_s16le
: 指定 音频编解码器 为 pcm_s16le , 这是一种 PCM 音频格式,使用 16 位小端字节序 s16le , 这个编解码器用于将音频数据以未压缩的形式存储 ;-ar 44100
: 设置 音频采样率为 44100 Hz , 采样率 是 每秒钟采集多少个音频样本 ;-ac 2
: 设置音频通道数为 2 , 双声道 立体声 ;-f s16le
: 指定输出格式为 s16le , 这是音频的原始 PCM 数据格式 , 其中 s16 代表 16 位有符号整数 , le 代表小端字节序 Little Endian ;
参考 【FFmpeg】ffmpeg 命令行参数 ③ ( ffmpeg 音频参数解析 | 设置音频帧数 | 设置音频码率 | 设置音频采样率 | 设置音频通道数 | 设置音频编解码器 | 设置音频过滤器 ) 博客 ;
5、使用 ffplay 播放 PCM 格式数据
得到输出文件后 , 执行
代码语言:javascript复制ffplay -ar 44100 -ac 2 -f s16le 44100_16bit_2ch.pcm
命令 , 可以播放上述提取的 PCM 音频数据 ;
二、SDL 播放 PCM 流程
SDL 播放 PCM 音频 主要分为以下几个步骤
- 初始化 SDL - SDL_Init 函数
- 设置音频参数 - SDL_AudioSpec 结构体
- 打开音频设备 - SDL_OpenAudio 函数
- 设置音频回调函数 - SDL_AudioCallback 类型函数
- 读取 PCM 数据 - fread 函数
- 播放音频 - SDL_PauseAudio 函数
- 播放完毕后 关闭音频设备 - SDL_CloseAudio 函数
- 退出 SDL - SDL_Quit 函数
1、初始化 SDL
初始化 SDL 环境 , 就是调用 SDL_Init 函数 , 该函数用于初始化 SDL 系统上下文环境 , SDL 的任何操作之前都必须执行 初始化 SDL 步骤 ;
SDL_Init 函数原型如下 , 传入的 flags 参数用于设置要使用 SDL 中的哪个子系统 , 本篇博客中设置 SDL_INIT_AUDIO 音频子系统 , 用于 PCM 音频播放 ;
代码语言:javascript复制int SDL_Init(Uint32 flags);
具体的函数原型参考 【FFmpeg】SDL 音视频开发 ① ( SDL 窗口绘制 | SDL 视频显示函数 | SDL_Window 窗口 | SDL_Renderer 渲染器 | SDL_Texture 纹理 ) 博客章节中第一章内容 ;
2、设置音频参数
在 SDL 中 , 使用 SDL_AudioSpec 结构体来设置音频参数 , 该结构体种包含了音频的多个关键属性 , 创建一个 SDL_AudioSpec 结构体 , 设置该结构体的各个成员参数 ;
- 采样频率 ( freg ) : 整数 , 表示音频数据的采样频率 , 常见的采样率有44100Hz、48000Hz等 , 这决定了音频的播放质量 , 采样率越大质量越高 ;
- 音频数据格式 ( format ) : SDL_AudioFormat 枚举类型 , 表示每个样本的格式 ;
- AUDIO_S16SYS 表示 有符号 16 位 整数样本 ;
- AUDIO_S8 表示 有符号 8 位 整数样本 ;
- AUDIO_F32SYS 表示 32 位 浮点数 样本 ;
- 声道数 ( channels ) : 1 表示单声道 , 2 表示立体声 ;
- 静音值 ( silence ) : 无符号 8 位整数 , 表示音频数据中每个样本的静音字节值 ;
- 音频缓冲区的总字节数 ( size ) : 无符号 32 位整数 , 这个值通常需要是 2 的幂次方 , 该参数 决定了音频回调函数的调用频率和每次需要处理的数据量 ;
- 计算公式 :
samples * channels * (SDL_AUDIO_BITSIZE(format) / 8)
;
- 计算公式 :
- 用户自定义数据指针 ( userdata ) : 指向开发者定义的数据的指针 , SDL 本身不会使用这个指针 , 开发者可以用它来存储与音频数据相关的自定义信息 ;
SDL_AudioSpec 结构体原型如下 :
代码语言:javascript复制/*
SDL_AudioSpec 结构体 由 SDL_OpenAudio() 函数计算得出
对于多声道音频,默认的 SDL 声道映射为:
2: 左前(FL)右前(FR) (立体声)
3: 左前(FL)右前(FR)低频增强(LFE) (2.1 环绕声)
4: 左前(FL)右前(FR)左后(BL)右后(BR)(四声道)
5: 左前(FL)右前(FR)中置(FC)左后(BL)右后(BR)(四声道 中置)
6: 左前(FL)右前(FR)中置(FC)低频增强(LFE)左环绕(SL)右环绕(SR)(5.1 环绕声 - 最后两个也可以是左后 BL 和右后 BR)
7: 左前(FL)右前(FR)中置(FC)低频增强(LFE)后置中置(BC)左环绕(SL)右环绕(SR)(6.1 环绕声)
8: 左前(FL)右前(FR)中置(FC)低频增强(LFE)左后(BL)右后(BR)左环绕(SL)右环绕(SR)(7.1 环绕声)
*/
typedef struct SDL_AudioSpec {
int freq; // 采样频率(Sample Rate)
SDL_AudioFormat format; // 音频数据格式
Uint8 channels; // 声道数(1 = 单声道, 2 = 立体声, etc.)
Uint8 silence; // 静音值(每个样本的静音字节值)
Uint16 samples; // 音频缓冲区中的样本数
Uint16 padding; // 必要的填充值,以保证结构体大小为偶数(用于某些平台的对齐)
Uint32 size; // 音频缓冲区的总字节数(= samples * channels * (SDL_AUDIO_BITSIZE(format) / 8))
void *userdata; // 用户自定义数据指针(可由开发者自由使用)
Uint8 *buffer; // 指向实际音频数据的指针
unsigned int length; // 音频缓冲区的长度(以字节为单位)(在 SDL 2.0.9 中已弃用,建议使用 size 字段)
} SDL_AudioSpec;
SDL_AudioSpec 结构体设置示例 :
代码语言:javascript复制#include <SDL2/SDL.h>
int main() {
SDL_AudioSpec spec;
// 设置采样频率为 44100 Hz
spec.freq = 44100;
// 设置音频格式为 16-bit 签名整数,系统字节序
spec.format = AUDIO_S16SYS;
// 设置为立体声(2 个声道)
spec.channels = 2;
// 设置静音值为 0(对于 16-bit 签名整数,通常使用 0)
spec.silence = 0;
// 设置每个缓冲区的样本数为 1024
spec.samples = 1024;
// 计算音频缓冲区的总字节数
spec.size = spec.samples * spec.channels * (SDL_AUDIO_BITSIZE(spec.format) / 8);
// 用户数据指针设为 NULL(无自定义数据)
spec.userdata = NULL;
// 分配音频缓冲区(需要手动分配内存)
spec.buffer = (Uint8 *)SDL_malloc(spec.size);
// 确保内存分配成功
if (spec.buffer == NULL) {
SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "Failed to allocate audio buffer");
return -1;
}
// 初始化缓冲区为静音(可选)
SDL_memset(spec.buffer, spec.silence, spec.size);
// ... 使用 spec 进行音频播放或捕捉 ...
// 释放分配的缓冲区
SDL_free(spec.buffer);
return 0;
}
3、打开音频设备
SDL_OpenAudio 函数 用于 设置音频参数 并 打开音频设备 , 为后续的音频播放做准备 ;
SDL_OpenAudio 函数原型如下 :
代码语言:javascript复制int SDL_OpenAudio(SDL_AudioSpec *desired, SDL_AudioSpec *obtained);
SDL_AudioSpec *desired
参数 : 设置用户期望的音频配置 ;SDL_AudioSpec *obtained
参数 : 实际的音频设备的参数 , 在本篇博客中暂时设置为 NULL ;
4、设置播放回调函数
SDL_AudioCallback 是 SDL ( Simple DirectMedia Layer ) 库中的 PCM 音频播放 回调函数类型 , 当 SDL 播放完当前音频缓冲区中的数据后 , 会自动回调该函数 , 为音频设备提供后续音频播放数据 , 该函数的主要作用如下 :
- 提供音频数据 : 每当音频设备需要更多的数据时 , SDL 会调用这个回调函数 , 向 stream 参数 指向的音频数据缓冲区 填充音频数据 ;
- 处理音频数据 : 在回调函数中 , 可以根据应用程序的需要生成或处理音频数据 , 例如 : 从文件中读取数据、合成音频、或应用音效等 ;
几乎所有的 PCM 音频播放都需要提供一个回调函数 , OpenSL / AAudio 也有一个类似的回调函数 ;
SDL_AudioCallback 函数原型 :
代码语言:javascript复制/**
* 当音频设备需要更多数据时,将调用此函数。
*
* param userdata 保存在 SDL_AudioSpec 结构中的应用程序特定参数
* param stream 指向音频数据缓冲区的指针
* param len 缓冲区的长度(以字节为单位)
*
* 一旦回调函数返回,缓冲区将不再有效。
* 立体声音频样本以 LRLRLR 的顺序存储。
*
* 如果愿意,您可以选择避免使用回调函数,改用 SDL_QueueAudio()。
* 只需使用 NULL 回调打开您的音频设备即可。
*/
typedef void (SDLCALL * SDL_AudioCallback) (void *userdata, Uint8 * stream,
int len);
void *userdata
参数 : 指向用户自定义数据的指针 , 在回调函数中 可借助该指针 访问或存储额外的信息 ;Uint8 * stream
参数 : 指向音频数据缓冲区的指针 , 将音频数据写入这个缓冲区 , 就可以被音频设备播放 ;int len
参数 : 缓冲区的字节长度 , 使用时需要确保向缓冲区写入的数据长度不超过这个值 ;
在本示例中 , 实现的 SDL_AudioCallback 回调函数 如下 :
代码语言:javascript复制// 一帧 PCM 数据有 1024 个采样点
// 每个采样 都是 2 通道 立体声 ( 左右声道 ) , 每个通道的采样都是 16 位 (bit) 也就是 2 字节 (Byte)
// 每次读取 2 帧 PCM 数据
// 1024 ( 采样数 ) * 2 ( 通道数 ) * 2 ( 2 字节 / 16 位 ) * 2 ( 帧数为 2 帧 )
#define PCM_BUFFER_SIZE (1024 * 2 * 2 * 2)
// 音频PCM数据缓存指针
static Uint8 *s_audio_buf = NULL;
// 当前读取的位置
static Uint8 *s_audio_pos = NULL;
// 缓存结束位置
static Uint8 *s_audio_end = NULL;
// 音频设备回调函数
void fill_audio_pcm(void *udata, Uint8 *stream, int len)
{
SDL_memset(stream, 0, len); // 将流缓冲区初始化为0
if(s_audio_pos >= s_audio_end) // 如果数据已读取完毕
{
return; // 退出回调函数
}
// 数据够了就读预设长度,数据不够时只读取剩余数据
int remain_buffer_len = s_audio_end - s_audio_pos;
len = (len < remain_buffer_len) ? len : remain_buffer_len;
// 将数据拷贝到stream并调整音量
SDL_MixAudio(stream, s_audio_pos, len, SDL_MIX_MAXVOLUME/8);
printf("len = %dn", len); // 输出当前读取的数据长度
s_audio_pos = len; // 移动缓存指针到下一个位置
}
5、播放音频数据
调用 SDL_PauseAudio 函数 可以 恢复 / 暂停 播放音频数据 ;
SDL_PauseAudio 函数原型如下 :
代码语言:javascript复制void SDL_PauseAudio(int pause_on);
int pause_on
参数 是一个整数值 , 决定了音频设备的状态 ;
- 0 : 恢复音频播放 , 如果音频设备之前是暂停的 , 调用此函数将会恢复音频播放 ;
- 1 : 暂停音频播放 , 如果音频设备正在播放音频 , 调用此函数将会暂停音频播放 ;
部分代码示例 :
代码语言:javascript复制#include <SDL2/SDL.h>
int main() {
// 初始化 SDL
if (SDL_Init(SDL_INIT_AUDIO) < 0) {
// 错误处理
return -1;
}
// 打开音频设备、设置音频回调等(略)
// 开始播放音频
SDL_PauseAudio(0); // 传递 0 表示恢复音频播放
// 在适当的时候暂停音频
SDL_PauseAudio(1); // 传递 1 表示暂停音频播放
// 结束音频播放、清理资源等(略)
// 清理 SDL
SDL_Quit();
return 0;
}
6、关闭音频设备
播放完毕后 调用 SDL_CloseAudio 函数 , 关闭音频设备 , 释放 PCM 播放时申请的系统资源 ;
SDL_CloseAudio 函数原型如下 , 该函数用于关闭音频设备 ;
代码语言:javascript复制void SDL_CloseAudio(void);
部分代码示例 :
代码语言:javascript复制#include <SDL/SDL.h>
int main() {
// 初始化 SDL
if (SDL_Init(SDL_INIT_AUDIO) < 0) {
// 错误处理
return -1;
}
// 设置音频参数和打开音频设备(略)
// 关闭音频设备
SDL_CloseAudio();
// 清理 SDL
SDL_Quit();
return 0;
}
7、SDL 播放 PCM 音频数据的 关键步骤 代码示例
代码示例 :
代码语言:javascript复制#include <SDL2/SDL.h>
#include <stdio.h>
// 音频回调函数
void audio_callback(void *userdata, Uint8 *stream, int len) {
// 这里填充音频数据到 stream 中
// len 是需要填充的字节数
SDL_memset(stream, 0, len); // 简单地将缓冲区静音
}
int main(int argc, char *argv[]) {
// 初始化 SDL 音频子系统
if (SDL_Init(SDL_INIT_AUDIO) < 0) {
printf("SDL 无法初始化! SDL_Error: %sn", SDL_GetError());
return -1;
}
// 配置音频设备参数
SDL_AudioSpec desired;
desired.freq = 44100; // 采样频率 44.1kHz
desired.format = SDL_AUDIO_S16SYS; // 音频格式:16 位系统字节顺序
desired.channels = 2; // 立体声
desired.samples = 4096; // 每次回调的样本数
desired.callback = audio_callback; // 音频回调函数
desired.userdata = NULL; // 用户数据(这里没有使用)
SDL_AudioSpec obtained;
// 打开音频设备
if (SDL_OpenAudio(&desired, &obtained) < 0) {
printf("无法打开音频设备! SDL_Error: %sn", SDL_GetError());
SDL_Quit();
return -1;
}
// 在这里,你可以开始播放音频了
// 例如,你可以调用 SDL_PauseAudio(0) 来开始播放
// 开始播放音频
SDL_PauseAudio(0);
// 注意:在实际应用中,你需要一个循环或某种方式来持续调用回调函数
// 这里只是为了示例而简化了代码
// 当你完成音频播放后,记得关闭音频设备
SDL_CloseAudio();
SDL_Quit();
return 0;
}
三、完整代码示例
1、完整代码示例
代码语言:javascript复制// 导入标准 IO 库
#include <stdio.h>
// 导入 SDL 库的头文件
#include <SDL.h>
// 一帧 PCM 数据有 1024 个采样点
// 每个采样 都是 2 通道 立体声 ( 左右声道 ) , 每个通道的采样都是 16 位 (bit) 也就是 2 字节 (Byte)
// 每次读取 2 帧 PCM 数据
// 1024 ( 采样数 ) * 2 ( 通道数 ) * 2 ( 2 字节 / 16 位 ) * 2 ( 帧数为 2 帧 )
// 每次从 本地 PCM 数据文件中读取 1024 * 2 * 2 * 2 字节的 音频 数据
#define PCM_BUFFER_SIZE (1024 * 2 * 2 * 2)
// 音频 PCM 数据缓存指针 , 该指针指向的堆内存中包含了完整的 PCM 文件数据
static Uint8 *s_audio_buf = NULL;
// 当前读取的位置 , 开始播放时指向 s_audio_buf 指针指向数据的首地址
static Uint8 *s_audio_pos = NULL;
// 缓存结束位置 , 指向 s_audio_buf 指针指向数据的 尾地址 , 防止数据越界出现 未知错误
static Uint8 *s_audio_end = NULL;
// 音频设备回调函数
void fill_audio_pcm(void *udata, Uint8 *stream, int len)
{
// 清空缓冲区 , 将流缓冲区初始化为 0 , 防止有干扰数据
SDL_memset(stream, 0, len);
// 确保读取数据时不会出现越界 , 读取到其它未知数据
if(s_audio_pos >= s_audio_end) // 如果数据已读取完毕
{
return; // 退出回调函数
}
// 计算剩余数据 : 数据够了就读预设长度 , 数据不够时只读取剩余数据
// 之前读取的数据都是 len 字节
// 最后的部分不足 len 字节时 , 读取 remain_buffer_len 字节数据
int remain_buffer_len = s_audio_end - s_audio_pos;
len = (len < remain_buffer_len) ? len : remain_buffer_len;
// 将数据拷贝到stream并调整音量
SDL_MixAudio(stream, s_audio_pos, len, SDL_MIX_MAXVOLUME/8);
printf("len = %dn", len); // 输出当前读取的数据长度
s_audio_pos = len; // 移动缓存指针到下一个位置
}
// 使用 ffmpeg 命令 提取 PCM 数据 :
// ffmpeg -i input.mp4 -codec:a pcm_s16le -ar 44100 -ac 2 -f s16le 44100_16bit_2ch.pcm
// 使用 ffplay 命令 播放 PCM 数据 , 播放 PCM 数据必须指定 采样率 / 通道数 / 采样位数
// ffplay -ar 44100 -ac 2 -f s16le 44100_16bit_2ch.pcm
#undef main
int main(int argc, char *argv[])
{
int ret = -1; // 返回值初始化为-1
FILE *audio_fd = NULL; // 文件指针初始化为空
SDL_AudioSpec spec; // SDL音频规格
const char *path = "44100_16bit_2ch.pcm"; // PCM文件路径
// 每次缓存的长度
size_t read_buffer_len = 0;
// 初始化SDL音频
if(SDL_Init(SDL_INIT_AUDIO)) // 初始化SDL音频支持
{
fprintf(stderr, "Could not initialize SDL - %sn", SDL_GetError()); // 输出错误信息
return ret; // 返回错误代码
}
// 打开PCM文件
audio_fd = fopen(path, "rb"); // 以只读模式打开PCM文件
if(!audio_fd)
{
fprintf(stderr, "Failed to open pcm file!n"); // 打开文件失败
goto _FAIL; // 跳转到失败处理
}
s_audio_buf = (uint8_t *)malloc(PCM_BUFFER_SIZE); // 为音频缓冲区分配内存
// 设置音频参数SDL_AudioSpec
spec.freq = 44100; // 采样频率为44100Hz
spec.format = AUDIO_S16SYS; // 采样点格式为16位系统格式
spec.channels = 2; // 2通道
spec.silence = 0; // 静音值为0
spec.samples = 1024; // 每次读取1024个采样点
spec.callback = fill_audio_pcm; // 设置音频回调函数
spec.userdata = NULL; // 用户数据为空
// 打开音频设备
if(SDL_OpenAudio(&spec, NULL))
{
fprintf(stderr, "Failed to open audio device, %sn", SDL_GetError()); // 打开音频设备失败
goto _FAIL; // 跳转到失败处理
}
// 开始播放音频
SDL_PauseAudio(0); // 取消音频暂停状态
int data_count = 0; // 数据计数器初始化为0
while(1)
{
// 从文件读取PCM数据
read_buffer_len = fread(s_audio_buf, 1, PCM_BUFFER_SIZE, audio_fd); // 读取PCM数据到缓存
if(read_buffer_len == 0)
{
break; // 如果没有更多数据,则退出循环
}
data_count = read_buffer_len; // 累加读取的数据总字节数
printf("now playing d bytes data.n",data_count); // 输出当前播放的数据字节数
s_audio_end = s_audio_buf read_buffer_len; // 更新缓存的结束位置
s_audio_pos = s_audio_buf; // 更新缓存的起始位置
// 主线程等待PCM数据被消耗
while(s_audio_pos < s_audio_end)
{
SDL_Delay(10); // 等待10毫秒
}
}
printf("play PCM finishn"); // 播放完成提示
// 关闭音频设备
SDL_CloseAudio(); // 关闭音频设备
_FAIL:
// 释放资源
if(s_audio_buf)
free(s_audio_buf); // 释放音频缓存内存
if(audio_fd)
fclose(audio_fd); // 关闭文件
// 退出SDL
SDL_Quit(); // 退出SDL库
return 0; // 返回成功代码
}
2、执行结果
由于播放的是音频 , 播放时没有窗口界面 ;
从视频中提取的 的 PCM 音频数据 , 拷贝到了 编译输出的可执行文件的根目录中 ;