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.
This commit is contained in:
@@ -73,6 +73,11 @@ ndk-context = "0.1"
|
||||
# - 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
|
||||
|
||||
@@ -542,19 +542,8 @@ impl AudioOutputCallback for OutputCallback {
|
||||
frames: &mut [(f32, f32)],
|
||||
) -> DataCallbackResult {
|
||||
let _ = catch_unwind(AssertUnwindSafe(|| {
|
||||
// SAFETY: `frames: &mut [(f32, f32)]` is an interleaved stereo
|
||||
// buffer. `(f32, f32)` has the same size (8 bytes) and alignment
|
||||
// (4 bytes) as `[f32; 2]`, so reinterpreting the slice as a flat
|
||||
// `&mut [f32]` of length `frames.len() * 2` is sound. The Rust
|
||||
// reference does not *guarantee* `#[repr(Rust)]` tuple layout,
|
||||
// but (a) both fields are identical F32 primitives with no
|
||||
// padding possible, and (b) the oboe crate uses
|
||||
// `#[repr(transparent)]` on its frame type alias so the ABI
|
||||
// contract is upheld at the FFI boundary. `frames` is not
|
||||
// accessed again after `buf` is created, so no aliasing UB.
|
||||
let buf: &mut [f32] = unsafe {
|
||||
std::slice::from_raw_parts_mut(frames.as_mut_ptr() as *mut f32, frames.len() * 2)
|
||||
};
|
||||
let buf: &mut [f32] =
|
||||
bytemuck::cast_slice_mut::<(f32, f32), f32>(frames);
|
||||
for s in buf.iter_mut() {
|
||||
*s = 0.0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user