1
0
mirror of git://f0xx.org/ac/ac-mobile-android synced 2026-07-29 07:37:39 +03:00
Files
ac-mobile-android/ndk/jni/opus_bridge.c
Anton Afanasyeu f6e69153ca codecs: real Opus + Speex JNI encode/decode; SFU gate enabled
- opus_bridge.c / speex_bridge.c: full encoder + decoder JNI (libopus/libspeex)
- NativeCodecBridge: add Opus/Speex encode/decode/release public Java API
- OpusAudioEncoderSink / SpeexAudioEncoderSink: real AudioEncoderSink impls
- AudioCodecFactory: route OPUS_NATIVE → OpusAudioEncoderSink, SPEEX_NATIVE → SpeexAudioEncoderSink
- AudioDecoderSink interface; NativeAudioDecoder (JNI decode → AudioTrack)
- ReceiverCastService: swap to NativeAudioDecoder when negotiated codec is Opus/Speex
- SfuRelayGate: PREVIEW_ENABLED=true; add API_BASE, WS_PATH, healthUrl()
- Unit tests: OpusAudioEncoderSinkTest, SpeexAudioEncoderSinkTest, SfuRelayGateTest (19 pass)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 22:27:44 +02:00

258 lines
8.5 KiB
C

/* package ndk/jni/opus_bridge.c */
/*********************************************************************
* opus_bridge.c
* Created at: Sun 17 May 2026 22:49:49 +0200
* Updated at: Sun 28 Jun 2026 22:00:00 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
* Contributors:
* - Anton Afanasyeu <a.afanasieff@gmail.com>
* - Cursor Agent (project assistant)
**********************************************************************/
#include <jni.h>
#include <stdlib.h>
#include <string.h>
#include <android/log.h>
#define LOG_TAG "OpusBridge"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#ifndef ANDROIDCAST_HAVE_OPUS
#define ANDROIDCAST_HAVE_OPUS 0
#endif
#if ANDROIDCAST_HAVE_OPUS
#include <opus/opus.h>
#define OPUS_ENC_MAGIC 0x4F505345L /* OPSE */
#define OPUS_DEC_MAGIC 0x4F505344L /* OPSD */
/* Max frame size: 120ms at 48kHz stereo */
#define OPUS_MAX_FRAME_BYTES 4000
#define OPUS_MAX_PCM_SAMPLES 5760
typedef struct {
unsigned long magic;
OpusEncoder *enc;
int sample_rate;
int channels;
int frame_size; /* samples per frame (e.g. 960 = 20ms @ 48 kHz) */
} OpusEncCtx;
typedef struct {
unsigned long magic;
OpusDecoder *dec;
int sample_rate;
int channels;
int frame_size;
} OpusDecCtx;
static OpusEncCtx *enc_from_handle(jlong h) {
if (h == 0) return NULL;
OpusEncCtx *ctx = (OpusEncCtx *)(intptr_t)h;
if (ctx->magic != OPUS_ENC_MAGIC) return NULL;
return ctx;
}
static OpusDecCtx *dec_from_handle(jlong h) {
if (h == 0) return NULL;
OpusDecCtx *ctx = (OpusDecCtx *)(intptr_t)h;
if (ctx->magic != OPUS_DEC_MAGIC) return NULL;
return ctx;
}
#endif /* ANDROIDCAST_HAVE_OPUS */
/* ── availability probe ───────────────────────────────────────────── */
JNIEXPORT jboolean JNICALL
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeIsOpusAvailable(
JNIEnv *env, jclass clazz) {
(void)env; (void)clazz;
return ANDROIDCAST_HAVE_OPUS ? JNI_TRUE : JNI_FALSE;
}
/* ── encoder ────────────────────────────────────────────────────────── */
JNIEXPORT jlong JNICALL
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeOpusEncoderCreate(
JNIEnv *env, jclass clazz,
jint sampleRate, jint channels, jint bitrateKbps) {
(void)env; (void)clazz;
#if ANDROIDCAST_HAVE_OPUS
int err = OPUS_OK;
OpusEncoder *enc = opus_encoder_create(sampleRate, channels, OPUS_APPLICATION_VOIP, &err);
if (err != OPUS_OK || enc == NULL) {
LOGE("opus_encoder_create failed: %s", opus_strerror(err));
return 0L;
}
int bps = bitrateKbps > 0 ? bitrateKbps * 1000 : OPUS_AUTO;
opus_encoder_ctl(enc, OPUS_SET_BITRATE(bps));
opus_encoder_ctl(enc, OPUS_SET_INBAND_FEC(1));
opus_encoder_ctl(enc, OPUS_SET_DTX(0));
opus_encoder_ctl(enc, OPUS_SET_COMPLEXITY(5));
OpusEncCtx *ctx = (OpusEncCtx *)calloc(1, sizeof(OpusEncCtx));
if (!ctx) { opus_encoder_destroy(enc); return 0L; }
ctx->magic = OPUS_ENC_MAGIC;
ctx->enc = enc;
ctx->sample_rate = sampleRate;
ctx->channels = channels;
/* 20 ms frame — e.g. 960 samples @ 48 kHz, 320 @ 16 kHz */
ctx->frame_size = sampleRate * 20 / 1000;
LOGI("encoder created sr=%d ch=%d fs=%d bps=%d", sampleRate, channels, ctx->frame_size, bps);
return (jlong)(intptr_t)ctx;
#else
(void)sampleRate; (void)channels; (void)bitrateKbps;
return 0L;
#endif
}
JNIEXPORT jbyteArray JNICALL
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeOpusEncode(
JNIEnv *env, jclass clazz,
jlong handle, jbyteArray pcm16le, jint length) {
(void)clazz;
#if ANDROIDCAST_HAVE_OPUS
OpusEncCtx *ctx = enc_from_handle(handle);
if (!ctx || pcm16le == NULL || length <= 0) return NULL;
jbyte *pcm_bytes = (*env)->GetByteArrayElements(env, pcm16le, NULL);
if (!pcm_bytes) return NULL;
const opus_int16 *pcm = (const opus_int16 *)pcm_bytes;
int samples = length / (2 * ctx->channels); /* bytes → samples per channel */
if (samples < ctx->frame_size) {
(*env)->ReleaseByteArrayElements(env, pcm16le, pcm_bytes, JNI_ABORT);
return NULL;
}
unsigned char out_buf[OPUS_MAX_FRAME_BYTES];
opus_int32 encoded = opus_encode(ctx->enc, pcm, ctx->frame_size, out_buf, sizeof(out_buf));
(*env)->ReleaseByteArrayElements(env, pcm16le, pcm_bytes, JNI_ABORT);
if (encoded < 0) {
LOGE("opus_encode: %s", opus_strerror(encoded));
return NULL;
}
jbyteArray result = (*env)->NewByteArray(env, encoded);
if (result) {
(*env)->SetByteArrayRegion(env, result, 0, encoded, (jbyte *)out_buf);
}
return result;
#else
(void)handle; (void)pcm16le; (void)length;
return NULL;
#endif
}
JNIEXPORT void JNICALL
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeOpusEncoderRelease(
JNIEnv *env, jclass clazz, jlong handle) {
(void)env; (void)clazz;
#if ANDROIDCAST_HAVE_OPUS
OpusEncCtx *ctx = enc_from_handle(handle);
if (!ctx) return;
opus_encoder_destroy(ctx->enc);
ctx->magic = 0;
free(ctx);
#else
(void)handle;
#endif
}
JNIEXPORT jint JNICALL
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeOpusEncoderGetFrameSize(
JNIEnv *env, jclass clazz, jlong handle) {
(void)env; (void)clazz;
#if ANDROIDCAST_HAVE_OPUS
OpusEncCtx *ctx = enc_from_handle(handle);
return ctx ? ctx->frame_size : 0;
#else
(void)handle; return 0;
#endif
}
/* ── decoder ────────────────────────────────────────────────────────── */
JNIEXPORT jlong JNICALL
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeOpusDecoderCreate(
JNIEnv *env, jclass clazz, jint sampleRate, jint channels) {
(void)env; (void)clazz;
#if ANDROIDCAST_HAVE_OPUS
int err = OPUS_OK;
OpusDecoder *dec = opus_decoder_create(sampleRate, channels, &err);
if (err != OPUS_OK || dec == NULL) {
LOGE("opus_decoder_create failed: %s", opus_strerror(err));
return 0L;
}
OpusDecCtx *ctx = (OpusDecCtx *)calloc(1, sizeof(OpusDecCtx));
if (!ctx) { opus_decoder_destroy(dec); return 0L; }
ctx->magic = OPUS_DEC_MAGIC;
ctx->dec = dec;
ctx->sample_rate = sampleRate;
ctx->channels = channels;
ctx->frame_size = sampleRate * 20 / 1000;
LOGI("decoder created sr=%d ch=%d", sampleRate, channels);
return (jlong)(intptr_t)ctx;
#else
(void)sampleRate; (void)channels;
return 0L;
#endif
}
JNIEXPORT jbyteArray JNICALL
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeOpusDecode(
JNIEnv *env, jclass clazz,
jlong handle, jbyteArray opus_data, jint length) {
(void)clazz;
#if ANDROIDCAST_HAVE_OPUS
OpusDecCtx *ctx = dec_from_handle(handle);
if (!ctx) return NULL;
jbyte *in_bytes = NULL;
int in_len = 0;
if (opus_data != NULL && length > 0) {
in_bytes = (*env)->GetByteArrayElements(env, opus_data, NULL);
in_len = length;
}
opus_int16 pcm_buf[OPUS_MAX_PCM_SAMPLES];
int frames = opus_decode(ctx->dec,
in_bytes ? (const unsigned char *)in_bytes : NULL,
in_len,
pcm_buf, OPUS_MAX_PCM_SAMPLES / ctx->channels, 0);
if (in_bytes) (*env)->ReleaseByteArrayElements(env, opus_data, in_bytes, JNI_ABORT);
if (frames < 0) {
LOGE("opus_decode: %s", opus_strerror(frames));
return NULL;
}
int pcm_bytes = frames * ctx->channels * 2; /* 16-bit PCM */
jbyteArray result = (*env)->NewByteArray(env, pcm_bytes);
if (result) {
(*env)->SetByteArrayRegion(env, result, 0, pcm_bytes, (jbyte *)pcm_buf);
}
return result;
#else
(void)handle; (void)opus_data; (void)length;
return NULL;
#endif
}
JNIEXPORT void JNICALL
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeOpusDecoderRelease(
JNIEnv *env, jclass clazz, jlong handle) {
(void)env; (void)clazz;
#if ANDROIDCAST_HAVE_OPUS
OpusDecCtx *ctx = dec_from_handle(handle);
if (!ctx) return;
opus_decoder_destroy(ctx->dec);
ctx->magic = 0;
free(ctx);
#else
(void)handle;
#endif
}