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 | | 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. | | `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 JNA, implementing the core audio interfaces. | | `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`. | | `swing` | `ts3-client-swing` | Swing desktop UI + entry point. Depends on `core` and `desktop`. |
The core exposes `AudioBackend` / `VoiceInput` / `VoiceOutput`; the frontend injects 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 ## Features
### Voice (the priority) ### Voice (the priority)
- **Native Opus codec** via a direct JNA binding to the system `libopus` - **Native Opus codec** via a direct Panama (Foreign Function & Memory API) binding —
(no bundled/native-jar dependency). Encoding at 48 kHz, 20 ms frames. 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: - **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. - **Volume Gate** — RMS/dBFS threshold with a live input meter and hangover.
- **Automatic** — a dependency-free speech detector (short-term energy + spectral - **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. - Change your nickname, mute/deafen from the toolbar.
## Requirements ## Requirements
- Java 17+ (developed/tested on Temurin 26) - Java 26+ (developed/tested on Temurin 26)
- The native Opus library on the system: - The native Opus library on the system:
- Debian/Ubuntu: `sudo apt install libopus0` - Debian/Ubuntu: `sudo apt install libopus0`
- Arch: `sudo pacman -S opus` - Arch: `sudo pacman -S opus`
@@ -124,7 +127,7 @@ core/ com.ts3client
└── ConnectionListener frontend callbacks └── ConnectionListener frontend callbacks
desktop/ com.ts3client.audio.desktop desktop/ com.ts3client.audio.desktop
├── Opus JNA binding to native libopus ├── Opus Panama (FFM) binding to native libopus
├── OpusEncoder/OpusDecoder thin codec wrappers ├── OpusEncoder/OpusDecoder thin codec wrappers
├── AudioDevices device enumeration + line opening (48 kHz/16-bit) ├── AudioDevices device enumeration + line opening (48 kHz/16-bit)
├── JavaSoundVoiceInput capture + VAD/PTT gating + Opus encode ├── JavaSoundVoiceInput capture + VAD/PTT gating + Opus encode

View File

@@ -12,7 +12,7 @@
<artifactId>ts3-client-desktop</artifactId> <artifactId>ts3-client-desktop</artifactId>
<name>TS3J Client Desktop Audio</name> <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> <dependencies>
<dependency> <dependency>
@@ -23,9 +23,43 @@
<groupId>com.github.manevolent</groupId> <groupId>com.github.manevolent</groupId>
<artifactId>ts3j</artifactId> <artifactId>ts3j</artifactId>
</dependency> </dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
</dependency>
</dependencies> </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> </project>

View File

@@ -24,7 +24,7 @@ public final class JavaSoundAudioBackend implements AudioBackend {
@Override @Override
public String description() { public String description() {
try { try {
return "Opus " + Opus.INSTANCE.opus_get_version_string(); return "Opus " + Opus.getVersionString();
} catch (Throwable t) { } catch (Throwable t) {
return "Opus (native library unavailable)"; return "Opus (native library unavailable)";
} }

View File

@@ -1,64 +1,283 @@
package com.ts3client.audio.desktop; package com.ts3client.audio.desktop;
import com.sun.jna.Library; import java.io.IOException;
import com.sun.jna.Native; import java.io.InputStream;
import com.sun.jna.ptr.PointerByReference; import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.nio.IntBuffer; 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}/ * <p>The library is resolved by first trying the system-installed {@code libopus}
* {@code opus.dll}; failing that, JNA extracts and loads the copy we bundle on * under its usual SONAMEs; failing that, the copy we bundle on the classpath for
* the classpath under its platform resource path (e.g. {@code linux-x86-64/libopus.so}, * the running platform (e.g. {@code linux-x86-64/libopus.so}, {@code win32-x86-64/opus.dll})
* {@code win32-x86-64/opus.dll}), so the client runs from the single JAR with no * is extracted to a temporary file and loaded, so the client runs from the single
* external Opus install. Opus always operates internally at 48&nbsp;kHz which * JAR with no external Opus install. Opus always operates internally at 48&nbsp;kHz
* matches what the TeamSpeak 3 protocol uses on the wire. * 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) ---- // ---- application types (opus_defines.h) ----
int OPUS_APPLICATION_VOIP = 2048; public static final int OPUS_APPLICATION_VOIP = 2048;
int OPUS_APPLICATION_AUDIO = 2049; public static final int OPUS_APPLICATION_AUDIO = 2049;
int OPUS_APPLICATION_RESTRICTED_LOWDELAY = 2051; public static final int OPUS_APPLICATION_RESTRICTED_LOWDELAY = 2051;
// ---- CTL request codes ---- // ---- CTL request codes ----
int OPUS_SET_BITRATE_REQUEST = 4002; public static final int OPUS_SET_BITRATE_REQUEST = 4002;
int OPUS_SET_VBR_REQUEST = 4006; public static final int OPUS_SET_VBR_REQUEST = 4006;
int OPUS_SET_COMPLEXITY_REQUEST = 4010; public static final int OPUS_SET_COMPLEXITY_REQUEST = 4010;
int OPUS_SET_INBAND_FEC_REQUEST = 4012; public static final int OPUS_SET_INBAND_FEC_REQUEST = 4012;
int OPUS_SET_PACKET_LOSS_PERC_REQUEST = 4014; public static final int OPUS_SET_PACKET_LOSS_PERC_REQUEST = 4014;
int OPUS_SET_SIGNAL_REQUEST = 4024; public static final int OPUS_SET_SIGNAL_REQUEST = 4024;
int OPUS_RESET_STATE = 4028; public static final int OPUS_RESET_STATE = 4028;
// ---- signal hints ---- // ---- signal hints ----
int OPUS_AUTO = -1000; public static final int OPUS_AUTO = -1000;
int OPUS_SIGNAL_VOICE = 3001; public static final int OPUS_SIGNAL_VOICE = 3001;
int OPUS_SIGNAL_MUSIC = 3002; public static final int OPUS_SIGNAL_MUSIC = 3002;
// ---- encoder ---- private static final ValueLayout.OfInt INT = ValueLayout.JAVA_INT;
PointerByReference opus_encoder_create(int fs, int channels, int application, IntBuffer error); 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 ---- private static final MethodHandle STRERROR =
PointerByReference opus_decoder_create(int fs, int channels, IntBuffer error); 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 ---- private static MemorySegment find(String symbol) {
String opus_get_version_string(); 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; package com.ts3client.audio.desktop;
import com.sun.jna.ptr.PointerByReference; import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.nio.IntBuffer; import java.lang.foreign.ValueLayout;
/** /**
* Thin wrapper around a native Opus decoder. * Thin wrapper around a native Opus decoder.
@@ -13,7 +13,12 @@ import java.nio.IntBuffer;
*/ */
public final class OpusDecoder implements AutoCloseable { 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 frameSize;
private final int channels; private final int channels;
private boolean closed; private boolean closed;
@@ -22,12 +27,17 @@ public final class OpusDecoder implements AutoCloseable {
this.frameSize = frameSize; this.frameSize = frameSize;
this.channels = channels; this.channels = channels;
IntBuffer error = IntBuffer.allocate(1); MemorySegment error = arena.allocate(ValueLayout.JAVA_INT);
handle = Opus.INSTANCE.opus_decoder_create(sampleRate, channels, error); MemorySegment created = Opus.decoderCreate(sampleRate, channels, error);
if (handle == null || error.get(0) != 0) { int errorCode = error.get(ValueLayout.JAVA_INT, 0);
throw new IllegalStateException("opus_decoder_create failed: " if (created.address() == 0 || errorCode != 0) {
+ Opus.INSTANCE.opus_strerror(error.get(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) { public int decode(byte[] packet, float[] out) {
if (closed) throw new IllegalStateException("decoder closed"); if (closed) throw new IllegalStateException("decoder closed");
int samples = Opus.INSTANCE.opus_decode_float(
handle, MemorySegment data = MemorySegment.NULL;
packet, int len = 0;
packet == null ? 0 : packet.length, if (packet != null) {
out, if (packet.length > MAX_PACKET_BYTES) {
frameSize, throw new IllegalArgumentException("packet too large: " + packet.length);
0);
if (samples < 0) {
throw new IllegalStateException("opus_decode_float failed: "
+ Opus.INSTANCE.opus_strerror(samples));
} }
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; return samples;
} }
public void reset() { public void reset() {
if (closed) return; if (closed) return;
Opus.INSTANCE.opus_decoder_ctl(handle, Opus.OPUS_RESET_STATE); Opus.decoderCtl(handle, Opus.OPUS_RESET_STATE, 0);
} }
public int getChannels() { public int getChannels() {
@@ -67,6 +83,7 @@ public final class OpusDecoder implements AutoCloseable {
public void close() { public void close() {
if (closed) return; if (closed) return;
closed = true; closed = true;
Opus.INSTANCE.opus_decoder_destroy(handle); Opus.decoderDestroy(handle);
arena.close();
} }
} }

View File

@@ -1,21 +1,29 @@
package com.ts3client.audio.desktop; package com.ts3client.audio.desktop;
import com.sun.jna.ptr.PointerByReference; import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.nio.IntBuffer; import java.lang.foreign.ValueLayout;
/** /**
* Thin wrapper around a native Opus encoder configured for TeamSpeak voice. * 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 * <p>Fixed at 48&nbsp;kHz. Frames are 20&nbsp;ms (960 samples per channel), which
* is the frame size the TS3 client uses. * 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 { 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 frameSize;
private final int channels; private final int channels;
private final byte[] out = new byte[4096];
private final Object lock = new Object(); private final Object lock = new Object();
private boolean closed; private boolean closed;
@@ -23,12 +31,17 @@ public final class OpusEncoder implements AutoCloseable {
this.frameSize = frameSize; this.frameSize = frameSize;
this.channels = channels; this.channels = channels;
IntBuffer error = IntBuffer.allocate(1); MemorySegment error = arena.allocate(ValueLayout.JAVA_INT);
handle = Opus.INSTANCE.opus_encoder_create(sampleRate, channels, application, error); MemorySegment created = Opus.encoderCreate(sampleRate, channels, application, error);
if (handle == null || error.get(0) != 0) { int errorCode = error.get(ValueLayout.JAVA_INT, 0);
throw new IllegalStateException("opus_encoder_create failed: " if (created.address() == 0 || errorCode != 0) {
+ Opus.INSTANCE.opus_strerror(error.get(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) { public void setBitrate(int bitsPerSecond) {
@@ -58,10 +71,10 @@ public final class OpusEncoder implements AutoCloseable {
private void ctl(int request, int value) { private void ctl(int request, int value) {
synchronized (lock) { synchronized (lock) {
if (closed) return; if (closed) return;
int r = Opus.INSTANCE.opus_encoder_ctl(handle, request, value); int r = Opus.encoderCtl(handle, request, value);
if (r < 0) { if (r < 0) {
throw new IllegalStateException("opus_encoder_ctl(" + request + ") failed: " 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 * @return a newly allocated byte array holding the encoded packet
*/ */
public byte[] encode(float[] pcm) { public byte[] encode(float[] pcm) {
if (pcm.length != frameSize * channels) { int expected = frameSize * channels;
throw new IllegalArgumentException("expected " + (frameSize * channels) if (pcm.length != expected) {
+ " samples, got " + pcm.length); throw new IllegalArgumentException("expected " + expected + " samples, got " + pcm.length);
} }
synchronized (lock) { synchronized (lock) {
if (closed) throw new IllegalStateException("encoder closed"); 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) { if (len < 0) {
throw new IllegalStateException("opus_encode_float failed: " throw new IllegalStateException("opus_encode_float failed: " + Opus.strerror(len));
+ Opus.INSTANCE.opus_strerror(len));
} }
byte[] packet = new byte[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; return packet;
} }
} }
@@ -95,7 +108,8 @@ public final class OpusEncoder implements AutoCloseable {
synchronized (lock) { synchronized (lock) {
if (closed) return; if (closed) return;
closed = true; closed = true;
Opus.INSTANCE.opus_encoder_destroy(handle); Opus.encoderDestroy(handle);
arena.close();
} }
} }
} }

View File

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