Files
chanora/crates/chanora_audio/Cargo.toml
T
Edison Jwa 5f1423c349 feat(voice): unified mobile voice bar with gesture-isolated PTT row (#22)
* feat(voice): unified mobile voice bar with gesture-isolated PTT row

Replace separate VoiceStatusChip + VoicePttButton with a single
CompactVoiceBar widget that combines both into a two-row layout:

- Control row (tap): status text, mute, deafen, settings chevron
- PTT row (hold): full-width hold-to-talk, shown only in PTT mode

Gesture isolation prevents mis-touch between rows: the control row
uses tap-only InkWell/IconButton while the PTT row uses a raw
Listener for pointer-down/up events.

Key changes:
- Add CompactVoiceBar widget with state-colored container (normal,
  muted, talk-power-blocked)
- Remove mute/deafen IconButtons from AppBar headerActions
- Restructure voice details sheet into primary section + collapsible
  ExpansionTiles (audio processing, PTT capability, debug)
- Optimistic state updates for mute/deafen to eliminate tap delay
- Instant PTT visual feedback (no AnimatedContainer fade)
- Constant geometry across all states (no layout shift on toggle)

* fix(voice): preserve current PTT button format

* feat(voice): move mute/deafen controls into VoiceStatusChip

* fix(voice): ensure consistent chip height across mute states

Remove isSelected/selectedIcon from IconButtons inside VoiceStatusChip.
Material 3 toggle IconButtons (_SelectableIconButton) can vary in height
when the selected state changes due to tap target sizing. Use simple
conditional icons instead and set shrinkWrap tap target size with tight
constraints for stable 40x40 buttons regardless of state.

* fix(voice): remove leftover duplicate mute/deafen buttons in VoiceStatusChip

* fix(voice): replace unsafe stereo cast with bytemuck and localise talk-power tooltip

Replace the raw-pointer `&mut [(f32, f32)]` to `&mut [f32]` cast in
the oboe output callback with `bytemuck::cast_slice_mut`, eliminating
the unsafe block and relying on bytemuck compile-time NoUninit
verification instead.

Add voiceTalkPowerBlocked l10n key (en + zh) and replace the only
remaining hard-coded English tooltip in VoiceStatusChip with it.
2026-06-05 20:58:16 +09:00

170 lines
7.7 KiB
TOML

[package]
name = "chanora_audio"
description = "Chanora audio subsystem — platform-native capture/playback, audiopus encode, tsclientlib AudioHandler for decode + jitter buffer + mix. DEC-011, DEC-011.1."
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
[dependencies]
chanora_protocol = { path = "../chanora_protocol" }
thiserror.workspace = true
tracing.workspace = true
sonora = "0.1"
webrtc-vad = "0.4"
# ndarray is required by ort's tensor construction API and by
# Silero VAD ONNX inference on non-iOS targets.
ndarray = "0.17"
# Opus encoder. tsclientlib already pulls this; we depend explicitly so
# this crate can compile against it without going through tsclientlib.
audiopus = "0.3.0-rc.0"
# AudioHandler lives in the tsclientlib crate behind the `audio`
# feature. We import the crate just for the AudioHandler type; the
# Connection type stays inside chanora_protocol.
tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", default-features = false, features = ["audio"] }
tokio = { version = "1", features = ["sync", "rt", "macros", "time"] }
rustfft = "6.2.0"
crossbeam = { version = "0.8", default-features = false, features = ["alloc", "crossbeam-queue"] }
[target.'cfg(all(not(target_os = "android"), not(target_os = "ios"), not(target_os = "macos")))'.dependencies]
# Desktop audio I/O for Windows capture/playback and Linux capture.
# Linux playback uses SDL2; Apple platforms use direct VoiceProcessingIO
# AudioUnits via `coreaudio-rs` for the voice path.
cpal = "0.17.3"
[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies]
# Direct CoreAudio AudioUnit access on Apple platforms (DEC-011 follow-up).
# cpal's Apple path does not expose the voice-processing controls Chanora
# needs for VoIP. Use `kAudioUnitSubType_VoiceProcessingIO` directly via
# `coreaudio-rs` so capture/playback share Apple's native AEC + AGC + NS
# voice unit on both iOS and macOS.
#
# Default features keep `audio_toolbox` + `core_audio`, both required
# for AudioUnit construction + property access.
coreaudio-rs = "0.14"
# Grand Central Dispatch bindings — used to run AudioUnit initialize/start
# on the main queue to avoid the VPIO RPC timeout on iOS simulator.
dispatch2 = "0.3"
[target.'cfg(not(target_os = "ios"))'.dependencies]
ort = { version = "2.0.0-rc.12", default-features = false, features = ["load-dynamic", "ndarray", "api-24"] }
[target.'cfg(target_os = "android")'.dependencies]
# JNI bindings to flip Android's AudioManager into MODE_IN_COMMUNICATION
# when the voice-comm preset is requested. ndk_context is initialised
# by the bridge crate's android_init shim.
jni = { version = "0.21", default-features = false }
ndk-context = "0.1"
# Oboe-rs (Google Oboe wrapper) for low-latency voice capture + playback.
# Primary backend for SDD-111..SDD-115. The pre-compiled static library
# shipped with `oboe-sys` 0.6 covers armv7 / aarch64 / x86 / x86_64.
# Default features keep the precompiled library + pregenerated bindings
# so we avoid the clang-sys / libclang requirement on the build host.
#
# Using local fork edisonjwa/oboe-rs (v0.6.2) with:
# - catch_unwind safety in callbacks
# - unwrap_or_default in enum getters (no more SessionId panic)
# - get_raw_session_id() for JNI hardware effect binding
# - deduplicated macro impls
# - PowerSavingOffloaded PerformanceMode variant
oboe = { git = "https://github.com/EdisonJwa/oboe-rs", rev = "a14f9b83ecea8c93f5a692f2ee7808445b938c35" }
# Safe slice reinterpret for the oboe stereo output callback.
# bytemuck::cast_slice_mut replaces the raw-pointer cast from
# `&mut [(f32, f32)]` to `&mut [f32]` with a provenance-correct
# and UB-free transmute backed by `NoUninit`.
bytemuck = { version = "1", features = ["derive"] }
[target.'cfg(target_os = "windows")'.dependencies]
# Real Windows global PTT (SDD-083 / SDD-084): RegisterRawInputDevices
# + WM_INPUT translation backed by a hidden message-only window, and
# SetWindowsHookExW(WH_KEYBOARD_LL / WH_MOUSE_LL) fallback. Both
# require a per-backend OS thread that owns a message pump.
windows = { version = "0.54", features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_System_LibraryLoader",
"Win32_System_Threading",
"Win32_UI_Input",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",
] }
[dev-dependencies]
# `test-util` enables `start_paused` / virtual-clock tests used by
# the missed-key-up watchdog unit tests.
tokio = { version = "1", features = ["sync", "rt", "macros", "time", "test-util"] }
# Cross-platform recording Layer for the SDD-090 / DEC-027 privacy
# invariant integration test (`tests/ptt_privacy.rs`).
tracing-subscriber = { version = "0.3", features = ["registry"] }
# SDD-120 §3 — criterion bench harness (realtime_capture / opus_codec /
# resampler). `harness = false` per bench entry below disables the
# default libtest harness so criterion can install its own.
criterion = "0.5"
# SDD-120 §3 item 1 — dhat is used as the global allocator inside
# `benches/realtime_capture.rs` to count post-warmup heap allocations
# on the realtime capture path. Dev-dep only — does NOT affect
# production builds.
dhat = "0.3"
# SDD-120 §5 / §8 — JSON serialization for `emit_baseline` /
# `compare_baseline` binaries that consume criterion's per-bench
# `estimates.json` outputs and emit the SRS-217 baseline schema.
serde_json = "1"
[[bench]]
name = "realtime_capture"
harness = false
path = "benches/realtime_capture.rs"
[[bench]]
name = "opus_codec"
harness = false
path = "benches/opus_codec.rs"
[[bench]]
name = "resampler"
harness = false
path = "benches/resampler.rs"
[target.'cfg(target_os = "linux")'.dependencies]
# GNOME-on-Wayland Global Push-to-Talk uses the freedesktop
# `org.freedesktop.portal.GlobalShortcuts` interface over D-Bus.
# `zbus` is the standard async D-Bus crate; the `tokio` runtime
# selector is mandatory in zbus 5; we share the tokio runtime
# the rest of the audio + core crates already depend on. The
# `blocking-api` feature is retained so the audio-engine
# probe path can do a synchronous portal-version read without
# starting an async runtime; the live session flow uses the
# async surface.
zbus = { version = "5", default-features = false, features = ["tokio", "blocking-api"] }
# Stream / sink utilities for consuming portal signals on the
# async path.
futures-util = { version = "0.3", default-features = false, features = ["std"] }
# Random token bytes for the portal handle_token / session_handle_token
# options. The portal recommends fresh tokens to scope its own
# object paths per call.
rand = "0.8"
# SDL2 audio for Linux. Replaces the cpal playback path on Linux only;
# cpal stays in use for Linux capture and Windows capture/playback.
# Apple platforms use direct VoiceProcessingIO AudioUnits. Rationale: the
# cpal Linux backend opens raw ALSA `default`, which on most Arch
# / Fedora / Debian installs routes through `dmix` + `plug` with
# nearest-neighbour resampling and very small period sizes — the
# combination produces audible crackling/popping. SDL2 on the same
# systems routes through PipeWire's PulseAudio compat bridge (or
# real PulseAudio), both of which carry a high-quality resampler
# and a sensible default period. The upstream tsclientlib audio
# example (`tsclientlib/examples/audio_utils/ts_to_audio.rs`) and
# the official Qint client both use SDL2 in exactly this shape;
# this dep brings Chanora in line with that pattern.
#
# `bundled` is OFF deliberately — we link against the system
# libSDL2.so. Arch ships `sdl2-compat`; Debian/Ubuntu ship
# `libsdl2-2.0-0`; Fedora ships `SDL2`. The chanora-flutter Linux
# build documentation lists this as a runtime dependency.
sdl2 = { version = "0.37", default-features = false }