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:
@@ -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)";
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
*
|
||||
* <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 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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 kHz. Frames are 20 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user