【音视频|ALSA】基于alsa-lib开发ALSA应用层程序--附带源码

😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀
🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C++、数据结构、音视频🍭
🤣本文内容🤣:🍭基于alsa-lib开发ALSA应用层程序🍭
😎金句分享😎:🍭盖士人读书,第一要有志,第二要有识,第三要有恒。有志则断不甘为下流,有识则知学问无尽,不敢以一得自足,如河伯之观海,如井蛙之窥天,皆无识者也。有恒则断无不成之事,此三者缺一不可。 ------《曾国藩家书》🍭

【音视频|ALSA】ALSA是什么?ALSA框架详细介绍
【音视频|ALSA】SS528开发板编译Linux内核ALSA驱动、移植alsa-lib、采集与播放usb耳机声音

目录


一、ALSA应用层开发基础知识

  • sample:样本,采样点。数字音频最小单位,其大小与位宽有关,一般为8bit(1个字节)、16bit(2个字节);
  • channel:声道,一般单声道(mono)和立体声(stereo),还有一些多声道如5.1声道。
  • frame:帧,一个完整的声音单元,即单次采样的所有声道的数据。frame=sample*channel;
    例如:48Khz、16位的立体声PCM流的1帧是4个字节。
  • sample rate:采样率,即每秒的采样次数。如果采样率为48kHz,则说明一秒采样48000帧。
  • period size:周期大小,是每次硬件中断之间的帧数。
  • buffer size:缓冲区大小,必须大于一个周期的大小。一般为周期大小的2倍。单位也是帧数。


例子:

结合上面的知识点,这里以48kHz、16bit的立体声音频流举例:

  • 16bit则每个样本为2个字节,
  • 立体声表示有2个声道,
  • 48kHz 表示每秒有48000个音频帧。

由此可以计算出每秒钟传输的数据大小:2 * 2 * 48000=192000字节;

现在如果ALSA每秒钟产生一个硬件中断,在每秒结束时,我们需要准备好192000字节;

如果它每半秒中断一次,对于同一个流,我们需要在每次中断时准备好192000/2 = 96000字节;

如果每100毫秒发生一次中断,我们需要在每次中断时准备好192000*(0.1/1) = 19200字节。

我们可以通过设置周期大小(以帧为单位)来控制PCM中断的产生时间。

如果我们将48kHz、16bit的立体声音频流的period size设置为4800帧(也就是48002 2=19200字节),则每19200字节就会产生一个中断,也就是100ms。

相应地,buffer size至少应为2*period_size = 2*4800= 9600帧(96002 2 = 38400字节)。

实际编程中,可能需要计算一个周期的总字节数period bytes,就是等于period size乘以每一帧的大小。同样的,buffer的总字节数buffer bytes等于buffer size乘以一帧大小。
如果已知音频的采样率、通道数、位宽、周期数,则buffer size、buffer time、period size、period time 这四个值可以相互推断出来:

以48000Hz采样率、2声道、16bit、4周期来举例,这样的音频流一秒钟的帧数是48000帧,如果buffer size是48000,则buffer time刚好就是一秒;如果buffer time是500ms,则buffer size是24000帧。

而period size=buffer size/周期数;period time=buffer time/周期数

二、alsa-lib常用函数

alsa-lib的函数声明在pcm.h,总共可以分为16个模块:

  • PCM Interface
  • Stream Information
  • Hardware Parameters
  • Software Parameters
  • Access Mask Functions
  • Format Mask Functions
  • Subformat Mask Functions
  • Status Functions
  • Description Functions
  • Debug Functions
  • Direct Access (MMAP) Functions
  • Helper Functions
  • Hook Extension
  • Scope Plugin Extension
  • Simple setup functions
  • Deprecated Functions

可以在官方文档查看对应的模块函数说明:

https://www.alsa-project.org/alsa-doc/alsa-lib/group___p_c_m.html

下表介绍一些常用的函数:

函数 说明
snd_pcm_open 根据pcm设备名字打开一个pcm设备
snd_pcm_hw_params_malloc 使用标准malloc分配无效的snd_pcm_hw_params_t
snd_pcm_hw_params_any 用PCM的完整配置空间填充参数。
snd_pcm_hw_params_set_access 将配置空间限制为仅包含一种访问类型。
snd_pcm_hw_params_set_format 将配置空间限制为仅包含一种格式。
snd_pcm_hw_params_set_channels 将配置空间限制为仅包含一个通道计数。
snd_pcm_hw_params_set_rate_near 将配置空间限制为具有最接近目标的速率。
snd_pcm_hw_params_get_buffer_time_max 从配置空间中提取最大缓冲时间。
snd_pcm_stream 获取PCM句柄的流
snd_pcm_hw_params_set_buffer_time_near 限制配置空间以使缓冲时间最接近目标。
snd_pcm_hw_params_set_period_time_near 限制配置空间以使周期时间最接近目标。
snd_pcm_hw_params 安装从配置空间中选择的一个PCM硬件配置,并调用snd_pcm_prepare。
snd_pcm_nonblock 设置非阻塞模式
snd_pcm_hw_params_get_period_size 从配置空间中提取周期大小。
snd_pcm_hw_params_get_buffer_size 从配置空间中提取周期大小。
snd_pcm_format_physical_width 返回存储PCM样本所需的位。

更多函数说明参考:

https://www.alsa-project.org/alsa-doc/alsa-lib/group___p_c_m___h_w___params.html

https://www.alsa-project.org/alsa-doc/alsa-lib/group___p_c_m.html

三、编写ALSA应用层程序

这小节介绍简单的ALSA应用程序开发流程,以及给出例子源码。

在编写代码前,可以先使用cat /proc/asound/card0/stream0查看ALSA设备支持的参数:

sh 复制代码
# cat /proc/asound/card0/stream0 
Rapoo Gaming Headset at usb-10300000.xhci_0-1.1, full speed : USB Audio

Playback:
  Status: Stop
  Interface 1
    Altset 1
    Format: S16_LE
    Channels: 2
    Endpoint: 1 OUT (ADAPTIVE)
    Rates: 48000, 44100

Capture:
  Status: Stop
  Interface 2
    Altset 1
    Format: S16_LE
    Channels: 1
    Endpoint: 2 IN (ASYNC)
    Rates: 48000, 44100

Playback:播放设备

Capture:录音设备

Interface:接口序号

Format:格式

Channels:通道数

Rates:支持的采样率

3.1 alsa播放程序开发--alsa-playback.c

开发流程:

  • 1、打开设备:调用 snd_pcm_open,指定类型为SND_PCM_STREAM_PLAYBACK,以及设备名称,打开设备;
  • 2、设置硬件参数
    设置存取方式、格式、通道数、采样率、缓冲时间、周期时间,最后将参数写入设备;
    如果有一些参数不清楚怎么设置,可以使用命令cat /proc/asound/card0/stream0查看支持的参数:
  • 3、播放音频
    每次往alsa驱动写入一个周期大小的字节,不足一周期的要填0;
  • 4、释放资源,关闭设备

下面是一个非常简单的ALSA播放音频的代码,复制后保存为alsa-playback.c,使用命令aarch64-mix210-linux-gcc alsa-playback.c -I /usr/lib/alsa-lib-1.2.10/include/ -L /usr/lib/alsa-lib-1.2.10/lib/ -l asound -lpthread -ldl -lm -o alsa-playback 已编译通过。

48000Hz-16bit-2ch-ChengDu.pcm 文件下载:https://download.csdn.net/download/wkd_007/88421282

c 复制代码
// alsa-playback.c
// aarch64-mix210-linux-gcc alsa-playback.c -I /usr/lib/alsa-lib-1.2.10/include/ -L /usr/lib/alsa-lib-1.2.10/lib/ -l asound -lpthread -ldl -lm -o alsa-playback

/*
* snd_pcm_hw_params_alloca 申请的内存在函数返回后会自动释放,不需要手动释放。这个函数会在栈上分配一块内存,函数返回后,栈上的内存会自动被回收。
*/
#include <stdio.h>
#include <alsa/asoundlib.h>

#define PCM_NAME	"hw:0,0"
#define PLAYBACK_FILE "48000Hz-16bit-2ch-ChengDu.pcm"

snd_pcm_hw_params_t *hw_params;
static unsigned int rate = 48000;           /* stream rate */

int set_hw_params(snd_pcm_t *handle, int format, int channels, snd_pcm_uframes_t *period_frames)
{
	int err = -1;
    // 分配硬件参数空间,调用 alloca 在栈分配内存,函数结束后自动释放,不需要调用 snd_pcm_hw_params_free
    snd_pcm_hw_params_alloca(&hw_params);

    //1、以默认值填充硬件参数
    if ((err = snd_pcm_hw_params_any(handle, hw_params)) < 0) {
        return err;
    }

    //2、 Restrict a configuration space to contain only real hardware rates.
    if ((err = snd_pcm_hw_params_set_rate_resample(handle, hw_params, 0)) < 0) {
        return err;
    }

    //3、设置存取方式为交叉存储
    if ((err = snd_pcm_hw_params_set_access(handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
        return err;
    }

    //4、设置格式,S16_LE等
    if ((err = snd_pcm_hw_params_set_format(handle, hw_params, format)) < 0) {
        return err;
    }

    //5、设置通道
    if ((err = snd_pcm_hw_params_set_channels(handle, hw_params, channels)) < 0) {
        return err;
    }

    //6、大致设置采样率
    unsigned int rrate;
    rrate =rate;
    if ((err = snd_pcm_hw_params_set_rate_near(handle, hw_params, &rrate, NULL)) < 0) 	   {
        return err;
    }
	
    //7、设置缓冲时间
	unsigned int buffer_time, period_time;
	// 先获取缓存时间
	if((err = snd_pcm_hw_params_get_buffer_time_max(hw_params, &buffer_time, 0))<0){
		return err;
	}
	
	if (buffer_time > 500000){
		buffer_time = 500000; // 500ms读取完整个buffer,结合下面代码一个周期就是 buffer_time/4=125ms,每个周期会产生一个中断
		printf("[%s %d] buffer_time=%d, irq=%d\n",__FILE__,__LINE__,buffer_time, buffer_time/4);
	}

	// 设置缓冲时间
	if ((err = snd_pcm_hw_params_set_buffer_time_near(handle, hw_params, &buffer_time, 0)) < 0) {
		return err;
	}
    
	// 8、设置周期时间,也就是中断时间
	period_time = buffer_time / 4;
	if ((err = snd_pcm_hw_params_set_period_time_near(handle, hw_params, &period_time, 0)) < 0) {
		return err;
	}

    // 9、将参数写入设备
    if ((err = snd_pcm_hw_params(handle, hw_params)) < 0){
        return err;
    }

    snd_pcm_uframes_t buffer_frames;
    snd_pcm_hw_params_get_buffer_size(hw_params, &buffer_frames);

    if(period_frames != NULL) {
		//获取一个周期有多少帧数据
		if((err =snd_pcm_hw_params_get_period_size(hw_params, period_frames, 0)) < 0){
			printf("cannot get period size (%s)\n", snd_strerror(err));
			return err;
		}
	}
	
	if(err = (snd_pcm_nonblock(handle, 1) < 0)){
		return err;
	}
	
	// 10、释放 snd_pcm_hw_params_malloc 分配的内存
	//snd_pcm_hw_params_free(hw_params);

    return 0;
}

int main()
{
	int err = -1;
	snd_pcm_t *playback_handle;
	snd_pcm_uframes_t period_frames; // 一周期的帧数
	
	// 1、打开设备
	if((err = snd_pcm_open(&playback_handle, PCM_NAME, SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
		printf("cannot snd_pcm_open (%s)\n",snd_strerror(err));
		return -1;
	}
	
	// 2、设置硬件参数
	set_hw_params(playback_handle, SND_PCM_FORMAT_S16_LE, 2, &period_frames);
	
	// 3、播放音频
	// 3.1 打开pcm文件
	int fd = open(PLAYBACK_FILE,O_RDONLY,0644);
	if(fd < 0){
		printf("open %s error!!!\n",PLAYBACK_FILE);
		return -1;
	}
	
	// 3.2 获取一周期的字节数
	const int period_bytes = snd_pcm_frames_to_bytes(playback_handle,period_frames);
	char *playback_buf = malloc(period_bytes);
	
	// 3.3 循环播放音频
	int readframes = 0;
	while(readframes = read(fd, playback_buf, period_bytes)) {
		//解决最后一个周期数据问题
		if(readframes < period_bytes) {
			memset(playback_buf+readframes, 0, period_bytes-readframes);
		}
		
		//向PCM写入数据,播放
		err = snd_pcm_writei(playback_handle, playback_buf, period_frames);
		if(err == -EPIPE) {
			snd_pcm_prepare(playback_handle);
			fprintf(stderr, "<<< snd_pcm_writei --> Buffer Underrun >>> \n");
			err = snd_pcm_writei(playback_handle, playback_buf, period_frames);
			if(err != period_frames) {
				printf("write to audio interface failede err:%d (period_frames:%d)\n",err,period_frames);
				break;
			}
		}
		else if(err != period_frames) {
			printf("write to audio interface failede err:%d (period_frames:%d)\n",err,period_frames);
			break;
		}
		//printf("process:playback wrote %d frames\n",period_frames);
		//usleep(100*1000);
		usleep(130*1000); //测试用,超过 125ms,会报错 Underrun
	}
	
	// 4.释放资源,关闭设备
	free(playback_buf);
	close(fd);
	snd_pcm_close(playback_handle);
	
	return 0;
}

3.2 alsa录制音频程序开发--alsa-capture.c

开发流程:

  • 1、打开设备:调用 snd_pcm_open,指定类型为SND_PCM_STREAM_CAPTURE,以及设备名称,打开设备;
  • 2、设置硬件参数
    设置存取方式、格式、通道数、采样率、缓冲时间、周期时间,最后将参数写入设备;
    如果有一些参数不清楚怎么设置,可以使用命令cat /proc/asound/card0/stream0查看支持的参数:
  • 3、读取音频
    每次从alsa驱动读取一个周期大小的字节;
  • 4、释放资源,关闭设备
c 复制代码
// alsa-capture.c
// aarch64-mix210-linux-gcc alsa-capture.c -I /usr/lib/alsa-lib-1.2.10/include/ -L /usr/lib/alsa-lib-1.2.10/lib/ -l asound -lpthread -ldl -lm -o alsa-capture

/*
* snd_pcm_hw_params_alloca 申请的内存在函数返回后会自动释放,不需要手动释放。这个函数会在栈上分配一块内存,函数返回后,栈上的内存会自动被回收。
*/
#include <stdio.h>
#include <alsa/asoundlib.h>

#define PCM_NAME	"hw:0,0"
#define CAPTURE_FILE "alsa-capture.pcm"

snd_pcm_hw_params_t *hw_params;
static unsigned int rate = 48000;           /* stream rate */

int set_hw_params(snd_pcm_t *handle, int format, int channels, snd_pcm_uframes_t *period_frames)
{
	int err = -1;
    // 分配硬件参数空间,调用 alloca 在栈分配内存,函数结束后自动释放,不需要调用 snd_pcm_hw_params_free
    snd_pcm_hw_params_alloca(&hw_params);

    //1、以默认值填充硬件参数
    if ((err = snd_pcm_hw_params_any(handle, hw_params)) < 0) {
        return err;
    }

    //2、 Restrict a configuration space to contain only real hardware rates.
    if ((err = snd_pcm_hw_params_set_rate_resample(handle, hw_params, 0)) < 0) {
        return err;
    }

    //3、设置存取方式为交叉存储
    if ((err = snd_pcm_hw_params_set_access(handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
        return err;
    }

    //4、设置格式,S16_LE等
    if ((err = snd_pcm_hw_params_set_format(handle, hw_params, format)) < 0) {
        return err;
    }

    //5、设置通道
    if ((err = snd_pcm_hw_params_set_channels(handle, hw_params, channels)) < 0) {
        return err;
    }

    //6、大致设置采样率
    unsigned int rrate;
    rrate =rate;
    if ((err = snd_pcm_hw_params_set_rate_near(handle, hw_params, &rrate, NULL)) < 0) 	   {
        return err;
    }
	
    //7、设置缓冲时间
	unsigned int buffer_time, period_time;
	// 先获取缓存时间
	if((err = snd_pcm_hw_params_get_buffer_time_max(hw_params, &buffer_time, 0))<0){
		return err;
	}
	
	if (buffer_time > 500000){
		buffer_time = 500000; // 500ms写完整个buffer,结合下面代码一个周期就是 buffer_time/4=125ms,每个周期会产生一个中断
		printf("[%s %d] buffer_time=%d, irq=%d\n",__FILE__,__LINE__,buffer_time, buffer_time/4);
	}

	// 设置缓冲时间
	if ((err = snd_pcm_hw_params_set_buffer_time_near(handle, hw_params, &buffer_time, 0)) < 0) {
		return err;
	}
    
	// 8、设置周期时间,也就是中断时间
	period_time = buffer_time / 4;
	if ((err = snd_pcm_hw_params_set_period_time_near(handle, hw_params, &period_time, 0)) < 0) {
		return err;
	}

    // 9、将参数写入设备
    if ((err = snd_pcm_hw_params(handle, hw_params)) < 0){
        return err;
    }

    snd_pcm_uframes_t buffer_frames;
    snd_pcm_hw_params_get_buffer_size(hw_params, &buffer_frames);

    if(period_frames != NULL) {
		//获取一个周期有多少帧数据
		if((err =snd_pcm_hw_params_get_period_size(hw_params, period_frames, 0)) < 0){
			printf("cannot get period size (%s)\n", snd_strerror(err));
			return err;
		}
	}
	
	if(err = (snd_pcm_nonblock(handle, 1) < 0)){
		return err;
	}
	
	// 10、释放 snd_pcm_hw_params_malloc 分配的内存
	//snd_pcm_hw_params_free(hw_params);

    return 0;
}

int main()
{
	int err = -1;
	snd_pcm_t *capture_handle;
	snd_pcm_uframes_t period_frames; // 一周期的帧数
	
	// 1、打开设备
	if((err = snd_pcm_open(&capture_handle, PCM_NAME, SND_PCM_STREAM_CAPTURE, 0)) < 0) {
		printf("cannot snd_pcm_open (%s)\n",snd_strerror(err));
		return -1;
	}
	
	// 2、设置硬件参数
	set_hw_params(capture_handle, SND_PCM_FORMAT_S16_LE, 1, &period_frames);
	
	// 3、获取音频
	// 3.1 打开录制文件
	int fd = open(CAPTURE_FILE,O_RDWR | O_TRUNC | O_CREAT,0644);
	if(fd < 0){
		printf("open %s error!!!\n",CAPTURE_FILE);
		return -1;
	}
	
	// 3.2 获取一周期的字节数
	const int period_bytes = snd_pcm_frames_to_bytes(capture_handle,period_frames);
	char *capture_buf = malloc(period_bytes);
	
	int count = 100; // 捕获100个周期
	int readframes = 0;
	while(count--) {
		//向PCM读一周期数据
		memset(capture_buf,0,period_bytes);
		if((readframes = snd_pcm_readi(capture_handle, capture_buf, period_frames)) < 0) {
			if(readframes == -EPIPE)
				printf("read from audio interface failed (%d), overrun, Need to read faster\n",readframes);
			else
				printf("read from audio interface failed (%d)\n",readframes);
			break;
		}
		printf("--process:capture read %d frames\n",readframes);
		write(fd,capture_buf,snd_pcm_frames_to_bytes(capture_handle,readframes));
		usleep(100*1000); 
		//usleep(130*1000); //测试用,超过 125ms,会报错 overrun
	}
	
	// 4、释放资源,关闭设备
	free(capture_buf);
	close(fd);
	snd_pcm_close(capture_handle);
	
	return 0;
}

四、XRUN( underrun和overrun)

在 ALSA 数据传输中,最容易出现的错误是 underrun 和 overrun。

  • underrun:pcm 播放的时候,接口 snd_pcm_writei 返回 -EPIPE,为 underrun(不足)
    出现这问题原因是应用准备的音频数据不够,比如,驱动需要播放需要 1026 帧数据,但应用只准备好了 1024 帧。可以根据采样率和buffer size、period size去调整;
  • overrun:录制音频的时候, 接口 snd_pcm_readi 返回 -EPIPE, 为 overrun(超载)
    alsa驱动一直往buffer里面写,但应用程序却读取的很慢,例如:驱动写了1026帧,而应用层只读取了1024帧。需要加快读取速度。或者调整buffer size、period size。

五、总结

文章介绍了alsa的基础知识,以及基于alsa-lib开发ALSA应用层程序的开发流程和alsa开发过程钟常见的报错,提供了简单的alsa应用层代码。


参考资料:

ALSA官网资料------FramesPeriods:https://alsa-project.org/main/index.php/FramesPeriods

【Linux&音频】Alsa音频编程【精华】:https://blog.csdn.net/u012183924/article/details/53407668

ALSA 音频数据传输 underrun 和 overrun:https://blog.csdn.net/qq_38350702/article/details/111995039

Linux应用开发【第八章】ALSA应用开发:https://blog.csdn.net/thisway_diy/article/details/121809633

如果文章有帮助的话,点赞👍、收藏⭐,支持一波,谢谢 😁😁😁

相关推荐
hflmwl88 小时前
Wise Data Recovery找回误删的文档照片音乐和视频
音视频·datarecovery·找回误删的文档照片音乐和视频·找回误删的文档照片音乐视频
技灵AI8 小时前
AI 视频转场提示词怎么写?让前后画面衔接更自然
人工智能·prompt·aigc·音视频
youandyouyou10 小时前
跨区培训的屏幕共享怎么做才稳?同屏同步与弱网优化实践
音视频·网络通信
开开心心就好10 小时前
课堂随机点名工具,绿色版免安装双击即用
java·服务器·前端·ffmpeg·音视频·perl·symfony
美狐美颜SDK开放平台11 小时前
直播APP集成美颜SDK,如何梳理第三方组件的数据权限?
开发语言·人工智能·深度学习·音视频·第三方美颜sdk
l143723326712 小时前
批量视频硬字幕擦除多少钱?影响报价的关键因素全解析
音视频
重生六道12 小时前
B站视频时长统计:一段JS脚本搞定分P/合集总时长(附倍速计算)
开发语言·javascript·音视频
音视频牛哥12 小时前
不换摄像机,也能接入 WebRTC?SmartMediaKit RTSP/RTMP 转 WHIP 技术实践
音视频·webrtc·smartmediakit·rtsp转whip·rtmp转whip·whip推流·rtsp转webrtc播放
lll235269177913 小时前
Android APP短视频音频提取,主流工具怎么选
ffmpeg·音视频
乐橙开放平台14 小时前
门店监控想集中看?乐橙含光支持多品牌摄像头跨店管理
网络·笔记·物联网·自动化·音视频·智能家居