feat(android): the mic goes through the echo canceller, and the host stops hearing itself
The capture stream opened under AAudio's default VoiceRecognition input preset, which deliberately bypasses the HAL's acoustic echo canceller — so a phone playing the game audio out of its own speaker fed that audio straight back to the host. Two layers fix it, both behind a new "Echo cancellation" setting (default ON, next to the Microphone toggle in the touch and console settings, per-profile like every tier-P setting): - Native: the mic opens under the VoiceCommunication preset (HAL AEC/NS on the capture path) and allocates an audio session id. The open ladder is Exclusive+voice → Shared+voice → Exclusive → Shared — some HALs refuse the preset or a session id outright, and a mic without echo cancellation still beats no mic; the last rungs are exactly the preset-less open this always did. - Kotlin backstop: nativeStartMic now returns the allocated session id (0 = none), and StreamScreen hangs the Java AcousticEchoCanceler + NoiseSuppressor off it (guarded by isAvailable), releasing them on every mic-stop path — the surface teardown and the final dispose — so a surface recreate re-attaches instead of leaking effect engines. The playback stream is untouched: retagging it voice/communication would route it through the phone-call chain and regress quality. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -390,28 +390,41 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStopAudio(
|
||||
})
|
||||
}
|
||||
|
||||
/// `NativeBridge.nativeStartMic(handle)` — start mic capture (AAudio input → Opus → host `send_mic`).
|
||||
/// No-op if already running or on a `0` handle. Caller MUST hold RECORD_AUDIO; a failure (e.g. no
|
||||
/// permission) leaves the rest of the session streaming.
|
||||
/// `NativeBridge.nativeStartMic(handle, echoCancel): Int` — start mic capture (AAudio input →
|
||||
/// Opus → host `send_mic`). `echoCancel` opens the capture under the `VoiceCommunication` preset
|
||||
/// (the HAL's echo canceller / noise suppressor) and allocates an audio session id; the return
|
||||
/// value is that id (`> 0`), so Kotlin can attach the Java `AcousticEchoCanceler`/`NoiseSuppressor`
|
||||
/// as a backstop — `0` when none was allocated (echoCancel off, the preset fell back to the plain
|
||||
/// open, a `0` handle, or the mic failed entirely). Already running (a surface recreate) returns
|
||||
/// the running capture's id. Caller MUST hold RECORD_AUDIO; a failure (e.g. no permission) leaves
|
||||
/// the rest of the session streaming.
|
||||
#[cfg(target_os = "android")]
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStartMic(
|
||||
_env: JNIEnv,
|
||||
_this: JObject,
|
||||
handle: jlong,
|
||||
) {
|
||||
echo_cancel: jboolean,
|
||||
) -> jni::sys::jint {
|
||||
if handle == 0 {
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
// SAFETY: live handle per the nativeConnect/nativeClose contract.
|
||||
let h = unsafe { &*(handle as *const SessionHandle) };
|
||||
let mut guard = h.mic.lock().unwrap();
|
||||
if guard.is_some() {
|
||||
return; // already capturing
|
||||
if let Some(m) = guard.as_ref() {
|
||||
return m.session_id(); // already capturing — same stream, same session
|
||||
}
|
||||
match crate::mic::MicCapture::start(h.client.clone()) {
|
||||
Some(m) => *guard = Some(m),
|
||||
None => log::error!("nativeStartMic: mic init failed (RECORD_AUDIO? — session unaffected)"),
|
||||
match crate::mic::MicCapture::start(h.client.clone(), echo_cancel != 0) {
|
||||
Some(m) => {
|
||||
let session_id = m.session_id();
|
||||
*guard = Some(m);
|
||||
session_id
|
||||
}
|
||||
None => {
|
||||
log::error!("nativeStartMic: mic init failed (RECORD_AUDIO? — session unaffected)");
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user