You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
197 lines
6.4 KiB
197 lines
6.4 KiB
package com.jotuntech.sketcher.client.voice;
|
|
|
|
import java.io.IOException;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.ByteOrder;
|
|
|
|
import org.xiph.speex.SpeexDecoder;
|
|
|
|
|
|
public class VoiceChannel {
|
|
public final static int SPEEX_FRAME_BYTES = 640;
|
|
|
|
private SpeexDecoder decoder;
|
|
private ByteBuffer[] packetArray;
|
|
private int expectedPacketNumber;
|
|
private int newestPacketNumber;
|
|
private float qualityPercent;
|
|
private ByteBuffer audioBuffer;
|
|
private VoiceMixer voiceMixer;
|
|
private Integer channelKey;
|
|
private boolean buffering;
|
|
private long lastPlayTime;
|
|
private boolean dead;
|
|
private VoiceListener listener;
|
|
|
|
public VoiceChannel(VoiceMixer voiceMixer, Integer channelKey) {
|
|
this.voiceMixer = voiceMixer;
|
|
this.channelKey = channelKey;
|
|
|
|
packetArray = new ByteBuffer[voiceMixer.getBufferSize() / SPEEX_FRAME_BYTES];
|
|
expectedPacketNumber = -1;
|
|
newestPacketNumber = 0;
|
|
qualityPercent = 50f;
|
|
|
|
decoder = new SpeexDecoder();
|
|
decoder.init(1, 16000, 1, true);
|
|
|
|
buffering = true;
|
|
|
|
lastPlayTime = System.currentTimeMillis();
|
|
dead = false;
|
|
}
|
|
|
|
public void packet(ByteBuffer packetBuffer) throws IOException {
|
|
if(lastPlayTime < System.currentTimeMillis() - 10000) {
|
|
dead = true;
|
|
return;
|
|
}
|
|
|
|
if(packetBuffer.remaining() < 4) {
|
|
/** Packet too short to even contain its own number */
|
|
return;
|
|
}
|
|
|
|
int packetNumber = packetBuffer.getInt();
|
|
if(expectedPacketNumber == -1) {
|
|
/** Lock on first packet in sequence */
|
|
expectedPacketNumber = packetNumber;
|
|
}
|
|
if(packetNumber - expectedPacketNumber > packetArray.length - 1) {
|
|
/** Packet overflow */
|
|
|
|
/** Decode the N packets up to, and including, the overflowed packet */
|
|
expectedPacketNumber = packetNumber - packetArray.length + 1;
|
|
newestPacketNumber = packetNumber;
|
|
|
|
/** Place overflowing packet at head of the queue */
|
|
packetArray[packetNumber % packetArray.length] = packetBuffer;
|
|
|
|
if(listener != null) {
|
|
listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_PACKET_OVERFLOW, channelKey));
|
|
}
|
|
} else if(packetNumber < expectedPacketNumber) {
|
|
/** Packet arrived too late and is dropped */
|
|
} else {
|
|
/** Packet has arrived in order. */
|
|
packetArray[packetNumber % packetArray.length] = packetBuffer;
|
|
if(packetNumber > newestPacketNumber) {
|
|
/** Tell decoder to expect newer packets. */
|
|
newestPacketNumber = packetNumber;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void process() throws IOException {
|
|
if(lastPlayTime < System.currentTimeMillis() - 10000) {
|
|
dead = true;
|
|
return;
|
|
}
|
|
|
|
/** Hold off playback until we are buffered up */
|
|
if(expectedPacketNumber == -1 || (buffering && newestPacketNumber - expectedPacketNumber < packetArray.length - 1)) {
|
|
if(listener != null) {
|
|
int bufferPercent = expectedPacketNumber == -1 ? 0 : Math.max(0, ((newestPacketNumber - expectedPacketNumber) * 200) / packetArray.length);
|
|
listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_PACKET_BUFFERING, channelKey, bufferPercent));
|
|
}
|
|
return;
|
|
}
|
|
|
|
buffering = false;
|
|
|
|
/** Attempt to decode packets if there is room for them in the voice mixer buffer. */
|
|
while(voiceMixer.available(channelKey) >= SPEEX_FRAME_BYTES) {
|
|
/** Look for the next expected packet */
|
|
ByteBuffer packetBuffer = packetArray[expectedPacketNumber % packetArray.length];
|
|
if(packetBuffer == null) {
|
|
/** We have no packet. What to do? */
|
|
if(voiceMixer.available(channelKey) >= voiceMixer.getBufferSize() / 2) {
|
|
/** Playback buffer is half empty, so we "decode" a lost packet. */
|
|
decoder.processData(true);
|
|
|
|
/** Reduce quality average for missing packets */
|
|
qualityPercent -= qualityPercent * 0.1f;
|
|
|
|
if(expectedPacketNumber > newestPacketNumber) {
|
|
/** Packet underflow */
|
|
buffering = true;
|
|
expectedPacketNumber = -1;
|
|
if(listener != null) {
|
|
int bufferPercent = Math.max(0, ((newestPacketNumber - expectedPacketNumber) * 200) / packetArray.length);
|
|
listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_PACKET_UNDERFLOW, channelKey, bufferPercent, (int) qualityPercent));
|
|
}
|
|
break;
|
|
} else {
|
|
/** Packet missing */
|
|
++expectedPacketNumber;
|
|
if(listener != null) {
|
|
int bufferPercent = Math.max(0, ((newestPacketNumber - expectedPacketNumber) * 200) / packetArray.length);
|
|
listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_PACKET_LOST, channelKey, bufferPercent, (int) qualityPercent));
|
|
}
|
|
}
|
|
} else {
|
|
/** Playback buffer is good, so we wait for the packet. */
|
|
break;
|
|
}
|
|
} else {
|
|
/** We have a packet and we retrieve it. */
|
|
byte[] packetBufferArray = packetBuffer.array();
|
|
int dataOffset = packetBuffer.arrayOffset() + packetBuffer.position();
|
|
int dataLength = packetBuffer.remaining();
|
|
|
|
/** Decode the packet. */
|
|
decoder.processData(packetBufferArray, dataOffset, dataLength);
|
|
|
|
/** Erase packet and increment counter */
|
|
packetArray[expectedPacketNumber++ % packetArray.length] = null;
|
|
|
|
/** Update last play time */
|
|
lastPlayTime = System.currentTimeMillis();
|
|
|
|
if(listener != null) {
|
|
int bufferPercent = ((newestPacketNumber - expectedPacketNumber) * 200) / packetArray.length;
|
|
qualityPercent += (100 - qualityPercent) * 0.1f;
|
|
listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_PACKET_RECEIVED, channelKey, bufferPercent, (int) qualityPercent));
|
|
}
|
|
}
|
|
|
|
/** Read the decoded packet into audio buffer */
|
|
int audioByteLength = decoder.getProcessedDataByteSize();
|
|
if(audioBuffer == null || audioByteLength > audioBuffer.capacity()) {
|
|
audioBuffer = ByteBuffer.allocate(audioByteLength);
|
|
audioBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
System.err.println("Created " + audioByteLength + " byte audio buffer.");
|
|
}
|
|
audioBuffer.clear();
|
|
decoder.getProcessedData(audioBuffer.array(), audioBuffer.arrayOffset() + audioBuffer.position());
|
|
audioBuffer.position(audioBuffer.position() + audioByteLength);
|
|
audioBuffer.flip();
|
|
|
|
if(listener != null) {
|
|
/** Measure peak volume of audio buffer */
|
|
int peak = 0;
|
|
for(int i = audioBuffer.position(); i < audioBuffer.limit(); i += 2) {
|
|
int absSample = Math.abs(audioBuffer.getShort(i));
|
|
if(absSample > peak) {
|
|
peak = absSample;
|
|
}
|
|
}
|
|
|
|
/** Notify listener of peak volume */
|
|
listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_PACKET_VOLUME, channelKey, peak));
|
|
}
|
|
|
|
|
|
/** Write audio buffer to voice mixer. */
|
|
voiceMixer.write(channelKey, audioBuffer);
|
|
}
|
|
}
|
|
|
|
public boolean isDead() {
|
|
return dead;
|
|
}
|
|
|
|
public void setListener(VoiceListener listener) {
|
|
this.listener = listener;
|
|
}
|
|
} |