diff --git a/examples/audio/bensound-ukulele.mp3 b/examples/audio/bensound-ukulele.mp3 new file mode 100644 index 0000000..c56b657 Binary files /dev/null and b/examples/audio/bensound-ukulele.mp3 differ diff --git a/examples/audio/pom.xml b/examples/audio/pom.xml index 22b5370..8fc2f95 100644 --- a/examples/audio/pom.xml +++ b/examples/audio/pom.xml @@ -14,6 +14,10 @@ tomp2p http://tomp2p.net/dev/mvn/ + + jitpack.io + https://jitpack.io + @@ -56,5 +60,11 @@ ts3j 1.0-SNAPSHOT + + + com.github.manevolent + ffmpeg4j + 1.0 + \ 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 index 881abd6..f443b86 100644 --- 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 @@ -21,7 +21,7 @@ public class AudioSender { TeamspeakFastMixerSink sink = new TeamspeakFastMixerSink( AUDIO_FORMAT, - (int) AUDIO_FORMAT.getSampleRate() * AUDIO_FORMAT.getChannels() * 4 /*2=16bit float*/, + (int) AUDIO_FORMAT.getSampleRate() * AUDIO_FORMAT.getChannels() * 4 /*4=32bit float*/, new OpusParameters( 20, 96000, // 96kbps diff --git a/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/MusicPlayer.java b/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/MusicPlayer.java new file mode 100644 index 0000000..32ac824 --- /dev/null +++ b/examples/audio/src/main/java/com/github/manevolent/ts3j/examples/audio/MusicPlayer.java @@ -0,0 +1,141 @@ +package com.github.manevolent.ts3j.examples.audio; + +import com.github.manevolent.ffmpeg4j.*; +import com.github.manevolent.ffmpeg4j.filter.audio.FFmpegAudioResampleFilter; +import com.github.manevolent.ffmpeg4j.source.FFmpegAudioSourceSubstream; +import com.github.manevolent.ffmpeg4j.stream.source.FFmpegSourceStream; +import com.github.manevolent.ts3j.identity.LocalIdentity; +import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket; +import com.github.manevolent.ts3j.util.Ts3Debugging; + +import java.io.EOFException; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; + +import static com.github.manevolent.ts3j.examples.audio.TeamspeakFastMixerSink.AUDIO_FORMAT; + +public class MusicPlayer { + public static void main(String[] args) throws Exception { + Ts3Debugging.setEnabled(false); + + // Open file + FFmpeg.register(); + InputStream inputStream = new FileInputStream("bensound-ukulele.mp3"); + FFmpegInput input = new FFmpegInput(inputStream); + FFmpegSourceStream stream = input.open(FFmpeg.getInputFormatByName("mp3")); + FFmpegAudioSourceSubstream audioSourceSubstream = + (FFmpegAudioSourceSubstream) stream.registerStreams() + .stream() + .filter(x -> x.getMediaType() == MediaType.AUDIO) + .findFirst().orElse(null); + + if (audioSourceSubstream == null) throw new NullPointerException("ya dun goofed, bud"); + + // Teamspeak start up + LocalTeamspeakClientSocket client = new LocalTeamspeakClientSocket(); + LocalIdentity identity = LocalIdentity.generateNew(10); + client.setIdentity(identity); + client.setNickname(MusicPlayer.class.getSimpleName()); + client.setHWID("TestTestTest"); + + // Create a sink + TeamspeakFastMixerSink sink = new TeamspeakFastMixerSink( + AUDIO_FORMAT, + (int) AUDIO_FORMAT.getSampleRate() * AUDIO_FORMAT.getChannels() * 4 /*4=32bit 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); + + sink.start(); + + client.connect( + "your.teamspeak.server", + 10000L + ); + + int bufferSize = AUDIO_FORMAT.getChannels() * (int) AUDIO_FORMAT.getSampleRate(); // Just to keep it orderly + FFmpegAudioResampleFilter resampleFilter = new FFmpegAudioResampleFilter( + audioSourceSubstream.getFormat(), + new AudioFormat( + (int) AUDIO_FORMAT.getSampleRate(), + AUDIO_FORMAT.getChannels(), + FFmpeg.guessFFMpegChannelLayout(AUDIO_FORMAT.getChannels()) + ), + bufferSize + ); + + Queue frameQueue = new LinkedBlockingQueue<>(); + AudioFrame currentFrame = null; + int frameOffset = 0; // offset within current frame + + long wake = System.nanoTime(); + long delay = 150 * 1_000_000; // 50ms interval + long sleep; + while (true) { + int available = sink.availableInput(); + + if (available > 0) { + if (currentFrame == null || frameOffset >= currentFrame.getLength()) { + if (frameQueue.peek() == null) { + try { + frameQueue.addAll(resampleFilter.apply(audioSourceSubstream.next())); + } catch (EOFException ex) { + // flush currentFrame + break; + } + } + + currentFrame = frameQueue.remove(); + frameOffset = 0; + } + + int write = Math.min(currentFrame.getLength() - frameOffset, available); + + sink.write( + currentFrame.getSamples(), + frameOffset, + write + ); + + frameOffset += write; + + continue; + } + + wake += delay; + sleep = (wake - System.nanoTime()) / 1_000_000; + + if (sleep > 0) Thread.sleep(sleep); + } + + System.err.println("Draining..."); + + sink.drain(); // make sure TS gets everything + + client.disconnect("BYE"); + + 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()); + + System.exit(0); + } +} 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 index 7454e03..0c7b49b 100644 --- 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 @@ -77,6 +77,10 @@ public class TeamspeakFastMixerSink implements TeamspeakMixerSink { } public void write(float[] buffer, int len) { + write(buffer, 0, len); + } + + public void write(float[] buffer, int offs, int len) { synchronized (stateLock) { if (!running) throw new IllegalStateException("not running"); @@ -85,9 +89,9 @@ public class TeamspeakFastMixerSink implements TeamspeakMixerSink { throw new IllegalArgumentException(len + " > " + availableInput()); } - if (len > buffer.length) { + if (offs+len > buffer.length) { overflowed++; - throw new IllegalArgumentException(len + " > " + buffer.length); + throw new IllegalArgumentException(offs+len + " > " + buffer.length); } if (len <= 0) @@ -97,7 +101,7 @@ public class TeamspeakFastMixerSink implements TeamspeakMixerSink { throw new IllegalArgumentException("not a full frame"); // Write to the buffer - System.arraycopy(buffer, 0, sampleBuffer, samplePosition, len); + System.arraycopy(buffer, offs, sampleBuffer, samplePosition, len); samplePosition += len; availableInput -= len; 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 4780f1c..95aaf08 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 @@ -536,7 +536,8 @@ public class LocalTeamspeakClientSocket // Set a state and send a muted event } else if (microphone.isReady()) { byte[] data = microphone.provide(); - if (data.length <= 0) return; + if (data.length <= 0) + return; PacketBody0Voice voice = new PacketBody0Voice(getRole().getOut()); diff --git a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java index 13e352a..bb206d4 100644 --- a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java +++ b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java @@ -1,31 +1,19 @@ package com.github.manevolent.ts3j; ; -import com.github.manevolent.ts3j.api.Client; -import com.github.manevolent.ts3j.audio.Microphone; -import com.github.manevolent.ts3j.enums.CodecType; -import com.github.manevolent.ts3j.event.*; import com.github.manevolent.ts3j.identity.LocalIdentity; -import com.github.manevolent.ts3j.protocol.Packet; 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 java.math.BigInteger; -import java.net.InetSocketAddress; -import java.util.Base64; -import java.util.function.Consumer; - import static org.junit.Assert.assertEquals; public class ServerConnectionTest { public static void main(String[] args) throws Exception { Ts3Debugging.setEnabled(true); - LocalTeamspeakClientSocket client = new LocalTeamspeakClientSocket(); - LocalIdentity identity = LocalIdentity.generateNew(10); + LocalTeamspeakClientSocket client = new LocalTeamspeakClientSocket(); client.setIdentity(identity); client.setNickname(ServerConnectionTest.class.getSimpleName()); client.setHWID("TestTestTest"); @@ -38,6 +26,8 @@ public class ServerConnectionTest { assertEquals(client.getState(), ClientConnectionState.CONNECTED); + client.subscribeAll(); + client.disconnect("BYE"); } catch (Exception ex) { ex.printStackTrace();