From cc53b3d6b0429463760576e11eca44e7395d0e62 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 7 Aug 2026 16:06:56 +0200 Subject: [PATCH] =?UTF-8?q?fix(host/audio):=20unwire=20the=20minted=20micr?= =?UTF-8?q?ophone=20=E2=80=94=20the=20driver=20mic=20path=20is=20unusable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final pitch-probe verdict on the SSM driver pair: the render pin is stereo-only, the capture pin mono-only (stamping either differently makes the endpoint unopenable), and the crossing between them is a RAW byte pass — so voice fed through the render endpoint reads back an octave low and no format stamp can fix it. S3 peak-based PASS = false pass; per the design doc revert clause the mic falls back to the name ladder (a virtual cable), pending the user re-decision. The SPEAKERS substrate keeps tier-0 (no driver crossing — a plain engine loopback tap, measured clean). minted_ids() publishes speakers only; the mic endpoints stay minted and recorded (provisioned()) for the micpitch probe and a possible future non-render transport, and their format stamps now pin each side to its pin one true format — healing the endpoints this branch earlier mis-stamped. --- .../src/audio/windows/audio_probe.rs | 9 ++- .../src/audio/windows/minted.rs | 55 +++++++++++++++---- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/crates/punktfunk-host/src/audio/windows/audio_probe.rs b/crates/punktfunk-host/src/audio/windows/audio_probe.rs index 680e282a..38de8640 100644 --- a/crates/punktfunk-host/src/audio/windows/audio_probe.rs +++ b/crates/punktfunk-host/src/audio/windows/audio_probe.rs @@ -105,8 +105,13 @@ pub(crate) fn run(args: &[String]) -> Result<()> { // ~220 Hz = a link runs at half the declared rate (the octave-down voice). Some("micpitch") => { super::minted::ensure_blocking(); - let ids = super::minted::minted_ids(); - let (Some(render), Some(capture)) = (ids.mic_render, ids.mic_capture) else { + // The RAW provisioning record: the wiring-facing `minted_ids` deliberately hides + // the mic pair (raw crossing, octave-low — this probe is how that was measured). + let Some(m) = super::minted::provisioned() else { + bail!("nothing minted on this box — run `audio-probe mint` first"); + }; + let (Some(render), Some(capture)) = (m.mic_render.clone(), m.mic_capture.clone()) + else { bail!("no minted microphone pair on this box — run `audio-probe mint` first"); }; println!("audio-probe micpitch: render={render}"); diff --git a/crates/punktfunk-host/src/audio/windows/minted.rs b/crates/punktfunk-host/src/audio/windows/minted.rs index 68d7fddb..1fa2a82f 100644 --- a/crates/punktfunk-host/src/audio/windows/minted.rs +++ b/crates/punktfunk-host/src/audio/windows/minted.rs @@ -108,17 +108,32 @@ static PROVISIONING: AtomicBool = AtomicBool::new(false); static LAST_ATTEMPT: Mutex> = Mutex::new(None); /// The wiring plan's tier-0 input: the minted ids, or all-empty while nothing is provisioned. +/// +/// **The mic ids are deliberately NOT published** (2026-08-07 pitch measurements): the SSM +/// driver's pins are format-locked (render stereo-only, capture mono-only) and the crossing +/// is a RAW byte pass, so voice fed through the render endpoint reads back an octave low and +/// no stamp can fix it — S3's peak-based PASS was a false pass. The mic therefore falls back +/// to the name ladder (a virtual cable) per the design's revert clause, while the SPEAKERS +/// substrate — which involves no driver crossing, just an engine loopback tap — stays tier-0. +/// The mic endpoints are still minted and recorded ([`provisioned`]) for the `micpitch` +/// probe and for a future transport that bypasses the render path. 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(), + mic_render: None, + mic_capture: None, }, None => wiring_plan::MintedIds::default(), } } +/// The raw provisioning record — the probe's view (unlike [`minted_ids`], the mic ids are +/// visible here). +pub(crate) fn provisioned() -> Option> { + PROVISIONED.get().cloned() +} + /// 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() { @@ -303,6 +318,20 @@ const WFX_F32_2CH_48K: [u8; 40] = [ 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71, // KSDATAFORMAT_SUBTYPE_IEEE_FLOAT ]; +/// The capture pin's one format: 1 ch / 48 kHz / 32-bit float, mask 0x4 (FRONT_CENTER). +const WFX_F32_1CH_48K: [u8; 40] = [ + 0xfe, 0xff, // wFormatTag = WAVE_FORMAT_EXTENSIBLE + 0x01, 0x00, // nChannels = 1 + 0x80, 0xbb, 0x00, 0x00, // nSamplesPerSec = 48000 + 0x00, 0xee, 0x02, 0x00, // nAvgBytesPerSec = 192000 + 0x04, 0x00, // nBlockAlign = 4 + 0x20, 0x00, // wBitsPerSample = 32 + 0x16, 0x00, // cbSize = 22 + 0x20, 0x00, // wValidBitsPerSample = 32 + 0x04, 0x00, 0x00, 0x00, // dwChannelMask = SPEAKER_FRONT_CENTER + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, + 0x71, // KSDATAFORMAT_SUBTYPE_IEEE_FLOAT +]; /// Best-effort: write the role's display name — plus, on the mic RENDER, the mono format set — /// onto one endpoint and wait for the audio stack to SERVE it. Never fails the role — an @@ -320,31 +349,37 @@ fn stamp_identity(endpoint_id: &str, role: Role, capture: bool) { value: pe::StampValue::Str("Punktfunk"), }, ]; - // Both sides of the mic pair declare the SAME stereo float format (see WFX_F32_2CH_48K: - // the crossing is raw and the render pin is stereo-only, so stereo-everywhere is the one - // coherent choice). `capture` is accepted for symmetry — both directions get the set. - let _ = capture; + // Format stamps pin each mic-pair side to ITS pin's one supported format — measured: the + // render pin is stereo-only and the capture pin mono-only (stamping either differently + // makes the endpoint unopenable, 0x88890008 on every Initialize). These stamps exist to + // HEAL endpoints an earlier build of this branch mis-stamped, and to keep the pair pinned + // against drift; they cannot fix the raw crossing (see [`minted_ids`]). if role == Role::Mic { + let wfx: &'static [u8; 40] = if capture { + &WFX_F32_1CH_48K + } else { + &WFX_F32_2CH_48K + }; stamps.extend([ pe::Stamp { label: "device-format", key: pe::PKEY_DEVICE_FORMAT, - value: pe::StampValue::Format(&WFX_F32_2CH_48K), + value: pe::StampValue::Format(wfx), }, pe::Stamp { label: "mix-format-2", key: pe::PKEY_MIX_FORMAT_2, - value: pe::StampValue::Format(&WFX_F32_2CH_48K), + value: pe::StampValue::Format(wfx), }, pe::Stamp { label: "mix-format-3", key: pe::PKEY_MIX_FORMAT_3, - value: pe::StampValue::Format(&WFX_F32_2CH_48K), + value: pe::StampValue::Format(wfx), }, pe::Stamp { label: "host-format", key: pe::PKEY_HOST_FORMAT, - value: pe::StampValue::Format(&WFX_F32_2CH_48K), + value: pe::StampValue::Format(wfx), }, ]); }