Русский
Русский
English
Статистика
Реклама

Звук. От механических колебаний до ALSA SoC Layer



Мы в SberDevices делаем устройства, на которых можно послушать музыку, посмотреть кино и ещё много всего. Как вы понимаете, без звука это всё не представляет интереса. Давайте посмотрим, что происходит со звуком в устройстве, начиная со школьной физики и заканчивая ALSA-подсистемой в Linux.

Что же такое звук, который мы слышим? Если совсем упрощать, то это колебания частиц воздуха, которые доходят до нашей барабанной перепонки. Мозг их, разумеется, потом переводит в приятную музыку или в звук проезжающего за окном мотоциклиста, но давайте пока остановимся на колебаниях.

Люди ещё в 19 веке поняли, что можно попытаться записать звуковые колебания, а потом их воспроизвести.

Для начала посмотрим, как работало одно из первых звукозаписывающих устройств.


Фонограф и его изобретатель Томас Эдисон
Источник фото

Тут всё просто. Брали какой-нибудь цилиндр, обматывали фольгой. Потом брали что-нибудь конусообразное (чтобы было погромче) с мембраной на конце. К мембране присоединена маленькая иголка. Иголку прислоняли к фольге. Потом специально обученный человек крутил цилиндр и что-нибудь говорил в резонатор. Иголка, приводимая в движение мембраной, делала в фольге углубления. Если достаточно равномерно крутить цилиндр, то получится намотанная на цилиндр зависимость амплитуды колебаний мембраны от времени.



Чтобы проиграть сигнал, надо было просто прокрутить цилиндр ещё раз с начала иголка будет попадать в углубления и передавать записанные колебания в мембрану, а та в резонатор. Вот мы и слышим запись. Можно легко найти интересные записи энтузиастов на ютубе.

Переход к электричеству


Теперь рассмотрим что-нибудь более современное, но не очень сложное. Например, катушечный микрофон. Колебания воздуха теперь изменяют положение магнита внутри катушки и благодаря электромагнитной индукции мы получаем на выходе зависимость амплитуды колебаний магнита (а значит, и мембраны) от времени. Только теперь эта зависимость выражается не углублениями на фольге, а зависимостью электрического напряжения на выходе микрофона от времени.



Чтобы можно было хранить такое представление колебаний в памяти компьютера, их надо дискретизировать. Этим занимается специальная железка аналогово-цифровой преобразователь (АЦП). АЦП умеет много раз за одну секунду запоминать значение напряжения (с точностью до разрешения целочисленной арифметики АЦП) на входе и записывать его в память. Количество таких отсчётов за секунду называется sample rate. Типичные значения 8000 Hz 96000 Hz.

Не будем вдаваться в подробности работы АЦП, потому что это заслуживает отдельной серии статей. Перейдём к главному весь звук, с которым работают Linux-драйверы и всякие устройства, представляется именно в виде зависимости амплитуды от времени. Такой формат записи называется PCM (Pulse-code modulation). Для каждого кванта времени длительностью 1/sample_rate указано значение амплитуды звука. Именно из PCM состоят .wav-файлы.

Пример визуализации PCM для .wav-файла с музыкой, где по горизонтальной оси отложено время, а по вертикальной амплитуда сигнала:



Так как на нашей плате стереовыход под динамики, надо научиться хранить в одном .wav-файле стереозвук: левый и правый канал. Тут всё просто сэмплы будут чередоваться вот так:



Такой способ хранения данных называется interleaved. Бывают и другие способы, но сейчас их рассматривать не будем.

Теперь разберёмся, какие электрические сигналы нам нужны, чтобы можно было организовать передачу данных между устройствами. А нужно не много:

  1. Bit Clock(BCLK) тактирующий сигнал (или клок), по которому аппаратура определяет, когда надо отправить следующий бит.
  2. Frame Clock (FCLK или его ещё называют LRCLK) тактирующий сигнал, по которому аппаратура понимает, когда надо начать передавать другой канал.
  3. Data сами данные.



Например, у нас есть файл со следующими характеристиками:
  • sample width = 16 bits;
  • sampling rate = 48000 Hz;
  • channels = 2.

Тогда нам надо выставить следующие значения частот:
  • FCLK = 48000 Hz;
  • BCLK = 48000 * 16 * 2 Hz.

Чтобы передавать ещё больше каналов, используется протокол TDM, который отличается от I2S тем, что FCLK теперь не обязан иметь скважность 50%, и восходящий фронт лишь задаёт начало пакета сэмплов, принадлежащих разным каналам.

Общая схема


Под рукой как раз оказалась плата amlogic s400, к которой можно подключить динамик. На неё установлено ядро Linux из upstream. На этом примере и будем работать.

Наша плата состоит из SoC (amlogic A113x), к которому подключен ЦАП TAS5707PHPR. И общая схема выглядит следующим образом:

Что умеет SoC:
  • SoC имеет 3 пина: BCLK, LRCLK, DATA;
  • можно сконфигурировать CLK-пины через специальные регистры SoC, чтобы на них были правильные частоты;
  • ещё этому SoC можно сказать: Вот тебе адрес в памяти. Там лежат PCM-данные. Передавай эти данные бит за битом через DATA-линию. Такую область памяти будем называть hwbuf.

Чтобы воспроизвести звук, Linux-драйвер говорит SoC, какие нужно выставить частоты на линиях BCLK и LRCLK. К тому же Linux-драйвер подсказывает, где находится hwbuf. После этого ЦАП (TAS5707) получает данные по DATA-линии и преобразует их в два аналоговых электрических сигнала. Эти сигналы потом передаются по паре проводов {analog+; analog-} в два динамика.

Переходим к Linux


Мы готовы перейти к тому, как эта схема выглядит в Linux. Во-первых, для работы со звуком в Linux есть библиотека, которая размазана между ядром и userspace. Называется она ALSA, и рассматривать мы будем именное её. Суть ALSA в том, чтобы userspace и ядро договорились об интерфейсе работы со звуковыми устройствами.

Пользовательская 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 говорит, что устройство для воспроизведения звука (playback);
c говорит, что устройство для записи звука (capture).

В нашем случае устройство pcmC0D0p как раз соответствует playback I2S-интерфейсу. D1 это spdif, а D2 pdm-микрофоны, но о них мы говорить не будем.

Device tree


Описание звуковой карты начинается с 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-режиме.

Далее указано, какой ЦАП (внутри Linux они входят в понятие кодек) установлен на плате с помощью структуры speaker_amp1. Заметим, что тут же указано, к какой I2C-линии (не путать с I2S!) подключен наш ЦАП TAS5707. Именно по этой линии будет потом производиться включение и настройка усилителя из драйвера.

Структура tdmif_c описывает, какие пины SoC будут выполнять роли I2S-интерфейса.

ALSA SoC Layer


Для SoC, внутри которых есть поддержка аудио, в Linux есть ALSA SoC layer. Он позволяет описывать кодеки (напомню, что именно так называется любой ЦАП в терминах ALSA), позволяет указывать, как эти кодеки соединены.

Кодеки в терминах Linux kernel называются DAI (Digital Audio Interface). Сам TDM/I2S интерфейс, который есть в SoC, тоже называется 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().
Она нужна для того, чтобы настроить всё оборудование SoC согласно параметрам PCM-файла, который мы пытаемся проиграть. Именно она в дальнейшем вызовет функции из группы выше, чтобы установить TDM-клоки.

...int (*trigger)(struct snd_pcm_substream *, int,    struct snd_soc_dai *);...
А эта функция делает самый последний шаг после настройки кодека переводит кодек в активный режим.

ЦАП, который будет выдавать аналоговый звук на динамик, описывается ровно такой же структурой. snd_soc_dai_ops в этом случае будут настраивать ЦАП на прием данных в правильном формате. Такая настройка ЦАП как правило осуществляется через I2C-интерфейс.

Все кодеки, которые указаны в device tree в структуре,
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 */};

Если какой-нибудь из кодеков в цепочке не поддерживает конкретные параметры, всё закончится ошибкой.

Соответствующую реализацию TDM-драйвера для amlogic s400 можно посмотреть в sound/soc/meson/axg-tdm-interface.c. А реализацию драйвера кодека TAS5707 в sound/soc/codecs/tas571x.c

Пользовательская часть


Теперь посмотрим что происходит, когда пользователь хочет проиграть звук. Удобный для изучения пример реализации пользовательской ALSA это tinyalsa. Исходный код, относящийся ко всему нижесказанному, можно посмотреть там.
В комплект входит утилита tinyplay. Чтобы проиграть звук надо запустить:

bash$ tinyplay ./music.wav -D 0 -d 0
(-D и -d параметры говорят, что звук надо проигрывать через /dev/snd/pcmC0D0p).

Что происходит?
Вот краткая блок-схема, а потом будут пояснения:



  1. [userspace] Парсим .wav header, чтобы узнать PCM-параметры (sample rate, bit width, channels) воспроизводимого файла. Складываем все параметры в struct snd_pcm_hw_params.
  2. [userspace] Открываем устройство /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] копируем пользовательский буфер buf в hwbuf (см. пункт Общая схема) с помощью copy_from_user().
  10. [userspace] goto 6.

Реализацию ядерной части ioctl можно посмотреть, поискав по слову SNDRV_PCM_IOCTL_*

Заключение


Теперь у нас есть представление о том, куда попадает звук в Linux ядре. В следующих статьях будет разбор того, как звук проигрывается из Android-приложений, а для этого ему надо пройти ещё немалый путь.
Источник: habr.com
К списку статей
Опубликовано: 22.09.2020 10:23:41
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании sberdevices

Блог компании сбербанк

Звук

Научно-популярное

Системное программирование

Linux kernel

Alsa

Аудио

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru