merge(audio): land feat/linux-stream-sink — Linux host-owned stream sink

Merges 44b71e74 (see its message for the full design: the "Punktfunk Stream
Speaker" Audio/Sink claimed as the session's default output — the crackle
root fix for hardware-sink churn — plus the core-error liveness fix and true
5.1/7.1 capture) across the W1–W8 refactor that landed since the branch
forked:

- punktfunk1.rs was split on main: the audio_thread idle()/drain() hunks are
  hand-ported into native/audio.rs (same three sites: reuse-drain comment,
  encoder-fail park, end-of-thread park).
- pwinit moved to pf-capture (W6.2): the branch's crate::pwinit calls in
  pw_thread and stream_sink.rs become pf_capture::pwinit.
- Kept main's log-tier demotions (stream-state + setup lines at debug)
  inside the restructured pw_thread.

Resolution verified on Linux (home-worker-5): clippy -D warnings clean +
173 punktfunk-host tests green at the 691c064a-based equivalent ba5973a2;
re-verified at this base before landing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 16:35:02 +02:00
5 changed files with 676 additions and 141 deletions
+7 -4
View File
@@ -71,9 +71,9 @@ pub(super) fn audio_thread(
// Reuse the cached capturer ONLY when its channel count matches this session's; a stereo
// capturer left by a prior session must not feed a 5.1/7.1 session (the encoder + the client's
// decoder are sized for `want`, so a mismatched capturer would garble/desync the audio).
let capturer = match audio_cap.lock().unwrap().take() {
let mut capturer = match audio_cap.lock().unwrap().take() {
Some(mut c) if c.channels() == want as u32 => {
c.drain(); // discard audio captured between sessions
c.drain(); // discard audio captured between sessions (also re-claims routing)
c
}
prev => {
@@ -91,6 +91,7 @@ pub(super) fn audio_thread(
Ok(e) => e,
Err(e) => {
tracing::warn!(error = %e, "opus encoder init failed — session continues without audio");
capturer.idle(); // parked, not streaming — release the routing claim
*audio_cap.lock().unwrap() = Some(capturer);
return;
}
@@ -172,8 +173,10 @@ pub(super) fn audio_thread(
}
}
}
// Return the live capturer for the next session (None if it died and never reopened).
if let Some(c) = capturer {
// Return the live capturer for the next session (None if it died and never reopened),
// releasing its session-scoped routing claim (Linux: the default sink moves back).
if let Some(mut c) = capturer {
c.idle();
*audio_cap.lock().unwrap() = Some(c);
}
}