diff --git a/crates/punktfunk-host/src/audio/windows/minted.rs b/crates/punktfunk-host/src/audio/windows/minted.rs index a34e6c9a..b3145820 100644 --- a/crates/punktfunk-host/src/audio/windows/minted.rs +++ b/crates/punktfunk-host/src/audio/windows/minted.rs @@ -230,6 +230,17 @@ fn ensure_role(role: Role) -> Result<(String, String, Option)> { Role::Speakers => None, }; + // Stamp the human name onto every endpoint of the role. Field-measured necessity, not + // cosmetics: unstamped, the minted instances read "Lautsprecher (2- Steam Streaming + // Microphone)" etc. and even the box's owner picked the wrong device out of the Sound + // settings zoo. Names only (a wider stamp set makes AudioEndpointBuilder re-mint the + // endpoint under a new GUID — the pad program measured that); stamping needs the SYSTEM + // ACL route on the MMDevices keys, so a dev-run devtest may leave the names unstamped — + // the wiring never depends on them (identity is the recorded id). + for ep in [Some(&render), capture.as_ref()].into_iter().flatten() { + stamp_name(ep, role); + } + // Freshly registered endpoints can grab a default; the wiring plan owns default policy, // not the mint. if let Some(prev) = prev_render { @@ -255,6 +266,59 @@ fn ensure_role(role: Role) -> Result<(String, String, Option)> { Ok((devnode, render, capture)) } +/// How many stamp/settle passes a name gets before we accept "stored but not yet served" +/// (a settled endpoint takes the stamp on the first pass; a freshly minted one may need the +/// audio stack to notice — it serves after the next Audiosrv restart/reboot at the latest). +const STAMP_ATTEMPTS: usize = 3; +/// Settle time between a stamp write and its served-check (mirrors the pad provisioner: +/// checking immediately reports success on passes that later get reverted). +const STAMP_SETTLE: Duration = Duration::from_millis(1200); + +/// Best-effort: write the role's display name onto one endpoint and wait for the audio stack +/// to SERVE it. Never fails the role — an unnamed endpoint still wires correctly by id. +fn stamp_name(endpoint_id: &str, role: Role) { + let stamps = [ + pe::Stamp { + label: "device-desc", + key: pe::PKEY_DEVICE_DESC, + value: pe::StampValue::Str(role.desc()), + }, + pe::Stamp { + label: "device-name", + key: pe::PKEY_ENDPOINT_DEVICE_NAME, + value: pe::StampValue::Str("Punktfunk"), + }, + ]; + // Steady state (every boot after the first): the names are already served — no writes, + // no settle sleeps. + if pe::stamps_served(endpoint_id, &stamps) { + return; + } + for attempt in 0..STAMP_ATTEMPTS { + if let Err(e) = pe::write_stamps(endpoint_id, &stamps) { + tracing::info!(role = role.label(), endpoint = %endpoint_id, + error = %format!("{e:#}"), + "could not stamp the minted endpoint's name (needs the SYSTEM ACL route) — \ + the endpoint still wires correctly, it just keeps the driver's default name"); + return; + } + thread::sleep(STAMP_SETTLE); + if pe::stamps_served(endpoint_id, &stamps) { + if attempt > 0 { + tracing::debug!( + role = role.label(), + attempt = attempt + 1, + "minted endpoint name held after a re-pass" + ); + } + return; + } + } + tracing::info!(role = role.label(), endpoint = %endpoint_id, + "minted endpoint name is stored but not yet served — it appears after the next \ + audio-stack restart or reboot"); +} + /// Poll audiosrv for the endpoint a minted devnode registers in one direction. fn wait_for(devnode: &str, capture: bool) -> Result { let deadline = Instant::now() + ENDPOINT_WAIT; diff --git a/crates/punktfunk-host/src/audio/windows/pad_endpoint.rs b/crates/punktfunk-host/src/audio/windows/pad_endpoint.rs index c061d56c..f285c17b 100644 --- a/crates/punktfunk-host/src/audio/windows/pad_endpoint.rs +++ b/crates/punktfunk-host/src/audio/windows/pad_endpoint.rs @@ -132,13 +132,15 @@ pub struct PadEndpoint { // --- the stamp set ------------------------------------------------------------------------- /// One endpoint property to stamp: the property-store key, the value, a short log label. -struct Stamp { - label: &'static str, - key: PROPERTYKEY, - value: StampValue, +/// pub(crate): the minted-audio provider stamps its endpoint names through the same machinery +/// (store-first, registry fallback, served-check) — see [`write_stamps`]. +pub(crate) struct Stamp { + pub(crate) label: &'static str, + pub(crate) key: PROPERTYKEY, + pub(crate) value: StampValue, } -enum StampValue { +pub(crate) enum StampValue { Str(&'static str), /// The PFDS container (VT_CLSID / serialized-CLSID registry blob). Container(GUID), @@ -154,9 +156,10 @@ const fn pkey(fmtid: u128, pid: u32) -> PROPERTYKEY { } /// `PKEY_Device_DeviceDesc` — the "description" half of the endpoint display name. -const PKEY_DEVICE_DESC: PROPERTYKEY = pkey(0xa45c254e_df1c_4efd_8020_67d146a850e0, 2); +pub(crate) const PKEY_DEVICE_DESC: PROPERTYKEY = pkey(0xa45c254e_df1c_4efd_8020_67d146a850e0, 2); /// Endpoint-store "device name" half of the display name. -const PKEY_ENDPOINT_DEVICE_NAME: PROPERTYKEY = pkey(0xb3f8fa53_0004_438e_9003_51a46e139bfc, 6); +pub(crate) const PKEY_ENDPOINT_DEVICE_NAME: PROPERTYKEY = + pkey(0xb3f8fa53_0004_438e_9003_51a46e139bfc, 6); /// Endpoint-store devnode link: `"{1}."` — how an endpoint is tied back to /// the devnode that owns it. const PKEY_ENDPOINT_DEVNODE: PROPERTYKEY = pkey(0xb3f8fa53_0004_438e_9003_51a46e139bfc, 2); @@ -991,7 +994,14 @@ fn set_store_value(store: &IPropertyStore, s: &Stamp) -> Result<()> { /// restart), raw registry for whatever it rejects. Idempotent — already-served keys are /// skipped entirely. fn stamp_endpoint(endpoint_id: &str, pad_index: u8) -> Result<()> { - let stamps = active_stamps(pad_index); + write_stamps(endpoint_id, &active_stamps(pad_index)) +} + +/// The generic stamp writer behind [`stamp_endpoint`], shared with the minted-audio provider +/// (which stamps "Punktfunk Speakers/Microphone" names — field-measured necessity: without +/// them even the box's owner could not tell the minted instances from Steam's primaries in +/// the Sound settings zoo). +pub(crate) fn write_stamps(endpoint_id: &str, stamps: &[Stamp]) -> Result<()> { let dev = open_mmdevice(endpoint_id)?; let pending: Vec<&Stamp> = { // SAFETY: read-only property store on a COM-initialized thread. @@ -1000,7 +1010,7 @@ fn stamp_endpoint(endpoint_id: &str, pad_index: u8) -> Result<()> { stamps.iter().filter(|s| !stamp_served(&store, s)).collect() }; if pending.is_empty() { - tracing::debug!(endpoint = %endpoint_id, pad = pad_index, "pad endpoint already fully stamped"); + tracing::debug!(endpoint = %endpoint_id, "endpoint already fully stamped"); return Ok(()); } let mut via_store: Vec<&'static str> = Vec::new(); @@ -1041,10 +1051,9 @@ fn stamp_endpoint(endpoint_id: &str, pad_index: u8) -> Result<()> { } tracing::info!( endpoint = %endpoint_id, - pad = pad_index, property_store = ?via_store, registry = ?via_registry.iter().map(|s| s.label).collect::>(), - "pad endpoint stamped (route per key)" + "endpoint stamped (route per key)" ); Ok(()) } @@ -1197,6 +1206,11 @@ fn registry_stamp(endpoint_id: &str, stamps: &[&Stamp]) -> Result<()> { /// i.e. the audio stack SERVES the identity rather than merely storing it. Any error counts as /// "not served" (the only consumer is the needs-AEB-kick decision). fn all_served(endpoint_id: &str, pad_index: u8) -> bool { + stamps_served(endpoint_id, &active_stamps(pad_index)) +} + +/// [`all_served`]'s generic body — shared with the minted-audio provider. +pub(crate) fn stamps_served(endpoint_id: &str, stamps: &[Stamp]) -> bool { let Ok(dev) = open_mmdevice(endpoint_id) else { return false; }; @@ -1204,9 +1218,7 @@ fn all_served(endpoint_id: &str, pad_index: u8) -> bool { let Ok(store) = (unsafe { dev.OpenPropertyStore(STGM_READ) }) else { return false; }; - active_stamps(pad_index) - .iter() - .all(|s| stamp_served(&store, s)) + stamps.iter().all(|s| stamp_served(&store, s)) } // --- public provisioning API ----------------------------------------------------------------