Skip to content

feat(macos): follow the system's audio output - #2

Merged
parda21 merged 1 commit into
feat/macos-now-playingfrom
feat/macos-follow-output
Oct 15, 2026
Merged

parda21 merged 1 commit into
feat/macos-now-playingfrom
feat/macos-follow-output

Conversation

@yk-sgr

@yk-sgr yk-sgr commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #1.

What changes

  • Playback follows the system's default output. It moves to AirPods or headphones as they connect, or to another output picked in Control Center.
  • It pauses when the output in use disconnects, the way Apple's apps do, instead of carrying on through the speakers. The TUI says which output went away (Paused: AirPods Pro disconnected).
  • The stall is fixed. librespot's rodio output stays on the device that was the default when it opened. When that device disconnected, its write loop waited for a stream that never drained again, and playback stopped for good.

How

  • cloofy-os::audio::OutputWatch is an OS-neutral watcher for "the default output changed". On macOS it's a CoreAudio property listener; elsewhere it's None, since PulseAudio follows the default output by itself.
  • cloofy-player gets a macOS output that reopens on the new default when it changes:
    • If the output it played on is gone, it reports it and plays silence until the pause arrives, so nothing leaks through the speakers.
    • Resuming afterwards plays on the new default.
    • All its waits are bounded.
  • Engine::new also returns the receiver for these reports. The TUI, cloofy play and cloofy connect pause on them.

Verified

  • Three unit tests drive the real output on the Mac. They return early on machines without one. They simulate an output going away by renaming the open output and flagging a change:
    • while playing, it reports the loss and goes silent;
    • after another output was picked, it moves on without pausing;
    • after a loss while paused, it plays on when resumed.
  • The real TUI logs audio output: MacBook Pro-Lautsprecher from the new output at startup.
  • I couldn't test it with actual hardware here, and switching your system's output from a script seemed too intrusive.

To test by hand

  • Play through the speakers and connect AirPods: playback moves to the AirPods
  • Put the AirPods back in the case: playback pauses with "Paused: … disconnected"; resuming plays through the speakers
  • Pick another output in Control Center while playing: playback moves there without pausing
  • On Apple silicon, unplug wired headphones: playback pauses

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5b10e076-5c53-4667-939d-2263375f0ede

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@yk-sgr
yk-sgr force-pushed the feat/macos-follow-output branch from 7872b54 to 629ad77 Compare October 15, 2026 13:15
Playback moves to AirPods or headphones as they connect, and pauses
when the output in use disconnects, instead of carrying on through the
speakers.

librespot's rodio output stays on the device that was the default when
it opened. When that device disconnected, its write loop waited forever
for a stream that never drained again, and playback stalled.

- cloofy-os reports changes of the default output, through a CoreAudio
  property listener on macOS.
- cloofy-player gets a macOS output that reopens on the new default when
  it changes. If the output it played on is gone, it reports it and
  plays silence until the pause arrives, so nothing leaks through the
  speakers. Its waits are bounded, so a dead stream can't block the
  player anymore.
- The TUI and `cloofy play`/`cloofy connect` pause on the report. The
  TUI says which output went away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yk-sgr
yk-sgr force-pushed the feat/macos-now-playing branch from af403da to 2fe8d99 Compare October 15, 2026 14:47
@yk-sgr
yk-sgr force-pushed the feat/macos-follow-output branch from 629ad77 to 015e57f Compare October 15, 2026 14:47
@parda21
parda21 merged commit 015e57f into feat/macos-now-playing Oct 15, 2026
3 checks passed
@parda21

parda21 commented Oct 15, 2026

Copy link
Copy Markdown
Owner

Merged through #7, unchanged. The review found two things, fixed on top:

  • 339a474: when the output went away, the player was paused but still shown as playing until it confirmed. A pause from the headphones in that window (AirPods switching devices) toggled playback back on, through the speakers. It now pauses regardless of state, shows paused at once, and answers the system's pause with a pause instead of a toggle.
  • 3d2c99c: if opening the new default output failed (Bluetooth headphones sometimes refuse at first), playback went on in silence. It now retries once a second and logs only the first failure.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants