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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (S1–S3 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 S1–S3 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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user