Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ All notable changes to PolterType are recorded here. The format is
loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and the project follows [Semantic Versioning](https://semver.org/).

## [Unreleased]

### macOS

- **PolterType no longer blocks display / system sleep when the sound
output is HDMI (or DisplayPort).** The audio worker cached its
CoreAudio `OutputStream` for the whole life of the process once a
sound had played, and an open output stream on an HDMI device keeps
coreaudiod's power assertion alive — macOS then refuses to turn the
screen off or sleep, as if audio were playing forever. The worker
now releases the stream after 30 s without a sound command (the
existing `STREAM_IDLE_REFRESH` window) and reopens it lazily on the
next play; the ~20-50 ms reopen cost is hidden under the synth
tone's lead silence.

## [0.12.0] — the AI socket ships in the box

### Changed
Expand Down Expand Up @@ -62,7 +77,6 @@ and the project follows [Semantic Versioning](https://semver.org/).
quotes. Still no automatic restart — a plug-in that crashes on
startup would become a fork bomb, and the failure would go back to
being invisible.

## [0.11.0] — plug-ins that run, and the first release Windows was actually held to

Two blocks, and they meet in one place: the plug-in system landed in
Expand Down
7 changes: 5 additions & 2 deletions crates/poltertype-core/src/audio/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

use std::time::Duration;

/// Drop the cached `OutputStream` after this much idle time so the
/// next play picks up the (possibly changed) default audio device.
/// Drop the cached `OutputStream` after this much idle time. Two
/// reasons: the next play picks up the (possibly changed) default
/// audio device, and — critically on macOS with HDMI output — an
/// idle open stream otherwise keeps coreaudiod's power assertion
/// alive and blocks display/system sleep.
/// 30 s is well above any plausible "pause-resume burst" cadence,
/// so rapid hotkey use stays on the warm cached stream.
pub(crate) const STREAM_IDLE_REFRESH: Duration = Duration::from_secs(30);
Expand Down
8 changes: 8 additions & 0 deletions crates/poltertype-core/src/audio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@
//! these handle "user just plugged in headphones" gracefully without
//! paying the per-play cost during normal pause / resume bursts.
//!
//! The cached stream is also **released outright** after
//! [`STREAM_IDLE_REFRESH`] with no commands. A permanently open
//! CoreAudio output on an HDMI / DisplayPort device keeps
//! coreaudiod's power assertion alive, which on macOS blocks display
//! and system sleep ("app holds the audio focus" symptom). Letting
//! go of the stream between plays costs one ~20-50 ms reopen, hidden
//! under the synth's lead silence.
//!
//! Themes live in `<config-dir>/sound-themes/<name>/<event>.ogg`.
//! Missing files are silent — we never crash because audio is absent.

Expand Down
35 changes: 33 additions & 2 deletions crates/poltertype-core/src/audio/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,42 @@ use std::io::BufReader;
use tracing::{debug, info, warn};

pub(crate) fn run_worker(rx: crossbeam_channel::Receiver<AudioCmd>) {
info!("audio worker started (cached OutputStream + idle refresh)");
use crossbeam_channel::RecvTimeoutError;

info!("audio worker started (cached OutputStream + idle release)");

let mut state = WorkerState::new();

while let Ok(cmd) = rx.recv() {
loop {
// Block indefinitely while there is no stream to release;
// only poll on a timeout while a stream is cached. Waiting
// with `recv_timeout` unconditionally would wake the worker
// every 30 s for the life of the process just to discover
// there is nothing to drop.
let cmd = if state.stream.is_some() {
match rx.recv_timeout(STREAM_IDLE_REFRESH) {
Ok(cmd) => cmd,
Err(RecvTimeoutError::Timeout) => {
// No plays for a whole refresh window — release
// the stream. A long-lived open CoreAudio output
// on an HDMI / DisplayPort device keeps
// coreaudiod's power assertion alive, which on
// macOS blocks display sleep and system sleep.
// Dropping the stream hands the device back and
// costs only a ~20-50 ms reopen on the next sound
// (cushioned by LEAD_SILENCE_MS).
debug!("audio: idle timeout — releasing output stream");
state.invalidate();
continue;
}
Err(RecvTimeoutError::Disconnected) => break,
}
} else {
match rx.recv() {
Ok(cmd) => cmd,
Err(_) => break,
}
};
match cmd {
AudioCmd::Refresh {
theme_dir: d,
Expand Down