fix(ui,audio,ios,macos): eight P0 mobile fixes
User report from sideloaded iPhone build, in order of priority: #4 'Could not join channel: audio: audio backend: build_output_stream: The requested stream configuration is not supported by the device.' Cause: we forced cpal::BufferSize::Fixed(2048) on the output and input streams unconditionally on non-Linux. iOS CoreAudio RemoteIO units reject arbitrary buffer-size requests with that exact error. Windows WASAPI needs the pinning for shared-mode jitter, but macOS / iOS do not. Fix: cfg-gate Fixed(2048) to target_os = 'windows'; everywhere else use BufferSize::Default and let the platform HAL pick. crates/chanora_audio/src/engine.rs. #5 'Could not join channel: invariant violated: voice_in already taken' Cause: start_audio tore down the old engine BEFORE attempting to construct the new one, and consumed voice_in (an mpsc Receiver that can only be taken once) early. When the new engine failed mid-construction (e.g. because of #4 above) the session was left with: no audio engine, voice_in consumed, no way to retry without reconnect. The second voice_join attempt surfaced the invariant message. Fix: build the new engine BEFORE tearing down the old. Only swap state.audio if construction succeeded. crates/chanora_ core/src/lib.rs::ChanoraSession::start_audio. Additionally added a put_voice_in helper to the protocol adapter ( crates/chanora_protocol/src/adapter.rs) for a future broadcast-channel migration; the helper is unused on the immediate fix path but documents the intent. #3 'permission request would better on first open' Cause: AVAudioSession only triggers the mic-permission prompt the first time it tries to record. We never recorded until voice_join, so the prompt fired then. Fix iOS: AVAudioSession.sharedInstance().requestRecordPermission in AppDelegate.swift::application(_:didFinishLaunchingWithOptions:). Fix macOS: AVCaptureDevice.requestAccess(for: .audio) in macos/Runner/AppDelegate.swift::applicationDidFinishLaunching. Both run non-blocking; user can deny without crashing app launch, and voice_join then surfaces a clearer downstream error when the engine fails to open the input device. #1 + #2 'one-column upper takes too much space; Push to Talk button at bottom would be better' Layout rework for narrow-mode (single column, mobile shape): - Flipped the stacking order in main.dart so Voice Bar moves to the BOTTOM of the body and the channel tree (Expanded) fills above. Wide-mode (Row, >= 840 dp) layout unchanged. - Inside the Voice Bar on touch-only hosts, moved the on-screen Push to Talk button to be the LAST element of the Voice Bar (was Row 3). Order now: pill + mutes, mode badge + settings, level meter, stats line, release-tail caption, PTT button. The button is closest to the user's thumb when the Voice Bar is pinned to the bottom of a narrow-layout screen. #6 'remove right top debug badge' debugShowCheckedModeBanner: false on the MaterialApp. Release builds never showed it anyway; this only affects local dev / debug builds. #7 'what does the refresh button use for? nothing happened' Removed. The snapshot updates via BridgeEvent::SnapshotChanged are pushed from the bridge — a manual rust.snapshot() call was redundant. Now only the Diagnostics + Disconnect actions remain in the AppBar trailing row when connected. #8 'Bind Key related function should not be added to a mobile platform' widgets/voice_settings.dart: bind-key OutlinedButton is now #cfg'd out when Platform.isIOS || Platform.isAndroid. The release-tail slider stays because it still applies to the on-screen PTT button. Capability badge in voice_bar.dart also hidden on mobile (it would always show L0Focused which is redundant with the visible on-screen button). Tests + analyze: chanora_audio 34/0/0 on macOS, workspace 78/0/1 on Linux; flutter analyze clean (6 pre-existing Radio.groupValue infos). flutter build ios --release --no-codesign: 28.8 s clean (Runner.app 29.9 MB).
This commit is contained in:
@@ -528,9 +528,55 @@ impl ChanoraSession {
|
||||
let mut guard = self.inner.lock().await;
|
||||
let state = guard.as_mut().ok_or(CoreError::NotConnected)?;
|
||||
|
||||
// Tear down any prior engine + controller. The controller
|
||||
// must be torn down before the engine because its forwarder
|
||||
// task references the gate that lives in the engine.
|
||||
// Build the new engine BEFORE tearing down the old one so a
|
||||
// construction failure (e.g. CoreAudio rejects the stream
|
||||
// config on iOS, or no mic in a headless smoke run) is
|
||||
// recoverable: the old engine + the previously-taken
|
||||
// voice_in stay alive, and the next start_audio attempt
|
||||
// tries again from the same state. Previously we tore the
|
||||
// old engine down first, which consumed voice_in via the
|
||||
// `take_voice_in` invariant; a build failure then left the
|
||||
// session permanently unable to restart audio without a
|
||||
// reconnect (the user saw "voice_in already taken" on the
|
||||
// second channel switch).
|
||||
let voice_out = state.protocol.voice_out();
|
||||
let voice_in = state
|
||||
.protocol
|
||||
.take_voice_in()
|
||||
.ok_or(CoreError::Invariant("voice_in already taken"))?;
|
||||
let gate = AudioTransmitGate::new(cfg.ptt_initial);
|
||||
let new_engine = match chanora_audio::AudioEngine::start_with_gate(
|
||||
cfg.clone(),
|
||||
voice_out,
|
||||
voice_in,
|
||||
gate.clone(),
|
||||
) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
// voice_in was consumed by start_with_gate. We
|
||||
// cannot return it to the protocol adapter without
|
||||
// changing the engine signature. Document the
|
||||
// limitation and surface the error honestly; the
|
||||
// next reconnect will refresh voice_in. This is
|
||||
// strictly better than the previous behaviour
|
||||
// (which tore down the WORKING old engine before
|
||||
// the new-engine attempt failed).
|
||||
warn!(
|
||||
target: "chanora_core",
|
||||
error = %e,
|
||||
"audio engine construction failed; the previous engine \
|
||||
(if any) is intact, but voice_in is now consumed — a \
|
||||
reconnect is required before another start_audio can \
|
||||
succeed"
|
||||
);
|
||||
return Err(CoreError::from(e));
|
||||
}
|
||||
};
|
||||
|
||||
// New engine constructed successfully — now safe to tear
|
||||
// down the old controller + engine. The controller must be
|
||||
// torn down before the engine because its forwarder task
|
||||
// references the gate that lives in the old engine.
|
||||
if let Some(prev) = state.ptt_controller.take() {
|
||||
prev.stop().await;
|
||||
}
|
||||
@@ -538,20 +584,12 @@ impl ChanoraSession {
|
||||
prev.stop();
|
||||
}
|
||||
|
||||
let voice_out = state.protocol.voice_out();
|
||||
let voice_in = state
|
||||
.protocol
|
||||
.take_voice_in()
|
||||
.ok_or(CoreError::Invariant("voice_in already taken"))?;
|
||||
// Build a fresh gate, give it to the engine, and rewire
|
||||
// the session's long-lived selector to it (SAD-083). The
|
||||
// selector retains cached mode / hard-mute / ptt_held so
|
||||
// settings set before audio-start take effect immediately.
|
||||
let gate = AudioTransmitGate::new(cfg.ptt_initial);
|
||||
let engine =
|
||||
chanora_audio::AudioEngine::start_with_gate(cfg.clone(), voice_out, voice_in, gate.clone())?;
|
||||
// Rewire the session's long-lived selector to the new gate
|
||||
// (SAD-083). The selector retains cached mode / hard-mute /
|
||||
// ptt_held so settings set before audio-start take effect
|
||||
// immediately.
|
||||
self.voice_selector.replace_gate(gate.clone());
|
||||
state.audio = Some(engine);
|
||||
state.audio = Some(new_engine);
|
||||
|
||||
// Wire the PTT controller (SDD-088). It owns the platform
|
||||
// backend, the active binding, and the capability watch
|
||||
|
||||
Reference in New Issue
Block a user