diff --git a/src/com/limelight/binding/audio/JavaxAudioRenderer.java b/src/com/limelight/binding/audio/JavaxAudioRenderer.java index e6f94f2..dfd84ce 100644 --- a/src/com/limelight/binding/audio/JavaxAudioRenderer.java +++ b/src/com/limelight/binding/audio/JavaxAudioRenderer.java @@ -1,30 +1,60 @@ package com.limelight.binding.audio; -import java.nio.ByteBuffer; - import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.DataLine; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.SourceDataLine; +import com.limelight.nvstream.av.ShortBufferDescriptor; import com.limelight.nvstream.av.audio.AudioRenderer; -import com.limelight.nvstream.av.audio.OpusDecoder; public class JavaxAudioRenderer implements AudioRenderer { private SourceDataLine soundLine; + private SoundBuffer soundBuffer; + private byte[] lineBuffer; + private int channelCount; + private int sampleRate; + private boolean reallocateLines; + + public static final int DEFAULT_BUFFER_SIZE = 0; + public static final int STARING_BUFFER_SIZE = 4096; + public static final int STAGING_BUFFERS = 3; // 3 complete frames of audio @Override public void playDecodedAudio(short[] pcmData, int offset, int length) { if (soundLine != null) { - byte[] pcmDataBytes = new byte[length * 2]; - ByteBuffer.wrap(pcmDataBytes).asShortBuffer().put(pcmData, offset, length); - if (soundLine.available() < length) { - soundLine.write(pcmDataBytes, 0, soundLine.available()); + // Queue the decoded samples into the staging sound buffer + soundBuffer.queue(new ShortBufferDescriptor(pcmData, offset, length)); + + int available = soundLine.available(); + if (reallocateLines) { + // Kinda jank. If the queued is larger than available, we are going to have a delay + // so we increase the buffer size + if (available < soundBuffer.size()) { + System.out.println("buffer too full, buffer size: " + soundLine.getBufferSize()); + int currentBuffer = soundLine.getBufferSize(); + soundLine.close(); + createSoundLine(currentBuffer*2); + if (soundLine != null) { + available = soundLine.available(); + System.out.println("creating new line with buffer size: " + soundLine.getBufferSize()); + } + else { + available = 0; + System.out.println("failed to create sound line"); + } + } } - else { - soundLine.write(pcmDataBytes, 0, pcmDataBytes.length); + + // If there's space available in the sound line, pull some data out + // of the staging buffer and write it to the sound line + if (available > 0) { + int written = soundBuffer.fill(lineBuffer, 0, available); + if (written > 0) { + soundLine.write(lineBuffer, 0, written); + } } } } @@ -35,18 +65,51 @@ public class JavaxAudioRenderer implements AudioRenderer { soundLine.close(); } } - - @Override - public void streamInitialized(int channelCount, int sampleRate) { + + private void createSoundLine(int bufferSize) { AudioFormat audioFormat = new AudioFormat(sampleRate, 16, channelCount, true, true); - DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat, OpusDecoder.getMaxOutputShorts()); + + DataLine.Info info; + + if (bufferSize == DEFAULT_BUFFER_SIZE) { + info = new DataLine.Info(SourceDataLine.class, audioFormat); + } + else { + info = new DataLine.Info(SourceDataLine.class, audioFormat, bufferSize); + } + try { soundLine = (SourceDataLine) AudioSystem.getLine(info); - soundLine.open(audioFormat, OpusDecoder.getMaxOutputShorts()*4*2); + + if (bufferSize == DEFAULT_BUFFER_SIZE) { + soundLine.open(audioFormat); + } + else { + soundLine.open(audioFormat, bufferSize); + } + soundLine.start(); + lineBuffer = new byte[soundLine.getBufferSize()]; + soundBuffer = new SoundBuffer(STAGING_BUFFERS); } catch (LineUnavailableException e) { soundLine = null; } } + @Override + public void streamInitialized(int channelCount, int sampleRate) { + this.channelCount = channelCount; + this.sampleRate = sampleRate; + + // Workaround OS X's bad Java mixer + if (System.getProperty("os.name").contains("Mac OS X")) { + createSoundLine(STARING_BUFFER_SIZE); + reallocateLines = true; + } + else { + createSoundLine(DEFAULT_BUFFER_SIZE); + reallocateLines = false; + } + } + } diff --git a/src/com/limelight/binding/audio/SoundBuffer.java b/src/com/limelight/binding/audio/SoundBuffer.java new file mode 100644 index 0000000..8859bad --- /dev/null +++ b/src/com/limelight/binding/audio/SoundBuffer.java @@ -0,0 +1,61 @@ +package com.limelight.binding.audio; + +import java.nio.ByteBuffer; +import java.nio.ShortBuffer; +import java.util.LinkedList; + +import com.limelight.nvstream.av.ShortBufferDescriptor; + +public class SoundBuffer { + + private LinkedList bufferList; + private int maxBuffers; + + public SoundBuffer(int maxBuffers) { + this.bufferList = new LinkedList(); + this.maxBuffers = maxBuffers; + } + + public void queue(ShortBufferDescriptor buff) { + if (bufferList.size() > maxBuffers) { + bufferList.removeFirst(); + } + + bufferList.addLast(buff); + } + + public int size() { + int size = 0; + for (ShortBufferDescriptor desc : bufferList) { + size += desc.length; + } + return size; + } + + public int fill(byte[] data, int offset, int length) { + int filled = 0; + + // Convert offset and length to be relative to shorts + offset /= 2; + length /= 2; + + ShortBuffer sb = ByteBuffer.wrap(data).asShortBuffer(); + sb.position(offset); + while (length > 0 && !bufferList.isEmpty()) { + ShortBufferDescriptor buff = bufferList.getFirst(); + + if (buff.length > length) { + break; + } + + sb.put(buff.data, buff.offset, buff.length); + length -= buff.length; + filled += buff.length; + + bufferList.removeFirst(); + } + + // Return bytes instead of shorts + return filled * 2; + } +}