mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
* fix(audio): poll only the default device when none is pinned (#996) The device watcher ran a full output_devices() + per-device description() CoreAudio enumeration every 3s, even with no pinned device. On some macOS setups this contends with the audio render thread and causes a brief dropout once per poll — a stutter whose cadence tracks the poll interval exactly. The full enumeration is only needed to detect a pinned device disappearing. With no pin (system default, the common case) only the current default is needed, so the enumeration is skipped entirely in that case; the cheap single default_output_device() query still detects default-device changes. Confirmed with a diagnostic build: throttling the enumeration to ~60s moved the reporter's stutter cadence from ~3s to ~60s, isolating the enumeration as the cause. * docs: add CHANGELOG entry for PR #1039
This commit is contained in:
@@ -167,6 +167,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
|
||||
|
||||
### Playback — macOS stutter from background device checks
|
||||
|
||||
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1039](https://github.com/Psychotoxical/psysonic/pull/1039)**
|
||||
|
||||
* On some macOS setups playback stuttered at a steady ~3-second cadence: a background check that scans every audio output device ran on each poll and briefly contended with playback. It now runs only when a specific output device is pinned; with the system default (the common case) a single lightweight check runs instead.
|
||||
|
||||
|
||||
|
||||
### Track preview — Symphonia 0.6 format hints and fast stream start
|
||||
|
||||
**By [@cucadmuh](https://github.com/cucadmuh), PR [#1006](https://github.com/Psychotoxical/psysonic/pull/1006)**
|
||||
|
||||
@@ -224,9 +224,18 @@ pub fn start_device_watcher(engine: &AudioEngine, app: tauri::AppHandle) {
|
||||
}
|
||||
}
|
||||
|
||||
// Enumerate all available output devices and the current default.
|
||||
// The full `output_devices()` + per-device `description()` scan is the
|
||||
// CoreAudio HAL call that contends with the audio render thread and
|
||||
// produces a brief dropout once per poll interval (issue #996: stutter
|
||||
// every ~3s, cadence tracking the poll exactly). It is only needed to
|
||||
// detect a *pinned* output device disappearing. With no pin — system
|
||||
// default, the common case — only the current default is needed, a
|
||||
// single cheap query, so the full enumeration is skipped entirely.
|
||||
let pinned = selected_device.lock().unwrap().clone();
|
||||
let need_full_enum = pinned.is_some();
|
||||
|
||||
// Suppress stderr on Unix to avoid ALSA probing noise (JACK, OSS, dmix).
|
||||
let (current_default, available) = tauri::async_runtime::spawn_blocking(|| {
|
||||
let (current_default, available) = tauri::async_runtime::spawn_blocking(move || {
|
||||
use rodio::cpal::traits::{DeviceTrait, HostTrait};
|
||||
#[cfg(unix)]
|
||||
let _guard = unsafe {
|
||||
@@ -244,26 +253,28 @@ pub fn start_device_watcher(engine: &AudioEngine, app: tauri::AppHandle) {
|
||||
let default = host
|
||||
.default_output_device()
|
||||
.and_then(|d| d.description().ok().map(|desc| desc.name().to_string()));
|
||||
let available: Vec<String> = host
|
||||
.output_devices()
|
||||
.map(|iter| {
|
||||
iter.filter_map(|d| d.description().ok().map(|desc| desc.name().to_string()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let available: Vec<String> = if need_full_enum {
|
||||
host.output_devices()
|
||||
.map(|iter| {
|
||||
iter.filter_map(|d| d.description().ok().map(|desc| desc.name().to_string()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
(default, available)
|
||||
}).await.unwrap_or((None, vec![]));
|
||||
|
||||
// Empty list almost always means a transient enumeration failure, not
|
||||
// that every output device vanished. Treating it as "pinned missing"
|
||||
// caused false audio:device-reset (UI jumped back to system default)
|
||||
// when switching to external USB / class-compliant interfaces.
|
||||
if available.is_empty() {
|
||||
// Empty list (only when we actually enumerated for a pinned device)
|
||||
// almost always means a transient enumeration failure, not that every
|
||||
// output device vanished. Treating it as "pinned missing" caused false
|
||||
// audio:device-reset (UI jumped back to system default) when switching
|
||||
// to external USB / class-compliant interfaces.
|
||||
if need_full_enum && available.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pinned = selected_device.lock().unwrap().clone();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if pinned.is_some() {
|
||||
// Do not infer "unplugged" from `output_devices()` when a device is pinned.
|
||||
|
||||
Reference in New Issue
Block a user