Skip to content

Commit 64e0b82

Browse files
committed
Add volume polling for macos
1 parent d9baea1 commit 64e0b82

1 file changed

Lines changed: 23 additions & 129 deletions

File tree

  • src-tauri/src/sendspin/volume_control

src-tauri/src/sendspin/volume_control/macos.rs

Lines changed: 23 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -244,83 +244,34 @@ impl VolumeControlImpl for MacOSVolumeControl {
244244
}
245245

246246
fn set_change_callback(&mut self, callback: VolumeChangeCallback) -> Result<(), String> {
247-
// TEMPORARY: Disable property listeners to test if they're causing audio issues
248-
// The CoreAudio property listener was interfering with audio playback (static noise)
249-
// TODO: Find alternative approach - polling, different API, or accept one-way control
250-
eprintln!("[VolumeControl] macOS volume change listener temporarily disabled");
251-
let _ = callback;
252-
Ok(())
253-
254-
/* DISABLED CODE - was causing audio playback issues
255-
// Property listener callback - called when volume or mute changes
256-
// CRITICAL: This runs on CoreAudio's real-time audio thread and must be FAST
257-
// Do minimal work here - just signal that a change occurred
258-
// This callback is LOCK-FREE - no mutexes, no allocations
259-
#[allow(clippy::items_after_statements)]
260-
unsafe extern "C" fn property_listener(
261-
_device_id: AudioObjectID,
262-
_num_addresses: u32,
263-
_addresses: *const AudioObjectPropertyAddress,
264-
client_data: *mut std::ffi::c_void,
265-
) -> OSStatus {
266-
if client_data.is_null() {
267-
return 0;
268-
}
269-
270-
// Reconstruct the Arc<Sender> from the raw pointer (but keep it alive)
271-
let sender_arc = Arc::from_raw(client_data as *const std::sync::mpsc::Sender<()>);
272-
273-
// Send signal - this is non-blocking on unbounded channels
274-
// If send fails, just ignore it (channel closed, controller dropped)
275-
let _ = sender_arc.send(());
276-
277-
// Keep the Arc alive for next callback
278-
mem::forget(sender_arc);
279-
280-
0
281-
}
282-
283-
// Create a channel for signaling changes from audio thread
284-
let (change_tx, change_rx) = std::sync::mpsc::channel::<()>();
285-
286-
// Keep the sender alive for the duration of the controller
287-
self._change_signal = Some(change_tx.clone());
288-
289-
// Spawn worker thread to handle volume reading off the audio thread
247+
// Use polling instead of property listeners to avoid interfering with audio playback
248+
// CoreAudio property listeners were causing static noise during playback
290249
let device_id = self.device_id;
291250
let last_self_change = Arc::clone(&self.last_self_change);
292-
let worker_thread = std::thread::spawn(move || {
293-
use std::time::{Duration, Instant};
294251

295-
// Rate limiting: minimum time between notifications
296-
const MIN_NOTIFICATION_INTERVAL: Duration = Duration::from_millis(50);
297-
// Ignore notifications within this window after self-initiated changes
298-
const SELF_CHANGE_GRACE_PERIOD: u64 = 200; // milliseconds
252+
let polling_thread = std::thread::spawn(move || {
253+
use std::time::Duration;
254+
255+
const POLL_INTERVAL: Duration = Duration::from_secs(2);
256+
const SELF_CHANGE_GRACE_PERIOD: u64 = 1000; // milliseconds
299257

300-
let mut last_notification = Instant::now();
301258
let mut last_values: Option<(u8, bool)> = None;
302259

303-
while let Ok(()) = change_rx.recv() {
304-
// Drain any pending signals to coalesce rapid-fire events
305-
while change_rx.try_recv().is_ok() {}
260+
loop {
261+
std::thread::sleep(POLL_INTERVAL);
306262

307-
// Check if this change was self-initiated (within grace period)
263+
// Check if this was recently self-initiated
308264
let now_ms = SystemTime::now()
309265
.duration_since(UNIX_EPOCH)
310266
.unwrap()
311267
.as_millis() as u64;
312268
let last_self_ms = last_self_change.load(Ordering::Relaxed);
313269
if now_ms.saturating_sub(last_self_ms) < SELF_CHANGE_GRACE_PERIOD {
314-
// Skip notification - this was triggered by our own volume change
315-
continue;
316-
}
317-
318-
// Rate limit: only process if enough time has passed
319-
if last_notification.elapsed() < MIN_NOTIFICATION_INTERVAL {
270+
// Skip - recently set by us
320271
continue;
321272
}
322273

323-
// Read current volume and mute state (off audio thread)
274+
// Read current volume
324275
let volume_result = unsafe {
325276
let property_address = AudioObjectPropertyAddress {
326277
mSelector: kAudioDevicePropertyVolumeScalar,
@@ -347,6 +298,7 @@ impl VolumeControlImpl for MacOSVolumeControl {
347298
}
348299
};
349300

301+
// Read current mute state
350302
let mute_result = unsafe {
351303
let property_address = AudioObjectPropertyAddress {
352304
mSelector: kAudioDevicePropertyMute,
@@ -377,83 +329,25 @@ impl VolumeControlImpl for MacOSVolumeControl {
377329
}
378330
};
379331

380-
// Send notification only if values changed and we successfully read both
332+
// Send notification only if values changed
381333
if let (Some(volume), Some(muted)) = (volume_result, mute_result) {
382334
let current_values = (volume, muted);
383335

384-
// Only notify if values actually changed
385-
if last_values != Some(current_values) && callback.send(current_values).is_ok()
386-
{
387-
last_values = Some(current_values);
388-
last_notification = Instant::now();
336+
if last_values != Some(current_values) {
337+
if callback.send(current_values).is_ok() {
338+
last_values = Some(current_values);
339+
} else {
340+
// Channel closed, exit thread
341+
break;
342+
}
389343
}
390344
}
391345
}
392346
});
393347

394-
self._worker_thread = Some(worker_thread);
395-
396-
// Wrap the change sender in Arc for sharing across callbacks
397-
let sender_arc = Arc::new(change_tx);
398-
399-
// Register listener for volume changes
400-
let volume_address = AudioObjectPropertyAddress {
401-
mSelector: kAudioDevicePropertyVolumeScalar,
402-
mScope: kAudioDevicePropertyScopeOutput,
403-
mElement: kAudioObjectPropertyElementMain,
404-
};
405-
406-
let client_data = Arc::into_raw(Arc::clone(&sender_arc)) as *mut std::ffi::c_void;
407-
408-
unsafe {
409-
let status = AudioObjectAddPropertyListener(
410-
self.device_id,
411-
&volume_address,
412-
Some(property_listener),
413-
client_data,
414-
);
415-
416-
if status != 0 {
417-
// Clean up the Arc we created
418-
let _ = Arc::from_raw(client_data as *const std::sync::mpsc::Sender<()>);
419-
return Err(format!(
420-
"Failed to add volume property listener: {}",
421-
status
422-
));
423-
}
424-
}
425-
426-
// Register listener for mute changes (if supported)
427-
let mute_address = AudioObjectPropertyAddress {
428-
mSelector: kAudioDevicePropertyMute,
429-
mScope: kAudioDevicePropertyScopeOutput,
430-
mElement: kAudioObjectPropertyElementMain,
431-
};
432-
433-
if unsafe { AudioObjectHasProperty(self.device_id, &mute_address) } != 0 {
434-
let client_data = Arc::into_raw(sender_arc) as *mut std::ffi::c_void;
435-
436-
unsafe {
437-
let status = AudioObjectAddPropertyListener(
438-
self.device_id,
439-
&mute_address,
440-
Some(property_listener),
441-
client_data,
442-
);
443-
444-
if status != 0 {
445-
// Clean up the Arc we created
446-
let _ = Arc::from_raw(client_data as *const std::sync::mpsc::Sender<()>);
447-
eprintln!(
448-
"[VolumeControl] Warning: Failed to add mute property listener: {}",
449-
status
450-
);
451-
}
452-
}
453-
}
348+
self._worker_thread = Some(polling_thread);
454349

455-
eprintln!("[VolumeControl] macOS volume change listener registered");
350+
eprintln!("[VolumeControl] macOS volume polling enabled (2s interval)");
456351
Ok(())
457-
*/ // END DISABLED CODE
458352
}
459353
}

0 commit comments

Comments
 (0)