diff --git a/ts3-client/README.md b/ts3-client/README.md index c96b1ac..067d8bb 100644 --- a/ts3-client/README.md +++ b/ts3-client/README.md @@ -10,8 +10,8 @@ TeaVM/CheerpJ) without touching the library: | Module | Artifact | Responsibility | |--------|----------|----------------| -| `core` | `ts3-client-core` | Frontend-agnostic library: protocol integration, server model, connection orchestration, audio **abstractions**. No UI, no platform audio, no JNA. | -| `desktop` | `ts3-client-desktop` | Desktop audio backend: Java Sound capture/playback + native Opus via JNA, implementing the core audio interfaces. | +| `core` | `ts3-client-core` | Frontend-agnostic library: protocol integration, server model, connection orchestration, audio **abstractions**. No UI, no platform audio, no native code. | +| `desktop` | `ts3-client-desktop` | Desktop audio backend: Java Sound capture/playback + native Opus via the FFM API (project Panama), implementing the core audio interfaces. | | `swing` | `ts3-client-swing` | Swing desktop UI + entry point. Depends on `core` and `desktop`. | The core exposes `AudioBackend` / `VoiceInput` / `VoiceOutput`; the frontend injects @@ -21,8 +21,11 @@ frontend supplies its own UI and audio backend while reusing `core` unchanged. ## Features ### Voice (the priority) -- **Native Opus codec** via a direct JNA binding to the system `libopus` - (no bundled/native-jar dependency). Encoding at 48 kHz, 20 ms frames. +- **Native Opus codec** via a direct Panama (Foreign Function & Memory API) binding — + no JNA, no other third-party FFI. The system `libopus` is used when installed, + otherwise a bundled copy is extracted from the JAR. Only the Windows x86-64 build + is bundled by default (build with `-Dnatives.all` to package every platform); + elsewhere install libopus from your package manager. Encoding at 48 kHz, 20 ms frames. - **Voice Activation Detection (VAD)** with the same three modes as the TS3 client: - **Volume Gate** — RMS/dBFS threshold with a live input meter and hangover. - **Automatic** — a dependency-free speech detector (short-term energy + spectral @@ -78,7 +81,7 @@ frontend supplies its own UI and audio backend while reusing `core` unchanged. - Change your nickname, mute/deafen from the toolbar. ## Requirements -- Java 17+ (developed/tested on Temurin 26) +- Java 26+ (developed/tested on Temurin 26) - The native Opus library on the system: - Debian/Ubuntu: `sudo apt install libopus0` - Arch: `sudo pacman -S opus` @@ -124,7 +127,7 @@ core/ com.ts3client └── ConnectionListener frontend callbacks desktop/ com.ts3client.audio.desktop -├── Opus JNA binding to native libopus +├── Opus Panama (FFM) binding to native libopus ├── OpusEncoder/OpusDecoder thin codec wrappers ├── AudioDevices device enumeration + line opening (48 kHz/16-bit) ├── JavaSoundVoiceInput capture + VAD/PTT gating + Opus encode diff --git a/ts3-client/desktop/pom.xml b/ts3-client/desktop/pom.xml index a3e0ee3..7c15870 100644 --- a/ts3-client/desktop/pom.xml +++ b/ts3-client/desktop/pom.xml @@ -12,7 +12,7 @@ ts3-client-desktop TS3J Client Desktop Audio - Desktop audio backend: Java Sound capture/playback and native Opus via JNA + Desktop audio backend: Java Sound capture/playback and native Opus via the FFM API @@ -23,9 +23,43 @@ com.github.manevolent ts3j - - net.java.dev.jna - jna - + + + + + + src/main/resources + + darwin/** + darwin-*/** + linux-*/** + win32-x86/** + + + + + + + + all-natives + + + natives.all + + + + + + src/main/resources + + + + + diff --git a/ts3-client/desktop/src/main/java/com/ts3client/audio/desktop/JavaSoundAudioBackend.java b/ts3-client/desktop/src/main/java/com/ts3client/audio/desktop/JavaSoundAudioBackend.java index 1bd2b7b..cc17f16 100644 --- a/ts3-client/desktop/src/main/java/com/ts3client/audio/desktop/JavaSoundAudioBackend.java +++ b/ts3-client/desktop/src/main/java/com/ts3client/audio/desktop/JavaSoundAudioBackend.java @@ -24,7 +24,7 @@ public final class JavaSoundAudioBackend implements AudioBackend { @Override public String description() { try { - return "Opus " + Opus.INSTANCE.opus_get_version_string(); + return "Opus " + Opus.getVersionString(); } catch (Throwable t) { return "Opus (native library unavailable)"; } diff --git a/ts3-client/desktop/src/main/java/com/ts3client/audio/desktop/Opus.java b/ts3-client/desktop/src/main/java/com/ts3client/audio/desktop/Opus.java index 275e667..9a9f8e6 100644 --- a/ts3-client/desktop/src/main/java/com/ts3client/audio/desktop/Opus.java +++ b/ts3-client/desktop/src/main/java/com/ts3client/audio/desktop/Opus.java @@ -1,64 +1,283 @@ package com.ts3client.audio.desktop; -import com.sun.jna.Library; -import com.sun.jna.Native; -import com.sun.jna.ptr.PointerByReference; - -import java.nio.IntBuffer; +import java.io.IOException; +import java.io.InputStream; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.List; /** - * Minimal JNA binding to the native Opus codec library (libopus). + * Binding to the native Opus codec library (libopus) via the Foreign Function & + * Memory API (project Panama). * - *

{@link Native#load} first tries a system-installed {@code libopus.so}/ - * {@code opus.dll}; failing that, JNA extracts and loads the copy we bundle on - * the classpath under its platform resource path (e.g. {@code linux-x86-64/libopus.so}, - * {@code win32-x86-64/opus.dll}), so the client runs from the single JAR with no - * external Opus install. Opus always operates internally at 48 kHz which - * matches what the TeamSpeak 3 protocol uses on the wire. + *

The library is resolved by first trying the system-installed {@code libopus} + * under its usual SONAMEs; failing that, the copy we bundle on the classpath for + * the running platform (e.g. {@code linux-x86-64/libopus.so}, {@code win32-x86-64/opus.dll}) + * is extracted to a temporary file and loaded, so the client runs from the single + * JAR with no external Opus install. Opus always operates internally at 48 kHz + * which matches what the TeamSpeak 3 protocol uses on the wire. */ -public interface Opus extends Library { +public final class Opus { - Opus INSTANCE = Native.load("opus", Opus.class); + private Opus() { + } // ---- application types (opus_defines.h) ---- - int OPUS_APPLICATION_VOIP = 2048; - int OPUS_APPLICATION_AUDIO = 2049; - int OPUS_APPLICATION_RESTRICTED_LOWDELAY = 2051; + public static final int OPUS_APPLICATION_VOIP = 2048; + public static final int OPUS_APPLICATION_AUDIO = 2049; + public static final int OPUS_APPLICATION_RESTRICTED_LOWDELAY = 2051; // ---- CTL request codes ---- - int OPUS_SET_BITRATE_REQUEST = 4002; - int OPUS_SET_VBR_REQUEST = 4006; - int OPUS_SET_COMPLEXITY_REQUEST = 4010; - int OPUS_SET_INBAND_FEC_REQUEST = 4012; - int OPUS_SET_PACKET_LOSS_PERC_REQUEST = 4014; - int OPUS_SET_SIGNAL_REQUEST = 4024; - int OPUS_RESET_STATE = 4028; + public static final int OPUS_SET_BITRATE_REQUEST = 4002; + public static final int OPUS_SET_VBR_REQUEST = 4006; + public static final int OPUS_SET_COMPLEXITY_REQUEST = 4010; + public static final int OPUS_SET_INBAND_FEC_REQUEST = 4012; + public static final int OPUS_SET_PACKET_LOSS_PERC_REQUEST = 4014; + public static final int OPUS_SET_SIGNAL_REQUEST = 4024; + public static final int OPUS_RESET_STATE = 4028; // ---- signal hints ---- - int OPUS_AUTO = -1000; - int OPUS_SIGNAL_VOICE = 3001; - int OPUS_SIGNAL_MUSIC = 3002; + public static final int OPUS_AUTO = -1000; + public static final int OPUS_SIGNAL_VOICE = 3001; + public static final int OPUS_SIGNAL_MUSIC = 3002; - // ---- encoder ---- - PointerByReference opus_encoder_create(int fs, int channels, int application, IntBuffer error); + private static final ValueLayout.OfInt INT = ValueLayout.JAVA_INT; + private static final java.lang.foreign.AddressLayout PTR = ValueLayout.ADDRESS; - int opus_encode_float(PointerByReference st, float[] pcm, int frameSize, byte[] data, int maxDataBytes); + private static final Linker LINKER = Linker.nativeLinker(); + private static final SymbolLookup LOOKUP = NativeLibraries.loadOpus(); - int opus_encoder_ctl(PointerByReference st, int request, Object... args); + // OpusEncoder *opus_encoder_create(opus_int32 fs, int channels, int application, int *error) + private static final MethodHandle ENCODER_CREATE = + downcall("opus_encoder_create", FunctionDescriptor.of(PTR, INT, INT, INT, PTR)); + // opus_int32 opus_encode_float(OpusEncoder *st, const float *pcm, int frame_size, + // unsigned char *data, opus_int32 max_data_bytes) + private static final MethodHandle ENCODE_FLOAT = + downcall("opus_encode_float", FunctionDescriptor.of(INT, PTR, PTR, INT, PTR, INT)); + private static final MethodHandle ENCODER_DESTROY = + downcall("opus_encoder_destroy", FunctionDescriptor.ofVoid(PTR)); - void opus_encoder_destroy(PointerByReference st); + // OpusDecoder *opus_decoder_create(opus_int32 fs, int channels, int *error) + private static final MethodHandle DECODER_CREATE = + downcall("opus_decoder_create", FunctionDescriptor.of(PTR, INT, INT, PTR)); + // int opus_decode_float(OpusDecoder *st, const unsigned char *data, opus_int32 len, + // float *pcm, int frame_size, int decode_fec) + private static final MethodHandle DECODE_FLOAT = + downcall("opus_decode_float", FunctionDescriptor.of(INT, PTR, PTR, INT, PTR, INT, INT)); + private static final MethodHandle DECODER_DESTROY = + downcall("opus_decoder_destroy", FunctionDescriptor.ofVoid(PTR)); - // ---- decoder ---- - PointerByReference opus_decoder_create(int fs, int channels, IntBuffer error); + private static final MethodHandle STRERROR = + downcall("opus_strerror", FunctionDescriptor.of(PTR, INT)); + private static final MethodHandle GET_VERSION_STRING = + downcall("opus_get_version_string", FunctionDescriptor.of(PTR)); - int opus_decode_float(PointerByReference st, byte[] data, int len, float[] pcm, int frameSize, int decodeFec); + // int opus_encoder_ctl(OpusEncoder *st, int request, ...) — the value argument is + // variadic, so the linker must be told where the variadic part starts (index 2). + private static final MethodHandle ENCODER_CTL_SET = variadicCtl("opus_encoder_ctl"); + private static final MethodHandle DECODER_CTL_SET = variadicCtl("opus_decoder_ctl"); - int opus_decoder_ctl(PointerByReference st, int request, Object... args); + private static MethodHandle downcall(String symbol, FunctionDescriptor descriptor) { + return LINKER.downcallHandle(find(symbol), descriptor); + } - void opus_decoder_destroy(PointerByReference st); + private static MethodHandle variadicCtl(String symbol) { + return LINKER.downcallHandle( + find(symbol), + FunctionDescriptor.of(INT, PTR, INT, INT), + Linker.Option.firstVariadicArg(2)); + } - // ---- misc ---- - String opus_get_version_string(); + private static MemorySegment find(String symbol) { + return LOOKUP.find(symbol) + .orElseThrow(() -> new UnsatisfiedLinkError("libopus: unresolved symbol " + symbol)); + } - String opus_strerror(int error); + static MemorySegment encoderCreate(int sampleRate, int channels, int application, MemorySegment error) { + try { + return (MemorySegment) ENCODER_CREATE.invokeExact(sampleRate, channels, application, error); + } catch (Throwable t) { + throw wrap(t); + } + } + + static int encodeFloat(MemorySegment st, MemorySegment pcm, int frameSize, + MemorySegment data, int maxDataBytes) { + try { + return (int) ENCODE_FLOAT.invokeExact(st, pcm, frameSize, data, maxDataBytes); + } catch (Throwable t) { + throw wrap(t); + } + } + + static void encoderDestroy(MemorySegment st) { + try { + ENCODER_DESTROY.invokeExact(st); + } catch (Throwable t) { + throw wrap(t); + } + } + + static int encoderCtl(MemorySegment st, int request, int value) { + try { + return (int) ENCODER_CTL_SET.invokeExact(st, request, value); + } catch (Throwable t) { + throw wrap(t); + } + } + + static MemorySegment decoderCreate(int sampleRate, int channels, MemorySegment error) { + try { + return (MemorySegment) DECODER_CREATE.invokeExact(sampleRate, channels, error); + } catch (Throwable t) { + throw wrap(t); + } + } + + static int decodeFloat(MemorySegment st, MemorySegment data, int len, + MemorySegment pcm, int frameSize, int decodeFec) { + try { + return (int) DECODE_FLOAT.invokeExact(st, data, len, pcm, frameSize, decodeFec); + } catch (Throwable t) { + throw wrap(t); + } + } + + static void decoderDestroy(MemorySegment st) { + try { + DECODER_DESTROY.invokeExact(st); + } catch (Throwable t) { + throw wrap(t); + } + } + + static int decoderCtl(MemorySegment st, int request, int value) { + try { + return (int) DECODER_CTL_SET.invokeExact(st, request, value); + } catch (Throwable t) { + throw wrap(t); + } + } + + /** Human-readable message for an Opus error code. */ + public static String strerror(int error) { + try { + MemorySegment message = (MemorySegment) STRERROR.invokeExact(error); + return cString(message); + } catch (Throwable t) { + throw wrap(t); + } + } + + /** Version string of the loaded libopus, e.g. {@code libopus 1.4}. */ + public static String getVersionString() { + try { + MemorySegment version = (MemorySegment) GET_VERSION_STRING.invokeExact(); + return cString(version); + } catch (Throwable t) { + throw wrap(t); + } + } + + /** Reads a NUL-terminated C string from a zero-length (unbounded) return segment. */ + private static String cString(MemorySegment segment) { + if (segment.address() == 0) { + return null; + } + return segment.reinterpret(Long.MAX_VALUE).getString(0); + } + + private static RuntimeException wrap(Throwable t) { + if (t instanceof RuntimeException re) { + return re; + } + if (t instanceof Error e) { + throw e; + } + return new IllegalStateException("libopus call failed", t); + } + + /** + * Resolves libopus: system install first, bundled classpath copy second. + */ + private static final class NativeLibraries { + + static SymbolLookup loadOpus() { + Arena arena = Arena.global(); + for (String name : candidateNames()) { + try { + return SymbolLookup.libraryLookup(name, arena); + } catch (IllegalArgumentException ignored) { + // not present under this name; try the next one + } + } + return SymbolLookup.libraryLookup(extractBundled(), arena); + } + + /** SONAMEs/filenames a system-installed libopus may carry, in preference order. */ + private static List candidateNames() { + String os = System.getProperty("os.name", "").toLowerCase(); + if (os.contains("win")) { + return List.of("opus.dll", "libopus-0.dll", "libopus.dll"); + } + if (os.contains("mac") || os.contains("darwin")) { + return List.of("libopus.dylib", "libopus.0.dylib"); + } + return List.of("libopus.so.0", "libopus.so"); + } + + private static Path extractBundled() { + String libraryName = System.mapLibraryName("opus"); + for (String directory : platformDirectories()) { + String resource = directory + "/" + libraryName; + try (InputStream in = Opus.class.getClassLoader().getResourceAsStream(resource)) { + if (in == null) { + continue; + } + Path dir = Files.createTempDirectory("ts3-client-opus"); + dir.toFile().deleteOnExit(); + Path file = dir.resolve(libraryName); + Files.copy(in, file, StandardCopyOption.REPLACE_EXISTING); + file.toFile().deleteOnExit(); + return file; + } catch (IOException e) { + throw new UnsatisfiedLinkError("failed to extract bundled libopus: " + e); + } + } + throw new UnsatisfiedLinkError("libopus not found on this system and no bundled copy for " + + platformDirectories()); + } + + /** + * Classpath directories that may hold the bundled native (JNA's platform layout), + * most specific first — macOS also ships an architecture-agnostic {@code darwin/}. + */ + private static List platformDirectories() { + String os = System.getProperty("os.name", "").toLowerCase(); + String arch = System.getProperty("os.arch", "").toLowerCase(); + + String normalizedArch = switch (arch) { + case "amd64", "x86_64" -> "x86-64"; + case "i386", "i486", "i586", "i686", "x86" -> "x86"; + case "aarch64", "arm64" -> "aarch64"; + default -> arch.startsWith("arm") ? "arm" : arch; + }; + + if (os.contains("win")) { + return List.of("win32-" + normalizedArch); + } + if (os.contains("mac") || os.contains("darwin")) { + return List.of("darwin-" + normalizedArch, "darwin"); + } + return List.of("linux-" + normalizedArch); + } + } } diff --git a/ts3-client/desktop/src/main/java/com/ts3client/audio/desktop/OpusDecoder.java b/ts3-client/desktop/src/main/java/com/ts3client/audio/desktop/OpusDecoder.java index c3c9bd1..48736b9 100644 --- a/ts3-client/desktop/src/main/java/com/ts3client/audio/desktop/OpusDecoder.java +++ b/ts3-client/desktop/src/main/java/com/ts3client/audio/desktop/OpusDecoder.java @@ -1,8 +1,8 @@ package com.ts3client.audio.desktop; -import com.sun.jna.ptr.PointerByReference; - -import java.nio.IntBuffer; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; /** * Thin wrapper around a native Opus decoder. @@ -13,7 +13,12 @@ import java.nio.IntBuffer; */ public final class OpusDecoder implements AutoCloseable { - private final PointerByReference handle; + private static final int MAX_PACKET_BYTES = 4096; + + private final Arena arena = Arena.ofShared(); + private final MemorySegment handle; + private final MemorySegment packetBuffer; + private final MemorySegment pcmBuffer; private final int frameSize; private final int channels; private boolean closed; @@ -22,12 +27,17 @@ public final class OpusDecoder implements AutoCloseable { this.frameSize = frameSize; this.channels = channels; - IntBuffer error = IntBuffer.allocate(1); - handle = Opus.INSTANCE.opus_decoder_create(sampleRate, channels, error); - if (handle == null || error.get(0) != 0) { - throw new IllegalStateException("opus_decoder_create failed: " - + Opus.INSTANCE.opus_strerror(error.get(0))); + MemorySegment error = arena.allocate(ValueLayout.JAVA_INT); + MemorySegment created = Opus.decoderCreate(sampleRate, channels, error); + int errorCode = error.get(ValueLayout.JAVA_INT, 0); + if (created.address() == 0 || errorCode != 0) { + arena.close(); + throw new IllegalStateException("opus_decoder_create failed: " + Opus.strerror(errorCode)); } + + this.handle = created; + this.packetBuffer = arena.allocate(MAX_PACKET_BYTES); + this.pcmBuffer = arena.allocate(ValueLayout.JAVA_FLOAT, (long) frameSize * channels); } /** @@ -40,23 +50,29 @@ public final class OpusDecoder implements AutoCloseable { */ public int decode(byte[] packet, float[] out) { if (closed) throw new IllegalStateException("decoder closed"); - int samples = Opus.INSTANCE.opus_decode_float( - handle, - packet, - packet == null ? 0 : packet.length, - out, - frameSize, - 0); - if (samples < 0) { - throw new IllegalStateException("opus_decode_float failed: " - + Opus.INSTANCE.opus_strerror(samples)); + + MemorySegment data = MemorySegment.NULL; + int len = 0; + if (packet != null) { + if (packet.length > MAX_PACKET_BYTES) { + throw new IllegalArgumentException("packet too large: " + packet.length); + } + MemorySegment.copy(packet, 0, packetBuffer, ValueLayout.JAVA_BYTE, 0, packet.length); + data = packetBuffer; + len = packet.length; } + + int samples = Opus.decodeFloat(handle, data, len, pcmBuffer, frameSize, 0); + if (samples < 0) { + throw new IllegalStateException("opus_decode_float failed: " + Opus.strerror(samples)); + } + MemorySegment.copy(pcmBuffer, ValueLayout.JAVA_FLOAT, 0, out, 0, samples * channels); return samples; } public void reset() { if (closed) return; - Opus.INSTANCE.opus_decoder_ctl(handle, Opus.OPUS_RESET_STATE); + Opus.decoderCtl(handle, Opus.OPUS_RESET_STATE, 0); } public int getChannels() { @@ -67,6 +83,7 @@ public final class OpusDecoder implements AutoCloseable { public void close() { if (closed) return; closed = true; - Opus.INSTANCE.opus_decoder_destroy(handle); + Opus.decoderDestroy(handle); + arena.close(); } } diff --git a/ts3-client/desktop/src/main/java/com/ts3client/audio/desktop/OpusEncoder.java b/ts3-client/desktop/src/main/java/com/ts3client/audio/desktop/OpusEncoder.java index 658af4c..530e4d6 100644 --- a/ts3-client/desktop/src/main/java/com/ts3client/audio/desktop/OpusEncoder.java +++ b/ts3-client/desktop/src/main/java/com/ts3client/audio/desktop/OpusEncoder.java @@ -1,21 +1,29 @@ package com.ts3client.audio.desktop; -import com.sun.jna.ptr.PointerByReference; - -import java.nio.IntBuffer; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; /** * Thin wrapper around a native Opus encoder configured for TeamSpeak voice. * *

Fixed at 48 kHz. Frames are 20 ms (960 samples per channel), which * is the frame size the TS3 client uses. + * + *

The native scratch buffers live in a shared arena owned by this instance and + * are freed by {@link #close()}; access is serialised by the instance lock, so the + * encoder may be driven from any thread. */ public final class OpusEncoder implements AutoCloseable { - private final PointerByReference handle; + private static final int MAX_PACKET_BYTES = 4096; + + private final Arena arena = Arena.ofShared(); + private final MemorySegment handle; + private final MemorySegment pcmBuffer; + private final MemorySegment packetBuffer; private final int frameSize; private final int channels; - private final byte[] out = new byte[4096]; private final Object lock = new Object(); private boolean closed; @@ -23,12 +31,17 @@ public final class OpusEncoder implements AutoCloseable { this.frameSize = frameSize; this.channels = channels; - IntBuffer error = IntBuffer.allocate(1); - handle = Opus.INSTANCE.opus_encoder_create(sampleRate, channels, application, error); - if (handle == null || error.get(0) != 0) { - throw new IllegalStateException("opus_encoder_create failed: " - + Opus.INSTANCE.opus_strerror(error.get(0))); + MemorySegment error = arena.allocate(ValueLayout.JAVA_INT); + MemorySegment created = Opus.encoderCreate(sampleRate, channels, application, error); + int errorCode = error.get(ValueLayout.JAVA_INT, 0); + if (created.address() == 0 || errorCode != 0) { + arena.close(); + throw new IllegalStateException("opus_encoder_create failed: " + Opus.strerror(errorCode)); } + + this.handle = created; + this.pcmBuffer = arena.allocate(ValueLayout.JAVA_FLOAT, (long) frameSize * channels); + this.packetBuffer = arena.allocate(MAX_PACKET_BYTES); } public void setBitrate(int bitsPerSecond) { @@ -58,10 +71,10 @@ public final class OpusEncoder implements AutoCloseable { private void ctl(int request, int value) { synchronized (lock) { if (closed) return; - int r = Opus.INSTANCE.opus_encoder_ctl(handle, request, value); + int r = Opus.encoderCtl(handle, request, value); if (r < 0) { throw new IllegalStateException("opus_encoder_ctl(" + request + ") failed: " - + Opus.INSTANCE.opus_strerror(r)); + + Opus.strerror(r)); } } } @@ -73,19 +86,19 @@ public final class OpusEncoder implements AutoCloseable { * @return a newly allocated byte array holding the encoded packet */ public byte[] encode(float[] pcm) { - if (pcm.length != frameSize * channels) { - throw new IllegalArgumentException("expected " + (frameSize * channels) - + " samples, got " + pcm.length); + int expected = frameSize * channels; + if (pcm.length != expected) { + throw new IllegalArgumentException("expected " + expected + " samples, got " + pcm.length); } synchronized (lock) { if (closed) throw new IllegalStateException("encoder closed"); - int len = Opus.INSTANCE.opus_encode_float(handle, pcm, frameSize, out, out.length); + MemorySegment.copy(pcm, 0, pcmBuffer, ValueLayout.JAVA_FLOAT, 0, expected); + int len = Opus.encodeFloat(handle, pcmBuffer, frameSize, packetBuffer, MAX_PACKET_BYTES); if (len < 0) { - throw new IllegalStateException("opus_encode_float failed: " - + Opus.INSTANCE.opus_strerror(len)); + throw new IllegalStateException("opus_encode_float failed: " + Opus.strerror(len)); } byte[] packet = new byte[len]; - System.arraycopy(out, 0, packet, 0, len); + MemorySegment.copy(packetBuffer, ValueLayout.JAVA_BYTE, 0, packet, 0, len); return packet; } } @@ -95,7 +108,8 @@ public final class OpusEncoder implements AutoCloseable { synchronized (lock) { if (closed) return; closed = true; - Opus.INSTANCE.opus_encoder_destroy(handle); + Opus.encoderDestroy(handle); + arena.close(); } } } diff --git a/ts3-client/pom.xml b/ts3-client/pom.xml index 1b7384a..5895cd1 100644 --- a/ts3-client/pom.xml +++ b/ts3-client/pom.xml @@ -19,10 +19,9 @@ - 17 + 26 UTF-8 1.0.3 - 5.14.0 @@ -32,11 +31,6 @@ ts3j ${ts3j.version} - - net.java.dev.jna - jna - ${jna.version} - com.ts3client ts3-client-core