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


1604

被折叠的 条评论
为什么被折叠?



