理解ALSA:概览
Posted 快乐的池塘里面有只小青蛙
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了理解ALSA:概览相关的知识,希望对你有一定的参考价值。
文章目录
1 ALSA概览图
2 流传输时的ALSA栈
2.1 一个最小的录音程序和一个最小的播放程序
为方便说明,让我们用tinyalsalib写一个最简单的录音程序来录5秒的音频(PCM数据)并输出到stdout,然后写一个最简单的音频播放程序来播放来自stdin的PCM数据。
— sample1.c —
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tinyalsa/asoundlib.h>
int main(void)
unsigned int card = 0;
unsigned int device = 0;
struct pcm *pcm = NULL;
struct pcm_config config;
unsigned int size = 0;
char *buffer = NULL;
int duration_secs = 5;
int n = 0;
memset(&config, 0, sizeof(config));
config.channels = 2;
config.rate = 48000;
config.period_size = 1024;
config.period_count = 4;
config.format = PCM_FORMAT_S16_LE;
pcm = pcm_open(card, device, PCM_IN, &config);
if (!pcm || !pcm_is_ready(pcm))
fprintf(stderr, "Unable to open PCM device (%s)\\n", pcm_get_error(pcm));
goto err;
size = pcm_frames_to_bytes(pcm, pcm_get_buffer_size(pcm));
buffer = malloc(size);
if (!buffer)
fprintf(stderr, "Unable to allocate %u bytes\\n", size);
goto err;
n = pcm_frames_to_bytes(pcm, duration_secs * config.rate) / size;
while (n-- && !pcm_read(pcm, buffer, size))
fwrite(buffer, 1, size, stdout);
err:
free(buffer);
pcm_close(pcm);
return 0;
编译:
gcc sample1.c -ltinyalsa -ldl
运行:
./a.out > test.pcm
录到的声音被保存成test.pcm
文件。
现在让我们写一个最简单的程序去播放这个文件。
— sample2.c —
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tinyalsa/asoundlib.h>
int main(void)
unsigned int card = 0;
unsigned int device = 0;
struct pcm_config config;
struct pcm *pcm = NULL;
char *buffer = NULL;
unsigned int size;
int num_read;
memset(&config, 0, sizeof(config));
config.channels = 2;
config.rate = 48000;
config.period_size = 1024;
config.period_count = 4;
config.format = PCM_FORMAT_S16_LE;
pcm = pcm_open(card, device, PCM_OUT, &config);
if (!pcm || !pcm_is_ready(pcm))
fprintf(stderr, "Unable to open PCM device %u (%s)\\n",
device, pcm_get_error(pcm));
goto err;
size = pcm_frames_to_bytes(pcm, pcm_get_buffer_size(pcm));
buffer = malloc(size);
if (!buffer)
fprintf(stderr, "Unable to allocate %d bytes\\n", size);
goto err;
while ((num_read = fread(buffer, 1, size, stdin)) > 0)
if (pcm_write(pcm, buffer, num_read))
fprintf(stderr, "Error playing sample\\n");
break;
err:
free(buffer);
pcm_close(pcm);
return 0;
编译:
gcc sample2.c -ltinyalsa -ldl
运行:
./a.out < test.pcm
我们可以听到刚刚录的5秒声音(test.pcm)从喇叭里播了出来!
注:这里写的两个程序,我们假设card=0,device=0,并且这个设备支持48k 16bit 2ch录音和播放。如果你的card 0 device 0不支持这些参数,这个两个程序会不工作,需要调整合适的参数。
接下来,让我们看看每一个alsa函数的调用栈是什么样的。
2.2 pcm_open()栈
用户的pcm_open()
相当于先对ASoC各个驱动模块startup()
,再做hw_params()
。
pcm_open()
pcm->fd = open("/dev/snd/pcmC0D0c")
snd_pcm_capture_open()
snd_pcm_open(SNDRV_PCM_STREAM_CAPTURE)
snd_pcm_open_file()
snd_pcm_open_substream()
substream->ops->open()
soc_pcm_open()
cpu_dai->driver->ops->startup()
platform->driver->ops->open()
codec_dai->driver->ops->startup()
rtd->dai_link->ops->startup()
ioctl(pcm->fd, SNDRV_PCM_IOCTL_HW_PARAMS, ¶ms)
snd_pcm_hw_params_user()
snd_pcm_hw_params()
substream->ops->hw_params()
soc_pcm_hw_params()
rtd->dai_link->ops->hw_params()
dai->driver->ops->hw_params()
platform->driver->ops->hw_params()
ioctl(pcm->fd, SNDRV_PCM_IOCTL_SW_PARAMS, &sparams)
2.3 pcm_read()栈
用户的pcm_read()
相当于做从内核缓冲区到用户缓冲区的copy_to_user()
。即把硬件写到内核缓冲区的数据拷贝到用户缓冲区。(mmap模式例外,其没有数据拷贝的动作,性能更好。)
pcm_read()
if (!pcm->running)
pcm_start()
pcm->running = 1
ioctl(pcm->fd, SNDRV_PCM_IOCTL_READI_FRAMES, &x)
snd_pcm_lib_read()
snd_pcm_lib_read1(transfer)
transfer(substream, appl_ofs, data, offset, frames)
snd_pcm_lib_read_transfer()
substream->ops->copy() *or* copy_to_user()
2.4 pcm_start()栈
用户的pcm_start()
相当于对ASoC的各个驱动模块做prepare()
和trigger(START)
动作。
pcm_start()
ioctl(pcm->fd, SNDRV_PCM_IOCTL_PREPARE)
snd_pcm_prepare()
snd_pcm_do_prepare()
substream->ops->prepare()
soc_pcm_prepare()
rtd->dai_link->ops->prepare()
platform->driver->ops->prepare()
codec_dai->driver->ops->prepare()
cpu_dai->driver->ops->prepare()
ioctl(pcm->fd, SNDRV_PCM_IOCTL_START)
snd_pcm_action_lock_irq()
snd_pcm_do_start()
substream->ops->trigger(substream, SNDRV_PCM_TRIGGER_START)
soc_pcm_trigger()
codec_dai->driver->ops->trigger()
platform->driver->ops->trigger()
cpu_dai->driver->ops->trigger()
rtd->dai_link->ops->trigger()
2.5 pcm_close()栈
用户的pcm_close()
相当于对ASoC的各个驱动模块做trigger(STOP)
, hw_free()
和shutdown()
动作。
pcm_close()
close(pcm->fd)
snd_pcm_release()
snd_pcm_release_substream()
snd_pcm_drop(substream)
snd_pcm_stop(substream, SNDRV_PCM_STATE_SETUP)
snd_pcm_do_stop()
substream->ops->trigger(substream, SNDRV_PCM_TRIGGER_STOP)
soc_pcm_trigger()
codec_dai->driver->ops->trigger()
platform->driver->ops->trigger()
cpu_dai->driver->ops->trigger()
rtd->dai_link->ops->trigger()
if (substream->hw_opened)
if (substream->ops->hw_free != NULL)
substream->ops->hw_free(substream)
substream->ops->close(substream)
soc_pcm_close()
cpu_dai->driver->ops->shutdown()
codec_dai->driver->ops->shutdown()
rtd->dai_link->ops->shutdown()
platform->driver->ops->close()
substream->hw_opened = 0
3 播放时的数据传输
ALSA内核缓冲区是一个环状缓冲区。播放的时候,appl_ptr
为写指针,hw_ptr
为读指针。
3.1 正常情况
appl_ptr
v
|XXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXX|
v
hw_ptr
hw_ptr
以采样率的速度移动,也就是说硬件以采样率的速度消耗数据,并且每读走一个period
数据,发一次中断给内核。appl_ptr
应该要能够移动地足够快,这样才可以保证缓冲区里有足够的数据供硬件消耗。当缓冲区数据满了的时候,appl_ptr
会被hw_ptr
阻塞。然而当缓冲区数据空的时候,hw_ptr
一般来说不会被appl_ptr
阻塞。
3.2 start_threshold
appl_ptr
v
|XXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXX|----------------|----------------|
v
hw_ptr
start_threshold
决定了硬件什么时候开始消耗数据(开始输出)。通常它的值被设为内核缓冲区的一半或者整个缓冲区,这样的话起播的时候就不容易发生underrun。但是start_threshold
越大的话,延时就会越大。对于延时敏感的应用,这个值要仔细考虑。
3.3 stop_threshold
appl_ptr
v
|----------------|XXXXXXXXXXXXXXXX|----------------|----------------|
v
hw_ptr
stop_threshold
决定了硬件什么时候停止消耗数据(停止输出)。当缓冲区里空位置的数量超过了这个值,播放就自动停止。如果这个值被为整个缓冲区,那么播放会到所有数据都消耗光了才停止。这里有个风险,如果hw_ptr
停止不及时,一些垃圾数据会被读走播放出来产生杂音。所有stop_threshold
经常被设为比整个缓冲区稍微小一点,这样能尽量保证没有杂音输出,然而最后几帧也会相应丢失。
3.4 xrun的情况
appl_ptr
v
|----------------|----------------|----------------|----------------|
v
hw_ptr
当缓冲区里所有的数据被消耗光了,而应用又不写入新的数据,underrun就会发生,同时alsa core会进入xrun状态。应用会得到-EPIPE
的错误,需要做一些动作来将xrun状态恢复成正常状态。
4 录音时的数据传输
ALSA内核缓冲区是一个环状缓冲区。录音的时候,appl_ptr
为读指针,hw_ptr
为写指针。
4.1 正常情况
appl_ptr
^
|----------------|XXXXXXXXXXXXXXXX|----------------|----------------|
^
hw_ptr
hw_ptr
以采样率的速度移动,也就是说硬件以采用率的速度向缓冲区里写数据,并且每写入一个period数据,发一次中断给内核。appl_ptr
应该能够移动地足够快,缓冲区才不会被复写。当缓冲区数据空的时候,appl_ptr
会被hw_ptr
阻塞;然而当缓冲区数据满的时候,hw_ptr
一般来说不会被appl_ptr
阻塞。
4.2 start_threshold
appl_ptr
^
|XXXXXXXXXXXXXXXX|----------------|----------------|----------------|
^
hw_ptr
start_threshold
决定了应用开始读取数据的时间点。通常它被设为1帧,意思是只要缓冲区里有1帧,应用就可以把他读走。
4.3 stop_threshold
appl_ptr
^
|XXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXX|----------------|
^
hw_ptr
stop_threshold
决定了硬件什么时候停止填数据。当缓冲区里的数据量超过了这个值时,录音就自动停止。当这个值被设为整个缓冲区,那么意味着当数据填满整个缓冲区时,hw_ptr
才停止。
4.4 xrun的情况
appl_ptr
^
|XXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXX|
^
hw_ptr
当缓冲区里的数据满了,应用又不读走,overrun就会发生,并且alsa core会进入xrun状态。应用会得到-EPIPE
的错误,需要做一些动作来将xrun状态恢复成正常状态。
5 xrun发生的时候,应该要做什么呢?
无论对于录音还是播放,ALSA处理xrun的流程都是一样的。首先内核的硬件中断处理函数是第一个发现xrun的人。从调用栈能看出,当avail超过一个阈值时,立刻做xrun处理,其中就包含第一时间做snd_pcm_stop()。
irq_handler()
snd_pcm_period_elapsed()
snd_pcm_update_hw_ptr0()
snd_pcm_update_state()
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
avail = snd_pcm_playback_avail(runtime)
else
avail = snd_pcm_capture_avail(runtime)
if (avail >= runtime->stop_threshold)
xrun(substream)
snd_pcm_stop(substream, SNDRV_PCM_STATE_XRUN)
用户程序随即发现ioctl(SNDRV_PCM_IOCTL_READI_FRAMES)
失败,并且errno == EPIPE
。在下次循环中,重新做pcm_start()
,以使xrun状态恢复。
pcm_read()
for (;;)
if (!pcm->running)
if (pcm_start(pcm) < 0)
fprintf(stderr, "start error");
return -errno;
if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_READI_FRAMES, &x))
pcm->prepared = 0;
pcm->running = 0;
if (errno == EPIPE)
/* we failed to make our window -- try to restart */
pcm->underruns++;
continue;
return oops(pcm, errno, "cannot read stream data");
return 0;
6 有用的调试信息
$ tinycap test.wav
Capturing sample: 1 ch, 48000 hz, 16 bit
$ ls /proc/asound/card0/pcm0c/sub0
hw_params
info
status
sw_params
$ cat /proc/asound/card0/pcm0c/sub0/info
card: 0
device: 0
subdevice: 0
stream: CAPTURE
id: MultiMedia1 (*)
name:
subname: subdevice #0
class: 0
subclass: 0
subdevices_count: 1
subdevices_avail: 0
$ cat /proc/asound/card0/pcm0c/sub0/hw_params
access: RW_INTERLEAVED
format: S16_LE
subformat: STD
channels: 1
rate: 48000 (48000/1)
period_size: 1024
buffer_size: 4096
$ cat /proc/asound/card0/pcm0c/sub0/sw_params
tstamp_mode: ENABLE
period_step: 1
avail_min: 1
start_threshold: 1
stop_threshold: 40960
silence_threshold: 0
silence_size: 0
boundary: 1073741824
$ cat /proc/asound/card0/pcm0c/sub0/status
state: RUNNING
owner_pid : 6004
trigger_time: 1197453.884234423
tstamp : 1197760.259512913
delay : 0
avail : 0
avail_max : 1024
-----
hw_ptr : 14685184
appl_ptr : 14685184
Ubuntu 命令行配置默认声卡、录音播放与音量调节
参考技术A alsa设置默认声卡理解和使用Alsa的配置文件
alsa的配置文件是alsa.conf位于/usr/share/alsa目录下,通常还有/usr/share/alsa/card和/usr/share/alsa/pcm两个子目录用来设置card相关的参数,别名以及一些PCM默认设置。
免驱蓝牙适配器
用户配置
https://alsa.opensrc.org/Asoundrc
在home目录添加 .asoundrc文件:
全局配置
在文件最后添加一下内容
1)调节常用命令
ubuntu操音量调整命令amixer
2)使用softvol控制主音量
Softvol
如何使用softvol控制主音量
如果声卡无法控制硬件的音量(如PCM5102),或者驱动程序不支持声卡的此功能,则可以定义一个新的虚拟pcm设备,该设备将控制软件方面的音量。
Ubuntu Linux:从命令行和键盘快捷方式增加减少音量
使用是pulseaudio的自带命令pactl
1.系统不播放音乐,连接过了10分钟,蓝牙自带断开
2.root用户无法调节系统音量
以上是关于理解ALSA:概览的主要内容,如果未能解决你的问题,请参考以下文章
嵌入式linux/android alsa_aplay alsa_amixer命令行用法
linux中的alsa工具与Android中的tinyalsa工具
android audio/linux alsa音频-应用层基础
android audio/linux alsa音频-应用层基础