理解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, &params)
        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音频-应用层基础

RK3588平台开发系列讲解(AUDIO篇)Android音频调试--tiny-alsa 工具

RK3588平台开发系列讲解(AUDIO篇)Android音频调试--tiny-alsa 工具