最简单的使用SDL2 播放原始音频数据程序


author: hjjdebug

date: 2025年 04月 15日 星期二 14:02:05 CST

description: 最简单的使用SDL2 播放原始音频数据程序


文章目录

    • 1.最简单的播放音频的程序是什么样子的?
    • [2. 怎样用SDL 来编写音频播放器代码?](#2. 怎样用SDL 来编写音频播放器代码?)
      • [2.1 SDL播放音频核心代码:混音函数](#2.1 SDL播放音频核心代码:混音函数)
      • [2.2 先看看音频播放的可能的两种框架. 同步播放,异步播放](#2.2 先看看音频播放的可能的两种框架. 同步播放,异步播放)
      • [2.3: 回调函数 fill_audio()](#2.3: 回调函数 fill_audio())
      • [2.4: SDL 播放音频的工作流程](#2.4: SDL 播放音频的工作流程)
        • [2.4.1. SDL_Init() 初始化音频设备](#2.4.1. SDL_Init() 初始化音频设备)
        • [2.4.2. SDL_OpenAudio() 打开音频设备](#2.4.2. SDL_OpenAudio() 打开音频设备)
        • [2.4.3. 播放声音. SDL_PauseAudio(0)](#2.4.3. 播放声音. SDL_PauseAudio(0))
    • [3.附件: 完整的音频播放程序](#3.附件: 完整的音频播放程序)
    • [4. 小结:](#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 先看看音频播放的可能的两种框架. 同步播放,异步播放

  1. 同步播放: 我有了音频数据,就让它播放, 等播放完了,我再取音频数据,再让他播放.

    这个架构不错,简单, 有一个毛病就是主进程一但调用你的播放函数,就陷进去了,

    出不来了,非得等你把数据消费完了才能返回来, 假如给你一个frame数据去播放,

    这一个函数调用下来就花了零点几秒. 在计算机的世界里,零点几秒就是了不起的资源浪费.

    我主程序干不了别的了.

  2. 异步播放: 有一个线程它在瓦拉瓦拉瓦拉的播放这我的音频,它把数据播放完了给我要数据,我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.附件: 完整的音频播放程序

参考了雷神的代码, 做了以下改动:

  1. 移植到linux下
  2. 修改,删除了一些变量名称,参数名称使更容易理解.
  3. 使整个流程更加严谨.
    不足百行代码,才好逐行分析
c 复制代码
#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, 要根据媒体实际参数设置采用率,声道,数据格式format
	SDL_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;
		//Play
		printf("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.

...

相关推荐
Panesle4 小时前
HunyuanCustom:文生视频框架论文速读
人工智能·算法·音视频·文生视频
程序员JerrySUN11 小时前
驱动开发硬核特训 · Day 30(下篇): 深入解析 lm48100q I2C 音频编解码器驱动模型(基于 i.MX8MP)
linux·驱动开发·架构·音视频
读心悦16 小时前
5000字总结 HTML5 中的音频和视频,关羽标签、属性、API 和最佳实践
前端·音视频·html5
东风西巷17 小时前
BLURRR剪辑软件免费版:创意剪辑,轻松上手,打造个性视频
android·智能手机·音视频·生活·软件需求
weixin_4462608517 小时前
视觉革命来袭!ComfyUI-LTXVideo 让视频创作更高效
人工智能·音视频
拧螺丝专业户17 小时前
外网访问内网海康威视监控视频的方案:WebRTC + Coturn 搭建
音视频·webrtc·监控视频
追随远方1 天前
Android平台FFmpeg音视频开发深度指南
android·ffmpeg·音视频
Oliverro1 天前
嵌入式音视频通话EasyRTC基于WebRTC技术驱动智能带屏音箱:开启智能交互新体验
人工智能·音视频
佩奇的技术笔记1 天前
AI编程: 使用Trae1小时做成的音视频工具,提取音频并识别文本
音视频·ai编程
路溪非溪1 天前
音频类网站或者资讯总结
音视频