Added audio sender test and disconnect reasons
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.github.manevolent.ts3j</groupId>
|
||||
<artifactId>audio-example</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>tomp2p.net</id>
|
||||
<name>tomp2p</name>
|
||||
<url>http://tomp2p.net/dev/mvn/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.8.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Opus playback -->
|
||||
<dependency>
|
||||
<groupId>net.tomp2p</groupId>
|
||||
<artifactId>opus-wrapper</artifactId>
|
||||
<version>1.4</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Careful since this dependency may conflict -->
|
||||
<dependency>
|
||||
<groupId>com.sun.jna</groupId>
|
||||
<artifactId>jna</artifactId>
|
||||
<version>3.0.9</version>
|
||||
</dependency>
|
||||
|
||||
<!-- if you've loaded the entire repo into your IDE, and ran the maven "install" step this should work -->
|
||||
<dependency>
|
||||
<groupId>com.github.manevolent</groupId>
|
||||
<artifactId>ts3j</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+73
@@ -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);
|
||||
}
|
||||
}
|
||||
+83
@@ -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();
|
||||
}
|
||||
}
|
||||
+201
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
+410
@@ -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<OpusPacket> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -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();
|
||||
|
||||
}
|
||||
+22
-4
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user