Глава 04: Воспроизведение аудио
Воспроизведение иногда называют презентацией или рендерингом. Это общие термины, которые применимы к другим типам носителей помимо звука. Существенной особенностью является то, что последовательность данных доставляется куда-то для возможного восприятия пользователем. Если данные основаны на времени, как и звук, они должны доставляться с правильной скоростью. Когда звук даже больше, чем видео, важно поддерживать скорость потока данных, потому что перерывы в воспроизведении звука часто вызывают громкие щелчки или раздражающие искажения. Java Sound API разработан, чтобы помочь прикладным программам воспроизводить звуки плавно и непрерывно, даже очень длинные звуки.
В предыдущей главе обсуждалось, как получить линию от аудиосистемы или микшера. В этой главе показано, как воспроизводить звук через линию.
Есть два типа линий, которые вы можете использовать для воспроизведения звука: Clip и SourceDataLine. Эти два интерфейса были кратко представлены в разделе "Иерархия линейного интерфейса" в главе 2 "Обзор пакета Sampled." Основное различие между ними заключается в том, что с помощью Clip вы указываете все звуковые данные одновременно, перед воспроизведением, тогда как с помощью SourceDataLine вы продолжаете непрерывно записывать новые буферы данных во время воспроизведения. Хотя есть много ситуаций, в которых вы можете использовать либо Clip или SourceDataLine, следующие критерии помогают определить, какой тип линии лучше подходит для конкретной ситуации:
- Используйте Clip , если у вас есть звуковые данные не в реальном времени, которые можно предварительно загрузить в память.
Например, вы можете вставить в клип короткий звуковой файл. Если вы хотите, чтобы звук воспроизводился более одного раза, Clip более удобен, чем SourceDataLine, особенно если вы хотите, чтобы воспроизведение было циклическим (циклически повторялось через весь или часть звука). Если вам нужно начать воспроизведение в произвольной позиции звука, интерфейс Clip предоставляет способ, позволяющий легко это сделать. Наконец, воспроизведение из Clip обычно имеет меньшую задержку, чем буферизованное воспроизведение из SourceDataLine. Другими словами, поскольку звук предварительно загружен в клип, воспроизведение может начаться немедленно, а не ждать заполнения буфера.
- Используйте SourceDataLine для потоковой передачи данных, например длинного звукового файла, который не умещается в памяти сразу, или звука, данные которого не могут быть известны до воспроизведения.
В качестве примера последнего случая предположим, что вы отслеживаете ввод звука, то есть воспроизводите звук по мере его захвата. Если у вас нет микшера, который может посылать входной аудиосигнал обратно через выходной порт, ваша прикладная программа должна будет взять захваченные данные и отправить их в микшер аудиовыхода. В этом случае SourceDataLine более уместен, чем Clip. Другой пример звука, который невозможно узнать заранее, возникает, когда вы синтезируете или манипулируете звуковыми данными в интерактивном режиме в ответ на ввод пользователя. Например, представьте игру, которая дает звуковую обратную связь, «трансформируясь» от одного звука к другому, когда пользователь перемещает мышь. Динамический характер преобразования звука требует, чтобы прикладная программа постоянно обновляла звуковые данные во время воспроизведения, вместо того, чтобы предоставлять их все до начала воспроизведения.
Использование Clip
Вы получаете Clip , как описано ранее в разделе "Получение линии желаемого типа" в главе 3, "Доступ к ресурсам аудиосистемы": Создайте объект DataLine.Info c Clip.class для первого аргумента и передайте этот DataLine.Info в качестве аргумента методу getLine класса AudioSystem или Mixer.
Настройка Clip для воспроизведения
Получение линии просто означает, что у вас есть способ сослаться на нее; getLine на самом деле не резервирует линию для вас. Поскольку у микшера может быть ограниченное количество доступных линий желаемого типа, может случиться так, что после того, как вы вызовете getLine для получения клипа, другая прикладная программа выскочит и захватит клип, прежде чем вы будете готовы начать воспроизведение. Чтобы фактически использовать клип, вам необходимо зарезервировать его для исключительного использования вашей программой, вызвав один из следующих методов Clip:
void open(AudioInputStream stream) void open(AudioFormat format, byte[] data, int offset, int bufferSize)
Несмотря на аргумент bufferSize во втором методе open выше, Clip (в отличие от SourceDataLine) не содержит методов для записи новых данных в буфер. Аргумент bufferSize здесь просто указывает, какая часть массива байтов загружается в клип. Это не буфер, в который вы впоследствии можете загрузить больше данных, как вы можете с буфером SourceDataLine.
После открытия клипа вы можете указать, в какой точке данных он должен начинать воспроизведение, с помощью методов setFramePosition или setMicroSecondPosition в классе Clip. В противном случае он начнется с самого начала. Вы также можете настроить повторный цикл воспроизведения с помощью метода setLoopPoints.
Запуск и остановка воспроизведения
Когда вы будете готовы начать воспроизведение, просто вызовите метод start. Чтобы остановить или приостановить клип, вызовите метод stop, а для возобновления воспроизведения снова вызовите start. Клип запоминает позицию мультимедиа, на которой он остановил воспроизведение, поэтому нет необходимости в явных методах паузы и возобновления. Если вы не хотите, чтобы он продолжался с того места, на котором он остановился, вы можете «перемотать» клип в начало (или в любую другую позицию, если на то пошло), используя методы позиционирования кадра или микросекунды, упомянутые выше.
Уровень громкости Clip и статус активности (активный или неактивный) можно отслеживать, вызывая методы DataLine getLevel и isActive соответственно. Активный Clip - это тот, который в настоящее время воспроизводит звук.
Использование SourceDataLine
Получение SourceDataLine аналогично получению Clip. См. "Получение линии желаемого типа" в главе 3 "Доступ к ресурсам аудиосистемы."
Настройка SourceDataLine для воспроизведения
Открытие SourceDataLine также похоже на открытие Clip, поскольку цель снова состоит в том, чтобы зарезервировать линию. Однако вы используете другой метод, унаследованный от DataLine:
void open(AudioFormat format)
Обратите внимание, что когда вы открываете SourceDataLine, вы еще не связываете какие-либо звуковые данные со линией, в отличие от открытия Clip. Вместо этого вы просто указываете формат аудиоданных, которые хотите воспроизвести. Система выбирает длину буфера по умолчанию.
Вы также можете указать определенную длину буфера в байтах, используя этот вариант:
void open(AudioFormat format, int bufferSize)
Для согласованности с аналогичными методами аргумент размера буфера buffersize выражается в байтах, но он должен соответствовать целому количеству кадров.
Как бы вы выбрали размер буфера? Это зависит от потребностей вашей программы.
Начнем с того, что более короткие размеры буфера означают меньшую задержку. Когда вы отправляете новые данные, вы их слышите раньше. Для некоторых прикладных программ, особенно интерактивных, такая скорость отклика важна. Например, в игре начало воспроизведения может быть тесно синхронизировано с визуальным событием. Таким программам может потребоваться задержка менее 0,1 секунды. Другой пример: приложение для конференц-связи должно избегать задержек как при воспроизведении, так и при захвате. Однако многие прикладные программы могут позволить себе большую задержку, до секунды или более, потому что не имеет значения, когда именно начинается воспроизведение звука, если задержка не сбивает с толку и не раздражает пользователя. Это может быть случай с прикладной программой, которая передает большой аудиофайл с использованием односекундных буферов. Пользователь, вероятно, не будет заботиться о том, чтобы воспроизведение началось за секунду, потому что сам звук длится очень долго, а процесс не очень интерактивный.
С другой стороны, более короткие размеры буфера также означают больший риск того, что вы не сможете достаточно быстро записать данные в буфер. Если это произойдет, аудиоданные будут содержать разрывы, которые, вероятно, будут слышны как щелчки или прерывания звука. Более короткие размеры буфера также означают, что ваша программа должна усерднее работать, чтобы буферы оставались заполненными, что приводит к более интенсивной загрузке ЦП. Это может замедлить выполнение других потоков в вашей программе, не говоря уже о других программах.
Таким образом, оптимальным значением размера буфера является такое, которое минимизирует задержку до степени, приемлемой для вашей прикладной программы, при этом сохраняя ее достаточно большой, чтобы снизить риск переполнения буфера и избежать ненужного потребления ресурсов ЦП. Для такой программы, как приложение для конференц-связи, задержки больше раздражают, чем звук с низким качеством воспроизведения, поэтому предпочтительнее небольшой размер буфера. Для потоковой передачи музыки начальная задержка приемлема, но не прерывание звука. Таким образом, для потоковой передачи музыки предпочтительнее использовать буфер большего размера - скажем, второй. (Обратите внимание, что высокая частота дискретизации увеличивает размер буферов с точки зрения количества байтов, которые являются единицами измерения размера буфера в API DataLine.)
Вместо использования метода open, описанного выше, также можно открыть SourceDataLine с помощью метода open() класса Line без аргументов. В этом случае линия открывается с аудиоформатом по умолчанию и размером буфера. Однако вы не можете изменить их позже. Если вы хотите узнать звуковой формат линии по умолчанию и размер буфера, вы можете вызвать методы getFormat и getBufferSize класса DataLine, даже до того, как линия когда-либо была открыта.
Запуск и остановка воспроизведения
Как только SourceDataLine открыт, вы можете начать воспроизведение звука. Вы делаете это, вызывая метод start класса DataLine, а затем повторно записывая данные в буфер воспроизведения линии.
Метод start позволяет линии начать воспроизведение звука, как только в ее буфере появятся какие-либо данные. Вы помещаете данные в буфер следующим образом:
int write(byte[] b, int offset, int length)
Смещение в массиве выражается в байтах, как и длина массива.
Линия начинает посылать данные как можно скорее в свой микшер. Когда микшер сам доставляет данные к своей цели, SourceDataLine генерирует событие START. (В типичной реализации Java Sound API задержка между моментом, когда исходная линия доставляет данные в микшер, и моментом, когда микшер доставляет данные в свою цель, пренебрежимо мала—то есть намного меньше времени одного образца.) Это событие START отправляется слушателям линии, как описано ниже в разделе "Мониторинг состояния линии." Линия теперь считается активной, поэтому метод isActive класса DataLine вернет true. Обратите внимание, что все это происходит только после того, как буфер содержит данные для воспроизведения, а не обязательно сразу после вызова метода start. Если вы вызвали start на новом SourceDataLine, но никогда не записывали данные в буфер, линия никогда не будет активна и событие START никогда не будет отправлено. (Однако в этом случае метод isRunning класса DataLine вернет true.)
Итак, как вы узнаете, сколько данных нужно записать в буфер и когда отправить вторую партию данных? К счастью, вам не нужно время второго вызова write для синхронизации с концом первого буфера! Вместо этого вы можете воспользоваться преимуществами блокирующего поведения метода write:
- Метод возвращается, как только данные были записаны в буфер. Он не ждет, пока все данные в буфере закончат воспроизведение. (Если бы это было так, у вас не было бы времени, чтобы записать следующий буфер, не создавая разрыв в звуке.)
- Можно попытаться записать больше данных, чем может вместить буфер. В этом случае метод блокируется (не возвращается) до тех пор, пока все запрошенные данные не будут фактически помещены в буфер. Другими словами, ваши данные будут записываться в буфер и воспроизводиться до тех пор, пока все оставшиеся данные не умещаются в буфере, после чего метод возвращается. Независимо от того, блокируется метод или нет, он возвращается, как только может быть записано значение последнего буфера из этого вызова. Опять же, это означает, что ваш код, по всей вероятности, восстановит контроль до того, как закончится воспроизведение данных последнего буфера.
- Хотя во многих случаях допустимо записывать больше данных, чем может вместить буфер, если вы хотите быть уверенным, что следующая выполненная запись не будет блокироваться, вы можете ограничить количество записываемых байтов до числа, которое возвращает метод available (доступно) класса DataLine.
Вот пример перебора фрагментов данных, считываемых из потока, при записи одного фрагмента в SourceDataLine для воспроизведения:
// читать фрагменты из потока и записывать их в линию исходных данных line.start(); while (total < totalToRead && !stopped)} numBytesRead = stream.read(myData, 0, numBytesToRead); if (numBytesRead == -1) break; total += numBytesRead; line.write(myData, 0, numBytesRead); }
Если вы не хотите, чтобы метод write блокировался, вы можете сначала вызвать метод available (внутри цикла), чтобы узнать, сколько байтов можно записать без блокировки, а затем ограничьте значение переменной numBytesToRead этим числом перед чтением из потока. Однако в приведенном примере блокировка не имеет большого значения, поскольку метод записи вызывается внутри цикла, который не завершится, пока последний буфер не будет записан в последней итерации цикла. Независимо от того, используете вы технику блокировки или нет, вы, вероятно, захотите вызвать этот цикл воспроизведения в отдельном потоке от остальной части прикладной программы, чтобы ваша программа не зависала при воспроизведении длинного звука. На каждой итерации цикла вы можете проверить, запрашивал ли пользователь остановку воспроизведения. Такой запрос должен установить для логического значения stopped, используемого в приведенном выше коде, значение true.
Поскольку write возвращается до того, как все данные закончат воспроизведение, как узнать, когда воспроизведение действительно завершилось? Один из способов - вызвать метод drain класса DataLine после записи данных последнего буфера. Этот метод блокируется до тех пор, пока не будут воспроизведены все данные. Когда управление вернется к вашей программе, вы можете при желании освободить линию, не опасаясь преждевременного прерывания воспроизведения любых аудиосэмплов:
line.write(b, offset, numBytesToWrite); // это последний вызов записи line.drain(); line.stop(); line.close(); line = null;
Конечно, вы можете намеренно остановить воспроизведение раньше времени. Например, прикладная программа может предоставить пользователю кнопку «Стоп». Вызовите метод stop класса DataLine , чтобы немедленно остановить воспроизведение, даже в середине буфера. При этом в буфере остаются не воспроизведенные данные, так что, если вы впоследствии вызываете start, воспроизведение возобновляется с того места, где оно было остановлено. Если это не то, что вы хотите, вы можете сбросить данные, оставшиеся в буфере, вызвав flush.
SourceDataLine генерирует событие STOP всякий раз, когда поток данных был остановлен, независимо от того, была ли эта остановка инициирована методом drain, методом stop, или методом flush, или потому, что конец буфера воспроизведения был достигнут до того, как программа снова вызвала write для предоставления новых данных. Событие STOP не обязательно означает, что был вызван метод stop, и не обязательно означает, что последующий вызов isRunning вернет false. Однако это означает, что isActive вернет false. (Когда метод start был вызван, метод isRunning вернет true, даже если сгенерировано событие STOP, и он начнет возвращать false только после вызова метода stop.) Важно понимать, что события START и STOP соответствуют в isActive, а не в isRunning.
Мониторинг статуса линии
После того, как вы начали воспроизведение звука, как вы обнаружите, что он закончился? Выше мы видели одно решение - вызов метода слива после записи последнего буфера данных - но этот подход применим только к SourceDataLine. Другой подход, который работает как для SourceDataLines, так и для Clips, заключается в регистрации для получения уведомлений из линии всякий раз, когда линия меняет свое состояние. Эти уведомления генерируются в форме объектов LineEvent , которых есть четыре типа: OPEN, CLOSE, START, и STOP.
Любой объект в вашей программе, реализующий интерфейс LineListener, может зарегистрироваться для получения таких уведомлений. Чтобы реализовать интерфейс LineListener объекту просто нужен метод update, принимающий аргумент LineEvent. Чтобы зарегистрировать этот объект в качестве одного из слушателей линии, вы вызываете следующий метод Line:
public void addLineListener(LineListener listener)
Каждый раз, когда линия открывается, закрывается, запускается или останавливается, она отправляет сообщение об обновлении всем своим слушателям. Ваш объект может запросить LineEvent, который он получает. Сначала вы можете вызвать LineEvent.getLine , чтобы убедиться, что остановленная линия - это именно та, которая вам нужна. В случае, который мы здесь обсуждаем, вы хотите знать, закончился ли звук, чтобы увидеть, имеет ли LineEvent тип STOP. Если это так, вы можете проверить текущую позицию звука, которая также сохраняется в объекте LineEvent, и сравнить ее с длиной звука (если она известна), чтобы увидеть, достиг ли он конца и не был ли остановлен другими способами (например, когда пользователь нажимает кнопку Стоп, хотя вы, вероятно, сможете определить эту причину в другом месте вашего кода).
Аналогичным образом, если вам нужно знать, когда линия открыта, закрыта или запущена, вы используете тот же механизм. LineEvents генерируются разными типами линий, а не только Clips и SourceDataLines. Однако в случае с Port вы не можете рассчитывать на получение события, чтобы узнать об открытом или закрытом состоянии линии. Например, Port может быть изначально открыт при создании, поэтому вы не вызываете метод open, и Port никогда не генерирует событие OPEN. (См. "Выбор портов ввода и вывода" в главе 3 "Доступ к ресурсам аудиосистемы.")
Синхронизация воспроизведения на нескольких линиях.
IЕсли вы воспроизводите несколько звуковых дорожек одновременно, вы, вероятно, захотите, чтобы все они запускались и останавливались в одно и то же время. Некоторые микшеры облегчают такое поведение с помощью своего метода synchronize, который позволяет применять такие операции, как open, close, start, и stop, к группе линий данных с помощью одной команды, вместо того, чтобы управлять каждой линией по отдельности. Кроме того, степень точности, с которой операции применяются к линиям, можно контролировать.
Чтобы узнать, предлагает ли конкретный микшер эту функцию для указанной группы линий данных, вызовите метод isSynchronizationSupported интерфейса Mixer:
boolean isSynchronizationSupported(Line[] lines, boolean maintainSync)
Первый параметр указывает группу конкретных линий данных, а второй параметр указывает точность, с которой должна поддерживаться синхронизация. Если второй параметр имеет значение true, запрос спрашивает, способен ли микшер поддерживать требуемую точность выборки при управлении указанными линиями в любое время; в противном случае точная синхронизация требуется только во время операций запуска и остановки, но не во время воспроизведения.
Обработка исходящего аудио
Некоторые исходные линии передачи данных имеют элементы управления обработкой сигналов, такие как усиление, панорамирование, реверберация и частота дискретизации. Аналогичные элементы управления, особенно усиления, могут присутствовать и на выходных портах. Дополнительные сведения о том, как определить, есть ли в линии такие элементы управления, и как их использовать, если они есть, см. В главе 6, "Обработка звука с помощью элементов управления."
Предыдущая | Следующая |