Sonido. De vibraciones mecánicas a ALSA SoC Layer





En SberDevices creamos dispositivos en los que puede escuchar música, ver películas y mucho más. Como puedes imaginar, sin sonido, todo esto no tiene interés. Echemos un vistazo a lo que sucede con el sonido en el dispositivo, desde la física escolar hasta el subsistema ALSA en Linux.



¿Cuál es el sonido que escuchamos? Para simplificar por completo, se trata de vibraciones de partículas de aire que llegan a nuestro tímpano. Su cerebro, por supuesto, se traduce en una música agradable o en el sonido de un motociclista que pasa por la ventana, pero por ahora nos detenemos en las vibraciones.



En el siglo XIX, la gente se dio cuenta de que se puede intentar grabar vibraciones sonoras y luego reproducirlas.



Primero, echemos un vistazo a cómo funcionó uno de los primeros dispositivos de grabación.





El fonógrafo y su inventor Thomas Edison

Fuente fotográfica



Aquí todo es sencillo. Tomaron un cilindro y lo envolvieron en papel de aluminio. Luego tomaron algo afilado (para hacerlo más ruidoso) con una membrana al final. Se adjunta una pequeña aguja a la membrana. La aguja estaba apoyada contra la lámina. Luego, una persona especialmente entrenada giró el cilindro y dijo algo en el resonador. Una aguja, impulsada por una membrana, hizo hendiduras en la lámina. Si es suficiente girar uniformemente el cilindro, resultará la dependencia de la amplitud de oscilación de la membrana en el tiempo "enrollado" en el cilindro.







Para reproducir la señal, solo tuvo que girar el cilindro nuevamente desde el principio: la aguja caerá en las ranuras y transferirá las vibraciones registradas a la membrana y eso al resonador. Entonces escuchamos la grabación. Puede encontrar fácilmente publicaciones interesantes de entusiastas en YouTube.



Transición a la electricidad



Ahora veamos algo más moderno, pero no muy complicado. Por ejemplo, un micrófono de carrete. Las oscilaciones del aire ahora cambian la posición del imán dentro de la bobina y, gracias a la inducción electromagnética, obtenemos en la salida la dependencia de la amplitud de las oscilaciones del imán (y por lo tanto de la membrana) con el tiempo. Solo que ahora esta dependencia se expresa no por depresiones en la lámina, sino por la dependencia del voltaje eléctrico en la salida del micrófono en el tiempo.







Para almacenar tal representación de fluctuaciones en la memoria de la computadora, deben estar discretizadas. Esto se realiza mediante una pieza especial de hardware: un convertidor de analógico a digital (ADC). El ADC puede memorizar el valor de voltaje (hasta la resolución de la aritmética de enteros del ADC) en la entrada muchas veces en un segundo y escribirlo en la memoria. El número de tales muestras por segundo se denomina frecuencia de muestreo. Los valores típicos son 8000 Hz - 96000 Hz.



No entraremos en detalles del ADC, porque merece una serie de artículos por separado. Pasemos a lo principal: todo el sonido con el que funcionan los controladores de Linux y todo tipo de dispositivos se representa precisamente en forma de una dependencia de amplitud versus tiempo. Este formato de grabación se llama PCM (modulación de código de pulso). Para cada segmento de tiempo con una duración de 1 / sample_rate, se indica el valor de la amplitud del sonido. Es a partir de PCM que se componen los archivos .wav.



Un ejemplo de visualización PCM para un archivo .wav con música, donde el eje horizontal es el tiempo y el eje vertical es la amplitud de la señal:







dado que nuestra placa tiene una salida estéreo para altavoces, debe aprender a almacenar sonido estéreo en un archivo .wav: canales izquierdo y derecho. Aquí todo es simple: las muestras se alternarán así:







Esta forma de almacenar datos se denomina intercalado. Hay otras formas, pero no las consideraremos ahora.



Ahora averigüemos qué señales eléctricas necesitamos para organizar la transferencia de datos entre dispositivos. Y no se necesita mucho:



  1. Bit Clock (BCLK) es una señal de reloj (o reloj) mediante la cual el hardware determina cuándo enviar el siguiente bit.
  2. Frame Clock (FCLK o también llamado LRCLK) es una señal de temporización por la cual el equipo entiende cuándo es necesario comenzar a transmitir otro canal.
  3. Los datos son los datos en sí mismos.






Por ejemplo, tenemos un archivo con las siguientes características:

  • ancho de muestra = 16 bits;
  • frecuencia de muestreo = 48000 Hz;
  • canales = 2.


Entonces necesitamos establecer los siguientes valores de frecuencia:

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


Para transmitir aún más canales, se utiliza el protocolo TDM, que se diferencia del I2S en que ya no se requiere que FCLK tenga un ciclo de trabajo del 50%, y el flanco ascendente solo establece el comienzo de un paquete de muestras pertenecientes a diferentes canales.



Esquema general



Justo a la mano estaba la placa amlogic s400, a la que se puede conectar un altavoz. Tiene instalado el kernel de Linux ascendente. Trabajaremos en este ejemplo.



Nuestra placa consta de un SoC (amlogic A113x) al que está conectado el DAC TAS5707PHPR. Y el esquema general se ve así:



Qué puede hacer el SoC:

  • El SoC tiene 3 pines: BCLK, LRCLK, DATA;
  • puedes configurar los pines CLK a través de los registros especiales del SoC para que tengan las frecuencias correctas;
  • También puede decirle a este SoC: “Aquí hay una dirección en la memoria. Contiene datos PCM. Envíe estos datos bit a bit a través de la línea DATA. " Esta área de memoria se llamará hwbuf.


Para reproducir sonido, el controlador de Linux le dice al SoC qué frecuencias establecer en las líneas BCLK y LRCLK. Además, el controlador de Linux le indica dónde se encuentra hwbuf. El DAC (TAS5707) luego recibe los datos a través de la línea DATA y los convierte en dos señales eléctricas analógicas. Estas señales se transmiten luego a través de un par de cables {analógico +; analógico-} en dos altavoces.



Pasando a Linux



Estamos listos para pasar a cómo se ve este circuito en Linux. Primero, hay una "biblioteca" para trabajar con sonido en Linux, que se distribuye entre el kernel y el espacio de usuario. Se llama ALSA, y consideraremos su nombre. La esencia de ALSA es que el espacio de usuario y el núcleo "acuerden" la interfaz para trabajar con dispositivos de sonido.



La biblioteca ALSA personalizada interactúa con el kernel mediante la interfaz ioctl. Se utilizan los dispositivos pcmC {x} D {y} {c, p} creados en el directorio / dev / snd /. Estos dispositivos son creados por un controlador que debe ser escrito por el proveedor de SoC. Por ejemplo, aquí está el contenido de esta carpeta en amlogic s400:



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


En nombre de pcmC {x} D {y} {c, p}:

X - número de tarjeta de sonido (puede haber varias);

Y: el número de la interfaz en la tarjeta (por ejemplo, pcmC0D0p puede ser responsable de reproducir en los altavoces a través de la interfaz tdm y pcmC0D1c ​​para grabar sonido desde micrófonos a través de una interfaz de hardware diferente);

p - dice que el dispositivo para reproducir sonido (reproducción);

c - dice que el dispositivo para grabar sonido (captura).



En nuestro caso, el dispositivo pcmC0D0p corresponde exactamente a la interfaz de reproducción I2S. D1 es spdif y D2 son micrófonos pdm, pero no hablaremos de ellos.



Árbol de dispositivos



La descripción de la tarjeta de sonido comienza con 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";
};


Aquí vemos esos 3 dispositivos que luego aparecerán en / dev / snd: tdmif_c, spdif, pdm.



El dispositivo a través del cual pasará el sonido se llama dai-link-6. Funcionará bajo el control del controlador TDM. Surge la pregunta: estábamos hablando de cómo transmitir sonido a través de I2S, y luego, de repente, TDM. Esto es fácil de explicar: como escribí anteriormente, I2S sigue siendo el mismo TDM, pero con requisitos claros para el ciclo de trabajo LRCLK y la cantidad de canales, debería haber dos de ellos. El controlador TDM leerá el campo dai-format = "i2s"; y comprenderá que necesita trabajar en modo I2S.



A continuación, se indica qué DAC (dentro de Linux se denominan "códec") está instalado en la placa utilizando la estructura speaker_amp1. Tenga en cuenta que se indica inmediatamente a qué línea I2C (¡no confundir con I2S!) Nuestro DAC TAS5707 está conectado. A lo largo de esta línea, el amplificador se encenderá y sintonizará desde el controlador.



La estructura tdmif_c describe qué pines SoC actuarán como interfaz I2S.



Capa de ALSA SoC



Para los SoC que tienen soporte de audio en su interior, Linux tiene una capa de ALSA SoC. Le permite describir códecs (recuerde que así es como se llama cualquier DAC en términos de ALSA), le permite especificar cómo se conectan estos códecs.



Los códecs en términos del kernel de Linux se denominan DAI (Interfaz de audio digital). La propia interfaz TDM / I2S, que se encuentra en el SoC, también se denomina DAI, y el trabajo con ella se realiza de manera similar.



El controlador describe el códec usando struct snd_soc_dai. La parte más interesante de la descripción del códec es la operación para configurar los parámetros de transmisión TDM. Se encuentran aquí: struct snd_soc_dai -> struct snd_soc_dai_driver -> struct snd_soc_dai_ops. Consideremos los campos más importantes para la comprensión (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);
    ...
Estas son las mismas funciones a las que se exponen los relojes TDM. Estas funciones generalmente las implementa el proveedor de SoC.



...
int (*hw_params)(struct snd_pcm_substream *,
    struct snd_pcm_hw_params *, struct snd_soc_dai *);
...
La función más interesante es hw_params ().

Es necesario para configurar todo el hardware SoC de acuerdo con los parámetros del archivo PCM que estamos intentando reproducir. Es ella quien luego llamará a las funciones del grupo anterior para instalar relojes TDM.



...
int (*trigger)(struct snd_pcm_substream *, int,
    struct snd_soc_dai *);
...
Y esta función da el último paso después de configurar el códec: pone el códec en modo activo.



El DAC que emitirá sonido analógico al altavoz se describe exactamente con la misma estructura. snd_soc_dai_ops en este caso configurará el DAC para recibir datos en el formato correcto. Esta configuración de DAC generalmente se realiza a través de la interfaz I2C.



Todos los códecs que se especifican en el árbol de dispositivos de la estructura,

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


- y puede haber muchos de ellos, se agregan a una lista y se adjuntan al dispositivo / dev / snd / pcm *. Esto es necesario para que al reproducir sonido, el kernel pueda omitir todos los controladores de códec necesarios y configurarlos / habilitarlos.



Cada códec debe indicarle qué parámetros PCM admite. Hace esto con una estructura:

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 alguno de los códecs de la cadena no admite parámetros específicos, todo terminará con un error.



La implementación del controlador TDM correspondiente para amlogic s400 se puede ver en sound / soc / meson / axg-tdm-interface.c . Y la implementación del controlador de códec TAS5707 está en sound / soc / codecs / tas571x.c



Parte del usuario



Ahora veamos qué sucede cuando el usuario quiere reproducir un sonido. Un ejemplo fácil de aprender de una implementación de ALSA personalizada es tinyalsa . Allí se puede ver el código fuente de todo lo siguiente.

Incluye utilidad tinyplay. Para reproducir el sonido necesitas ejecutar:



bash$ tinyplay ./music.wav -D 0 -d 0
(Las opciones -D y -d le dicen que reproduzca el sonido a través de / dev / snd / pcmC0D0p).



¿Qué esta pasando?

Aquí hay un diagrama de bloques rápido, seguido de explicaciones:







  1. [espacio de usuario] Analice el encabezado .wav para averiguar los parámetros PCM (frecuencia de muestreo, ancho de bits, canales) del archivo que se está reproduciendo. Agregamos todos los parámetros a struct snd_pcm_hw_params.
  2. [espacio de usuario] Abra el 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 el usuario buf a hwbuf (ver Esquema general) usando copy_from_user ().
  10. [espacio de usuario] vaya a 6.


La implementación de la parte del kernel de ioctl se puede ver buscando la palabra SNDRV_PCM_IOCTL_ *



Conclusión



Ahora tenemos una idea de a dónde va el sonido en el kernel de Linux. En los próximos artículos se analizará cómo se reproduce el sonido desde las aplicaciones de Android, y para ello queda un largo camino por recorrer.



All Articles