Replace JNA with the FFM API (Panama) for the Opus binding

Bind libopus through java.lang.foreign downcall handles instead of JNA:
the ctl functions are linked with firstVariadicArg(2), and the encoder and
decoder each own a shared Arena holding the handle plus reusable native
PCM/packet buffers, so the hot path only allocates the returned packet.

The library is resolved by system SONAME first, falling back to a bundled
copy extracted from the JAR. Only the Windows x86-64 build is packaged by
default now (-Dnatives.all restores all platforms), since every other
platform ships libopus through its package manager.

Requires Java 26.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 23:35:57 +00:00
parent 022aef633b
commit 9d63522ef2
7 changed files with 381 additions and 100 deletions

View File

@@ -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

View File

@@ -12,7 +12,7 @@
<artifactId>ts3-client-desktop</artifactId>
<name>TS3J Client Desktop Audio</name>
<description>Desktop audio backend: Java Sound capture/playback and native Opus via JNA</description>
<description>Desktop audio backend: Java Sound capture/playback and native Opus via the FFM API</description>
<dependencies>
<dependency>
@@ -23,9 +23,43 @@
<groupId>com.github.manevolent</groupId>
<artifactId>ts3j</artifactId>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
</dependency>
</dependencies>
<build>
<!--
Only the Windows x86-64 Opus build is packaged by default: every other
supported platform ships libopus through its package manager (or, on
macOS, Homebrew), and the FFM binding loads the system library first.
Build with -Dnatives.all to bundle the copies for all platforms.
-->
<resources>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>darwin/**</exclude>
<exclude>darwin-*/**</exclude>
<exclude>linux-*/**</exclude>
<exclude>win32-x86/**</exclude>
</excludes>
</resource>
</resources>
</build>
<profiles>
<profile>
<id>all-natives</id>
<activation>
<property>
<name>natives.all</name>
</property>
</activation>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
</profile>
</profiles>
</project>

View File

@@ -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)";
}

View File

@@ -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 &amp;
* Memory API (project Panama).
*
* <p>{@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&nbsp;kHz which
* matches what the TeamSpeak 3 protocol uses on the wire.
* <p>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&nbsp;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<String> 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<String> 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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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.
*
* <p>Fixed at 48&nbsp;kHz. Frames are 20&nbsp;ms (960 samples per channel), which
* is the frame size the TS3 client uses.
*
* <p>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();
}
}
}

View File

@@ -19,10 +19,9 @@
</modules>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<maven.compiler.release>26</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<ts3j.version>1.0.3</ts3j.version>
<jna.version>5.14.0</jna.version>
</properties>
<dependencyManagement>
@@ -32,11 +31,6 @@
<artifactId>ts3j</artifactId>
<version>${ts3j.version}</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>${jna.version}</version>
</dependency>
<dependency>
<groupId>com.ts3client</groupId>
<artifactId>ts3-client-core</artifactId>