02 - Java Sound API, Начало работы, Часть 2, Захват с помощью выбранного микшера.
Предисловие
Эта серия уроков предназначена для того, чтобы научить вас использовать Java Sound API. Первый урок из этой серии назывался Java Sound API, Введение. Предыдущий урок был озаглавлен Java Sound API, Начало работы, Часть 1, Воспроизведение. Этот урок, озаглавленный Java Sound, Начало Работы, Часть 2, Захват С Помощью Указанного Микшера, является продолжением предыдущего урока.
Два типа аудиоданных
Java Sound API поддерживает два разных типа аудиоданных:
- Выборочные аудиоданные
- Данные цифрового интерфейса музыкальных инструментов (MIDI)
Эти два типа аудиоданных очень разные. На данный момент я концентрируюсь на дискретизированных аудиоданных. Я отложу обсуждение MIDI на потом.
Совет по просмотру
Возможно, вам будет полезно открыть еще одну копию этого урока в отдельном окне браузера. Это упростит вам перемещение вперед и назад между различными списками и цифрами, пока вы читаете о них.
Общие сведения.
Java Sound API основан на концепции линий и микшеров. В этом уроке я предоставлю программу, которую вы можете использовать для первого захвата, а затем для воспроизведения звука.
В предыдущем уроке подробно обсуждался раздел воспроизведения программы. Однако этот урок не раскрыл возможности использования микшера. Скорее, в этом уроке использовались методы класса AudioSystem, которые абстрагируют использование микшеров до фона.
В этом уроке я подробно объясню код, используемый для захвата аудиоданных с микрофона. Несмотря на то, что в этом нет необходимости, в этом уроке также будет представлена спецификация конкретного микшера для захвата аудиоданных.
Обсуждение и образец кода
Микшеры
Вот часть того, что Sun говорит о микшере:
"Микшер - это аудиоустройство с одной или несколькими линиями. Он не должен быть предназначен для микширования аудиосигналов. Микшер, который фактически микширует аудио, имеет несколько входных (исходных) линий и по крайней мере одну выходную (целевую) линию. Первые часто являются экземплярами классов, реализующих SourceDataLine, а вторые - TargetDataLine. Объекты Port, также являются исходными или целевыми линиями. Микшер может принимать в качестве входных данных предварительно записанный, зацикленный звук, если некоторые из его исходных линий являются экземплярами объектов, реализующих интерфейс Clip."
Линии
Sun так говорит об интерфейсе Line:
"Линия - это элемент «конвейера» цифрового аудио, такой как порт аудиовхода или вывода, микшер или путь аудиоданных в микшер или из него. Аудиоданные, передаваемые по линии, могут быть монофоническими или многоканальными (например, стерео). ... Линяя может иметь элементы управления, такие как усиление, панорамирование и реверберация."
Некоторые важные термины
В приведенных выше цитатах Sun упоминаются следующие термины:
- TargetDataLine
- Mixer
- Port
- Controls
На рисунке 1 показано, как эти термины могут объединиться в простую систему аудиовхода.
Рис.1 Система аудиовхода
На рисунке 1 объект Mixer настроен с одним или несколькими портами, некоторыми элементами управления и объектом TargetDataLine.
Что такое TargetDataLine?
Используемая здесь терминология может сбивать с толку. Объект TargetDataLine - это выходной объект микшера потоковой передачи.
(Объект обеспечивает вывод из микшера, а не из программы. Фактически, он часто служит входом в программу.)
Объект этого типа доставляет аудиоданные из микшера, служа входными данными для других частей программы.
Ввод аудиоданных в программу
Данные, предоставленные объектом TargetDataLine, могут быть помещены в какую-либо другую конструкцию программы в реальном времени. Фактическим местом назначения аудиоданных может быть любое из множества мест назначения, например аудиофайл, сетевое соединение или буфер в памяти.
(Пример программы в этом уроке считывает аудиоданные из объекта TargetDataLine и записывает их в объект ByteArrayInputStream в памяти.)
По программе с предыдущего урока
На предыдущем уроке я представил и обсудил программу, которая захватывает аудиоданные с порта микрофона, сохраняет эти данные в памяти и воспроизводит захваченные данные обратно через порт динамика. На этом уроке подробно обсуждалась часть программы, связанная с воспроизведением, но не обсуждалась часть программы по захвату данных.
Немного измененная версия этой программы будет обсуждаться на этом уроке. Копия измененной программы показана в листинге 12 ближе к концу этого урока. На этом занятии подробно рассматривается часть программы по сбору данных.
Явный объект Mixer
Предыдущая версия программы не использовала явным образом объект Mixer. Следовательно, невозможно было увидеть, как концепция микшера вошла в программу. (Микшер использовался неявно, но не был идентифицирован как таковой.)
Хотя в этом не было необходимости для успешной работы программы, я модифицировал эту версию программы, чтобы показать явное использование объекта Mixer.
Графический интерфейс пользователя (GUI)
Большая часть этой программы посвящена созданию графического пользовательского интерфейса, который используется для управления работой программы. Поскольку этот код прост, я не буду обсуждать эти части программы.
Сторона программы сбора данных
Как упоминалось ранее, на предыдущем уроке я подробно рассказал о воспроизведении программы. В этом уроке я подробно расскажу о захвате данных в программе.
Как вы увидите позже, часть программы захвата данных захватывает аудиоданные с микрофона и сохраняет их в объекте ByteArrayOutputStream. Затем метод воспроизведения с именем playAudio (который обсуждался в предыдущем уроке) воспроизводит аудиоданные, которые хранятся в объекте ByteArrayOutputStream. Я внес очень мало изменений в сторону воспроизведения программы. Таким образом, обсуждения в предыдущем уроке должно быть достаточно для вашего понимания стороны воспроизведения программы.
Пользовательский интерфейс
Когда эта программа выполняется, на экране появляется графический интерфейс, показанный на рисунке 2. Как видите, этот графический интерфейс содержит три кнопки:
- Захватить
- Стоп
- Воспроизведение
Figure 2 Program GUI
Входные данные с микрофона захватываются и сохраняются в объекте ByteArrayOutputStream, когда пользователь нажимает кнопку Захватить.
Сбор данных останавливается, когда пользователь нажимает кнопку Стоп.
Воспроизведение захваченных данных начинается, когда пользователь нажимает кнопку Воспроизведение.
Доступные микшеры
Не все компьютеры имеют одинаковый набор микшеров. Эта версия программы отображает список микшеров, доступных на машине во время выполнения. Следующий список микшеров был создан при запуске программы на моей машине:
Java Sound Audio Engine
Microsoft Sound Mapper
Modem #0 Line Record
ESS Maestro
Таким образом, моя машина имела четыре перечисленных выше микшера, доступных во время запуска программы (ваш компьютер может отображать другой список микшеров)
Использование специального микшера
После отображения списка доступных микшеров программа получает и использует один из доступных микшеров из списка. Это отличается от версии программы, рассмотренной в предыдущем уроке. Эта версия программы просто запрашивала совместимый микшер, а не определяла конкретный микшер.
Некоторые микшеры работают, а некоторые нет
Я экспериментально определил, что любой из следующих микшеров может быть успешно использован в этой программе на моей машине:
Microsoft Sound Mapper
ESS Maestro
Я также экспериментально определил, что ни один из следующих микшеров не будет работать в этой программе на моей машине:
Java Sound Audio Engine
Modem #0 Line Record
Эти два микшера выходят из строя во время выполнения по разным причинам.
Микшер Java Sound Audio Engine вышел из строя из-за проблемы совместимости формата данных (возможно, эту ошибку можно было исправить, указав другой формат данных, но я не пробовал).
Модем #0 Сбой микшера линии записи из-за "Неожиданная ошибка."
Программа была протестирована с использованием Java SDK 1.4.1_01 под Win2000.
Обсудим по фрагментам
Те из вас, кто следит за моей работой, не удивятся, узнав, что я буду обсуждать эту программу по фрагментам. Полный список программы показан в листинге 12 ближе к концу урока.
Класс с именем AudioCapture02
Определение класса для управляющего класса начинается в листинге 1.
public class AudioCapture02 extends JFrame{ boolean stopCapture = false; ByteArrayOutputStream byteArrayOutputStream; AudioFormat audioFormat; TargetDataLine targetDataLine; AudioInputStream audioInputStream; SourceDataLine sourceDataLine;
Листинг 1
Переменные экземпляра
Код в листинге 1 объявляет несколько переменных экземпляра. Одна из этих переменных, названная stopCapture, управляет запуском и остановкой процесса сбора данных.
Значение этой переменной инициализируется значением false. Позже значение изменяется на true, когда пользователь нажимает кнопку Stop в графическом интерфейсе. Код в потоке сбора данных завершается, когда значение этой переменной изменяется на true.
Использование других переменных экземпляра, объявленных в листинге 1, станет очевидным по мере их появления в коде.
Метод с именем captureAudio
Теперь я собираюсь обсудить метод под названием captureAudio. Это метод, который вызывается, когда пользователь нажимает кнопку Capture в графическом интерфейсе. Этот метод захватывает входные аудиоданные с микрофона и сохраняет эти данные в объекте ByteArrayOutputStream для последующего воспроизведения.
Начало метода с именем captureAudio показано в листинге 2.
Показать доступные микшеры
Фрагмент кода в листинге 2 использует метод getMixerInfo класса AudioSystem для получения и отображения списка доступных микшеров в системе во время выполнения программы.
private void captureAudio(){ try{ Mixer.Info[] mixerInfo = AudioSystem.getMixerInfo(); System.out.println("Available mixers:"); for(int cnt = 0; cnt < mixerInfo.length; cnt++){ System.out.println(mixerInfo[cnt]. getName()); }//конец цикла
Листинг 2
Массив объектов Mixer.Info
Метод getMixerInfo заполняет и возвращает ссылку на объект массива, который содержит ссылки на объекты типа Mixer.Info. Каждый такой объект содержит информацию об одном из доступных микшеров. Свойство length объекта массива указывает количество доступных микшеров. Согласно Sun:
"Класс Mixer.Info представляет информацию о аудиомикшере, включая название продукта, версию и поставщика, а также текстовое описание. Эта информация может быть получена с помощью метода getMixerInfo интерфейса Mixer."
Я уже рассказал вам кое-что из того, что Sun говорила об интерфейсе Mixer ранее.
Показать список доступных микшеров
Код в листинге 2 также выполняет итерацию по объекту массива для отображения имени каждого микшера, доступного в настоящее время в системе. Как указывалось ранее, этот код при запуске программы на моем компьютере выдавал следующий экран, указывающий, что в настоящее время доступны четыре микшера:
Available mixers:
Java Sound Audio Engine
Microsoft Sound Mapper
Modem #0 Line Record
ESS Maestro
Ссылка на объект Mixer.Info, описывающий каждый из этих микшеров, содержится в объекте массива, на который ссылается ссылочная переменная с именем mixerInfo в листинге 2. Я буду использовать содержимое одного из элементов массива позже, чтобы выбрать конкретный микшер. для использования программой.
Формат аудиоданных
Для облегчения захвата аудиоданных требуется довольно много настроек. В листинге 3 начинается процесс настройки всего для захвата аудиоданных с микрофона.
Одна из требуемых вещей - это спецификация формата аудиоданных. Код в листинге 3 вызывает метод getAudioFormat для получения объекта типа AudioFormat и сохранения его ссылки в переменной экземпляра с именем audioFormat.
audioFormat = getAudioFormat();
Листинг 3
Метод с именем getAudioFormat
На этом этапе я кратко рассмотрю метод под названием audioFormat. (Это обсуждение будет кратким, потому что я обсуждал этот метод на предыдущем уроке.)
Весь метод getAudioFormat показан в листинге 4.
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); }//конец getAudioFormat
Листинг 4
Помимо объявления некоторых инициализированных переменных, код в листинге 3 состоит из одного исполняемого оператора.
Объект AudioFormat
Метод getAudioFormat создает и возвращает объект класса AudioFormat.
(Я не верю, что есть какая-либо гарантия, что данный набор параметров аудиоформата будет работать во всех системах, поскольку системная звуковая карта является частью процесса. Существует множество марок и типов системных звуковых карт. Если эти параметры формата не работают для вас, попробуйте некоторые из других допустимых значений параметров, которые представлены ниже.)
Что говорит Sun ?
Вот часть того, что Sun говорит о классе AudioFormat:
"AudioFormat - это класс, который определяет конкретное расположение данных в звуковом потоке. Изучая информацию, хранящуюся в аудиоформате, вы можете узнать, как интерпретировать биты в двоичных звуковых данных."
Доступны два конструктора
Класс AudioFormat имеет два конструктора. (Я решил использовать более простой из двух.) Для этого конструктора требуются следующие параметры:
- Частота дискретизации в отсчетах в секунду. (Допустимые значения включают 8000, 11025, 16000, 22050 и 44100 выборок в секунду.)
- Размер выборки в битах. (Допустимые значения включают 8 и 16 бит на выборку.)
- Количество каналов. (Допустимые значения включают 1 канал для моно и 2 канала для стерео.)
- Подписанные или неподписанные данные. (Допустимые значения включают истину и ложь для подписанных данных или беззнаковых данных.)
- Порядок с прямым или обратным порядком байтов. (Это связано с порядком, в котором байты данных хранятся в памяти. Вы можете узнать об этой теме здесь.)
Как вы можете видеть в листинге 4, этот метод определяет следующие параметры для нового объекта AudioFormat:
- 8000 выборок в секунду
- 16 бит на выборку
- 1 канал (моно)
- Подписанные данные
- Порядок с прямым порядком байтов
Кодировка данных по умолчанию - linear PCM.
Есть несколько способов кодирования двоичных аудиоданных в доступные биты. Самый простой способ известен как линейный PCM. Конструктор, который я использовал, создает объект AudioFormat с линейным кодированием PCM и параметрами, перечисленными выше (я расскажу больше о линейном кодировании PCM и других схемах кодирования в будущих уроках).
Теперь вернемся к методу captureAudio.
После установки формата аудиоданных следующим шагом будет получение объекта типа DataLine.Info, как показано в листинге 5.
DataLine.Info dataLineInfo = new DataLine.Info( TargetDataLine.class, audioFormat);
Листинг 5
Класс DataLine.Info расширяет класс Line.Info. Давайте начнем наше расследование с того, что посмотрим на то, что Sun говорит о классе Line.Info.
"Объект Line.Info содержит информацию о линии. Единственная информация, предоставляемая самой Line.Info, - это Java-класс линии. Подкласс Line.Info добавляет другие виды информации о линии. Эта дополнительная информация зависит от того, какой субинтерфейс Line реализуется типом линии, описываемой подклассом Line.Info."
Код в листинге 5 создает экземпляр нового объекта класса DataLine.Info, который является одним из подклассов Line.Info.
DataLine.Info класс
Вот часть того, что Sun говорит о классе DataLine.Info:
"Помимо информации о классе, унаследованной от его суперкласса, DataLine.Info предоставляет дополнительную информацию, специфичную для линии данных. Эта информация включает:
- аудиоформаты, поддерживаемые линией данных
- минимальный и максимальный размеры его внутреннего буфера
Поскольку Line.Info знает класс линии, которую он описывает, объект DataLine.Info может описывать субинтерфейсы DataLine, такие как SourceDataLine, TargetDataLine и Clip. Вы можете запросить в микшере линии любого из этих типов, передав соответствующий экземпляр DataLine.Info в качестве аргумента такому методу, как Mixer.getLine (Line.Info)."
Конструктор DataLine.Info
Для объекта DataLine.Info доступны три перегруженных конструктора. Два из них позволяют указать информацию о размере буфера. Я решил использовать самый простой из трех, который не требует указания информации о буфере, но вместо этого использует размеры буфера по умолчанию.
По словам Sun, конструктор, который я решил использовать:
"Создает информационный объект линии данных из указанной информации, которая включает единственный аудиоформат."
Обратите внимание на два параметра, переданных конструктору для нового объекта DataLine.Info в листинге 5. Как вы можете видеть, экземпляр объекта DataLine.Info, созданный в листинге 5, описывает линию типа TargetDataLine с форматом, который был указан ранее.
Что такое TargetDataLine?
TargetDataLine - это подчиненный интерфейс DataLine, который, в свою очередь, является подчиненным интерфейсом Line. Поэтому, прежде чем углубляться в детали TargetDataLine, нам нужно взглянуть на интерфейс DataLine.
Интерфейс DataLine
Вот часть того, что Sun говорит об интерфейсе DataLine:
"DataLine добавляет функции, связанные с мультимедиа, в свой суперинтерфейс Line. Эта функциональность включает методы управления транспортом, которые запускают, останавливают, осушают и очищают аудиоданные, проходящие по линии."
Например, на стороне воспроизведения этой программы используется метод drain, чтобы убедиться, что внутренний буфер линии пуст перед закрытием линии.
"Линии данных используются для вывода звука с помощью субинтерфейсов SourceDataLine или Clip, которые позволяют прикладной программе записывать данные. Точно так же аудиовход обрабатывается подинтерфейсом TargetDataLine, который позволяет считывать данные."
Эта цитата из Sun представляет для нас особый интерес, поскольку мы будем использовать TargetDataLine для захвата входных аудиоданных с микрофона.
Есть несколько других интересных аспектов интерфейса DataLine, которые мы будем использовать в будущих уроках. Поэтому я не буду их обсуждать на этом уроке.
Интерфейс TargetDataLine
На рисунке 5 показан объект DataLine.Info, описывающий линию типа TargetDataLine. Вот часть того, что Sun говорит об интерфейсе TargetDataLine:
"Целевая линия данных - это тип DataLine, из которого могут быть прочитаны аудиоданные. Самый распространенный пример - линия данных, которая получает данные от устройства захвата звука. (Устройство реализовано как микшер, который записывает в целевую линию данных.)"
Мы обсуждаем код в этой программе, который захватывает аудиоданные с микрофона. В соответствии с приведенной выше цитатой комбинацию микрофона и микшера можно рассматривать как устройство захвата звука, которое захватывает аудиоданные с микрофона и записывает эти данные в объект TargetDataLine. Позже вы увидите код, который считывает аудиоданные из объекта TargetDataLine и передает их в объект ByteArrayOutputStream.
Запутанная терминология
Очень важно соблюдать четкое соглашение об именах, потому что оно может быть прямо противоположным тому, что вы ожидаете. Вот что Sun говорит о соглашении об именах для интерфейса TargetDataLine:
"Обратите внимание, что соглашение об именах для этого интерфейса отражает отношения между линией и ее микшером. С точки зрения приложения, целевая линия данных может действовать как источник аудиоданных."
Точно так же Sun говорит об интерфейсе SourceDataLine в отношении соглашения об именах:
"С точки зрения приложения линия исходных данных может действовать как цель для аудиоданных."
Цель - это источник, а источник - это цель.
Вы еще не запутались?
С точки зрения приложения (в отличие от точки зрения микшера) TargetDataLine является источником аудиоданных (например, данных, захваченных с микрофона).
С точки зрения приложения (в отличие от точки зрения микшера) SourceDataLine является целью для аудиоданных (например, динамика).
Получение объекта TargetDataLine
Sun говорит нам:
"Целевая линия данных может быть получена из микшера путем вызова метода getLine класса Mixer с соответствующим объектом DataLine.Info."
Именно этим мы и займемся позже.
Внутренний буфер ...
Объект TargetDataLine имеет внутренний буфер, который используется для временного хранения входных аудиоданных, пока они не будут прочитаны приложением. У Sun есть несколько предостережений относительно использования этого буфера:
"Интерфейс TargetDataLine предоставляет метод для чтения захваченных данных из буфера целевой линии данных. Приложения, которые записывают звук, должны считывать данные из целевой линии данных достаточно быстро, чтобы не допустить переполнения буфера, что может вызвать прерывания в захваченных данных, которые будут восприниматься как щелчки. ... Если буфер переполняется, самые старые данные в очереди отбрасываются и заменяются новыми данными."
Надеюсь, ваш компьютер будет достаточно быстрым, чтобы захватывать входные аудиоданные с микрофона без переполнения буфера. Мой компьютер не особенно быстрый, и, кажется, он без проблем собирает данные со скоростью 8000 выборок в секунду.
Выбор доступного микшера
Как я и обещал ранее, эта программа выберет один из доступных микшеров, чего не было в версии программы, обсуждаемой в предыдущем уроке. Более ранняя версия программы просто запрашивала доступ к совместимому микшеру без указания какого-либо конкретного микшера.
Кроме того, как я упоминал ранее, я экспериментально определил, что только два из четырех доступных микшеров на моей машине будут работать в этой программе.
Массив данных Mixer.Info
Ранее в программе мы создали и заполнили объект массива, элементы которого относятся к объектам Mixer.Info, которые описывают четыре доступных микшера на моей машине (ваша машина может содержать разные микшеры). Код в листинге 6 получает ссылку на объект Mixer, описанный объектом Mixer.Info в индексе 3 массива.
Mixer mixer = AudioSystem. getMixer(mixerInfo[3]);
Листинг 6
Эксперимент грубой силы
Просто перекомпилировав и запустив программу с разными значениями индексов в листинге 6, я определил два микшера, которые будут работать на моей машине, как те, чье описание было сохранено в индексе 1 и индексе 3 в массиве. Однако на вашем компьютере это может быть не так. Возможно, вам придется провести аналогичный эксперимент для определения совместимых микшеров.
(Есть более элегантные способы определения совместимых микшеров, но я решил сделать это грубой силой сейчас и обсудить более элегантные способы на будущих уроках.)
После выполнения кода из листинга 6 переменная с именем mixer содержит ссылку на объект типа Mixer, описанный как ESS Maestro на моей машине.
(Как бы то ни было, звуковая подсистема на моем компьютере описана в свойствах оборудования как ESS Maestro2E MPU-401 Compatible.)
Получить объект TargetDataLine
Теперь, когда у нас есть микшер, следующее, что нам нужно сделать, это получить линию. Код в листинге 7 вызывает метод getLine для объекта Mixer, чтобы получить объект TargetDataLine.
targetDataLine = (TargetDataLine) mixer.getLine(dataLineInfo); targetDataLine.open(audioFormat); targetDataLine.start();
Листинг 7
Согласно Sun, для метода getLine интерфейса Mixer требуется входящий параметр типа Line.Info. Метод:
"Получает линию, доступную для использования и соответствующую описанию в указанном объекте Line.Info."
Как вы помните из листинга 5, наш объект Line.Info описывает TargetDataLine. Код в листинге 7 передает объект Line.Info, который мы создали ранее в листинге 5, в качестве параметра методу getLine.
Метод getLine возвращает ссылку на объект типа Line. Мы должны уменьшить его до типа TargetDataLine, чтобы использовать его.
Подготовьте линию к использованию
Когда у нас есть объект TargetDataLine, необходимо выполнить еще пару шагов, чтобы подготовить его к использованию. Код в листинге 7 вызывает метод open объекта линии, передавая наш объект формата в качестве параметра. По словам Sun, эта версия метода open
"Открывает линию с указанным форматом, в результате чего линия получает все необходимые системные ресурсы и становится работоспособной."
Доступны две перегруженные версии метода open. Версия, которую я решил использовать, выбирает размер буфера автоматически. Другая версия требует, чтобы программист указал размер буфера.
Вызвать метод start
Код в листинге 7 также вызывает метод start объекта TargetDataLine. По словам Sun, метод start
"Позволяет линии заниматься вводом-выводом данных."
На этом этапе аудиосистема начинает собирать данные с микрофона, сохранять их во внутреннем буфере и делать доступными для программы.
Не допускайте переполнения внутреннего буфера
Программа должна начать чтение данных из этого внутреннего буфера очень быстро, иначе внутренний буфер может переполниться. Как обсуждалось ранее, программа должна продолжать считывать данные из внутреннего буфера с достаточно высокой скоростью, чтобы внутренний буфер не переполнялся.
Захват некоторых аудиоданных
На этом этапе мы, наконец, подготовили все необходимое, чтобы сделать возможным получение аудиоданных с микрофона. Следующим шагом является создание объекта Thread (для захвата и сохранения данных) и запуск потока.
Код в листинге 8 создает объект класса CaptureThread и запускает его работу.
Thread captureThread = new CaptureThread(); captureThread.start();
Листинг 8
Продолжать работать до Stop
Этот поток будет продолжать работать и экономить аудио данные до тех пор, пока пользователь не нажимает кнопку Stop.
(Однако обратите внимание, что захваченные данные сохраняются в памяти. Если вы позволите ему захватывать слишком много данных, у вас может закончиться память.)
Если вы изучите код в листинге 12 ближе к концу урока, вы увидите, что, за исключением блока catch, это конец метода с именем captureAudio. Код в блоке catch очень прост, поэтому я не буду его здесь обсуждать.
Класс CaptureThread
В листинге 9 показано начало класса CaptureThread. Этот класс расширяет класс Thread и используется для чтения данных из внутреннего буфера линии. Аудиоданные, считанные из этого буфера, сохраняются в объекте типа ByteArrayOutputStream.
class CaptureThread extends Thread{ byte tempBuffer[] = new byte[10000];
Листинг 9
Класс CaptureThread объявляет одну переменную экземпляра, которая ссылается на объект массива типа byte. Этот объект используется как промежуточный буфер в процессе перемещения аудиоданных из внутреннего буфера линии в объект ByteArrayOutputStream. Размер этого массива был установлен довольно произвольно - 10 000 байт.
Параллельная работа
Поскольку объект этого класса является потоком Thread, он выполняется одновременно с другими потоками в программе. Таким образом, он выполняется одновременно с потоком, который обрабатывает события, возникающие в результате нажатия кнопок в графическом интерфейсе.
Метод run
Каждый класс Thread должен определять метод с именем run, который определяет поведение потока. Начало метода run для класса CaptureThread показано в листинге 10.
public void run(){ byteArrayOutputStream = new ByteArrayOutputStream(); stopCapture = false;
Листинг 10
Код в методе run (листинг 10) начинается с создания экземпляра нового объекта ByteArrayOutputStream и сохранения ссылки на этот объект в переменной экземпляра с именем byteArrayOutputStream.
Если вы используете кнопки графического интерфейса для многократного цикла этой программы через цикл Capture/Stop/Playback, новый объект ByteArrayOutputStream будет создаваться и использоваться каждый раз, когда вы нажимаете кнопку Capture.
Управляющая переменная с именем stopCapture
Самая интересная вещь в листинге 10 - это инициализация логической boolean переменной экземпляра (с именем stopCapture) значением false. Эта переменная используется для управления продолжительностью захвата аудиоданных. Его значение переключается с false на true обработчиком событий, когда пользователь нажимает кнопку Stop.
Как вы вскоре увидите, когда значение stopCapture переключается на true, процесс захвата аудиоданных прекращается.
(Чтобы упростить обсуждение, я опущу код обработки исключений в методе run. Этот код прост, и вы можете просмотреть его в листинге 12 ближе к концу урока.)
Оставшийся код в методе run
Помимо кода обработки исключений, оставшийся код в методе run показан в листинге 11.
while(!stopCapture){ // Считать данные из внутреннего буфера // линии данных. int cnt = targetDataLine.read(tempBuffer, 0, tempBuffer.length); if(cnt > 0){ //Сохранить данные в объекте потока вывода. byteArrayOutputStream.write(tempBuffer, 0, cnt); }//конец if }//конец while byteArrayOutputStream.close(); }//конец run }//конец внутреннего класса CaptureThread
Листинг 11
Код в методе run выполняет цикл и захват аудиоданных до тех пор, пока обработчик событий на кнопке Stop не переключит значение stopCapture с false на true.
Вот что происходит во время каждой итерации цикла while.
Получить аудиоданные из внутреннего буфера TargetDataLine
Метод read вызывается для объекта TargetDataLine при попытке прочитать достаточно байтов из внутреннего буфера этого объекта, чтобы заполнить объект массива, на который ссылается tempBuffer.
(Не всегда возможно прочитать такое количество байтов из внутреннего буфера линии. Во внутреннем буфере может быть не так много байтов аудиоданных.)
Метод read передает доступные байты из внутреннего буфера в массив, предоставленный как входящий параметр.
(Если количество доступных байтов во внутреннем буфере превышает размер массива, передается только количество, необходимое для заполнения массива. Лишние байты остаются во внутреннем буфере и доступны для следующей метод read.)
Затем метод read возвращает количество байтов, фактически переданных как тип int. Это значение сохраняется в переменной cnt.
Перенести данные в объект ByteArrayOutputStream
Затем код метода run в листинге 11 вызывает метод write объекта ByteArrayOutputStream для передачи байтов из массива, на который ссылается tempBuffer, объекту потока, на который указывает byteArrayOutputStream.
Вернуться к началу цикла
Затем управление переходит обратно в начало цикла while, где значение stopCapture снова проверяется.
Если значение stopCapture по-прежнему равно false, это означает, что пользователь не нажал кнопку Stop. Процесс повторяется на следующей итерации.
Однако, если пользователь нажал кнопку Stop, в результате чего значение stopCapture изменилось с false на true, цикл завершается. В этом случае объект ByteArrayOutputStream закрывается, а метод run завершается. Это приводит к естественной смерти потока и прекращает захват аудиоданных.
Разъяснение
Однако обратите внимание, что я не вызывал метод stop для объекта TargetDataLine. В результате линия будет продолжать получать аудиоданные и делать эти данные доступными во внутреннем буфере, пока программа не завершится.
Поскольку метод run потока прекратил чтение байтов из внутреннего буфера линии, буфер просто переполнится. Должна быть возможность перезапустить линию и прочитать аудиоданные, полученные в течение интервала. Однако эта программа не была разработана для поддержки такого рода операций.
Если вас беспокоит, что TargetDataLine продолжает потреблять ресурсы, вы можете вставить следующий оператор в листинг 11 сразу после окончания цикла while:
targetDataLine.stop();
Согласно Sun, вызов метода stop на линии:
"Останавливает линию. Остановленная линия должна прекратить операции ввода-вывода."
Что такое объект ByteArrayOutputStream?
Согласно Sun, класс ByteArrayOutputStream
"реализует выходной поток, в котором данные записываются в массив байтов. Буфер автоматически увеличивается по мере записи в него данных."
Должна быть возможна нехватка памяти, если будет сделана попытка записать слишком много данных в массив байтов. Однако документация Sun для SDK 1.4.1_01 не сообщает нам, что происходит в этом случае. Будем надеяться, что возникнет ошибка OutOfMemory, которая приведет к аварийному завершению этой программы.
Подведем итоги
Давайте вспомним шаги, связанные с захватом аудиоданных с микрофона и сохранением этих данных в объекте ByteArrayOutputStream, как это реализовано в этой программе.
- Определяет доступные микшеры и сохраните объект Mixer.Info, описывающий совместимый микшер.
- Создаёт экземпляр объекта AudioFormat, который задает конкретное расположение байтов аудиоданных в звуковом потоке. Здесь доступно множество вариантов.
- Создаёт экземпляр объекта DataLine.Info, который описывает объект типа TargetDataLine, настроенный для AudioFormat, описанного выше.
- Вызывает метод getMixer класса AudioSystem, чтобы получить ссылку на объект Mixer, который соответствует сохраненному ранее объекту Mixer.Info.
- Вызывает метод getLine для объекта Mixer, чтобы получить объект TargetDataLine, который соответствует характеристикам объекта DataLine.Info, созданного ранее.
- Вызывает метод open объекта TargetDataLine, передав объект AudioFormat в качестве параметра.
- Вызывает метод start для объекта TargetDataLine, чтобы линия начала сбор данных с микрофона.
- Создаёт и запускает объект Thread для передачи аудиоданных в реальном времени из внутреннего буфера объекта TargetDataLine в объект ByteArrayOutputStream.
- Когда соответствующий объем аудиоданных был захвачен, заставляет объект Thread прекратить передачу данных из объекта TargetDataLine в объект ByteArrayOutputStream.
- При необходимости вызывает метод stop для объекта TargetDataLine, чтобы он прекратил получение аудиоданных с микрофона.
Обратите внимание, что не всегда необходимо явно указывать микшер, как это было сделано в этой программе. Аналогичная программа из предыдущего урока просто вызвала метод getLine класса AudioSystem, чтобы получить объект TargetDataLine для определенного формата данных на совместимом микшере. Я решил явно указать микшер в этой программе только для иллюстрации.
Запуск программы
На этом этапе вы можете найти полезным скомпилировать и запустить программу, показанную в листинге 12, ближе к концу урока.
Захват и воспроизведение аудиоданных
Эта программа демонстрирует возможность захвата аудиоданных с микрофона и их воспроизведения через динамики вашего компьютера. Инструкции по использованию просты:
- Запустите программу. На экране появится простой графический интерфейс, показанный на рисунке 2.
- Нажмите кнопку Capture и говорите в микрофон.
- Нажмите кнопку Stop, чтобы прекратить сбор данных.
- Нажмите кнопку Playback , чтобы воспроизвести записанный голос через динамики системы.
Если вы ничего не слышите во время воспроизведения, возможно, вам нужно увеличить громкость динамика.
Эта программа сохраняет захваченные данные в памяти, поэтому будьте осторожны, чтобы избежать нехватки памяти.
Резюме
В этом уроке я показал вам, как использовать Java Sound API для захвата аудиоданных с микрофона и как сохранить эти данные в объекте ByteArrayOutputStream. Я также показал вам, как идентифицировать микшеры, доступные в вашей системе, и как указать конкретный микшер для использования при получении аудиоданных с микрофона.
Полный листинг программы
Полный список программы показан в листинге 12.
/*File AudioCapture02.java Эта программа демонстрирует захват и последующее воспроизведение аудиоданных. На экране появится графический интерфейс со следующими кнопками: Захватить Стоп Воспроизведение Входные данные с микрофона захватываются и сохраняются в объекте ByteArrayOutputStream, когда пользователь нажимает кнопку Захватить. Сбор данных останавливается, когда пользователь нажимает кнопку Стоп. Воспроизведение начинается, когда пользователь нажимает кнопку воспроизведения. Эта версия программы получает и отображает список доступных микшеров, производя следующий вывод: Доступные микшеры: Java Sound Audio Engine Microsoft Sound Mapper Modem #0 Line Record ESS Maestro Таким образом, на момент выполнения программы у этой машины были четыре перечисленных выше микшера. Затем программа получает и использует один из доступных микшеров вместо того, чтобы просто запрашивать совместимый микшер, как это было в предыдущей версии программы. В этой программе можно использовать любой из следующих двух микшеров: Microsoft Sound Mapper ESS Maestro Ни один из следующих двух микшеров не будет работать в этой программе. Микшеры выходят из строя во время выполнения по разным причинам: Java Sound Audio Engine Modem #0 Line Record Микшер Java Sound Audio Engine не работает из-за проблемы совместимости формата данных. Микшер линейной записи модема №0 выходит из строя из-за
"Неожиданная ошибка" Протестировано с использованием SDK 1.4.0 под Win2000 ************************************************/ import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.io.*; import javax.sound.sampled.*; public class AudioCapture02 extends JFrame{ boolean stopCapture = false; ByteArrayOutputStream byteArrayOutputStream; AudioFormat audioFormat; TargetDataLine targetDataLine; AudioInputStream audioInputStream; SourceDataLine sourceDataLine; public static void main(String args[]){ new AudioCapture02(); }//конец main public AudioCapture02(){//конструктор 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); // Захват входных данных с микрофона, // пока не будет нажата кнопка // Стоп. captureAudio(); }//конец actionPerformed }//конец ActionListener );//конец addActionListener() getContentPane().add(captureBtn); stopBtn.addActionListener( new ActionListener(){ public void actionPerformed( ActionEvent e){ captureBtn.setEnabled(true); stopBtn.setEnabled(false); playBtn.setEnabled(true); // Прекратить захват входных данных // с микрофона. stopCapture = true; }//конец actionPerformed }//конец ActionListener );//конец addActionListener() getContentPane().add(stopBtn); playBtn.addActionListener( new ActionListener(){ public void actionPerformed( ActionEvent e){ // Воспроизведите все данные, которые // были сохранены во время захвата. playAudio(); }//конец actionPerformed }//конец ActionListener );//конец addActionListener() 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{ // Получить и отобразить список // доступных микшеров. Mixer.Info[] mixerInfo = AudioSystem.getMixerInfo(); System.out.println("Available mixers:"); for(int cnt = 0; cnt < mixerInfo.length; cnt++){ System.out.println(mixerInfo[cnt]. getName()); }// конец цикла // Подготовить все для захвата audioFormat = getAudioFormat(); DataLine.Info dataLineInfo = new DataLine.Info( TargetDataLine.class, audioFormat); // Выберите один из доступных // микшеров. Mixer mixer = AudioSystem. getMixer(mixerInfo[3]); // Получить TargetDataLine на выбранном // микшере. targetDataLine = (TargetDataLine) mixer.getLine(dataLineInfo); // Подготовить линию к использованию. targetDataLine.open(audioFormat); targetDataLine.start(); // Создать поток, чтобы захватить данные микрофона // и запустить его. Он будет работать, // пока не будет нажата кнопка «Стоп». Thread captureThread = new CaptureThread(); captureThread.start(); } catch (Exception e) { System.out.println(e); System.exit(0); }//конец catch }//конец метода captureAudio // Этот метод воспроизводит аудиоданные, // сохраненные в 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 PlayThread(); playThread.start(); } catch (Exception e) { System.out.println(e); System.exit(0); }//конец catch }//конец playAudio // Этот метод создает и возвращает объект 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); }//конец getAudioFormat //=============================================// // Внутренний класс для захвата данных с микрофона class CaptureThread extends Thread{ // Буфер временного хранения произвольного размера byte tempBuffer[] = new byte[10000]; public void run(){ byteArrayOutputStream = new ByteArrayOutputStream(); stopCapture = false; try{// Цикл до тех пор, пока stopCapture // не будет установлен другим потоком, // который обслуживает кнопку Stop. while(!stopCapture){ // Считайте данные из внутреннего буфера // линии данных. int cnt = targetDataLine.read(tempBuffer, 0, tempBuffer.length); if(cnt > 0){ // Сохраните данные в объекте потока вывода. byteArrayOutputStream.write(tempBuffer, 0, cnt); }//конец if }//конец while byteArrayOutputStream.close(); }catch (Exception e) { System.out.println(e); System.exit(0); }//конец catch }//конец run }//конец внутреннего класса CaptureThread //===================================// // Внутренний класс для воспроизведения // сохраненных данных. class PlayThread extends Thread{ byte tempBuffer[] = new byte[10000]; public void run(){ try{ int cnt; // Продолжайте цикл до тех пор, пока метод // read ввода не вернет -1 для пустого потока. while((cnt = audioInputStream.read( tempBuffer, 0, tempBuffer.length)) != -1){ if(cnt > 0){ // Записать данные во внутренний буфер // линии данных, откуда они будут // доставлены говорящему. sourceDataLine.write(tempBuffer,0,cnt); }//конец if }//конец while // Заблокируйте и дождитесь, пока внутренний буфер // линии данных опустеет. sourceDataLine.drain(); sourceDataLine.close(); }catch (Exception e) { System.out.println(e); System.exit(0); }//конец catch }//конец run }//конец внутреннего класса PlayThread //=============================================// }//конец внешнего класса AudioCapture02.java
Листинг 12
Предыдущая | Следующая |