mirror of
git://f0xx.org/ac/ac-mobile-android
synced 2026-07-29 03:37:51 +03:00
- 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>
258 lines
8.5 KiB
C
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
|
|
}
|