Что: 952afcae493f91e4018dfe7c2e65b5aad48b3788
Когда: 2021-10-13 21:12:34+03:00
Темы: bsd hard multimedia
Звук в FreeBSD https://meka.rs/blog/2021/10/12/freebsd-audio/ Ёмкая статья про ситуацию со звуком в FreeBSD. Если коротко, то FreeBSD давным давно была куда более пригодной для работы со звуком, ибо маленькие задержки и jitter -- Linux какаха (по собственному опыту помню). А всякие современные средства типа virtual_oss позволяют очень гибко самовыражаться. FreeBSD, как и прежде, рулит во многих областях.
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.
From: Sergey Matveev Date: 2021-10-14 11:04:09Z Я совершенно далёк от звука в плане программирования -- являюсь чисто пользователем, с никогда даже не писавшим какое-либо воспроизведение.
From: kmeaw Date: 2021-10-14 16:38:07Z > Но разве эта проблема не остаётся и в архитектуре с pull-ом? Разница в том, что ядро знает, сколько нужно данных (оно представляет себе и бюджет CPU, который был выделен программе, и время, за которое опустошится буфер) и сообщает программе об этом в явном виде (len), а в push прикладной разработчик должен сам решить, сколько данный отдать. Управлять косвенно количеством переключений контекста можно и в pull - достаточно заказать буфер побольше. > Никто же не гарантирует что квант времени исполнения будет передан > вовремя программе, чтобы она выполнила эту функцию Тут есть простая и понятная связь с причиной. Возникло прерывание от звуковой карты - значит пора вытеснять текущий поток (и это удобно в этот момент делать, как раз из-за прерывания ядро получило управление) и переключаться в тот, который генерирует семплы. > Неприятно что тут попахивает эмпирикой, видимо, чтобы понять какие > именно нужны значения. Именно в этом и проблема. Конкурентная нагрузка на систему возросла, аудиопоток стал вытесняться чаще. Всё, что можно сделать в push, чтобы адекватно отреагировать на такую ситуацию - это пытаться запихнуть ещё и ещё, пока write не заблокируется. То есть при попытке эмулировать pull с помощью push придётся вызывать callback с фиксированным len несколько раз, а не один раз сразу с большим размером. Либо в планировщик ОС добавлять костыль, который будет понимать, что вот этот select, poll или просто блокировка на write особенная, и надо поставить поток в расписание таким образом, чтобы к моменту его пробуждения типичный размер его write (ещё одна эвристика) привёл к дозаполнению буфера.
From: Sergey Matveev Date: 2021-10-14 18:18:12Z
Сгенерирован: SGBlog 0.34.0