27 нояб. 2015 г.

Примеры декодирования и проигрывания аудио-файла в Linux

Здесь можно найти несколько примеров того, как декодировать и воспроизвести аудио-файл, с использованием разных библиотек.

Код доступен на github.

Каждый пример это небольшая программа. Примеры делятся на два типа:
  • decode: программа декодирует заданный файл и выводит сырые сэмплы в stdout;
  • play: программа читает сырые сэмплы из stdin и посылает в звуковую карту.

Разделение на два шага введено специально, чтобы продемонстрировать, как можно получить сырые сэмплы, и как их можно воспроизвести. Библиотеки вроде FFmpeg и SoX позволяют сделать это также и без промежуточного шага, если построить более сложный pipeline.

Сырые сэмплы на выходе/входе всех примеров имеют одинаковый формат:
  • два канала (front Left, front Right);
  • interleaved format (L R L R ...);
  • сэмплы это 32-битные float-ы в little endian;
  • частота сэмплирования 44100 Hz.

Примеры не выполняют конвертацию порядка байт на входе/выходе, так что будут работать только на little-endian архитектурах.


Пример запуска


Программы decode и play можно объединять через пайп. Допустимы любые комбинации, например:

$ ./ffmpeg_decode     cool_song.mp3   |  ./alsa_play_tuned
$ ./sox_decode_chain  cool_song.mp3   |  ./ffmpeg_play
$ ./sndfile_decode    cool_song.flac  |  ./sox_play


FFmpeg


ffmpeg_decode

  • открывает входной файл, ищет в нем аудио-стрим и открывает подходящий декодер;
  • инициализирует конвертер из формата входного файла (определенного автоматически) в выходной формат (2 канала, 32-битные флоаты);
  • читает пакеты из входного файла;
  • передает полученные пакеты декодер и получает из него декодированные фреймы;
  • передает декодированные фреймы в конвертер и получает из него выходные сэмплы;
  • пишет сэмплы в stdout.
Конвертер может выполнять буферизацию, если входных данных больше, чем выходных. Если аккуратно настроить размеры пакетов/фреймов, можно избежать буферизации.

ffmpeg_play

  • открывает устройство вывода ALSA;
  • настраивает формат вывода;
  • читает сэмплы из stdin, формирует из них пакеты и отправляет в устройство вывода.

ffmpeg_play_encoder

  • открывает устройство вывода ALSA;
  • настраивает формат вывода;
  • создает энкодер для выходного формата;
  • читает сэмплы из stdin, формирует из них фреймы и передает их в энкодер;
  • получает их энкодера пакеты и отправляет их в устройство вывода.

SoX


sox_decode_simple

  • открывает входной файл;
  • если входной формат (количество каналов, частота сэмплирования) отличается от выходного, завершается с ошибкой;
  • читает сэмплы из входного файла и пишет их в stdout.

sox_decode_chain

  • открывает входной файл и определяет входные параметры;
  • настраивает выходные параметры;
  • создает и запускает цепочку эффектов:
    • ввод: чтение входного файла;
    • ресемплинг (эффект включается, если входная и выходная частота сэмплирования отличаются);
    • ремаппинг каналов (эффект включается, если входные и выходные каналы отличаются);
    • вывод: вызов коллбэка для записи сэмплов в stdout.

sox_play

  • открывает устройство вывода ALSA (также можно заменить "alsa" на "pulseaudio");
  • читает сэмплы из stdin и пишет их в устройство вывода.

 

ALSA (libasound)


alsa_play_simple

  • открывает pcm;
  • устанавливает параметры в значения по-умолчанию;
  • читает сэмплы из stdin и отправляет в pcm.

alsa_play_tuned

  • открывает pcm;
  • устанавливает все доступные параметры;
  • читает сэмплы из stdin и отправляет в pcm.

Интерес представляют параметры, связанные с циклическим буфером. При записи данных в pcm, они добавляются в циклический буфер, из которого ALSA читает по таймеру.

Переполнение буфера называется "buffer overrun". Попытка чтения (со стороны ALSA, по таймеру) из пустого буфера называет "buffer underrun". Вместе эти события называются "xrun".

В частности, при возникновении buffer underrun, воспроизведение прекращается до вызова snd_pcm_recover(). Если софт все время не успевает добавить данные в буфер до того, как ALSA попытается их прочитать, пользователь будет слышать "заикания" и видеть в консоли сообщения про "alsa xrun".

Настройки циклического буфера:
  • buffer_size - количество сэмплов в циклическом буфере;
  • buffer_time - продолжительность всего буфера в микросекундах;
  • period_size - количество сэмплов, которое ALSA вычитывает из циклического буфера по тику таймера;
  • period_time - интервал таймера в микросекундах.
И связанные с ним:
  • start_threshold - перед стартом воспроизведения, ALSA не должна начитать вычитывать данные, пока в циклическом буфере не накопится start_threshold сэмплов;
  • avail_min - во время воспроизведения, ALSA не должна вычитывать данные, пока в циклическом буфере не накопится avail_min сэмплов.
Как выбирать значения для избежания buffer underrun:
  • period_size надо  установить равным размеру буферов, которые софт будет отправлять в pcm за одну запись; чем больше, тем меньше вероятность underrun и больше latency;
  • buffer_size надо установить кратным period_size и в несколько раз больше;
  • start_threshold разумно установить раным buffer_size; в этом случае, чем больше buffer_size, тем меньше вероятность underrun и больше latency;
  • avail_min разумно установить равным period_size.

libsndfile


sndfile_decode

  • открывает входной файл и получет его параметры;
  • если входной формат (количество каналов, частота сэмплирования) отличается от выходного, завершается с ошибкой;
  • читает сэмплы из входного файла и пишет их в stdout.

libsndfile очень компактная библиотека с простым API, но поддерживает только простые форматы, вроде WAV и FLAC. Возможности ограничиваются чтением/записью файлов, но для ресемплинга можно использовать libresample (SRC) от того же автора.

Замечания


SoX может использовать libsndfile как бэкенд. FFmpeg может использовать SoX для ресемплинга. И SoX, и FFmpeg поддерживают ALSA и pulseaudio, но утилиты FFmpeg выводят звук не через них, а через SDL.

Другие  библиотеки, для которых нет примеров:

Все библиотеки (и для которых есть примеры, и для которых нет) кроссплатформенные.

Icons made by Anton Saputro from www.flaticon.com is licensed by CC BY 3.0

25 нояб. 2015 г.

Файловые блокировки в Linux

Advisory locking


Рекоммендательные блокировки работают только тогда, когда процессы явно устанавливают  или снимают блокировку. Они не влияют на процессы, которые ничего не знают о блокировках.

У всех блокировок есть два режима: exclusive и shared.

BSD locks (flock)

Самый простой вид блокировок это flock(2). Особенности:
  • входят в POSIX;
  • устанавливают блокировку на файл целиком;
  • привязаны к файловому дескриптору;
  • не обеспечивают атомарное изменение режима блокировки;
  • не поддерживают NFS (в Linux).
Эти блокировки ассоциируются с файловым дескриптором:
  • дубликаты файловых дескриторов созданные с помощью dup(2) ссылаются на одну и ту же блокировку;
  • независимые файловые дескпторы, полученные для одного и того же или разных файлов с помощью open(2) ссылаются на разные блокировки.
Это, в частности, позволяет избежать проблем с потоками (см. следующую секцию).

Однако, flock() не гарантирует атомарного переключения между режимами exclusive и shared. При изменении режима блокировки, flock() сначала отпускает ее, а затем ожидает возможности захватить ее с новым режимом.

POSIX record locks (fcntl)

Более сложный (и медленный) вид блокировок это POSIX record locks (см. секцию "Advisory record locking" в fcntl(2)). Особенности:
  • входят в POSIX;
  • устанавливают блокировку на заданный интервал байт в файле;
  • привязаны к паре [i-node, pid], а не файловому дескриптору;
  • обеспечивают атомарное изменение режима блокировки;
  • поддерживают NFS (в Linux).
 В POSIX также входит функция lockf(3). В Linux она является оберткой над POSIX record locks.

Эти блокировки ассоциируются с парой [i-node, pid]:
  • любые (даже несвязанные) файловые дескрипторы, отктрытые для одного и того же файла одним и тем же процессом, ссылаются на одну и ту же блокировку;
  • как следствие, все потоки одного процесса также разделяют одну и ту же блокировку для каждого файла, т.к. имеют одинаковый pid;
  • вызов close() для любого из открытых для файла дескрипторов снимает блокировку с файла, не зависимо от того, через какой дескриптор она была установленна и остаются ли у процесса другие открытые дескрипторы для этого файла.
Эта особенность затрудняет использование record locks при написании библиотек (т.е. при отсутсвии контроля над всем приложением) и в многопоточной среде.

Если необходима гарантия атомарного переключения блокировки между режимами exclusive и shared, есть только два варианта:
  • использовать open file description locks, которые доступны в современных ядрах Linux;
  • использовать record locks и избежать открытия процессом одного и того же файла блокировки через несколько файловых дескрипторов.

Если требуется атомарное переключение режима блокировки, нет возможности использовать open file description locks, и блокировки нужны для синхронизации не только процессов, но и потоков, можно сделать следующее:
  • централизованно хранить все дескрипторы файлов блокировок, отрытые процессом, и не допускать открытия нескольких дескрипторов для одного файла;
  • ассоциировать с каждым дескриптором rw-mutex, например pthread_rwlock, и счетчик количества потоков, захвативших блокировку одновременно (он будет больше единицы только при захвате в shared mode);
  • для захвата блокировки сначала захватывать мьютекс и увеличивать счетчик; и только если счетчик был нулевой, захватывать и файловую блокировку;
  • для освобождения блокировки уменьшать счетчик и только если он стал нулевым, освобождать и файловую блокировку; затем освобождать мьютекс.

Open file description locks (fcntl)

Этот вид блокировок сочетает в себе достоинства BSD locks и record locks. (см. секцию "Open file description locks (non-POSIX)" в fcntl(2)). Особенности:
  • linux-specific; доступны начиная с ядра 3.15;
  • устанавливают блокировку на заданный интервал байт в файле;
  • привязаны к файловому дескриптору;
  • обеспечивают атомарное изменение режима блокировки.
Другими словами, эти блокировки сочетают в себе достоинства BSD locks (привязка к файловому дескриптору) и POSIX record locks (блокировка интервала, атомарное переключение режима).

Mandatory locking


В Linux также есть ограниченная поддержака обязательных блокировок:
  • обязательные блокировки задействуются только если они включены при монтировании раздела и для блокировки используются POSIX record locks;
  • после захвата exclusive или shared блокировки любые системные вызовы, изменяющие файл (например write(), truncate()) будут блокированы до тех пор, пока блокировка не будет отпущена;
  • после захвата exclusive блокировки, также буду блокироваться любые системные вызовы, читающие из файла.
Однако, реализация в Linux считается ненадежной, см. сецию "Mandatory locking" в fcntl(2):
  • возможны гонки при одновременном захвате блокировки и параллельном вызове write() или read();
  • возможны гонки при одновременном использовании с mmap().
Также, обязательные блокировки не решают проблемы, описанные в секции "Удаление и переименование" ниже.

Замечания

 

Обработка падений

Приятной особенностью всех видов блокировок (и flock() и fcntl()) является то, что блокировка автоматически снимается при завершении процесса, в том числе аварийном.

Другими словами, наличие блокировки гарантирует, что существует процесс, удерживающий ее.

Удаление и переименование

Ни рекоммендательные, ни обязательные блокировки не учитываются, когда выполняются вызовы unlink() или rename().

Также не получится устанавливать блокировку на каталог, из которого удаляется или перемещается файл, потому что блокировки можно устанавливать только на файлы.

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

Производительность

Если имеет значение производительность, вместо файловых блокировок можно использовать POSIX semaphores. В glibc они реализованы поверх futex-ов (однако имеют оверхед в одну страницу памяти на семафор).

Ссылки