最简单的使用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.

...

相关推荐
九流下半1 小时前
window wsl 环境下编译openharmony,HarmonyOS 三方库 FFmpeg
windows·ffmpeg·harmonyos·编译·openharmony·三方库
小雪人8283 小时前
如何批量旋转视频90度?
音视频
apihz5 小时前
通用图片搜索-搜狗源免费API接口使用指南
android·java·python·php·音视频
aqi005 小时前
FFmpeg开发笔记(七十四)Windows给FFmpeg集成二维码图像的编解码器
ffmpeg·音视频·直播·流媒体
Antonio9155 小时前
【音视频】HLS拉流抓包分析
音视频
肥or胖6 小时前
Visual Studio 2022 上使用ffmpeg
ide·ffmpeg·visual studio
9527华安19 小时前
FPGA实现SDI转LVDS视频发送,基于GTP+OSERDES2原语架构,提供工程源码和技术支持
fpga开发·音视频·lvds·gtp·sdi·oserdes2
王者鳜錸1 天前
基于Selenium和FFmpeg的全平台短视频自动化发布系统
selenium·ffmpeg·音视频
aqi001 天前
FFmpeg开发笔记(七十三)Windows给FFmpeg集成MPEG-5视频编解码器
ffmpeg·音视频·直播·流媒体