Звук Как было указано в предыдущей главе, в апплетах реализуется интерфейс Audioclip. Экземпляр объекта, реализующего этот интерфейс можно получить методом getAudioClip(), который, кроме того, загружает звуковой файл, а затем пользоваться методами play о, loop о и stop о этого интерфейса для проигрывания музыки. Для применения данного же приема в приложениях в класс Applet введен статический метод newAudioclp(URL address), загружающий звуковой файл, находящийся по адресу address, и возвращающий объект, реализующий интерфейс Audioclip. Его можно использовать для проигрывания звука в приложении, если конечно звуковая система компьютера уже настроена. В листинге 15.14 приведено простейшее консольное приложение, бесконечно проигрывающее звуковой файл doom.mid, находящийся в текущем каталоге. Для завершения приложения требуется применить средства операционной системы, например, комбинацию клавиш <Ctrl>+<C>. Листинг 15.14. Простейшее аудиоприложение import j ava.applet.* ; import j ava.net.*; class SimpleAudio{ SimpleAudio () { try{ AudioClip ac = Applet.newAudioClip(new URL("file:doom.mid")); ac.loop(); }catch(Exception e){} } public static void main(String[] args){ new SimpleAudio(); } } Таким способом можно проигрывать звуковые файлы типов AU, WAVE, AIFF, MIDI без сжатия. В состав виртуальной машины Java, входящей в SUN J2SDK начиная с версии 1.3, включено устройство, проигрывающее звук, записанный в одном из форматов AU, WAVE, AIFF, MIDI, преобразующее, микширующее и записывающее звук в тех же форматах. Для работы с этим устройством созданы классы, собранные в пакеты javax.sound.sampled, javax.sound.midi, javax.sound.sampled.spi и javax.sound.midi.spi. Перечисленный набор классов для работы со звуком получил название Java Sound API. Проигрывание звука в Java 2 Проигрыватель звука, встроенный в JVM, рассчитан на два способа записи звука: моно и стерео оцифровку (digital audio) с частотой дискретизации (sample rate) от 8 000 до 48 000 Гц и аппроксимацией (quantization) 8 и 16 битов, и MIDIпоследовательности (sequences) типа 0 и 1. Оцифрованный звук должен храниться в файлах типа AU, WAVE и AIFF. Его можно проигрывать двумя способами. Первый способ описан в интерфейсе clip. Он рассчитан на воспроизведение небольших файлов или неоднократное проигрывание файла и заключается в том, что весь файл целиком загружается в оперативную память, а затем проигрывается. Второй способ описан в интерфейсе SourceDataLine. Согласно этому способу файл загружается в оперативную память по частям в буфер, размер которого можно задать произвольно. Перед загрузкой файла надо задать формат записи звука в объекте класса AudioFormat. Конструктор этого класса: AudioFormat(float sampleRate, int sampleSize, int channels, boolean signed, boolean bigEndian) требует знания частоты дискретизации sampleRate (по умолчанию 44 100 Гц), аппроксимации sampleSize, заданной в битах (по умолчанию 16), числа каналов channels (1 — моно, по умолчанию 2 — стерео), запись чисел со знаком, signed == true, или без знака, и порядка расположения байтов в числе bigEndian. Такие сведения обычно неизвестны, поэтому их получают косвенным образом из файла. Это осуществляется в два шага. На первом шаге получаем формат файла статическим методом getAudioFiieFormato класса AudioSystem, на втором — формат записи звука методом getFormato класса AudioFiieFormat. Это описано в листинге 15.15. После того как формат записи определен и занесен в объект класса AudioFormat, в объекте класса DataLine. infо собирается информация о входной линии (line) и способе проигрывания clip или SourceDataLine. Далее следует проверить, сможет ли проигрыватель обслуживать линию с таким форматом. Затем надо связать линию с проигрывателем статическим методом getLine () класса AudioSystem. Потом создаем поток данных из файла — объект класса Audioinputstream. Из этого потока тоже можно извлечь объект класса AudioFormat методом getFormat (). Данный вариант выбран в листинге 15.16. Открываем созданный поток методом орепо. У-фф! Все готово, теперь можно начать проигрывание методом start (), завершить методом stop(), "перемотать" в начало методом setFramePosition(0) ИЛИ setMillisecondPosition(0). Можно задать проигрывание п раз подряд методом loop(n) или бесконечное число раз методом loop (Clip.LOOP_CONTINUOUSLY) . Перед этим необходимо установить начальную n и конечную m позиции повторения методом setLoopPoints(n, m). По окончании проигрывания следует закрыть линию методом close (). Вся эта последовательность действий показана в листинге 15.15. Листинг 15.15. Проигрывание аудиоклипа import javax.sound.sampled.*; import java.io.*; class PlayAudio{ PlayAudio(String s){ play(s); } public void play(String file){ Clip line = null; try{ // Создаем объект, представляющий файл File f = new File (file); // Получаем информацию о способе записи файла AudioFileFormat aff = AudioSystem.getAudioFileFormat(f); // Получаем информацию о способе записи звука AudioFormat af = aff.getFormat(); // Собираем всю информацию вместе, // добавляя сведения о классе Class DataLine.Infо info = new DataLine.Info(Clip.class, af) ; // Проверяем, можно ли проигрывать такой формат if (!AudioSystem.isLineSupported(info)){ System.err.printlnt"Line is not supported"); System.exit(0); } // Получаем линию связи с файлом line = (Clip)AudioSystem.getLine(info); // Создаем поток байтов из файла AudioInputStream ais - AudioSystem.getAudioInputStream(f); // Открываем линию line.open(ais); }catch(Exception e){ System.err.println(e); } // Начинаем проигрывание line.start(); // Здесь надо сделать задержку до окончания проигрывания // или остановить его следующим методом: line.stop(); //По окончании проигрывания закрываем линию line.close(); } public static void main(String[] args){ if (args.length != 1) System.out.printlnt"Usage: Java PlayAudio filename"); new PlayAudio(args[0]); } } Как видите, методы Java Sound API выполняют элементарные действия, которые надо повторять из программы в программу. Как говорят, это методы "низкого уровня" (low level). Второй способ, использующий методы интерфейса SourceDataLine, требует предварительного создания буфера произвольного размера. Листинг 15.16. Проигрывание аудиофайла import javax.sound.sampled.*; import j ava.io.*; class PlayAudioLine( PlayAudioLine(String s){ play(s); } public void play(String file){ SourceDataLine line = null; AudioInputStream ais = null; byte[] b = new byte[2048]; // Буфер данных try{ File f = new File(file); // Создаем входной поток байтов из файла f ais = AudioSystem.getAudioInputStream(f); // Извлекаем из потока информацию о способе записи звука AudioFormat af = ais.getFormat () ; // Заносим эту информацию в объект info DataLine.Infо info = new DataLine.Infо(SourceDataLine.class, af); // Проверяем, приемлем ли такой способ записи звука if (!AudioSystem.isLineSupported(info)){ System.err.println("Line is not supported"); System.exit(0); } // Получаем входную линию line = (SourceDataLine)AudioSystem.getLine(info); // Открываем линию line.open(af); // Начинаем проигрывание line.start(); // Ждем появления данных в буфере int num = 0; // Раз за разом заполняем буфер while(( num = ais.read(b)) != -1) line.write(b, 0, num); // "Сливаем" буфер, проигрывая остаток файла line.drain(); // Закрываем поток ais.close(); } catch (Exception e) { System, err.println (e); } // Останавливаем проигрывание line.stop(); // Закрываем линию line.close(); } public static void main(String[] args){ String s = "mrmba.aif"; if (args.length > 0) s = args[0]; new PlayAudioLine(s) ; } } Управлять проигрыванием файла можно с помощью событий. Событие класса LineEvent происходит при открытии, OPEN, и закрытии, CLOSE, потока, при начале, START, и окончании, STOP, проигрывания. Характер события отмечается указанными константами. Соответствующий интерфейс LineListener описывает только один метод update (). В MIDI-файлах хранится последовательность (sequence) команд для секвен-сора (sequencer) — устройства для записи, проигрывания и редактирования MlDIпоследовательности, которым может быть физическое устройство или программа. Последовательность состоит из нескольких дорожек (tracks), на которых записаны MIDI-события (events). Каждая дорожка загружается в своем канале (channel). Обычно дорожка содержит звучание одного музыкального инструмента или запись голоса одного исполнителя или запись нескольких исполнителей, микшированную синтезатором (synthesizer). Для проигрывания MIDI-последовательности в простейшем случае надо создать экземпляр секвенсора, открыть его и направить в него последовательность, извлеченную из файла, как показано в листинге 15.17. После этого следует начать проигрывание методом start (). Закончить проигрывание можно методом stop(), "перемотать" последовательность на начало записи или на указанное время проигрывания — методами setMicrosecondPositionflong mcs) или setTickPosition(long tick). Листинг 15.17. Проигрывание MIDI-последовательности import javax.sound.midi.*; import j ava.io.*; class PlayMIDK PlayMIDKString s) { play(s); } public void play(String file){ try{ File f = new File(file); // Получаем секвенсор по умолчанию Sequencer sequencer = MidiSystem.getSequencerО; // Проверяем, получен ли секвенсор if (sequencer = null) { System.err.println("Sequencer is not supported"); System.exit(0); } // Открываем секвенсор sequencer.open(); // Получаем MIDI-последовательность из файла Sequence seq = MidiSystem.getSequence(f); // Направляем последовательность в секвенсор sequencer.setSequence(seq); // Начинаем проигрывание sequencer.start(); // Здесь надо сделать задержку на время проигрывания, // а затем остановить: sequencer.stop(); )catch(Exception e){ System.err.println(e); } } public static void main(String[] args){ String s = "doom.mid"; if (args.length > 0) s = args[0]; new PlayMIDI(s); } } Синтез и запись звука в Java 2 Синтез звука заключается в создании MIDI-последовательности — объекта класса sequence — каким-либо способом: с микрофона, линейного входа, синтезатора, из файла, или просто создать в программе, как это делается в листинге 15.18. Сначала создается пустая последовательность одним из двух конструкторов: Sequence(float divisionType, int resolution) Sequence(float divisionType, int resolution, int numTracks) Первый аргумент divisionType определяет способ отсчета моментов (ticks) MIDIсобытий — это одна из констант: PPQ (Pulses Per Quarter note) — отсчеты замеряются в долях от длительности звука в четверть; SMPTE_24, SMPTE_25, SMPTE_so, SMPTE_30DROP (Society of Motion Picture and Television Engineers) — отсчеты в долях одного кадра, при указанном числе кадров в секунду. Второй аргумент resolution задает количество отсчетов в указанную единицу, например, Sequence seq = new Sequence)Sequence.PPQ, 10); задает 10 отсчетов в звуке длительностью в четверть. Третий аргумент numTracks определяет количество дорожек в MIDI-последовательности. Потом, если применялся первый конструктор, в последовательности создается одна или несколько дорожек: Track tr = seq.createTrack(); Если применялся второй конструктор, то надб получить уже созданные конструктором дорожки: Track[] trs = seq.getTracks(); Затем дорожки заполняются MIDI-событиями с помощью MIDl-сообще-ний. Есть несколько типов сообщений для разных типов событий. Наиболее часто встречаются сообщения типа shortMessage, которые создаются конструктором по умолчанию и потом заполняются методом setMessageo: ShortMessage msg = new ShortMessage(); rasg.setMessage(ShortMessage.NOTEJDN, 60, 93); Первый аргумент указывает тип сообщения: NOTE_ON — начать звучание, NOTE_OFF — прекратить звучание и т. д. Второй аргумент для типа NOTE_ОN показывает высоту звука, в стандарте MIDI это числа от 0 до 127, 60 — нота "до" первой октавы. Третий аргумент означает "скорость" нажатия клавиши MIDI-инструмента и по-разному понимается различными устройствами. Далее создается MIDI-событие: MidiEvent me = new MidiEvent{msg, ticks); Первый аргумент конструктора msg — это сообщение, второй аргумент ticks — время наступления события (в нашем примере проигрывания ноты "до") в единицах последовательности seq (в нашем примере в десятых долях четверти). Время отсчитывается от начала проигрывания последовательности. Наконец, событие заносится на дорожку: tr.add(me); Указанные действия продолжаются, пока все дорожки не будут заполнены всеми событиями. В листинге 15.18 это делается в цикле, но обычно MIDI-события создаются в методах обработки нажатия клавиш на обычной или специальной MIDIклавиатуре. Еще один способ — вывести на экран изображение клавиатуры и создавать MIDI-собьгшя в методах обработки нажатий кнопки мыши на этой клавиатуре. После создания последовательности ее можно проиграть, как в листинге 15.17, или записать в файл или выходной поток. Для этого вместо метода start() надо применить метод startRecording (), который одновременно и проигрывает последовательность, и подготавливает ее к записи, которую осуществляют статические методы: write(Sequence in, int type, File out) write(Sequence in, int type, OutputStream out) Второй аргумент type задает тип MIDI-файла, который лучше всего определить для заданной последовательности seq статическим методом getMidiFiieTypes(seq). Данный метод возвращает массив возможных типов. Надо воспользоваться нулевым элементом массива, ,Все это. показало в листинге 15.18. Листинг 15.18. Создание MIDI-последовательности нот звукоряда import javax.sound.midi. *; import java.io.*; class SynMIDI { SynMIDI() { play(synth()); } public Sequence synth(){ Sequence seq = null; try{ // Последовательность будет отсчитывать по 10 // MIDI-событий на Звук длительйостью в четверть seq = new Sequence(Sequence.PPQ, 10); // Создаем в последовательности одну дорожку Track tr = seq.createTrack(); for (int k = 0; k < 100; k++){ ShortMessage msg = new ShortMessage(); // Пробегаем MIDI-ноты от номера 10 до 109 msg.setMessage(ShortMessage.NOTE_ON, 10+k, 93); // Будем проигрывать ноты через каждые 5 отсчетов tr.add(new MidiEvent(msg, 5*k)); msg = null; } } catch (Exception e) { System, err.printing "From synth(): "+e); System.exit (0); } return seq; } public void play (Sequence seq) { try{ Sequencer sequencer = MidiSystem.getSequencer(); if (sequencer = null){ System.err.println("Sequencer is not supported"); System.exit(0); } sequencer.open(); sequencer.setSequence(seq); sequencer.startRecording(); int[] type = MidiSystem.getMidiFileTypes(seq); MidiSystem.write(seq, type[0], new File("gammas.mid")); }catch(Exception e) { System.err.println("From play(): " + e); } } public static void main(String[] args)( new SynMIDI(); } } К сожалению, объем книги не позволяет коснуться темы о работе с синтезатором (synthesizer), микширования звука, работы с несколькими инструментами и прочих возможностей Java Sound API. В документации SUN J2SDK, в каталоге docs\guide\sound\prog_guide, есть подробное руководство программиста, а в каталоге demo\sound\src лежат исходные тексты синтезатора, использующего Java Sound API.