fix(audio,macos): address PR #27 review findings
- ios_voice_unit.rs: add producer_shutdown AtomicBool flag (macOS only). The macOS start path spawns a tokio producer task that holds clones of Arc<Mutex<AudioHandler>>, Arc<ArrayQueue<f32>>, and the output gain/muted atomics, then loops on a 20 ms tokio interval. Without a shutdown signal the task runs forever on engine stop/restart and leaks all four Arcs every cycle. Drop now stores 'true' on the flag; the producer checks it at the top of each tick and exits, releasing its clones within at most one 20 ms tick. - macos/Runner/Release.entitlements: strengthen the existing justification comment for com.apple.security.network.server. Document the specific failure mode (tokio::net::UdpSocket::bind -> sandbox 'network-outbound deny' -> EPERM) and explain why network.client alone does not cover bind()-then-sendto. The entitlement is required, not over-broad. cargo check (host + aarch64-apple-darwin): clean cargo test -p chanora_audio --lib: 133 passed flutter test: 186 passed, 2 skipped dart analyze: clean
This commit is contained in:
@@ -4,9 +4,12 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<!-- Release builds omit cs.allow-jit (Flutter hot-reload only). They keep
|
<!-- Release builds omit cs.allow-jit (Flutter hot-reload only). They keep
|
||||||
network.server because the macOS App Sandbox treats every UDP bind()
|
network.server because the macOS App Sandbox treats every UDP bind()
|
||||||
- including the ephemeral 0.0.0.0:0 that tsclientlib uses for
|
- including the ephemeral 0.0.0.0:0 that tsclientlib's
|
||||||
outbound traffic - as a server operation. Without this entitlement
|
tokio::net::UdpSocket::bind() issues for outbound traffic - as a
|
||||||
UdpSocket::bind fails with EPERM and the TS3 connect never starts. -->
|
server operation. Without this entitlement UdpSocket::bind fails
|
||||||
|
with EPERM (sandbox_init: 'network-outbound deny') and the TS3
|
||||||
|
connect never starts. network.client alone is insufficient because
|
||||||
|
it gates connect()-style flows, not bind()-then-sendto. -->
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
|
|||||||
@@ -551,6 +551,13 @@ pub struct IosVoiceUnit {
|
|||||||
// wrapper's own Drop calls AudioComponentInstanceDispose
|
// wrapper's own Drop calls AudioComponentInstanceDispose
|
||||||
// after stop returns.
|
// after stop returns.
|
||||||
unit: Option<AudioUnit>,
|
unit: Option<AudioUnit>,
|
||||||
|
// macOS-only: producer task (spawned in start_macos) polls
|
||||||
|
// this on every 20 ms tick and exits when set. Without it
|
||||||
|
// the tokio task captures `Arc<Mutex<AudioHandler>>` +
|
||||||
|
// `Arc<ArrayQueue<f32>>` and runs forever, leaking on every
|
||||||
|
// engine stop/restart cycle.
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
producer_shutdown: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IosVoiceUnit {
|
impl IosVoiceUnit {
|
||||||
@@ -847,6 +854,9 @@ impl IosVoiceUnit {
|
|||||||
// * iOS: keep the existing direct fill_buffer path — VPIO on iOS
|
// * iOS: keep the existing direct fill_buffer path — VPIO on iOS
|
||||||
// requests 480-frame slices that ARE 20 ms aligned so the
|
// requests 480-frame slices that ARE 20 ms aligned so the
|
||||||
// cadence mismatch does not arise there.
|
// cadence mismatch does not arise there.
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let producer_shutdown = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
// 100 ms capacity = 9600 stereo f32. Sized so that the 60 ms
|
// 100 ms capacity = 9600 stereo f32. Sized so that the 60 ms
|
||||||
@@ -865,6 +875,8 @@ impl IosVoiceUnit {
|
|||||||
let output_gain_for_render = params.output_gain.clone();
|
let output_gain_for_render = params.output_gain.clone();
|
||||||
let output_muted_for_render = params.output_muted.clone();
|
let output_muted_for_render = params.output_muted.clone();
|
||||||
|
|
||||||
|
let producer_shutdown_for_task = producer_shutdown.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut pull_scratch: Vec<f32> = vec![0.0; PULL_SAMPLES];
|
let mut pull_scratch: Vec<f32> = vec![0.0; PULL_SAMPLES];
|
||||||
let mut interval =
|
let mut interval =
|
||||||
@@ -874,6 +886,9 @@ impl IosVoiceUnit {
|
|||||||
);
|
);
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
|
if producer_shutdown_for_task.load(Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
match handler_for_producer.try_lock() {
|
match handler_for_producer.try_lock() {
|
||||||
Ok(mut h) => {
|
Ok(mut h) => {
|
||||||
pull_scratch.fill(0.0);
|
pull_scratch.fill(0.0);
|
||||||
@@ -1193,7 +1208,11 @@ impl IosVoiceUnit {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self { unit: Some(unit) })
|
Ok(Self {
|
||||||
|
unit: Some(unit),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
producer_shutdown,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restart the audio unit after route change handling.
|
/// Restart the audio unit after route change handling.
|
||||||
@@ -1237,6 +1256,11 @@ impl IosVoiceUnit {
|
|||||||
|
|
||||||
impl Drop for IosVoiceUnit {
|
impl Drop for IosVoiceUnit {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
|
// Signal the macOS producer task to exit on its next tick
|
||||||
|
// (up to 20 ms) so it releases its handler / ring clones.
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
self.producer_shutdown.store(true, Ordering::Relaxed);
|
||||||
|
|
||||||
// Stop the audio unit so the render callback no longer
|
// Stop the audio unit so the render callback no longer
|
||||||
// fires. The coreaudio-rs wrapper's own Drop calls
|
// fires. The coreaudio-rs wrapper's own Drop calls
|
||||||
// AudioComponentInstanceDispose afterwards.
|
// AudioComponentInstanceDispose afterwards.
|
||||||
|
|||||||
Reference in New Issue
Block a user