源码
/**
* SDL2播放PCM
*
*
* 本程序使用SDL2播放PCM音频采样数据。SDL实际上是对底层绘图
* API(Direct3D,OpenGL)的封装,使用起来明显简单于直接调用底层
* API。
* 测试的PCM数据采用采样率44.1k, 采用精度S16SYS, 通道数2
*
* 函数调用步骤如下:
*
* [初始化]
* SDL_Init(): 初始化SDL。
* SDL_OpenAudio(): 根据参数(存储于SDL_AudioSpec)打开音频设备。
* SDL_PauseAudio(): 播放音频数据。
*
* [循环播放数据]
* SDL_Delay(): 延时等待播放完成。
*
*/
#include <stdio.h>
#include <SDL.h>
#include <iostream>
// 每次读取2帧数据, 以1024个采样点一帧 2通道 16bit采样点为例
// 一个采样点为16bit
#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)
{
std::cout << "fill_audio_pcm" << std::endl;
SDL_memset(stream, 0, len);
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 = %d\n", len);
s_audio_pos += len; // 移动缓存指针
}
// 提取PCM文件
// ffmpeg -i input.mp4 -t 20 -codec:a pcm_s16le -ar 44100 -ac 2 -f s16le 44100_16bit_2ch.pcm
// 测试PCM文件
// ffplay -ar 44100 -ac 2 -f s16le 44100_16bit_2ch.pcm
#undef main
int main(int argc, char* argv[])
{
int ret = -1;
FILE* audio_fd = NULL;
SDL_AudioSpec spec;//音频设备
const char* path = "44100_16bit_2ch.pcm";
// 每次缓存的长度
size_t read_buffer_len = 0;
//SDL initialize
if (SDL_Init(SDL_INIT_AUDIO)) // 支持AUDIO
{
fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
return ret;
}
//打开PCM文件
fopen_s(&audio_fd,path, "rb");
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; // 采样频率
spec.format = AUDIO_S16SYS; // 采样点格式
spec.channels = 2; // 2通道
spec.silence = 0;
spec.samples = 1024; // 23.2ms -> 46.4ms 每次读取的采样数量,多久产生一次回调和 samples
spec.callback = fill_audio_pcm; // 回调函数
spec.userdata = NULL;
//打开音频设备
if (SDL_OpenAudio(&spec, NULL))
{
fprintf(stderr, "Failed to open audio device, %s\n", SDL_GetError());
//goto _FAIL;
}
//play audio
SDL_PauseAudio(0);
int data_count = 0;
while (1)
{
// 从文件读取PCM数据
read_buffer_len = fread(s_audio_buf, 1, PCM_BUFFER_SIZE, audio_fd);
if (read_buffer_len == 0)
{
break;
}
data_count += read_buffer_len; // 统计读取的数据总字节数
printf("now playing %10d bytes data.\n", data_count);
s_audio_end = s_audio_buf + read_buffer_len; // 更新buffer的结束位置
s_audio_pos = s_audio_buf; // 更新buffer的起始位置
//the main thread wait for a moment
while (s_audio_pos < s_audio_end)
{
SDL_Delay(10); // 等待PCM数据消耗
}
}
printf("play PCM finish\n");
// 关闭音频设备
SDL_CloseAudio();
_FAIL:
//release some resources
if (s_audio_buf)
free(s_audio_buf);
if (audio_fd)
fclose(audio_fd);
//quit SDL
SDL_Quit();
return 0;
}
音频播放的时机
当使用SDL_PauseAudio(0)时, 表示音频开始播放
-参数 0:
当参数为 0 时,表示恢复音频播放。相反,如果传入 1,则表示暂停音频播放。
SDL_PauseAudio(0);
读取数据
如果对c++指针不熟悉的, 可能这里不太明白, 在一开始, 就声明了pcm的三个字段, 其中一个是缓存, 剩下的是起始位置和结束位置, !!! 这里注意, 他们使用的类型都是Uint8*, 这是指向一段内存的地址, 如果使用s_audio_buf可以存储缓存, 那么其他两个也可以
// 音频PCM数据缓存
static Uint8* s_audio_buf = NULL;
// 目前读取的位置
static Uint8* s_audio_pos = NULL;
// 缓存结束位置
static Uint8* s_audio_end = NULL;
这个函数的功能是将缓存的数据存放到stream中, 但是这里使用的并不是s_audio_buf, 而是s_audio_pos, 其实s_audio_pos也指向缓存(这里刚刚解释过了)
// 拷贝数据到stream并调整音量
SDL_MixAudio(stream, s_audio_pos, len, SDL_MIX_MAXVOLUME / 8);
什么时候更新数据
音频的数据和视频还不太一样, 视频是以帧为单位播放的, 可以指定单位时间内播放的帧的数量并且视频帧是需要在显示屏上停留一段微小的时间, 但是音频是连续的, 无法直接给定时间. 当给stream的值消耗完成后, 及s_audio_pos ==s_audio_end, 重新从文件中获取数据
// 等待PCM数据消耗,
while (s_audio_pos < s_audio_end)
{
SDL_Delay(10); // 每次循环延时10ms
}
pcm采样率
首先, pcm是一种纯文本格式, 也就是说, pcm的播放速度是人为指定的, 这个就是我们使用的采样率
spec.freq = 44100; // 采样频率
当使用不同采样率的时候, 播放速度是不一样的, 可以通过计时器来验证一下
当采样频率比较小的时候, 播放时间就会比较长, 但是并不是所有的音频都可以通过修改Player的采样频率来实现是否倍数播放, 比如非纯文本格式的mp3是不可以的, 这样做的后果只是video的清晰度而已
粉丝福利,博主耗时2个月整理了一份详细的音视频开发学习教程,涵盖了音视频开发FFmmpeg、流媒体客户端、流媒体服务器、WebRTC、Android NDK开发、IOS音视频开发等等全栈技术栈,并提供了配套的免费领取C++音视频学习资料包、技术视频/代码,内容包括(FFmpeg ,WebRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs流媒体服务器,音视频通话等等)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓