Библиотека сайта rus-linux.net
Audacity
Глава 2 из книги "Архитектура приложений с открытым исходным кодом", том 1.
Оригинал: Audacity,
глава из книги "The Architecture of Open Source Applications" том 1.
Автор: James Crook
Перевод: Н.Ромоданов
2.5. Библиотека PortAudio: запись и воспроизведение
PortAudio является аудио-библиотекой, которая дает Audacity возможность воспроизводить и записывать звук независимо от используемой платформы. Без нее Audacity не сможет использовать звуковую карту устройства, на котором оно работает. В PortAudio предоставляются кольцевые буферы, средства, позволяющие изменять частоту дискретизации при воспроизведении/записи, и, самое главное, предоставляется интерфейс API, который скрывает различия между аудиообработкой на платформах Mac, Linux и Windows. В PortAudio есть файлы альтернативных реализаций для поддержки этого интерфейса API для каждой из платформ.
Мне никогда не нужно было копаться в PortAudio для того, чтобы разобраться, что там внутри происходит. Однако, полезно знать, как происходит взаимодействие с PortAudio. Audacity принимает пакеты данных от PortAudio (запись) и посылает их в PortAudio (воспроизведение). Стоит взглянуть на то, как именно происходит отправка и получение пакетов и как это согласуется с чтением и записью на диск и с обновлением экрана.
В одно и то же время происходят несколько различных процессов. Некоторые происходят часто, передавая небольшие объемы данных, и реакция на них должна быть быстрой. Другие происходят реже, передавая большие объемы данных, и точное время, когда это происходит, менее критично. В результате между процессами возникает рассогласование и для его выравнивания используются буферы. Вторую часть картины составляет то, что мы имеем дело с аудио устройствами, жесткими дисками и экраном. Мы не идем глубже и должны работать с тем интефейсом API, который нам предоставлен. Хотя для нас было бы лучше, чтобы каждый из наших процессов выглядел одинакового, например, чтобы каждый из них работал через wxThread, но у нас нет такой возможности (рис. 2.4).
Рис.2.4: Потоки и буферы, используемые при воспроизведении и записи
Один аудио поток запускается кодом PortAudio и непосредственно взаимодействует с аудиоустройством. Это тот поток, который управляет записью и воспроизведением. Этот поток должен реагировать быстро, иначе пакеты будут потеряны. Поток, находящийся под управлением кода PortAudio, называется audacityAudioCallback
, который, когда происходит запись, добавляет вновь поступившие небольшие пакеты к большему (в пять секунд) буферу захвата. При воспроизведении он берет небольшие кусочки данных из буфера воспроизведения, размер которого равен пяти секундам. Библиотека PortAudio ничего не знает о wxWidgets и поэтому этот поток, созданный PortAudio, является потоком pthread.
Второй поток запускается код в классе AudioIO в Audacity. При записи, AudioIO берет данные из буфера захвата и добавляет их в дорожки Audacity, которые, в конечном счете, будут отображаться на экране. Кроме того, когда будет добавлено достаточное количество данных, AudioIO записывает данные на диск. Этот же поток при воспроизведении аудиозаписей также выполняет операции чтения с диска. Здесь функция AudioIO::FillBuffers
является ключевой функцией и, в зависимости от настроек некоторых логических переменных, обрабатывает как запись, так и воспроизведение. Важно, чтобы обоих направлениях работала одна функция. Когда происходит «программное воспроизведение», при котором вы накладываете одну запись на другую, которая была записана ранее, одновременно используются части - часть записи и часть воспроизведения, Поток AudioIO мы полностью отдали на откуп операциям ввода/вывода на диск, которые выполняются операционной системой. Во время чтения или записи на диск мы можем останавливаться на неопределенный промежуток времени. Мы не могли вставить эти операции чтения или записи в функцию audacityAudioCallback
, поскольку она должна реагировать достаточно быстро.
Связь между этими двумя потоками происходит через общие переменные. Поскольку мы контролируем, какие потоки осуществляют записи в эти переменные, нам не требуются более дорогостоящие механизмы типа mutexe (механизм управления взаимодействующими процессами).
Как в случае воспроизведения, так и в случае записи, имеется дополнительное требование: Audacity также должна обновлять графический пользовательский интерфейс. Это наименее критичная по времени операция. Обновление происходит в основном потоке графического пользовательского интерфейса и выполняется по таймеру, который тикает двадцать раз в секунду. Этот тик таймера вызывает TrackPanel::OnTimer
, и, если обнаружено, что необходимо обновление графического интерфейса, то эти обновления выполняются. Такой поток, обслуживающий графический пользовательский интерфейс, создается в wxWidgets, а не нашем собственном коде. Особенность его в том, что другие потоки не могут напрямую обновлять графический пользовательский интерфейс. Использование таймера для доступа к графическому пользовательскому интерфейсу для проверки того, нужно ли обновление экрана, позволяет сократить количество перерисовок до уровня, который приемлем для быстро работающих дисплеев и не требует от процессора слишком больших затрат на выдачу изображения.
Хорошим ли проектным решением является использование потока аудиоустройства, потока буфера/диска и потока графического пользовательского интерфейса с таймером, по которому происходят все эти перенаправления аудиоданных? Это специальное решение, представляющее собой три различных потока, которые не заданы в одном абстрактном базовом классе. Впрочем, такая специфика в значительной степени продиктована используемым нами библиотеками. Предполагается, что PortAudio создает собственный поток. Во фреймворке wxWidgets автоматически создается поток графического пользовательского интерфейса. Наша потребность в использовании потока заполнения буфера обусловлена тем, что нам нужно скорректировать несогласованность между достаточно частыми небольшими пакетами потока аудиоустройства и менее частыми, но большими пакетами дискового устройства. То, что мы используем эти библиотеки, дает нам определенные преимущества. Плата за использование библиотек состоит в том, что мы, в конечном итоге, пользуемся только теми абстракции, которые они нам предлагают. В результате мы копируем данные из одного места в памяти в другое в гораздо большем объеме, чем это необходимо. В быстрых коммутаторах данных, с которыми я работал, я видел чрезвычайно эффективный код для обработки рассогласования подобного рода, в которых использовались прерывания и, вообще, не использовались потоки. Повсюду передавались указатели на буферы, а не копировались данные. Вы можете применять этот подход только в том случае, если библиотеки, которыми вы пользуетесь, разработаны так, что позволяют работать с абстракцией буфера, обладающей большими возможностями. Воспользовавшись существующими интерфейсами, мы вынуждены пользоваться потоками и вынуждены копировать данные.
Продолжение статьи: Блочные файлы BlockFile.