Стало мне интересно поэкспериментировать с передачей звука по сети.
Выбрал для этого технологию 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, можно транслировать музыку на другие устройства, и, таким образом, слушать музыку одновременно в нескольких комнатах квартиры (при наличии соответствующего количества девайсов).
Способ трансляции звука тривиален — считываем поток байтов с микрофона, и записываем его в выходной поток сокета.
Работа с микрофоном и передача данных по сети происходит в отдельных потоках:
Поток для работы с микрофоном:
UPD. Примечание: важно правильно подобрать параметр CHUNK_SIZE. При слишком малом значении будут слышны заикания, при слишком большом — становится заметной задержка звука.
Поток для передачи звука:
Оба класса потоков — вложенные, переменные внешнего класса data, numBytesRead, senderNotReady, sendersCreated и monitor должны быть объявлены как volatile.
Объект monitor используется для синхронизации потоков.
Способ так же тривиален — считываем поток байтов из сокета, и записываем в аудиовыход.
Способ тот же самый.
Единственное отличие — вместо javax.sound.sampled.SourceDataLine используем android.media.AudioTrack.
Так же нужно учесть, что в Android работы с сетью не может происходить в основном потоке выполнения приложения.
С созданием сервисов решил не заморачиваться, запускать рабочий поток будем из основной Activity.
Код самого рабочего потока:
В 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.
В итоге были выбраны следующие форматы:
Между ноутбуком на Windows 8 и десктопом на Debian Wheezy все завелось сразу без проблем.
Приемник на Android изначально издавал лишь шум, но эта проблема устранилась после правильного подбора параметров signed и bigEndian для формата аудио.
На Raspberry Pi (Raspbian Wheezy) изначально были слышны заикания — понадобились костыли в виде установки легковесной виртуальной java-машины avian.
Написал следующий скрипт запуска:
Исходные коды всех компонент здесь:
github.com/tabatsky/NetworkingAudio
Выбрал для этого технологию 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/NetworkingAudio
