Du son. Des vibrations mécaniques à la couche SoC ALSA





Chez SberDevices, nous fabriquons des appareils sur lesquels vous pouvez écouter de la musique, regarder des films et bien plus encore. Comme vous pouvez l'imaginer, sans son, tout cela n'a aucun intérêt. Jetons un coup d'œil à ce qui arrive au son dans l'appareil, de la physique de l'école au sous-système ALSA sous Linux.



Quel est le son que nous entendons? Pour simplifier complètement, ce sont des vibrations de particules d'air qui atteignent notre tympan. Leur cerveau, bien sûr, se traduit alors par une musique agréable ou le son d'un motocycliste passant par la fenêtre, mais arrêtons-nous aux vibrations pour l'instant.



Au XIXe siècle, les gens se sont rendu compte qu'il était possible d'essayer d'enregistrer les vibrations sonores, puis de les reproduire.



Voyons d'abord comment fonctionnait l'un des premiers appareils d'enregistrement.





Le phonographe et son inventeur Thomas Edison

Source photo



Tout est simple ici. Ils ont pris un cylindre et l'ont emballé dans du papier d'aluminium. Ensuite, ils ont pris quelque chose de fuselé (pour le rendre plus fort) avec une membrane à la fin. Une petite aiguille est attachée à la membrane. L'aiguille était appuyée contre la feuille. Puis une personne spécialement formée a tordu le cylindre et a dit quelque chose dans le résonateur. Une aiguille, entraînée par une membrane, faisait des indentations dans la feuille. S'il suffit de tordre uniformément le cylindre, alors la dépendance de l'amplitude d'oscillation de la membrane sur le temps «enroulé» sur le cylindre se révélera.







Pour jouer le signal, il suffit de tourner à nouveau le cylindre depuis le début - l'aiguille tombera dans les creux et transmettra les vibrations enregistrées à la membrane, et cela au résonateur. Nous entendons donc l'enregistrement. Vous pouvez facilement trouver des articles intéressants de passionnés sur YouTube.



Transition vers l'électricité



Regardons maintenant quelque chose de plus moderne, mais pas très compliqué. Par exemple, un microphone à bobine. Les vibrations de l'air modifient maintenant la position de l'aimant à l'intérieur de la bobine et, grâce à l'induction électromagnétique, on obtient en sortie la dépendance de l'amplitude des oscillations de l'aimant (et donc de la membrane) au temps. Ce n'est que maintenant que cette dépendance s'exprime non par des dépressions sur la feuille, mais par la dépendance de la tension électrique à la sortie du microphone sur le temps.







Pour pouvoir stocker une telle représentation des fluctuations dans la mémoire de l'ordinateur, elles doivent être discrétisées. Cela se fait par un matériel spécial - un convertisseur analogique-numérique (ADC). L'ADC peut mémoriser la valeur de tension (jusqu'à la résolution de l'arithmétique entière ADC) à l'entrée plusieurs fois en une seconde et l'écrire en mémoire. Le nombre de ces échantillons par seconde est appelé la fréquence d'échantillonnage. Les valeurs typiques sont 8000 Hz - 96000 Hz.



Nous n'entrerons pas dans les détails de l'ADC, car il mérite une série d'articles à part. Passons à l'essentiel - tout le son avec lequel les pilotes Linux et toutes sortes de périphériques fonctionnent est représenté précisément sous la forme d'une dépendance d'amplitude en fonction du temps. Ce format d'enregistrement est appelé PCM (Pulse-code modulation). Pour chaque tranche de temps d'une durée de 1 / sample_rate, la valeur de l'amplitude du son est indiquée. Les fichiers .Wav sont composés de PCM.



Un exemple de visualisation PCM pour un fichier .wav avec musique, où l'axe horizontal est le temps et l'axe vertical est l'amplitude du signal:







puisque notre carte a une sortie stéréo pour les haut-parleurs, vous devez apprendre à stocker le son stéréo dans un fichier .wav: canaux gauche et droit. Tout est simple ici - les échantillons alterneront comme ceci:







Cette façon de stocker des données est appelée entrelacée. Il existe d'autres moyens, mais nous ne les examinerons pas maintenant.



Voyons maintenant de quels signaux électriques nous avons besoin pour organiser le transfert de données entre appareils. Et il ne faut pas grand chose:



  1. Bit Clock (BCLK) est un signal d'horloge (ou horloge) par lequel le matériel détermine quand envoyer le bit suivant.
  2. L'horloge de trame (FCLK ou elle est également appelée LRCLK) est un signal de synchronisation par lequel l'équipement comprend quand il est nécessaire de commencer à émettre un autre canal.
  3. Les données sont les données elles-mêmes.






Par exemple, nous avons un fichier avec les caractéristiques suivantes:

  • largeur d'échantillon = 16 bits;
  • fréquence d'échantillonnage = 48000 Hz;
  • canaux = 2.


Ensuite, nous devons définir les valeurs de fréquence suivantes:

  • FCLK = 48 000 Hz;
  • BCLK = 48000 * 16 * 2 Hz.


Pour transmettre encore plus de canaux, le protocole TDM est utilisé, qui diffère de I2S en ce que FCLK n'est plus obligé d'avoir un rapport cyclique de 50%, et le front montant ne définit que le début d'un paquet d'échantillons appartenant à différents canaux.



Schéma général



Juste à portée de main se trouvait la carte amlogic s400, à laquelle vous pouvez connecter un haut-parleur. Le noyau Linux en amont est installé. Nous allons travailler sur cet exemple.



Notre carte se compose d'un SoC (amlogic A113x) auquel le DAC TAS5707PHPR est connecté. Et le schéma général ressemble à ceci:



Ce que le SoC peut faire:

  • SoC a 3 broches: BCLK, LRCLK, DATA;
  • vous pouvez configurer les broches CLK via les registres spéciaux du SoC afin qu'elles aient les fréquences correctes;
  • Vous pouvez également dire à ce SoC: «Voici une adresse en mémoire. Il y a des données PCM. Envoyez ces données petit à petit via la ligne DATA. " Cette zone mémoire s'appellera hwbuf.


Pour lire le son, le pilote Linux indique au SoC les fréquences à régler sur les lignes BCLK et LRCLK. De plus, le pilote Linux vous indique où se trouve hwbuf. Le DAC (TAS5707) reçoit alors les données via la ligne DATA et les convertit en deux signaux électriques analogiques. Ces signaux sont ensuite transmis sur une paire de fils {analogique +; analogique-} en deux haut-parleurs.



Passer à Linux



Nous sommes prêts à passer à l'aspect de ce circuit sous Linux. Tout d'abord, il existe une "bibliothèque" pour travailler avec le son sous Linux, qui est répartie entre le noyau et l'espace utilisateur. Il s'appelle ALSA, et nous considérerons son nom. L'essence d'ALSA est que l'espace utilisateur et le noyau «s'accordent» sur l'interface pour travailler avec des périphériques audio.



La bibliothèque ALSA personnalisée interagit avec le noyau à l'aide de l'interface ioctl. Les périphériques pcmC {x} D {y} {c, p} créés dans le répertoire / dev / snd / sont utilisés. Ces périphériques sont créés par un pilote qui doit être écrit par le fournisseur SoC. Par exemple, voici le contenu de ce dossier sur amlogic s400:



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


Au nom de pcmC {x} D {y} {c, p}:

X - numéro de carte son (il peut y en avoir plusieurs);

Y est le numéro de l'interface sur la carte (par exemple, pcmC0D0p peut être responsable de la lecture vers les haut-parleurs via l'interface tdm, et pcmC0D1c ​​- pour enregistrer le son des microphones via une interface matérielle différente);

p - dit que l'appareil pour lire le son (lecture);

c - dit que l'appareil pour enregistrer le son (capture).



Dans notre cas, le périphérique pcmC0D0p correspond exactement à l'interface de lecture I2S. D1 est spdif et D2 est des microphones pdm, mais nous n'en parlerons pas.



Arborescence des appareils



La description de la carte son commence par 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";
};


Ici, nous voyons ces 3 périphériques qui apparaîtront ensuite dans / dev / snd: tdmif_c, spdif, pdm.



L'appareil par lequel le son passera s'appelle dai-link-6. Il fonctionnera sous le contrôle du pilote TDM. La question se pose: on parlait de comment transmettre le son via I2S, puis, du coup, TDM. C'est facile à expliquer: comme je l'ai écrit ci-dessus, I2S est toujours le même TDM, mais avec des exigences claires pour le cycle de service LRCLK et le nombre de canaux - il devrait y en avoir deux. Le pilote TDM lira alors le champ dai-format = "i2s"; et comprendra qu'il a besoin de travailler en mode I2S.



Ensuite, il est indiqué quel DAC (sous Linux, ils sont appelés "codec") est installé sur la carte en utilisant la structure speaker_amp1. A noter qu'il est immédiatement indiqué sur quelle ligne I2C (à ne pas confondre avec I2S!) Notre DAC TAS5707 est connecté. C'est le long de cette ligne que l'amplificateur sera ensuite allumé et réglé par le pilote.



La structure tdmif_c décrit les broches SoC qui agiront en tant qu'interface I2S.



Couche SoC ALSA



Pour les SoC qui ont un support audio à l'intérieur, Linux a une couche SoC ALSA. Il vous permet de décrire les codecs (rappelez-vous que c'est ce que tout DAC est appelé en termes ALSA), vous permet de spécifier comment ces codecs sont connectés.



Les codecs dans les termes du noyau Linux sont appelés DAI (Digital Audio Interface). L'interface TDM / I2S elle-même, qui se trouve dans le SoC, est également appelée DAI et son travail est effectué de la même manière.



Le pilote décrit le codec à l'aide de struct snd_soc_dai. La partie la plus intéressante de la description du codec est l'opération de réglage des paramètres de transmission TDM. Ils se trouvent ici: struct snd_soc_dai -> struct snd_soc_dai_driver -> struct snd_soc_dai_ops. Considérons les domaines les plus importants pour la compréhension (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);
    ...
Ce sont les fonctions mêmes avec lesquelles les horloges TDM sont exposées. Ces fonctions sont généralement implémentées par le fournisseur SoC.



...
int (*hw_params)(struct snd_pcm_substream *,
    struct snd_pcm_hw_params *, struct snd_soc_dai *);
...
La fonction la plus intéressante est hw_params ().

Il est nécessaire pour configurer tout le matériel SoC en fonction des paramètres du fichier PCM que nous essayons de lire. C'est elle qui appellera plus tard les fonctions du groupe ci-dessus pour installer les horloges TDM.



...
int (*trigger)(struct snd_pcm_substream *, int,
    struct snd_soc_dai *);
...
Et cette fonction prend la toute dernière étape après la configuration du codec - elle met le codec en mode actif.



Le DAC qui émettra un son analogique vers le haut-parleur est décrit exactement par la même structure. snd_soc_dai_ops dans ce cas configurera le DAC pour recevoir les données au format correct. Cette configuration DAC se fait généralement via l'interface I2C.



Tous les codecs spécifiés dans l'arborescence des appareils de la structure,

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


- et il peut y en avoir beaucoup, sont ajoutés à une liste et attachés au périphérique / dev / snd / pcm *. Ceci est nécessaire pour que lors de la lecture du son, le noyau puisse contourner tous les pilotes de codec nécessaires et les configurer / les activer.



Chaque codec doit vous indiquer les paramètres PCM qu'il prend en charge. Il le fait avec une structure:

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


Si l'un des codecs de la chaîne ne prend pas en charge des paramètres spécifiques, tout se terminera par une erreur.



L'implémentation du pilote TDM correspondant pour amlogic s400 peut être visualisée dans sound / soc / meson / axg-tdm-interface.c . Et l'implémentation du pilote de codec TAS5707 se trouve dans sound / soc / codecs / tas571x.c



Partie utilisateur



Voyons maintenant ce qui se passe lorsque l'utilisateur veut jouer un son. Un exemple facile à apprendre d'une implémentation ALSA personnalisée est tinyalsa . Le code source de tous les éléments suivants peut être consulté ici.

Inclut l'utilitaire tinyplay. Pour jouer le son dont vous avez besoin pour exécuter:



bash$ tinyplay ./music.wav -D 0 -d 0
(Les options -D et -d vous indiquent de jouer le son via / dev / snd / pcmC0D0p).



Que ce passe-t-il?

Voici un court schéma fonctionnel, suivi d'explications:







  1. [espace utilisateur] Analysez l'en-tête .wav pour connaître les paramètres PCM (fréquence d'échantillonnage, largeur de bits, canaux) du fichier en cours de lecture. Nous ajoutons tous les paramètres à la structure snd_pcm_hw_params.
  2. [espace utilisateur] Ouvrez le périphérique / 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] copiez l'utilisateur buf dans hwbuf (voir Schéma général) en utilisant copy_from_user ().
  10. [espace utilisateur] goto 6.


L'implémentation de la partie noyau d'ioctl peut être visualisée en recherchant le mot SNDRV_PCM_IOCTL_ *



Conclusion



Nous avons maintenant une idée de la destination du son dans le noyau Linux. Dans les articles suivants, il y aura une analyse de la façon dont le son est lu à partir des applications Android, et pour cela, il y a un long chemin à parcourir.



All Articles