feat(host/audio): minted Punktfunk endpoints become the wiring plan's tier-0

The audio-substrate program's Phase 2 (spikes S2+S3 measured green on the
target box): the host mints its OWN instances of Valve's streaming-audio
drivers and wires by IDENTITY instead of borrowing Steam's primaries —

  minted.rs      the provider: one devnode per role ('Punktfunk Speakers'
                 from SteamStreamingSpeakers.inf, 'Punktfunk Microphone'
                 from SteamStreamingMicrophone.inf), marker-matched across
                 restarts (PunktfunkAudioRole in Device Parameters — names
                 are NOT identity, a minted instance is name-identical to
                 the primaries), provisioned on a startup worker like pad
                 audio, retried with a 60 s cool-down from wiring passes,
                 defaults restored when a fresh endpoint grabs them.
  wiring_plan    MintedIds tier-0: the mic takes its minted device outright
                 (capture side paired by the provider's id — a name search
                 cannot tell it from the primary), the loopback prefers the
                 minted sink at the head of the silent tier, an operator
                 override still beats everything, a narrowing minted sink
                 demotes below real hardware, and stale ids fall back to
                 the ladder unchanged. Plus AudioReadiness — the
                 full/audio-only/mic-only/nothing classification, logged
                 with every plan change (§C4's seed).
  audio-probe    'mint' runs the provider synchronously; 'plan' prints one
                 real wiring pass + readiness — the field-triage command.

Without Steam's drivers nothing changes: provisioning degrades to absent
ids and the plan keeps the name-based ladder (primaries → cable → real
hardware) exactly as before.
This commit is contained in:
2026-08-07 11:22:06 +02:00
parent 03ee3e55d7
commit 0b160a4e22
8 changed files with 906 additions and 142 deletions
+5
View File
@@ -194,6 +194,11 @@ pub(crate) mod pad_endpoint;
#[cfg(target_os = "windows")]
#[path = "audio/windows/audio_probe.rs"]
pub(crate) mod audio_probe;
// The minted "Punktfunk Speakers/Microphone" provider — punktfunk-owned instances of Valve's
// streaming-audio drivers, the wiring plan's tier-0 (the audio-substrate program).
#[cfg(target_os = "windows")]
#[path = "audio/windows/minted.rs"]
pub(crate) mod minted;
#[cfg(target_os = "windows")]
#[path = "audio/windows/wasapi_cap.rs"]
mod wasapi_cap;
@@ -198,6 +198,14 @@ pub(crate) fn wire_now_full(set_playback: bool) -> WiredPlan {
// cannot carry stereo cannot carry 5.1 either.
2,
&pad_ids,
// The minted "Punktfunk Speakers/Microphone" ids — tier-0 identity, empty until the
// provider latches. The ensure hook makes a box where Steam arrives later mint on a
// wiring pass instead of at the next reboot (cheap once latched; cooled-down retries
// while not).
&{
super::minted::ensure_provisioned();
super::minted::minted_ids()
},
);
let done = |wiring: Wiring| WiredPlan {
wiring,
@@ -220,6 +228,7 @@ pub(crate) fn wire_now_full(set_playback: bool) -> WiredPlan {
loopback_render = wiring.loopback_render.as_ref().map(|(n, _)| n.as_str()),
loopback_last_resort = wiring.loopback_last_resort,
mic_withheld = wiring.mic_withheld,
readiness = ?wiring_plan::readiness(&wiring),
renders = ?renders.iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>(),
"audio wiring plan"
);
@@ -265,7 +274,16 @@ pub(crate) fn wire_now_full(set_playback: bool) -> WiredPlan {
if let Some((mic_name, mic_id)) = &wiring.mic_render {
if default_render_id().as_deref() == Some(mic_id.as_str()) {
// Audible preference = the host_audio plan's loopback pick (real hardware first).
match plan(&renders, &captures, want.as_deref(), true, &pad_ids).loopback_render {
match plan(
&renders,
&captures,
want.as_deref(),
true,
&pad_ids,
&super::minted::minted_ids(),
)
.loopback_render
{
Some((name, id)) => match set_default_endpoint(&id) {
Ok(()) => tracing::info!(mic = %mic_name, device = %name,
"default playback was the virtual-mic target — moved it so desktop \
@@ -34,7 +34,7 @@ use std::time::{Duration, Instant};
use wasapi::{Direction, SampleType, StreamMode, WaveFormat};
use windows::core::PCWSTR;
use windows::Win32::Devices::DeviceAndDriverInstallation::{
SetupDiEnumDeviceInfo, SetupDiOpenDevRegKey, DICS_FLAG_GLOBAL, DIREG_DEV, SPDRP_HARDWAREID,
SetupDiEnumDeviceInfo, SetupDiOpenDevRegKey, DICS_FLAG_GLOBAL, DIREG_DEV,
};
use windows::Win32::System::Registry::{
RegCloseKey, RegQueryValueExW, RegSetValueExW, KEY_QUERY_VALUE, KEY_SET_VALUE, REG_DWORD,
@@ -70,7 +70,36 @@ pub(crate) fn run(args: &[String]) -> Result<()> {
probe_sss_primary(secs)
}
Some("cleanup") => cleanup(),
_ => bail!("usage: punktfunk-host audio-probe <ssm|sink|sss-primary|cleanup> [--keep]"),
// The provider's synchronous pass: mint (or re-find) "Punktfunk Speakers/Microphone"
// and publish them for THIS process — `plan` then shows the tier-0 pick.
Some("mint") => super::minted::devtest_mint(),
// One real wiring pass (no default parking) + the verdict, readiness included — the
// field-triage "what would the host do right now" command.
Some("plan") => {
let plan = super::audio_control::wire_now_full(false);
let w = &plan.wiring;
let show = |ep: &Option<super::wiring_plan::Endpoint>| match ep {
Some((name, id)) => format!("{name:?} ({id})"),
None => "-".into(),
};
println!("audio-plan: mic_render = {}", show(&w.mic_render));
println!("audio-plan: mic_capture = {}", show(&w.mic_capture));
println!("audio-plan: loopback = {}", show(&w.loopback_render));
println!("audio-plan: last_resort = {}", w.loopback_last_resort);
println!("audio-plan: mic_withheld = {}", w.mic_withheld);
println!(
"audio-plan: narrowing = {}",
w.loopback_narrowing.as_deref().unwrap_or("-")
);
println!(
"audio-plan: readiness = {:?}",
super::wiring_plan::readiness(w)
);
Ok(())
}
_ => bail!(
"usage: punktfunk-host audio-probe <ssm|sink|sss-primary|mint|plan|cleanup> [--keep]"
),
}
}
@@ -243,57 +272,9 @@ fn probe_sss_primary(secs: u32) -> Result<()> {
Ok(())
}
// --- driver discovery ----------------------------------------------------------------------
// --- driver discovery — shared with the minted provider -------------------------------------
/// Find the (exact hardware id, INF path) for a Steam streaming driver: prefer any installed
/// devnode whose hardware-id list contains `needle` (its `oemNN.inf` is the driver Windows
/// already trusts), else fall back to Steam's driver directory.
fn discover_driver(needle: &str, inf_name: &str) -> Result<(String, String)> {
let set = pe::media_class_devs()?;
for i in 0.. {
let mut did = pe::devinfo_data();
// SAFETY: live set; `did` is a live out-param with cbSize set.
if unsafe { SetupDiEnumDeviceInfo(set.0, i, &mut did) }.is_err() {
break;
}
let Some(hwid) = pe::devnode_multi_sz_prop(&set, &did, SPDRP_HARDWAREID)
.into_iter()
.find(|h| h.to_lowercase().contains(needle))
else {
continue;
};
if let Some(inf) = pe::devnode_inf_path(&set, &did) {
let windir = std::env::var("WINDIR").unwrap_or_else(|_| r"C:\Windows".into());
let full = format!(r"{windir}\INF\{inf}");
if std::path::Path::new(&full).exists() {
return Ok((hwid, full));
}
}
// Devnode exists but its INF is gone — keep its exact hwid, try Steam's directory.
if let Some(w) = super::wasapi_mic::steam_driver_inf_path(inf_name) {
let s = String::from_utf16_lossy(&w)
.trim_end_matches('\0')
.to_string();
if std::path::Path::new(&s).exists() {
return Ok((hwid, s));
}
}
}
// No installed devnode at all: canonical hwid + Steam's directory.
if let Some(w) = super::wasapi_mic::steam_driver_inf_path(inf_name) {
let s = String::from_utf16_lossy(&w)
.trim_end_matches('\0')
.to_string();
if std::path::Path::new(&s).exists() {
let hwid = format!("ROOT\\{}", inf_name.trim_end_matches(".inf"));
return Ok((hwid, s));
}
}
bail!(
"no installed devnode matches {needle:?} and Steam's driver directory has no \
{inf_name} — install Steam (it never needs to run)"
)
}
use super::minted::discover_driver;
// --- probe devnode marker + cleanup --------------------------------------------------------
@@ -0,0 +1,378 @@
//! Minted punktfunk-owned audio endpoints — the Windows audio substrate.
//!
//! The audio-substrate decision (`windows-audio-endpoints-and-vbcable.md`, 2026-08-07, spikes
//! S2+S3 measured green): instead of borrowing Steam's primary endpoints and bundling VB-Cable
//! for the mic, the host mints its OWN instances of Valve's streaming-audio drivers —
//!
//! * **"Punktfunk Speakers"** (`SteamStreamingSpeakers.inf`): the client-only loopback sink.
//! Desktop audio routes here (the wiring plan parks the default playback on it during a
//! stream), its WASAPI loopback feeds the encoder, and the host stays silent. Measured
//! clean: 48 kHz stereo f32, loopback peak == rendered peak (S2).
//! * **"Punktfunk Microphone"** (`SteamStreamingMicrophone.inf`): the virtual mic. The host
//! writes the client's decoded voice into its render side; its capture side surfaces as the
//! microphone host apps record. Measured bit-faithful render→capture (S3).
//!
//! Provisioning mirrors the pad-audio provider: a background worker at host start, idempotent
//! devnode-per-role with a durable `PunktfunkAudioRole` marker in `Device Parameters` (names
//! are NOT identity — a minted instance is name-identical to Steam's primaries), results
//! published once for the wiring plan to consume BY ID ([`minted_ids`] →
//! [`wiring_plan::MintedIds`]). Everything is best-effort: no Steam driver, a denied install,
//! or `PUNKTFUNK_NO_AUDIO_MINT` leaves the ids empty and the wiring plan falls back to the
//! name-based ladder (Steam primaries → cable → real hardware) unchanged.
//!
//! Endpoints are PERSISTENT by design, like pad endpoints — they survive host restarts and
//! re-resolve by marker on the next start. `punktfunk-host audio-probe` carries the manual
//! `mint` / `plan` inspection paths.
use super::pad_endpoint as pe;
use super::{audio_control, wiring_plan};
use anyhow::{bail, Context, Result};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::thread;
use std::time::{Duration, Instant};
/// Durable role marker in a minted devnode's `Device Parameters` key.
const ROLE_MARKER: &str = "PunktfunkAudioRole";
/// How long to wait for audiosrv to register a freshly minted endpoint.
const ENDPOINT_WAIT: Duration = Duration::from_secs(15);
/// Minimum spacing between provisioning retries once the startup attempt failed
/// ([`ensure_provisioned`] is called from wiring passes, which recur freely).
const RETRY_COOLDOWN: Duration = Duration::from_secs(60);
/// The two minted roles. `value` is the persisted marker; the needles drive
/// [`discover_driver`].
#[derive(Clone, Copy, PartialEq)]
enum Role {
Speakers,
Mic,
}
impl Role {
fn value(self) -> u32 {
match self {
Role::Speakers => 1,
Role::Mic => 2,
}
}
fn desc(self) -> &'static str {
match self {
Role::Speakers => "Punktfunk Speakers",
Role::Mic => "Punktfunk Microphone",
}
}
fn needle(self) -> &'static str {
match self {
Role::Speakers => "steamstreamingspeakers",
Role::Mic => "steamstreamingmicrophone",
}
}
fn inf_name(self) -> &'static str {
match self {
Role::Speakers => "SteamStreamingSpeakers.inf",
Role::Mic => "SteamStreamingMicrophone.inf",
}
}
fn label(self) -> &'static str {
match self {
Role::Speakers => "speakers",
Role::Mic => "mic",
}
}
}
/// The provider's published result. Partial is possible and usable (one driver leg failing
/// must not cost the other role); consumers read the per-role `Option`s.
#[derive(Debug, Default, Clone)]
pub(crate) struct MintedAudio {
pub speakers_devnode: Option<String>,
pub speakers_render: Option<String>,
pub mic_devnode: Option<String>,
pub mic_render: Option<String>,
pub mic_capture: Option<String>,
}
impl MintedAudio {
fn any(&self) -> bool {
self.speakers_render.is_some() || self.mic_render.is_some()
}
}
/// Set once by the worker, and only when at least one role provisioned (the pad provider's R5
/// lesson: latching an empty result turns one transient failure into a process-lifetime
/// disability).
static PROVISIONED: OnceLock<Arc<MintedAudio>> = OnceLock::new();
/// A provisioning attempt is in flight — keeps concurrent askers to one worker.
static PROVISIONING: AtomicBool = AtomicBool::new(false);
/// When the last attempt STARTED — the [`RETRY_COOLDOWN`] anchor.
static LAST_ATTEMPT: Mutex<Option<Instant>> = Mutex::new(None);
/// The wiring plan's tier-0 input: the minted ids, or all-empty while nothing is provisioned.
pub(crate) fn minted_ids() -> wiring_plan::MintedIds {
match PROVISIONED.get() {
Some(m) => wiring_plan::MintedIds {
speakers_render: m.speakers_render.clone(),
mic_render: m.mic_render.clone(),
mic_capture: m.mic_capture.clone(),
},
None => wiring_plan::MintedIds::default(),
}
}
/// Spawn the provisioning worker (idempotent; returns immediately). Called at host start next
/// to the pad provider, and again from [`ensure_provisioned`] on the retry path.
pub(crate) fn provision_at_startup() {
if std::env::var_os("PUNKTFUNK_NO_AUDIO_MINT").is_some() {
return;
}
if PROVISIONED.get().is_some() || PROVISIONING.swap(true, Ordering::SeqCst) {
return;
}
*LAST_ATTEMPT.lock().unwrap() = Some(Instant::now());
let spawned = thread::Builder::new()
.name("punktfunk-audio-mint".into())
.spawn(|| {
match ensure_all() {
Ok(m) if m.any() => {
tracing::info!(
speakers = m.speakers_render.as_deref().unwrap_or("-"),
mic_render = m.mic_render.as_deref().unwrap_or("-"),
mic_capture = m.mic_capture.as_deref().unwrap_or("-"),
"minted audio endpoints ready (the wiring plan's tier-0)"
);
let _ = PROVISIONED.set(Arc::new(m));
}
Ok(_) => tracing::info!(
"no minted audio endpoints (Steam's streaming drivers absent?) — the \
wiring plan keeps the name-based ladder"
),
Err(e) => tracing::warn!(error = %format!("{e:#}"),
"minted-audio provisioning failed — the wiring plan keeps the name-based \
ladder and a later wiring pass retries"),
}
PROVISIONING.store(false, Ordering::SeqCst);
});
if let Err(e) = spawned {
PROVISIONING.store(false, Ordering::SeqCst);
tracing::warn!(error = %e, "could not spawn the minted-audio provisioning thread");
}
}
/// Retry hook for wiring passes: cheap once latched; while unlatched it re-asks at most every
/// [`RETRY_COOLDOWN`] — a box where Steam arrives later mints on a later pass instead of at
/// the next reboot.
pub(crate) fn ensure_provisioned() {
if PROVISIONED.get().is_some() {
return;
}
{
let last = LAST_ATTEMPT.lock().unwrap();
if last.is_some_and(|t| t.elapsed() < RETRY_COOLDOWN) {
return;
}
}
provision_at_startup();
}
/// One synchronous provisioning pass over both roles (worker thread + the `audio-probe mint`
/// devtest). Per-role failures degrade to that role being absent.
fn ensure_all() -> Result<MintedAudio> {
wasapi::initialize_mta()
.ok()
.context("CoInitializeEx (MTA, minted-audio)")?;
let mut out = MintedAudio::default();
for role in [Role::Speakers, Role::Mic] {
match ensure_role(role) {
Ok((devnode, render, capture)) => match role {
Role::Speakers => {
out.speakers_devnode = Some(devnode);
out.speakers_render = Some(render);
}
Role::Mic => {
out.mic_devnode = Some(devnode);
out.mic_render = Some(render);
out.mic_capture = capture;
}
},
Err(e) => tracing::info!(role = role.label(), error = %format!("{e:#}"),
"minted-audio role unavailable"),
}
}
Ok(out)
}
/// Ensure one role's devnode + endpoint(s): reuse the marker-matched devnode from an earlier
/// run, else mint one; (re)bind the driver idempotently; wait for audiosrv's endpoints; put
/// back any default device the fresh endpoint grabbed (measured on the pad program: a newly
/// registered endpoint can take either default).
fn ensure_role(role: Role) -> Result<(String, String, Option<String>)> {
let prev_render = audio_control::default_render_id();
let prev_capture = audio_control::default_capture_id();
let (hwid, inf) = discover_driver(role.needle(), role.inf_name())?;
let devnode = match find_role_devnode(role)? {
Some(inst) => inst,
None => {
let inst = pe::create_media_devnode(role.desc(), &hwid, |set, did| {
pe::write_devparam_dword(set, did, ROLE_MARKER, role.value())
})?;
tracing::info!(role = role.label(), devnode = %inst, "minted an audio devnode");
inst
}
};
pe::bind_driver(&hwid, &inf)?;
let render = wait_for(&devnode, false)?;
let capture = match role {
Role::Mic => Some(wait_for(&devnode, true).with_context(|| {
format!("the minted mic devnode {devnode} produced no capture endpoint")
})?),
Role::Speakers => None,
};
// Freshly registered endpoints can grab a default; the wiring plan owns default policy,
// not the mint.
if let Some(prev) = prev_render {
if audio_control::default_render_id().as_deref() != Some(prev.as_str())
&& audio_control::set_default_endpoint(&prev).is_ok()
{
tracing::info!(
role = role.label(),
"default playback restored after minting"
);
}
}
if let Some(prev) = prev_capture {
if audio_control::default_capture_id().as_deref() != Some(prev.as_str())
&& audio_control::set_default_endpoint(&prev).is_ok()
{
tracing::info!(
role = role.label(),
"default recording restored after minting"
);
}
}
Ok((devnode, render, capture))
}
/// Poll audiosrv for the endpoint a minted devnode registers in one direction.
fn wait_for(devnode: &str, capture: bool) -> Result<String> {
let deadline = Instant::now() + ENDPOINT_WAIT;
loop {
let found = if capture {
pe::find_capture_endpoint_for_devnode(devnode)?
} else {
pe::find_endpoint_for_devnode(devnode)?
};
if let Some(ep) = found {
return Ok(ep);
}
if Instant::now() >= deadline {
bail!(
"no {} endpoint appeared for {devnode} within {}s — is Audiosrv running?",
if capture { "capture" } else { "render" },
ENDPOINT_WAIT.as_secs()
);
}
thread::sleep(Duration::from_millis(250));
}
}
/// The devnode a previous run minted for `role` (marker-matched — names are not identity).
fn find_role_devnode(role: Role) -> Result<Option<String>> {
let set = pe::media_class_devs()?;
for i in 0.. {
let mut did = pe::devinfo_data();
// SAFETY: live set; `did` is a live out-param with cbSize set.
if unsafe {
windows::Win32::Devices::DeviceAndDriverInstallation::SetupDiEnumDeviceInfo(
set.0, i, &mut did,
)
}
.is_err()
{
break;
}
if pe::read_devparam_dword(&set, &did, ROLE_MARKER) == Some(role.value()) {
if let Some(inst) = pe::instance_id(&set, &did) {
return Ok(Some(inst));
}
}
}
Ok(None)
}
/// Find the (exact hardware id, INF path) for one of Steam's streaming drivers: prefer any
/// installed devnode whose hardware-id list contains `needle` (its `oemNN.inf` is the driver
/// Windows already trusts), else fall back to Steam's driver directory. Shared with the
/// `audio-probe` devtest.
pub(crate) fn discover_driver(needle: &str, inf_name: &str) -> Result<(String, String)> {
use windows::Win32::Devices::DeviceAndDriverInstallation::{
SetupDiEnumDeviceInfo, SPDRP_HARDWAREID,
};
let steam_dir_inf = || -> Option<String> {
let w = super::wasapi_mic::steam_driver_inf_path(inf_name)?;
let s = String::from_utf16_lossy(&w)
.trim_end_matches('\0')
.to_string();
std::path::Path::new(&s).exists().then_some(s)
};
let set = pe::media_class_devs()?;
for i in 0.. {
let mut did = pe::devinfo_data();
// SAFETY: live set; `did` is a live out-param with cbSize set.
if unsafe { SetupDiEnumDeviceInfo(set.0, i, &mut did) }.is_err() {
break;
}
let Some(hwid) = pe::devnode_multi_sz_prop(&set, &did, SPDRP_HARDWAREID)
.into_iter()
.find(|h| h.to_lowercase().contains(needle))
else {
continue;
};
if let Some(inf) = pe::devnode_inf_path(&set, &did) {
let windir = std::env::var("WINDIR").unwrap_or_else(|_| r"C:\Windows".into());
let full = format!(r"{windir}\INF\{inf}");
if std::path::Path::new(&full).exists() {
return Ok((hwid, full));
}
}
// Devnode exists but its INF is gone — keep its exact hwid, try Steam's directory.
if let Some(s) = steam_dir_inf() {
return Ok((hwid, s));
}
}
// No installed devnode at all: canonical hwid + Steam's directory.
if let Some(s) = steam_dir_inf() {
return Ok((format!("ROOT\\{}", inf_name.trim_end_matches(".inf")), s));
}
bail!(
"no installed devnode matches {needle:?} and Steam's driver directory has no \
{inf_name} — install Steam (it never needs to run)"
)
}
/// `audio-probe mint` devtest body: one synchronous provisioning pass, results printed.
pub(crate) fn devtest_mint() -> Result<()> {
let m = ensure_all()?;
println!(
"audio-mint: speakers devnode={} render={}",
m.speakers_devnode.as_deref().unwrap_or("-"),
m.speakers_render.as_deref().unwrap_or("-")
);
println!(
"audio-mint: mic devnode={} render={} capture={}",
m.mic_devnode.as_deref().unwrap_or("-"),
m.mic_render.as_deref().unwrap_or("-"),
m.mic_capture.as_deref().unwrap_or("-")
);
if m.any() {
let _ = PROVISIONED.set(Arc::new(m));
println!(
"audio-mint: published for this process — `audio-probe plan` shows the tier-0 pick"
);
} else {
println!("audio-mint: nothing minted (Steam's streaming drivers absent?)");
}
Ok(())
}
@@ -571,9 +571,14 @@ pub(crate) fn devnode_inf_path(set: &DevInfoSet, did: &SP_DEVINFO_DATA) -> Optio
(len > 0).then(|| String::from_utf16_lossy(&units[..len]))
}
/// The persisted pad slot of a devnode (the `PunktfunkPadIndex` value under its
/// `Device Parameters` key), or `None` for foreign devnodes.
fn devnode_pad_index(set: &DevInfoSet, did: &SP_DEVINFO_DATA) -> Option<u32> {
/// Read a REG_DWORD from a devnode's `Device Parameters` key — the durable owner-marker
/// mechanism every punktfunk-minted devnode family uses (pad slot, minted-audio role, probe
/// marker). `None`: no key, no value, or wrong type — a foreign devnode.
pub(crate) fn read_devparam_dword(
set: &DevInfoSet,
did: &SP_DEVINFO_DATA,
value_name: &str,
) -> Option<u32> {
// SAFETY: live set + element; DIREG_DEV opens the devnode's Device Parameters key.
let hkey = unsafe {
SetupDiOpenDevRegKey(
@@ -586,7 +591,7 @@ fn devnode_pad_index(set: &DevInfoSet, did: &SP_DEVINFO_DATA) -> Option<u32> {
)
}
.ok()?;
let name = wide(PAD_INDEX_VALUE);
let name = wide(value_name);
let mut data = [0u8; 4];
let mut len = data.len() as u32;
let mut ty = REG_VALUE_TYPE(0);
@@ -609,6 +614,67 @@ fn devnode_pad_index(set: &DevInfoSet, did: &SP_DEVINFO_DATA) -> Option<u32> {
(rc.is_ok() && ty == REG_DWORD && len == 4).then(|| u32::from_le_bytes(data))
}
/// Write a REG_DWORD into a devnode's `Device Parameters` key, creating the key on a fresh
/// devnode — the write side of [`read_devparam_dword`].
pub(crate) fn write_devparam_dword(
set: &DevInfoSet,
did: &mut SP_DEVINFO_DATA,
value_name: &str,
value: u32,
) -> Result<()> {
// SAFETY: live set + element; DIREG_DEV opens the devnode's Device Parameters key.
let opened = unsafe {
SetupDiOpenDevRegKey(
set.0,
did,
DICS_FLAG_GLOBAL.0,
0,
DIREG_DEV,
KEY_SET_VALUE.0,
)
};
let hkey = match opened {
Ok(k) => k,
// SAFETY: same set + element; a fresh devnode has no Device Parameters key yet, so
// create it (no INF association).
Err(_) => unsafe {
SetupDiCreateDevRegKeyW(
set.0,
did,
DICS_FLAG_GLOBAL.0,
0,
DIREG_DEV,
None,
PCWSTR::null(),
)
}
.with_context(|| format!("create the Device Parameters key for {value_name}"))?,
};
let name = wide(value_name);
// SAFETY: the value name is NUL-terminated and outlives the call; the DWORD bytes travel
// with the slice.
let rc = unsafe {
RegSetValueExW(
hkey,
PCWSTR(name.as_ptr()),
None,
REG_DWORD,
Some(&value.to_le_bytes()),
)
};
// SAFETY: closing the key opened/created above, exactly once.
unsafe {
let _ = RegCloseKey(hkey);
}
rc.ok().with_context(|| format!("write {value_name}"))
}
/// The persisted pad slot of a devnode (the `PunktfunkPadIndex` value under its
/// `Device Parameters` key), or `None` for foreign devnodes.
fn devnode_pad_index(set: &DevInfoSet, did: &SP_DEVINFO_DATA) -> Option<u32> {
read_devparam_dword(set, did, PAD_INDEX_VALUE)
}
/// Find the devnode previously created for `pad_index` (see the module doc: the persisted
/// index value is the durable marker; DeviceDesc only survives until the INF installs).
fn find_devnode(pad_index: u8) -> Result<Option<String>> {
@@ -688,51 +754,7 @@ fn create_devnode(pad_index: u8) -> Result<String> {
/// Persist `pad_index` in the devnode's `Device Parameters` key (created on a fresh devnode).
fn write_pad_index(set: &DevInfoSet, did: &mut SP_DEVINFO_DATA, pad_index: u8) -> Result<()> {
// SAFETY: live set + element; DIREG_DEV opens the devnode's Device Parameters key.
let opened = unsafe {
SetupDiOpenDevRegKey(
set.0,
did,
DICS_FLAG_GLOBAL.0,
0,
DIREG_DEV,
KEY_SET_VALUE.0,
)
};
let hkey = match opened {
Ok(k) => k,
// SAFETY: same set + element; a fresh devnode has no Device Parameters key yet, so
// create it (no INF association).
Err(_) => unsafe {
SetupDiCreateDevRegKeyW(
set.0,
did,
DICS_FLAG_GLOBAL.0,
0,
DIREG_DEV,
None,
PCWSTR::null(),
)
}
.context("create the devnode's Device Parameters key")?,
};
let name = wide(PAD_INDEX_VALUE);
// SAFETY: the value name is NUL-terminated and outlives the call; the DWORD bytes travel
// with the slice.
let rc = unsafe {
RegSetValueExW(
hkey,
PCWSTR(name.as_ptr()),
None,
REG_DWORD,
Some(&(pad_index as u32).to_le_bytes()),
)
};
// SAFETY: closing the key opened/created above, exactly once.
unsafe {
let _ = RegCloseKey(hkey);
}
rc.ok().context("write PunktfunkPadIndex")
write_devparam_dword(set, did, PAD_INDEX_VALUE, pad_index as u32)
}
/// The Steam Streaming Speakers INF to feed `UpdateDriverForPlugAndPlayDevices`: prefer the
+388 -35
View File
@@ -107,6 +107,49 @@ pub(crate) fn no_formats(_: &Endpoint) -> Option<MixFormat> {
None
}
/// The host's own MINTED endpoints — instances of Valve's streaming-audio driver the
/// [`minted`](super::minted) provider created at startup — by WASAPI endpoint id.
///
/// Tier-0 is an IDENTITY tier, not a name tier: a minted instance is indistinguishable by
/// friendly name from Steam's own primaries (S1 measured exactly that confusion — the probe's
/// name match grabbed a stamped instance instead of the primary), so the provider records what
/// it minted and the plan matches by id. All fields empty when nothing is minted (Steam
/// absent, provisioning disabled or still running) — every rule then falls back to the
/// name-based ladder unchanged.
#[derive(Debug, Default, Clone, PartialEq)]
pub(crate) struct MintedIds {
/// "Punktfunk Speakers" — an SSS-driver instance reserved as the client-only loopback
/// sink. Never contended by Steam's own Remote Play, deterministic across re-plans.
pub speakers_render: Option<String>,
/// "Punktfunk Microphone" render side — the virtual mic's write target.
pub mic_render: Option<String>,
/// "Punktfunk Microphone" capture side — the microphone host apps record.
pub mic_capture: Option<String>,
}
/// The one-line runtime answer "does desktop audio work, does the mic work" — the §C4
/// classification (logged with every plan change; the status API surfaces it later).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AudioReadiness {
/// Both roles have endpoints.
Full,
/// Desktop audio yes, mic passthrough no.
AudioOnly,
/// Mic yes, desktop audio no.
MicOnly,
/// Neither role has an endpoint.
Nothing,
}
pub(crate) fn readiness(w: &Wiring) -> AudioReadiness {
match (w.loopback_render.is_some(), w.mic_render.is_some()) {
(true, true) => AudioReadiness::Full,
(true, false) => AudioReadiness::AudioOnly,
(false, true) => AudioReadiness::MicOnly,
(false, false) => AudioReadiness::Nothing,
}
}
/// The coherent endpoint assignment for one wiring pass. Computed fresh on every mic/capture
/// (re)open — Windows endpoints churn (boot-time registration, hotplug, driver installs), so a
/// once-per-process plan goes stale.
@@ -224,6 +267,7 @@ pub(crate) fn plan(
mic_want: Option<&str>,
host_audio: bool,
pad_renders: &[String],
minted: &MintedIds,
) -> Wiring {
plan_with_formats(
renders,
@@ -233,6 +277,7 @@ pub(crate) fn plan(
&no_formats,
2,
pad_renders,
minted,
)
}
@@ -251,6 +296,7 @@ pub(crate) fn plan(
/// exists — narrow audio beats no audio — but flagged in [`Wiring::loopback_narrowing`] so the
/// capture side can say why. An unknown format (probe failed) counts as fine, so this can never
/// make the plan worse than it was before formats existed.
#[allow(clippy::too_many_arguments)] // mirrors the enumeration inputs; a param struct would only rename the problem
pub(crate) fn plan_with_formats(
renders: &[Endpoint],
captures: &[Endpoint],
@@ -259,6 +305,7 @@ pub(crate) fn plan_with_formats(
format_of: FormatProbe,
want_channels: u8,
pad_renders: &[String],
minted: &MintedIds,
) -> Wiring {
// 0. Pad-audio endpoints are invisible to the plan: never the mic target (client voice
// would play out of a pad "speaker"), never a loopback source (a game's controller
@@ -279,10 +326,24 @@ pub(crate) fn plan_with_formats(
.cloned()
};
// 1. Mic target first — it has the narrower requirements (must be a virtual cable).
// Tier-0 lookups: the minted ids resolved against THIS enumeration (an id the provider
// recorded but audiosrv no longer serves must not produce a phantom assignment).
let find_by_id = |id: &Option<String>| -> Option<Endpoint> {
id.as_deref()
.and_then(|id| renders.iter().find(|(_, rid)| rid == id).cloned())
};
let minted_mic = find_by_id(&minted.mic_render);
let minted_sink = find_by_id(&minted.speakers_render);
// 1. Mic target first — it has the narrower requirements (must be a virtual cable). The
// minted "Punktfunk Microphone" outranks every name-based candidate: it exists for
// exactly this role, and taking it can never cost the loopback anything (the minted
// sink is its counterpart). An operator override still beats it.
let mic_render = match mic_want {
Some(w) => find_render(w),
None => MIC_CANDIDATES.iter().find_map(|c| find_render(c)),
None => minted_mic
.clone()
.or_else(|| MIC_CANDIDATES.iter().find_map(|c| find_render(c))),
};
// Game audio outranks the mic: the Steam Streaming Microphone's render side is also the
// only silent client-only loopback sink, so the mic may hold it only while the loopback
@@ -315,8 +376,17 @@ pub(crate) fn plan_with_formats(
other => other,
};
// 2. Its capture side (what host apps record).
let mic_capture = mic_render.as_ref().and_then(|(name, _)| {
// 2. Its capture side (what host apps record). A minted mic resolves by the provider's
// recorded CAPTURE id — a name search cannot tell the minted microphone from Steam's
// primary (same friendly name), and pairing the minted render with the primary's
// capture would record a mic nothing writes into.
let mic_capture = mic_render.as_ref().and_then(|(name, id)| {
if Some(id) == minted.mic_render.as_ref() {
return minted
.mic_capture
.as_deref()
.and_then(|cid| captures.iter().find(|(_, c)| c == cid).cloned());
}
capture_for(&name.to_lowercase()).iter().find_map(|c| {
captures
.iter()
@@ -364,13 +434,38 @@ pub(crate) fn plan_with_formats(
.iter()
.find(|(n, id)| not_mic(id) && n.to_lowercase().contains("steam streaming speakers"))
};
// Tier-0 sink: the minted "Punktfunk Speakers". Same quality discipline as every silent
// sink — a narrowing minted instance demotes below real hardware rather than silently
// costing quality (S2 measured the driver clean at 48 kHz stereo, so this is a guard, not
// an expectation).
let minted_intact = || {
minted_sink
.as_ref()
.filter(|(_, id)| not_mic(id))
.filter(|ep| narrowing_of(ep).is_none())
};
let minted_narrow = || {
minted_sink
.as_ref()
.filter(|(_, id)| not_mic(id))
.filter(|ep| narrowing_of(ep).is_some())
};
// A narrowing silent sink sits below real hardware in BOTH modes: preferring silence on the
// host is a routing choice, but it must not silently cost audio quality when a clean endpoint
// is right there.
// is right there. The minted sink heads its tier in both modes — it is the one endpoint
// whose whole purpose is this role.
let preferred = if host_audio {
real_hw().or_else(silent_intact).or_else(silent_narrow)
real_hw()
.or_else(minted_intact)
.or_else(silent_intact)
.or_else(minted_narrow)
.or_else(silent_narrow)
} else {
silent_intact().or_else(real_hw).or_else(silent_narrow)
minted_intact()
.or_else(silent_intact)
.or_else(real_hw)
.or_else(minted_narrow)
.or_else(silent_narrow)
};
let (loopback_render, loopback_last_resort) = match preferred {
Some(ep) => (Some(ep.clone()), false),
@@ -503,7 +598,7 @@ mod tests {
ep("Microphone (Webcam)"),
ep("CABLE Output (VB-Audio Virtual Cable)"),
];
let w = plan(&renders, &captures, None, false, &[]);
let w = plan(&renders, &captures, None, false, &[], &MintedIds::default());
assert_eq!(
w.mic_render.unwrap().0,
"CABLE Input (VB-Audio Virtual Cable)"
@@ -532,7 +627,7 @@ mod tests {
ep("CABLE Output (VB-Audio Virtual Cable)"),
ep("Microphone (Steam Streaming Microphone)"),
];
let w = plan(&renders, &captures, None, false, &[]);
let w = plan(&renders, &captures, None, false, &[], &MintedIds::default());
assert_eq!(
w.mic_render.unwrap().0,
"CABLE Input (VB-Audio Virtual Cable)"
@@ -552,7 +647,7 @@ mod tests {
ep("CABLE Input (VB-Audio Virtual Cable)"),
ep("Speakers (Steam Streaming Microphone)"),
];
let w = plan(&renders, &[], None, true, &[]);
let w = plan(&renders, &[], None, true, &[], &MintedIds::default());
assert_eq!(
w.loopback_render.unwrap().0,
"Speakers (Apple Audio Device)"
@@ -569,7 +664,7 @@ mod tests {
ep("CABLE In 16ch (VB-Audio Virtual Cable)"),
];
for host_audio in [false, true] {
let w = plan(&renders, &[], None, host_audio, &[]);
let w = plan(&renders, &[], None, host_audio, &[], &MintedIds::default());
assert!(w.loopback_render.is_none(), "host_audio={host_audio}");
}
}
@@ -581,7 +676,7 @@ mod tests {
fn headless_cable_only_mic_wins() {
let renders = [ep("CABLE Input (VB-Audio Virtual Cable)")];
let captures = [ep("CABLE Output (VB-Audio Virtual Cable)")];
let w = plan(&renders, &captures, None, false, &[]);
let w = plan(&renders, &captures, None, false, &[], &MintedIds::default());
assert!(w.mic_render.is_some(), "mic must claim the only cable");
assert!(w.loopback_render.is_none(), "no echo-safe loopback exists");
}
@@ -599,7 +694,7 @@ mod tests {
ep("CABLE Output (VB-Audio Virtual Cable)"),
ep("Microphone (Steam Streaming Microphone)"),
];
let w = plan(&renders, &captures, None, false, &[]);
let w = plan(&renders, &captures, None, false, &[], &MintedIds::default());
assert_eq!(
w.mic_render.unwrap().0,
"CABLE Input (VB-Audio Virtual Cable)"
@@ -628,7 +723,7 @@ mod tests {
ep("Speakers (Realtek HD Audio)"),
];
let captures = [ep("Microphone (Steam Streaming Microphone)")];
let w = plan(&renders, &captures, None, false, &[]);
let w = plan(&renders, &captures, None, false, &[], &MintedIds::default());
assert_eq!(
w.mic_render.unwrap().0,
"Speakers (Steam Streaming Microphone)"
@@ -644,7 +739,7 @@ mod tests {
fn steam_mic_only_audio_wins() {
let renders = [ep("Speakers (Steam Streaming Microphone)")];
let captures = [ep("Microphone (Steam Streaming Microphone)")];
let w = plan(&renders, &captures, None, false, &[]);
let w = plan(&renders, &captures, None, false, &[], &MintedIds::default());
assert!(w.mic_render.is_none());
assert!(w.mic_withheld);
assert_eq!(
@@ -668,7 +763,7 @@ mod tests {
ep("Microphone (Steam Streaming Microphone)"),
ep("Voicemeeter Out B1 (VB-Audio Voicemeeter VAIO)"),
];
let w = plan(&renders, &captures, None, false, &[]);
let w = plan(&renders, &captures, None, false, &[], &MintedIds::default());
assert_eq!(
w.mic_render.as_ref().unwrap().0,
"Voicemeeter Input (VB-Audio Voicemeeter VAIO)"
@@ -696,7 +791,7 @@ mod tests {
ep("Speakers (Steam Streaming Speakers)"),
];
for host_audio in [false, true] {
let w = plan(&renders, &[], None, host_audio, &[]);
let w = plan(&renders, &[], None, host_audio, &[], &MintedIds::default());
assert_eq!(
w.loopback_render.as_ref().unwrap().0,
"Speakers (Steam Streaming Speakers)",
@@ -719,7 +814,14 @@ mod tests {
];
let captures = [ep("Microphone (Steam Streaming Microphone)")];
for host_audio in [false, true] {
let w = plan(&renders, &captures, None, host_audio, &[]);
let w = plan(
&renders,
&captures,
None,
host_audio,
&[],
&MintedIds::default(),
);
assert!(w.mic_render.is_none(), "host_audio={host_audio}");
assert!(w.mic_withheld, "host_audio={host_audio}");
assert_eq!(
@@ -747,6 +849,7 @@ mod tests {
Some("steam streaming microphone"),
false,
&[],
&MintedIds::default(),
);
assert_eq!(
w.mic_render.unwrap().0,
@@ -771,7 +874,14 @@ mod tests {
];
let captures = [ep("Microphone (Steam Streaming Microphone)")];
for host_audio in [false, true] {
let w = plan(&renders, &captures, None, host_audio, &[]);
let w = plan(
&renders,
&captures,
None,
host_audio,
&[],
&MintedIds::default(),
);
assert_eq!(
w.loopback_render.as_ref().unwrap().0,
"Speakers (Realtek HD Audio)",
@@ -793,7 +903,14 @@ mod tests {
];
let captures = [ep("CABLE Output (VB-Audio Virtual Cable)")];
for host_audio in [false, true] {
let w = plan(&renders, &captures, None, host_audio, &[]);
let w = plan(
&renders,
&captures,
None,
host_audio,
&[],
&MintedIds::default(),
);
assert!(w.loopback_render.is_none(), "host_audio={host_audio}");
assert!(!w.loopback_last_resort, "host_audio={host_audio}");
assert!(w.loopback_unsatisfiable(), "host_audio={host_audio}");
@@ -842,7 +959,16 @@ mod tests {
("steam streaming microphone", fmt(24_000, 1)),
("odyssey", fmt(48_000, 2)),
]);
let w = plan_with_formats(&renders, &captures, None, false, &p, 2, &[]);
let w = plan_with_formats(
&renders,
&captures,
None,
false,
&p,
2,
&[],
&MintedIds::default(),
);
assert_eq!(
w.loopback_render.as_ref().unwrap().0,
"1 - Odyssey G60SD (AMD High Definition Audio Device)",
@@ -872,7 +998,16 @@ mod tests {
("steam streaming microphone", fmt(48_000, 2)),
("realtek", fmt(48_000, 2)),
]);
let w = plan_with_formats(&renders, &[], None, false, &p, 2, &[]);
let w = plan_with_formats(
&renders,
&[],
None,
false,
&p,
2,
&[],
&MintedIds::default(),
);
assert_eq!(
w.loopback_render.unwrap().0,
"Speakers (Steam Streaming Microphone)"
@@ -888,7 +1023,16 @@ mod tests {
ep("Speakers (Steam Streaming Microphone)"),
];
let p = probe(vec![("steam streaming microphone", fmt(16_000, 1))]);
let w = plan_with_formats(&renders, &[], None, false, &p, 2, &[]);
let w = plan_with_formats(
&renders,
&[],
None,
false,
&p,
2,
&[],
&MintedIds::default(),
);
assert_eq!(
w.loopback_render.as_ref().unwrap().0,
"Speakers (Steam Streaming Microphone)"
@@ -904,7 +1048,16 @@ mod tests {
fn narrowing_is_reported_for_real_hardware_too() {
let renders = [ep("Headset (Hands-Free AG Audio)")];
let p = probe(vec![("headset", fmt(16_000, 1))]);
let w = plan_with_formats(&renders, &[], None, false, &p, 2, &[]);
let w = plan_with_formats(
&renders,
&[],
None,
false,
&p,
2,
&[],
&MintedIds::default(),
);
assert_eq!(
w.loopback_render.as_ref().unwrap().0,
"Headset (Hands-Free AG Audio)"
@@ -924,8 +1077,24 @@ mod tests {
];
let captures = [ep("CABLE Output (VB-Audio Virtual Cable)")];
for host_audio in [false, true] {
let a = plan(&renders, &captures, None, host_audio, &[]);
let b = plan_with_formats(&renders, &captures, None, host_audio, &no_formats, 2, &[]);
let a = plan(
&renders,
&captures,
None,
host_audio,
&[],
&MintedIds::default(),
);
let b = plan_with_formats(
&renders,
&captures,
None,
host_audio,
&no_formats,
2,
&[],
&MintedIds::default(),
);
assert_eq!(a, b, "host_audio={host_audio}");
assert!(a.loopback_narrowing.is_none());
}
@@ -943,7 +1112,7 @@ mod tests {
("steam streaming microphone", fmt(24_000, 1)),
("realtek", fmt(48_000, 2)),
]);
let w = plan_with_formats(&renders, &[], None, true, &p, 2, &[]);
let w = plan_with_formats(&renders, &[], None, true, &p, 2, &[], &MintedIds::default());
assert_eq!(w.loopback_render.unwrap().0, "Speakers (Realtek HD Audio)");
}
@@ -971,7 +1140,14 @@ mod tests {
ep("Voicemeeter Input (VB-Audio Voicemeeter VAIO)"),
];
let captures = [ep("Voicemeeter Out B1 (VB-Audio Voicemeeter VAIO)")];
let w = plan(&renders, &captures, Some("voicemeeter input"), false, &[]);
let w = plan(
&renders,
&captures,
Some("voicemeeter input"),
false,
&[],
&MintedIds::default(),
);
assert_eq!(
w.mic_render.unwrap().0,
"Voicemeeter Input (VB-Audio Voicemeeter VAIO)"
@@ -987,7 +1163,7 @@ mod tests {
#[test]
fn no_virtual_device() {
let renders = [ep("Speakers (Realtek HD Audio)")];
let w = plan(&renders, &[], None, false, &[]);
let w = plan(&renders, &[], None, false, &[], &MintedIds::default());
assert!(w.mic_render.is_none());
assert_eq!(w.loopback_render.unwrap().0, "Speakers (Realtek HD Audio)");
}
@@ -1005,7 +1181,14 @@ mod tests {
];
let captures = [ep("Voicemeeter Out B1 (VB-Audio Voicemeeter VAIO)")];
for host_audio in [false, true] {
let w = plan(&renders, &captures, None, host_audio, &[]);
let w = plan(
&renders,
&captures,
None,
host_audio,
&[],
&MintedIds::default(),
);
assert_eq!(
w.mic_render.as_ref().unwrap().0,
"Voicemeeter Input (VB-Audio Voicemeeter VAIO)",
@@ -1028,7 +1211,7 @@ mod tests {
ep("Voicemeeter Aux Input (VB-Audio Voicemeeter AUX VAIO)"),
];
for host_audio in [false, true] {
let w = plan(&renders, &[], None, host_audio, &[]);
let w = plan(&renders, &[], None, host_audio, &[], &MintedIds::default());
assert!(w.mic_render.is_some(), "host_audio={host_audio}");
assert!(w.loopback_render.is_none(), "host_audio={host_audio}");
}
@@ -1043,7 +1226,7 @@ mod tests {
ep("CABLE Input (VB-Audio Virtual Cable)"),
ep("Speakers (Some Virtual Audio Device)"),
];
let w = plan(&renders, &[], None, false, &[]);
let w = plan(&renders, &[], None, false, &[], &MintedIds::default());
assert!(w.loopback_render.is_none());
}
@@ -1077,6 +1260,7 @@ mod tests {
Some("steam streaming microphone"),
false,
&[],
&MintedIds::default(),
);
assert!(w.loopback_unsatisfiable());
let msg = describe_no_loopback(&renders, &w);
@@ -1088,7 +1272,7 @@ mod tests {
// anyway), while the Steam pair is the remedy that adds a capturable sink.
let renders = [ep("CABLE Input (VB-Audio Virtual Cable)")];
let captures = [ep("CABLE Output (VB-Audio Virtual Cable)")];
let w = plan(&renders, &captures, None, false, &[]);
let w = plan(&renders, &captures, None, false, &[], &MintedIds::default());
assert!(w.loopback_unsatisfiable());
let msg = describe_no_loopback(&renders, &w);
assert!(msg.contains("install Steam"), "{msg}");
@@ -1106,7 +1290,7 @@ mod tests {
ep("Speakers (Realtek HD Audio)"),
];
let pads = [renders[0].1.clone()];
let w = plan(&renders, &[], None, false, &pads);
let w = plan(&renders, &[], None, false, &pads, &MintedIds::default());
assert_eq!(w.loopback_render.unwrap().0, "Speakers (Realtek HD Audio)");
// Even an operator mic override matching the pad's name must not claim it; with the
// pad as the only render endpoint there is honestly no mic target and no loopback.
@@ -1116,6 +1300,7 @@ mod tests {
Some("wireless controller"),
false,
&pads,
&MintedIds::default(),
);
assert!(w.mic_render.is_none());
assert!(w.loopback_render.is_none());
@@ -1143,6 +1328,7 @@ mod tests {
Some("steam streaming microphone"),
false,
&pads,
&MintedIds::default(),
);
assert_eq!(
w.loopback_render.as_ref().unwrap().0,
@@ -1153,7 +1339,14 @@ mod tests {
// …and with the pad as the ONLY candidate left, the plan stays honestly unsatisfiable
// rather than falling back onto the coils.
let w = plan(&renders[..1], &captures, None, false, &pads);
let w = plan(
&renders[..1],
&captures,
None,
false,
&pads,
&MintedIds::default(),
);
assert!(
w.loopback_render.is_none(),
"a pad was taken as the last resort"
@@ -1161,4 +1354,164 @@ mod tests {
assert!(!w.loopback_last_resort);
assert!(w.loopback_unsatisfiable());
}
// ---- minted tier-0 (the audio-substrate program) -------------------------------------
/// The minted zoo: both punktfunk instances present alongside the primaries, real
/// hardware, AND a cable — deliberately name-identical to the primaries, because that is
/// what the driver produces (S1 measured the confusion).
fn minted_zoo() -> ([Endpoint; 6], [Endpoint; 3], MintedIds) {
let renders = [
ep("Speakers (Realtek HD Audio)"),
ep("CABLE Input (VB-Audio Virtual Cable)"),
ep("Lautsprecher (Steam Streaming Speakers)"),
ep("Lautsprecher (Steam Streaming Microphone)"),
(
"Lautsprecher (Steam Streaming Speakers)".into(),
"id-minted-spk".into(),
),
(
"Lautsprecher (Steam Streaming Microphone)".into(),
"id-minted-mic-r".into(),
),
];
let captures = [
ep("CABLE Output (VB-Audio Virtual Cable)"),
ep("Mikrofon (Steam Streaming Microphone)"),
(
"Mikrofon (Steam Streaming Microphone)".into(),
"id-minted-mic-c".into(),
),
];
let minted = MintedIds {
speakers_render: Some("id-minted-spk".into()),
mic_render: Some("id-minted-mic-r".into()),
mic_capture: Some("id-minted-mic-c".into()),
};
(renders, captures, minted)
}
/// The end-state: with the minted pair present, the mic takes its own device and the
/// loopback takes the minted sink — by ID, ignoring the name-identical primaries, the
/// cable, and real hardware. Both features coexist without VB-Cable, client-only silent.
#[test]
fn minted_pair_is_tier_zero() {
let (renders, captures, minted) = minted_zoo();
let w = plan(&renders, &captures, None, false, &[], &minted);
assert_eq!(w.mic_render.as_ref().unwrap().1, "id-minted-mic-r");
assert_eq!(
w.mic_capture.as_ref().unwrap().1,
"id-minted-mic-c",
"the capture side must pair by the provider's id, never by name"
);
assert_eq!(w.loopback_render.as_ref().unwrap().1, "id-minted-spk");
assert!(!w.loopback_last_resort);
assert!(!w.mic_withheld);
assert_eq!(readiness(&w), AudioReadiness::Full);
}
/// `host_audio` still prefers real hardware for the loopback; the mic keeps its minted
/// device either way.
#[test]
fn minted_host_audio_prefers_hardware() {
let (renders, captures, minted) = minted_zoo();
let w = plan(&renders, &captures, None, true, &[], &minted);
assert_eq!(w.mic_render.as_ref().unwrap().1, "id-minted-mic-r");
assert_eq!(w.loopback_render.unwrap().0, "Speakers (Realtek HD Audio)");
}
/// The operator override still beats the minted mic — an explicit choice wins everything.
#[test]
fn env_override_beats_minted() {
let (renders, captures, minted) = minted_zoo();
let w = plan(
&renders,
&captures,
Some("cable input"),
false,
&[],
&minted,
);
assert_eq!(
w.mic_render.unwrap().0,
"CABLE Input (VB-Audio Virtual Cable)"
);
// The minted sink still serves the loopback.
assert_eq!(w.loopback_render.unwrap().1, "id-minted-spk");
}
/// Partial mint (speakers only — the SSM leg failed): the mic falls back to the name
/// ladder, the loopback keeps the minted sink. Nothing regresses below today's behavior.
#[test]
fn minted_speakers_only_mic_uses_ladder() {
let (renders, captures, mut minted) = minted_zoo();
minted.mic_render = None;
minted.mic_capture = None;
let w = plan(&renders, &captures, None, false, &[], &minted);
assert_eq!(
w.mic_render.unwrap().0,
"CABLE Input (VB-Audio Virtual Cable)"
);
assert_eq!(w.loopback_render.unwrap().1, "id-minted-spk");
}
/// A minted id the enumeration no longer serves must not produce a phantom assignment —
/// the plan falls back to the ladder exactly as if nothing were minted.
#[test]
fn stale_minted_ids_fall_back() {
let renders = [
ep("Speakers (Realtek HD Audio)"),
ep("CABLE Input (VB-Audio Virtual Cable)"),
];
let captures = [ep("CABLE Output (VB-Audio Virtual Cable)")];
let minted = MintedIds {
speakers_render: Some("id-gone".into()),
mic_render: Some("id-gone-too".into()),
mic_capture: Some("id-gone-three".into()),
};
let a = plan(&renders, &captures, None, false, &[], &minted);
let b = plan(&renders, &captures, None, false, &[], &MintedIds::default());
assert_eq!(a, b);
}
/// A minted sink that NARROWS the mix demotes below real hardware like any silent sink —
/// tier-0 is an identity privilege, not a quality exemption.
#[test]
fn minted_sink_narrowing_demotes() {
let renders = [
ep("Speakers (Realtek HD Audio)"),
(
"Lautsprecher (Steam Streaming Speakers)".into(),
"id-minted-spk".into(),
),
];
let minted = MintedIds {
speakers_render: Some("id-minted-spk".into()),
..Default::default()
};
let p = probe(vec![("steam streaming", fmt(16_000, 1))]);
let w = plan_with_formats(&renders, &[], None, false, &p, 2, &[], &minted);
assert_eq!(w.loopback_render.unwrap().0, "Speakers (Realtek HD Audio)");
}
/// The readiness classification the log line (and later the status API) carries.
#[test]
fn readiness_table() {
let (renders, captures, minted) = minted_zoo();
let full = plan(&renders, &captures, None, false, &[], &minted);
assert_eq!(readiness(&full), AudioReadiness::Full);
// Steam-pair-only, no cable: audio yes (withheld mic), mic no.
let renders = [ep("Altavoces (Steam Streaming Microphone)")];
let captures = [ep("Microphone (Steam Streaming Microphone)")];
let w = plan(&renders, &captures, None, false, &[], &MintedIds::default());
assert_eq!(readiness(&w), AudioReadiness::AudioOnly);
// Cable-only headless: mic yes, audio no.
let renders = [ep("CABLE Input (VB-Audio Virtual Cable)")];
let captures = [ep("CABLE Output (VB-Audio Virtual Cable)")];
let w = plan(&renders, &captures, None, false, &[], &MintedIds::default());
assert_eq!(readiness(&w), AudioReadiness::MicOnly);
// Nothing at all.
let w = plan(&[], &[], None, false, &[], &MintedIds::default());
assert_eq!(readiness(&w), AudioReadiness::Nothing);
}
}
+7 -5
View File
@@ -495,11 +495,13 @@ pub fn dualsense_windows_test(args: &[String]) -> Result<()> {
/// state without changing anything; `remove` deletes the devnode via pnputil — the escape
/// hatch only, endpoints are persistent by design. Stamping needs SYSTEM (the MMDevices ACL);
/// run `ensure` under the service account or PsExec when the property-store route is denied.
/// Windows: the audio-substrate spike measurements (S1S3 in
/// `windows-audio-endpoints-and-vbcable.md`) — `audio-probe ssm|sink|sss-primary|cleanup
/// [--keep]`. `ssm` is the decision gate: mint a second Steam Streaming Microphone devnode and
/// prove render→capture end to end; `sink` parks the default on a minted Speakers instance and
/// loopback-measures it; `sss-primary` re-measures the primary Speakers' known-silent loopback.
/// Windows: the audio-substrate toolbox (`windows-audio-endpoints-and-vbcable.md`) —
/// `audio-probe ssm|sink|sss-primary|mint|plan|cleanup [--keep]`. The S1S3 spikes (`ssm` =
/// the decision gate: mint a second Steam Streaming Microphone devnode and prove
/// render→capture end to end; `sink` parks the default on a minted Speakers instance and
/// loopback-measures it; `sss-primary` re-measures the primary Speakers' loopback), plus the
/// product paths: `mint` runs the minted-endpoint provider synchronously and `plan` prints
/// one real wiring pass with its readiness verdict.
#[cfg(target_os = "windows")]
pub fn audio_probe(args: &[String]) -> Result<()> {
crate::audio::audio_probe::run(args)
+5
View File
@@ -359,6 +359,11 @@ pub(crate) async fn serve(
// Failures log once and leave the feature off: pads still work, just without pad audio.
#[cfg(target_os = "windows")]
crate::audio::pad_endpoint::provision_at_startup();
// Windows: mint the punktfunk-owned audio endpoints ("Punktfunk Speakers/Microphone" —
// instances of Valve's streaming drivers, the wiring plan's tier-0). Best-effort on a
// worker thread; without Steam's drivers the wiring plan keeps its name-based ladder.
#[cfg(target_os = "windows")]
crate::audio::minted::provision_at_startup();
// Host-lifetime worker that fires debounced TV-session restores (the managed gamescope path
// restores the box's autologin gaming session on idle, not per-disconnect — see
// `vdisplay::restore_managed_session`). Held for serve()'s lifetime; dropping it stops it.