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
|
### 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)**
|
**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).
|
// 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};
|
use rodio::cpal::traits::{DeviceTrait, HostTrait};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let _guard = unsafe {
|
let _guard = unsafe {
|
||||||
@@ -244,26 +253,28 @@ pub fn start_device_watcher(engine: &AudioEngine, app: tauri::AppHandle) {
|
|||||||
let default = host
|
let default = host
|
||||||
.default_output_device()
|
.default_output_device()
|
||||||
.and_then(|d| d.description().ok().map(|desc| desc.name().to_string()));
|
.and_then(|d| d.description().ok().map(|desc| desc.name().to_string()));
|
||||||
let available: Vec<String> = host
|
let available: Vec<String> = if need_full_enum {
|
||||||
.output_devices()
|
host.output_devices()
|
||||||
.map(|iter| {
|
.map(|iter| {
|
||||||
iter.filter_map(|d| d.description().ok().map(|desc| desc.name().to_string()))
|
iter.filter_map(|d| d.description().ok().map(|desc| desc.name().to_string()))
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
(default, available)
|
(default, available)
|
||||||
}).await.unwrap_or((None, vec![]));
|
}).await.unwrap_or((None, vec![]));
|
||||||
|
|
||||||
// Empty list almost always means a transient enumeration failure, not
|
// Empty list (only when we actually enumerated for a pinned device)
|
||||||
// that every output device vanished. Treating it as "pinned missing"
|
// almost always means a transient enumeration failure, not that every
|
||||||
// caused false audio:device-reset (UI jumped back to system default)
|
// output device vanished. Treating it as "pinned missing" caused false
|
||||||
// when switching to external USB / class-compliant interfaces.
|
// audio:device-reset (UI jumped back to system default) when switching
|
||||||
if available.is_empty() {
|
// to external USB / class-compliant interfaces.
|
||||||
|
if need_full_enum && available.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let pinned = selected_device.lock().unwrap().clone();
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
if pinned.is_some() {
|
if pinned.is_some() {
|
||||||
// Do not infer "unplugged" from `output_devices()` when a device is pinned.
|
// Do not infer "unplugged" from `output_devices()` when a device is pinned.
|
||||||
|
|||||||
Reference in New Issue
Block a user