fix(audio,ui): address PR #27 review findings

- ios_voice_unit.rs:856-868: Revert iOS render callback from blocking
  lock() back to try_lock() with silence-on-contention (WouldBlock
  branch increments callback_xrun stat and returns the pre-zeroed
  scratch buffer). Blocking lock() inside the CoreAudio HAL render
  callback can stall the realtime IO thread when the decode task on
  engine.rs:1309 holds the same AudioHandler Mutex, re-introducing
  the underrun pattern this codebase already fixed elsewhere.

- ios_voice_unit.rs:782: Preallocate scratch_stereo to Apple's VPIO
  MaximumFramesPerSlice (4096 frames * 2 channels = 8192 f32) at
  setup time, so the realtime render callback never grows the Vec
  via resize(). The defensive 'len() < needed' branch is kept for
  the (impossible) case that the audio unit later raises max frames.

- main.dart:52-56: Gate _showAudioDebugOverlay behind kDebugMode &&
  _isMacOS so the internal audio stats panel does not ship in
  release builds. kDebugMode is a Dart compile-time const, so the
  overlay subtree is tree-shaken out of release/profile binaries.

- Rebuilt macOS chanora_bridge.framework binary (universal arm64 +
  x86_64) from the fixed source with the new CARGO_PROFILE_RELEASE_*
  env vars (DWARF preserved for dsymutil). install_name patched back
  to @rpath/chanora_bridge.framework/Versions/A/chanora_bridge.

  Verified:
    cargo test -p chanora_audio --lib: 129 passed, 0 failed
    cargo check -p chanora_audio --target aarch64-apple-ios: clean
    dart analyze lib/main.dart: no issues
    xcrun lipo -archs: x86_64 arm64
    xcrun otool -D: @rpath install_name preserved
This commit is contained in:
Edison Jwa
2026-06-07 23:27:46 +09:00
parent 23bfddb6b4
commit ebae1274d6
2 changed files with 20 additions and 4 deletions
+5 -1
View File
@@ -49,7 +49,11 @@ import 'widgets/voice_settings.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
bool get _isMacOS => !kIsWeb && Platform.isMacOS; 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); const Color _appSurfaceColor = Color(0xFFFFFBFE);
+15 -3
View File
@@ -779,7 +779,15 @@ impl IosVoiceUnit {
// callback's actual output channels. Same as Linux/SDL, // callback's actual output channels. Same as Linux/SDL,
// just stereo-f32 -> interleaved-i16 converted at the // just stereo-f32 -> interleaved-i16 converted at the
// boundary. // boundary.
let mut scratch_stereo: Vec<f32> = 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<f32> = vec![0.0; 4096 * 2];
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
let handler_for_render = params.handler.clone(); let handler_for_render = params.handler.clone();
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -846,11 +854,15 @@ impl IosVoiceUnit {
let _removed = handler_for_render.fill_buffer(&mut scratch_stereo[..needed]); let _removed = handler_for_render.fill_buffer(&mut scratch_stereo[..needed]);
} }
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
match handler_for_render.lock() { match handler_for_render.try_lock() {
Ok(mut h) => { Ok(mut h) => {
let _removed = h.fill_buffer(&mut scratch_stereo[..needed]); 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. // Never panic on the realtime IO thread.
warn!(target: "chanora_audio", "AudioHandler mutex poisoned: {e}"); warn!(target: "chanora_audio", "AudioHandler mutex poisoned: {e}");
} }