GrabDuck

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