/* 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 * Contributors: * - Anton Afanasyeu * - Cursor Agent (project assistant) **********************************************************************/ #include #include #include #include #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 #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 }