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:
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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}");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user