Транслируем звук по сети с помощью 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/NetworkingAudio

    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 26
      0
      В классе ReceiverPlayer у поля finishFlag забыли поставить volatile.
        0
        Спасибо, как-то недоглядел.
        0
        Уточните пожалуйста, вы использовали голый PCM без компрессии/синхронизации?
          0
          Да, голый PCM.
            0
            А почему вы проигнорировали сжатие и специализированные медиа-протоколы вроде RTP?
              0
              Просто хотелось поэкспериментировать, попробовать реализовать все это на более-менее низком уровне.
                0
                К примеру, для голосовой связи вполне подходит
                sampleRate = 8000, sampleSizeInBits = 8, channels = 1.
                Итого — всего 64 kbs.

                Для трансляции музыки в домашней сети можно установить качество получше. Даже без сжатия — скорость вполне позволяет.

                О сжатии и специализированных протоколах стоит задуматься, если искать более серьезное применение.
                  0
                  Если эта домашняя сеть проложена как минимум Ethernet. При удовлетворительном качестве CD-DA имеем 44100*16*2 — примерно полтора мегабита. Вайфай, да через пару капитальных стен, такое не протянет.
                    +1
                    У меня стоит вай-фай роутер и точка доступа в качестве повторителя (как раз из-за капитальных стен в 3к-квартире ее поставил).
                    На 16000*16*2 (0.5 мегабит) тянет вполне.
                    И качество звука приличное.
                    Вообще стандарт 802.11n предусматривает 150 Мбит/с при одной антенне.
                      0
                      эти 150 мегабит даны для идеальных условий — ровно два устройства без препятствий и помех. Как только в эксперименте появляются капитальные стены, репитеры, микроволновки и соседи со своими точками, настроенными кучно именно на ваш канал, вся прелесть от громкого показателя улетучивается.
                        0
                        WiFi сеть единого информационного пространства, и если поставить вторую пару девайсов, скорость начнет делиться между ними. А когда дом многоквартирный и видно порядка 20-30 точек и при этом ни одного свободного канала. Учитывая еще факт, что 5ГГц у нас как бы и не разрешен, а в 2.4ГГц желающие тупо влезть и не могут. Так в таких условиях на 802.11n скорость проседает по самое нихочу. А когда еще половина точек сидит на 802.11bg и покрывают все доступные каналы, то вашим 802.11n можно и в туалете подтереться, все 802.11n точки будут работать в режиме 802.11bg и не более. Ну и да, забыл еще момент, если даже у всех точки 802.11n, но используют устройства 802.11bg, то работать будет именно в режиме 802.11bg. И ни у кого на данном канале не будет 802.11n, вообще.
                          0
                          5 ГГц в России разрешены примерно на тех же условиях, что и 2.4: в закрытых помещениях — без дополнительных разрешений: Решение ГКРЧ от 20 декабря 2011 г. № 11-13-07-1 «О внесении изменений в решение ГКРЧ от 7 мая 2007 г. № 07-20-03-001 «О выделении полос радиочастот устройствам малого радиуса действия»
                            0
                            Ну судя по этому же документу, допускается видимо только в случае, если стены заэкранировать, а так под сноску можно приписать всё что угодно (смотрим 4й пунктик):

                            >> Условие применения устройств малого радиуса действия внутри закрытых помещений предусматривает
                            >> дополнительное ослабление радиосигнала от указанных устройств в направлении других РЭС,
                            >> функционирующих в соответствии с Таблицей распределения полос частот между радиослужбами
                            >> Российской Федерации, вносимое конструкциями помещений

                            4й смотрим потому что точки обычно имеют мощность 100мВт, но ширина канала WiFi 20 или 40МГц, получаем, что вылезает за рамки 2мВт/МГц и 2й пунктик уже не подходит, да и во 2м указан только 2.4ГГц. Так что дяди могут придти, посмотреть и сказать, а чего у вас стеночки-то не заэкранированы, нехорошл получается, нарушаемс.
                        0
                        Попробовал замерить реальную скорость вай-фая между двумя самыми удаленными комнатами с помощью wget.
                        Примерно 1.8 МБайт/c — или 14.4 Мбит.
                          0
                          Вот, реальный показатель в 10 меньше маркетингового. Еще одна неприятность — пинг около пары миллисекунд, то есть если синхронно транслировать в двух комнатах, будет противный эффект эхо.
                            0
                            Почему так, описал выше. Проводил опыт в идеальных условиях, да, всё нормально, скорости заявленные. Но если есть влияние на канал устаревшего оборудования, начинаются проблемы.
                  0
                  вполне годно для начала написания корпоративной скайпо-подобной системы
                    0
                    По идее можно сделать сервер, который будет хранить текущие айпишники каждого абонента.
                    Клиент получает с сервера нужный айпишник, и делает прямой звонок.
                    Ну и шифрование данных можно сделать какое-нибудь.
                    Простейший вариант, который приходит на ум — обычное XOR-шифрование.
                    Если зашифрованный XOR'ом текст можно подвергнуть статистическому анализу — сделать то же самое для потока байтов, кодирующих звук, по-моему, на порядок сложнее (с первого взгляда вообще невозможно).
                    Допустим, при каждом создании канала генерируется случайный ключ определенного размера и передается второму абоненту методом двойного рукопожатия. Получается дешево и сердито.
                      +3
                      (Закадровым голосом из Цивилизации):
                      Вы изобрели протоколы SRTP и SIP.

                      В потоке байтов, кодирующих звук, будет преобладать 0, 0 XOR A = A: пока собеседник молчит, будет передаваться сам ключ, увы.

                      В качестве корпоративной системы стоит взять таки Asterisk в роли сервера и Linphone в роли клиента (есть на все полатформы). На Андроид также есть CSipSimple с огромным количеством настроек.
                        0
                        Точно. Тишину я как-то не учел ))
                          0
                          Если уж брать готовые системы, то для того же XMPP(Jabber) существуют клиенты с возможностью прямой голосовой связи.
                          В плане настройки по-моему XMPP-сервер на порядок проще, чем Asterisk.
                            0
                            Если речь идет о «просто поговорить», то нужно поправить (мануалы есть в огромных количествах) два файла — sip.conf и extensions.conf
                            (Ну как поправить… списать дуб из мануала и готово)

                            Найти не закрытого клиента на андроид, который в состоянии позвонить клиенту на десктопе по Jabber/Jinge мне пока не удалось.
                      0
                      Очень странная (и не используемая) переменная в п.1
                      Scanner sc = new Scanner(System.in);
                        0
                        Изначально вместо while(true) стояло while (!sc.next().equals(«quit»)).
                        Просто переменную не удалил.
                        Спасибо за замечание, сейчас исправлю.
                          0
                          А исходники можно куда-нибудь перезалить? Ссылка на Гите протухла…
                            0

                            Исправил ссылку.

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

                      Самое читаемое