From 607bdfebcd375d72e22a3c3eef7a510eb4ca2d6a Mon Sep 17 00:00:00 2001 From: "wzh.team" Date: Sat, 7 Nov 2026 19:56:40 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20WASAPI=20=E7=8B=AC?= =?UTF-8?q?=E5=8D=A0=E6=A8=A1=E5=BC=8F=E4=BD=8D=E6=B7=B1=E5=8D=8F=E5=95=86?= =?UTF-8?q?=E9=94=81=E5=AE=9A=2016bit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../audio-engine/src/output/wasapi/format.rs | 64 +++++++++++-------- native/audio-engine/src/output/wasapi/mod.rs | 2 +- native/audio-engine/src/output/wasapi/pcm.rs | 14 ++-- .../src/output/wasapi/tests/format.rs | 29 +++++++++ .../src/output/wasapi/tests/pcm.rs | 52 +++++++++++++-- 5 files changed, 121 insertions(+), 40 deletions(-) diff --git a/native/audio-engine/src/output/wasapi/format.rs b/native/audio-engine/src/output/wasapi/format.rs index e89f7fcee..d02ca1f33 100644 --- a/native/audio-engine/src/output/wasapi/format.rs +++ b/native/audio-engine/src/output/wasapi/format.rs @@ -81,15 +81,24 @@ pub(super) fn build_wave_format(format: &ExclusiveFormat) -> WAVEFORMATEXTENSIBL } } -/// 位深候选:优先音源位深,16bit 作通用兜底 +/// 位深候选:优先音源位深,其次更高位深,16bit 作通用兜底 fn valid_bits_candidates(source_bits: u32) -> Vec { match source_bits { - 0..=16 => vec![16, 24], - 24 => vec![24, 16], + 0..=16 => vec![16, 24, 32], + 24 => vec![24, 32, 16], _ => vec![32, 24, 16], } } +/// 容器位深候选:先按 32bit 容器探测保持既有行为,再补打包 24bit—— +/// 大量 USB DAC 的独占格式表只接受 24-in-24,缺此候选会让协商恒落到 16bit +fn container_candidates(valid_bits: u16) -> Vec { + match valid_bits { + 24 => vec![32, 24], + bits => vec![bits], + } +} + /// 采样率候选:音源原始值优先,回退设备常见的离散值 fn sample_rate_candidates(source_rate: u32) -> Vec { let mut rates = vec![source_rate, 48_000, 44_100, 96_000, 192_000]; @@ -116,30 +125,33 @@ fn negotiate_format( for &channels in &channels_candidates { for &valid_bits in &valid_bits_candidates(source_bits) { for &rate in &sample_rate_candidates(source_rate) { - let format = ExclusiveFormat { - sample_rate: rate, - channels, - container_bits: if valid_bits == 16 { 16 } else { 32 }, - valid_bits, - }; - let wave = build_wave_format(&format); - // windows 0.62 中 IsFormatSupported 返回原始 HRESULT - let hr = probe.IsFormatSupported( - AUDCLNT_SHAREMODE_EXCLUSIVE, - &wave as *const WAVEFORMATEXTENSIBLE as *const WAVEFORMATEX, - None, - ); - if hr.is_ok() { - info!( - rate = format.sample_rate, - channels = format.channels, - bits = format.valid_bits, - "独占模式格式协商成功" + // 容器只是打包形式,优先级最低:同一有效位深和采样率下先试宽容器 + for &container_bits in &container_candidates(valid_bits) { + let format = ExclusiveFormat { + sample_rate: rate, + channels, + container_bits, + valid_bits, + }; + let wave = build_wave_format(&format); + // windows 0.62 中 IsFormatSupported 返回原始 HRESULT + let hr = probe.IsFormatSupported( + AUDCLNT_SHAREMODE_EXCLUSIVE, + &wave as *const WAVEFORMATEXTENSIBLE as *const WAVEFORMATEX, + None, ); - return Ok(format); - } - if hr == AUDCLNT_E_DEVICE_IN_USE { - return Err(ExclusiveOpenError::DeviceInUse); + if hr.is_ok() { + info!( + rate = format.sample_rate, + channels = format.channels, + bits = format.valid_bits, + "独占模式格式协商成功" + ); + return Ok(format); + } + if hr == AUDCLNT_E_DEVICE_IN_USE { + return Err(ExclusiveOpenError::DeviceInUse); + } } } } diff --git a/native/audio-engine/src/output/wasapi/mod.rs b/native/audio-engine/src/output/wasapi/mod.rs index 848bb39d7..1e4f33909 100644 --- a/native/audio-engine/src/output/wasapi/mod.rs +++ b/native/audio-engine/src/output/wasapi/mod.rs @@ -46,7 +46,7 @@ pub struct ExclusiveFormat { pub sample_rate: u32, /// 声道数 pub channels: u16, - /// 容器位深(16 / 32) + /// 容器位深(16 / 24 / 32) pub container_bits: u16, /// 有效位深(16 / 24 / 32) pub valid_bits: u16, diff --git a/native/audio-engine/src/output/wasapi/pcm.rs b/native/audio-engine/src/output/wasapi/pcm.rs index 208645769..f72a70a37 100644 --- a/native/audio-engine/src/output/wasapi/pcm.rs +++ b/native/audio-engine/src/output/wasapi/pcm.rs @@ -1,16 +1,14 @@ use super::*; /// f32 → 整型样本转换(应用音量增益后写入) +/// 返回值按有效位深定标,容器更宽时由 `fill_buffer` 左移补零 pub(super) fn convert_sample(sample: f32, gain: f32, valid_bits: u16) -> i32 { let clamped = (sample * gain).clamp(-1.0, 1.0); match valid_bits { 16 => (clamped * 32_768.0).round().clamp(-32_768.0, 32_767.0) as i32, - 24 => { - ((clamped * 8_388_608.0) - .round() - .clamp(-8_388_608.0, 8_388_607.0) as i32) - << 8 - } + 24 => (clamped * 8_388_608.0) + .round() + .clamp(-8_388_608.0, 8_388_607.0) as i32, _ => (clamped * 2_147_483_648.0).round() as i32, } } @@ -27,6 +25,8 @@ pub(super) fn fill_buffer( format: &ExclusiveFormat, ) { let bytes_per_sample = usize::from(format.container_bits / 8); + // 容器比有效位深宽时左移补零,有效位保持在高位(如 24bit 装入 32bit 容器) + let shift = format.container_bits - format.valid_bits; source.begin_callback(); let samples = buffer.len() / bytes_per_sample; let from = gain.unwrap_or(target); @@ -42,7 +42,7 @@ pub(super) fn fill_buffer( 0 } else { current += step; - convert_sample(source.next().unwrap_or(0.0), current, format.valid_bits) + convert_sample(source.next().unwrap_or(0.0), current, format.valid_bits) << shift }; raw[..bytes_per_sample].copy_from_slice(&value.to_le_bytes()[..bytes_per_sample]); chunk.copy_from_slice(&raw[..bytes_per_sample]); diff --git a/native/audio-engine/src/output/wasapi/tests/format.rs b/native/audio-engine/src/output/wasapi/tests/format.rs index 0c77edf91..9e190c271 100644 --- a/native/audio-engine/src/output/wasapi/tests/format.rs +++ b/native/audio-engine/src/output/wasapi/tests/format.rs @@ -15,3 +15,32 @@ fn exclusive_format_describes_all_eight_channels() { assert_eq!(size, 22); assert_eq!(align, 32); } + +#[test] +fn packed_24bit_format_uses_three_byte_block_align() { + let wave = build_wave_format(&ExclusiveFormat { + sample_rate: 44_100, + channels: 2, + container_bits: 24, + valid_bits: 24, + }); + // packed 字段先拷贝到局部变量再断言;Samples 是 union,读取字段需要 unsafe + let bits = wave.Format.wBitsPerSample; + let align = wave.Format.nBlockAlign; + let avg = wave.Format.nAvgBytesPerSec; + let valid = unsafe { wave.Samples.wValidBitsPerSample }; + assert_eq!(bits, 24); + assert_eq!(align, 6); + assert_eq!(avg, 44_100 * 6); + assert_eq!(valid, 24); +} + +#[test] +fn bit_depth_candidates_cover_packed_and_extended_device_formats() { + assert_eq!(valid_bits_candidates(16), vec![16, 24, 32]); + assert_eq!(valid_bits_candidates(24), vec![24, 32, 16]); + assert_eq!(valid_bits_candidates(32), vec![32, 24, 16]); + assert_eq!(container_candidates(16), vec![16]); + assert_eq!(container_candidates(24), vec![32, 24]); + assert_eq!(container_candidates(32), vec![32]); +} diff --git a/native/audio-engine/src/output/wasapi/tests/pcm.rs b/native/audio-engine/src/output/wasapi/tests/pcm.rs index 65f68a22c..8a7520082 100644 --- a/native/audio-engine/src/output/wasapi/tests/pcm.rs +++ b/native/audio-engine/src/output/wasapi/tests/pcm.rs @@ -1,4 +1,7 @@ use super::*; +use crate::decoder::buffer::{AudioChunk, Shared}; +use crate::dsp::fft::FftAnalyzer; +use std::sync::Arc; #[test] fn pcm16_roundtrip_preserves_every_sample_at_unity_gain() { @@ -13,10 +16,7 @@ fn pcm16_roundtrip_preserves_every_sample_at_unity_gain() { #[test] fn pcm24_roundtrip_preserves_every_sample_at_unity_gain() { for sample in -8_388_608..=8_388_607 { - assert_eq!( - convert_sample(sample as f32 / 8_388_608.0, 1.0, 24), - sample << 8 - ); + assert_eq!(convert_sample(sample as f32 / 8_388_608.0, 1.0, 24), sample); } } @@ -24,7 +24,7 @@ fn pcm24_roundtrip_preserves_every_sample_at_unity_gain() { fn pcm_conversion_clips_without_wrapping_and_applies_gain() { for (bits, min, max) in [ (16, -32_768, 32_767), - (24, i32::MIN, 0x7FFFFF00), + (24, -8_388_608, 8_388_607), (32, i32::MIN, i32::MAX), ] { assert_eq!(convert_sample(-2.0, 1.0, bits), min); @@ -32,5 +32,45 @@ fn pcm_conversion_clips_without_wrapping_and_applies_gain() { assert_eq!(convert_sample(1.0, 0.0, bits), 0); } assert_eq!(convert_sample(0.5, 0.5, 16), 8192); - assert_eq!(convert_sample(-0.5, 0.5, 24), -2_097_152 << 8); + assert_eq!(convert_sample(-0.5, 0.5, 24), -2_097_152); +} + +/// 有效位深写入容器时保持在高位,打包 24bit 三个字节即完整样本 +#[test] +fn fill_buffer_places_valid_bits_in_the_container_high_bits() { + for (container_bits, expected) in [ + (24, [0x00, 0x00, 0x40, 0x00, 0x00, 0xC0].as_slice()), + ( + 32, + [0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xC0].as_slice(), + ), + ] { + let shared = Shared::new(1_000, 1); + shared.push_output(AudioChunk { + normalization_gain: 1.0, + stem_samples: None, + player_samples: vec![0.5, -0.5], + fft_samples: Vec::new(), + source_sample_count: 2, + }); + shared.mark_output_eof(); + let mut source = DecoderSource::new(shared, Arc::new(FftAnalyzer::new())); + let mut buffer = [0u8; 8]; + let format = ExclusiveFormat { + sample_rate: 1_000, + channels: 1, + container_bits, + valid_bits: 24, + }; + let bytes = container_bits as usize / 8 * 2; + fill_buffer( + &mut buffer[..bytes], + &mut source, + &mut None, + 1.0, + false, + &format, + ); + assert_eq!(&buffer[..bytes], expected); + } }