Clarify ONNX Runtime guidance with direct-open install hints, restore desktop WebRTC VAD visibility, map mouse side buttons through focused PTT capture/runtime paths, and wait for server acks before showing chat sends as successful.
Constraint: Linux release UX must stay functional when ONNX Runtime is optional and GNOME portal availability varies
Rejected: Keep desktop VAD locked to Silero only | misleads users when ONNX Runtime is skipped
Confidence: medium
Scope-risk: moderate
Directive: Preserve the protocol send-ack wait path for chat so UI success always tracks real server acceptance
Tested: flutter analyze lib/main.dart lib/widgets/chat_views.dart lib/widgets/input_dialogs.dart lib/widgets/startup_dependency_screen.dart; flutter test test/widgets/input_dialogs_test.dart test/widgets/chat_views_test.dart test/services/startup_dependency_check_test.dart test/widgets/startup_dependency_screen_test.dart test/widgets/voice_settings_controls_test.dart test/widgets/audio_processing_config_state_test.dart; cargo test -p chanora_protocol --lib; cargo test -p chanora_audio ptt_backends --lib
Not-tested: Live manual GNOME portal rebind/global PTT on a real desktop session; observer-bot chat against a live server after the sender-name fallback change
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).
1. Hard-mute now informs the server (setInputMuted) in addition to
clamping the local TransmitGate. Without the server-side flag,
other clients keep seeing us un-muted; without the local clamp
a beat of in-flight audio leaks through. Drive both together so
the mic icon and the actual silence land at the same time.
2. Split the badge's Configure affordance from the Voice Bar's
'Voice settings' gear. The gear opens the mode + release-tail
dialog (onConfigure); the badge's configure opens the bind-key
capture flow directly (new onBindKey). Previously both routed
to the settings dialog, so 'Voice settings' and the badge's
'Configure' were the same screen — useless duplication.
3. Bind-key label is now PTT-only. The mode-badge row no longer
prints 'PTT: Space' when Continuous / Voice Activity is
selected. A new PTT-only secondary line carries the bound key
plus the release-tail value together, hidden entirely for
non-PTT modes.
4. Release-tail row is now PTT-only in BOTH the Voice Bar and the
Voice settings dialog. The dialog previously kept the slider
visible across all modes; switching to Continuous left the
user staring at a control that did nothing.
5. PTT capability badge is now PTT-only. In Continuous and Voice
Activity modes there is no key binding to surface a capability
for, so the 'L0Focused (focused)' line + its info sheet and
the Configure button disappear from the Voice Bar when the
user isn't in PTT mode.
All five fixes are pure UI; no Rust changes needed. flutter analyze
remains clean (6 pre-existing Radio.groupValue deprecation infos).
Implement the SDD-094 / SDD-095 / SDD-096 / SDD-097 detailed designs
committed in dfa84ee.
Rust side
- chanora_audio::TransmitMode enum (Ptt/Continuous/VoiceActivity) with
serde-friendly u8 repr (SDD-095).
- chanora_audio::TransmitModeSelector: lock-free Atomic-backed selector
that is the sole writer of transmit_active (per SAD-083), applying
hard_mute as a final clamp. VoiceActivity falls through to Continuous
for v1 (DEC-030 placeholder).
- chanora_audio::ReleaseTailTimer: tokio-task-owning struct driving the
selector's ptt_held input; default 200 ms tail, configurable 0–500 ms
with AtomicU32 hot read; pending JoinHandle held in a std::sync::Mutex
touched only on PTT edge transitions (SDD-096).
- chanora_storage: AudioMeta persisted as audio_meta.json next to
identity.dek; get/set_transmit_mode + get/set_release_tail_ms with
0..=500 clamp on write.
- chanora_core::ChanoraSession: voice_join(channel, password) and
voice_leave() are the new lifecycle entry points; ensure_audio_running
and shutdown_audio_if_idle are private helpers around the existing
Option<AudioEngine> field. SessionEvent::VoiceState carries the
in_channel / transmit_mode / mute / release_tail_ms tuple. Selector
state survives reconnect; supervisor rewires it to each fresh engine
gate.
- chanora_bridge: drop start_audio; add voice_join, voice_leave,
set/get_transmit_mode, set/get_release_tail_ms, set_hard_mute.
BridgeEvent::VoiceState mirrors the core event. AudioStarted/Stopped
kept for backwards compat but Flutter ignores them in the new UI.
Flutter side
- New apps/chanora_flutter/lib/widgets/voice_bar.dart replaces the
legacy _AudioControls widget. Renders channel pill, mode badge,
mute toggle, level meter, PttCapabilityBadge, leave button. No
manual Start affordance anywhere.
- New apps/chanora_flutter/lib/widgets/voice_settings.dart dialog with
TransmitMode radio group (VoiceActivity disabled with 'Coming soon'
trailing label per DEC-030), bind-key button, release-tail slider
0–500 ms step 25.
- main.dart: state fields _inChannel, _transmitMode, _hardMute,
_releaseTailMs driven by BridgeEvent_VoiceState. Channel-tap now
calls voiceJoin instead of moveToChannel. Removed _onStartAudio,
_audioStarted-gated branch, and the FilledButton.
- l10n: 11 new strings in app_en.arb + app_zh.arb.
Verification
- cargo check --workspace: clean.
- cargo test --workspace --lib: 72 passed / 0 failed / 1 ignored
(chanora_audio: +12 new tests for TransmitMode/Selector/ReleaseTail;
chanora_storage: +2 new tests for audio_meta round-trip).
- flutter analyze: 0 errors, 0 warnings; 6 infos are the Flutter 3.32
Radio.groupValue deprecation (pre-existing API usage).
- FRB Dart/Rust bindings regenerated via flutter_rust_bridge_codegen.
Follow-up (intentionally deferred)
- PttController and per-platform PTT backends still drive AudioTransmitGate
directly via the legacy set_ptt path; routing those key edges through
ChanoraSession::release_tail_timer().{key_down,key_up} so the tail
applies to native PTT input is a contained wiring change in a follow-up.
- Real audio-level RMS in BridgeAudioStats (current meter is binary).
- VoiceActivity backend (DEC-030).