Предисловие.

Эта серия уроков предназначена для того, чтобы научить вас использовать Java Sound API. Первый урок из этой серии назывался « Java Sound API, Введение ». Предыдущий урок был озаглавлен « Java Sound API, захват данных микрофона в аудиофайл ».

Два типа аудиоданных.

Java Sound API поддерживает два разных типа аудиоданных:

  • Оцифрованные(вискретизированные) аудиоданные
  • Данные цифрового интерфейса музыкальных инструментов (MIDI)

Эти два типа аудиоданных очень разные. На данный момент я концентрируюсь на дискретизированных аудиоданных. Я отложу обсуждение MIDI на потом.

Совет по просмотру.

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

Анонс.

На предыдущем уроке было показано, как использовать Java Sound API для написания программ для захвата данных микрофона в аудиофайлы любого типа по вашему выбору. В то время я сказал вам, что вы сможете воспроизводить аудиофайлы с помощью легко доступных медиаплееров, таких как Windows Media Player.

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

Обсуждение и образец кода.

Что такое SourceDataLine?

В этой программе я буду использовать объект SourceDataLine. Объект SourceDataLine - это входной объект микшера потоковой передачи. (В предыдущем уроке подробно объяснялись линии и микшеры.)

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

Непонятная терминология.

Терминология, используемая в Java sound API, может сбивать с толку. Вот часть того, что Sun говорит об объекте SourceDataLine.

"Линия исходных данных - это линия данных, в которую могут быть записаны данные. Он действует как источник для его микшера. Приложение записывает байты звука в линию исходных данных, которая обрабатывает буферизацию байтов и доставляет их в микшер. Микшер может ... доставлять микс к цели, такой как порт вывода (который может представлять устройство вывода звука на звуковой карте).

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

Эта концепция подробно обсуждается в уроке под названием Java Sound API. Начало работы, Часть 1, Воспроизведение. 

Вывод аудиоданных.

Данные, записанные в объект SourceDataLine, могут быть помещены в микшер в реальном времени. Фактическим местом назначения аудиоданных может быть любое из множества мест назначения.

(Пример программы в этом уроке записывает аудиоданные в объект SourceDataLine, который передает данные на динамики компьютера.)

Пользовательский интерфейс.

Когда эта программа выполняется, на экране появляется графический интерфейс, показанный на рисунке 1. Как видите, этот графический интерфейс содержит следующие компоненты:

  • Текстовое поле
  • Кнопка Play
  • Кнопка Stop

Рисунок 1 Графический интерфейс программы

Работа программы.

Пользователь вводит путь и имя файла для аудиофайла в текстовое поле, а затем нажимает кнопку Play . Содержимое аудиофайла воспроизводится через системные динамики.

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

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

Отображается формат аудиоданных.

Программа также отображает формат аудиоданных перед воспроизведением файла. Формат отображается на экране командной строки.

Обсудим по фрагментам.

Как обычно, я буду обсуждать эту программу по частям. Полный список программы показан в листинге 17 ближе к концу урока.

Класс с именем AudioPlayer02.

Программа под названием AudioPlayer02 демонстрирует использование программы Java для воспроизведения содержимого аудиофайла. Определение класса для управляющего класса начинается в листинге 1.

public class AudioPlayer02 extends JFrame{

  AudioFormat audioFormat;
  AudioInputStream audioInputStream;
  SourceDataLine sourceDataLine;
  boolean stopPlayback = false;

  final JButton stopBtn = new JButton("Stop");
  final JButton playBtn = new JButton("Play");
  final JTextField textField =
                       new JTextField("junk.au");

Листинг 1

Переменные экземпляра.

В листинге 1 объявлено несколько переменных экземпляра. Некоторые из этих переменных экземпляра также инициализированы. Стоит отметить, что объект текстового поля инициализируется, чтобы содержать имя файла по умолчанию junk.au. Это позволяет запускать программу без необходимости вводить имя файла в текстовое поле при условии, что аудиофайл с именем junk.au находится в том же каталоге, что и файл управляющего класса. (Предыдущий урок содержал программу, которая создавала аудиофайл с именем junk.au.)

Метод main.

Метод main этого приложения Java, показанный в листинге 2, чрезвычайно прост.

  public static void main(String args[]){
    new AudioPlayer02();
  }//end main

Листинг 2

 

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

Конструктор.

Конструктор начинается в листинге 3.

  public AudioPlayer02(){//конструктор

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

Листинг 3

Код в листинге 3 вызывает отключение кнопки Stop и включение кнопки Play при первом запуске программы. Это ситуация, показанная на рисунке 1.

Прослушиватель действий на кнопке Play.

Код в листинге 4 создает и регистрирует прослушиватель действия на кнопке Play.

    playBtn.addActionListener(
      new ActionListener(){
        public void actionPerformed(
                                  ActionEvent e){
          stopBtn.setEnabled(true);
          playBtn.setEnabled(false);
          playAudio();//******** Воспроизвести файл
        }//конец actionPerformed
      }//конец ActionListener
    );//конец addActionListener()

Листинг 4

Я объяснил этот синтаксис анонимного внутреннего класса на предыдущем уроке. Самым важным в коде в листинге 4 является оператор, выделенный звездочками. Этот оператор вызывает метод playAudio для воспроизведения содержимого аудиофайла, когда пользователь нажимает кнопку Play.

Два других оператора в листинге 4 просто меняют включенное / выключенное состояние кнопок Play и Stop.

Прослушиватель действий на кнопке Stop.

Мы все еще обсуждаем конструктор управляющего класса. Код в листинге 5 создает и регистрирует прослушиватель действия на кнопке Stop.

    stopBtn.addActionListener(
      new ActionListener(){
        public void actionPerformed(
                                  ActionEvent e){
          //Terminate playback before EOF
          stopPlayback = true;
        }//end actionPerformed
      }//end ActionListener
    );//end addActionListener()

Листинг 5

Когда пользователь нажимает кнопку Stop, код в листинге 5 устанавливает для флага stopPlayback значение true. Как вы увидите позже, поток, который обрабатывает фактическую операцию воспроизведения, периодически проверяет этот флаг. Если флаг переключается с false на true, код в методе run потока воспроизведения прекращает воспроизведение в этот момент времени.

Перед прекращением воспроизведения может быть задержка по времени.

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

Завершите построение графического интерфейса.

Код в листинге 6 завершает построение графического интерфейса пользователя.

    getContentPane().add(playBtn,"West");
    getContentPane().add(stopBtn,"East");
    getContentPane().add(textField,"North");

    setTitle("Copyright 2003, R.G.Baldwin");
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    setSize(250,70);
    setVisible(true);
  }//конец конструктора

Листинг 6

Этот код делает следующее:

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

Метод playAudio.

Напомним, что обработчик события на кнопке Play вызывает метод playAudio. Метод playAudio воспроизводит аудиоданные из аудиофайла, где имя аудиофайла указывается в текстовом поле графического интерфейса. Метод playAudio начинается в листинге 7.

  private void playAudio() {
    try{
      File soundFile =
                   new File(textField.getText());

Листинг 7

Объект File.

Первое, что нам нужно сделать, это создать новый объект File, который обеспечивает абстрактное представление пути к файлу и каталогу, введенному пользователем в текстовое поле графического интерфейса.

В листинге 7 это достигается следующим образом:

  • Получение объекта String, который инкапсулирует текст, введенный пользователем в текстовое поле.
  • Передача ссылки на этот объект String конструктору класса File.

Объект AudioInputStream.

Далее нам нужно получить объект AudioInputStream, который подключен к аудиофайлу. Это сделано в листинге 8.

      audioInputStream = AudioSystem.
                  getAudioInputStream(soundFile);

Листинг 8

В листинге 8 ссылка на объект File передается статическому методу getAudioInputStream класса AudioSystem. В Java SDK версии 1.4.1 есть несколько перегруженных версий метода getAudioInputStream. Вот часть того, что Sun говорит о версии метода, используемого в листинге 8.

"Получает входной аудиопоток из предоставленного файла. Файл должен указывать на допустимые данные аудиофайла."

Аудиоформат.

Из предыдущих уроков вы узнали, что аудиоданные бывают в различных аудиоформатах. Первый оператор в листинге 9 вызывает метод getFormat объекта AudioInputStream, чтобы получить и сохранить аудиоформат указанного аудиофайла.

      audioFormat = audioInputStream.getFormat();

      System.out.println(audioFormat);

Листинг 9

Примеры форматов.

Второй оператор в листинге 9 отображает отчетный формат.

Вот заявленный формат для файла с именем tada.wav, который я нашел в каталоге носителей операционной системы в моей системе Win2000.

PCM_SIGNED, 22050.0 Hz, 16 bit, stereo, little-endian, audio data

Вот отчетный формат файла с именем junk.au, который был создан программой AudioRecorder02 на предыдущем уроке.

PCM_SIGNED, 8000.0 Hz, 16 bit, mono, big-endian, audio data

Будьте осторожны с обратной косой чертой.

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

Объект DataLine.Info.

На предыдущем уроке вы узнали о требовании создать объект DataLine.Info, описывающий TargetDataLine, необходимый для обработки получения аудиоданных с микрофона.

Аналогичное требование существует и в этой программе. Однако в этой программе нам нужен объект DataLine.Info, описывающий объект SourceDataLine, который мы будем использовать для передачи аудиоданных в динамики. Объект DataLine.Info создан в листинге 10.

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

Листинг 10

Конструктор DataLine.Info.

Обратите внимание, что в этом случае первым параметром конструктора DataLine.Info является объект Class, который представляет тип объекта, созданного из класса, реализующего интерфейс SourceDataLine.

Второй параметр конструктора определяет формат аудиоданных, которые будут обрабатываться объектом SourceDataLine.

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

Наконец, код в листинге 11 вызывает статический метод getLine класса AudioSystem для получения объекта SourceDataLine, который соответствует описанию объекта, предоставленному объектом DataLine.Info.

      sourceDataLine =
             (SourceDataLine)AudioSystem.getLine(
                                   dataLineInfo);

Листинг 11

Я подробно обсуждал статический метод getLine класса AudioSystem на предыдущем уроке, поэтому я не буду обсуждать его здесь.

Создайте и запустите поток для обработки воспроизведения.

Выражение жирным шрифтом в листинге 12 создает поток (для воспроизведения данных) и запускает поток. Поток будет выполняться до тех пор, пока не будет достигнут конец файла или пока пользователь не нажмет кнопку Stop , в зависимости от того, что произойдет раньше. (Из-за задействованных буферов данных обычно будет задержка между нажатием кнопки Stop и фактическим прекращением воспроизведения.)

      new PlayThread().start();
    }catch (Exception e) {
      e.printStackTrace();
      System.exit(0);
    }//end catch
  }//end playAudio

Листинг 12

После запуска потока метод playAudio возвращает управление обработчику события действия на кнопке Play, который вскоре прекращает свою работу.

Остающийся код в листинге 12 состоит просто из необходимого блока catch. Метод playAudio заканчивается в листинге 12.

Метод run класса PlayThread.

PlayThread - это внутренний класс. Объект этого класса используется для фактического воспроизведения данных из аудиофайла.

У каждого объекта потока есть метод запуска, который определяет поведение потока. В листинге 13 показано начало метода run класса PlayThread.

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

  public void run(){
    try{
      sourceDataLine.open(audioFormat);
      sourceDataLine.start();

Листинг 13

The array object of type byte that is instantiated in Listing 13 will be used to transfer the data from the audio file stream to the SourceDataLine object.

Метод open объекта SourceDataLine.

Код в листинге 13 вызывает метод open объекта SourceDataLine. Согласно Sun,  метод open  объекта SourceDataLine

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

Метод start объекта SourceDataLine.

Код в листинге 13 также вызывает метод start объекта SourceDataLine. Согласно Sun, метод start объекта SourceDataLine.

"Позволяет линии заниматься вводом-выводом данных. При вызове в уже запущенной линии этот метод ничего не делает."

Таким образом, когда код из листинга 13 был выполнен, объект SourceDataLine получил все необходимые системные ресурсы и готов передавать аудиоданные на системные динамики.

Цикл передачи данных.

Условное предложение цикла while в листинге 14 немного сложное. Цикл while в листинге 14 повторяется до тех пор, пока:

  • Метод read объекта AudioInputStream возвращает минус 1, что указывает на пустой поток (конец файла) или
  • Пользователь нажимает кнопку Stop, в результате чего stopPlayback переключается с false на true.
      int cnt;

      while((cnt = audioInputStream.read(
           tempBuffer,0,tempBuffer.length)) != -1
                       && stopPlayback == false){
        if(cnt > 0){
          sourceDataLine.write(
                             tempBuffer, 0, cnt);
        }//end if
      }//end while loop

Листинг 14

Положительное значение из метода read.

Если метод read возвращает положительное значение, это значение указывает количество байтов, которые были прочитаны из аудиофайла во временный буфер с именем tempBuffer.

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

Преобразование дискретизированных аудиоданных в волны звукового давления.

The SourceDataLine object causes the conversion of sampled audio data values ​​to analog voltages, which are sent to the speakers, where the voltages are converted to sound pressure waves.

Для этого объект SourceDataLine должен знать формат аудиоданных, включая частоту дискретизации, количество каналов, количество бит на выборку и т. Д. Эта информация была предоставлена ​​объекту SourceDataLine с помощью Объект DataLine.Info в листинге 10.

Слейте буфер SourceDataLine.

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

      sourceDataLine.drain();
      sourceDataLine.close();

Листинг 15

Вот часть того, что Sun говорит о методе drain.

"Удаляет данные из очереди из линии, продолжая ввод-вывод данных до тех пор, пока внутренний буфер линии данных не будет опустошен. Этот метод блокируется до завершения слива."

Метод close.

Код в листинге 15 также вызывает метод close объекта SourceDataLine. Вот часть того, что Sun говорит о методе close .

"Закрывает линию, указывая, что любые системные ресурсы, используемые линией, могут быть освобождены."

Таким образом, когда код в листинге 15 завершил выполнение, все данные, доставленные в объект SourceDataLine, были переданы динамикам, а объект SourceDataLine получил системные ресурсы для освобождения.

Подготовка к воспроизведению другого файла.

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

      stopBtn.setEnabled(false);
      playBtn.setEnabled(true);
      stopPlayback = false;

Листинг 16

За исключением блока catch, код в листинге 16:

  • Сигнализирует об окончании метода run.
  • Сигнализирует об окончании внутреннего класса PlayThread.
  • Сигнализирует об окончании программы.

Вы можете просмотреть блок catch в листинге 17 ближе к концу урока.

Запуск программы.

На этом этапе вы можете найти полезным скомпилировать и запустить программу, показанную в листинге 17, ближе к концу урока.

Указание аудиофайла.

Запустите программу и введите путь и имя файла аудиофайла в текстовое поле, которое появляется в графическом интерфейсе пользователя, показанном на рисунке 1. Вы должны разделять каталоги в пути, используя косую черту вместо обратной. Если вы работаете под Windows и хотите использовать обратную косую черту, используйте две обратные косые черты в каждом случае вместо одной обратной косой черты. Если аудиофайл находится в том же каталоге, что и программа (текущий каталог), вам не нужно вводить путь.

Начать воспроизведение.

После ввода имени файла нажмите кнопку Play. Воспроизведение должно начаться и продолжаться до конца аудиофайла, если вы не нажмете кнопку Stop во время воспроизведения файла.

Кнопка Stop.

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

Контроль громкости.

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

Резюме.

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

Полный список программ.

Полный список программы показан в листинге 17.

/*File AudioPlayer02.java
Copyright 2003 Richard G. Baldwin
 
Демонстрирует воспроизведение аудиофайла. 
Путь и имя аудиофайла указывается 
пользователем в текстовом поле.
 
На экране появится графический интерфейс, 
содержащий следующие компоненты:
  Текстовое поле для имени файла
  Play
  Stop
 
После ввода имени аудиофайла в текстовое 
поле пользователь может нажать кнопку Play, 
чтобы программа воспроизвела аудиофайл. 
По умолчанию программа воспроизводит файл 
целиком, а затем готовится воспроизвести 
другой файл или снова воспроизвести тот же 
файл.
 
Если пользователь нажимает кнопку Stop 
во время воспроизведения файла, 
воспроизведение прекращается. Однако, 
поскольку аудиоданные буферизуются в большом 
буфере в цикле воспроизведения, может быть 
заметная задержка между моментом нажатия 
кнопки Stop и временем фактического завершения 
воспроизведения.
 
Текстовое поле содержит имя аудиофайла по 
умолчанию, junk.au, когда графический интерфейс 
впервые появляется на экране.
 
Программа отображает формат аудиоданных в файле 
перед воспроизведением файла. Формат отображается 
на экране командной строки.
 
Tested using SDK 1.4.1 under Win2000
************************************************/
 
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import javax.sound.sampled.*;
 
public class AudioPlayer02 extends JFrame{
 
  AudioFormat audioFormat;
  AudioInputStream audioInputStream;
  SourceDataLine sourceDataLine;
  boolean stopPlayback = false;
  final JButton stopBtn = new JButton("Stop");
  final JButton playBtn = new JButton("Play");
  final JTextField textField =
                       new JTextField("junk.au");
 
  public static void main(String args[]){
    new AudioPlayer02();
  }//конец main
  //-------------------------------------------//
 
  public AudioPlayer02(){//конструктор
 
    stopBtn.setEnabled(false);
    playBtn.setEnabled(true);
 
    // Создать и зарегистрировать слушателей 
	// действий на кнопках Play и Stop.
    playBtn.addActionListener(
      new ActionListener(){
        public void actionPerformed(
                                  ActionEvent e){
          stopBtn.setEnabled(true);
          playBtn.setEnabled(false);
          playAudio();//Play the file
        }//конец actionPerformed
      }//конец ActionListener
    );//конец addActionListener()
 
    stopBtn.addActionListener(
      new ActionListener(){
        public void actionPerformed(
                                  ActionEvent e){
          // Прекратить воспроизведение до EOF
          stopPlayback = true;
        }//конец actionPerformed
      }//конец ActionListener
    );//конец addActionListener()
 
    getContentPane().add(playBtn,"West");
    getContentPane().add(stopBtn,"East");
    getContentPane().add(textField,"North");
 
    setTitle("Copyright 2003, R.G.Baldwin");
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    setSize(250,70);
    setVisible(true);
  }//конец конструктора
  //-------------------------------------------//
 
  // Этот метод воспроизводит аудиоданные
  // из аудиофайла, имя которого указано
  // в текстовом поле.
  private void playAudio() {
    try{
      File soundFile =
                   new File(textField.getText());
      audioInputStream = AudioSystem.
                  getAudioInputStream(soundFile);
      audioFormat = audioInputStream.getFormat();
      System.out.println(audioFormat);
 
      DataLine.Info dataLineInfo =
                          new DataLine.Info(
                            SourceDataLine.class,
                                    audioFormat);
 
      sourceDataLine =
             (SourceDataLine)AudioSystem.getLine(
                                   dataLineInfo);
 
      // Создать поток для воспроизведения данных и 
      // запустить его. Он будет выполняться до
      // конца файла или до нажатия кнопки Stop, 
      // в зависимости от того, что произойдет раньше. 
      // Из-за задействованных буферов данных
      // обычно будет задержка между нажатием кнопки 
      // Stop и фактическим прекращением
      // воспроизведения.
      new PlayThread().start();
    }catch (Exception e) {
      e.printStackTrace();
      System.exit(0);
    }//конец catch
  }//конец playAudio
 
 
//=============================================//
// Внутренний класс для воспроизведения данных 
// из аудиофайла.
class PlayThread extends Thread{
  byte tempBuffer[] = new byte[10000];
 
  public void run(){
    try{
      sourceDataLine.open(audioFormat);
      sourceDataLine.start();
 
      int cnt;
      // Продолжать цикл до тех пор, пока метод read
      // не вернет -1 для пустого потока или пока 
      // пользователь не нажмет кнопку Stop, 
      // sчто приведет к переключению 
      // stopPlayback с false на true.
      while((cnt = audioInputStream.read(
           tempBuffer,0,tempBuffer.length)) != -1
                       && stopPlayback == false){
        if(cnt > 0){
          // Записать данные во внутренний буфер
          // линии данных, где они будут доставлены
          // в динамики.
          sourceDataLine.write(
                             tempBuffer, 0, cnt);
        }//конец if
      }//конец while
      // Заблокируй и дождись, пока внутренний 
      // буфер линии данных опустеет.
      sourceDataLine.drain();
      sourceDataLine.close();
 
      // Подготовьтесь к воспроизведению другого файла
      stopBtn.setEnabled(false);
      playBtn.setEnabled(true);
      stopPlayback = false;
    }catch (Exception e) {
      e.printStackTrace();
      System.exit(0);
    }//конец catch
  }//конец run
}//конец внутреннего класса PlayThread
//===================================//
 
}//конец внешнего класса AudioPlayer02.java

Листинг 17

 

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