Стало мне интересно поэкспериментировать с передачей звука по сети.
Выбрал для этого технологию 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
Автор: jatx