From 9e37201198a0dc5138fc51b016db54075698ceaa Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Thu, 9 Apr 2026 21:50:06 +0400 Subject: [PATCH] android(audio): Usage::VoiceCommunication + MODE_IN_COMMUNICATION, default handset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With da106bd (Usage::Media + MODE_NORMAL) audio works but is always on the loudspeaker — we want handset as the default with a user-driven toggle for speaker (and later bluetooth). The right Oboe usage for a VoIP app is VoiceCommunication, which honours AudioManager.setSpeakerphoneOn / setBluetoothScoOn for routing. Bisection across previous builds showed that setAudioApi(AAudio) + Usage::VoiceCommunication made the playout callback stop draining the ring after cb#0 (build 8c36fb5 logs). Letting Oboe pick the AudioApi implicitly keeps the callback alive — 96be740's Media-usage callbacks fired at steady 50Hz without any explicit setAudioApi. So: keep the Usage change, DROP the explicit AAudio force. - oboe_bridge.cpp: Usage::VoiceCommunication, no setAudioApi, no ContentType override. - MainActivity.kt: setMode(MODE_IN_COMMUNICATION) + setSpeakerphoneOn(false) = handset default, plus max both STREAM_VOICE_CALL and STREAM_MUSIC volumes for belt-and-braces. Next build will add a JNI-based Tauri command to flip speakerphoneOn at runtime so the user can toggle handset↔speaker during a call. --- crates/wzp-native/cpp/oboe_bridge.cpp | 16 ++++++++- .../main/java/com/wzp/desktop/MainActivity.kt | 36 +++++++++++-------- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/crates/wzp-native/cpp/oboe_bridge.cpp b/crates/wzp-native/cpp/oboe_bridge.cpp index 3381f02..e22e378 100644 --- a/crates/wzp-native/cpp/oboe_bridge.cpp +++ b/crates/wzp-native/cpp/oboe_bridge.cpp @@ -297,6 +297,20 @@ int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings) { // config that at least drove callbacks correctly, plus the // Kotlin-side MODE_IN_COMMUNICATION + setSpeakerphoneOn(true) // handled in MainActivity.kt to route audio to the loud speaker. + // Usage::VoiceCommunication is the correct Oboe usage for a VoIP app + // — it respects Android's in-call audio routing and lets + // AudioManager.setSpeakerphoneOn/setBluetoothScoOn actually switch + // between earpiece, loudspeaker, and Bluetooth headset. Combined with + // MODE_IN_COMMUNICATION set from MainActivity.kt and + // speakerphoneOn=false by default, this produces handset/earpiece as + // the default output. + // + // IMPORTANT: do NOT add setAudioApi(AAudio) here. Build 8c36fb5 proved + // forcing AAudio with Usage::VoiceCommunication makes the playout + // callback stop draining the ring after cb#0, even though the stream + // opens successfully. Letting Oboe pick the API (which will be AAudio + // on API ≥ 27 but via a different codepath) kept callbacks firing in + // every other build. oboe::AudioStreamBuilder playoutBuilder; playoutBuilder.setDirection(oboe::Direction::Output) ->setPerformanceMode(oboe::PerformanceMode::LowLatency) @@ -305,7 +319,7 @@ int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings) { ->setChannelCount(config->channel_count) ->setSampleRate(config->sample_rate) ->setFramesPerDataCallback(config->frames_per_burst) - ->setUsage(oboe::Usage::Media) + ->setUsage(oboe::Usage::VoiceCommunication) ->setDataCallback(&g_playout_cb); result = playoutBuilder.openStream(g_playout_stream); diff --git a/desktop/src-tauri/gen/android/app/src/main/java/com/wzp/desktop/MainActivity.kt b/desktop/src-tauri/gen/android/app/src/main/java/com/wzp/desktop/MainActivity.kt index 4217f2c..1d4bba6 100644 --- a/desktop/src-tauri/gen/android/app/src/main/java/com/wzp/desktop/MainActivity.kt +++ b/desktop/src-tauri/gen/android/app/src/main/java/com/wzp/desktop/MainActivity.kt @@ -57,19 +57,20 @@ class MainActivity : TauriActivity() { } /** - * Max out STREAM_MUSIC so the Oboe playout stream (opened with - * Usage::Media, which routes to STREAM_MUSIC) is actually audible. + * Put the phone into VoIP call mode with handset (earpiece) as the + * default output. The Oboe playout stream is opened with + * Usage::VoiceCommunication which honours this routing, so: * - * DELIBERATELY does NOT call setMode(IN_COMMUNICATION) or - * setSpeakerphoneOn: build 8c36fb5 confirmed that combining those with - * Usage::Media OR with Usage::VoiceCommunication (both tried) broke the - * Oboe playout callback entirely — the ring filled once at startup and - * Oboe stopped draining it. Keeping audio mode in MODE_NORMAL so the - * Media stream follows the normal speaker-output path, controlled by - * the media volume slider. + * MODE_IN_COMMUNICATION + speakerphoneOn=false → earpiece (handset) + * MODE_IN_COMMUNICATION + speakerphoneOn=true → loudspeaker + * MODE_IN_COMMUNICATION + bluetoothScoOn=true → bluetooth headset * - * A polished version of the app will setMode/setSpeakerphoneOn on a - * per-call basis once we've figured out the correct combo with AAudio. + * The speaker/handset/BT toggle itself is wired up via the Tauri + * command `set_speakerphone(on)` in a follow-up build. For now the + * default is handset, matching the user's stated preference. + * + * STREAM_VOICE_CALL volume is cranked to max since the in-call volume + * slider is separate from media volume on most devices. */ private fun configureAudioForCall() { try { @@ -80,12 +81,19 @@ class MainActivity : TauriActivity() { "musicVol=${am.getStreamVolume(AudioManager.STREAM_MUSIC)}/" + "${am.getStreamMaxVolume(AudioManager.STREAM_MUSIC)}") - // Crank media volume to max — STREAM_MUSIC is what Usage::Media - // plays through. User can adjust with hardware volume buttons. + am.mode = AudioManager.MODE_IN_COMMUNICATION + am.isSpeakerphoneOn = false // default: handset / earpiece + + // Crank both voice-call and music volumes so nothing silent slips + // through regardless of which stream actually ends up driving. + val maxVoice = am.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL) + am.setStreamVolume(AudioManager.STREAM_VOICE_CALL, maxVoice, 0) val maxMusic = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC) am.setStreamVolume(AudioManager.STREAM_MUSIC, maxMusic, 0) - Log.i(TAG, "audio state after: mode=${am.mode} musicVol=${am.getStreamVolume(AudioManager.STREAM_MUSIC)}/$maxMusic") + Log.i(TAG, "audio state after: mode=${am.mode} speaker=${am.isSpeakerphoneOn} " + + "voiceVol=${am.getStreamVolume(AudioManager.STREAM_VOICE_CALL)}/$maxVoice " + + "musicVol=${am.getStreamVolume(AudioManager.STREAM_MUSIC)}/$maxMusic") } catch (e: Throwable) { Log.e(TAG, "configureAudioForCall failed: ${e.message}", e) }