fix(host/audio): the minted microphone renders MONO — voice was an octave low
Measured with the new pitch probe: 440 Hz into the minted mic render came back as 220 Hz off its capture side. The driver forwards the render stream RAW into its mono capture, so a stereo-declared render (the driver-default we inherited) turns every stereo frame into two mono samples — half speed, octave down, exactly the field report. The mic render now gets a coherent MONO 48 kHz format set stamped alongside its name (PCM16 device format + float mix/host formats), making the engine downmix before the driver crossing. The mic pump keeps pushing stereo; shared-mode autoconvert handles the rest.
This commit is contained in:
@@ -233,12 +233,17 @@ fn ensure_role(role: Role) -> Result<(String, String, Option<String>)> {
|
||||
// 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);
|
||||
// settings zoo. The MIC RENDER additionally gets a MONO format set — measured (440 Hz in,
|
||||
// 220 Hz out): the driver forwards the render stream RAW into its mono capture side, so a
|
||||
// stereo-declared render plays back an octave low; declaring mono makes the engine
|
||||
// downmix before the crossing. Minimal stamps only (a wider 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 them unstamped — the wiring never depends on them (identity is the recorded
|
||||
// id).
|
||||
stamp_identity(&render, role, false);
|
||||
if let Some(cap) = capture.as_ref() {
|
||||
stamp_identity(cap, role, true);
|
||||
}
|
||||
|
||||
// Freshly registered endpoints can grab a default; the wiring plan owns default policy,
|
||||
@@ -274,10 +279,43 @@ const STAMP_ATTEMPTS: usize = 3;
|
||||
/// 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 = [
|
||||
/// `WAVEFORMATEXTENSIBLE`: 1 ch / 48 kHz / 16-bit PCM, mask 0x4 (FRONT_CENTER), PCM subtype —
|
||||
/// the mic render's device format. The driver's capture side is mono 48 kHz by its own
|
||||
/// default; the render must MATCH it (measured: a stereo render crosses the driver raw and
|
||||
/// plays back an octave low).
|
||||
const WFX_PCM16_1CH_48K: [u8; 40] = [
|
||||
0xfe, 0xff, // wFormatTag = WAVE_FORMAT_EXTENSIBLE
|
||||
0x01, 0x00, // nChannels = 1
|
||||
0x80, 0xbb, 0x00, 0x00, // nSamplesPerSec = 48000
|
||||
0x00, 0x77, 0x01, 0x00, // nAvgBytesPerSec = 96000
|
||||
0x02, 0x00, // nBlockAlign = 2
|
||||
0x10, 0x00, // wBitsPerSample = 16
|
||||
0x16, 0x00, // cbSize = 22
|
||||
0x10, 0x00, // wValidBitsPerSample = 16
|
||||
0x04, 0x00, 0x00, 0x00, // dwChannelMask = SPEAKER_FRONT_CENTER
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b,
|
||||
0x71, // KSDATAFORMAT_SUBTYPE_PCM
|
||||
];
|
||||
/// The float leg of ↑ (mix/host formats).
|
||||
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
|
||||
/// unstamped endpoint still wires correctly by id.
|
||||
fn stamp_identity(endpoint_id: &str, role: Role, capture: bool) {
|
||||
let mut stamps = vec![
|
||||
pe::Stamp {
|
||||
label: "device-desc",
|
||||
key: pe::PKEY_DEVICE_DESC,
|
||||
@@ -289,6 +327,30 @@ fn stamp_name(endpoint_id: &str, role: Role) {
|
||||
value: pe::StampValue::Str("Punktfunk"),
|
||||
},
|
||||
];
|
||||
if role == Role::Mic && !capture {
|
||||
stamps.extend([
|
||||
pe::Stamp {
|
||||
label: "device-format",
|
||||
key: pe::PKEY_DEVICE_FORMAT,
|
||||
value: pe::StampValue::Format(&WFX_PCM16_1CH_48K),
|
||||
},
|
||||
pe::Stamp {
|
||||
label: "mix-format-2",
|
||||
key: pe::PKEY_MIX_FORMAT_2,
|
||||
value: pe::StampValue::Format(&WFX_F32_1CH_48K),
|
||||
},
|
||||
pe::Stamp {
|
||||
label: "mix-format-3",
|
||||
key: pe::PKEY_MIX_FORMAT_3,
|
||||
value: pe::StampValue::Format(&WFX_F32_1CH_48K),
|
||||
},
|
||||
pe::Stamp {
|
||||
label: "host-format",
|
||||
key: pe::PKEY_HOST_FORMAT,
|
||||
value: pe::StampValue::Format(&WFX_F32_1CH_48K),
|
||||
},
|
||||
]);
|
||||
}
|
||||
// Steady state (every boot after the first): the names are already served — no writes,
|
||||
// no settle sleeps.
|
||||
if pe::stamps_served(endpoint_id, &stamps) {
|
||||
|
||||
@@ -166,12 +166,12 @@ const PKEY_ENDPOINT_DEVNODE: PROPERTYKEY = pkey(0xb3f8fa53_0004_438e_9003_51a46e
|
||||
/// `PKEY_Device_ContainerId` — what games match against the pad's HID container.
|
||||
const PKEY_CONTAINER_ID: PROPERTYKEY = pkey(0x8c7ed206_3f8a_4827_b3ab_ae9e1faefc6c, 2);
|
||||
/// `PKEY_AudioEngine_DeviceFormat` (16-bit PCM leg of the format set).
|
||||
const PKEY_DEVICE_FORMAT: PROPERTYKEY = pkey(0xf19f064d_082c_4e27_bc73_6882a1bb8e4c, 0);
|
||||
pub(crate) const PKEY_DEVICE_FORMAT: PROPERTYKEY = pkey(0xf19f064d_082c_4e27_bc73_6882a1bb8e4c, 0);
|
||||
/// Endpoint format pair (float leg) — pids 2 and 3 of the same fmtid.
|
||||
const PKEY_MIX_FORMAT_2: PROPERTYKEY = pkey(0x3d6e1656_2e50_4c4c_8d85_d0acae3c6c68, 2);
|
||||
const PKEY_MIX_FORMAT_3: PROPERTYKEY = pkey(0x3d6e1656_2e50_4c4c_8d85_d0acae3c6c68, 3);
|
||||
pub(crate) const PKEY_MIX_FORMAT_2: PROPERTYKEY = pkey(0x3d6e1656_2e50_4c4c_8d85_d0acae3c6c68, 2);
|
||||
pub(crate) const PKEY_MIX_FORMAT_3: PROPERTYKEY = pkey(0x3d6e1656_2e50_4c4c_8d85_d0acae3c6c68, 3);
|
||||
/// Host processing format (float leg).
|
||||
const PKEY_HOST_FORMAT: PROPERTYKEY = pkey(0xe4870e26_3cc5_4cd2_ba46_ca0a9a70ed04, 0);
|
||||
pub(crate) const PKEY_HOST_FORMAT: PROPERTYKEY = pkey(0xe4870e26_3cc5_4cd2_ba46_ca0a9a70ed04, 0);
|
||||
|
||||
/// `WAVEFORMATEXTENSIBLE`: 4 ch / 48 kHz / 16-bit PCM, mask 0x33 (FL FR BL BR), PCM subtype.
|
||||
const WFX_PCM16_4CH_48K: [u8; 40] = [
|
||||
|
||||
Reference in New Issue
Block a user