author: hjjdebug
date: 2025年 04月 15日 星期二 14:02:05 CST
description: 最简单的使用SDL2 播放原始音频数据程序
文章目录
- 1.最简单的播放音频的程序是什么样子的?
- 2. 怎样用SDL 来编写音频播放器代码?
- 2.1 SDL播放音频核心代码:混音函数
- 2.2 先看看音频播放的可能的两种框架. 同步播放,异步播放
- 2.3: 回调函数 fill_audio()
- 2.4: SDL 播放音频的工作流程
- 2.4.1. SDL_Init() 初始化音频设备
- 2.4.2. SDL_OpenAudio() 打开音频设备
- 2.4.3. 播放声音. SDL_PauseAudio(0)
- 3.附件: 完整的音频播放程序
- 4. 小结:
- 5.执行结果:
1.最简单的播放音频的程序是什么样子的?
如果有一个接口函数,给它文件名,调用接口函数它就开始播放,这个最简单.
但这是应用级的,而且早就有人做好了,你要这个接口可能没什么用途.
例如: ffplay,
你只要调用这个程序后面再跟上一个文件名就可以播放了.
你可能会说, 我要的是一个函数接口,我要编程去调用它,不是要一个应用程序.
这也不难, 你可以把ffplay 做为一个进程来调用,实现你编程调用的目的.
关于如何调用一个进程及与进程通讯就不在这里讲了.
如果你不要进程调用,就要函数接口调用也不难, 因为ffplay是开源的.把它的代码拿过来.
用你的代码去调用它的main函数代码,就是填充一个argc,argv[]参数,就能播放音频了.
你还是觉得没有学到东西,因为编程总是要与数据打交道的,你只是传递了个文件名就搞定了,
做得也太少了. 为啥? 因为你调用的是应用级的,别人都给你做好了.
想了解一下音频的播放原理,建议你用SDL 来编程.
SDL 编程非常简单,又能让你了解到原理.
2. 怎样用SDL 来编写音频播放器代码?
2.1 SDL播放音频核心代码:混音函数
SDL_MixAudio(dst_buf,audio_src,len,SDL_MIX_MAXVOLUME);
就是把声音数据与目标缓冲区的数据相混合. MixAudio
只所以不叫向目标copy数据,是因为它不是简单的copy,而是叠加进来,
这样如果多次调用该函数,就是多个音轨的数据相混合,你能够听到多个声音而不是一个声音.
SDL_MIX_MAXVOLUME 是控制音量的,这个值是128,
给128这个参数表示本次混音按最大音量来混音.
那混音函数怎么调用呢? 目标缓冲dst_buf又在哪里呢?
先不忙,
2.2 先看看音频播放的可能的两种框架. 同步播放,异步播放
-
同步播放: 我有了音频数据,就让它播放, 等播放完了,我再取音频数据,再让他播放.
这个架构不错,简单, 有一个毛病就是主进程一但调用你的播放函数,就陷进去了,
出不来了,非得等你把数据消费完了才能返回来, 假如给你一个frame数据去播放,
这一个函数调用下来就花了零点几秒. 在计算机的世界里,零点几秒就是了不起的资源浪费.
我主程序干不了别的了. -
异步播放: 有一个线程它在瓦拉瓦拉瓦拉的播放这我的音频,它把数据播放完了给我要数据,我copy给它
让它继续播就是了.
它怎么给我要数据? 这就是回调函数. 本博就来介绍这种方式.
2.3: 回调函数 fill_audio()
void fill_audio(void *udata,Uint8 *dst_buf,int len){
SDL_memset(dst_buf, 0, len); //清理目标数据
//混音播放,不是简单的copy而是叠加到目标,例如多音轨混音后会听到多个声音
SDL_MixAudio(dst_buf,audio_src,len,SDL_MIX_MAXVOLUME);
audio_src += len; //audio_src 地址不断增加
available_len -= len; //available_len 不断减少
}
那怎样调用这个回调函数呢?
2.4: SDL 播放音频的工作流程
做好初始化,打开设备,准备数据.播放音频
2.4.1. SDL_Init() 初始化音频设备
if(SDL_Init(SDL_INIT_AUDIO)!=0)
万事都有开头,要初始化音频设备, 这样就能使用这个硬件了.
就好比说先看看你的机器上有没有安装声卡,没有声卡,初始化音频设备肯定就失败了
2.4.2. SDL_OpenAudio() 打开音频设备
if (SDL_OpenAudio(&audio_spec, NULL)<0) //打开音频设备
打开音频设备需要传递给它一个参数,就是指定它的工作模式之意.
就好比说你有一台复印机,有几种工作模式,你选择了一种模式,然后打开了电源.
这个spec 参数,重要的是音频三要素: 采样率,通道数,采样点格式.
freq=44100
channels=2
format=AUDIO_S16SYS
还有一个samples=1024 采样数决定了缓存的大小
另外一个关键参数就是回调函数call_back, 设置了回调函数,它就可以工作在异步模式.
播放器需要数据了就调用回调函数填充数据.
2.4.3. 播放声音. SDL_PauseAudio(0)
想象一下你搬出了音频cd播放机,插上了电源, 按下了开关.下一步该干什么?
放上cd,按下播放键.
我们把音频文件打开(pcm,裸数据),读到缓冲中src_buffer, 这就是放cd 的过程
前边的回调函数就会从我们的数据源缓存中读取数据.
按下播放键, 就是调用 SDL_PauseAudio(0)
数据就开始不断的被消费,我们就听到美妙的音乐了.
我们主程序还需要干什么?
主程序就是要关注一下数据缓存,发现数据被用光时,赶紧重新把水池子填满,然回调继续能从中取数据
3.附件: 完整的音频播放程序
参考了雷神的代码, 做了以下改动:
- 移植到linux下
- 修改,删除了一些变量名称,参数名称使更容易理解.
- 使整个流程更加严谨.
不足百行代码,才好逐行分析
#include <stdio.h>
#include <stdbool.h>
#include "SDL2/SDL.h"//下面这两个变量, 主线程和播放线程都会修改它们的值,严格意义上是需要mutex 保护的,
//但由于在这个简单模型中,主线程啥事都不干,专等着填数,而播放线程得到一帧数据,要消耗一帧的时间,
//所以不会造成冲突, 为简单期间,就不加锁了.
Uint8 *audio_src;
int available_len; //只所以这样命名,因为它就是可用的大小/* SDL_PauseAudio Callback 函数* 参数说明* udata: 没有使用,在audio_spec中用户指定的指针* dst_buf: 目标指针地址, 该例实测是一个堆栈区地址,这样命名是因为我知道它是目标指针* len: dst_buf的长度,该例实测为:4096,一个frame的大小* 功能: 可见是内部播放线程开辟了一个缓存,用来储存一帧数据,* 线程通过该函数向用户索要数据* */
void fill_audio(void *udata,Uint8 *dst_buf,int len){ (void) udata;SDL_memset(dst_buf, 0, len);//如果没有数据了,就返回,这也是防止主线程,工作线程数据冲突的一种简单方式//同时,由于dst_buf被清0,就静音了if(available_len==0) return; len=(len>available_len?available_len:len); //重新计算长度,看能提供多少数据//混音播放,不是简单的copy而是叠加到目标,例如多音轨混音后会听到多个声音SDL_MixAudio(dst_buf,audio_src,len,SDL_MIX_MAXVOLUME); audio_src += len; //audio_src 地址不断增加available_len -= len; //available_len 不断减少
} int main()
{//Init// if(SDL_Init(SDL_INIT_AUDIO | SDL_INIT_TIMER)) { if(SDL_Init(SDL_INIT_AUDIO )) { printf( "Could not initialize SDL - %s\n", SDL_GetError()); return -1;}//SDL_AudioSpec, 要根据媒体实际参数设置采用率,声道,数据格式formatSDL_AudioSpec audio_spec;audio_spec.freq = 44100; audio_spec.format = AUDIO_S16SYS; audio_spec.channels = 2; audio_spec.silence = 0; audio_spec.samples = 1024; //采样数决定了一帧的大小audio_spec.callback = fill_audio; //回调函数来填充数据//打开音频设备if (SDL_OpenAudio(&audio_spec, NULL)<0){ printf("can't open audio.\n"); return -1; } //打开文件FILE *fp=fopen("flat_44.1k_s16le.pcm","rb+");if(fp==NULL){printf("cannot open this file\n");return -1;}//开辟一1帧的数据缓存,一帧有1024次采样//大小等于samples * channels *sizeof(1个采样点的大小), AUDIO_S16SYS 1个点2bytes//缓存的大小也可以开大一点,例如存2个frame,则2次回调才能读完缓存数据,给1个,给16个都能工作unsigned int src_buffer_size=4096*16; char *src_buffer=(char *)malloc(src_buffer_size);int file_pos=0;bool has_play=false;int read_size;while(1){//读数据到缓存if(feof(fp)) //到达文件尾{ //读不到所需的大小,就是读到了文件尾,重新从头再读fseek(fp, 0, SEEK_SET);read_size=fread(src_buffer, 1, src_buffer_size, fp);file_pos=read_size;}else{read_size=fread(src_buffer, 1, src_buffer_size, fp);file_pos+=read_size;}//主线程在修改这两个参数时,此是应保证播放线程不会调用回调也同时修改这两个参数//这个模型可以做到这一点,为简单期间,所以没有加锁保护.audio_src = (Uint8*)src_buffer;available_len =read_size;//Playprintf("play positon is %10d.\n",file_pos-read_size);if(has_play==false) //这个has_play 要不要都行,因为SDL_PauseAudio()多调用几遍也无所谓{SDL_PauseAudio(0); //播放音频has_play=true;}
//主程序不断的查询available_len,当发现为0时,就立即从文件中再读数据
//否则,等待,所以它大部分时间是等待,偶尔填充一下缓冲区,供消费者消费while(available_len>0)SDL_Delay(1); //这就是最普通的delay 函数,sleep 函数}return 0;
}
4. 小结:
音频播放器. 采用异步播放,写好回调函数fill_data,
然后进行初始化, sdl_init, 打开设备 open_audio
然后读数据到缓存,开始播放PauseAudio
5.执行结果:
一边听着美妙的音乐,一边不断打印播放到的数据位置
$ ./main
play positon is 0.
play positon is 65536.
play positon is 131072.
play positon is 196608.
play positon is 262144.
play positon is 327680.
…