[about] [index] [prev] [2021-10-13 21:12:34+03:00] [952afcae493f91e4018dfe7c2e65b5aad48b3788]
Topics: [bsd][multimedia]

Звук в FreeBSD

https://meka.rs/blog/2021/10/12/freebsd-audio/
Ёмкая статья про ситуацию со звуком в FreeBSD. Если коротко, то FreeBSD
давным давно была куда более пригодной для работы со звуком, ибо
маленькие задержки и jitter -- Linux какаха (по собственному опыту помню).
А всякие современные средства типа virtual_oss позволяют очень гибко
самовыражаться. FreeBSD, как и прежде, рулит во многих областях.

[leave comment]
comment 0:
From: kmeaw
Date: 2021-10-14 00:38:04Z

Уже очень давно не программировал под OSS, но его (по крайней мере
прежний) дизайн кажется ошибочным.

Хотя мне и не нравится современная тенденция в Linux делать для всего
новые ad-hoc интерфейсы вместо универсальных (как в UNIX/Plan9)
read/write для всего, /dev/dsp не выглядит хорошим интерфейсом для
звука, и на мой взгляд реализация особого API для звука - это
необходимость.

Дело в том, что человек чувствителен к buffer overrun. Если очередной
кадр видеопотока раз в пару минут отобразится на несколько миллисекунд
позже, то он ничего не заметит, а со звуком такое не пройдёт. При
воспроизведении видеофайлов, плееру по этой причине сильно проще
синхронизировать скорость видео к скорости аудио, а не наоборот -
последний вариант тоже возможен, но требует нетривиальной обработки
(растягивания звука во времени без изменения частоты).

Типичная звуковая карта при воспроизведении звука использует аппаратный
буфер, в который с одной стороны пишет процессор, а с другой стороны
читает (с помощью DMA) и преобразует в аудиосигнал аппаратура карты.
Когда читающий указатель оказывается в опасной близости от пишущего,
генерируется прерывание, сообщающее ОС, что пора бы побольше семплов
докинуть.

В DOS на SB16 это выглядит очень просто. У карты есть два буфера,
заполняем оба двумя первыми блоками аудиофайла и командуем DSP начать
воспроизведение, предварительно настроив регистры (sampling rate,
transfer mode, block size). Когда закончит играться первый буфер, SB16
возбудит прерывание, и программа будет должна обновить первый буфер до
того, как заончит играться второй - в этот момент SB16 снова оповестит
программу, которая заполнит второй буфер новыми данными. И так до тех
пор, пока у нас есть, что играть.

В многозадачных ОС с драйверами появляется масса прослоек, а программы
уже не владеют процессором монопольно. Есть два очевидных способа
построить API для взаимодействия прикладного приложения с аудиокартой.
Первый это push - приложение делает write-подобный вызов, сообщая ОС,
что надо сыграть вот эти семплы. И, наоборот, pull - ОС забирает данные
из программы.

push-семантика хорошо ложится на write, и она реализована в OSS -
приложение открывает /dev/dsp, с помощью ioctl настраивает желаемый
формат данных, после чего периодически делает write. Так легко написать
какой-нибудь аудиоплеер, где все данные известны заранее, из которых
процессор с лёгкостью (скорость генерации в тысячи раз превышает
скорость потребления) создаёт семплы, которые передаются во write.

pull-семантика гораздо больше похожа на то, что реализовано в железе.
Более того, имея pull API, можно в юзерспейсе эмулировать push - для
этого достаточно создать кольцевой буфер, откуда pull callback будет
забирать то, что положил туда push-клиент. Наоборот, эмулировать pull
имея только push-интерфейс, уже не получится.

Проблема push в том, что пользовательский поток в не-реалтаймовой ОС не
имеет гарантии на минимальное время до получения очередного кванта
исполнения. А значит он не знает, сколько данных надо за'push'ить, чтобы
не случился buffer overrun. select/poll в чистом виде тут не сработает,
так как ОС разблокирует поток, даже если туда можно будет записать всего
один семпл. В OSS есть костыль, который позволяет обойти эту проблему -
ioctl SNDCTL_DSP_LOW_WATER, позволяющий указать, сколько свободных байт
должно появиться в буфере, прежде чем select/poll должен
разблокироваться. Но насколько сильно задрать low watermark - тоже
непонятно, поэтому приходится делать всё с запасом. Может быть ядро
FreeBSD особенным образом планирует процессы, заблокированные на
select/poll/kqueue содержащие /dev/dsp в своём наборе ожидания, чтобы
избежать этих трудностей?

У pull такой проблемы нет - приложение точно знает, сколько данных от
него хотят. Вот пример на SDL:
https://www.libsdl.org/release/SDL-1.2.15/docs/html/guideaudioexamples.html
JACK, которым пользуются профессионалы, тоже предоставляет похожий API:
https://github.com/jackaudio/example-clients/blob/master/metro.c

Жаль, что в статье не рассказывается, какая именно ошибка (дизайна или
реализации) приводит к тому, что у Linux jitter оказывается заметно
выше, чем у FreeBSD.
comment 1:
From: Sergey Matveev
Date: 2021-10-14 11:04:09Z

Я совершенно далёк от звука в плане программирования -- являюсь чисто
пользователем, с никогда даже не писавшим какое-либо воспроизведение.

*** kmeaw [2021-10-14 03:32]:
>Проблема push в том, что пользовательский поток в не-реалтаймовой ОС не
>имеет гарантии на минимальное время до получения очередного кванта
>исполнения.

Но разве эта проблема не остаётся и в архитектуре с pull-ом?
ALSA/SDL/whatever дёргают hook/handler (fill_audio в SDL примере), но
никто же не гарантирует что квант времени исполнения будет передан
вовремя программе, чтобы она выполнила эту функцию? То что в pull чётко
говорится сколько данных хотят от программы -- ok, а в push мы должны
или на каждый чих, по одному sample засовывать данные, либо использовать
хак, который "будит" select не на каждый чих. Это вроде бы ясно, но
разве что либо из этого влияет на гарантии что программе дадут
управление в нужный момент времени? Вроде бы, что дёргнуть нужный hook
или вернуть управление из select-а -- одно и тоже же по сути?

Да и вариант с LOW_WATER мне нравится тем, что завысив его, я могу
управлять косвенно количеством переключений контекста: я не прочь
потратить мегабайт памяти чтобы один раз его заполнил
декомпрессированным FLAC/WavPack/whatever, и ждать много секунд до
переключения в проигрыватель назад. А если мне нужна VoIP программа, то
она будет маленькие буферы использовать, чтобы и понимать в каком она
сейчас состоянии во времени находится. Неприятно что тут попахивает
эмпирикой, видимо, чтобы понять какие именно нужны значения.
comment 2:
From: kmeaw
Date: 2021-10-14 16:38:07Z

> Но разве эта проблема не остаётся и в архитектуре с pull-ом?

Разница в том, что ядро знает, сколько нужно данных (оно представляет
себе и бюджет CPU, который был выделен программе, и время, за которое
опустошится буфер) и сообщает программе об этом в явном виде (len), а в
push прикладной разработчик должен сам решить, сколько данный отдать.

Управлять косвенно количеством переключений контекста можно и в pull -
достаточно заказать буфер побольше.

> Никто же не гарантирует что квант времени исполнения будет передан
> вовремя программе, чтобы она выполнила эту функцию

Тут есть простая и понятная связь с причиной. Возникло прерывание от
звуковой карты - значит пора вытеснять текущий поток (и это удобно в
этот момент делать, как раз из-за прерывания ядро получило управление) и
переключаться в тот, который генерирует семплы.

> Неприятно что тут попахивает эмпирикой, видимо, чтобы понять какие
> именно нужны значения.

Именно в этом и проблема. Конкурентная нагрузка на систему возросла,
аудиопоток стал вытесняться чаще. Всё, что можно сделать в push, чтобы
адекватно отреагировать на такую ситуацию - это пытаться запихнуть ещё и
ещё, пока write не заблокируется. То есть при попытке эмулировать pull с
помощью push придётся вызывать callback с фиксированным len несколько
раз, а не один раз сразу с большим размером.

Либо в планировщик ОС добавлять костыль, который будет понимать, что вот
этот select, poll или просто блокировка на write особенная, и надо
поставить поток в расписание таким образом, чтобы к моменту его
пробуждения типичный размер его write (ещё одна эвристика) привёл к
дозаполнению буфера.
comment 3:
From: Sergey Matveev
Date: 2021-10-14 18:18:12Z

*** kmeaw [2021-10-14 19:33]: [...]
Ok, спасибо. Звучит всё логично. Действительно, с "pull" архитектурой,
как минимум, ядро понимает особенности hook-а и может предпринимать
что-то особое для планировщика.

Вот правда, пока искал что нового в OSS4 (относительно прежних версий,
которые даже когда-то в Linux были), нашёл тьму статей (вроде бы и
свежих), что OSS всё равно имеет меньший задержки и гораздо надёжнее в
плане стабильной подачи звука. Ещё статьи есть про sndio подсистему
OpenBSD, вроде бы считающуюся чуть ли не лучшей. Но это наверное уже
связано с тем, что BSD умеют даже с "push" OSS-ом работать хорошо.
Верное же заметили что косяки в воспроизведении звука будут абсолютно
недопустимы для человека -- мы это очень хорошо замечаем сразу же.

Но это holywar тема про опыт на практике, ибо найдутся миллионы людей
для которых PulseAudio работает как часы без проблем, и ещё миллион для
которых звук работает достойно только после удаления PulseAudio (реально
ведь в рассылках не раз видел: "проблема со звуком: ...", "сделайте apt
remote pulseaudio", "спасибо, всё заработало, помогло!" :-)). А я то
ведь ещё играл в Quake3 порт для GNU/Linux, который умел выводить только
в OSS, никак иначе.