【Linux开发】基于ALSA库实现音频播放

安装ALSA播放音频

1、环境搭建

1.1、交叉编译器

aarch64-linux-gnu-gcc 交叉编译工具链,可以在其他架构的系统中,编译生成 64 位 arm 架构可执行的程序,aarch64-linux-gnu-gcc 是由 Linaro 公司基于 GCC 推出的的 ARM 交叉编译工具,可用于交叉编译 ARMv8 64 位目标中的裸机程序、u-boot、Linux kernel、根文件系统和应用层程序。

可以先从 linaro 的官网下载相应版本的 gcc-linaro 交叉编译工具https://releases.linaro.org/components/toolchain/(32位交叉编译器编译的可执行文件也能在64位开发板上运行,但是需要注意如果用到动态库就要配置好32位所需的动态库才能运行,如果编译成静态文件可以直接运行),然后复制到cp /usr/local/arm中进行解压(哪里解压都行,主要是把解压后的文件都放在/usr/local/arm目录下即可),解压后还需要手动配置环境变量,输入命令sudo vim /etc/profile修改配置文件,在最后面添加以下语句配置环境变量,这样才能找到命令:

export PATH=$PATH:/usr/local/arm/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin

在运行以下命令加载更新配置:

# source /etc/profile

或者直接通过apt进行下载,命令如下:

sudo apt-get install gcc-aarch64-linux-gnu

一般重新加载后就可以使用bin下的编译程序命令了,可以使用以下命名查看版本来检验命令是否生效:

# aarch64-linux-gnu-gcc -v

如果有显示版本所有命令可以正常使用,但是有时重新打开一个终端发现命令又找不到了,可能还是环境变量配置问题,需要在当前用户的家目录下的.bashrc文件也加入上面的环境变量

1.2、ALSA库

如果直接复制程序进行编译程序找不到asoundlib.h文件,可能是因为没有alsa-lib库,alsa-lib库下载地址:http://www.alsa-project.org/main/index.php/Main_Page

下载好这个安装包并解压并不意味着安装好了这个软件,这只是该软件的源代码,而不是可以直接在系统运行的程序或者库,因此还需要进行编译和安装。

下载完成后通过下面命令解压:

tar -jxvf alsa-lib-1.2.11.tar.bz2

然后进入alsa-lib文件夹进行编译配置:

./configure --host=arm-linux-gnueabihf --disable-python --prefix=/opt/arecord/
  • host:交叉编译器(此处用的交叉编译器跟上面下载的不一样,这是我之前下的32位交叉编译器,需要注意一下,如果开发板是64位并且没有支持32位的动态库,最好全文都选择使用64位的交叉编译器)
  • disable:不编译什么(此处不编译alsa库中与python相关的,节省编译时间)
  • prefix:要将alsa安装到哪个目录中,后面执行make install将会把alsa-lib安装到该目录中

运行这条命令之后,通常会看到一系列的检测结果和配置摘要,如果一切正常,你可以继续运行 make (make前最好进入root模式并配置好交叉编译器环境变量)来编译 alsa-lib,然后运行 make install 来安装它

编译过程错误:(如果报找不到交叉编译器命令可能是环境变量没配,如果一直找不到可以把所有用户下的.bashrc文件都配置一遍,然后记得source生效,如果报文件创建失败可能是权限不够)

在编译时用到了交叉编译器,但是交叉编译器的环境变量是配置在~/.bashrc文件下的,也就是用户自己的环境变量,因此在进行make编译时如果使用sudo make也就是超级用户来编译,可能会出现找不到交叉编译器命令的情况,因为root用户的交叉编译器环境变量没配置,但是编译时可能对某些文件无法操作,需要root权限,因此最好进入root目录配置下面的.bashrc添加交叉编译器环境变量,同时su root进入超级用户在进入alsa源码进行make(尝试不进入root使用sudo make来编译还是会出现找不到交叉编译器的错误,只有进入root在make才行)

安装完成后就可以在/opt/arecord/目录下看到alsa的源码程序了

2、编写代码

2.1、向驱动写入数据

snd_pcm_writei():用于向设备缓冲区写入数据

直接向设备写入用户提供的缓冲区数据实现,适用较小的数据块,处理大型音频流时可能会导致性能问题。

snd_pcm_sframes_t snd_pcm_writei(snd_pcm_t *pcm, const void *buffer, snd_pcm_uframes_t size);
  • pcm:设备控制句柄

  • buffer:要写入的数据地址(为要写入的音频文件的缓冲区地址)

  • size:一个周期写入的帧数量,单位为帧

  • return:返回值为已写入的帧数量,如果>0&&<size,则说明没有写入一个周期的帧数量,可能是缓冲区内存不够,驱动没来得及读出去数据

    返回*-EPIPE*表示缓冲区下溢(underrun),写入速度慢于设备驱动读取速度,导致设备没数据可以读,此时要提高写入的速度

snd_pcm_mmap_writei():用于向设备缓冲区写入数据

与上面的写入函数不同在于,该函数使用内存映射技术实现,运行应用程序直接访问内核中的音频缓冲区,节约了中间的数据复制步骤,适合处理大型音频数据。

  • 参数与上面一致

在做音频播放时,出现调用snd_pcm_writei写入一直返回-22(Invalid argument)的错误码,后面改用这个snd_pcm_mmap_writei来写入就没有出现这个问题了(具体原因未知)

snd_pcm_recover():用于从任何ALSA函数库返回的错误状态中回复

这个函数比较特别,比如当出现缓冲区欠载(underrun occurred:在播放时出现该情况原因在于,应用层传给底层的音频数据时的速度太慢,导致播放缓冲区音频数据被播放完了没有音频可以播放,就会出现欠载)时可以使用该函数恢复。

但是当我使用UDP从客户端传输音频到服务端,服务端播放时出现欠载时,我是通过降低每个周期的帧数来提高音频数据传输的速率,以此来解决欠载的问题(虽然依然会有欠载的情况,但不会像之前出现严重欠载和杂波),从一开始的每个周期1024帧降低到一个周期256帧,虽然传播的音频数据量没变,但传播的速度从原来的1024帧传一次,到256帧传一次,这样可以及时向缓冲区补充音频数据,如果1024帧传一次的话,则每个传输的时间间隔较长,当缓冲区的音频数据被播放完后会有较长时间的欠载,就会出现哔哔哔的情况。

2.2、完整播放代码

#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>
#include <alsa/asoundlib.h>

bool debug_msg(int result, const char *str);
void open_music_file(const char *path_name);
void pcm_init(void);
void play_music(void);

snd_pcm_t *handle;						// 控制句柄
snd_pcm_hw_params_t *params;			// 参数结构体
snd_pcm_uframes_t frames = 1024;		// 一个周期的帧数
snd_pcm_access_t access_mode = SND_PCM_ACCESS_MMAP_NONINTERLEAVED ;// 访问模式:交错访问
snd_pcm_format_t format = SND_PCM_FORMAT_S16_LE;				// 采样位数:16位,小端存储
int channel = 1;						// 通道数(单声道设为1,立体双声道为2)
unsigned int simple_rate = 44100;		// 采样率

FILE *fp;			// 音乐文件指针变量
char *buff = NULL;	// 缓冲区指针
int buffer_size;	// 缓冲区大小

int ret;	// 记录函数返回值
// 定义WAV格式结构体
struct WAV_HEADER
{
	char 		chunk_id[4]; 	// riff 标志号
	uint32_t 	chunk_size; 	// riff长度
	char 		format[4]; 		// 格式类型(wav)
	
	char 		sub_chunk1_id[4]; 	// fmt 格式块标识
	uint32_t 	sub_chunk1_size; 	// fmt 长度 格式块长度。
	uint16_t 	audio_format; 		// 编码格式代码							常见的 WAV 文件使用 PCM 脉冲编码调制格式,该数值通常为 1
	uint16_t 	num_channels; 		// 声道数 								单声道为 1,立体声或双声道为 2
	uint32_t  	sample_rate; 		// 采样频率 							每个声道单位时间采样次数。常用的采样频率有 11025, 22050 和 44100 kHz。
	uint32_t 	byte_rate; 			// 传输速率 							该数值为:声道数×采样频率×每样本的数据位数/8。播放软件利用此值可以估计缓冲区的大小。
	uint16_t	block_align; 		// 数据块对齐单位						采样帧大小。该数值为:声道数×位数/8。播放软件需要一次处理多个该值大小的字节数据,用该数值调整缓冲区。
	uint16_t 	bits_per_sample; 	// 采样位数								存储每个采样值所用的二进制数位数。常见的位数有 4、8、12、16、24、32

	char 		sub_chunk2_id[4]; 	
	uint32_t 	sub_chunk2_size; 	// 音频数据大小
} wav_header;

int main(int argc, char *argv [])
{
	if((ret = getopt(argc,argv,"m:")) == 'm'){
		open_music_file(optarg);
	} else {
		printf("argument error!\n");
		return 0;
	}
	pcm_init();
	
	play_music();	// 播放音乐
	
	fclose(fp);
    snd_pcm_drain(handle);
    snd_pcm_close(handle);
	return 0;
}

void play_music(void)
{
	int writed_size;	// 记录已写入的数据量
	printf("Start playing.............\n");
    do{
		snd_pcm_prepare(handle);
		snd_pcm_wait(handle, 5000);// 单位微妙
		if((writed_size + (frames*2)) > buffer_size){
			ret =  snd_pcm_writei(handle, buff, (buffer_size-writed_size)/2);
			printf("playback ended!\n");
			break;
		}
		else{
			ret =  snd_pcm_writei(handle, buff, frames);
		}
		snd_pcm_drain(handle);
		buff += (ret * 2);			// 修改音频数据写入位置
		writed_size += (ret * 2);	// 记录已写入的音频数据字节个数
        if( ret>0 && ret<frames ){
            printf("short write\n");
        }else if(ret == -EPIPE){
            printf("underrun occurred\n");
            snd_pcm_prepare(handle);
        }else if (ret < 0){
            printf("error %d from writei: %s\n",ret,snd_strerror(ret));
            break;
        }
    }while(1);
}

// 用于根据错误码解析错误原因
bool debug_msg(int result, const char *str)
{
	if(result < 0){
		printf("err: %s error!, result = %d, err_info = %s \n", str, result, snd_strerror(result));
		exit(1);
	}
	return true;
}
// 打开音频文件并读取音频数据
void open_music_file(const char *path_name)
{
	int wav_header_size; 	// 接收wav_header数据结构体的大小
	fp = fopen(path_name, "rb");
	if(fp == NULL){
		printf("music file is NULL \n");
		fclose(fp);
		exit(1);
	}
	// 把文件指针定位到文件的开头处
	fseek(fp, 0, SEEK_SET);
	// 读取文件,并解析文件头获取有用信息
	wav_header_size = fread(&wav_header, 1, sizeof(wav_header), fp);
	printf("wav文件头结构体大小:	%d 			\n", wav_header_size);
	printf("RIFF标志:		\t 		%c%c%c%c 	\n", wav_header.chunk_id[0], wav_header.chunk_id[1], wav_header.chunk_id[2], wav_header.chunk_id[3]);
	printf("文件大小: 		\t 		%d			\n", wav_header.chunk_size);
	printf("文件格式: 		\t 		%c%c%c%c 	\n", wav_header.format[0], wav_header.format[1], wav_header.format[2], wav_header.format[3]);
	printf("格式块标识:	\t\t\t 	%c%c%c%c	\n", wav_header.sub_chunk1_id[0], wav_header.sub_chunk1_id[1], wav_header.sub_chunk1_id[2], wav_header.sub_chunk1_id[3]);
	printf("格式块长度:	\t\t\t 	%d 			\n", wav_header.sub_chunk1_size);
	printf("编码格式代码: 	\t\t\t 	%d 			\n", wav_header.audio_format);
	printf("声道数:			\t 		%d			\n", wav_header.num_channels);
	printf("采样频率:		\t 		%d			\n", wav_header.sample_rate);
	printf("传输速率: 		\t\t 	%d 			\n", wav_header.byte_rate);
	printf("数据块对齐单位: \t\t\t 	%d			\n", wav_header.block_align);
	printf("采样位数(长度):		\t 		%d			\n", wav_header.bits_per_sample);
	// 根据音频数据块大小创建缓冲区
	buffer_size = wav_header.sub_chunk2_size;
	buff = (char *)malloc(buffer_size*2);
	// 以1个字节为单位从fp文件中读取buffer_size个字节数据放到缓存中
	ret = fread(buff, 1, buffer_size, fp);
	if(ret == 0){
		printf("end of music file input! \n");
		exit(1);
	}
	if(ret < 0){
		printf("read pcm from file! \n");
		exit(1);
	}
}
void pcm_init(void)
{
	char *pcm_name;	
	/* 获取设备名称并打开pcm设备 */
	pcm_name = strdup("plughw:0,0");	
    debug_msg(snd_pcm_open(&handle, pcm_name,SND_PCM_STREAM_PLAYBACK, 0),"打开pcm设备");
    /* 分配一个硬件参数对象 */
    debug_msg(snd_pcm_hw_params_malloc(&params),"给硬件参数分配空间");
    debug_msg(snd_pcm_hw_params_any(handle, params),"初始化参数");		/* 使用默认值填充参数对象. */
    /* 设置硬件参数 */
    debug_msg(snd_pcm_hw_params_set_access(handle, params,access_mode),"设置模式");	/* 非交错模式 non Interleaved mode */
    debug_msg(snd_pcm_hw_params_set_format(handle, params,format),"设置采样位数");	/* 采样位数 Signed 16-bit little-endian format */
    debug_msg(snd_pcm_hw_params_set_channels(handle, params, channel),"设置通道数");/* 通道数 one channels  */
    debug_msg(snd_pcm_hw_params_set_rate_near(handle, params,&simple_rate, NULL),"设置采样率");/* 采样率 44100 bits/second sampling rate (CD quality) */
    debug_msg(snd_pcm_hw_params_set_period_size_near(handle,params, &frames, NULL),"设置周期帧数");/* 周期帧数一般为 1024 */
    debug_msg(snd_pcm_hw_params(handle, params),"将设置参数写入驱动");
}

3、交叉编译

交叉编译时需要注意确保链接了 ALSA 库,通过在编译命令中添加 -l asound 来完成链接,不然会出现找不到相关函数的情况,具体命令如下:

# arm-linux-gnueabihf-gcc alsa_play.c -I /opt/arecord/include/ -L /opt/arecord/lib/ -l asound
  • -I(大写的i):告诉编译器在哪里查找包含的头文件
  • -L:告诉编译器在哪里查找链接时需要的库文件(默认在/usr/lib或者/usr/lib/arm-linux-gnueabihf/)
  • -l(小写的L):告诉编译器链接时链接到asound库,即ALSA库(需要注意-l选项后面的库名不包括前缀lib和后缀(.so或者.a),因此只需要填asound,就会自动找到libasound.so库)

需要注意的是不能加-static选项编译成静态可执行文件,因为本身就通过-l链接到了libasound.so动态库,如果还加static就会报错一直显示找不到asound

4、运行

将生成的可执行文件放到开发板上,chmod 777 a.out赋予执行权限,发现一运行就报*-bash: ./a.out: No such file or directory*,通过命令file a.out命令查看文件的格式:

root@firefly:~# file a.out
a.out: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 2.6.31, BuildID[sha1]=c945a5b76662a9b14175436f4c20661846e09d4d, with debug_info, not stripped
  • 32-bit: 表示这是一个32位架构的可执行文件(通过uname -m显示系统的处理器架构,返回aarch64为64位,所以可能是因为开发板上的系统可能没有32位的libasound动态库)
  • ARM:这表示该文件是为 ARM 架构编译的
  • dynamically linked:这表示 a.out 是一个动态链接的可执行文件。它依赖于外部的共享库(.so 文件)来运行(说明该可执行文件再编译时确实链接到了动态库,我们在连接时也确实通过-l asound连接到了动态库)

因此需要注意可执行文件的格式,是否与开发板位数一致,如果位数不一致但是没有动态连接,为一个静态可执行文件也可以正常执行,但是如果连接到了动态库,程序与开发板系统位数又不一样,就会导致出现找不到文件或其他无法运行的情况(此时可能就要重头开始并选择好相应位数的交叉编译器重新编译了)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值