Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 38 additions & 26 deletions native/audio-engine/src/output/wasapi/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,24 @@ pub(super) fn build_wave_format(format: &ExclusiveFormat) -> WAVEFORMATEXTENSIBL
}
}

/// 位深候选:优先音源位深,16bit 作通用兜底
/// 位深候选:优先音源位深,其次更高位深,16bit 作通用兜底
fn valid_bits_candidates(source_bits: u32) -> Vec<u16> {
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<u16> {
match valid_bits {
24 => vec![32, 24],
bits => vec![bits],
}
}

/// 采样率候选:音源原始值优先,回退设备常见的离散值
fn sample_rate_candidates(source_rate: u32) -> Vec<u32> {
let mut rates = vec![source_rate, 48_000, 44_100, 96_000, 192_000];
Expand All @@ -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);
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion native/audio-engine/src/output/wasapi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 7 additions & 7 deletions native/audio-engine/src/output/wasapi/pcm.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
Expand All @@ -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);
Expand All @@ -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]);
Expand Down
29 changes: 29 additions & 0 deletions native/audio-engine/src/output/wasapi/tests/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
52 changes: 46 additions & 6 deletions native/audio-engine/src/output/wasapi/tests/pcm.rs
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -13,24 +16,61 @@ 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);
}
}

#[test]
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);
assert_eq!(convert_sample(2.0, 1.0, bits), max);
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);
}
}