Звук в JAVA, часть первая, Начало. Проигрывание звука

Это начальный из серии в восемь уроков, который полностью ознакомит вас с Java Sound API. 
Что такое звук в человеческом восприятии? Это ощущение, которое мы испытываем когда изменение давления воздуха передается на крохотные сенсорные участки внутри наших ушей.
И главная цель создания Sound API, соответственно обеспечить вас средствами для написания кода, который и поможет в передаче волн давления в уши нужному субъекту в нужное время.

Типы звука в Java:

  1. В Java Sound API поддерживаются два основных типа аудио (звука).
  2. Звук оцифрованный и записанный непосредственно в виде файла
  3. Запись в виде MIDI файла. Очень отдаленно, но похожа на нотную запись, где музыкальные инструменты играют в нужной последовательности.


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

Анонс.


Java Sound API основывается на концепции линий и микшеров.

Далее:
Мы приведём описание физических и электрических характеристик аналогового представления звука применительно к аудио микшеру.
Мы обратимся к сценарию работы начинающей рок-группы, которая использует в данном случае шесть микрофонов и два стерео динамика. Это нам понадобится для понимания работы аудио микшера.
Далее мы рассмотрим ряд Java Sound тем для программирования, таких как линии, микшеры, форматы для аудио данных и прочее.
Мы разберемся в связях существующих между объектами SourceDataLine, Clip, Mixer, AudioFormat и создадим простую программу воспроизводящую аудио.
Ниже мы приведем пример этой программы, которую вы сможете использовать для того, чтобы записать, а затем проиграть записанный звук.
В дальнейшем мы представим полное объяснение программного кода использованного для этой цели. Но отнюдь не полностью именно в этом уроке.

Пример кода и его рассмотрение


Физические и электрические характеристики аналогового звука

Цель нашего урока ознакомить вас с основами программирования на Java, используя Java Sound API.
Java Sound API основан на концепции аудио микшера, который является устройством обычно используемым при воспроизведении звука практически где угодно: от рок концертов, до прослушивания CD дисков дома. Но прежде чем пуститься в детальные разъяснения работы аудио микшера, будет полезным ознакомиться с физическими и электрическими характеристиками самого аналогового звука.

Посмотрите на Рис. 1



Вася Пупыркин толкает речь.

Этот рисунок показывают Васю, который произносит речь, используя систему известную как широкоадресную. Такая система обычно содержит микрофон, усилитель и громкоговоритель. Назначение этой системы — усилить голос Васи, чтобы его могли услышать даже в большой толпе.

Колебания в воздухе

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

Динамический микрофон

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


Рис. 2 Схема динамического микрофона

Звуковые колебания воздействуют на мембрану

Давление колебаний звука воздействует на гибкую мембрану внутри микрофона. Это заставляет мембрану вибрировать, при этом колебания мембраны повторяют колебания звуковых волн.

Перемещение катушки

Катушка намотанная из тонкого провода прикреплена к мембране микрофона. Поскольку мембрана колеблется, то и катушка совершает возвратно-поступательные движения в магнитном поле сердечника сделанном из сильного постоянного магнита. И как еще установил Фарадей, при этом в катушке возникает электрический ток.

Электрический сигнал повторяет форму звуковых волн

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

Громкоговоритель

По сути принцип работы громкоговорителя повторяет устройство динамического микрофона, только включенного в обратном направлении. (Естественно, что в этом случае провода обмотки гораздо толще, а мембрана намного больше, чтобы обеспечить работу с усиленным сигналом)




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

Рок концерт

К этому времени вы можете задаться вопросом, какое отношение это всё имеет к Java Sound API? Но подождите ещё чуть-чуть, мы ведем путь к основам работы аудио микшера.
Схема описанная выше была довольно проста. Она состояла из Васи Пупыркина, одного микрофона, усилителя и громкоговорителя. Теперь рассмотрим схему с Рис. 4, на котором представлена сцена подготовленная для рок концерта начинающей музыкальной группы.



Шесть микрофонов и два громкоговорителя

На Рис. 4 шесть микрофонов расположено на сцене. Два громкоговорителя (динамика) размещены по бокам сцены. Когда начинается концерт, исполнители поют или играют музыку в каждый из шести микрофонов. Соответственно у нас будет шесть электрических сигналов, которые должны быть индивидуально усилены и поданы затем на оба динамика. В дополнение к этому, исполнители могут задействовать различные звуковые спецэффекты, например, реверберацию, которые тоже необходимо будет перевести в электрические сигналы, перед тем как подать их на громкоговорители.

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

Аудио микшер

Задача рассмотренная выше как раз выполняется электронным устройством, которое называется аудио микшер.

Аудио линия (канал)

Хотя автор не эксперт в аудио микшерах, в его скромном понимании, типичный аудио микшер имеет возможность получать на входе какое-то количество независимых друг от друга электрических сигналов, каждый из которых представляет исходный звуковой сигнал или линию (канал).
(концепция аудио канала станет весьма важной, когда мы начнём в деталях разбираться с Java Sound API.

Независимая обработка каждого аудио канала

В любом случае, стандартный аудио микшер имеет возможность усиления каждой аудио линии независимо от остальных других каналов. Также микшер обычно имеет возможность наложения звуковых спецэффектов, таких как, к примеру, реверберация на любую из аудио линий. В конечном же итоге, микшер, как это следует из его названия, может смешивать все индивидуальные электрические сигналы в выходных каналах как ему будет задано, таким образом, чтобы контролировать вклад каждой аудио линии в выходные каналы.(Этот контроль обычно называется pan или панорамирование — распределение в пространстве).

Возвращаясь к стереозвуку

Таким образом на схеме с Рис. 4, звукооператор аудио микшера имеет возможность, объединить сигналы с шести микрофонов, чтобы получить два выходных сигнала, каждый из которых передается на свой громкоговоритель.
Для успешной работы сигнал с каждого микрофона должен подаваться в соответствующей пропорции, зависящей от физического месторасположения микрофона на сцене. (Изменяя панорамирование, квалифицированный звукооператор может менять вклад каждого микрофона при необходимости, если например, ведущий вокалист перемещается во время концерта по сцене).

Время вернуться в мир программирования

Давайте теперь вернёмся из физического мира в мир программирования. Согласно Sun: «Java Sound не предполагает специальной конфигурации аппаратного обеспечения; он спроектирован для того, чтобы позволить различным аудио компонентам быть инсталлированными в систему и быть доступными пользователю через API. Java Sound поддерживает стандартную функциональность входа и выхода со звуковой карты (к примеру, для записи и проигрывания аудио файлов), а также возможность микширования нескольких аудио потоков»

Микшеры и каналы

Как уже было сказано Java Sound API построено на концепции микшеров и каналов. Если двигаться из физического мира в мир программирования, то Sun пишет следующее, относительно микшера:

«Микшер это аудио устройство с одним или более каналами. Но миксер который действительно смешивает аудио сигнал должен иметь несколько входных каналов источников source и как минимум один выходной target канал ».
Входные линии могут являться экземплярами классов с объектами SourceDataLine, а выходные — TargetDataLine. Микшер может принимать на вход также заранее записанный и закольцованный звук, определяя свои входные source каналы как экземпляры объектов класса реализующий интерфейс Clip.

Интерфейс канал Line.
Sun сообщает следующее от интерфейсе Line: «Line это элемент цифрового аудио конвейера такого как входной или выходной аудио порт, микшер или маршрут аудиоданных в или из микшера. Аудиоданные проходящие через канал могут быть моно или многоканальными (к примеру, стерео). … Канал может иметь элементы управления Controls, такие как усиление, панорамирование и реверберацию».

Объединяя термины вместе

Итак, вышеуказанные цитаты от Sun обозначали следующие термины

SourceDataLine
TargetDataLine
Port
Clip
Controls

Рис. 5 показывает пример использования этих терминов для построения простой программы вывода аудио



Сценарий программы

С программной точки зрения Рис. 5 показывает объект Микшер, полученный с одним объектом Clip и двумя SourceDataLine объектами.

Что такое Clip

Clip это объект на входе микшера, содержимое которого не меняется со временем. Другими словами, вы загружаете аудио данные в объект Clip до того как проиграете его. Аудиоконтент объекта Clip может проигран один или более раз. Вы можете закольцевать Clip и тогда контент будет проигрываться снова и снова.

Входной поток

Объект SourceDataLine с другой стороны представляет собой объект потока на входе микшера. Объект такого типа может принимать поток аудио данных и отправлять его в микшер в режиме реального времени. Необходимые аудио данные могут быть получены из различных источников, таких как аудио файлы, сетевое соединение или буфер памяти.

Различные типы каналов

Таким образом объекты Clip и SourceDataLine могут быть рассмотрены как входные каналы для объекта Mixer. Каждый из этих входных каналов может иметь свои собственные: панорамирование, усиление и реверберацию.

Проигрывание аудио контента

В такой несложной системе Mixer читает данные с входных линий, использует контроль для смешивания входных сигналов и обеспечивает выходной поток в один или более выходные каналы, такие как динамик, линейный выход, разъём для наушников и так далее.

В листинге 11 приведена простая программа, которая захватывает аудио данные с микрофонного порта, запоминает эти данные в памяти, а затем проигрывает их через порт динамика.

Мы будем обсуждать только захват и проигрывание. Большая часть приведенной программы приходится на создание окна и графического интерфейса для пользователя для того, чтобы было возможно управлять записью и проигрыванием. Мы не будем обсуждать эту часть, как выходящую за рамки поставленной цели. Но зато мы рассмотрим захват и проигрывание данных. Проигрывание мы обсудим в этом уроке, а захват в следующем. Попутно мы проиллюстрируем использование аудио канала с Java Sound API.

Захваченные данные запоминаются в объекте ByteArrayOutputStream.
Фрагмент кода захвата данных обеспечивает чтение аудио данных с микрофона и запоминание их в виде объекта ByteArrayOutputStream.
Метод под название playAudio, которые начинается в листинге 1, проигрывает аудио данные, что были захвачены и сохранены в объекте ByteArrayOutputStream.

private void playAudio() {
    try{
      byte audioData[] =
                 byteArrayOutputStream.
                         toByteArray();

      InputStream byteArrayInputStream
            = new ByteArrayInputStream(
                            audioData);

Листинг 1

Начинаем со стандартного кода

Фрагмент программы в листинге 1, в действительности пока еще не имеет отношения к Java Sound.

Его назначение состоит в том чтобы:

  • Преобразовать ранее сохраненные данные в массив типа byte.
  • Получить входной поток для байтового массива данных.

Это необходимо нам, чтобы сделать аудио данные доступными для последующего проигрывания.

Переходим к Sound API

Строчка кода из листинга 2 уже имеет отношение к Java Sound API.

      AudioFormat audioFormat =
                      getAudioFormat();

Листинг 2

Здесь мы коротко коснёмся темы, которая будет детально обсуждаться в следующем уроке.

Два независимых формата

Чаще всего мы имеем дело с двумя независимыми форматами для аудио данных.
Формат файла, (любой) который содержит аудио данные (в нашей программе пока его нет, так как данные сохраняются в памяти)
Формат представленных аудио данных сам по себе.

Что же такое аудио формат?

Вот что пишет об этом Sun:
“Каждый канал данных имеет свой аудио формат связанный с его потоком данным. Формат (экземпляр AudioFormat) определяет порядок следования байтов а аудио потоке. Параметрами формата могут являться количество каналов, частота дискретизации, разрядность квантования, способ кодирования и т. д. Обычными способами кодирования могут быть линейная импульсно-кодовая модуляция ИКМ и её разновидности.”

Последовательность байтов

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

Небольшое отступление

Здесь мы пока оставим метод playAudio и рассмотрим метод getAudioFormat из листинга 2.
Полностью метод getAudioFormat представлен в листинге 3.

 private AudioFormat getAudioFormat(){
    float sampleRate = 8000.0F;
    int sampleSizeInBits = 16;
    int channels = 1;
    boolean signed = true;
    boolean bigEndian = false;
    return new AudioFormat(
                      sampleRate,
                      sampleSizeInBits,
                      channels,
                      signed,
                      bigEndian);
  }//end getAudioFormat

Листинг 3

Кроме декларации инициализированных переменных код из листинга 3 содержит одно исполняемое выражение.

Объект AudioFormat

Метод getAudioFormat создает и возвращает экземпляр объекта класса AudioFormat. Вот что Sun пишет об этом классе:
«Класс AudioFormat определяет конкретную упорядоченность данных в аудио потоке. Обратившись к полям объекта AudioFormat, вы может получить информацию как правильно интерпретировать биты в двоичном потоке данных.»

Используем простейший конструктор

Класс AudioFormat имеет два вида конструкторов (мы возьмём самый тривиальный). Для этого конструктора требуемые следующие параметры:

  • Частота дискретизации или частота отсчётов в секунду (Доступные величины: 8000, 11025, 16000, 22050 и 44100 отсчётов в секунду)
  • Разрядность данных в битах (доступны 8 и 16 битов на отсчёт)
  • Количество каналов ( один канал для моно и два для стерео)
  • Знаковые или беззнаковые данные, которые используются в потоке (к примеру величина меняется от 0 до 255 или от -127 до +127)
  • Порядок следования байтов Big-endian или little-endian. (если вы передаете байтовым потоком 16-разрядные величины, то важно знать какой байт идет первым — младший или старший, так как встречаются оба варианта).

Как вы можете видеть в листинге 3, в нашем случае мы использовали следующие параметры для экземпляра объекта AudioFormat.

  • 8000 отсчётов в секунду
  • 16 размер данных
  • данные знаковые
  • порядок Little-endian


По умолчанию данные кодируются линейной ИКМ.
Конструктор, который мы использовали, создает экземпляр объекта AudioFormat использующий линейную импульсно-кодовую модуляцию и параметры указанные выше (Мы вернемся к линейной ИКМ и другим способам кодирования в следующих уроках)

Снова возвращаемся к методу playAudio

Теперь, когда мы поняли как устроен формат аудио данных в Java sound, давайте вернёмся к методу playAudio. Как только мы захотим проиграть доступные аудио данные, нам понадобится объект класса AudioInputStream. Мы получим его экземпляр в листинге 4.

  audioInputStream =
        new AudioInputStream(
          byteArrayInputStream,
          audioFormat,
          audioData.length/audioFormat.
   

Листинг 4

Параметры для конструктора AudioInputStream

  • Конструктор для класса AudioInputStream требует следующие три параметра:
  • Поток на котором будет основан экземпляр объекта AudioInputStream (как мы видим для этой цели нам служит экземпляр объекта ByteArrayInputStream созданный ранее)
  • Формат аудио данных для этого потока (для этой цели мы уже создали экземпляр объекта AudioFormat)
  • Размер фрейма (кадра) для данных в этом потоке (смотрим описание ниже по тексту)
  • Первые два параметра понятны из кода в листинге 4. Однако третий параметр не так очевиден сам по себе.

Получение размера кадра

Как мы видим из листинга 4, величина третьего параметра создается при помощи вычислений. Это как раз один из атрибутов формата аудио, о котором мы раньше не упоминали, и называется он — кадр.

Что такое кадр?

Для простой линейной ИКМ использованной в нашей программе, кадр содержит набор отсчётов для всех каналов в данный момент времени.
Таким образом размер кадра равен размеру величины отсчета в байтах умноженной на количество каналов.
Как вы уже может быть догадались, метод с названием getFrameSize возвращает размер кадра в байтах.

Подсчёт размера кадров

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

Получение объекта SourceDataLine

Следующая часть программы, которую мы обсудим — это простая система вывода аудио. Как мы можем видеть из схемы на Рис.5, для решения этой задачи нам потребуется объект SourceDataLine.
Существует несколько путей, чтобы получить экземпляр объекта SourceDataLine, причем все они весьма заковыристы. Код в листинге 5 получает и сохраняет ссылку на экземпляр объекта SourceDataLine.
(Обратите внимание, что этот код не просто создает экземпляр объекта SourceDataLine. Он получает его довольно окольным способом.)

    DataLine.Info dataLineInfo =
                new DataLine.Info(
                  SourceDataLine.class,
                          audioFormat);

      sourceDataLine = (SourceDataLine)
                   AudioSystem.getLine(
   

Листинг 5

Что представляет собой объект SourceDataLine?

Насчёт этого Sun пишет следующее:
«SourceDataLine это канал данных в который данные могут быть записаны. Он работает как вход для микшера. Приложение записывает байтовую последовательность в SourceDataLine, который буферизует данные и доставляет их к своему микшеру. Микшер же может передать обработанные им данные для следующего этапа, к примеру на выходной порт.
Заметьте, что соглашение об именовании для такого сопряжения отражает взаимоотношение между каналом и его микшером»


Метод getLine для класса AudioSystem

Один из способов получения экземпляра объекта SourceDataLine состоит в том, чтобы вызвать статический getLine метод из класса AudioSystem (У нас будет много чего сообщить о нём на следующих уроках).
Метод getLine требует входной параметр типа Line.Info и возвращает объект Line, который соответствует описанию в уже определенном объекте Line.Info.

Еще одно короткое отступление

Sun сообщает следующую информацию об объекте Line.Info:
«Канал имеет свой информационный объект (экземпляр Line.Info ), который показывает какой микшер (если есть) отправляет смикшированные аудио данные как выходные непосредственно в канал, и какой микшер (если имеется) получает аудио данные как входные непосредственно из канала. Разновидности Line могут соответствовать подклассам Line.Info, что позволяет указывать другие виды параметров относящихся уже к конкретным типам каналов»

Объект DataLine.Info

Первое выражение в листинге 5 создает новый экземпляр объекта DataLine.Info, который является специальной формой (подклассом) объекта Line.Info.
Существует несколько перегружаемых конструкторов для класса DataLine.Info. Мы выбрали для использования самый простой. Этот конструктор требует два параметра.

Объект Class

Первый параметр это Class, который представляет класс, который мы определили как SourceDataLine.class
Второй параметр определяет желаемый формат данных для канала. Мы используем для него экземпляр объекта AudioFormat, который уже был определён ранее.

Мы уже там где нужно?

К сожалению, у нас всё еще нет самого требуемого нам объекта SourceDataLine. Пока у нас есть объект, который только представляет информацию о нужном нам объекте SourceDataLine.

Получение объекта SourceDataLine

Второе выражение в листинге 5 наконец-то создает и сохраняет так необходимый нам экземпляр объекта SourceDataLine. Это происходит путем вызова статического метода getLine класса AudioSystem и передачи dataLineInfo как параметра. (На следующем уроке мы рассмотрим как получить объект Line, работая непосредственно с объектом Mixer ).
Метод getLine возвращает ссылку на объект типа Line, которым является родительским по отношению к SourceDataLine. Поэтому здесь необходимо нисходящее приведение типа перед тем как возвращаемое значение будет сохранено как SourceDataLine.

Приготовимся использовать объект SourceDataLine

Как только мы получили экземпляр объекта SourceDataLine мы должны подготовить его для открытия и запуска, как показано в листинге 6.

      sourceDataLine.open(audioFormat);
      sourceDataLine.start();

Листинг 6

Открывающий метод

Как вы можете видеть из листинга 6, мы отправили объект AudioFormat в открывающий метод для объекта SourceDataLine.

В соответствии с Sun это метод:
«Открывает линию (канал) с определенным ранее форматом, позволяя ему получать любые требуемые им системные ресурсы и быть в рабочем (действующем) состоянии»

Состояние открытия

Есть еще немного, что пишет о нём Sun в этой теме.
«Открытие и закрытие канала воздействует на распределение системных ресурсов. Успешное открытие канала гарантирует, что все необходимые ресурсы каналу предоставлены.
Открытие микшера, который имеет свои входные и выходные порты для аудио данных, включает ко всему прочему, задействование аппаратного обеспечения платформы на которой происходит работа и инициализацию необходимых программных компонентов.
Открытие канала, который является маршрутом для аудио данных в микшер или из него, включает в себя как его инициализацию, так и получение отнюдь не безграничных ресурсов микшера. Другими словами микшер имеет конечное количество каналов, поэтому несколько приложений со своими потребностями в каналах (и даже иногда одно приложение) должны корректно делить ресурсы микшера)»


Вызов метода start для канала

Согласно Sun вызов метода старт для канала означает следующее:
«Каналу разрешается использование линий ввода-вывода. Если производится попытка задействовать уже работающую линию, метод ничего не делает. Но после опустошения буфера с данными линия возобновляет запуск ввода-вывода, начиная с первого кадра, который она не успела обработать, после того как буфер был полностью загружен.»

В нашем случае, конечно, канал не останавливался. Поскольку мы его запустили в первый раз.

Теперь у нас есть почти всё, что нам нужно

К этому моменту мы получили все аудио ресурсы, которые нам нужны, чтобы проигрывать аудио данные, которые мы уже предварительно записали и сохранили в экземпляре объекта ByteArrayOutputStream. (Напомним, что этот объект существует только в оперативной памяти компьютера).

Запускаем потоки

Мы создадим и запустим поток чтобы воспроизвести аудио. Код в листинге 7 создает и запускает этот поток.
(Не путайте вызов метода start в этом потоке с вызовом метода start в объекте SourceDataLine из листинга 6. Это абсолютно разные операции)

      Thread playThread =
          new Thread(new PlayThread());
      playThread.start();
    } catch (Exception e) {
      System.out.println(e);
      System.exit(0);
    }//end catch
  }//end playAudio

Листинг 7

Незатейливый код

Фрагмент программы из листинга 7 хоть и очень несложен, но зато показывает пример много-поточного программирования в Java. Если он вам непонятен, то вам лучше ознакомиться с этой темой в специализированных темах по обучению Java.
Как только поток запущен, он будет работать до тех пор, пока все предварительно записанные аудио данные не будут проиграны до конца.

Новый объект Thread

Код в листинге 7 создает экземпляр объекта Thread (потока) принадлежащего классу PlayThread. Это класс определен как внутренний класс в нашей программе. Его описание начинается в листинге 8.

class PlayThread extends Thread{
  byte tempBuffer[] = new byte[10000];

Листинг 8

Метод run в классе Thread

За исключением объявления переменной tempBuffer (которая ссылается на массив байт), полное определение этого класса это просто определение метода run. Как вы уже должны знать, вызов метода start в объекте Thread, заставляет выполниться метод run этого объекта

Метод run для этого потока начинается в листинге 9

  public void run(){
    try{
      int cnt;
      //Цикл работает
      // пока не возвращается -1
      // 
      while((cnt = audioInputStream.
        read(tempBuffer, 0,
            tempBuffer.length)) != -1){
        if(cnt > 0){
          //Запись данных во
          // внутренний буфер канала
          // откуда они отправляются
          // на звуковой выход.
          sourceDataLine.write(
                   tempBuffer, 0, cnt);
        }//end if
      }//end while

Листинг 9

Первая часть фрагмента программы в методе run

Метод run содержит две важных части, первая из которых показана в листинге 9.
В сумме, здесь используется цикл, чтобы считывать аудио данные из объекта AudioInputStream и передавать их объекту SourceDataLine.
Данные отправленные объекту SourceDataLine автоматически передаются на звуковой выход работающий по умолчанию. Это может быть встроенный динамик компьютера или линейный выход. (Определять нужное звуковое устройств мы научимся в следующих уроках). Переменная cnt и буфер данных tempBuffer используется для контроля потока данных между операциями чтения и записи.

Чтение данных из AudioInputStream

Цикл чтения из объекта AudioInputStream, читает заданное максимальное количество байт данных из AudioInputStream и помещает их байтовый массив.

Возвращаемое значение

Далее этот метод возвращает итоговое количество прочитанных байт или -1, в случае, если был достигнут конец записанной последовательности. Количество прочитанных байт сохраняется в переменной cnt.

Цикл записи в SourceDataLine

Если количество прочитанных байт больше нуля, то тогда происходит переход к циклу записи данных в SourceDataLine. В этом цикле аудио данные поступают в микшер. Байты читаются из байтового массива в соответствии с их индексами и записывают в буфер канала.

Когда входной поток «пересыхает»

Когда цикл чтения возвращает -1, это означает, что все ранее записанные аудио данные закончились и далее контроль передается фрагменту программы в листинге 10.

      sourceDataLine.drain();
      sourceDataLine.close();
    }catch (Exception e) {
      System.out.println(e);
      System.exit(0);
    }//end catch
  }//end run
}//конец внутреннего класса PlayThread

Листинг 10

Блокировка и ожидание

Код в листинге 10 вызывает метод drain для объекта SourceDataLine, чтобы программа могла заблокироваться и ожидать опустошения внутреннего буфера в SourceDataLine. Когда буфер опустошен, это означает что вся очередная порция доставлена к звуковому выходу компьютера.

Закрытие SourceDataLine

Затем программа вызывает метод close для закрытия канала, показывая тем самым, что все системные ресурсы используемые каналом теперь свободны. Sun сообщает следующее о закрытии канала:

«Закрытие канала сигнализирует, что все ресурсы задействованные для этого канала могут быть освобождены. Чтобы освободить ресурсы, приложение должно закрыть каналы, независимо задействованы они или уже нет, а также при окончании работы приложения. Предполагается что микшеры совместно используют системные ресурсы и могут быть закрыты и открыты неоднократно. Другие каналы могут или не могут поддерживать повторное открытие, после того как они были закрыты. В общем случае механизмы для открытия линий варьируются в соответствии с разными подтипами».

А теперь конец истории

Итак здесь мы дали объяснение как наша программа использует Java Sound API для того, чтобы обеспечить доставку аудио данных из внутренней памяти компьютера до звуковой карты.

Запускаем программу

Теперь вы можете скомпилировать и запустить программу из листинга 11, венчающую собой конец нашего урока.

Захват и проигрывание аудио данных

Программа демонстрирует возможность записи данных с микрофона и проигрывание их через звуковую карту вашего компьютера. Инструкции по её использованию очень просты.
Запускаем программу. Простой графический интерфейс пользователя ГИП, который показан на Рис.6, должен появиться на экране.

  • Кликните кнопку Capture и запишите какие-либо звуки на микрофон.
  • Кликните кнопку Stop, чтобы остановить запись.
  • Кликните кнопку Playback, чтобы проиграть запись через звуковой выход вашего компьютера.


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

Заключение

  • Мы выяснили, что Java Sound API основывается на концепции каналов и микшеров.
  • Мы получили начальную информацию о физических и электрических характеристиках аналогового звука, чтобы понять затем устройство аудио микшера.
  • Мы использовали сценарий любительского рок концерта с использованием шести микрофонов и двух стерео колонок, чтобы описать возможность использования аудио микшера.
  • Мы обсудили ряд тем по программированию Java Sound, включая микшеры, каналы, формат данных и прочее.
  • Мы объяснили себе общие взаимосвязи между объектами SourceDataLine, Clip, Mixer, AudioFormat и порты в простой программе вывода аудио данных.
  • Мы ознакомились с программой позволяющей нам первоначально записать, а затем проиграть аудио данные.
  • Мы получили детальное объяснение кода использованного для проигрывания аудио данных записанных предварительно в память компьютера.


Что дальше?

На этом уроке, мы выяснили, что Java Sound API основан на концепции микшеров и каналов. Однако код, который мы обсуждали, не включал микшеры явным образом. Класс AudioSystem обеспечивал нас статическими методами, которые делают возможным писать программы по обработке аудио без непосредственных обращений к микшерам. Другими словами эти статические методы убирают микшеры от нас на задний план.

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

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import javax.sound.sampled.*;

public class AudioCapture01
                        extends JFrame{

  boolean stopCapture = false;
  ByteArrayOutputStream
                 byteArrayOutputStream;
  AudioFormat audioFormat;
  TargetDataLine targetDataLine;
  AudioInputStream audioInputStream;
  SourceDataLine sourceDataLine;

  public static void main(
                        String args[]){
    new AudioCapture01();
  }//конец main

  public AudioCapture01(){
    final JButton captureBtn =
                new JButton("Capture");
    final JButton stopBtn =
                   new JButton("Stop");
    final JButton playBtn =
               new JButton("Playback");

    captureBtn.setEnabled(true);
    stopBtn.setEnabled(false);
    playBtn.setEnabled(false);

    captureBtn.addActionListener(
      new ActionListener(){
        public void actionPerformed(
                        ActionEvent e){
          captureBtn.setEnabled(false);
          stopBtn.setEnabled(true);
          playBtn.setEnabled(false);
          // Захват данных
          // с микрофона
          //пока не нажата Stop
          captureAudio();
        }
      }
    );
    getContentPane().add(captureBtn);

    stopBtn.addActionListener(
      new ActionListener(){
        public void actionPerformed(
                        ActionEvent e){
          captureBtn.setEnabled(true);
          stopBtn.setEnabled(false);
          playBtn.setEnabled(true);
          // Остановка захвата
          // информации с микрофона
       
          stopCapture = true;
        }
      }
    );
    getContentPane().add(stopBtn);

    playBtn.addActionListener(
      new ActionListener(){
        public void actionPerformed(
                        ActionEvent e){
          // Проигрывание данных
          // которые были записаны
         
          playAudio();
        }
      }
    );
    getContentPane().add(playBtn);

    getContentPane().setLayout(
                     new FlowLayout());
    setTitle("Capture/Playback Demo");
    setDefaultCloseOperation(
                        EXIT_ON_CLOSE);
    setSize(250,70);
    setVisible(true);
  }

  //Этот метод захватывает аудио
  // с микрофона и сохраняет 
  // в объект ByteArrayOutputStream 
  private void captureAudio(){
    try{
      //Установим все для захвата
  
      audioFormat = getAudioFormat();
      DataLine.Info dataLineInfo =
                new DataLine.Info(
                  TargetDataLine.class,
                   audioFormat);
      targetDataLine = (TargetDataLine)
                   AudioSystem.getLine(
                         dataLineInfo);
      targetDataLine.open(audioFormat);
      targetDataLine.start();

      //Создаем поток для захвата аудио
      // и запускаем его
      //он будет работать
      //пока не нажмут кнопку
      Thread captureThread =
                new Thread(
                  new CaptureThread());
      captureThread.start();
    } catch (Exception e) {
      System.out.println(e);
      System.exit(0);
    }
  }

  //Этот метод проигрывает аудио
  // данные, которые были сохранены
  // в ByteArrayOutputStream
  private void playAudio() {
    try{
      //Устанавливаем всё
      //для проигрывания
 
      byte audioData[] =
                 byteArrayOutputStream.
                         toByteArray();
 
      InputStream byteArrayInputStream
            = new ByteArrayInputStream(
                            audioData);
      AudioFormat audioFormat =
                      getAudioFormat();
      audioInputStream =
        new AudioInputStream(
          byteArrayInputStream,
          audioFormat,
          audioData.length/audioFormat.
                       getFrameSize());
      DataLine.Info dataLineInfo =
                new DataLine.Info(
                  SourceDataLine.class,
                          audioFormat);
      sourceDataLine = (SourceDataLine)
                   AudioSystem.getLine(
                         dataLineInfo);
      sourceDataLine.open(audioFormat);
      sourceDataLine.start();

      //Создаем поток для проигрывания 
      // данных и запускаем его
      // он будет работать пока
      // все записанные данные не проиграются
    
      Thread playThread =
          new Thread(new PlayThread());
      playThread.start();
    } catch (Exception e) {
      System.out.println(e);
      System.exit(0);
    }
  }

  //Этот метод создает и возвращает
  // объект AudioFormat 

  private AudioFormat getAudioFormat(){
    float sampleRate = 8000.0F;
    //8000,11025,16000,22050,44100
    int sampleSizeInBits = 16;
    //8,16
    int channels = 1;
    //1,2
    boolean signed = true;
    //true,false
    boolean bigEndian = false;
    //true,false
    return new AudioFormat(
                      sampleRate,
                      sampleSizeInBits,
                      channels,
                      signed,
                      bigEndian);
  }
//===================================//

//Внутренний класс для захвата
// данных с микрофона
class CaptureThread extends Thread{

  byte tempBuffer[] = new byte[10000];
  public void run(){
    byteArrayOutputStream =
           new ByteArrayOutputStream();
    stopCapture = false;
    try{
        
       
      while(!stopCapture){
    
        
        int cnt = targetDataLine.read(
                    tempBuffer,
                    0,
                    tempBuffer.length);
        if(cnt > 0){
          //Сохраняем данные в выходной поток
        
          byteArrayOutputStream.write(
                   tempBuffer, 0, cnt);
        }
      }
      byteArrayOutputStream.close();
    }catch (Exception e) {
      System.out.println(e);
      System.exit(0);
    }
  }
}
//===================================//
//Внутренний класс  для
// проигрывания сохраненных аудио данных
class PlayThread extends Thread{
  byte tempBuffer[] = new byte[10000];

  public void run(){
    try{
      int cnt;
           // цикл пока не вернется -1 
    
      while((cnt = audioInputStream.
        read(tempBuffer, 0,
            tempBuffer.length)) != -1){
        if(cnt > 0){
          //Пишем данные во внутренний
          // буфер канала
          // откуда оно передастся 
          // на звуковой выход
          sourceDataLine.write(
                   tempBuffer, 0, cnt);
        }
      }
     
      sourceDataLine.drain();
      sourceDataLine.close();
    }catch (Exception e) {
      System.out.println(e);
      System.exit(0);
    }
  }
}
//===================================//

}//конец внешнего класса AudioCapture01.java

Листинг 11

 

Предыдущая Следующая