fix: DirectByteBuffer audio path — eliminate JNI array copies
Some checks failed
Build Release Binaries / build-amd64 (push) Failing after 3m43s
Some checks failed
Build Release Binaries / build-amd64 (push) Failing after 3m43s
Adds nativeWriteAudioDirect / nativeReadAudioDirect JNI functions that accept a DirectByteBuffer instead of ShortArray. The buffer's native memory is accessed directly by Rust via pointer — no GetShortArrayRegion / SetShortArrayRegion, no GC-managed array copies on the audio hot path. This fixes SIGBUS crashes on Android 16 where ART's concurrent mark-compact GC crashes when flipping thread roots during JNI array operations on MAX_PRIORITY audio threads. Old ShortArray methods kept for backward compatibility. AudioPipeline switched to use Direct variants. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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}")
|
Log.i(TAG, "capture started: ${SAMPLE_RATE}Hz mono, buf=$bufSize, aec=${aec?.enabled}, ns=${ns?.enabled}")
|
||||||
|
|
||||||
val pcm = ShortArray(FRAME_SAMPLES)
|
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
|
// Debug: PCM file + RMS CSV
|
||||||
var pcmOut: BufferedOutputStream? = null
|
var pcmOut: BufferedOutputStream? = null
|
||||||
var rmsCsv: OutputStreamWriter? = null
|
var rmsCsv: OutputStreamWriter? = null
|
||||||
@@ -224,7 +226,10 @@ class AudioPipeline(private val context: Context) {
|
|||||||
val read = recorder.read(pcm, 0, FRAME_SAMPLES)
|
val read = recorder.read(pcm, 0, FRAME_SAMPLES)
|
||||||
if (read > 0) {
|
if (read > 0) {
|
||||||
applyGain(pcm, read, captureGainDb)
|
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
|
// Debug: write raw PCM + RMS
|
||||||
if (pcmOut != null) {
|
if (pcmOut != null) {
|
||||||
@@ -287,6 +292,8 @@ class AudioPipeline(private val context: Context) {
|
|||||||
|
|
||||||
val pcm = ShortArray(FRAME_SAMPLES)
|
val pcm = ShortArray(FRAME_SAMPLES)
|
||||||
val silence = 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
|
// Debug: PCM file + RMS CSV for playout
|
||||||
var pcmOut: BufferedOutputStream? = null
|
var pcmOut: BufferedOutputStream? = null
|
||||||
var rmsCsv: OutputStreamWriter? = null
|
var rmsCsv: OutputStreamWriter? = null
|
||||||
@@ -303,7 +310,13 @@ class AudioPipeline(private val context: Context) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
while (running) {
|
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) {
|
if (read >= FRAME_SAMPLES) {
|
||||||
applyGain(pcm, read, playoutGainDb)
|
applyGain(pcm, read, playoutGainDb)
|
||||||
track.write(pcm, 0, read)
|
track.write(pcm, 0, read)
|
||||||
|
|||||||
@@ -117,6 +117,26 @@ class WzpEngine(private val callback: WzpCallback) {
|
|||||||
return nativeReadAudio(nativeHandle, pcm)
|
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 --------------------------------------------------
|
// -- JNI native methods --------------------------------------------------
|
||||||
|
|
||||||
private external fun nativeInit(): Long
|
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 nativeForceProfile(handle: Long, profile: Int)
|
||||||
private external fun nativeWriteAudio(handle: Long, pcm: ShortArray): Int
|
private external fun nativeWriteAudio(handle: Long, pcm: ShortArray): Int
|
||||||
private external fun nativeReadAudio(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)
|
private external fun nativeDestroy(handle: Long)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -214,7 +214,6 @@ pub unsafe extern "system" fn Java_com_wzp_engine_WzpEngine_nativeWriteAudio(
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let mut buf = vec![0i16; len];
|
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() {
|
if env.get_short_array_region(&pcm, 0, &mut buf).is_err() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -248,6 +247,56 @@ pub unsafe extern "system" fn Java_com_wzp_engine_WzpEngine_nativeReadAudio(
|
|||||||
result.unwrap_or(0)
|
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)]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "system" fn Java_com_wzp_engine_WzpEngine_nativeDestroy(
|
pub unsafe extern "system" fn Java_com_wzp_engine_WzpEngine_nativeDestroy(
|
||||||
_env: JNIEnv,
|
_env: JNIEnv,
|
||||||
|
|||||||
Reference in New Issue
Block a user