diff --git a/apps/chanora_flutter/lib/main.dart b/apps/chanora_flutter/lib/main.dart index c7ae27f..9ce64e0 100644 --- a/apps/chanora_flutter/lib/main.dart +++ b/apps/chanora_flutter/lib/main.dart @@ -49,7 +49,11 @@ import 'widgets/voice_settings.dart'; import 'package:share_plus/share_plus.dart'; bool get _isMacOS => !kIsWeb && Platform.isMacOS; -bool get _showAudioDebugOverlay => _isMacOS; +// Debug-only overlay. `kDebugMode` is a compile-time const in +// release builds (= false), so the overlay subtree is tree-shaken +// out of release/profile binaries entirely — release users never +// see internal audio stats and we don't pay the render cost. +bool get _showAudioDebugOverlay => kDebugMode && _isMacOS; const Color _appSurfaceColor = Color(0xFFFFFBFE); diff --git a/crates/chanora_audio/src/ios_voice_unit.rs b/crates/chanora_audio/src/ios_voice_unit.rs index 0c67494..f581c31 100644 --- a/crates/chanora_audio/src/ios_voice_unit.rs +++ b/crates/chanora_audio/src/ios_voice_unit.rs @@ -779,7 +779,15 @@ impl IosVoiceUnit { // callback's actual output channels. Same as Linux/SDL, // just stereo-f32 -> interleaved-i16 converted at the // boundary. - let mut scratch_stereo: Vec = Vec::with_capacity(2048); + // Preallocate to Apple's VPIO MaximumFramesPerSlice default + // (4096 frames) * 2 (stereo) = 8192 f32. The render callback + // is realtime; growing this Vec inside the callback would + // allocate on the audio thread and risk an underrun. We + // never resize below `needed` after this point — the `len() + // < needed` branch in the callback is a defensive no-op for + // the impossible case where the audio unit later raises + // MaximumFramesPerSlice beyond our preallocation. + let mut scratch_stereo: Vec = vec![0.0; 4096 * 2]; #[cfg(target_os = "ios")] let handler_for_render = params.handler.clone(); #[cfg(target_os = "macos")] @@ -846,11 +854,15 @@ impl IosVoiceUnit { let _removed = handler_for_render.fill_buffer(&mut scratch_stereo[..needed]); } #[cfg(target_os = "ios")] - match handler_for_render.lock() { + match handler_for_render.try_lock() { Ok(mut h) => { let _removed = h.fill_buffer(&mut scratch_stereo[..needed]); } - Err(e) => { + Err(std::sync::TryLockError::WouldBlock) => { + audio_processing_stats_for_render.increment_callback_xrun(); + // scratch_stereo is already zeroed above. + } + Err(std::sync::TryLockError::Poisoned(e)) => { // Never panic on the realtime IO thread. warn!(target: "chanora_audio", "AudioHandler mutex poisoned: {e}"); }