Транслируем звук по сети с помощью Java

Стало мне интересно поэкспериментировать с передачей звука по сети.Выбрал для этого технологию Java.В итоге написал три компоненты — передатчик для Java SE, приемник для Java SE и приемник для Android.В Java SE для работы со звуком использовались классы из пакета javax.sound.sampled, в Android — классы android.media.AudioFormat, android.media.AudioManager и android.media.AudioTrack.Для работы с сетью — стандартные Socket и ServerSocket.

С помощью этих компонент удалось успешно провести сеанс голосовой связи между Дальним Востоком России и Нидерландами.

И еще одно возможное применение — если установить виртуальную звуковую карту, например, Virtual Audio Cable, можно транслировать музыку на другие устройства, и, таким образом, слушать музыку одновременно в нескольких комнатах квартиры (при наличии соответствующего количества девайсов).

1. Передатчик.Способ трансляции звука тривиален — считываем поток байтов с микрофона, и записываем его в выходной поток сокета.

Работа с микрофоном и передача данных по сети происходит в отдельных потоках:

mr = new MicrophoneReader (); mr.start (); ServerSocket ss = new ServerSocket (7373); while (true) { Socket s = ss.accept (); Sender sndr = new Sender (s); senderList.add (sndr); sndr.start (); } Поток для работы с микрофоном:

public void run () { try { microphone = AudioSystem.getTargetDataLine (format); DataLine.Info info = new DataLine.Info (TargetDataLine.class, format); microphone = (TargetDataLine) AudioSystem.getLine (info); microphone.open (format); data = new byte[CHUNK_SIZE]; microphone.start (); while (! finishFlag) { synchronized (monitor) { if (senderNotReady==sendersCreated) { monitor.notifyAll (); continue; } numBytesRead = microphone.read (data, 0, CHUNK_SIZE); }

System.out.print («Microphone reader:»); System.out.print (numBytesRead); System.out.println (» bytes read»); } } catch (LineUnavailableException e) { e.printStackTrace (); } } UPD. Примечание: важно правильно подобрать параметр CHUNK_SIZE. При слишком малом значении будут слышны заикания, при слишком большом — становится заметной задержка звука.

Поток для передачи звука:

public void run () { try { OutputStream os = s.getOutputStream (); while (! finishFlag) { synchronized (monitor) { senderNotReady++; monitor.wait (); os.write (data, 0, numBytesRead); os.flush (); senderNotReady--; } System.out.print («Sender #»); System.out.print (senderNumber); System.out.print (»:»); System.out.print (numBytesRead); System.out.println (» bytes sent»); } } catch (Exception e) { e.printStackTrace (); } } Оба класса потоков — вложенные, переменные внешнего класса data, numBytesRead, senderNotReady, sendersCreated и monitor должны быть объявлены как volatile.Объект monitor используется для синхронизации потоков.

2. Приемник для Java SE. Способ так же тривиален — считываем поток байтов из сокета, и записываем в аудиовыход.

try { InetAddress ipAddr = InetAddress.getByName (host); Socket s = new Socket (ipAddr, 7373); InputStream is = s.getInputStream (); DataLine.Info dataLineInfo = new DataLine.Info (SourceDataLine.class, format); speakers = (SourceDataLine) AudioSystem.getLine (dataLineInfo); speakers.open (format); speakers.start (); Scanner sc = new Scanner (System.in); int numBytesRead; byte[] data = new byte[204800]; while (true) { numBytesRead = is.read (data); speakers.write (data, 0, numBytesRead); } } catch (Exception e) { e.printStackTrace (); } 3. Приемник для Android. Способ тот же самый.Единственное отличие — вместо javax.sound.sampled.SourceDataLine используем android.media.AudioTrack.Так же нужно учесть, что в Android работы с сетью не может происходить в основном потоке выполнения приложения.С созданием сервисов решил не заморачиваться, запускать рабочий поток будем из основной Activity.

toogle.setOnClickListener (new View.OnClickListener () { @Override public void onClick (View v) { if (! isRunning) { isRunning = true; toogle.setText («Stop»); rp = new ReceiverPlayer (hostname.getText ().toString ()); rp.start (); } else { toogle.setText («Start»); isRunning = false; rp.setFinishFlag (); } } }); Код самого рабочего потока:

class ReceiverPlayer extends Thread { volatile boolean finishFlag; String host; public ReceiverPlayer (String hostname) { host = hostname; finishFlag = false; } public void setFinishFlag () { finishFlag = true; } public void run () { try { InetAddress ipAddr = InetAddress.getByName (host); Socket s = new Socket (ipAddr, 7373); InputStream is = s.getInputStream (); int bufferSize = AudioTrack.getMinBufferSize (16000, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT); int numBytesRead; byte[] data = new byte[bufferSize]; AudioTrack aTrack = new AudioTrack (AudioManager.STREAM_MUSIC, 16000, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM); aTrack.play (); while (! finishFlag) { numBytesRead = is.read (data, 0, bufferSize); aTrack.write (data, 0, numBytesRead); } aTrack.stop (); s.close (); } catch (Exception e) { StringWriter sw = new StringWriter (); PrintWriter pw = new PrintWriter (sw); e.printStackTrace (pw); Log.e («Error», sw.toString ()); } } } 4. Примечание о форматах аудио. В Java SE используется класс javax.sound.sampled.AudioFormat.

В Android — параметры аудио передаются напрямую в конструктор объекта android.media.AudioTrack.

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

Java SE:

AudioFormat (float sampleRate, int sampleSizeInBits, int channels, boolean signed, boolean bigEndian)Constructs an AudioFormat with a linear PCM encoding and the given parameters.

Android:

AudioTrack (int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode).

Для успешного воспроизведения параметры приемника и передатчика sampleRate/sampleRate, sampleSizeInBits/audioFormat и channels/channelConfig должны соответствовать друг другу.

Помимо этого, значение mode для Android нужно установить в AudioTrack.MODE_STREAM.

Так же, экспериментально удалось установить, что для успешного воспроизведения на Android нужно передавать данные в формате signed little endian, то есть: signed = true; bigEndian = false.

В итоге были выбраны следующие форматы:

// Java SE: AudioFormat format = new AudioFormat (16000.0f, 16, 2, true, bigEndian);

// Android: AudioTrack aTrack = new AudioTrack (AudioManager.STREAM_MUSIC, 16000, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM); 5. Тестирование. Между ноутбуком на Windows 8 и десктопом на Debian Wheezy все завелось сразу без проблем.

Приемник на Android изначально издавал лишь шум, но эта проблема устранилась после правильного подбора параметров signed и bigEndian для формата аудио.

На Raspberry Pi (Raspbian Wheezy) изначально были слышны заикания — понадобились костыли в виде установки легковесной виртуальной java-машины avian.

Написал следующий скрипт запуска:

case »$1» in start) java -avian -jar jAudioReceiver.jar 192.168.1.50 & echo «kill -KILL $!»>kill_receiver.sh ;; stop) ./kill_receiver.sh ;; esac Исходные коды всех компонент здесь:

github.com/tabatsky/jatx/tree/master/NetworkingAudio

© Habrahabr.ru