diff --git a/examples/audio/pom.xml b/examples/audio/pom.xml
new file mode 100644
index 0000000..22b5370
--- /dev/null
+++ b/examples/audio/pom.xml
@@ -0,0 +1,60 @@
+
+
+ 4.0.0
+
+ com.github.manevolent.ts3j
+ audio-example
+ 1.0-SNAPSHOT
+
+
+
+ tomp2p.net
+ tomp2p
+ http://tomp2p.net/dev/mvn/
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 1.8
+ 1.8
+
+
+
+
+
+
+
+ org.apache.commons
+ commons-lang3
+ 3.8.1
+
+
+
+
+ net.tomp2p
+ opus-wrapper
+ 1.4
+
+
+
+
+ com.sun.jna
+ jna
+ 3.0.9
+
+
+
+
+ com.github.manevolent
+ ts3j
+ 1.0-SNAPSHOT
+
+
+
\ No newline at end of file
diff --git a/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/AudioSender.java b/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/AudioSender.java
new file mode 100644
index 0000000..881abd6
--- /dev/null
+++ b/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/AudioSender.java
@@ -0,0 +1,73 @@
+package com.github.manevolent.ts3j.examples.audio;
+
+import com.github.manevolent.ts3j.identity.LocalIdentity;
+import com.github.manevolent.ts3j.protocol.client.ClientConnectionState;
+import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket;
+import com.github.manevolent.ts3j.util.Ts3Debugging;
+
+import static com.github.manevolent.ts3j.examples.audio.TeamspeakFastMixerSink.AUDIO_FORMAT;
+
+public class AudioSender {
+ public static void main(String[] args) throws Exception {
+ Ts3Debugging.setEnabled(false);
+
+ LocalTeamspeakClientSocket client = new LocalTeamspeakClientSocket();
+
+ LocalIdentity identity = LocalIdentity.generateNew(10);
+
+ client.setIdentity(identity);
+ client.setNickname(AudioSender.class.getSimpleName());
+ client.setHWID("TestTestTest");
+
+ TeamspeakFastMixerSink sink = new TeamspeakFastMixerSink(
+ AUDIO_FORMAT,
+ (int) AUDIO_FORMAT.getSampleRate() * AUDIO_FORMAT.getChannels() * 4 /*2=16bit float*/,
+ new OpusParameters(
+ 20,
+ 96000, // 96kbps
+ 10, // max complexity
+ 0, // 0 expected packet loss
+ false, // no VBR
+ false, // no FEC
+ true // OPUS MUSIC - channel doesn't have to be Opus Music ;)
+ )
+ );
+
+ client.setMicrophone(sink);
+
+ // -1 to 1 (signed) positions of samples
+ float[] thirtySecondsOfAudio = new float[(int) AUDIO_FORMAT.getSampleRate() * AUDIO_FORMAT.getChannels()];
+
+ // Generate a sine wave
+ for (int i = 0; i < thirtySecondsOfAudio.length; i ++) {
+ thirtySecondsOfAudio[i] = (float) Math.sin(i / 60f);
+ }
+
+ sink.start();
+
+ // Write into the sink
+ sink.write(thirtySecondsOfAudio, thirtySecondsOfAudio.length);
+
+ client.connect(
+ "your.teamspeak.server",
+ 10000L
+ );
+
+ sink.drain(); // this will block until the sink is stopped
+
+ System.err.println("Encoder time taken = " + sink.getNanotime() + " ns");
+ System.err.println("Audio position = " + sink.getPosition() + " samples sank");
+ System.err.println("Encoder position = " + sink.getEncoderPosition() + " samples encoded");
+ System.err.println("Network position = " + sink.getNetworkPosition() + " bytes sent");
+ System.err.println("Packets encoded = " + sink.getPacketsEncoded() + " packets encoded");
+ System.err.println("Packets sent = " + sink.getPacketsSent() + " packets sent");
+ System.err.println("Overflow events = " + sink.getOverflows());
+
+ // (resource starvation; can cause lost packets)
+ System.err.println("Underflow events = " + sink.getUnderflows());
+
+ client.disconnect("BYE");
+
+ System.exit(0);
+ }
+}
diff --git a/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/OpusDecoder.java b/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/OpusDecoder.java
new file mode 100644
index 0000000..0a6b360
--- /dev/null
+++ b/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/OpusDecoder.java
@@ -0,0 +1,83 @@
+package com.github.manevolent.ts3j.examples.audio;
+
+import com.sun.jna.ptr.PointerByReference;
+import net.tomp2p.opuswrapper.Opus;
+
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+
+public class OpusDecoder implements AutoCloseable {
+ private final PointerByReference decoder;
+ private final int sampleRate, frameSize, channels;
+
+ public OpusDecoder(int sampleRate, int frameSize, int channels) {
+ this.channels = channels;
+ this.sampleRate = sampleRate;
+ this.frameSize = frameSize;
+
+ IntBuffer errorBuffer = IntBuffer.allocate(1);
+
+ decoder = Opus.INSTANCE.opus_decoder_create(
+ sampleRate,
+ channels,
+ errorBuffer
+ );
+
+ OpusUtil.checkError("opus_decoder_create", errorBuffer.get());
+ }
+
+ public int reset() {
+ return OpusUtil.checkError(
+ "opus_decoder_ctl/OPUS_RESET_STATE",
+ Opus.INSTANCE.opus_decoder_ctl(decoder, Opus.OPUS_RESET_STATE)
+ );
+ }
+
+ public int setDecoderValue(int field, Object... values) {
+ return OpusUtil.checkError(
+ "opus_decoder_ctl/" + field,
+ Opus.INSTANCE.opus_decoder_ctl(decoder, field, values)
+ );
+ }
+
+ public int decodePLC(float[] floats) {
+ return decode(null, floats);
+ }
+
+ public int decode(byte[] packet, float[] floats) {
+ if (floats == null)
+ throw new NullPointerException("floats");
+ else if (floats.length < frameSize * channels)
+ throw new IllegalArgumentException(floats.length + " < " + frameSize * channels);
+
+ int result = Opus.INSTANCE.opus_decode_float(
+ decoder,
+ packet,
+ packet == null ? 0 : packet.length,
+ FloatBuffer.wrap(floats),
+ frameSize,
+ 0
+ );
+
+ OpusUtil.checkError("opus_decode_float", result);
+
+ return result;
+ }
+
+ public int getSampleRate() {
+ return sampleRate;
+ }
+
+ public int getChannels() {
+ return channels;
+ }
+
+ @Override
+ public void close() {
+ Opus.INSTANCE.opus_decoder_destroy(decoder);
+ }
+
+ public static String getVersion() {
+ return Opus.INSTANCE.opus_get_version_string();
+ }
+}
diff --git a/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/OpusEncoder.java b/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/OpusEncoder.java
new file mode 100644
index 0000000..48ec239
--- /dev/null
+++ b/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/OpusEncoder.java
@@ -0,0 +1,201 @@
+package com.github.manevolent.ts3j.examples.audio;
+
+import com.sun.jna.ptr.PointerByReference;
+import net.tomp2p.opuswrapper.Opus;
+import org.apache.commons.lang3.ArrayUtils;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.IntBuffer;
+import java.nio.ShortBuffer;
+
+/**
+ * Thread-safe opus encoder
+ */
+public class OpusEncoder implements AutoCloseable {
+ private static final int[] OPUS_PERMITTED_SAMPLE_RATES =
+ { 8000, 12000, 16000, 24000, 48000 };
+
+ private static final int[] OPUS_PERMITTED_FRAME_SIZES =
+ { 120, 240, 480, 960, 1920, 2880 };
+
+ private static final int[] OPUS_PERMITTED_CHANNEL_COUNTS =
+ { 1, 2 };
+
+ private final PointerByReference encoder;
+ private final int sampleRate, frameSize, channels;
+ private final int expectedByteSize;
+ private final boolean bigEndian;
+
+ private final ShortBuffer sourceShortBuffer;
+ private final ByteBuffer sourceByteBuffer;
+ private final ByteBuffer targetBuffer;
+
+ private final Object encoderLock = new Object();
+
+ private boolean closed = false;
+
+ public OpusEncoder(int sampleRate, int frameSize, int channels, boolean bigEndian) {
+ this(sampleRate, frameSize, channels, bigEndian, 4096);
+ }
+
+ public OpusEncoder(int sampleRate, int frameSize,
+ int channels, boolean bigEndian,
+ int maxPacketLength) {
+ this.channels = channels;
+ this.sampleRate = sampleRate;
+ this.frameSize = frameSize;
+ this.bigEndian = bigEndian;
+ this.expectedByteSize = frameSize * channels * 2;
+
+ if (!ArrayUtils.contains(OPUS_PERMITTED_FRAME_SIZES, frameSize))
+ throw new IllegalArgumentException("Invalid Opus frame size: " + frameSize);
+
+ if (!ArrayUtils.contains(OPUS_PERMITTED_SAMPLE_RATES, sampleRate))
+ throw new IllegalArgumentException("Invalid Opus sample rate: " + sampleRate);
+
+ if (!ArrayUtils.contains(OPUS_PERMITTED_CHANNEL_COUNTS, channels))
+ throw new IllegalArgumentException("Invalid Opus channel count: " + channels);
+
+ this.sourceShortBuffer = ShortBuffer.allocate(frameSize * channels);
+ this.sourceByteBuffer = ByteBuffer.allocate(frameSize * channels * 2);
+ this.sourceByteBuffer.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN);
+
+ this.targetBuffer = ByteBuffer.allocate(maxPacketLength);
+
+ IntBuffer errorBuffer = IntBuffer.allocate(1);
+
+ encoder = Opus.INSTANCE.opus_encoder_create(
+ sampleRate,
+ channels,
+ Opus.OPUS_APPLICATION_AUDIO,
+ errorBuffer
+ );
+
+ if (encoder == null) throw new NullPointerException("encoder");
+
+ OpusUtil.checkError(
+ "opus_encoder_create",
+ errorBuffer.get()
+ );
+ }
+
+ public void reset() {
+ synchronized (encoderLock) {
+ OpusUtil.checkError(
+ "opus_encoder_ctl/OPUS_RESET_STATE",
+ Opus.INSTANCE.opus_encoder_ctl(encoder, Opus.OPUS_RESET_STATE)
+ );
+ }
+ }
+
+ public int setEncoderValue(int field, Object... values) {
+ synchronized (encoderLock) {
+ if (closed) throw new IllegalStateException("encoder closed");
+
+ return OpusUtil.checkError(
+ "opus_encoder_ctl/" + field,
+ Opus.INSTANCE.opus_encoder_ctl(encoder, field, values)
+ );
+ }
+ }
+
+ public byte[] encode(float[] floats, int len) {
+ if (len != getFrameSize() * getChannels())
+ throw new IllegalArgumentException(len + " != " + getFrameSize() * getChannels());
+
+ targetBuffer.clear();
+
+ int result;
+ synchronized (encoderLock) {
+ if (closed) throw new IllegalStateException("encoder closed");
+
+ if (encoder == null) throw new IllegalStateException("encoder is null");
+
+ result = Opus.INSTANCE.opus_encode_float(
+ encoder,
+ floats,
+ frameSize, // this is STATIC.
+ targetBuffer,
+ targetBuffer.capacity()
+ );
+ }
+
+ OpusUtil.checkError(
+ "opus_encode_float," +
+ " ch=" + channels +
+ " smprate=" + sampleRate +
+ " rate=" + sampleRate +
+ " len=" + len + "/" + floats.length + ", frame_size=" + frameSize +
+ ", max_data_bytes=" + targetBuffer.capacity(),
+ result
+ );
+
+ byte[] encoded = new byte[result];
+ targetBuffer.get(encoded);
+
+ return encoded;
+ }
+
+ public byte[] encode(float[] floats) {
+ return encode(floats, floats.length);
+ }
+
+ public byte[] encode(byte[] pcm) {
+ if (pcm.length != expectedByteSize)
+ throw new IllegalArgumentException(pcm.length + " != " + expectedByteSize);
+
+ // Clear source and input the PCM samples
+ sourceByteBuffer.clear();
+ sourceByteBuffer.put(pcm);
+ sourceByteBuffer.flip();
+
+ sourceShortBuffer.clear();
+ while (sourceByteBuffer.position() < sourceByteBuffer.limit())
+ sourceShortBuffer.put(sourceByteBuffer.getShort());
+
+ // Clear target
+ sourceShortBuffer.flip();
+ targetBuffer.clear();
+
+ int result;
+
+ synchronized (encoderLock) {
+ if (closed) throw new IllegalStateException("encoder closed");
+
+ result = Opus.INSTANCE.opus_encode(
+ encoder,
+ sourceShortBuffer,
+ frameSize, // this is STATIC.
+ targetBuffer,
+ targetBuffer.capacity()
+ );
+ }
+
+ OpusUtil.checkError("opus_encode", result);
+
+ byte[] encoded = new byte[result];
+ targetBuffer.get(encoded);
+
+ return encoded;
+ }
+
+ public int getFrameSize() { return frameSize; }
+
+ public int getSampleRate() {
+ return sampleRate;
+ }
+
+ public int getChannels() {
+ return channels;
+ }
+
+ @Override
+ public void close() {
+ synchronized (encoderLock) {
+ if (closed) throw new IllegalStateException("already closed");
+ Opus.INSTANCE.opus_encoder_destroy(encoder);
+ closed = true;
+ }
+ }
+}
diff --git a/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/OpusParameters.java b/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/OpusParameters.java
new file mode 100644
index 0000000..1c92517
--- /dev/null
+++ b/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/OpusParameters.java
@@ -0,0 +1,51 @@
+package com.github.manevolent.ts3j.examples.audio;
+
+public class OpusParameters {
+ private final int opusFrameRate;
+ private final int opusBitrate;
+ private final int opusComplexity;
+ private final int opusPacketLoss;
+ private final boolean opusVbr;
+ private final boolean opusFec;
+ private final boolean opusMusic;
+
+ public OpusParameters(int opusFrameRate, int opusBitrate,
+ int opusComplexity, int opusPacketLoss,
+ boolean opusVbr, boolean opusFec, boolean opusMusic) {
+ this.opusFrameRate = opusFrameRate;
+ this.opusBitrate = opusBitrate;
+ this.opusComplexity = opusComplexity;
+ this.opusPacketLoss = opusPacketLoss;
+ this.opusVbr = opusVbr;
+ this.opusFec = opusFec;
+ this.opusMusic = opusMusic;
+ }
+
+ public int getOpusFrameTime() {
+ return opusFrameRate;
+ }
+
+ public int getOpusBitrate() {
+ return opusBitrate;
+ }
+
+ public int getOpusComplexity() {
+ return opusComplexity;
+ }
+
+ public int getOpusPacketLoss() {
+ return opusPacketLoss;
+ }
+
+ public boolean isOpusVbr() {
+ return opusVbr;
+ }
+
+ public boolean isOpusFec() {
+ return opusFec;
+ }
+
+ public boolean isOpusMusic() {
+ return opusMusic;
+ }
+}
\ No newline at end of file
diff --git a/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/OpusUtil.java b/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/OpusUtil.java
new file mode 100644
index 0000000..95f6a3e
--- /dev/null
+++ b/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/OpusUtil.java
@@ -0,0 +1,26 @@
+package com.github.manevolent.ts3j.examples.audio;
+
+import net.tomp2p.opuswrapper.Opus;
+
+public class OpusUtil {
+ public static int checkError(String description, int returnCode) throws RuntimeException {
+ if (returnCode < Opus.OPUS_OK)
+ throw new RuntimeException("Opus error [" + description + "] (" + returnCode + "): " +
+ Opus.INSTANCE.opus_strerror(returnCode).trim()
+ );
+ else return returnCode;
+ }
+
+ public static int checkError(int returnCode) throws RuntimeException {
+ if (returnCode < Opus.OPUS_OK)
+ throw new RuntimeException(
+ "Opus error (" + returnCode + "): " +
+ Opus.INSTANCE.opus_strerror(returnCode).trim()
+ );
+ else return returnCode;
+ }
+
+ public static String getVersion() {
+ return Opus.INSTANCE.opus_get_version_string();
+ }
+}
diff --git a/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/TeamspeakFastMixerSink.java b/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/TeamspeakFastMixerSink.java
new file mode 100644
index 0000000..7454e03
--- /dev/null
+++ b/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/TeamspeakFastMixerSink.java
@@ -0,0 +1,410 @@
+package com.github.manevolent.ts3j.examples.audio;
+
+import com.github.manevolent.ts3j.enums.CodecType;
+import net.tomp2p.opuswrapper.Opus;
+
+import javax.sound.sampled.AudioFormat;
+import java.util.NoSuchElementException;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class TeamspeakFastMixerSink implements TeamspeakMixerSink {
+ public static final AudioFormat AUDIO_FORMAT =
+ new AudioFormat(48000, 32, 2, true, false);
+
+ private final Object stateLock = new Object();
+ private final Object drainLock = new Object();
+
+ // Audio format
+ private final AudioFormat audioFormat; // Audio output format (mostly just ensured to match certain parameters)
+
+ // OPUS variables
+ private final OpusParameters opusParameters; // Opus parameters set by configurations
+ private volatile OpusEncoder encoder = null; // Opus encoder instance
+ private final int opusFrameSize; // Opus frame size (PER CHANNEL!) (also the ASIO chunk size)
+ private long opusPacketsEncoded = 0, opusPacketsSent = 0;
+ private long opusTime = 0;
+ private long networkTime = 0;
+ private long opusPosition = 0;
+ private long opusBytePosition = 0;
+
+ private long waitTime = 0;
+
+ // Statistics variables
+ private long underflowed = 0; // Underflowed samples (ASIO has to return 0 samples to the encoder)
+ private long overflowed = 0; // Overflowed samples (write returns 0)
+ private long clipped = 0; // Clipped samples (outside -1F,1F bounds)
+ private long position = 0; // Position in samples
+
+ // I/O variables
+ private final Queue packetQueue = new ConcurrentLinkedQueue<>();
+ private final int bufferSize; // Buffer size, in samples
+ private volatile int availableInput;
+ private volatile int samplePosition; // Position, in samples, of the ASIO buffer (available samples)
+ private final float[] sampleBuffer; // ASIO buffer
+
+ // Mixer sink state variables
+ private volatile boolean running = false, opening = false;
+
+ public TeamspeakFastMixerSink(AudioFormat audioFormat,
+ int bufferSizeInBytes,
+ OpusParameters opusParameters) {
+
+ this.audioFormat = audioFormat;
+
+ if (audioFormat.getSampleRate() != 48000)
+ throw new IllegalArgumentException("Invalid audio sample rate: " +
+ audioFormat.getSampleRate() + " != 48000: OPUS requires 48KHz audio");
+ else if (audioFormat.getChannels() != 2)
+ throw new IllegalArgumentException("Invalid audio channel count: " +
+ audioFormat.getChannels() + " != 2: OPUS requires stereo audio");
+
+ this.bufferSize = bufferSizeInBytes / (audioFormat.getSampleSizeInBits() / 8);
+ if (bufferSize <= 0 || bufferSize % audioFormat.getChannels() != 0)
+ throw new IllegalArgumentException("invalid bufferSize: " + bufferSize);
+
+ this.sampleBuffer = new float[bufferSize];
+ this.availableInput = 0;
+
+ this.opusParameters = opusParameters;
+ this.opusFrameSize = (int) getAudioFormat().getSampleRate() / (1000 / opusParameters.getOpusFrameTime());
+ }
+
+ public AudioFormat getAudioFormat() {
+ return audioFormat;
+ }
+
+ public void write(float[] buffer, int len) {
+ synchronized (stateLock) {
+ if (!running) throw new IllegalStateException("not running");
+
+ if (len > availableInput()) { // This should never happen
+ overflowed++; // Probably never will run
+ throw new IllegalArgumentException(len + " > " + availableInput());
+ }
+
+ if (len > buffer.length) {
+ overflowed++;
+ throw new IllegalArgumentException(len + " > " + buffer.length);
+ }
+
+ if (len <= 0)
+ throw new IllegalArgumentException(len + " <= 0");
+
+ if (len % getChannels() != 0)
+ throw new IllegalArgumentException("not a full frame");
+
+ // Write to the buffer
+ System.arraycopy(buffer, 0, sampleBuffer, samplePosition, len);
+ samplePosition += len;
+ availableInput -= len;
+
+ // Read samples from the buffer and encode them into packets
+ position += encode(false);
+ }
+ }
+
+ /**
+ * Encodes the buffer to packets for the packet queue
+ * @return Packets encoded
+ */
+ private int encode(boolean flush) {
+ if (flush) Logger.getGlobal().log(Level.FINE, "Flushing TeamspeakFastMixerSink...");
+
+ int written = 0;
+ int frameSize = opusFrameSize * getChannels();
+ byte[] encoded;
+ long now;
+ int copy;
+
+ while (samplePosition >= (flush ? 1 : frameSize)) {
+ copy = Math.min(samplePosition, frameSize);
+ if (flush && copy < frameSize) {
+ for (int i = copy; i < frameSize; i++)
+ sampleBuffer[i] = 0f;
+
+ samplePosition = frameSize;
+ }
+
+ if (encoder == null) openOpusEncoder();
+
+ now = System.nanoTime();
+ encoded = encoder.encode(sampleBuffer, frameSize);
+ opusTime += (System.nanoTime() - now);
+ opusPacketsEncoded ++;
+ opusPosition += frameSize;
+
+ System.arraycopy(sampleBuffer, frameSize, sampleBuffer, 0, samplePosition - frameSize);
+ samplePosition -= frameSize;
+ if (packetQueue.add(new OpusPacket(frameSize, encoded)))
+ written+=frameSize;
+ }
+
+ if (written > 0) opening = false;
+
+ if (flush) Logger.getGlobal().log(Level.FINE, "Flushed TeamspeakFastMixerSink.");
+
+ return written;
+ }
+
+ public long getNanotime() {
+ return opusTime;
+ }
+ public long getFrameSize() {
+ return opusFrameSize;
+ }
+ public long getEncoderPosition() {
+ return opusPosition;
+ }
+ public long getNetworkPosition() {
+ return opusBytePosition;
+ }
+ public OpusParameters getEncoderParameters() {
+ return opusParameters;
+ }
+
+ @Override
+ public long getPacketsEncoded() {
+ return opusPacketsEncoded;
+ }
+
+ @Override
+ public long getPacketsSent() {
+ return opusPacketsSent;
+ }
+
+ /**
+ * Finds the count of available samples to be written to write() in the len param
+ * @return Available sample count
+ */
+ public int availableInput() {
+ return availableInput;
+ }
+
+ public boolean isRunning() {
+ return running;
+ }
+
+ /**
+ * Opens the Opus encoder
+ */
+ private void openOpusEncoder() {
+ synchronized (stateLock) {
+ if (encoder != null) return;
+ Logger.getGlobal().log(Level.FINE, "Opening TeamspeakFastMixerSink encoder...");
+
+ encoder = new OpusEncoder(
+ (int) getAudioFormat().getSampleRate(), // smp rate (always 48kHz)
+ opusFrameSize,
+ getChannels(),
+ audioFormat.isBigEndian()
+ //,MAXIMUM_OPUS_PACKET_SIZE // Prevent IP fragmentation
+ );
+
+ encoder.setEncoderValue(
+ Opus.OPUS_SET_SIGNAL_REQUEST,
+ opusParameters.isOpusMusic() ? Opus.OPUS_SIGNAL_MUSIC : Opus.OPUS_SIGNAL_VOICE
+ );
+
+ encoder.setEncoderValue(Opus.OPUS_SET_BITRATE_REQUEST, opusParameters.getOpusBitrate());
+ encoder.setEncoderValue(Opus.OPUS_SET_COMPLEXITY_REQUEST, opusParameters.getOpusComplexity());
+ encoder.setEncoderValue(Opus.OPUS_SET_PACKET_LOSS_PERC_REQUEST, opusParameters.getOpusPacketLoss());
+ encoder.setEncoderValue(Opus.OPUS_SET_VBR_REQUEST, opusParameters.isOpusVbr() ? 1 : 0);
+ encoder.setEncoderValue(Opus.OPUS_SET_INBAND_FEC_REQUEST, opusParameters.isOpusFec() ? 1 : 0);
+
+ Logger.getGlobal().log(Level.FINE, "Opened TeamspeakFastMixerSink encoder.");
+ }
+ }
+
+ private void closeOpusEncoder() {
+ synchronized (stateLock) {
+ if (encoder != null) {
+ Logger.getGlobal().log(Level.FINE, "Closing TeamspeakFastMixerSink encoder...");
+ encoder.close();
+ encoder = null;
+ Logger.getGlobal().log(Level.FINE, "Closed TeamspeakFastMixerSink encoder.");
+ }
+ }
+ }
+
+ public void close() {
+ if (isRunning()) stop();
+
+ closeOpusEncoder();
+ }
+
+ public boolean start() {
+ synchronized (stateLock) {
+ if (running) return false;
+
+ Logger.getGlobal().log(Level.FINE, "Starting TeamspeakFastMixerSink...");
+
+ // Flush buffers, clear outgoing packet queues.
+ for (int i = 0; i < sampleBuffer.length; i ++) sampleBuffer[i] = 0f;
+ samplePosition = 0;
+ availableInput = bufferSize;
+ packetQueue.clear();
+
+ // Open (or re-open) Opus encoder.
+ openOpusEncoder();
+
+ // Reset the encoder
+ if (encoder != null) {
+ Logger.getGlobal().log(Level.FINE, "Resetting TeamspeakFastMixerSink...");
+ encoder.reset(); // Reset encoder output
+ Logger.getGlobal().log(Level.FINE, "Reset TeamspeakFastMixerSink.");
+ }
+
+ // Mark as running.
+ opening = true;
+ running = true;
+
+ Logger.getGlobal().log(Level.FINE, "Started TeamspeakFastMixerSink.");
+
+ return true;
+ }
+ }
+
+ public void drain() throws InterruptedException {
+ synchronized (drainLock) {
+ while (running && packetQueue.peek() != null) {
+ drainLock.wait();
+ }
+ }
+ }
+
+ /**
+ * Closes mixer sink
+ *
+ * Note that we don't close the Opus encoder - just in case there are more samples to write/flush out.
+ */
+ public boolean stop() {
+ synchronized (stateLock) {
+ if (!running) return false;
+
+ Logger.getGlobal().log(Level.FINE, "Stopping TeamspeakFastMixerSink...");
+
+ try {
+ encode(true);
+ } catch (RuntimeException e) {
+ Logger.getGlobal().log(Level.WARNING, "Problem flushing audio buffer upon close", e);
+ }
+
+ running = false;
+ Logger.getGlobal().log(Level.FINE, "Stopped TeamspeakFastMixerSink.");
+
+ return true;
+ }
+ }
+
+ public int getBufferSize() {
+ return bufferSize;
+ }
+
+ public long getPosition() {
+ return position;
+ }
+
+ public long getClippedSamples() {
+ return clipped;
+ }
+
+ public long getUnderflows() {
+ return underflowed;
+ }
+
+ public long getOverflows() {
+ return overflowed;
+ }
+
+ /**
+ * Called by the TS3J encoder thread to determine if the sink is ready to send samples.
+ * @return true if the sink has samples to write, or if samples will become available.
+ */
+ @Override
+ public boolean isReady() {
+ return (running && !opening) || packetQueue.size() > 0;
+ }
+
+ @Override
+ public CodecType getCodec() {
+ return CodecType.OPUS_MUSIC;
+ }
+
+ /**
+ * Provides OPUS encoded audio (as a packet) to the TS3J UDP audio network thread.
+ * @return Encoded OPUS audio packet (zero-length packet if there is an underflow).
+ */
+ @Override
+ public byte[] provide() {
+ long start = System.nanoTime();
+
+ try {
+ if (packetQueue.peek() == null) {
+ underflowed ++;
+ return new byte[0];
+ }
+
+ OpusPacket packet = packetQueue.remove();
+
+ if (packet == null) {
+ underflowed ++;
+ return new byte[0];
+ }
+
+ availableInput += packet.getSamples();
+ opusBytePosition += packet.getBytes().length;
+ opusPacketsSent ++;
+
+ return packet.getBytes();
+ } catch (NoSuchElementException ex) {
+ underflowed ++;
+ return new byte[0];
+ } finally {
+ long networkTime = System.nanoTime() - start;
+
+ if (networkTime >= (opusParameters.getOpusFrameTime() * 1000000L)) {
+ underflowed++;
+
+ Logger.getGlobal().warning("[TeamspeakFastMixerSink] provide() took longer than the expected " +
+ opusParameters.getOpusFrameTime() + "ms: " + ((double)networkTime / 1_000_000D) + "ms");
+ }
+
+ this.networkTime += networkTime;
+
+ if (packetQueue.peek() == null) {
+ synchronized (drainLock) {
+ drainLock.notifyAll();
+ }
+ }
+ }
+ }
+
+ public int getChannels() {
+ return getAudioFormat().getChannels();
+ }
+
+ public long getWaitTime() {
+ return waitTime;
+ }
+
+ private final class OpusPacket {
+ private final int samples;
+ private final byte[] bytes;
+
+ protected OpusPacket(int samples, byte[] bytes) {
+ this.samples = samples;
+ this.bytes = bytes;
+ }
+
+ public int getSamples() {
+ return samples;
+ }
+
+ public byte[] getBytes() {
+ return bytes;
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/TeamspeakMixerSink.java b/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/TeamspeakMixerSink.java
new file mode 100644
index 0000000..22b8afa
--- /dev/null
+++ b/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/TeamspeakMixerSink.java
@@ -0,0 +1,54 @@
+package com.github.manevolent.ts3j.examples.audio;
+
+import com.github.manevolent.ts3j.audio.Microphone;
+
+public interface TeamspeakMixerSink extends Microphone {
+ /**
+ * Gets the Encoder parameters
+ * @return Encoder parameters
+ */
+ OpusParameters getEncoderParameters();
+
+ /**
+ * Gets the count of encoded Encoder packets
+ * @return Encoder packets
+ */
+ long getPacketsEncoded();
+
+ /**
+ * Gets the count of written encoded Encoder packets
+ * @return Encoder packets
+ */
+ long getPacketsSent();
+
+ /**
+ * Gets the time spent encoding Encoder packets, in nanoseconds
+ * @return Encoder time
+ */
+ long getNanotime();
+
+ /**
+ * Gets the Encoder frame size for packets
+ * @return Encoder frame size
+ */
+ long getFrameSize();
+
+ /**
+ * Gets the Encoder position in samples
+ * @return Encoder position
+ */
+ long getEncoderPosition();
+
+ /**
+ * Gets the Encoder network position in bytes
+ * @return Encoder network position
+ */
+ long getNetworkPosition();
+
+ /**
+ * Gets the channel count
+ * @return Channel count
+ */
+ int getChannels();
+
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java
index 2018ea3..4780f1c 100644
--- a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java
+++ b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java
@@ -1088,16 +1088,34 @@ public class LocalTeamspeakClientSocket
throws
IOException, TimeoutException,
ExecutionException, InterruptedException {
+ disconnect(null);
+ }
+
+ public void disconnect(String reason)
+ throws
+ IOException, TimeoutException,
+ ExecutionException, InterruptedException {
try {
switch (getState()) {
case CONNECTING:
case RETRIEVING_DATA:
waitForState(ClientConnectionState.CONNECTED, 10000L);
case CONNECTED:
- writePacket(new PacketBody2Command(
- ProtocolRole.CLIENT,
- new SingleCommand("clientdisconnect", ProtocolRole.CLIENT))
- );
+ if (reason == null) {
+ writePacket(new PacketBody2Command(
+ ProtocolRole.CLIENT,
+ new SingleCommand("clientdisconnect", ProtocolRole.CLIENT))
+ );
+ } else {
+ writePacket(new PacketBody2Command(
+ ProtocolRole.CLIENT,
+ new SingleCommand(
+ "clientdisconnect", ProtocolRole.CLIENT,
+ new CommandSingleParameter("reasonid", "8"),
+ new CommandSingleParameter("reasonmsg", reason)
+ ))
+ );
+ }
waitForState(ClientConnectionState.DISCONNECTED, 30000L);
diff --git a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java
index ca51b07..c02eb7c 100644
--- a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java
+++ b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java
@@ -10,6 +10,7 @@ import com.github.manevolent.ts3j.protocol.client.ClientConnectionState;
import com.github.manevolent.ts3j.protocol.packet.PacketBody0Voice;
import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket;
import com.github.manevolent.ts3j.util.Ts3Debugging;
+import com.sun.security.ntlm.Server;
import java.math.BigInteger;
import java.net.InetSocketAddress;
@@ -20,34 +21,29 @@ import static org.junit.Assert.assertEquals;
public class ServerConnectionTest {
public static void main(String[] args) throws Exception {
+ Ts3Debugging.setEnabled(true);
LocalTeamspeakClientSocket client = new LocalTeamspeakClientSocket();
- Ts3Debugging.setEnabled(false);
-
LocalIdentity identity = LocalIdentity.generateNew(10);
client.setIdentity(identity);
- client.setNickname("ts3j issue 2");
+ client.setNickname(ServerConnectionTest.class.getSimpleName());
client.setHWID("TestTestTest");
- client.setVoiceHandler(packet ->
- System.out.println(packet.getServerFlag0())
- );
+ try {
+ client.connect(
+ "teamlixo.net",
+ 10000L
+ );
- while (true) {
- try {
- client.connect(
- "teamlixo.net",
- 10000L
- );
+ assertEquals(client.getState(), ClientConnectionState.CONNECTED);
- assertEquals(client.getState(), ClientConnectionState.CONNECTED);
- } catch (Exception ex) {
- ex.printStackTrace();
- }
-
- Thread.sleep(30000);
+ client.disconnect("BYE");
+ } catch (Exception ex) {
+ ex.printStackTrace();
}
+
+ Thread.sleep(3000L);
}
}