声音。从机械振动到ALSA SoC层





在SberDevices,我们制造的设备可以让您听音乐,看电影等等。可以想象,没有声音,这完全没有兴趣。让我们看看从学校物理到Linux中的ALSA子系统,设备中的声音发生了什么。



我们听到的声音是什么?为了完全简化,这些是到达我们鼓膜的空气颗粒的振动。他们的大脑当然会转化为悦耳的音乐或电单车司机从窗户经过的声音,但现在让我们停止振动。



追溯到19世纪,人们意识到您可以尝试记录声音的振动,然后再进行再现。



首先,让我们看看最早的录音设备之一是如何工作的。





留声机及其发明家托马斯·爱迪生

图片来源



这里的一切都很简单。他们拿起一个圆筒,用铝箔纸包裹。然后,他们拿出一些锥形的东西(使其声音更大),并在末端贴上一层膜。小针头附着在膜上。针头靠在箔上。然后,受过专门训练的人扭动圆柱体,并向谐振器说了些什么。由薄膜驱动的针在金属箔上形成凹痕。如果足以均匀地扭转圆柱体,那么膜振动幅度就取决于圆柱体上“缠绕”时间的依赖性。







要播放信号,您只需要从头开始再次转动圆柱体-针就会掉入凹槽中,并将记录的振动传递到膜片,再传递到谐振器。因此,我们听到了录音。您可以在YouTube上轻松找到发烧友的有趣帖子。



过渡到电力



现在让我们看一些更现代的东西,但不是很复杂。例如,一个卷轴麦克风。现在,空气的振荡会改变磁体在线圈内部的位置,并且由于电磁感应,我们在输出端得到了磁体(进而是膜)的振荡幅度随时间变化的依赖性。直到现在,这种依赖性不是通过箔片上的凹陷来表示,而是通过麦克风输出端上的电压对时间的依赖性来表示。







为了能够将这种波动表示形式存储在计算机内存中,必须将它们离散化。这是通过特殊的硬件-模数转换器(ADC)来完成的。 ADC能够在一秒钟内多次存储输入上的电压值(高达ADC整数运算的分辨率),并将其写入存储器。每秒此类采样的数量称为采样率。典型值为8000 Hz-96000 Hz。



我们将不讨论ADC的细节,因为它值得单独撰写一系列文章。让我们继续讨论最主要的事情-Linux驱动程序和各种设备工作的所有声音都以幅度与时间的关系形式精确表示。这种记录格式称为PCM(脉冲编码调制)。对于持续时间为1 / sample_rate的每个时间片,将指示声音幅度的值。 .wav文件是由PCM组成的。



带有音乐的.wav文件的PCM可视化示例,其中水平轴是时间,垂直轴是信号幅度:







由于我们的板卡具有扬声器的立体声输出,因此您需要学习如何在一个.wav文件中存储立体声:左右声道。这里的一切都很简单-示例将像这样交替:







这种存储数据的方式称为交错。还有其他方法,但是我们现在不再考虑。



现在,让我们找出为组织设备之间的数据传输所需要的电信号。并不需要太多:



  1. 位时钟(BCLK)是一个时钟信号(或时钟),硬件可通过该时钟信号确定何时发送下一个位。
  2. 帧时钟(FCLK或也称为LRCLK)是一种定时信号,设备可以通过它了解何时需要开始发送另一个通道。
  3. 数据就是数据本身。






例如,我们有一个具有以下特征的文件:

  • 样本宽度= 16位;
  • 采样率= 48000 Hz;
  • 渠道= 2。


然后,我们需要设置以下频率值:

  • FCLK = 48000赫兹;
  • BCLK = 48000 * 16 * 2赫兹。


为了传输更多的信道,使用了TDM协议,该协议与I2S的不同之处在于,不再要求FCLK具有50%的占空比,并且上升沿仅设置属于不同信道的样本数据包的开始。



一般方案



右侧是amlogic s400板,您可以将其连接到扬声器。它安装了上游Linux内核。我们将处理此示例。



我们的电路板包含一个TAS5707PHPR DAC连接到的SoC(amlogic A113x)总体方案如下所示:



SoC可以做什么:

  • SoC具有3个引脚:BCLK,LRCLK,DATA;
  • 您可以通过SoC的特殊寄存器配置CLK引脚,以使其具有正确的频率;
  • 您也可以对此SoC说:“这是内存中的地址。有PCM数据。通过DATA线一点一点地发送此数据。” 该存储区将称为hwbuf。


要播放声音,Linux驱动程序会告诉SoC在BCLK和LRCLK线上设置哪些频率。另外,Linux驱动程序会告诉您hwbuf的位置。然后,DAC(TAS5707)通过DATA线接收数据,并将其转换为两个模拟电信号。然后,这些信号通过一对导线{analog +; 模拟}}分成两个扬声器。



转向Linux



我们准备继续讨论该电路在Linux中的外观。首先,有一个“库”用于在Linux中处理声音,该库分布在内核和用户空间之间。它被称为ALSA,我们将考虑其名称。 ALSA的本质是用户空间和内核在使用声音设备的界面上“达成一致”。



定制的ALSA库使用ioctl接口与内核交互。使用在/ dev / snd /目录中创建的pcmC {x} D {y} {c,p}设备。这些设备由必须由SoC供应商编写的驱动程序创建。例如,这是amlogic s400上此文件夹的内容:



# ls /dev/snd/
controlC0    pcmC0D0p   pcmC0D0   pcmC0D1c   pcmC0D1p   pcmC0D2c


以pcmC {x} D {y} {c,p}的名义:

X-声卡号(可能有几个);

Y-卡上接口的编号(例如pcmC0D0p可以负责通过tdm接口播放扬声器,而pcmC0D1c可以通过不同的硬件接口记录来自麦克风的声音);

p-表示播放声音的设备(播放);

c-表示用于录制声音的设备(捕获)。



在我们的情况下,pcmC0D0p设备与回放I2S接口完全对应。D1是spdif,D2是pdm麦克风,但我们不再赘述。



设备树



声卡描述以device_tree [arch / arm64 / boot / dts / amlogic / meson-axg-s400.dts]开头:



sound {
    compatible = "amlogic,axg-sound-card";
    model = "AXG-S400";
    audio-aux-devs = <&tdmin_a>, <&tdmin_b>,  <&tdmin_c>,
             <&tdmin_lb>, <&tdmout_c>;

    dai-link-6 {
        sound-dai = <&tdmif_c>;
        dai-format = "i2s";
        dai-tdm-slot-tx-mask-2 = <1 1>;
        dai-tdm-slot-rx-mask-1 = <1 1>;
        mclk-fs = <256>;
        codec-1 {
            sound-dai = <&speaker_amp1>;
        };
    };
    dai-link-7 {
        sound-dai = <&spdifout>;
        codec {
            sound-dai = <&spdif_dit>;
        };
    };
    dai-link-8 {
        sound-dai = <&spdifin>;
        codec {
            sound-dai = <&spdif_dir>;
        };
    };
    dai-link-9 {
        sound-dai = <&pdm>;
        codec {
            sound-dai = <&dmics>;
        };
    };
};


&i2c1 {
    speaker_amp1: audio-codec@1b {
        compatible = "ti,tas5707";
        reg = <0x1b>;
        reset-gpios = <&gpio_ao GPIOAO_4 GPIO_ACTIVE_LOW>;
        #sound-dai-cells = <0>;
    };
};
&tdmif_c {
    pinctrl-0 = <&tdmc_sclk_pins>, <&tdmc_fs_pins>,
            <&tdmc_din1_pins>, <&tdmc_dout2_pins>,
            <&mclk_c_pins>;
    pinctrl-names = "default";
    status = "okay";
};


在这里,我们看到那3个将出现在/ dev / snd中的设备:tdmif_c,spdif,pdm。



声音将通过的设备称为dai-link-6。它将在TDM驱动程序的控制下工作。问题出现了:我们正在谈论如何通过I2S,然后是TDM传输声音。这很容易解释:如上所述,I2S仍然是相同的TDM,但是对LRCLK占空比和通道数有明确的要求-应该有两个。然后,TDM驱动程序将读取dai-format =“ i2s”字段;并且会理解他需要以I2S模式工作。



接下来,指示使用Speaker_amp1结构将哪个DAC(在Linux内部将其称为“编解码器”)安装在板上。请注意,它会立即指示连接到哪条I2C线(请勿与I2S混淆!)。沿着这条线,放大器将被打开并从驱动器进行调谐。



tdmif_c结构描述了哪些SoC引脚将充当I2S接口。



ALSA SoC层



对于内部具有音频支持的SoC,Linux具有ALSA SoC层。它允许您描述编解码器(请记住,这就是用ALSA术语称呼的任何DAC),允许您指定这些编解码器的连接方式。



Linux内核术语中的编解码器称为DAI(数字音频接口)。SoC中的TDM / I2S接口本身也称为DAI,并且以类似的方式进行操作。



驱动程序使用struct snd_soc_dai描述编解码器。编解码器描述中最有趣的部分是用于设置TDM传输参数的操作。它们位于此处:struct snd_soc_dai-> struct snd_soc_dai_driver-> struct snd_soc_dai_ops。让我们考虑最重要的理解领域(sound / soc / soc-dai.h):



struct snd_soc_dai_ops {
    /*
     * DAI clocking configuration.
     * Called by soc_card drivers, normally in their hw_params.
     */
    int (*set_sysclk)(struct snd_soc_dai *dai,
        int clk_id, unsigned int freq, int dir);
    int (*set_pll)(struct snd_soc_dai *dai, int pll_id, int source,
        unsigned int freq_in, unsigned int freq_out);
    int (*set_clkdiv)(struct snd_soc_dai *dai, int div_id, int div);
    int (*set_bclk_ratio)(struct snd_soc_dai *dai, unsigned int ratio);
    ...
公开TDM时钟的功能。这些功能通常由SoC供应商实现。



...
int (*hw_params)(struct snd_pcm_substream *,
    struct snd_pcm_hw_params *, struct snd_soc_dai *);
...
最有趣的功能是hw_params()。

为了根据我们尝试播放的PCM文件的参数配置所有SoC硬件,这是必需的。她将稍后从上述组中调用功能来安装TDM时钟。



...
int (*trigger)(struct snd_pcm_substream *, int,
    struct snd_soc_dai *);
...
并且此功能在配置编解码器之后采取了最后一步-将编解码器置于活动模式。



完全相同的结构描述了将模拟声音输出到扬声器的DAC。在这种情况下,snd_soc_dai_ops将配置DAC以接收正确格式的数据。DAC的设置通常是通过I2C接口完成的。



在结构的设备树中指定的所有编解码器,

dai-link-6 {
    ...
    codec-1 {
        sound-dai = <&speaker_amp1>;
    };
};


-可以有很多,它们被添加到一个列表中并附加到/ dev / snd / pcm *设备。这是必需的,以便在播放声音时,内核可以绕过所有必需的编解码器驱动程序并配置/启用它们。



每个编解码器必须告诉您它支持哪些PCM参数。它使用以下结构进行操作:

struct snd_soc_pcm_stream {
    const char *stream_name;
    u64 formats;            /* SNDRV_PCM_FMTBIT_* */
    unsigned int rates;     /* SNDRV_PCM_RATE_* */
    unsigned int rate_min;      /* min rate */
    unsigned int rate_max;      /* max rate */
    unsigned int channels_min;  /* min channels */
    unsigned int channels_max;  /* max channels */
    unsigned int sig_bits;      /* number of bits of content */
};


如果链中的任何编解码器不支持特定参数,则所有操作都会以错误结尾。



可以在sound /soc/meson/axg-tdm-interface.c中查看对应于amlogic s400的TDM驱动程序实现TAS5707编解码器驱动程序的实现在声音/ soc /编解码器/ tas571x.c中



用户部分



现在,让我们看看当用户想要播放声音时会发生什么。自定义ALSA实现的一个易于学习的示例是tinyalsa可以在此处查看以下所有源代码。

包括tinyplay实用程序。要播放声音,您需要运行:



bash$ tinyplay ./music.wav -D 0 -d 0
(-D和-d选项表示应通过/ dev / snd / pcmC0D0p播放声音)。



发生了什么?

这是一个简短的框图,并附有解释:







  1. [用户空间]解析.wav标头,以查找正在播放的文件的PCM参数(采样率,位宽,通道)。我们将所有参数添加到struct snd_pcm_hw_params中。
  2. [用户空间]打开设备/ dev / snd / pcmC0D0p。
  3. [userspace] ioctl(…, SNDRV_PCM_IOCTL_HW_PARAMS ,…), PCM- .
  4. [kernel] PCM-, . :

    • ;
    • .
  5. , /dev/snd/pcmC0D0p ( ), .
  6. [userspace] , PCM-.
  7. [userspace] ioctl(…, SNDRV_PCM_IOCTL_WRITEI_FRAMES, …). I WRITEI , PCM- interleaved-.
  8. [kernelspace] , /dev/snd/pcmC0D0p , .
  9. [内核空间]使用copy_from_user()将用户buf复制到hwbuf(请参阅常规方案)。
  10. [用户空间]转到6。


通过搜索单词SNDRV_PCM_IOCTL_,可以查看ioctl内核部分的实现。



结论



现在我们有了一个声音在Linux内核中传播的想法。在以下文章中,将分析Android应用程序的声音播放方式,为此,还有很长的路要走。



All Articles