Skip to content

Commit 5c72a06

Browse files
author
Jeff Boles
committed
fix(audio): prevent device switching loop during initialization on macOS
Fixes #1371 On macOS 15.x, creating a private aggregate device for speaker audio capture triggers system-wide device change notifications. The DeviceMonitor was reacting to these events during initialization, causing the audio pipeline to restart in a loop and never stabilize. This change adds an initialization_complete flag that prevents device change events from being processed until both mic and speaker streams have successfully initialized. The flag is reset when the user explicitly changes devices to allow proper reinitialization. Root Cause: - PR #1471 introduced aggregate device for speaker capture - Creating "private" aggregate device unexpectedly triggers device change events - DeviceMonitor caught in reinitialization loop during startup - Audio pipeline never stabilized, no audio captured Solution: - Added Arc<AtomicBool> to track initialization completion - Device events ignored during initialization phase - Flag set to true after both mic+speaker streams start - Flag reset on explicit device changes for proper reinitialization Testing: - Verified on macOS 15.7.2 (M4 MacBook Pro) - Audio capture now works correctly - No device switching loop observed - Device changes handled gracefully during recording - Success message "audio_streams_initialized" confirms stable pipeline Changes: - Modified: plugins/listener/src/actors/source.rs (+17 lines) - No breaking changes - Backward compatible
1 parent 0df8e3d commit 5c72a06

File tree

1 file changed

+17
-0
lines changed

1 file changed

+17
-0
lines changed

plugins/listener/src/actors/source.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub struct SourceState {
4040
_silence_stream_tx: Option<std::sync::mpsc::Sender<()>>,
4141
_device_event_thread: Option<std::thread::JoinHandle<()>>,
4242
current_mode: ChannelMode,
43+
initialization_complete: Arc<AtomicBool>,
4344
}
4445

4546
pub struct SourceActor;
@@ -65,6 +66,8 @@ impl Actor for SourceActor {
6566
let device_monitor_handle = DeviceMonitor::spawn(event_tx);
6667

6768
let myself_clone = myself.clone();
69+
let initialization_complete = Arc::new(AtomicBool::new(false));
70+
let initialization_complete_clone = initialization_complete.clone();
6871

6972
let device_event_thread = std::thread::spawn(move || {
7073
use std::sync::mpsc::RecvTimeoutError;
@@ -77,6 +80,11 @@ impl Actor for SourceActor {
7780
Ok(event) => match event {
7881
DeviceEvent::DefaultInputChanged { .. }
7982
| DeviceEvent::DefaultOutputChanged { .. } => {
83+
if !initialization_complete_clone.load(Ordering::Relaxed) {
84+
tracing::info!(event = ?event, "device_event_ignored_during_init");
85+
continue;
86+
}
87+
8088
tracing::info!(event = ?event, "device_event_outer");
8189

8290
loop {
@@ -121,6 +129,7 @@ impl Actor for SourceActor {
121129
_silence_stream_tx: silence_stream_tx,
122130
_device_event_thread: Some(device_event_thread),
123131
current_mode: ChannelMode::Dual,
132+
initialization_complete,
124133
};
125134

126135
start_source_loop(&myself, &mut st).await?;
@@ -149,6 +158,7 @@ impl Actor for SourceActor {
149158
}
150159
SourceMsg::SetMicDevice(dev) => {
151160
st.mic_device = dev;
161+
st.initialization_complete.store(false, Ordering::Relaxed);
152162

153163
if let Some(cancel_token) = st.stream_cancel_token.take() {
154164
cancel_token.cancel();
@@ -193,6 +203,7 @@ async fn start_source_loop(
193203
let token = st.token.clone();
194204
let mic_muted = st.mic_muted.clone();
195205
let mic_device = st.mic_device.clone();
206+
let initialization_complete = st.initialization_complete.clone();
196207

197208
let stream_cancel_token = CancellationToken::new();
198209
st.stream_cancel_token = Some(stream_cancel_token.clone());
@@ -250,6 +261,9 @@ async fn start_source_loop(
250261
tokio::pin!(mic_stream);
251262
tokio::pin!(spk_stream);
252263

264+
initialization_complete.store(true, Ordering::Relaxed);
265+
tracing::info!("audio_streams_initialized");
266+
253267
loop {
254268
let Some(cell) = registry::where_is(ProcessorActor::name()) else {
255269
tracing::warn!("processor_actor_not_found");
@@ -312,6 +326,9 @@ async fn start_source_loop(
312326
tokio::pin!(mic_stream);
313327
tokio::pin!(spk_stream);
314328

329+
initialization_complete.store(true, Ordering::Relaxed);
330+
tracing::info!("audio_streams_initialized");
331+
315332
loop {
316333
let Some(cell) = registry::where_is(ProcessorActor::name()) else {
317334
tracing::warn!("processor_actor_not_found");

0 commit comments

Comments
 (0)