&arts; в деталях
Архитектура
Структура &arts;.
Модули и порты
Основная идея &arts; - это синтез звука при помощи небольших модулей, которые предназначены только для этой цели, и комбинирования из них различных структур. Модули обычно имеют входы, на которые поступают различные сигналы или параметры, и выходы, на которых вырабатываются некоторые сигналы.
В случае одного модуля (Synth_ADD) берутся два сигнала с входа и складываются вместе. Результатом является сигнал на выходе. Места, где модуль предоставляет свои входные/выходные сигналы, называются портами.
Структуры
Структура - это набор связанных модулей, некоторые из которых могут иметь параметры, кодируемые непосредственно в их входные порты, другие могут быть связаны с остальными или вообще ни с чем не связаны.
С помощью &arts-builder; вы можете описать структуры. Вы описываете, какие модули вы хотели бы соединить. Завершив описание, вы можете сохранить полученную структуру в файле или создать описанную структуру, запустив Execute.
Вы услышите звук, если всё сделали правильно.
Задержки
Что такое задержки?
Предположим, у вас есть программа mousepling
, которая издает звук, если вы нажимаете на кнопку. Задержка - это время между нажатием кнопки и тем, когда вы слышите звук. Задержка в данном случае составляется из других задержек, имеющих различные причины.
Задержка в простых программах
В этой простой программе задержки происходят в следующих местах:
Время, пока ядро операционной системы не оповестит сервер X11, что кнопка мыши была нажата.
Время, пока сервер X11 не оповестит вашу программу, что кнопка мыши была нажата.
Время, пока ваша программа не решит, что нужно проиграть звук.
Время указания программой звуковому серверу, что сервер должен проиграть звук.
Время прохода звукового фрагмента через буфер данных (когда звуковой сервер начинает микшировать другие потоки в один), пока он действительно не достигнет места для проигрывания звуковой картой.
Время прохождения звука от колонок до вашего уха.
Первые три причины задержки не относятся к &arts;. Они интересны, но выходят за область этого документа. Тем не менее нужно осознавать, что они существуют, так что даже если вы всё оптимизировали до действительно низких значений, вы можете не получить в точности расчетный результат.
Указание серверу проиграть что-нибудь обычно включает один вызов &MCOP;. Имеются тесты, которые подтверждают, что на одном и том же компьютере с unix-сокетами проигрывание сервером чего-нибудь возможно около 9000 раз в секунду в текущей реализации. Я предполагаю, что большая часть этой задержки создается ядром ОС при переключении между процессами. Конечно, это значение зависит от типа параметров. Если вы передаёте целый образ за один вызов, то это будет медленнее, чем если вы передадите только одно длинное значение. Это справедливо и для кода возврата. Конечно, для обычных строк (таких как имя воспроизводимого wav файла) это не должно быть проблемой.
Мы можем оценить это время как 1/9000 с, что меньше 0,15 мс. Мы увидим, что это не так важно.
Следующая задержка - это время между началом проигрывания сервером и временем прихода фрагмента на звуковую плату. Серверу необходима буферизация, так что когда запущены другие программы, как ваш сервер X11 или программа mousepling
, звук не прерывается. Под &Linux; создаются фрагменты определенного размера. Сервер постоянно заполняет фрагменты и отдаёт их звуковой плате для воспроизведения.
Предположим, есть три фрагмента. Сервер заполняет первый, звуковая плата начинает проигрывать его. Сервер заполняет второй. Сервер заполняет третий. Сервер завершает работу, другие программы могут начинать что-нибудь делать.
Как только звуковая плата проиграет первый фрагмент, она начинает проигрывать второй, и сервер начинает заполнять первый фрагмент. И так далее.
Максимальная задержка при этом равна (число фрагментов)*(размер каждого фрагмента)/((частота дискретизации)*(размер одного отсчета)). С параметрами 44 Кгц стерео и 7 фрагментов размером по 1024 байта (настройки aRts по умолчанию), мы получим 40 мс.
Данное значение может быть настроено соответственно вашим нуждам. Конечно, загрузка CPU увеличивается с уменьшением задержки, так как звуковому серверу нужно заполнять буфера более часто, и в меньших размерах. Почти невозможно получить хорошие значения без предоставления звуковому серверу приоритета реального времени, иначе вы часто будете слышать прерывание звучания.
Конечно, вполне реально сделать что-нибудь вроде 3-х фрагментов, каждый размером 256 байтов, которые дали бы задержку в 4.4 мс. С 4.4 мс задержки и загрузка CPU &arts; будет около 7.5%. С 40 мс задержкой она составит около 3% (на PII-350, и данное значение может зависеть от вашей звуковой карты, версии ядра и т.д.).
Затем есть время, необходимое для прохода звука от колонок до ваших ушей. Предполагается что от вас до колонок 2 метра. Звук перемещается со скоростью 330 метров в секунду. Мы можем округлить данное время до 6 мс.
Задержка в потоковых программах
Потоковые программы производят звук самостоятельно. Рассмотрим игру, которая создает постоянный поток отсчетов, и должна быть адаптирована для &arts;. Например, когда я нажимаю кнопку, фигура, которой я играю, прыгает, и раздаётся звук.
Сначала вам нужно знать, как &arts; работает с потоковым звуком. Это очень похоже на ввод/вывод со звуковой платой. Игра посылает некоторые пакеты с отсчетами звуковому серверу. Например, три пакета. Как только звуковой сервер закончит с первым пакетом, он посылает подтверждение игре, что этот пакет обработан.
Игра создаёт другой звуковой пакет и посылает его серверу. Тем временем сервер начинает поглощать второй пакет, и так далее. Задержка здесь схожа с простым случаем:
Время, пока ядро не оповестит сервер X11, что клавиша была нажата.
Время, пока сервер X11 не оповестит игру, что кнопка была нажата.
Время, пока игра не определит, что по этой кнопке стоит проиграть звук.
Время, пока звуковой пакет, который игра начала передавать серверу, не достигнет звукового сервера.
Время, необходимое для прохождения звука, который звуковой сервер смешивает с другим выводимым звуком в один, через буфер данных, пока тот действительно не достигнет места, где звуковая плата проигрывает звук.
Время, нужное для достижения звука из колонок вашего уха.
Внешние задержки, как и ранее, вне рамок этого документа.
Очевидно, что задержка потокового звука зависит от времени, необходимого для воспроизведения всех пакетов потока. Итак, это (число пакетов)*(размер пакета)/((частота образцов)*(размер отсчета)).
Как вы видите, эта же формула применяется для фрагментов. Конечно, для игр это несущественно из-за такой же небольшой задержки, как и раньше. Я бы посоветовал использовать конфигурацию для игр 2048 байт на пакет и использование 3 пакетов. Результирующая задержка составит 35 мс.
Это основано на следующей оценке. Допустим, игра идёт с частотой 25 кадров в секунду. Возможно, вы не заметите различий вывода звука в пределах одного кадра. Итак, задержка в 1/25 с при потоковом воспроизведении приемлема, что составляет 40 мс.
Большинство не запускает игры с приоритетом реального времени, и опасностью перерывов в звуке нельзя пренебрегать. Потоковый звук с 3 пакетами по 256 возможен - я проверял - но сильно загружает процессор.
Задержки сервера вы можете расчитать точно также.
О загрузке процессора
Имеется масса факторов, которые влияют на загрузку процессора в сложных случаях, с потоковыми и прочими приложениями и модулями сервера. Назовём несколько:
Загрузка процессора необходимыми вычислениями.
Накладные расходы внутреннего планирования &arts; - &arts; решает когда,какой модуль и что должен вычислить.
Накладные расходы преобразования целых чисел в числа с плавающей запятой.
Накладные расходы протокола &MCOP;.
Ядро: переключение процессов/контекстов
Ядро: накладные расходы связи
Если запущены два потока, то их загрузка процессора суммируется. Если вы применяете фильтр, то требуются некоторые вычисления. Упрощённо, сложение двух потоков может использовать четыре цикла CPU на сложение, для 350Мгц процессора это 44100*2*4/350000000 = 0.1% использования CPU.
Внутреннее планирование &arts;: &arts; должен решить, какой модуль, когда и что вычисляет. Это требует времени. Вам нужен профилировщик, если вы заинтересованы в этом. Чем меньше требуется обработки в реальном времени, (например, за счет обработки больших буферов), тем меньше накладные расходы планирования. Если обрабатывать блоки по 128 отсчетов (с использованием фрагментов размером по 512 байт), то на накладные расходы планирования можно не обращать внимания.
Накладные расходы преобразования целых чисел в числа с плавающей точкой: внутри &arts; используются данные в формате с плавающей точкой. Это упрощает обработку и на последних процессорах не медленнее, чем целочисленные операции. Конечно, если клиент использует целочисленные данные, (подобно игре, которая выводит звук через &arts;), то требуется их преобразовать. Также и звуковая карта принимает целочисленные данные, так что их нужно преобразовывать.
Вот данные для процессора Celeron, такты процессора на обработку одного отсчета, с опцией -O2 egcs 2.91.66 (получены Евгением Смитом hamster@null.ru). Конечно, это сильно зависит от процессора:
convert_mono_8_float: 14
convert_stereo_i8_2float: 28
convert_mono_16le_float: 40
interpolate_mono_16le_float: 200
convert_stereo_i16le_2float: 80
convert_mono_float_16le: 80
Итак загрузка составляет в среднем 1% для преобразования и 5% для интерполяции на 350 МГц процессоре.
Накладные расходы &MCOP; протокола, по приблизительным подсчётам, 9000 обращений в секунду. Многое из этого не является недостатком &MCOP;, но относится к двум моментам из ядра операционной системы, описанным ниже.Это даёт основу для вычислений стоимости затрат потоковой обработки.
Каждый пакет данных, передаваемый через потоки, может рассматриваться как одно &MCOP; обращение. Конечно же, большие пакеты медленнее, чем 9000 пакетов/с, но это только прикидки.
Предполагается, что вы используете пакеты размером 1024 байт. Таким образом, для передачи потока с 44 КГц стерео вам необходимо передать 44100*4/1024 = 172 пакета в секунду. Предполагается, что вы можете при 100% загрузке процессора передать 9000 пакетов, тогда получите (172*100)/9000 = 2% использования CPU для потока с пакетами в 1024 байт.
Это округлённые расчеты. Однако, они показывают, что намного лучше было бы, если время задержки вас устраивает, использовать к примеру пакеты по 4096 байт. Тогда мы имеем формулу вычисления размера пакета, который приводит к 100% загрузке CPU, 44100*4/9000 = 19.6 отсчета, и таким образом получаем формулу для оценок:
использование CPU потоком в процентах = 1960/(ваш размер пакета)
которая даёт нам 0.5% использования CPU потоками с размером пакета в 4096 байт.
Переключение ядром процессов/контекстов: это часть накладных расходов &MCOP; протокола. Переключение между двумя процессами требует времени - отображение новой памяти, непопадание в кэш, или что-то ещё (если это читает эксперт по ядру - дайте мне знать, что точно является причиной). Это забирает время.
Я не знаю, как с какой частотой &Linux; может переключать контексты, но это число не бесконечно. Таким образом, накладные расходы &MCOP; протокола, как я полагаю, в немалой степени вызваны переключением контекстов. В начале разработки &MCOP; я протестировал использование такого взаимодействия внутри одного процесса, и это было намного быстрее (в четыре раза быстрее или около того).
Ядро: накладные расходы взаимодействия: Это часть накладных расходов протокола &MCOP;. Передача данных между процессами сейчас сделана через сокеты. Это удобно, так как для определения прихода данных могут быть использованы обычные методы select(). И это может также сочетаться с другими источниками ввода-вывода, такими как звуковой ввод-вывод, сервер X11 или любыми другими.
Тем не менее, вызовы чтения и записи стоят процессорных тактов. Для небольших обращений (таких как передача одного midi события) это и не так плохо, но для больших обращений (как передача одного видеокадра размером в несколько мегабайт) это уже точно проблема.
Использование общей разделяемой памяти для &MCOP; там, где это возможно, было бы лучшим решением. Однако это должно быть сделано прозрачно для программиста приложения.
Возьмите профилировщик или сделайте другие тесты для выяснения, в какой степени на обработку текущего аудио потока влияет неиспользование общей памяти. Совсем не плохо, когда обработка аудио потока (проигрывание mp3) с помощью &artsd; и artscat с загружает процессор на 6% (на 5% с помощью только mp3-декодера). Конечно, это включает всё от выполнения необходимых вычислений до издержек работы с сокетами, таким образом я бы сказал, что возможно сэкономить 1%, используя общую память.
Некоторые сложные случаи
Это сделано в текущем разрабатываемом варианте. Я также хотел проверить по-настоящему сложные случаи, и это не то, что должны использовать обычные приложения.
Я написал приложение, названное streamsound, которое посылает потоковые данные &arts;. Оно работает с приоритетом реального времени (без проблем), и с одним небольшим модулем сервера (масштабирование и ограничение громкости):
4974 stefan 20 0 2360 2360 1784 S 0 17.7 1.8 0:21 artsd
5016 stefan 20 0 2208 2208 1684 S 0 7.2 1.7 0:02 streamsound
5002 stefan 20 0 2208 2208 1684 S 0 6.8 1.7 0:07 streamsound
4997 stefan 20 0 2208 2208 1684 S 0 6.6 1.7 0:07 streamsound
Каждый из этих процессов - обработка потока с 3 фрагментами по 1024 байта (18мс). Имеется три таких клиента, работающих одновременно. Я знаю, что это выглядит немного излишне, но как я сказал: возьмите профилировщик и выясните, на что тратится время, и если вы сочтёте необходимым, то исправьте.
Конечно, я не думаю, что в реальной жизни понадобится такая потоковая обработка. Для тестов я пробовал то, что уменьшаеит задержку до минимума. Результат: вы можете обрабатывать потоки без прерываний с одним клиентским приложением, если вы взяли 2 фрагмента по 128 байт между aRts и звуковой картой, и между клиентским приложением и aRts. Это означает, что вы имеете общую максимальную задержку в 128*4/44100*4 = 3 мс, где 1.5 мс происходят из-за ввода/вывода звуковой карты и 1.5 мс из-за взаимодействия с &arts;. Оба приложения нужно запускать с приоритетом реального времени.
Конечно, это сильно загружает процессор. Данный пример загружает мой P-II/350 приблизительно на 45%. Также будут слышны щелчки, если вы запустите top, начнёте перемещать окна на вашем дисплее X11 или выполнять операции дискового ввода/вывода. Всё это - проблемы ядра. Проблема планирования двух или большего числа приложений с приоритетом реального времени очень сложна, и ещё сложнее, если они взаимодействуют, оповещая друг друга и т. д.
Наконец, более жизненный пример. Это &arts; с artsd и одним artscat (один клиент обработки потока) запустивший 16 фрагментов по 4096 байт:
5548 stefan 12 0 2364 2364 1752 R 0 4.9 1.8 0:03 artsd
5554 stefan 3 0 752 752 572 R 0 0.7 0.5 0:00 top
5550 stefan 2 0 2280 2280 1696 S 0 0.5 1.7 0:00 artscat
Шины
Шины - это динамически созданные соединения, которые передают аудио. В основном, это некоторые входные и выходные связи. Все сигналы из входных связей складываются и посылаются на выходные связи.
Шины сейчас реализованы для операций со стерео, итак вы можете передавать через шины только стерео данные. Если вы хотите моно, хорошо, передавайте их только по одному каналу и установите другой в ноль или что-нибудь иное. Что же вам нужно сделать для этого, создать один или несколько Synth_BUS_UPLINK объектов и указать им имя шины, по которой они должны общаться ( например аудио
или барабаны
. Просто направляйте данные туда.
Затем, вам нужно создать один или несколько Synth_BUS_DOWNLINK объектов выходных связей, и указать им имя шины (аудио
или барабаны
... если это подходит для передачи данных через них), и обработанные данные будут на выходе.
Входные и выходные связи могут находиться внутри различных структур, вы можете даже иметь несколько запущенных копий &arts-builder; и начать передавать в одной и принимать данные в другой.
Шины хороши тем, что они полностью динамические. Клиенты могут подключаться и отключаться во время работы, не будет прерываний потока или шума.
Конечно, вы не должны отключать клиента, проигрывающего сигнал, после отключения от шины возможно сигнал не будет нулевым, и тогда поток прервётся.
Трейдинг
&arts;/&MCOP; основан на распределении задач по небольшим компонентам. Благодаря этому система становится более гибкой, тогда можно расширять систему, просто добавляя новые компоненты, реализующие новые эффекты, форматы файлов, генераторы, элементы пользовательского интерфейса, ... Поскольку почти все элементы - это компоненты, всё можно расширять очень просто, без изменения существующих исходников. Новые компоненты могут быть просто динамически загружены для улучшения уже существующих приложений.
Для этого необходимы две вещи:
Компоненты должны извещать о себе сами - они должны описывать собственную функциональность так, чтобы другие приложения могли бы использовать их.
Приложения должны активно просматривать наличие компонентов, которые они могут использовать, вместо использования одних и тех же способов выполнения задач.
Обобщенно: компоненты, которые говорят: здесь я, я то что нужно, используй меня
, и приложения (или если вам нужно, другие компоненты), которые выходят и смотрят, какой компонент они могут использовать для выполнения задачи. Это называется трейдинг.
В &arts;, компоненты описывают сами себя, указывая значения, которые они поддерживают
как свойства. Типичным свойством для компонента загрузки файлов может быть расширение обрабатываемых файлов. Типичные значения могут быть wav, aiff или mp3.
Фактически каждый компонент может предлагать несколько различных значений для одного свойства. Так, один компонент может предлагать прочитать оба, wav и aiff файла, указав что поддерживает эти значения для свойства Расширение
.
Для этого компонент должен поместить в соответствующее место файл .mcopclass, содержащий поддерживаемые свойства. Для нашего примера это может выглядеть подобно так (и установлено в componentdir/Arts/WavPlayObject.mcopclass):
Interface=Arts::WavPlayObject,Arts::PlayObject,Arts::SynthModule,Arts::Object
Author="Stefan Westerfeld <stefan@space.twc.de>"
URL="http://www.arts-project.org"
Extension=wav,aiff
MimeType=audio/x-wav,audio/x-aiff
Важно, чтобы имя .mcopclass-файла также было содержательным. Трейдер не просматривает содержание всего файла, если файл (как здесь) назван Arts/WavPlayObject.mcopclass, а интерфейс компонента назван Arts::WavPlayObject (модули отображаются на каталоги).
Для просмотра компонентов имеется два интерфейса (которые определены в core.idl, так что вы имеете их в каждом приложении), названные Arts::TraderQuery и Arts::TraderOffer. Вы ищете нужные компоненты следующим образом:
Создаёте объект запроса:
Arts::TraderQuery запрос;
Укажите, что вам нужно. Как вы видели выше, компоненты описывают сами себя, используя свойства, для которых они устанавливают определённые значения. Так что можно указать, что вам нужно, выбрав компоненты, которые поддерживают определённые значения для свойств. Это делается с помощью методов TraderQuery:
query.supports("Interface","Arts::PlayObject");
query.supports("Extension","wav");
Наконец, делаем запрос, используя метод запроса. Затем (как надеемся) получаем некоторые предложения:
vector<Arts::TraderOffer> *offers = query.query();
Сейчас вы можете проверить то, что нашли. Важен метод interfaceName интерфейса TraderOffer, который вернёт вам имя компонента, подходящего запросу. Вы также можете узнать дополнительные свойства, используя getProperty. Следующий код просто проходит по всем компонентам, выводит их имена интерфейсов (которые могли быть использованы для создания) и удаляет результаты запроса:
vector<Arts::TraderOffer>::iterator i;
for(i = offers->begin(); i != offers->end(); i++)
cout << i->interfaceName() << endl;
delete offers;
Чтобы служба трейдинга была полезна, важно прийти к согласию, каковы могут быть свойства компонентов. Существенно, чтобы практически все компоненты в определённой области использовали одинаковый набор свойств для описания самих себя (и по возможности одинаковый набор значений), так что приложения или другие компоненты могли бы найти их.
Author (строковый тип, необязательный): Это может быть использовано для того, чтобы показать миру, что вы это написали. Вы можете написать здесь всё, что угодно, конечно желателен адрес электронной почты.
Buildable (булевый тип, рекомендуется): говорит о возможности использования компонента инструментами RAD (такими как &arts-builder;), которые используют компоненты, устанавливая соответствие свойствам и соединяя порты. Рекомендуется устанавливать данное значение в истинное для большинства компонент обработки сигналов (таких как фильтры, эффекты, осцилляторы и т. д.), и для всех других, которые могут быть использованы в RAD, но не для внутренней работы как, например, Arts::InterfaceRepo.
Extension (строковый тип, используется, когда это уместно): вcё, что связано с файлами, должно задавать это. Вы можете указать расширение файла в нижнем регистре без .
, поэтому что-то типа wav подходит.
Interface (строковый тип, необходим): должно включать полный список поддерживаемых (полезных) интерфейсов вашими компонентами, возможно включая Arts::Object и, если это возможно, Arts::SynthModule.
Language (строковый тип, рекомендуется): если вы хотите загружать компонент динамически, то нужно указать здесь язык написания компонента. Сейчас допускается только значение C++, которое означает, что компонент был написан с использованием нормального API C++. Если вы сделали так, то вам ещё нужно установить свойство Library
, описанное ниже.
Library (строковый тип, используется, когда это уместно): компоненты, написанные на C++, могут быть динамически загружены. Для этого вам нужно скомпилировать их в динамически загружаемые .la модули, используя libtool. Здесь вы можете указать имя .la-файла, который содержит ваш компонент. Не забудьте использовать REGISTER_IMPLEMENTATION (всегда).
MimeType (строковый тип, используется, когда это уместно): вcё, что связано с файлами, должно задавать это. Здесь вы можете установить стандартный mimetype в нижнем регистре, для примера audio/x-wav.
&URL; (строковый тип, используется по усмотрению автора): если вы хотите указать людям, где они могут найти последнюю версию компонента (или домашнюю страницу или что-нибудь другое), вы можете сделать это. Это может быть стандартным адресом &HTTP; или &FTP;.
Пространства имен в &arts;
Введение
Каждое пространство имен соответствует модулю
, объявленному в &IDL; &MCOP;.
// mcop idl
module M {
interface A
{
}
};
interface B;
В этом случае, созданный С++ код для &IDL; выглядел бы так:
// C++ header
namespace M {
/* объявление A_base/A_skel/A_stub и подобных */
class A { // описание класса
/* [...] */
};
}
/* объявление B_base/B_skel/B_stub и подобных */
class B {
/* [...] */
};
Итак, когда вы ссылаетесь на классы из примера выше в вашем С++ коде, вы должны написать M::A, но просто B. Конечно, вы можете использовать using M
где-нибудь - как с любыми другими пространствами имён в С++.
Как &arts; использует пространства имён
Есть одно глобальное пространство имён, названное Arts
, которое используют все программы и библиотеки, относящиеся к &arts;, для объявления своих интерфейсов. Это означает, что когда пишется С++ код, который зависит от &arts;, вы обычно должны добавлять префикс Arts:: для каждого используемого класса, как в данном примере:
int main(int argc, char **argv)
{
Arts::Dispatcher dispatcher;
Arts::SimpleSoundServer server(Arts::Reference("global:Arts_SimpleSoundServer"));
server.play("/var/foo/somefile.wav");
Другая альтернатива - описать использование пространства имён Arts
только один раз, подобно:
using namespace Arts;
int main(int argc, char **argv)
{
Dispatcher dispatcher;
SimpleSoundServer server(Reference("global:Arts_SimpleSoundServer"));
server.play("/var/foo/somefile.wav");
[...]
В &IDL; файлах у вас нет выбора. Если вы пишете код, который относится к &arts;, то вы должны заключить его в конструкцию module &arts;.
// IDL файл для aRts:
#include <artsflow.idl>
module Arts { // поместить это в пространство имён Arts
interface Synth_TWEAK : SynthModule
{
in audio stream invalue;
out audio stream outvalue;
attribute float tweakFactor;
};
};
Если вы пишете код, который не относится к &arts;, то не обязаны помещать его в пространство имён Arts
. Конечно, вы можете сделать собственное пространство имён, если вам это нужно. В любом случае, вы должны устанавливать соответствующий префикс для классов из &arts;.
// IDL файл для кода, не относящегося к &arts;:
#include <artsflow.idl>
// написав без объявления модуля указываем, что созданные классы не используют
// пространства имён:
interface Synth_TWEAK2 : Arts::SynthModule
{
in audio stream invalue;
out audio stream outvalue;
attribute float tweakFactor;
};
// конечно, вы можете ещё выбрать собственное пространство имён, если вам нужно,
// так что, если вы пишете приложение "PowerRadio", вы могли, например, сделать так:
module PowerRadio {
struct Station {
string name;
float frequency;
};
interface Tuner : Arts::SynthModule {
attribute Station station; // нет необходимости в префиксе Station, так как модуль тот же
out audio stream left, right;
};
};
Подробности: как работает реализация класса
Часто, в интерфейсах, преобразованиях, методах и пр. &MCOP; нуждается в описании имён типов или интерфейсов. Они представляются в виде строк в общей &MCOP; структуре данных, в то время как пространство имён написано на С++. Это означает, что строки содержат M::A
и B
, из примера выше.
Заметим, что это применяется, даже если внутри текста &IDL; не было квалификаторов пространства имён, поскольку из контекста ясно, в каком пространстве имён используется интерфейс A.
Многопоточность в &arts;
Основы
Многопоточность доступна не на всех платформах. Вот почему &arts; был изначально написан без использования потоков. Для всех задач может быть написано как многопоточное, так и однопоточное решение, которое делает то же самое.
Так, вместо аудиовывода в отдельном блокирующем потоке &arts; использует аудиовывод без блокировок, и узнаёт, когда нужно записывать следующий фрагмент данных с помощью функции select().
Конечно, &arts; (в последних версиях) предоставляет поддержку для использования в объектах многопоточности. К примеру, если вы уже имеете код для проигрывателя mp3, и код предполагает запуск декодера mp3 в отдельном потоке, то это проще всего сделать с использованием многопоточности.
Реализация &arts;/&MCOP; опирается на разделение состояний между различными объектами в явном и неявном виде. Список объектов в разделяемой области включает в себя:
Объект диспетчера, который осуществляет взаимодействие &MCOP;.
Подсчёт ссылок, связей (Smartwrappers).
Диспетчер ввода-вывода, который реализует таймер и следит за дескрипторами файлов.
Менеджер объектов, который создаёт объекты и динамично загружает модули.
Объект контроля потока, который вызывает calculateBlock в подходящих ситуациях.
Ни один из предыдущих объектов не предполагает одновременного использования (например, вызова из различных потоков одновременно). В общем, имеются два подхода к разрешению этой проблемы:
для вызова функций этих объектов требуется создать блокировку перед использованием.
написание кода этих объектов действительно безопасным с точки зрения многопоточности и/или создание для каждой потока своей копии.
&arts; следует первому пути: вам нужна блокировка всякий раз, когда вы используете любой из этих объектов. Второй путь сложнее. Попытка реализовать его доступна на момент написания по ссылке http://space.twc.de/~stefan/kde/download/arts-mt.tar.gz. На настоящий момент простой подход, возможно, работает лучше и создаёт меньше проблем с существующими приложениями.
Когда и как задавать блокировку?
Можно установить или освободить блокировку двумя функциями:
Arts::Dispatcher::lock()
Arts::Dispatcher::unlock()
Обычно вам не нужна явная блокировка (и вы не должны пробовать делать это), если это уже сделано. Список условий, когда блокировка уже есть:
Вы получаете обратный вызов из диспетчера ввода-вывода (по таймеру или через дескриптор файлов).
Вы получаете вызов какого-то &MCOP; запроса.
Вы получаете вызов менеджера оповещения.
Вы получаете вызов системы контроля потока (calculateBlock)
Имеются также некоторые функции, которые вы можете вызывать только в основном потоке, и для них вам никогда не нужно получать блокировку:
Конструктор/деструктор диспетчера объектов или ввода-вывода.
Dispatcher::run() / IOManager::run()
IOManager::processOneEvent()
И всё. Для всего остального, что хоть как-то взаимодействует с &arts;, вам нужно получить блокировку и освободить её сразу после завершения. Всегда. Вот простой пример:
class SuspendTimeThread : Arts::Thread {
public:
void run() {
/*
* Вам нужна здесь блокировка, потому что:
* - создание ссылки нуждается в блокировке (глобальной: управление передаётся
* менеджеру объектов, который может нуждаться в объекте GlobalComm
* для поиска, куда нужно соединиться)
* - подключение smartwrapper нуждается в блокировке
* - создание объекта из ссылки нуждается в блокировке (потому что это
* может требоваться для подключения к серверу)
*/
Arts::Dispatcher::lock();
Arts::SoundServer server = Arts::Reference("global:Arts_SoundServer");
Arts::Dispatcher::unlock();
for(;;) { /*
* здесь вам нужна блокировка, так как
* - отключение smartwrapper нуждается в блокировке (может
* потребоваться создать что-то)
* - оповещения MCOP нуждаются в блокировке
*/
Arts::Dispatcher::lock();
long seconds = server.secondsUntilSuspend();
Arts::Dispatcher::unlock();
printf("seconds until suspend = %d",seconds);
sleep(1);
}
}
}
Классы для работы с многопоточностью
Доступны следующие классы:
Arts::Thread - интерфейс к потокам.
Arts::Mutex - интерфейс к мьютексам.
Arts::ThreadCondition - интерфейс к возобновлению условно приостановленных потоков.
Arts::SystemThreads - интерфейс к потокам самой операционной системы (несколько полезных функций для прикладных программистов).
См. ссылки на документацию.
Ссылки и обработка ошибок
Ссылки &MCOP; - одна из основных концепций в программировании &MCOP;. В этой части описано, как используются ссылки, и даётся попытка осветить случаи отказов (сбоев работы сервера).
Основные свойства ссылок
Ссылка &MCOP; не является объектом, но ссылается на объект: даже если следующее объявление,
Arts::Synth_PLAY p;
выглядит похожим на определение объекта, оно только объявляет ссылку на объект. Как С++ программист, вы можете воспринимать это как Synth_PLAY *, указатель на объект Synth_PLAY. Главным образом это означает, что p подобно указателю на NULL.
Вы можете создать ссылку на NULL явно.
Arts::Synth_PLAY p = Arts::Synth_PLAY::null();
Вызов функций с использованием ссылки на NULL приводит к сбою в core
Arts::Synth_PLAY p = Arts::Synth_PLAY::null();
string s = p.toString();
приводит к сбою. Сравним с указателями, по существу это похоже на
QWindow* w = 0;
w->show();
. Каждый программист С++ знает, как избегать таких ситуаций.
Неинициализированные объекты кое-как пытаются создать себя сами при первом использовании
Arts::Synth_PLAY p;
string s = p.toString();
это несколько отличается от разыменования указателя на NULL. Вы вообще не указали объекту, чем он является, и пытаетесь использовать его. Вообразим здесь, что вы хотели иметь новую локальную копию объекта Arts::Synth_PLAY. Конечно вы могли хотеть что-то ещё (вроде создания объекта где-то ещё или использования существующего внешнего объекта. Так или иначе, объект будет как-то создан, но созданный подобным образом объект не будет работать до тех пор пока вы не присвоите ему какое-то значение (также как и нулевая ссылка).
Эквивалент в терминах С++
QWidget* w;
w->show();
что в C++ безусловно приводит к ошибке обращения к памяти. Итак, есть отличия. Такое создание объекта может быть ошибочным потому, что необязательно существует реализация вашего интерфейса.
Для примера, рассмотрим абстрактную конструкцию, подобную Arts::PlayObject. Имеются вполне определённые объекты для воспроизведения mp3 или wav, но
Arts::PlayObject po;
po.play();
точно не будет работать. Проблема в том, что хотя код пытается как-то создать объект PlayObject, это не работает, так как имеются только реализации, подобные Arts::WavPlayObject. Так что создавать объекты вы можете только тогда, когда существует реализация.
Ссылки могут указывать на один и тот же объект
Arts::SimpleSoundServer s = Arts::Reference("global:Arts_SimpleSoundServer");
Arts::SimpleSoundServer s2 = s;
создаст две ссылки на один и тот же объект. Это не копирование значений и не создание двух объектов.
Все ссылки на объекты подсчитываются. Так что объект, на который нет ссылок, удаляется. Нет специальных методов для удаления объектов, однако, вы можете использовать что-то похожее на это
Arts::Synth_PLAY p;
p.start();
[...]
p = Arts::Synth_PLAY::null();
для удаления объекта Synth_PLAY в конце. В особенности, никогда не требуется использовать new и delete вместе со ссылками.
Случаи сбоев
Ссылки могут указывать на внешние объекты, а серверы, содержащие такие объекты, могут упасть. Что тогда происходит?
Сбой сервера не изменяет ссылку, если это нулевая ссылка. Это означает, что если foo.isNull() была true до падения сервера, тогда она также вернёт true и после падения сервера. Это также означает, что, если foo.isNull() была false до падения сервера (ссылка foo ссылалась на объект), тогда она также вернёт false и после падения сервера.
Вызовы методов действительной ссылки безопасны. Предположим, что сервер содержащий объект calc, упал. Подобный вызов
int k = calc.subtract(i,j)
безопасен. Явно subtract вернёт что-то неверное, потому что внешний объект уже не существует. В этом случае (k == 0) может быть верным. В основном, операции возвращают в качестве результата что-нибудь нейтральное
, как 0.0, нулевые ссылки на объекты или пустые строки, когда объект более не существует.
Существует функция error() для проверки того, как что-то отработало.
В предыдущем случае,
int k = calc.subtract(i,j)
if(k.error()) {
printf("k is not i-j!\n");
}
может вывести k is not i-j, если внешний вызов не работает. Иначе k будет действительным результатом выполнения операции subtract, выполненной внешним объектом (сервер не разрушен). Однако для методов, выполняющих операции подобные удалению файлов, вы не можете знать, что произошло в действительности. Конечно, это произошло, если .error() вернула false. Однако, если .error() вернула true, существуют две возможности:
Файл удалён и сервер упал сразу после удаления, но до передачи результата.
Сервер упал до удаления файла.
Использование вложенных вызовов опасно в отказоустойчивых программах
Использование кода
window.titlebar().setTitle("foo");
- плохая идея. Предположим, вы знаете, что ссылка на Window действительна. Предположим, вы вы знаете, что функция window.titlebar() возвратит ссылку на Titlebar, так как объект Window реализован верно. Однако этот код остаётся небезопасным.
Что может случится, когда сервер, содержащий объект Window, падает? Невзирая на то, как хорошо реализован объект Window, операция window.titlebar() вернёт вам нулевую ссылку. И затем вызов setTitle приводит к сбою.
Итак, безопасный вариант может быть таким:
Titlebar titlebar = window.titlebar();
if(!window.error())
titlebar.setTitle("foo");
. Добавьте подходящую обработку ошибок, если вам нужно. Если вы не доверяете реализации Window, используйте код
Titlebar titlebar = window.titlebar();
if(!titlebar.isNull())
titlebar.setTitle("foo");
, который безопаснее.
Имеются другие условия для сбоев, такие как нарушение сетевого соединения (предположим вы удалили кабель между сервером и клиентом, пока ваше приложение работает). Однако, такой эффект подобен сбою сервера.
В целом, это ваш подход к тому, насколько строго вы пробуете отследить ошибки взаимодействия внутри вашего приложения. Можно следовать методу если сервер падает, нужно отлаживать сервер, пока он совсем не перестанет падать
, который означает, что вам не нужно беспокоиться обо всех этих проблемах.
Подробности: Распределённый подсчёт ссылок
Объект может существовать только, когда им владеет кто-то. Если это не так, то он немедленно прекращает существовать. Владелец указывается с помощью счётчика ссылок на объект, который увеличивается при вызове функции _copy() и уменьшается при вызове функции _release(). Как только счётчик ссылок установится в ноль, объект удаляется.
Как вариант, удалённое (внешнее) использование устанавливается функцией _useRemote(), и аннулируется функцией _releaseRemote(). Эти функции ведут список серверов, из вызвавших (и поэтому владеющих объектами). Это используется в случае отключения сервера (например при его падении или сбое сети), для удаления оставшихся ссылок на объекты, с использованием функции _disconnectRemote().
Снова проблема. Рассмотрим возвращаемое значение. Обычно возвращаемое значение-объект не принадлежит вызывающей функцией. Оно, однако, также не принадлежит вызывающему объекту, до тех пор пока сообщение, содержащее объект, не будет получено. Итак, какое-то время объекты остаются беспризорными
.
Теперь, когда посылается объект, можно быть уверенным, что как только он будет получен, то его владельцем будет кто-нибудь снова, если только принимающий объект сам не успел уничтожиться. Тем не менее, это означает, что объект нужно поддерживать особо по крайней мере в течение его отправки, а возможно также и в течение приёма, чтобы он - объект - не исчез мгновенно.
&MCOP; делает это с помощью меток
объектов, которые находятся в процессе копирования. До начала копирования, вызывается функция _copyRemote. Это защищает объект от освобождения в течение 5 секунд. Когда принимающая сторона вызывает _useRemote(), метка снова сбрасывается. Итак все объекты, которые пересылаются, помечаются перед отправкой по сети.
Если принимающая сторона принимает объект, который находится на том же сервере, то для этого, конечно же, не используется функция _useRemote(). Для специального случая существует функция _cancelCopyRemote() для удаления метки вручную. Имеются также способы удаления метки, основанные на таймере, но принимающая сторона в действительности может не получить объект (из-за падения сервера, сбоя сети). Тогда удаление меток выполняется с помощью специального класса ReferenceClean.
Элементы графического пользовательского интерфейса
Элементы графического пользовательского интерфейса сейчас в стадии разработки. Эта глава описывает то, что будет воплощено, так что если вы разработчик, то сможете понять как &arts; будет работать с графическим пользовательским интерфейсом в будущем. Уже есть некоторый готовый код.
Элементы графического интерфейса могут быть использованы для создания структур, взаимодействующих с пользователем. В простейшем случае пользователь мог бы непосредственно изменять некоторые параметры структуры (такие как уровень усиления, который используется в конце, перед воспроизводящим модулем).
В более сложных настройках пользователь мог бы измененять параметры группы структур и/или ещё не работающих структур, таких как изменение группы ADSR активного инструмента &MIDI;, или установка имени файла для некоторых инструментов.
Также пользователю мог бы понадобиться монитор состояния синтезатора. Может представить себе осциллограф, спектроанализатор, измеритель уровня громкости и другие устройства для экспериментирования, которые рисовали бы кривую передаточной функции выбранного фильтра.
Наконец, элементы интерфейса должны уметь управлять всеми структурами, того что запущено внутри &arts;. Пользователь должны уметь устанавливать инструменты для каналов midi, запуская новые эффекты, конфигурируя основной микшер, который встроен в структуру &arts;, для усиления какого-либо канала и использования другой стратегии для эквалайзеров.
Вы видите - элементы пользовательского графического интерфейса, должны воспроизвести все возможности виртуальной студии &arts; для пользователя. Конечно, они также должны элегантно взаимодействовать со входами midi (бегунки должны перемещаться, если они управляют входами &MIDI;, которые также изменяются как параметры), и возможно создавать события, предоставлять пользователю возможность записывать звук на секвенсер.
Если говорить техническим языком, речь идёт о базовом классе на &IDL; для всех графических элементов (Arts::Widget), и наследовании из него (например Arts::Poti, Arts::Panel, Arts::Window, ...).
Тогда можно было бы реализовать эти графические примитивы, используя инструментарий, к примеру &Qt; или Gtk. Наконец, эффекты должны строить свой интерфейс на базе существующих элементов. Скажем, эффект freeverb может построить свой интерфейс с помощью пяти Arts::Poti и одного Arts::Window. Итак, если есть реализация таких окон на &Qt;, то эффекты могут отображать себя, используя &Qt;. Если имеется реализация на Gtk, то также можно работать с Gtk (что должно более или менее выглядеть и работать схожим образом).
Наконец, поскольку мы здесь используем &IDL;, &arts-builder; (или другой инструмент) может визуально встраивать элементы интерфейса или автоматически создавать их из подсказок для параметров, основываясь только на интерфейсах. Совсем несложно написать класс создания элементов интерфейса по их описанию
, который использует описание &GUI; и создаёт живой объект элемент интерфейса.
Опираясь на &IDL; и компонентную модель &arts;/&MCOP;, будет легко расширять объекты, которые могут быть использованы для построения пользовательского интерфейса, также как это сделано для добавления отдельных модулей, реализующих новые фильтры для &arts;.