Конспект по конфигурированию звуковой подсистемы ALSA (2019 г)
Часть I. Проникаемся конфигами
ALSA - Advanced Linux Sound Architecture. Набор драйверов, ядерных подсистем и утилит для поддержки звука под линухом.
Официальный сайт: https://alsa-project.org/wiki/Main_Page
Тут интересное : https://alsa-project.org/wiki/Documentation
Тоже глянуть : https://www.volkerschatz.com/noise/alsa.html
: https://wiki.archlinux.org/index.php/Advanced_Linux_Sound_Architecture
ALSA заменила OSS (более старую юниховую подсистему звука - Open Sound System), но, в, строгом соответствии традициям,
добавив кучу новых фич, она стала гораздо гораздо менее понятной как для пользователя так и для программиста.
Единственный документ, описывающий работу OSS, занимал < 100 страниц и был предельно прост.
По нему можно было написать прогу и она просто работала без вариантов (во всяком случае под FreeBSD).
Файлов конфигурации к OSS просто не было. Два канала на устройство, ввод/вывод, остальное - излишества.
Хотя, надо сказать, с развитием OSS под фряхой добавились и микширование вывода с разных прог и виртуальные регуляторы громкости
приложений и управление роутингом и даже возможность это всё выключить нафиг. Без файлов конфигов, просто через переменные sysctl.
К ALSA и на английском-то мало доков и чёткой структуры между ними не просматривается,
а на русском вообще только примеры настройки без комментариев (зачастую, скопированные с тех же немногочисленных англоязычных источников).
Зато у неё очень много разных сущностей, комбинируя которые можно делать самые безумные и бессмысленные вещи.
Здесь будет рассматриваться только алса, если у вас воткнут какой нибудь звуковой сервер (pulseaudio, ESD, ARts...) - это не ко мне.
Зачем её вообще рассматривать ? В то время как популярные PC-шные дистрибутивы уже более-менее вылизаны по части использования
звука, как только вы выходите из уютного мирка PC на суровые поля SoC-архитектур, вы тут же тонете в болотах аппаратных
драйверов. Вы думали, что умения конфигурировать и компилировать ядро должно хватить,
а наличие представлений о device tree blob сделает вас просто бессмертным, но реальность выглядит куда страшнее.
Вы узнаёте, что такое трёхчастная модель звуковых драйверов (machine driver, platform-driver и codec-driver) и когда, наконец,
aplay сможет спеть что нибудь похожее на музыку (до этого он сперва молчал, потому что одного из слоёв драйверов не хватало,
потом хрипел, потому что без сигнала синхронизации PLL-генератор кодека всё равно что-то генерирует) и вот, выяснив, что platform-driver для
вашего SoC работает только в режиме slave на SAI-шине и codec-driver только в режиме slave (да, поэтому и не было синхронизации)...
Так вот когда вы, проявив чудеса дипломатии, заставили их всё таки работать совместно (грязно подправив исходник ядра),
вот тогда-то вы и узнаете, что проги захватывают звуковое устройство монопольно и делится не хотят. А вам как раз нужно
правый канал отдать одной программе, а левый - другой. Добро пожаловать в суровый мир ALSA-конфигов.
Термины
Минимальная управляемая сущность alsa - это плагин.
Плагин + заданные для него параметры - это устройство (IMHO).
Звукашка с драйвером тоже выглядит как устройство.
Конвеер - это цепочка соединённых друг с другом устройств.
Т.е. чтобы услышать какой нибудь писк или хруст, вам достаточно указать плееру на устройство
"hw:0,0" - первая найденная голова у первой найденной звукашки.
Но вы получите много непрязни от плейера, если: 1) Аппаратная звукашка не поддерживает требуемый
плееру семплрейт, число каналов или что нибудь ещё. 2) Если другой плеер уже что-то играет или просто
занял эту звукашку.
Чтобы это всё решить, вы указываете плейеру не hw, а default - это ссылка на цепочку (конвеер)
плагинов, которые решают все проблемы. Однако самая чёрная магия требуется, чтобы создать такую цепочку.
И ещё много разных грибов и жабьих лапок, чтобы в итоге цепочка выполняла именно то, что вы от неё хотите.
Текущее состояние ALSA
Можно увидеть многое тут : /proc/asound/
Ещё прикольно вызвать "aplay -vvv -d 1 x.wav" и тоже увидеть реально выстроенную цепочку конвеера.
Файл конфигурации ALSA
Файлов много. Но есть несколько главных, с которыми остальные связаны правилами include:
/var/lib/alsa/asound.state - конфиг микшеров для всех карт в системе.
Читается при загрузке ОС утилитой alsactl, записывается при завершении работы ОС.
alsactl запускается либо через /etc/init.d/*alsa* либо через что-то из этого:
/lib/systemd/system/alsa-utils.service
/lib/systemd/system/basic.target.wants/alsa-restore.service
/lib/systemd/system/basic.target.wants/alsa-state.service
/lib/systemd/system/alsa-restore.service
/lib/systemd/system/alsa-state.service
/lib/udev/rules.d/90-alsa-restore.rules
/var/run/lock/asound.state.lock - lock-файл для предыдущего файла.
/var/lib/alsa/asound.state.new - промежуточный файл, создаётся при записи конфиги, затем переименовывается в asound.state.
Если alsactl скулит, что ему не хватает прав для создания asound.state (хотя бы даже там уже права 0777) - на самом деле он имеет ввиду
невозможность создать asound.state.new (например, если запрещена модификация каталога /var/lib/alsa/).
/usr/share/alsa/alsa.conf - конфиг конвееров. Не только звуковых.
/etc/asound.conf, ~/.asoundrc - дополнения к конфигу конвееров, вызываются из предыдущего файла.
Кто читает этот файл ? Т.е. как применить изменения ?
Файл читается любой прогой, которая хочет работать с ALSA. Линкер, загружая прогу, цепляет библиотеку libasound.so
- вот она и читает эти файлы. Так что если вы внесли изменения в конфиг - просто перезапустите вашу программу.
Если вы грохнете файл /usr/share/alsa/alsa.conf прога не будет запускаться, зато будет прикольно валиться с ошибкой.
Пакеты, которые могут быть полезны или нужны (названия по каталогу debian):
alsa-utils Utilities for configuring and using ALSA
Сюда входят разные полезные утилитки, которые конфигурируют микшер (его конфигурация хранится в ядре ОС),
а также позволяют проверить работу звуковой подсистемы.
Ставить не обязательно, но, в случае глюков, багов и неясностей, без неё будет сложно.
libasound2:amd64 Shared library for ALSA applications
Собственно библиотека libasound.so,
а также полезный файл с описанием синтаксиса конфигов:
/usr/share/doc/libasound2/examples/asoundrc.txt.gz
Реально полезный. Но не полный.
Там нет, как минимум:
1) Правил перезаписи/смешивания ассоциативных массивов (но правила -то есть!).
2) Упоминания плагина asym.
libasound2-data Configuration files and profiles for ALSA drivers
/usr/share/alsa
Файлы конфигурации. Да, алсе понадобился отдельный пакет для файлов конфигураций.
libasound2-dev:amd64 Shared library for ALSA applications -- development files
Заголовки .h
Для разработчиков типа программистов.
Формат конфигов:
См. https://alsa-project.org/wiki/Asoundrc
См. /usr/share/doc/libasound2/examples/asoundrc.txt.gz
См. https://www.alsa-project.org/alsa-doc/alsa-lib/pages.html
См. https://wiki.archlinux.org/index.php/Advanced_Linux_Sound_Architecture
Выглядит сложным, потому что синтаксический сахар. На самом деле конструкций не так уж много:
переменная = значение
"=" можно не писать.
Разделять присвоения можно переводами строк, запятыми и ";".
Ещё есть вложения:
<имя файла>
- вставить сюда указанный файл.
<confdir:путь>
- путь по умолчанию для поиска конфигов.
/ Следует заметить, что зачитать конфиг-файл из другого конфига можно через расширение @hooks /
От знака "#" до конца строки - комментарий.
Собственно и всё. Если вам это не нравится - см. рис. 1.
Значение может быть числом.
Значение может быть булевым.
Значение может быть строковым, если строка содержит странные символы - можно взять в кавычки.
Но не обязательно. Хотя фиг знает как тогда отличить от имени переменной.
Например, если вы хотите сослаться на конкретную звукашку (контроллер), то значение
pcm="hw:0,0"
берётся в кавычки, иначе двоеточие и запятая сыграют не так, как вам бы хотелось.
Значения могут быть массивом: тогда они обрамляются "[]".
Значения могут быть ассоциативным массивом: тогда имя-значения обрамляются в "{}".
Иногда ассоциативный массив из одного элемента заменяет обычную переменную:
slave {
pcm "hw:0,0"
}
то же что и
slave.pcm "hw:0,0"
Теперь понятно, что значит вот это:
pcm.default {
type "plug"
}
?
Это просто иная запись: pcm.default { type = plug } или pcm.default.type = plug.
Поэтому же (пример из /usr/share/doc/libasound2/examples/asoundrc.txt.gz):
DEF.NAME1 NAME2 # DEF.NAME1 is an alias for DEF.NAME2
Ибо это тоже что и (это я так предполагаю):
DEF { NAME1 = NAME2 }
Хотя кто его знает - может быть это означает NAME1 = "NAME2" ?
Есть ещё специальный механизм "@args", который позволяет вызвать slave с формированием нужных параметров:
pcm.demo {
@args [ CARD DEVICE ]
@args.CARD {
type string
default "supersonic"
}
@args.DEVICE {
type integer
default 0
}
type hw
card $CARD
device $DEVICE
}
Теперь его можно вызывать так (эквивалентно):
hw:0,1
hw:CARD=0,DEVICE=1
hw:{CARD 0 DEVICE 1}
А так можно ?
demo:0,1
demo:CARD=0,DEVICE=1
demo:{CARD 0 DEVICE 1}
Ну и прочие извращения:
plug:"hw:0,1"
plug:{SLAVE="hw:{CARD 0 DEV 1}"}
Что значат "!", часто употребляемые в примерах из инета ?
Для массивов pcm.!default означает, что предыдущее значение pcm.default можно забыть.
Если "!" нет - значит нужно объединить новое и предыдущее.
Есть и другие варианты: "?" - задаёт новое, если предыдущее не определено,
"+" - объединяет новое значение и предыдущее (действие по умолчанию),
"-" - изменяет существующее значение, если его нет - ругается.
Таким образом, строка, начинающаяся с !pcm, будет отличным способом выстрелить себе в ногу.
ALSA конфигурируется очень гибко.
Конфиг описывает конвейер обработки звука или чего нибудь ещё.
Конвеер состоит из элементов-обработчиков. Если в примере вам пишут "подключите плагин dmix" - это не значит, что dmix
- это такой настоящий плагин - типа как отдельный файл, который надо где-то скачать. Или модуль ядра, который загружается
через modprobe и виден через lsmod. Нифига: это просто кусок кода, вкомпиленный в asound.so. Так что не ищите его find'ом или lsmod'ом.
Обычно конвеер, с одной стороны, заканчивается ничем (и к нему подключается ваша программа), с другой стороны упирается в железку.
Однако ALSA также имеет механизм подключения и внешних плагинов (.so). Если вы их напишите, да.
Обработчиком может быть как плагин так и "устройство".
? "Устройства" можно рассматривать как обёртки вокруг плагинов. ?
? Возможно, устройства - это "этапы" конвеера (т.е. плагин + набор настроек) ?
Типичный синтаксис обработчика звукового конвеера (pcm):
pcm.ИМЯ-ЭТАПА {
type ПЛАГИН
ipc_key КАКОЕ-НИБУДЬ-ЧИСЛО
ещё какие нибудь параметры - они зависят от конкретного плагина
slave ИМЯ-ПРЕДЫДУЩЕГО-ЭТАПА
}
ИМЯ-ЭТАПА - его вы придумываете сами. Говорят, есть некие зарезервированные имена, но кроме default ничего в голову не приходит.
ПЛАГИН - plug, dmix, dsnoop, share, dshare, asym, hw, card, hooks, rate......
plug - преобразует формат по запросу (?то, что будет видно в списке aplay -L / arecord -L?) - полезно использовать как начало конвеера.
asym - тройник: ветвит конвеер на два продолжения (т.е. у него вместо "slave" отдельные "capture" и "playback" - продолжения).
multi - похож на asym, но конвеер ветвится не по направлению, а по каналам (левый, правый, фронт, тыл и т.д.).
route - Route & Volume: тасует каналы по заданной матрице уровней. Матрица содержит коэффициенты передачи каналов Client в каналы Slave.
Заметь, что plug тоже может делать подобное.
dmix - смешиватель нескольких потоков playback от разных программ в один поток.
dshare - обратный к multi: позволяет использовать отдельные каналы разными ветками master.
Похож на dmix, но легче и не умеет суммировать звук из разных программ. Работает тоже только с playback.
share - почти тоже самое, что dshare, но требует aserver.
dsnoop - расщепитель потока capture для нескольких разных программ.
null - глушитель. Ну да, даже такое есть.
softvol - тоже регулятор громкости, но совсем уж не документированный. Добавляет регулятор громкости в микшер ?
file - будет выгружать в файл/пайп либо брать оттуда.
hw - PCM-устройство ядра (ну то есть уже драйвер железки, обычно).
и тысячи их, включая всякие преобразователи, компрессоры, копировщики и ещё что-то.
См. /usr/share/doc/libasound2/examples/asoundrc.txt.gz
См. https://www.alsa-project.org/alsa-doc/alsa-lib/pcm_plugins.html
Набор остальных параметров зависит от типа плагина.
КАКОЕ-НИБУДЬ-ЧИСЛО - тоже придумайте сами, лишь бы было уникальным. Оно нужно для того, чтобы два разных процесса могли общаться относительно
совместного использования данного элемента конвеера. Нужно это не всем элементам. Например, модуль dmix как раз
и нужен, чтобы несколько прог могли использовать одну звукашку. Так что договариваться приходится. Так как всё крутиться в пользовательском
пространстве, то возможность договориться реализована через IPC. А чтобы было ещё веселее, к IPC можно применить традиционные права rwx
(ipc_perm, ipc_gid и ipc_key_add_uid) и получить презабавные эффекты.
ИМЯ-ПРЕДЫДУЩЕГО-ЭТАПА - куда скинуть результат дальше, если конвеер работает на вывод звука. Или от кого получать, если конвеер вводит звук.
Итоговым элементом конвеера можно задать собственно аппаратную звукашку, хотя, если глянуть /usr/share/doc/libasound2/examples/asoundrc.txt.gz
есть предположение, что можно заслать данные даже в сеть или в файл. Сослаться на аппаратную звукашку можно примерно так: pcm "hw:0,0".
Ссылаясь на предыдущий этап можно указать всякие интересные параметры (через ассоциативный массив):
period_time 0 period - это набор семплов, обрабатываемый за одно аппаратное прерывание. На прикладном уровне не очень интересен.
period_size 1024 size - объём в семлпах, time - объём в мкс.
buffer_size 8192 buffer - это набор period'ов, которые могут быть обработаны подряд,
без участия вашей программы. В общем-то это буфер, в который можно накидать и дрыхнуть. Или дрыхнуть, пока он заполняется.
rate 44100
channels 2
pcm "hw:0,0" вышеупомянуто
В программе вы ссылаетесь на звуковое устройство - им может быть любой из элементов конвеера, вы указываете на него через ИМЯ-ЭТАПА.
Всем известное ИМЯ-ЭТАПА - default.
Элемент конвеера может быть как двунаправленный (plug, например) так и однонаправленный (dmix, dsnoop...).
Плагин dmix имеет только playback, поэтому его slave должен уметь, как минимум, playback. Если slave умеет и capture - не проблема.
Плагин plug имеет playback и capture, поэтому напрямую dmix не может быть его slave'ом. Нужно использовать плагин asym между ними.
Но можно оставить только dmix и dsnoop концом конвеера, однако, в, таком случае, они будут разными устройствами и вам
придётся объяснять программе, что для playback у вас одно устройство, а для capture - другое.
И для mixer третье (и это не все программы смогут понять).
Какие конвееры ещё бывают, помимо pcm:
ctr - микшер
и другие - тысячи их: pcm_type, pcm_scope_type, pcm_scope, pcm_slave... и даже server. Конвееров как раз столько, сколько нужно.
Если вы думаете, что конвееров слишком мало или слишком много - см. рис. 1.
У каждого конвеера свои плагины. Конвеер pcm_slave часто необходимо использовать как обёртку для драйвера карты, хз почему
(т.е. нельзя сослаться на железку, например, как слейв для dshare - только через промежуточный pcm_slave).
На этом безумие гибкости ALSA не останавливается и состояния бельевой верёвки достигает в таком примере:
aplay -Dplug:\'dmix:SLAVE=\"hw:1,0\",RATE=44100\' x.wav
т.е. имя устройства - это не строка-идентификатор, как многие думают (как ещё вчера думал и я), а параметр, представляющий собой
- только в самом скучном случае - имя элемента в массиве pcm. На самом деле имя устройства - это такое же выражение,
которое можно написать в файле конфигурации.
/ Конечно, ALSA всё равно не сможет спихнуть sendmail с пьедестала по вывертам в конфиге. Но она уже обогнала squid по суммарному размеру файлов настроки /
Перехватить поток звука, варианты подумать и сконфигурировать:
- Виртуальная звуковуха-лупбэк: modprobe snd-aloop.
- Плагин pcm.type file: но файл только raw формата (впрочем, файлом может быть пайп,
а применив две копии плагина можно завернуть поток из playback в capture)
- Плагин pcm.shm + aserver
Варианты подумать и запрограммировать:
- Плагин pcm.ioplug + собственная прога.
- Можно написать собственный плагин в отдельной .so.
Сервер aserver
Осторожное теоретическое предположение (документов на него нет даже в исходниках, вопросов/ответов в инете тоже почти нет):
Так как asound - библиотека userland'a, для работы с несколькими приложениями ей нужно иметь возможность объединять данные
для немонопольного использования звукашки, т.е. координировать работу собственных копий кода, слинкованных с разными программами.
Для этого обычно используется разделяемая память (IPC). Но существует и другой механизм: aserver. Например, плагин pcm.dshare работает
через IPC, в то время как аналогичный по функционалу pcm.share - через aserver. Через IPC также работают dmix и dsnoop. Кто работает
через aserver кроме share - науке не известно. Нужен ли для pcm.share pcm.shm или же pcm.shm делает что-то обособленно-полезное также неизвестно.
Вероятно, для систем, где доступна IPC (shared memory) - это более быстрый вариант, чем aserver. Но aserver более универсальный (портируемый).
Буква 'd' в названиях dmix, dsnoop, dshare происходит от слова direct - т.е. прямой доступ к общему pcm-буферу.
Вопросы:
1) какие же из этапов конвеера видны по командам aplay -L / arecord -L ?
2) в чём разница между default и sysdefault ? почему sysdefault может быть не виден в списке, но доступен программам ?
3) что такое aserver.c, какая в нём польза, как использовать ? Связаны ли с ним плагин shm и конвеер server ?
4) плагин pcm.meter и level.c. Для чего нужен level.c ? Как это использовать ?
5) pcm.share vs pcm.dshare vs pcm.multi ? в чём разница ?
6) pcm.softvol - что же он делает или зачем ему min/max volume ?
7) dshare vs dmix ?
dmix суммирует звук от всех программ и выдаёт "итого". Каждый канал обрабатывает поотдельности.
dshare не суммирует звук, но позволяет каждый канал отдать отдельной программе, не получив при этом resource busy.
Как уверяет лично автор в alsa-devel@lists.sourceforge.net: если используется только одна программа на один канал,
то dmix аналогичен по эффекту dshare. Но dshare более программно-эффективен, так что не стоит без нужды использовать dmix.
-=-
Как изобразить две виртуальные одноканальные карты из двух каналов физической карты ?
(Чтобы два разных приложения могли читать и писать свой канал. Микшер не трогаем)
pcm.!default {
}
pcm_slave.hwcard {
pcm "hw:0,0"
# channels 8
# period_time 40000
# buffer_time 360000
# format S16_LE
# rate 32000
}
pcm.le {
type plug
slave.pcm {
type asym
playback.pcm {
type dshare
ipc_key 87654
slave hwcard
bindings [ 0 ]
}
capture.pcm {
type dsnoop
ipc_key 43210
slave hwcard
bindings [ 0 ]
}
}
}
pcm.ri {
type plug
slave.pcm {
type asym
playback.pcm {
type dshare
ipc_key 87654
slave hwcard
bindings [ 1 ]
}
capture.pcm {
type dsnoop
ipc_key 43210
slave hwcard
bindings [ 1 ]
}
}
}
ctl.mixer0 {
type hw
card 0
}
Теперь aplay -D le будет работать с левым каналом (думая, что у нас моно- карточка),
а arecord -D ri - с правым каналом.
arecord можно запустить несколько копий, а aplay - только одну копию на канал.
Часть II. Программируем сами лично.
Введение в плагин dshare. И выведение
Раньше моя программа работала нормально. Но после введения в конвеер плагина dshare начались приколы.
Пришлось узнать много нового и изобрести внеочередные костыли.
Сперва немного теории.
Я буду путать семплы и кадры. В алса они называются кадрами (frame). Кадр - это описатели мгновенных значений сигнала во всех каналах,
которые связаны с данным звуковым потоком. То есть если у вас два канала, в первом уровень 3032, во втором - 4046, то кадр - это вот
эти два числа - т.е. все 4 байта. Размер кадра зависит от числа каналов и числа бит на канал.
Но мне больше нравится слово "семл" (в алса он означает мгновенное значение сигнала в одном канале, но употребляется редко).
Звуковой поток (snd_pcm_t) описывается кучей параметров, некоторые из которых можно попробовать сменить.
Перечислим их, иногда включая оригинальное описание на английском:
start_threshold - автоматически начинает работу если число кадров в буфере >= threshold
stop_threshold - автоматически прекращает работу если число кадров >= threshold (для capture)
Если порог остановки >= boundary то автоматическая остановка запрещена и мы получаем бесконечный цикл.
For capture devices, an overrun happens when the number of available frames (i.e., frames captured but not yet read from the buffer)
reaches the stop threshold. Overruns can happen only with capture devices.
Для устройств захвата перебегание фиксируется, когда число доступных кадров (захваченных из мира, но не прочитанных программой)
>= stop threshold.
For playback devices, an underrun happens when the number of available frames (i.e., free space in the buffer) reaches the stop threshold.
Underruns can happen only with playback devices.
Для устройства воспроизведения недобегание фиксируется, когда число доступных кадров (свободное пространство в буфере)
>= stop threshold.
Т.е. понижение порога остановки сделает остановку более вероятной.
Эти два параметра вроде бы как понятны. Оба парамера задаются нашей программой и никак иначе.
Первый задаёт условие автоматического запуска кодека, второй - автоматической остановки. Если автоматика в вашем коде уместна.
В моём коде она была, практически, жизненно необходима.
Идём дальше:
silence_threshold - буфер воспроизведения заполняется тишиной, если воспроизведение недобегает (?ближе?) больше чем порог.
silence_size - Специальный случай когда silence_size >= boundary.
В этом случае неиспользуемая часть буфера заполняется тишиной на старте. Позднее, только что обработанная часть
заполняется тишиной. silence_threshold должен быть задан = 0.
snd_pcm_sw_params_get_silence_size()
snd_pcm_sw_params_set_silence_size()
A portion of playback buffer is overwritten with silence when playback underrun is nearer than silence threshold (see snd_pcm_sw_params_set_silence_threshold)
The special case is when silence size value is equal or greater than boundary.
The unused portion of the ring buffer (initial written samples are untouched) is filled with silence at start.
Later, only just processed sample area is filled with silence. Note: silence_threshold must be set to zero.
snd_pcm_sw_params_get_silence_threshold()
snd_pcm_sw_params_set_silence_threshold()
A portion of playback buffer is overwritten with silence (see snd_pcm_sw_params_set_silence_size) when playback underrun is nearer than silence threshold.
Застлала пелена непонимания глаза мои, когда я пытался это вкурить. Увы, безуспешно.
Да, ясно что это всё - для некоей гарантии того, что при исчезновении пакетов воспроизведение не начнёт что-то
недоумённо шипеть и щелкать, но будет вставлена некая тишина вместо пропавшего пакета, однако вот
как и по какой логике ? Может быть эта вставленная тишина будет временно откладывать срабатывание stop_threshold ?
Я не стал проверять. Заметил только, что во всех экспериментах и точках конвеера эти два параметра всегда были = 0.
И ещё три параметра, но эти параметры наша программа может прочитать сама, а также мы можем их увидеть в отладочном дампе
и, наверное, в /proc/asound/...
delay - зазор (в семплах этот и все последующие параметры) между реально проигрываемым семплом и последним переданным от программы семплом
(для воспроизведения). Для захвата примерно то же самое. Похоже, что delay + avail == buffer_size.
avail_min - min available space for wakeup is # microseconds. Как это понять ?
avail - Number of frames ready to be read/written. Это - очень полезный параметр. В звуковом канале всегда присутствует
некоторая гонка: либо вы накидываете семплы и кодех их воспроизводит либо кодек накидывает, а вы забираете.
avail - это параметр показывающий, на сколько кадров (семплов) вы можете записать (т.е. для воспроизведения - это объём свободной памяти)
или прочитать (для записи - это количество уже ожидающих вашу программу семплов).
avail_max - Maximum number of frames ready to be read/written, которые были доступны после последнего snd_pcm_status.
avail_max and overrange are reset to zero after the status call.
Этот параметр интересен для статистики, если у вас есть проблемы со стабильностью чтения/записи семплов.
tstamp - "now" timestamp from a PCM status container ( timestamp of last pointer update ).
trigger_time - время срабатывания триггера/фронта смены состояния канала (run/stop/pause...)
Не знаю, для чего они полезны, но всё равно прикольно.
Также напомню ещё о двух параметрах: buffer_size (или же buffer_time) и period_size (или period_time).
time - это время в мкс, size - время в кадрах (с учётом семпл-рейта). period - это некое количество семплов,
которые могут быть обработаны иногда только группой (например, определённый лимит на размер period может накладываться
драйвером звуковухи). buffer - это много period. Размер buffer кратен размеру period.
buffer нужен для того, чтобы кодеку было что петь или куда складывать данные, если ваша программа немного занята.
Вы можете задавать размеры как buffer так и period в своей проге или в конфиге alsa.
Вы можете задать buffer = 2 * period - один период будет воспроизводиться, во второй ваша прога в это время
будет сливать данные. Но вы можете сделать буфер и гораздо большего размера, если данные у вас поступают или
забираются неравномерно.
Типичный жизненный цикл звукового потока описывается в виде enum snd_pcm_state_t.
Там с десяток фаз, но самых интересных 4 (в скобках их численные значения, их удобно знать при отладке программ):
PREPARED (2) - состояние паузы перед началом работы. Когда мы корретно создаём поток воспроизведения, в начале своей жизни
он находится в этом состоянии. Выйти из него в режим RUNNNING он может либо по условию start_threshold >= avail
(т.е. ваша прога накидала в buffer достаточно данных для начала) либо при вызове процедуры snd_pcm_start().
Поток захвата всегда готов к чтению данных, если они есть, конечно. А вот какая при этом будет фаза - я не разбирался.
RUNNING (3) - состояние вопроизведения. Пока вы докидываете в поток воспроизведения данные, кодек работает.
Если вы перестали докидывать данные и свободного места уже avail >= stop_threshold кодек должен бы остановиться
(т.е. поток переходит в состояние XRUN).
XRUN (4) - вы не успевали забрать захваченные данные или докинуть новых для воспроизведения ? Поэтому поток попал в состояние
XRUN. Тут есть интересная особенность: в зависимости от фаз луны или конфигов (как тех, которые вы создали сами,
так и тех, которые, ценой неимоверных взаимных усилий, создали разные плагины вашего конвеера) или применяемых драйверов железа или от версии ALSA,
вы можете попробовать как продолжить подкидывать или читать данные в/из потока, тем самым переведя поток
в состояние RUNNING, так и может случиться следующее: на попытку что нибудь сделать вы будете получать ошибку
-PIPE. Вы будете удивлены, взбешены, перегуглите весь инет, и, если повезёт, найдете в примерах
хитрую процедуру xrun_recovery, которая вызывает разные спецслужбы в случае различных ошибок.
Она выглядит как-то примерно так:
if (err == -EPIPE) { // under-run : прога тормозит по отношению к драйверам (как при чтении так и при записи)
err = snd_pcm_prepare(handle); // чёткого объяснения, почему вызывается именно эта функция, не найдено
if (err < 0) printf("ALSA: Can't recovery from underrun, prepare failed: %s\n", snd_strerror(err));
return 0;
} else if (err == -ESTRPIPE) { // Выполнена приостановка драйверов. Зачем, почему - никто не знает.
while ((err = snd_pcm_resume(handle)) == -EAGAIN) sleep(1); // Ждём, как того требует документация.
if (err < 0) {
err = snd_pcm_prepare(handle);
if (err < 0) printf("Can't recovery from suspend, prepare failed: %s\n", snd_strerror(err));
}
return 0;
}
return err;
// Также есть любопытная функция snd_pcm_recover
// Также надо проверять на ошибку EAGAIN - она не требует восстановления, но просит повторить вызовы read/write на бис.
Но факт, что snd_pcm_prepare возвращает поток в состояние PREPARED.
Есть ещё одна полезная процедура, которую стоит знать: snd_pcm_drain. Она переводит поток в состояние ... какое бы вы думали ?
В состояние SETUP (1). Вот нет в 2, не в 4, а именно в 1 ! Бугага... Т.е. если вы просто хотите сделать перерывчик
в вещании, то мануал на процедуру snd_pcm_pause вежливо предупреждает, что это состояние может поддерживаться не любым железом,
так что забудьте о ней. А если вы нежно тормознёте поток через snd_pcm_drain, то потом его можно завести только сделав предварительно
snd_pcm_prepare. Иначе start_threshold не сработает (но может быть сработает snd_pcm_start ?).
Кстати, пробовали запускать aplay или arecord с параметрами -v ? Они выводят прикольный дамп отладочной инфы, в котором
показывается фактически созданный конвеер плагинов и их текущие параметры ? Такой же отчёт легко получить в своей программе
примерно таким образом:
printf("ALSA: dump\n"); // Много интересного
snd_pcm_dump(handle_play, output);
printf("ALSA: dump hw\n"); // Выжимка по основным параметрам
snd_pcm_dump_hw_setup(handle_play, output);
printf("ALSA: dump sw\n"); // Ещё выжимка
snd_pcm_dump_sw_setup(handle_play, output);
printf("ALSA: status\n"); // Ну кисонька, ну ещё капельку
snd_pcm_status_t * status;
snd_pcm_status_alloca(&status); // Кроме прочего, тут выводится состояние потока,
snd_pcm_status(handle_play, status); // но почему-то оно у меня всегда выводилось как RUNNING
snd_pcm_status_dump(status, output); // хотя snd_pcm_state возвращал более разнообразные значения.
Совсем не больно и не страшно. Надо только учитывать, что у юзерлендовой части ALSA есть свой кеш параметров,
и эти вызовы будут синхронизовать его с параметрами, сидящими в ядре. Таким образом, если ваша прога написана
с использованием быстрых вызовов (которые не обращаются к ядру), внедрение этой конструкции может повлиять
на логику работы.
То была красивая теория. И она работала с sysdefault и с default-устройствами. Особенно на обычных компах.
Но потом я заботливо разложил себе грабли. Судя по всему, они были целиком связаны не то с особой одарённостью плагина dshare,
не то с моим непониманием чего-то, чего я так и не понял.
Часть IIa - Пороги запуска и остановки (start_threshold, stop_threshold)
Итак, после подключения плагина dshare сперва стал икать звук. Громко и противно.
Замечу, что aplay работал при этом хорошо.
Причина была выявлена всего через пару часов ковыряний.
Оказалось, что все рекомендации моей программы по start_threshold, buffer и period пошли кисе под пушистый хвост.
И вот почему: dshare, если у его слейва (в моём случае слейвом был собственно драйвер звукашки)
не заданы параметры buffer и period, жестко задаёт period_time = 125000 мкс. И ему совсем не интересны иные
варианты. Как проверить ? Запустите aplay с параметром --dump-hw-params и увидите что нибудь вроде PERIOD_TIME: 125000.
И никаких вариантов. Если варианты есть, они выводятся как два числа в скобках.
Почему уплыли start_threshold и прочее ? Потому что прога вычисляла их исходя из своих хотелок, но
конвеер был вынужден подправлять значения под жестко заданный period_time и выправлялки просто приводили
к следующему: start_threshold == buffer_size. Т.е. пока прога целиком не забивала весь буфер, воспроизведение не начиналось.
А как только буфер оказывался полностью забит, забить ещё один пакетик, который уже вот-вот прибудет, получалось некуда и он отбрасывался
прогой.
Итого: потеря пакетов и отсюда икания. А заодно и сильно отложенный запуск воспроизведения.
Лечение пришлось проводить путём правки раздела драйвера в /etc/asound.conf:
pcm_slave.hwcard {
pcm "hw:0,0"
# Этот параметр можно задать только здесь, dshare не позволяет его задавать нигде больше,
# включая попытки задать его сверху, например, из клиентской программы
period_size 960
# Семпл-рейт dshare по умолчанию ставит 48000, что полезно, но это даёт period_size = 960.
# buffer_size можно задать любым, но он будет округлён до period_size * n.
# Похоже, лимит на buffer_size где-то хардкодед в значение 16к
buffer_size 15360
}
Часть IIb - что это было ?
Вторая проблема: икания после окончания воспроизведения.
Когда пакеты со звуком заканчиваются, происходит underflow буфера и звук должен прекратиться. Но dshare думает по другому.
Когда прога переставала закидывать пакеты, на звуковом выходе начинался повтор всего, что было в buffer.
Ну или не всего, но какой-то его значительной части.
Ещё часов 5 у меня ушло на то, чтобы убедится:
a) поток переходил в состояние XRUN - т.е. stop_threshold срабатывает. Попытки снизить stop_threshold ни к чему новому не приводили.
b) stop_threshold был логичным на уровне плагинов plug.pcm, dshare.pcm, а вот на уровне драйвера (hw) он был кране любопытным:
stop_threshold : 1572864000; boundary : 1572864000.
Согласно докам, это как раз тот - очень нужный - случай, когда автоматической остановки не будет и звук будет крутиться
белкой в буфере.
Интересно, что при работе через цепочку sysdefault (plug.pcm и hw-драйвер) hw-драйвер имел вполне корректные (заданные моей прогой)
пороги. Т.е. dshare либо забил на мои пожелания, либо сама логика его работы требует от дравера звукашки именно таких порогов.
Но почему тогда он не учитывает пожелания проги внутри себя ? Между прочим какой-то код, похожий на проверку avail vs stop_threshold
у него внутри есть...
Задать threshold в asound.conf нельзя.
Что пришлось делать ?
Сперва пришлось убедиться, что, всё таки, stop_threshold срабатывает, хотя бы где-то вблизи плагина plug.pcm.
А потом сделать примерно следующее:
snd_pcm_avail(handle_play);
if (snd_pcm_state(handle_play) == SND_PCM_STATE_XRUN) {
snd_pcm_drain(handle_play);
printf("playback underun\n");
}
if (snd_pcm_state(handle_play) == SND_PCM_STATE_SETUP) {
snd_pcm_prepare(handle_play);
}
Т.е. если мы обнаружили, что поток перешел в состояние XRUN, переводим его в состояние SETUP. Тогда, наконец, мяуканье
затыкается. А чтобы автоматика могла снова запустить поток после достаточного объёма write, сразу переводим его в состояние PREPARE.
А зачем нужен snd_pcm_avail в начале ? А потому что иначе snd_pcm_state возвращает _кешированное_, а не актуальное
состояние потока. Т.е. там в ядре уже сухой буфер, а здесь нам snd_pcm_state говорит - "да не, всё путём, ещё есть что спеть".
Дока это комментирует, заводя глазки в потолок: "This is a faster way to obtain only the PCM state without calling ::snd_pcm_status()."
Может быть, всё это можно было пролечить правильным выбором параметров silence_* ?
Может быть для того они и существуют ?
Часть IIc - what's fuck ?
Третья проблема: путаем каналы.
Я даже не представляю себе, чей это баг, какой части алса.
Моей проге в конфиге четко задаётся с каким из устройств она работает - le или ri. Она работает только с одним из устройств.
Причем le сконфигурирован захватывает данные, например, с канала 0, а воспроизводит на канале 1.
А ri - наоборот.
Но при перезагрузке ОС и первом запуске прога, которой, например, было задано устройство le, захватывала данные с нужного канала,
а выводила их не туда (т.е. на тот же, с которого захватывала) 8-).
При одновременно запуске aplay на другом устройстве (ri) там была та же чехарда - звук шел не в свой канал.
Если перезапускать aplay не перезапуская прогу, ничего не менялось.
Если обе проги перезапустить - всё становилось на свои места и больше не путалось. До перезагрузки ОС.
Решение, которое сработало: в /etc/rc.local запихнуть строку:
arecord -D ri -d 1 - | aplay -D le -
Она ничего не делает, кроме того, что лечит путаницу в каналах....
Может быть, "arecord -d 1 > /dev/zero" тоже бы сработало.
Как ? Где запоминается что-то, что влияет на дальнейшую работу ?
Моя прога при запуске сразу вызывает alsactl restore
так что, вроде бы, нигде в микшере ничего прятаться не должно.
Кстати, а почему я вызываю alsactl из своей проги ?
А потому что драйвер звукашки сбрасывает настройки микшера при вызове snd_pcm_hw_params. Опа!
Причем это явно драйвер, так как такое поведение наблюдалось только на конкретном драйвере.
Владимир