Som. De vibrações mecânicas à camada ALSA SoC





Nós da SberDevices criamos dispositivos nos quais você pode ouvir música, assistir a um filme e muito mais. Como você pode imaginar, sem som, tudo isso não tem interesse. Vamos dar uma olhada no que acontece com o som no dispositivo, desde a física escolar até o subsistema ALSA no Linux.



Qual é o som que ouvimos? Para simplificar completamente, essas são vibrações de partículas de ar que atingem nosso tímpano. O cérebro deles, é claro, se traduz em música agradável ou no som de um motociclista passando do lado de fora da janela, mas vamos nos concentrar nas vibrações por enquanto.



No século 19, as pessoas perceberam que você pode tentar gravar as vibrações sonoras e depois reproduzi-las.



Primeiro, vamos ver como um dos primeiros dispositivos de gravação de som funcionou.





O fonógrafo e seu inventor Thomas Edison

Photo, fonte



Tudo é simples aqui. Eles pegaram um cilindro e o embrulharam em papel alumínio. Em seguida, pegaram algo em forma de cone (para torná-lo mais alto) com uma membrana no final. Uma pequena agulha é colocada na membrana. A agulha estava encostada na folha. Então, uma pessoa especialmente treinada torceu o cilindro e disse algo no ressonador. Uma agulha, impulsionada por uma membrana, fez recortes na folha. Se for o suficiente para girar uniformemente o cilindro, então a dependência da amplitude de oscilação da membrana com o tempo "enrolado" no cilindro será revelada.







Para tocar o sinal, bastava girar o cilindro novamente desde o início - a agulha cairá nas ranhuras e transferirá as vibrações gravadas para a membrana e esta para o ressonador. Então ouvimos a gravação. Você pode encontrar facilmente postagens interessantes de entusiastas no YouTube.



Transição para eletricidade



Agora vamos ver algo mais moderno, mas não muito complicado. Por exemplo, um microfone de carretel. As vibrações do ar agora mudam a posição do ímã dentro da bobina e, graças à indução eletromagnética, obtemos na saída a dependência da amplitude das oscilações do ímã (e, portanto, da membrana) com o tempo. Só agora essa dependência é expressa não por depressões na folha, mas pela dependência da voltagem elétrica na saída do microfone no tempo.







Para poder armazenar tal representação das flutuações na memória do computador, elas devem ser discretizadas. Isso é feito por uma peça especial de hardware - um conversor analógico-digital (ADC). O ADC pode memorizar o valor da tensão (até a resolução da aritmética inteira do ADC) na entrada muitas vezes em um segundo e gravá-lo na memória. O número dessas amostras por segundo é denominado taxa de amostragem. Os valores típicos são 8.000 Hz - 96.000 Hz.



Não entraremos em detalhes sobre o ADC, pois ele merece uma série separada de artigos. Vamos passar ao ponto principal - todo o som com o qual os drivers do Linux e todos os tipos de dispositivos funcionam é representado precisamente na forma de uma dependência de amplitude versus tempo. Este formato de gravação é denominado PCM (modulação por código de pulso). Para cada intervalo de tempo com uma duração de 1 / sample_rate, o valor da amplitude do som é indicado. É a partir do PCM que os arquivos .wav são compostos.



Um exemplo de visualização PCM para um arquivo .wav com música, onde o eixo horizontal é o tempo e o eixo vertical é a amplitude do sinal:







como nossa placa tem uma saída estéreo para alto-falantes, você precisa aprender a armazenar som estéreo em um arquivo .wav: canais esquerdo e direito. Tudo é simples aqui - os exemplos irão se alternar assim:







Essa forma de armazenar dados é chamada de intercalada. Existem outras maneiras, mas não as consideraremos agora.



Agora vamos descobrir quais sinais elétricos precisamos para organizar a transferência de dados entre dispositivos. E não é preciso muito:



  1. Bit Clock (BCLK) é um sinal de clock (ou clock) pelo qual o hardware determina quando enviar o próximo bit.
  2. Frame Clock (FCLK ou também denominado LRCLK) é um sinal de temporização pelo qual o equipamento entende quando é necessário iniciar a transmissão de outro canal.
  3. Dados são os próprios dados.






Por exemplo, temos um arquivo com as seguintes características:

  • largura da amostra = 16 bits;
  • taxa de amostragem = 48.000 Hz;
  • canais = 2.


Em seguida, precisamos definir os seguintes valores de frequência:

  • FCLK = 48.000 Hz;
  • BCLK = 48000 * 16 * 2 Hz.


Para transmitir ainda mais canais, é usado o protocolo TDM, que difere do I2S porque o FCLK não precisa mais ter um ciclo de trabalho de 50% e a borda de subida define apenas o início de um pacote de amostras pertencentes a canais diferentes.



Esquema geral



Bem ao lado estava a placa amlogic s400, à qual você pode conectar um alto-falante. Ele tem o kernel Linux upstream instalado. Vamos trabalhar neste exemplo.



Nossa placa consiste em um SoC (amlogic A113x) ao qual o TAS5707PHPR DAC está conectado. E o esquema geral é assim:



O que o SoC pode fazer:

  • SoC tem 3 pinos: BCLK, LRCLK, DATA;
  • você pode configurar os pinos CLK através dos registros especiais do SoC para que tenham as frequências corretas;
  • Você também pode dizer a este SoC: “Aqui está um endereço na memória. Ele contém dados PCM. Envie esses dados bit a bit pela linha DATA. " Essa área de memória será chamada de hwbuf.


Para reproduzir o som, o driver do Linux informa ao SoC quais frequências definir nas linhas BCLK e LRCLK. Além disso, o driver Linux informa onde hwbuf está localizado. O DAC (TAS5707) então recebe os dados por meio da linha DATA e os converte em dois sinais elétricos analógicos. Esses sinais são então transmitidos por um par de fios {analógico +; analog-} em dois alto-falantes.



Passando para o Linux



Estamos prontos para ver como esse circuito se parece no Linux. Primeiro, existe uma "biblioteca" para trabalhar com som no Linux, que é distribuída entre o kernel e o espaço do usuário. Chama-se ALSA e iremos considerar o seu nome. A essência do ALSA é que o espaço do usuário e o kernel "concordem" sobre a interface para trabalhar com dispositivos de som.



A biblioteca ALSA customizada interage com o kernel usando a interface ioctl. Os dispositivos pcmC {x} D {y} {c, p} criados no diretório / dev / snd / são usados. Esses dispositivos são criados por um driver que deve ser escrito pelo fornecedor do SoC. Por exemplo, aqui está o conteúdo desta pasta no amlogic s400:



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


Em nome de pcmC {x} D {y} {c, p}:

X - número da placa de som (pode haver vários deles);

Y é o número da interface no cartão (por exemplo, pcmC0D0p pode ser responsável por tocar nos alto-falantes por meio da interface tdm e pcmC0D1c ​​- por gravar som de microfones por meio de uma interface de hardware diferente);

p - diz que o dispositivo para reproduzir o som (reprodução);

c - diz que o dispositivo para gravação de som (captura).



Em nosso caso, o dispositivo pcmC0D0p corresponde exatamente à interface I2S de reprodução. D1 é spdif e D2 são microfones pdm, mas não vamos falar sobre eles.



Árvore de dispositivos



A descrição da placa de som começa com 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";
};


Aqui vemos os 3 dispositivos que irão aparecer em / dev / snd: tdmif_c, spdif, pdm.



O dispositivo pelo qual o som passará é denominado dai-link-6. Ele funcionará sob o controle do driver TDM. Surge a pergunta: estávamos conversando sobre como transmitir som via I2S e, de repente, TDM. Isso é fácil de explicar: como escrevi acima, I2S ainda é o mesmo TDM, mas com requisitos claros para o ciclo de trabalho LRCLK e o número de canais - deve haver dois deles. O driver TDM irá então ler o campo dai-format = "i2s"; e vai entender que ele precisa trabalhar no modo I2S.



Em seguida, é indicado qual DAC (dentro do Linux eles são chamados de "codec") está instalado na placa usando a estrutura speaker_amp1. Observe que é imediatamente indicado a qual linha I2C (não deve ser confundido com I2S!) Nosso TAS5707 DAC está conectado. É nessa linha que o amplificador será então ligado e ajustado pelo driver.



A estrutura tdmif_c descreve quais pinos SoC atuarão como a interface I2S.



Camada ALSA SoC



Para SoCs que têm suporte de áudio interno, o Linux tem uma camada ALSA SoC. Ele permite que você descreva codecs (lembre-se de que isso é como qualquer DAC é chamado em termos de ALSA), permite que você especifique como esses codecs são conectados.



Os codecs nos termos do kernel do Linux são chamados de DAI (Digital Audio Interface). A própria interface TDM / I2S, que está no SoC, também é chamada de DAI, e o trabalho com ela é feito de maneira semelhante.



O driver descreve o codec usando struct snd_soc_dai. A parte mais interessante na descrição do codec é a operação de configuração dos parâmetros de transmissão TDM. Eles estão localizados aqui: struct snd_soc_dai -> struct snd_soc_dai_driver -> struct snd_soc_dai_ops. Vamos considerar os campos mais importantes para a compreensão (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);
    ...
As próprias funções com as quais os relógios TDM são expostos. Essas funções são geralmente implementadas pelo fornecedor SoC.



...
int (*hw_params)(struct snd_pcm_substream *,
    struct snd_pcm_hw_params *, struct snd_soc_dai *);
...
A função mais interessante é hw_params ().

Ele é necessário para configurar todo o hardware SoC de acordo com os parâmetros do arquivo PCM que estamos tentando reproduzir. É ela quem vai chamar posteriormente as funções do grupo acima para instalar os relógios TDM.



...
int (*trigger)(struct snd_pcm_substream *, int,
    struct snd_soc_dai *);
...
E essa função dá a última etapa após a configuração do codec - ela coloca o codec no modo ativo.



O DAC que emitirá som analógico para o alto-falante é descrito exatamente pela mesma estrutura. snd_soc_dai_ops neste caso configurará o DAC para receber dados no formato correto. Essa configuração do DAC geralmente é feita por meio da interface I2C.



Todos os codecs especificados na árvore de dispositivos da estrutura,

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


- e pode haver muitos deles, são adicionados a uma lista e anexados ao dispositivo / dev / snd / pcm *. Isso é necessário para que, ao reproduzir som, o kernel possa ignorar todos os drivers de codec necessários e configurá-los / habilitá-los.



Cada codec deve informar quais parâmetros PCM ele suporta. Ele faz isso com uma estrutura:

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 */
};


Se algum dos codecs da cadeia não suportar parâmetros específicos, tudo terminará com um erro.



A implementação do driver TDM correspondente para amlogic s400 pode ser visualizada em sound / soc / meson / axg-tdm-interface.c . E a implementação do driver do codec TAS5707 está em sound / soc / codecs / tas571x.c



Parte do usuário



Agora vamos ver o que acontece quando o usuário deseja reproduzir um som. Um exemplo fácil de aprender de uma implementação ALSA personalizada é o tinyalsa . O código-fonte para todos os itens a seguir pode ser visualizado lá.

Inclui o utilitário tinyplay. Para reproduzir o som, você precisa executar:



bash$ tinyplay ./music.wav -D 0 -d 0
(As opções -D e -d indicam para reproduzir o som por meio de / dev / snd / pcmC0D0p).



O que está acontecendo?

Aqui está um pequeno diagrama de blocos, seguido por explicações:







  1. [espaço do usuário] Analise o cabeçalho .wav para descobrir os parâmetros PCM (taxa de amostragem, largura de bits, canais) do arquivo sendo reproduzido. Adicionamos todos os parâmetros a struct snd_pcm_hw_params.
  2. [espaço do usuário] Abra o dispositivo / 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. [kernelspace] copia o buf do usuário para hwbuf (veja o Esquema Geral) usando copy_from_user ().
  10. [espaço do usuário] vá para 6.


A implementação da parte do kernel do ioctl pode ser vista procurando pela palavra SNDRV_PCM_IOCTL_ *



Conclusão



Agora temos uma ideia de onde o som vai no kernel do Linux. Nos artigos a seguir, faremos uma análise de como o som é reproduzido nos aplicativos Android, e para isso ainda há um longo caminho a percorrer.



All Articles