diff --git a/android/app/src/main/java/com/wzp/audio/AudioPipeline.kt b/android/app/src/main/java/com/wzp/audio/AudioPipeline.kt index 9223e50..363b3c4 100644 --- a/android/app/src/main/java/com/wzp/audio/AudioPipeline.kt +++ b/android/app/src/main/java/com/wzp/audio/AudioPipeline.kt @@ -205,6 +205,8 @@ class AudioPipeline(private val context: Context) { Log.i(TAG, "capture started: ${SAMPLE_RATE}Hz mono, buf=$bufSize, aec=${aec?.enabled}, ns=${ns?.enabled}") val pcm = ShortArray(FRAME_SAMPLES) + // DirectByteBuffer for zero-copy JNI (avoids ART GC SIGBUS on Android 16) + val directBuf = ByteBuffer.allocateDirect(FRAME_SAMPLES * 2).order(ByteOrder.LITTLE_ENDIAN) // Debug: PCM file + RMS CSV var pcmOut: BufferedOutputStream? = null var rmsCsv: OutputStreamWriter? = null @@ -224,7 +226,10 @@ class AudioPipeline(private val context: Context) { val read = recorder.read(pcm, 0, FRAME_SAMPLES) if (read > 0) { applyGain(pcm, read, captureGainDb) - engine.writeAudio(pcm) + // Zero-copy write via DirectByteBuffer (no GC array interaction) + directBuf.clear() + directBuf.asShortBuffer().put(pcm, 0, read) + engine.writeAudioDirect(directBuf, read) // Debug: write raw PCM + RMS if (pcmOut != null) { @@ -287,6 +292,8 @@ class AudioPipeline(private val context: Context) { val pcm = ShortArray(FRAME_SAMPLES) val silence = ShortArray(FRAME_SAMPLES) + // DirectByteBuffer for zero-copy JNI (avoids ART GC SIGBUS on Android 16) + val directBuf = ByteBuffer.allocateDirect(FRAME_SAMPLES * 2).order(ByteOrder.LITTLE_ENDIAN) // Debug: PCM file + RMS CSV for playout var pcmOut: BufferedOutputStream? = null var rmsCsv: OutputStreamWriter? = null @@ -303,7 +310,13 @@ class AudioPipeline(private val context: Context) { } try { while (running) { - val read = engine.readAudio(pcm) + // Zero-copy read via DirectByteBuffer + directBuf.clear() + val read = engine.readAudioDirect(directBuf, FRAME_SAMPLES) + if (read > 0) { + directBuf.rewind() + directBuf.asShortBuffer().get(pcm, 0, read) + } if (read >= FRAME_SAMPLES) { applyGain(pcm, read, playoutGainDb) track.write(pcm, 0, read) diff --git a/android/app/src/main/java/com/wzp/engine/WzpEngine.kt b/android/app/src/main/java/com/wzp/engine/WzpEngine.kt index 6e863df..651f3d8 100644 --- a/android/app/src/main/java/com/wzp/engine/WzpEngine.kt +++ b/android/app/src/main/java/com/wzp/engine/WzpEngine.kt @@ -117,6 +117,26 @@ class WzpEngine(private val callback: WzpCallback) { return nativeReadAudio(nativeHandle, pcm) } + /** + * Write captured PCM from a DirectByteBuffer — zero JNI array copy. + * The buffer must be a direct ByteBuffer with native byte order containing i16 samples. + * Called from the AudioRecord capture thread. + */ + fun writeAudioDirect(buffer: java.nio.ByteBuffer, sampleCount: Int): Int { + if (nativeHandle == 0L) return 0 + return nativeWriteAudioDirect(nativeHandle, buffer, sampleCount) + } + + /** + * Read decoded PCM into a DirectByteBuffer — zero JNI array copy. + * The buffer must be a direct ByteBuffer with native byte order. + * Called from the AudioTrack playout thread. + */ + fun readAudioDirect(buffer: java.nio.ByteBuffer, maxSamples: Int): Int { + if (nativeHandle == 0L) return 0 + return nativeReadAudioDirect(nativeHandle, buffer, maxSamples) + } + // -- JNI native methods -------------------------------------------------- private external fun nativeInit(): Long @@ -130,6 +150,8 @@ class WzpEngine(private val callback: WzpCallback) { private external fun nativeForceProfile(handle: Long, profile: Int) private external fun nativeWriteAudio(handle: Long, pcm: ShortArray): Int private external fun nativeReadAudio(handle: Long, pcm: ShortArray): Int + private external fun nativeWriteAudioDirect(handle: Long, buffer: java.nio.ByteBuffer, sampleCount: Int): Int + private external fun nativeReadAudioDirect(handle: Long, buffer: java.nio.ByteBuffer, maxSamples: Int): Int private external fun nativeDestroy(handle: Long) companion object { diff --git a/crates/wzp-android/src/jni_bridge.rs b/crates/wzp-android/src/jni_bridge.rs index 0dff475..ccb25ac 100644 --- a/crates/wzp-android/src/jni_bridge.rs +++ b/crates/wzp-android/src/jni_bridge.rs @@ -214,7 +214,6 @@ pub unsafe extern "system" fn Java_com_wzp_engine_WzpEngine_nativeWriteAudio( return 0; } let mut buf = vec![0i16; len]; - // GetShortArrayRegion copies Java array into our buffer if env.get_short_array_region(&pcm, 0, &mut buf).is_err() { return 0; } @@ -248,6 +247,56 @@ pub unsafe extern "system" fn Java_com_wzp_engine_WzpEngine_nativeReadAudio( result.unwrap_or(0) } +/// Write captured PCM from a DirectByteBuffer — zero JNI array copies. +/// The ByteBuffer must contain little-endian i16 samples. +/// Called from the AudioRecord capture thread. +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_wzp_engine_WzpEngine_nativeWriteAudioDirect( + env: JNIEnv, + _class: JClass, + handle: jlong, + buffer: jni::objects::JByteBuffer, + sample_count: jint, +) -> jint { + let result = panic::catch_unwind(panic::AssertUnwindSafe(|| { + let h = unsafe { handle_ref(handle) }; + let ptr = env.get_direct_buffer_address(&buffer).unwrap_or(std::ptr::null_mut()); + if ptr.is_null() || sample_count <= 0 { + return 0; + } + let samples = unsafe { + std::slice::from_raw_parts(ptr as *const i16, sample_count as usize) + }; + h.engine.write_audio(samples) as jint + })); + result.unwrap_or(0) +} + +/// Read decoded PCM into a DirectByteBuffer — zero JNI array copies. +/// The ByteBuffer will be filled with little-endian i16 samples. +/// Called from the AudioTrack playout thread. +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_wzp_engine_WzpEngine_nativeReadAudioDirect( + env: JNIEnv, + _class: JClass, + handle: jlong, + buffer: jni::objects::JByteBuffer, + max_samples: jint, +) -> jint { + let result = panic::catch_unwind(panic::AssertUnwindSafe(|| { + let h = unsafe { handle_ref(handle) }; + let ptr = env.get_direct_buffer_address(&buffer).unwrap_or(std::ptr::null_mut()); + if ptr.is_null() || max_samples <= 0 { + return 0; + } + let samples = unsafe { + std::slice::from_raw_parts_mut(ptr as *mut i16, max_samples as usize) + }; + h.engine.read_audio(samples) as jint + })); + result.unwrap_or(0) +} + #[unsafe(no_mangle)] pub unsafe extern "system" fn Java_com_wzp_engine_WzpEngine_nativeDestroy( _env: JNIEnv,