From 4517a451f0b51aaf1ed13d89f83b118500c653e7 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Mon, 17 Aug 2026 10:25:47 +0200 Subject: [PATCH 1/7] Only use the WAVEFORMATEX fallback for unambiguous formats A WAVEFORMATEX has no wValidBitsPerSample, so it cannot tell 24 bit packed from 24 bit padded in 32 bit containers. Some drivers accept a 24 bit format as WAVEFORMATEX and then treat it as padded, which gives noise and underruns. - to_waveformatex now returns an error unless wBitsPerSample is 8, 16, 32 or 64 and wValidBitsPerSample is equal to it. - is_supported_exclusive_with_quirks handles that error instead of unwrapping, and skips the fallback query for such formats. --- src/api.rs | 29 +++++++++++++++++++++-------- src/waveformat.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/api.rs b/src/api.rs index 371c8b7..ee711d7 100644 --- a/src/api.rs +++ b/src/api.rs @@ -877,6 +877,9 @@ impl AudioClient { /// Then call this function again with the new WafeFormat structure. /// If the driver then reports that the format is supported, use the original WaveFormat structure when calling [AudioClient::initialize_client]. /// + /// Note that [WaveFormat::to_waveformatex] returns an error for formats that a WAVEFORMATEX cannot describe without ambiguity. + /// A 24 bit format must never be queried as WAVEFORMATEX, since a driver may then accept it and treat it as 24 bit padded in 32 bit containers. + /// /// See also the helper function [is_supported_exclusive_with_quirks](AudioClient::is_supported_exclusive_with_quirks). pub fn is_supported( &self, @@ -948,6 +951,8 @@ impl AudioClient { /// The alternatives it tries are: /// - The format as given. /// - If one or two channels, try with the format as WAVEFORMATEX. + /// This is skipped for formats that a WAVEFORMATEX cannot describe without ambiguity, + /// such as 24 bit samples, see [WaveFormat::to_waveformatex]. /// - Try with different channel masks: /// - If channels <= 8: Recommended mask(s) from ksmedia.h. /// - If channels <= 18: Simple mask. @@ -966,14 +971,22 @@ impl AudioClient { return Ok(wave_fmt); } if wave_fmt.get_nchannels() <= 2 { - debug!("Repeating query with format as WAVEFORMATEX"); - let wave_formatex = wave_fmt.to_waveformatex().unwrap(); - if self - .is_supported(&wave_formatex, &ShareMode::Exclusive) - .is_ok() - { - debug!("The requested format is supported as WAVEFORMATEX"); - return Ok(wave_formatex); + // The WAVEFORMATEX representation is only tried for formats where it is unambiguous, + // see the note on WaveFormat::to_waveformatex. + match wave_fmt.to_waveformatex() { + Ok(wave_formatex) => { + debug!("Repeating query with format as WAVEFORMATEX"); + if self + .is_supported(&wave_formatex, &ShareMode::Exclusive) + .is_ok() + { + debug!("The requested format is supported as WAVEFORMATEX"); + return Ok(wave_formatex); + } + } + Err(err) => { + debug!("Skipping query with format as WAVEFORMATEX, {err}"); + } } } let masks = make_channelmasks(wave_fmt.get_nchannels() as usize); diff --git a/src/waveformat.rs b/src/waveformat.rs index 79dd82e..b447aa8 100644 --- a/src/waveformat.rs +++ b/src/waveformat.rs @@ -213,12 +213,24 @@ impl WaveFormat { } /// Return a copy in the simpler [WAVEFORMATEX](https://docs.microsoft.com/en-us/previous-versions/dd757713(v=vs.85)) format. + /// + /// A WAVEFORMATEX has no `wValidBitsPerSample`, so it can only describe formats + /// where the sample layout follows from `wBitsPerSample` alone. + /// This holds for 8, 16, 32 and 64 bits when all bits are valid. + /// A 24 bit sample can be stored either packed in three bytes, + /// or padded in a four byte container, and the two cannot be told apart + /// in a reliable way without `wValidBitsPerSample`. + /// This method returns an error for any format that would be ambiguous. pub fn to_waveformatex(&self) -> WasapiRes { let blockalign = self.wave_fmt.Format.nBlockAlign; let samplerate = self.wave_fmt.Format.nSamplesPerSec; let channels = self.wave_fmt.Format.nChannels; let byterate = self.wave_fmt.Format.nAvgBytesPerSec; let storebits = self.wave_fmt.Format.wBitsPerSample; + let validbits = unsafe { self.wave_fmt.Samples.wValidBitsPerSample }; + if !matches!(storebits, 8 | 16 | 32 | 64) || validbits != storebits { + return Err(WasapiError::UnsupportedFormat); + } let sample_type = match self.wave_fmt.SubFormat { KSDATAFORMAT_SUBTYPE_IEEE_FLOAT => WAVE_FORMAT_IEEE_FLOAT, KSDATAFORMAT_SUBTYPE_PCM => WAVE_FORMAT_PCM, @@ -359,3 +371,37 @@ pub fn make_simple_channelmask(channels: usize) -> u32 { _ => 0, } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn convert_unambiguous_formats() { + for (storebits, sample_type, formattag) in [ + (8, SampleType::Int, WAVE_FORMAT_PCM), + (16, SampleType::Int, WAVE_FORMAT_PCM), + (32, SampleType::Int, WAVE_FORMAT_PCM), + (32, SampleType::Float, WAVE_FORMAT_IEEE_FLOAT), + (64, SampleType::Float, WAVE_FORMAT_IEEE_FLOAT), + ] { + let fmt = WaveFormat::new(storebits, storebits, &sample_type, 48000, 2, None); + let fmtex = fmt.to_waveformatex().unwrap(); + assert_eq!(fmtex.wave_fmt.Format.wFormatTag as u32, formattag); + assert_eq!({ fmtex.wave_fmt.Format.cbSize }, 0); + assert_eq!(fmtex.get_bitspersample(), storebits as u16); + assert_eq!(fmtex.get_blockalign(), fmt.get_blockalign()); + assert_eq!(fmtex.get_avgbytespersec(), fmt.get_avgbytespersec()); + } + } + + #[test] + fn refuse_converting_ambiguous_formats() { + // The two 24 bit layouts, packed in three bytes and padded in four, + // cannot be told apart without wValidBitsPerSample. + let packed = WaveFormat::new(24, 24, &SampleType::Int, 48000, 2, None); + assert!(packed.to_waveformatex().is_err()); + let padded = WaveFormat::new(32, 24, &SampleType::Int, 48000, 2, None); + assert!(padded.to_waveformatex().is_err()); + } +} From 55ca7599d4b097538559f27d415d51337ec1f0e6 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Mon, 17 Aug 2026 10:52:47 +0200 Subject: [PATCH 2/7] Add exclusive mode capability probing Wasapi has no structured capability API, the only option is to call IsFormatSupported for every combination of rate, channel count, sample format and channel mask. Brute forcing the full matrix is thousands of calls, so CapabilityProbe prunes the search space with the heuristics from the CamillaDSP implementation. A scan of a laptop codec takes about 0.1 s. - Three levels of probe, one rate and channel count, one rate, and a full scan over all the standard rates, all returning WaveFormat. - The accepted channel mask per channel count is cached in the struct and shared between the calls. - New example, capabilities, that scans the default output device. --- README.md | 2 + examples/capabilities.rs | 68 +++++ src/capabilities.rs | 623 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + 4 files changed, 695 insertions(+) create mode 100644 examples/capabilities.rs create mode 100644 src/capabilities.rs diff --git a/README.md b/README.md index dbb1dc4..b447c9a 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ The following is a selection of the functionality currently available in the lib - Loopback capture - Notifications for volume change, device disconnect etc - Notifications when devices are added or removed, or when the default device changes +- Probing of the sample rates, channel counts and formats a device supports in exclusive mode - …and additional features beyond this list @@ -41,3 +42,4 @@ The following is a selection of the functionality currently available in the lib | `record_application` | Records audio from a single application, and saves the raw samples to a file. | | `aec` | Captures audio with Acoustic Echo Cancellation (AEC) enabled and saves the raw data to a file. | | `device_notifications` | Listens for devices being added, removed or changed, and for changes of the default device. | +| `capabilities` | Scans the default output device for the formats it supports in exclusive mode. | diff --git a/examples/capabilities.rs b/examples/capabilities.rs new file mode 100644 index 0000000..ab07b88 --- /dev/null +++ b/examples/capabilities.rs @@ -0,0 +1,68 @@ +use std::collections::BTreeMap; +use std::time::Instant; +use wasapi::*; + +use simplelog::*; + +/// Make a short label for a sample format, such as "S16", "F32" or "S24_in_32". +fn format_name(wave_fmt: &WaveFormat) -> String { + let sample_type = match wave_fmt.get_subformat() { + Ok(SampleType::Float) => "F", + _ => "S", + }; + let storebits = wave_fmt.get_bitspersample(); + let validbits = wave_fmt.get_validbitspersample(); + if storebits == validbits { + format!("{sample_type}{storebits}") + } else { + format!("{sample_type}{validbits}_in_{storebits}") + } +} + +// Scan the default output device for the formats it supports in exclusive mode. +fn main() { + let _ = SimpleLogger::init( + LevelFilter::Info, + ConfigBuilder::new() + .set_time_format_rfc3339() + .set_time_offset_to_local() + .unwrap() + .build(), + ); + + initialize_mta().unwrap(); + + let enumerator = DeviceEnumerator::new().unwrap(); + let device = enumerator.get_default_device(&Direction::Render).unwrap(); + println!( + "Scanning device {:?}, this takes a while..", + device.get_friendlyname().unwrap() + ); + + let mut probe = CapabilityProbe::new(device.get_iaudioclient().unwrap()); + let start = Instant::now(); + let formats = probe.supported_formats_all_rates(DEFAULT_MAX_CHANNELS); + println!("The scan took {:.1} s.", start.elapsed().as_secs_f32()); + + // Group the formats by channel count and sample rate. + let mut grouped: BTreeMap>> = BTreeMap::new(); + for wave_fmt in &formats { + grouped + .entry(wave_fmt.get_nchannels()) + .or_default() + .entry(wave_fmt.get_samplespersec()) + .or_default() + .push(format_name(wave_fmt)); + } + + if grouped.is_empty() { + println!("The device supports nothing in exclusive mode."); + return; + } + for (channels, rates) in grouped { + println!("{channels} channels:"); + for (samplerate, names) in rates { + println!(" {samplerate} Hz: {}", names.join(", ")); + } + } +} diff --git a/src/capabilities.rs b/src/capabilities.rs new file mode 100644 index 0000000..501f186 --- /dev/null +++ b/src/capabilities.rs @@ -0,0 +1,623 @@ +//! Probing of the formats a device supports in exclusive mode. + +use std::collections::{BTreeSet, HashMap}; + +use crate::{AudioClient, SampleType, WasapiRes, WaveFormat}; +use windows::Win32::Media::KernelStreaming::WAVE_FORMAT_EXTENSIBLE; + +/// The channel count ceiling used when nothing better is known. +pub const DEFAULT_MAX_CHANNELS: usize = 32; + +// Standard rates in each family, from the base rate upward through the multiples. +const FAMILY_48_RATES: &[usize] = &[48000, 96000, 192000, 384000, 768000]; +const FAMILY_44_RATES: &[usize] = &[44100, 88200, 176400, 352800, 705600]; + +// Sub-multiples and the 32 kHz family, probed after the upward scan. +const REMAINING_RATES: &[usize] = &[ + 24000, 12000, 6000, 22050, 11025, 5512, 16000, 8000, 32000, 64000, +]; + +/// A sample format to probe for, as stored bits, valid bits and sample type. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct Candidate { + storebits: usize, + validbits: usize, + sample_type: SampleType, +} + +impl Candidate { + const fn new(storebits: usize, validbits: usize, sample_type: SampleType) -> Self { + Candidate { + storebits, + validbits, + sample_type, + } + } +} + +/// The sample formats that get probed, in the order they are tried. +/// Both 24 bit layouts are probed, packed in three bytes and padded in four. +const CANDIDATE_FORMATS: &[Candidate] = &[ + Candidate::new(16, 16, SampleType::Int), + Candidate::new(24, 24, SampleType::Int), + Candidate::new(32, 24, SampleType::Int), + Candidate::new(32, 32, SampleType::Int), + Candidate::new(32, 32, SampleType::Float), +]; + +/// Accepted channel mask per channel count. +type ChannelMaskMap = HashMap; + +/// The device query the probing logic is built on. +/// Implemented for [AudioClient], and for fake devices in the unit tests. +trait FormatChecker { + fn check_exclusive(&self, wave_fmt: &WaveFormat) -> WasapiRes; +} + +impl FormatChecker for AudioClient { + fn check_exclusive(&self, wave_fmt: &WaveFormat) -> WasapiRes { + self.is_supported_exclusive_with_quirks(wave_fmt) + } +} + +/// Probes a device for the formats it supports in exclusive mode. +/// +/// The probes are available at three levels of detail, +/// for a single rate and channel count, for a single rate, +/// and for all rates. The more limited ones are a lot faster, +/// so an application that already knows what it wants should not run a full scan. +/// +/// The accepted channel masks are cached in the struct and shared between the calls, +/// so reusing the same instance for several probes is much faster than making a new one for each. +/// +/// # How the probing works +/// +/// Wasapi has no structured way of asking a device what it supports. +/// The only option is to call `IsFormatSupported` for every combination +/// of sample rate, channel count, sample format and channel mask. +/// Brute forcing the full matrix is thousands of calls and takes several +/// seconds per device, so the full scan prunes the search space: +/// +/// - The 48 kHz and 44.1 kHz families are probed interleaved from the base rate upward. +/// The first hit establishes an upper channel count limit, +/// and a reduced sample format set that all later probes reuse. +/// - Within a single rate, the sample format candidates are narrowed +/// as soon as the first channel count succeeds with fewer than the full set. +/// - The accepted channel mask of each channel count is cached and reused, +/// which avoids repeating the mask renegotiation of +/// [is_supported_exclusive_with_quirks](AudioClient::is_supported_exclusive_with_quirks). +/// - Each family gets an early cutoff. Once a family has a hit, +/// a miss at the next rate deactivates it, and the upward scan stops +/// when both families are inactive. +/// - The remaining low rates and the 32 kHz family are probed +/// using only the channel counts found during the upward scan. +/// +/// These heuristics cut the probing time down to something reasonable on normal hardware, +/// but they are still heuristics. +/// An unusual device may support combinations that fall outside the probed ones. +/// +/// The probed sample formats are 16 bit integer, 24 bit integer packed in three bytes, +/// 24 bit integer padded in four bytes, 32 bit integer and 32 bit float. +/// +/// ```no_run +/// use wasapi::{CapabilityProbe, Direction, DeviceEnumerator, DEFAULT_MAX_CHANNELS}; +/// # fn main() -> Result<(), Box> { +/// let device = DeviceEnumerator::new()?.get_default_device(&Direction::Render)?; +/// let mut probe = CapabilityProbe::new(device.get_iaudioclient()?); +/// +/// // Everything the device accepts at 48 kHz, for up to eight channels. +/// let formats = probe.supported_formats_at_rate(48000, 8); +/// +/// // Everything the device accepts, at any rate. +/// let all = probe.supported_formats_all_rates(DEFAULT_MAX_CHANNELS); +/// # Ok(()) +/// # } +/// ``` +pub struct CapabilityProbe { + client: AudioClient, + channel_masks: ChannelMaskMap, +} + +impl CapabilityProbe { + /// Create a new probe for the device of the given [AudioClient]. + /// + /// The client must not have been initialized, + /// and it can not be used for streaming while the probing runs. + pub fn new(client: AudioClient) -> Self { + CapabilityProbe { + client, + channel_masks: ChannelMaskMap::new(), + } + } + + /// Get a reference to the [AudioClient] the probe was created with. + pub fn client(&self) -> &AudioClient { + &self.client + } + + /// Get the formats the device accepts at the given sample rate and channel count. + /// + /// This is the cheapest probe, at most one query per sample format. + pub fn supported_formats(&mut self, samplerate: usize, channels: usize) -> Vec { + probe_formats( + &self.client, + &mut self.channel_masks, + samplerate, + channels, + CANDIDATE_FORMATS, + ) + .into_iter() + .map(|(_, wave_fmt)| wave_fmt) + .collect() + } + + /// Get the formats the device accepts at the given sample rate, + /// for every channel count from one up to and including `max_channels`. + pub fn supported_formats_at_rate( + &mut self, + samplerate: usize, + max_channels: usize, + ) -> Vec { + probe_rate( + &self.client, + &mut self.channel_masks, + samplerate, + 1..=max_channels, + CANDIDATE_FORMATS, + ) + .formats + } + + /// Get the formats the device accepts at any of the standard sample rates, + /// for channel counts up to and including `max_channels`. + /// + /// This is the full scan. It is the most expensive probe by far, + /// and the one that leans hardest on the pruning heuristics, + /// see the [struct documentation](CapabilityProbe). + /// Pass [DEFAULT_MAX_CHANNELS] unless the channel count is known to be lower. + pub fn supported_formats_all_rates(&mut self, max_channels: usize) -> Vec { + scan_all_rates(&self.client, &mut self.channel_masks, max_channels) + } +} + +/// Probe every candidate format at a single rate and channel count. +/// Returns the accepted formats, paired with the candidate that produced them. +fn probe_formats( + checker: &C, + channel_masks: &mut ChannelMaskMap, + samplerate: usize, + channels: usize, + candidates: &[Candidate], +) -> Vec<(Candidate, WaveFormat)> { + let mut supported = Vec::new(); + if channels == 0 { + return supported; + } + let mut preferred_mask = channel_masks.get(&channels).copied(); + if let Some(mask) = preferred_mask { + trace!("Probing {samplerate} Hz, {channels} ch using cached channel mask {mask:#010x}"); + } + for candidate in candidates { + let requested = WaveFormat::new( + candidate.storebits, + candidate.validbits, + &candidate.sample_type, + samplerate, + channels, + preferred_mask, + ); + let Ok(accepted) = checker.check_exclusive(&requested) else { + trace!("Unsupported {samplerate} Hz, {channels} ch, format {candidate:?}"); + continue; + }; + trace!("Supported {samplerate} Hz, {channels} ch, format {candidate:?}"); + if accepted.wave_fmt.Format.wFormatTag == WAVE_FORMAT_EXTENSIBLE as u16 { + let mask = accepted.get_dwchannelmask(); + if channel_masks.insert(channels, mask) != Some(mask) { + debug!("Channel count {channels} will use channel mask {mask:#010x}"); + } + preferred_mask = Some(mask); + supported.push((*candidate, accepted)); + } else { + // The device only accepted the format in the simpler WAVEFORMATEX representation. + // That is a known driver quirk for one and two channel formats, + // and the format to use for streaming is still the WAVEFORMATEXTENSIBLE one. + trace!("Accepted as WAVEFORMATEX, reporting the WAVEFORMATEXTENSIBLE form"); + supported.push((*candidate, requested)); + } + } + supported +} + +/// The outcome of probing a single sample rate. +struct RateProbe { + /// All accepted formats. + formats: Vec, + /// The channel counts that had at least one accepted format. + channel_counts: BTreeSet, + /// The union of the candidates that were accepted at any channel count. + supported_candidates: Vec, +} + +/// Probe a single rate for the given channel counts. +/// The candidate formats are narrowed as soon as a channel count +/// succeeds with fewer than all of them. +fn probe_rate( + checker: &C, + channel_masks: &mut ChannelMaskMap, + samplerate: usize, + channel_counts: I, + candidates: &[Candidate], +) -> RateProbe +where + C: FormatChecker, + I: IntoIterator, +{ + trace!("Probing {samplerate} Hz using sample formats {candidates:?}"); + let mut result = RateProbe { + formats: Vec::new(), + channel_counts: BTreeSet::new(), + supported_candidates: Vec::new(), + }; + let mut narrowed: Option> = None; + for channels in channel_counts { + let active = narrowed.as_deref().unwrap_or(candidates); + let supported = probe_formats(checker, channel_masks, samplerate, channels, active); + if supported.is_empty() { + trace!("No supported formats at {samplerate} Hz, {channels} ch"); + continue; + } + let found: Vec = supported.iter().map(|(candidate, _)| *candidate).collect(); + debug!("Found support at {samplerate} Hz, {channels} ch with formats {found:?}"); + if narrowed.is_none() && found.len() < candidates.len() { + debug!("Narrowing the formats for the rest of the {samplerate} Hz sweep to {found:?}"); + narrowed = Some(found.clone()); + } + for candidate in found { + if !result.supported_candidates.contains(&candidate) { + result.supported_candidates.push(candidate); + } + } + result.channel_counts.insert(channels); + result + .formats + .extend(supported.into_iter().map(|(_, wave_fmt)| wave_fmt)); + } + result +} + +/// Probe all the standard rates, up to the given channel count. +fn scan_all_rates( + checker: &C, + channel_masks: &mut ChannelMaskMap, + max_channels: usize, +) -> Vec { + debug!("Starting exclusive mode scan with channel ceiling {max_channels}"); + let mut formats = Vec::new(); + let mut channel_counts = BTreeSet::new(); + let mut learned: Option> = None; + + // Probe the two main families interleaved from the base rate upward. + // The first hit at any rate gives the channel limit for all the later probes. + // A family that has had a hit is deactivated by the first miss after it. + let families = [FAMILY_48_RATES, FAMILY_44_RATES]; + let mut channel_limit = 0; + let mut hit = [false; 2]; + let mut active = [true; 2]; + for step in 0..FAMILY_48_RATES.len().max(FAMILY_44_RATES.len()) { + if !active.iter().any(|is_active| *is_active) { + debug!("Stopping the upward scan, both families are inactive"); + break; + } + for (family_nbr, family) in families.iter().enumerate() { + if !active[family_nbr] { + continue; + } + let Some(&rate) = family.get(step) else { + continue; + }; + let limit = if channel_limit > 0 { + channel_limit + } else { + max_channels + }; + let candidates = learned.as_deref().unwrap_or(CANDIDATE_FORMATS); + let result = probe_rate(checker, channel_masks, rate, 1..=limit, candidates); + if let Some(&highest) = result.channel_counts.iter().next_back() { + hit[family_nbr] = true; + channel_limit = channel_limit.max(highest); + debug!( + "Rate {rate} Hz gave at most {highest} channels, limit is now {channel_limit}" + ); + if learned.is_none() { + debug!( + "Learned the sample formats {:?} from {rate} Hz, reusing them", + result.supported_candidates + ); + learned = Some(result.supported_candidates); + } + } else if hit[family_nbr] { + active[family_nbr] = false; + debug!("Stopping at {rate} Hz, this family had a miss after its earlier hits"); + } + channel_counts.extend(&result.channel_counts); + formats.extend(result.formats); + } + } + + // Probe the sub-multiples and the 32 kHz family. + // Reuse the channel counts found above, or take the full range if nothing was found. + let remaining_counts: Vec = if channel_counts.is_empty() { + debug!("Probing the remaining rates with the full channel range, nothing was found so far"); + (1..=max_channels).collect() + } else { + debug!("Probing the remaining rates with the channel counts {channel_counts:?}"); + channel_counts.iter().copied().collect() + }; + for &rate in REMAINING_RATES { + let candidates = learned.as_deref().unwrap_or(CANDIDATE_FORMATS); + let result = probe_rate( + checker, + channel_masks, + rate, + remaining_counts.iter().copied(), + candidates, + ); + if learned.is_none() && !result.supported_candidates.is_empty() { + debug!( + "Learned the sample formats {:?} from {rate} Hz, reusing them", + result.supported_candidates + ); + learned = Some(result.supported_candidates); + } + formats.extend(result.formats); + } + debug!("The exclusive mode scan found {} formats", formats.len()); + formats +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{make_channelmasks, WasapiError}; + use std::cell::RefCell; + + /// A single query made to the fake device. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct Query { + samplerate: usize, + channels: usize, + mask: u32, + candidate: Candidate, + } + + /// A fake device that accepts a fixed set of rates, channel counts and formats. + /// It only accepts a single channel mask per channel count, + /// and renegotiates the mask like + /// [is_supported_exclusive_with_quirks](AudioClient::is_supported_exclusive_with_quirks) does. + struct FakeDevice { + rates: Vec, + max_channels: usize, + skipped_channels: Vec, + formats: Vec, + queries: RefCell>, + } + + impl FakeDevice { + fn new(rates: &[usize], max_channels: usize, formats: &[Candidate]) -> Self { + FakeDevice { + rates: rates.to_vec(), + max_channels, + skipped_channels: Vec::new(), + formats: formats.to_vec(), + queries: RefCell::new(Vec::new()), + } + } + + /// Punch a hole in the supported channel counts. + fn without_channels(mut self, channels: &[usize]) -> Self { + self.skipped_channels = channels.to_vec(); + self + } + + /// The only mask this fake accepts for a channel count. + /// The last of the suggested masks, so that the mask always needs renegotiation. + fn accepted_mask(channels: usize) -> u32 { + *make_channelmasks(channels).last().unwrap() + } + + fn queries(&self) -> Vec { + self.queries.borrow().clone() + } + + fn queries_for(&self, samplerate: usize, channels: usize) -> Vec { + self.queries() + .into_iter() + .filter(|q| q.samplerate == samplerate && q.channels == channels) + .collect() + } + } + + impl FormatChecker for FakeDevice { + fn check_exclusive(&self, wave_fmt: &WaveFormat) -> WasapiRes { + let channels = wave_fmt.get_nchannels() as usize; + let candidate = Candidate::new( + wave_fmt.get_bitspersample() as usize, + wave_fmt.get_validbitspersample() as usize, + wave_fmt.get_subformat()?, + ); + let samplerate = wave_fmt.get_samplespersec() as usize; + self.queries.borrow_mut().push(Query { + samplerate, + channels, + mask: wave_fmt.get_dwchannelmask(), + candidate, + }); + if !self.rates.contains(&samplerate) + || channels > self.max_channels + || self.skipped_channels.contains(&channels) + || !self.formats.contains(&candidate) + { + return Err(WasapiError::UnsupportedFormat); + } + let mut accepted = wave_fmt.clone(); + accepted.wave_fmt.dwChannelMask = Self::accepted_mask(channels); + Ok(accepted) + } + } + + const S16: Candidate = Candidate::new(16, 16, SampleType::Int); + const S24_3: Candidate = Candidate::new(24, 24, SampleType::Int); + const S32: Candidate = Candidate::new(32, 32, SampleType::Int); + + /// Describe a format as rate, channels, stored bits and valid bits. + fn describe(wave_fmt: &WaveFormat) -> (u32, u16, u16, u16) { + ( + wave_fmt.get_samplespersec(), + wave_fmt.get_nchannels(), + wave_fmt.get_bitspersample(), + wave_fmt.get_validbitspersample(), + ) + } + + #[test] + fn probe_returns_the_supported_formats() { + let device = FakeDevice::new(&[48000], 2, &[S16, S32]); + let mut masks = ChannelMaskMap::new(); + let supported = probe_formats(&device, &mut masks, 48000, 2, CANDIDATE_FORMATS); + + let found: Vec = supported.iter().map(|(c, _)| *c).collect(); + assert_eq!(found, vec![S16, S32]); + assert_eq!(describe(&supported[0].1), (48000, 2, 16, 16)); + assert_eq!(describe(&supported[1].1), (48000, 2, 32, 32)); + // Every candidate is tried, also the ones that fail. + assert_eq!(device.queries().len(), CANDIDATE_FORMATS.len()); + } + + #[test] + fn probe_returns_nothing_for_unsupported_rates_and_channel_counts() { + let device = FakeDevice::new(&[48000], 2, &[S16]); + let mut masks = ChannelMaskMap::new(); + assert!(probe_formats(&device, &mut masks, 44100, 2, CANDIDATE_FORMATS).is_empty()); + assert!(probe_formats(&device, &mut masks, 48000, 4, CANDIDATE_FORMATS).is_empty()); + assert!(probe_formats(&device, &mut masks, 48000, 0, CANDIDATE_FORMATS).is_empty()); + } + + #[test] + fn the_accepted_channel_mask_is_cached_and_reused() { + let device = FakeDevice::new(&[48000, 96000], 2, &[S16, S32]); + let mut masks = ChannelMaskMap::new(); + let accepted = FakeDevice::accepted_mask(2); + + probe_formats(&device, &mut masks, 48000, 2, CANDIDATE_FORMATS); + assert_eq!(masks.get(&2), Some(&accepted)); + // The first query of the first probe still uses the default mask. + assert_ne!(device.queries_for(48000, 2)[0].mask, accepted); + + probe_formats(&device, &mut masks, 96000, 2, CANDIDATE_FORMATS); + // The cached mask is used from the very first query of the second probe. + assert!(device + .queries_for(96000, 2) + .iter() + .all(|q| q.mask == accepted)); + } + + #[test] + fn the_formats_are_narrowed_after_the_first_channel_count() { + let device = FakeDevice::new(&[48000], 4, &[S32]); + let mut masks = ChannelMaskMap::new(); + let result = probe_rate(&device, &mut masks, 48000, 1..=4, CANDIDATE_FORMATS); + + assert_eq!(result.supported_candidates, vec![S32]); + assert_eq!(result.channel_counts, BTreeSet::from([1, 2, 3, 4])); + // The first channel count pays for all the candidates, the rest only probe S32. + assert_eq!(device.queries_for(48000, 1).len(), CANDIDATE_FORMATS.len()); + for channels in 2..=4 { + let queries = device.queries_for(48000, channels); + assert_eq!(queries.len(), 1); + assert_eq!(queries[0].candidate, S32); + } + } + + #[test] + fn a_rate_probe_covers_all_channel_counts() { + // A device with a gap, it takes two and four channels but not three. + let device = FakeDevice::new(&[48000], 4, &[S16]).without_channels(&[3]); + let mut masks = ChannelMaskMap::new(); + let mut result = probe_rate(&device, &mut masks, 48000, [2, 3, 4], CANDIDATE_FORMATS); + result.formats.retain(|fmt| fmt.get_nchannels() == 4); + assert_eq!(result.channel_counts, BTreeSet::from([2, 4])); + assert_eq!(result.formats.len(), 1); + } + + #[test] + fn the_full_scan_finds_all_the_supported_combinations() { + let device = FakeDevice::new(&[44100, 48000, 96000, 32000], 2, &[S16, S24_3]); + let mut masks = ChannelMaskMap::new(); + let mut found: Vec<(u32, u16, u16, u16)> = scan_all_rates(&device, &mut masks, 8) + .iter() + .map(describe) + .collect(); + found.sort_unstable(); + + let mut expected = Vec::new(); + for rate in [32000, 44100, 48000, 96000] { + for channels in [1, 2] { + expected.push((rate, channels, 16, 16)); + expected.push((rate, channels, 24, 24)); + } + } + expected.sort_unstable(); + assert_eq!(found, expected); + } + + #[test] + fn the_full_scan_stops_a_family_after_a_miss() { + // 192 kHz is missing, so the 48 kHz family is dropped before 384 kHz. + let device = FakeDevice::new(&[48000, 96000, 384000], 2, &[S32]); + let mut masks = ChannelMaskMap::new(); + let found: Vec = scan_all_rates(&device, &mut masks, 8) + .iter() + .map(|fmt| fmt.get_samplespersec()) + .collect(); + + assert!(found.contains(&96000)); + assert!(!found.contains(&384000)); + assert!(!device.queries().iter().any(|q| q.samplerate == 384000)); + assert!(!device.queries().iter().any(|q| q.samplerate == 768000)); + // The 44.1 kHz family never had a hit, so it is probed to the end. + assert!(device.queries().iter().any(|q| q.samplerate == 705600)); + } + + #[test] + fn the_full_scan_limits_the_channel_counts_of_the_later_rates() { + let device = FakeDevice::new(&[48000, 44100, 32000], 2, &[S16]); + let mut masks = ChannelMaskMap::new(); + scan_all_rates(&device, &mut masks, 8); + + // The first rate probes the full range, the ceiling drops to two after that. + assert!(device.queries().iter().any(|q| q.channels == 8)); + assert!(!device + .queries() + .iter() + .any(|q| q.samplerate == 44100 && q.channels > 2)); + // The low rates only use the channel counts that were found. + assert!(!device + .queries() + .iter() + .any(|q| q.samplerate == 32000 && q.channels > 2)); + } + + #[test] + fn the_full_scan_of_a_device_without_support_finds_nothing() { + let device = FakeDevice::new(&[], 0, &[]); + let mut masks = ChannelMaskMap::new(); + assert!(scan_all_rates(&device, &mut masks, 2).is_empty()); + assert!(masks.is_empty()); + // Nothing was found, so the low rates are probed with the full channel range. + assert!(device + .queries() + .iter() + .any(|q| q.samplerate == 32000 && q.channels == 2)); + } +} diff --git a/src/lib.rs b/src/lib.rs index 4024bfc..e3076bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,12 @@ #![doc = include_str!("../README.md")] mod api; +mod capabilities; mod errors; mod events; mod waveformat; pub use api::*; +pub use capabilities::*; pub use errors::*; pub use events::*; pub use waveformat::*; From 61aad2bb71c64f80c6ef47e37d080ad1ba49319d Mon Sep 17 00:00:00 2001 From: HEnquist Date: Mon, 17 Aug 2026 15:13:22 +0200 Subject: [PATCH 3/7] Use the capabilities declared by the driver to guide the probing A WDM audio driver declares what a device accepts as a set of KS data ranges. Reading them means walking the device topology from the endpoint to the wave filter, and querying that filter with IOCTL_KS_PROPERTY. The ranges are real bounds, so a scan that has them needs none of the guessing of the staged scan, and can drop the family cutoff and the format narrowing that trade away coverage. On the 41 devices this was tried on, the two scans find exactly the same formats, and the bounded one is 2 to 10 times faster. - New Device::get_data_ranges, and CapabilityProbe::for_device that uses it. A device that declares nothing gets the staged scan as before. - New example, dataranges, that prints what each driver declares and compares it against a scan. - Document that the probe reports only the first accepted channel mask, and that probing works on a device that is in use. --- Cargo.toml | 4 +- README.md | 2 + examples/capabilities.rs | 16 +- examples/dataranges.rs | 127 +++++++ src/api.rs | 13 + src/capabilities.rs | 690 +++++++++++++++++++++++++++------------ src/dataranges.rs | 486 +++++++++++++++++++++++++++ src/lib.rs | 2 + 8 files changed, 1125 insertions(+), 215 deletions(-) create mode 100644 examples/dataranges.rs create mode 100644 src/dataranges.rs diff --git a/Cargo.toml b/Cargo.toml index 29451f0..ed9a9fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,9 @@ features = ["Foundation", "Win32_Media_Multimedia", "Win32_System_Threading", "Win32_System_Variant", - "Win32_Security",] + "Win32_Security", + "Win32_Storage_FileSystem", + "Win32_System_IO",] [dependencies] log = "0.4" diff --git a/README.md b/README.md index b447c9a..b574e44 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ The following is a selection of the functionality currently available in the lib - Notifications for volume change, device disconnect etc - Notifications when devices are added or removed, or when the default device changes - Probing of the sample rates, channel counts and formats a device supports in exclusive mode +- Reading the capabilities that a driver declares for a device - …and additional features beyond this list @@ -43,3 +44,4 @@ The following is a selection of the functionality currently available in the lib | `aec` | Captures audio with Acoustic Echo Cancellation (AEC) enabled and saves the raw data to a file. | | `device_notifications` | Listens for devices being added, removed or changed, and for changes of the default device. | | `capabilities` | Scans the default output device for the formats it supports in exclusive mode. | +| `dataranges` | Prints the capabilities each device driver declares, and verifies them with a scan. | diff --git a/examples/capabilities.rs b/examples/capabilities.rs index ab07b88..764dc4a 100644 --- a/examples/capabilities.rs +++ b/examples/capabilities.rs @@ -33,13 +33,25 @@ fn main() { initialize_mta().unwrap(); let enumerator = DeviceEnumerator::new().unwrap(); - let device = enumerator.get_default_device(&Direction::Render).unwrap(); + // Give a device name as an argument, or nothing to use the default device. + let device = match std::env::args().nth(1) { + Some(name) => enumerator + .get_device_collection(&Direction::Render) + .unwrap() + .get_device_with_name(&name) + .unwrap(), + None => enumerator.get_default_device(&Direction::Render).unwrap(), + }; println!( "Scanning device {:?}, this takes a while..", device.get_friendlyname().unwrap() ); - let mut probe = CapabilityProbe::new(device.get_iaudioclient().unwrap()); + // This uses the capabilities that the driver declares, when it has any. + let mut probe = CapabilityProbe::for_device(&device).unwrap(); + if !probe.data_ranges().is_empty() { + println!("The driver declares {} ranges.", probe.data_ranges().len()); + } let start = Instant::now(); let formats = probe.supported_formats_all_rates(DEFAULT_MAX_CHANNELS); println!("The scan took {:.1} s.", start.elapsed().as_secs_f32()); diff --git a/examples/dataranges.rs b/examples/dataranges.rs new file mode 100644 index 0000000..8e5e1c9 --- /dev/null +++ b/examples/dataranges.rs @@ -0,0 +1,127 @@ +// Compare the capabilities that a driver declares with what the device really accepts. +// +// For every active output device this prints the declared data ranges, +// then runs a full scan both with and without them, and compares the results. +// It answers two questions, whether the declared ranges can be trusted, +// and how much they help. +// +// Give a substring of a device name as an argument to only check the matching devices. + +use std::collections::BTreeSet; +use std::time::Instant; + +use wasapi::*; + +use simplelog::*; + +/// A format as rate, channels, stored bits, valid bits and sample type. +type Described = (u32, u16, u16, u16, String); + +/// Describe a format as rate, channels, stored bits, valid bits and sample type. +fn describe(wave_fmt: &WaveFormat) -> Described { + ( + wave_fmt.get_samplespersec(), + wave_fmt.get_nchannels(), + wave_fmt.get_bitspersample(), + wave_fmt.get_validbitspersample(), + match wave_fmt.get_subformat() { + Ok(sample_type) => sample_type.to_string(), + Err(_) => "unknown".to_string(), + }, + ) +} + +fn scan(probe: &mut CapabilityProbe) -> (BTreeSet, f32) { + let start = Instant::now(); + let formats = probe.supported_formats_all_rates(DEFAULT_MAX_CHANNELS); + let elapsed = start.elapsed().as_secs_f32(); + (formats.iter().map(describe).collect(), elapsed) +} + +fn main() { + let wanted = std::env::args().nth(1).unwrap_or_default().to_lowercase(); + let _ = SimpleLogger::init( + LevelFilter::Warn, + ConfigBuilder::new() + .set_time_format_rfc3339() + .set_time_offset_to_local() + .unwrap() + .build(), + ); + initialize_mta().ok().unwrap(); + + let enumerator = DeviceEnumerator::new().unwrap(); + let collections = [ + enumerator + .get_device_collection(&Direction::Render) + .unwrap(), + enumerator + .get_device_collection(&Direction::Capture) + .unwrap(), + ]; + + for device in collections.iter().flatten() { + let device = device.unwrap(); + let name = device.get_friendlyname().unwrap_or_default(); + if !wanted.is_empty() && !name.to_lowercase().contains(&wanted) { + continue; + } + println!("\n{} device {name:?}", device.get_direction()); + + let ranges = match device.get_data_ranges() { + Ok(ranges) if ranges.is_empty() => { + println!(" the driver declares nothing"); + Vec::new() + } + Ok(ranges) => { + for range in &ranges { + println!( + " declared: {} ch, {}-{} bits, {}-{} Hz, {}", + range.max_channels, + range.min_bits_per_sample, + range.max_bits_per_sample, + range.min_samplerate, + range.max_samplerate, + match range.sample_type() { + Some(sample_type) => sample_type.to_string(), + None => "any".to_string(), + } + ); + } + ranges + } + Err(err) => { + println!(" could not read the data ranges: {err}"); + Vec::new() + } + }; + + let mut plain = CapabilityProbe::new(device.get_iaudioclient().unwrap()); + let (staged, staged_time) = scan(&mut plain); + println!( + " the staged scan found {} formats in {staged_time:.2} s", + staged.len() + ); + + if ranges.is_empty() { + continue; + } + let mut bounded = CapabilityProbe::new(device.get_iaudioclient().unwrap()); + bounded.set_data_ranges(ranges); + let (found, found_time) = scan(&mut bounded); + println!( + " the bounded scan found {} formats in {found_time:.2} s", + found.len() + ); + + for missed in staged.difference(&found) { + println!(" MISSED by the declared ranges: {missed:?}"); + } + for extra in found.difference(&staged) { + println!(" found only with the declared ranges: {extra:?}"); + } + if staged == found { + println!(" the two scans agree"); + } + } +} diff --git a/src/api.rs b/src/api.rs index ee711d7..b16318d 100644 --- a/src/api.rs +++ b/src/api.rs @@ -570,6 +570,19 @@ impl Device { }) } + /// Get the [DataRange]s that the driver declares for this device. + /// + /// The ranges are an upper bound on what the device supports, + /// see [DataRange](crate::DataRange) for the details and the limitations. + /// They can be used to narrow down the search of a [CapabilityProbe](crate::CapabilityProbe). + /// + /// This only works for devices that are backed by a driver with a + /// kernel streaming filter. Devices that are implemented in software + /// have nothing to ask, and then this returns an error or an empty list. + pub fn get_data_ranges(&self) -> WasapiRes> { + crate::dataranges::read_data_ranges(&self.device, self.direction) + } + /// Gets an [IAudioSessionManager] from an [IMMDevice] pub fn get_iaudiosessionmanager(&self) -> WasapiRes { let session_manager = unsafe { diff --git a/src/capabilities.rs b/src/capabilities.rs index 501f186..c0143d5 100644 --- a/src/capabilities.rs +++ b/src/capabilities.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeSet, HashMap}; -use crate::{AudioClient, SampleType, WasapiRes, WaveFormat}; +use crate::{covered_by_any, AudioClient, DataRange, Device, SampleType, WasapiRes, WaveFormat}; use windows::Win32::Media::KernelStreaming::WAVE_FORMAT_EXTENSIBLE; /// The channel count ceiling used when nothing better is known. @@ -17,6 +17,13 @@ const REMAINING_RATES: &[usize] = &[ 24000, 12000, 6000, 22050, 11025, 5512, 16000, 8000, 32000, 64000, ]; +/// Every rate of the three lists above, in ascending order. +/// Used when the declared capabilities of the driver make the staged scan unnecessary. +const ALL_RATES: &[usize] = &[ + 5512, 6000, 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000, + 176400, 192000, 352800, 384000, 705600, 768000, +]; + /// A sample format to probe for, as stored bits, valid bits and sample type. #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct Candidate { @@ -76,16 +83,37 @@ impl FormatChecker for AudioClient { /// The only option is to call `IsFormatSupported` for every combination /// of sample rate, channel count, sample format and channel mask. /// Brute forcing the full matrix is thousands of calls and takes several -/// seconds per device, so the full scan prunes the search space: +/// seconds per device, so the search space has to be cut down. +/// +/// The probed sample formats are 16 bit integer, 24 bit integer packed in three bytes, +/// 24 bit integer padded in four bytes, 32 bit integer and 32 bit float. +/// The accepted channel mask of each channel count is cached and reused, +/// which avoids repeating the mask renegotiation of +/// [is_supported_exclusive_with_quirks](AudioClient::is_supported_exclusive_with_quirks). +/// +/// ## With the ranges the driver declares +/// +/// When the probe has [DataRange]s, from [CapabilityProbe::for_device] or +/// [CapabilityProbe::set_data_ranges], they give real bounds on the rates, +/// channel counts and sample formats. +/// The scan then only asks about the combinations that fall inside them, +/// and needs no guessing at all. +/// +/// The ranges are declared per pin and over-report, so every combination +/// inside them is still confirmed with a query. +/// A driver that declares too little would make the scan miss something, +/// but on the devices this has been tried on, the declared ranges and the +/// staged scan below agree exactly, and the bounded scan is several times faster. +/// +/// ## Without them +/// +/// A device that declares nothing gets a staged scan that guesses instead: /// /// - The 48 kHz and 44.1 kHz families are probed interleaved from the base rate upward. /// The first hit establishes an upper channel count limit, /// and a reduced sample format set that all later probes reuse. /// - Within a single rate, the sample format candidates are narrowed /// as soon as the first channel count succeeds with fewer than the full set. -/// - The accepted channel mask of each channel count is cached and reused, -/// which avoids repeating the mask renegotiation of -/// [is_supported_exclusive_with_quirks](AudioClient::is_supported_exclusive_with_quirks). /// - Each family gets an early cutoff. Once a family has a hit, /// a miss at the next rate deactivates it, and the upward scan stops /// when both families are inactive. @@ -94,16 +122,31 @@ impl FormatChecker for AudioClient { /// /// These heuristics cut the probing time down to something reasonable on normal hardware, /// but they are still heuristics. -/// An unusual device may support combinations that fall outside the probed ones. +/// A device that supports 48, 96 and 384 kHz but not 192 kHz loses the top rate to the +/// cutoff, and a format that only works at some other channel count can be narrowed away. /// -/// The probed sample formats are 16 bit integer, 24 bit integer packed in three bytes, -/// 24 bit integer padded in four bytes, 32 bit integer and 32 bit float. +/// # Probing a device that is in use +/// +/// The probing only queries, it never initializes a client or starts a stream, +/// so it does not disturb anything that is playing or recording. +/// A device that another process holds in exclusive mode can still be probed, +/// and gives the same answers as an idle one. +/// +/// # Channel masks +/// +/// Each returned format carries the first channel mask the device accepted for that channel count, +/// and that mask is then reused for the rest of the probing. +/// A device may well accept several masks for the same channel count, +/// for example both of the 5.1 layouts for six channels, +/// but the probing stops at the first one and the others are never tried. +/// Use [is_supported_exclusive_with_quirks](AudioClient::is_supported_exclusive_with_quirks) +/// directly to find out whether a specific layout is accepted. /// /// ```no_run /// use wasapi::{CapabilityProbe, Direction, DeviceEnumerator, DEFAULT_MAX_CHANNELS}; /// # fn main() -> Result<(), Box> { /// let device = DeviceEnumerator::new()?.get_default_device(&Direction::Render)?; -/// let mut probe = CapabilityProbe::new(device.get_iaudioclient()?); +/// let mut probe = CapabilityProbe::for_device(&device)?; /// /// // Everything the device accepts at 48 kHz, for up to eight channels. /// let formats = probe.supported_formats_at_rate(48000, 8); @@ -116,6 +159,7 @@ impl FormatChecker for AudioClient { pub struct CapabilityProbe { client: AudioClient, channel_masks: ChannelMaskMap, + data_ranges: Vec, } impl CapabilityProbe { @@ -123,13 +167,46 @@ impl CapabilityProbe { /// /// The client must not have been initialized, /// and it can not be used for streaming while the probing runs. + /// + /// Use [CapabilityProbe::for_device] instead to get the faster and + /// more thorough scan that the declared capabilities of the driver allow. pub fn new(client: AudioClient) -> Self { CapabilityProbe { client, channel_masks: ChannelMaskMap::new(), + data_ranges: Vec::new(), } } + /// Create a new probe for a [Device], using the [DataRange]s + /// that its driver declares to narrow down the search. + /// + /// Falls back to a probe without any ranges if the device has none, + /// which is the case for devices that are implemented in software. + pub fn for_device(device: &Device) -> WasapiRes { + let client = device.get_iaudioclient()?; + let data_ranges = device.get_data_ranges().unwrap_or_else(|err| { + debug!("Could not read the data ranges of the device, {err}"); + Vec::new() + }); + Ok(CapabilityProbe { + client, + channel_masks: ChannelMaskMap::new(), + data_ranges, + }) + } + + /// Get the [DataRange]s the probe uses to narrow down the search. + /// The list is empty when the probe has none, and then nothing is skipped. + pub fn data_ranges(&self) -> &[DataRange] { + &self.data_ranges + } + + /// Set the [DataRange]s the probe uses to narrow down the search. + pub fn set_data_ranges(&mut self, data_ranges: Vec) { + self.data_ranges = data_ranges; + } + /// Get a reference to the [AudioClient] the probe was created with. pub fn client(&self) -> &AudioClient { &self.client @@ -139,16 +216,11 @@ impl CapabilityProbe { /// /// This is the cheapest probe, at most one query per sample format. pub fn supported_formats(&mut self, samplerate: usize, channels: usize) -> Vec { - probe_formats( - &self.client, - &mut self.channel_masks, - samplerate, - channels, - CANDIDATE_FORMATS, - ) - .into_iter() - .map(|(_, wave_fmt)| wave_fmt) - .collect() + self.probing() + .formats(samplerate, channels, CANDIDATE_FORMATS) + .into_iter() + .map(|(_, wave_fmt)| wave_fmt) + .collect() } /// Get the formats the device accepts at the given sample rate, @@ -158,222 +230,274 @@ impl CapabilityProbe { samplerate: usize, max_channels: usize, ) -> Vec { - probe_rate( - &self.client, - &mut self.channel_masks, - samplerate, - 1..=max_channels, - CANDIDATE_FORMATS, - ) - .formats + let narrow = self.data_ranges.is_empty(); + self.probing() + .rate(samplerate, 1..=max_channels, CANDIDATE_FORMATS, narrow) + .formats } /// Get the formats the device accepts at any of the standard sample rates, /// for channel counts up to and including `max_channels`. /// - /// This is the full scan. It is the most expensive probe by far, - /// and the one that leans hardest on the pruning heuristics, - /// see the [struct documentation](CapabilityProbe). + /// This is the full scan. It is the most expensive probe by far. + /// Without any [DataRange]s it is also the one that leans hardest + /// on the pruning heuristics, see the [struct documentation](CapabilityProbe). /// Pass [DEFAULT_MAX_CHANNELS] unless the channel count is known to be lower. pub fn supported_formats_all_rates(&mut self, max_channels: usize) -> Vec { - scan_all_rates(&self.client, &mut self.channel_masks, max_channels) + self.probing().all_rates(max_channels) } -} -/// Probe every candidate format at a single rate and channel count. -/// Returns the accepted formats, paired with the candidate that produced them. -fn probe_formats( - checker: &C, - channel_masks: &mut ChannelMaskMap, - samplerate: usize, - channels: usize, - candidates: &[Candidate], -) -> Vec<(Candidate, WaveFormat)> { - let mut supported = Vec::new(); - if channels == 0 { - return supported; - } - let mut preferred_mask = channel_masks.get(&channels).copied(); - if let Some(mask) = preferred_mask { - trace!("Probing {samplerate} Hz, {channels} ch using cached channel mask {mask:#010x}"); - } - for candidate in candidates { - let requested = WaveFormat::new( - candidate.storebits, - candidate.validbits, - &candidate.sample_type, - samplerate, - channels, - preferred_mask, - ); - let Ok(accepted) = checker.check_exclusive(&requested) else { - trace!("Unsupported {samplerate} Hz, {channels} ch, format {candidate:?}"); - continue; - }; - trace!("Supported {samplerate} Hz, {channels} ch, format {candidate:?}"); - if accepted.wave_fmt.Format.wFormatTag == WAVE_FORMAT_EXTENSIBLE as u16 { - let mask = accepted.get_dwchannelmask(); - if channel_masks.insert(channels, mask) != Some(mask) { - debug!("Channel count {channels} will use channel mask {mask:#010x}"); - } - preferred_mask = Some(mask); - supported.push((*candidate, accepted)); - } else { - // The device only accepted the format in the simpler WAVEFORMATEX representation. - // That is a known driver quirk for one and two channel formats, - // and the format to use for streaming is still the WAVEFORMATEXTENSIBLE one. - trace!("Accepted as WAVEFORMATEX, reporting the WAVEFORMATEXTENSIBLE form"); - supported.push((*candidate, requested)); + /// Borrow the parts that the probing needs. + fn probing(&mut self) -> Probing<'_, AudioClient> { + Probing { + checker: &self.client, + channel_masks: &mut self.channel_masks, + data_ranges: &self.data_ranges, } } - supported } -/// The outcome of probing a single sample rate. -struct RateProbe { - /// All accepted formats. - formats: Vec, - /// The channel counts that had at least one accepted format. - channel_counts: BTreeSet, - /// The union of the candidates that were accepted at any channel count. - supported_candidates: Vec, +/// The state that is shared between the probes. +struct Probing<'a, C: FormatChecker> { + checker: &'a C, + channel_masks: &'a mut ChannelMaskMap, + data_ranges: &'a [DataRange], } -/// Probe a single rate for the given channel counts. -/// The candidate formats are narrowed as soon as a channel count -/// succeeds with fewer than all of them. -fn probe_rate( - checker: &C, - channel_masks: &mut ChannelMaskMap, - samplerate: usize, - channel_counts: I, - candidates: &[Candidate], -) -> RateProbe -where - C: FormatChecker, - I: IntoIterator, -{ - trace!("Probing {samplerate} Hz using sample formats {candidates:?}"); - let mut result = RateProbe { - formats: Vec::new(), - channel_counts: BTreeSet::new(), - supported_candidates: Vec::new(), - }; - let mut narrowed: Option> = None; - for channels in channel_counts { - let active = narrowed.as_deref().unwrap_or(candidates); - let supported = probe_formats(checker, channel_masks, samplerate, channels, active); - if supported.is_empty() { - trace!("No supported formats at {samplerate} Hz, {channels} ch"); - continue; +impl Probing<'_, C> { + /// Probe every candidate format at a single rate and channel count. + /// Returns the accepted formats, paired with the candidate that produced them. + fn formats( + &mut self, + samplerate: usize, + channels: usize, + candidates: &[Candidate], + ) -> Vec<(Candidate, WaveFormat)> { + let mut supported = Vec::new(); + if channels == 0 { + return supported; } - let found: Vec = supported.iter().map(|(candidate, _)| *candidate).collect(); - debug!("Found support at {samplerate} Hz, {channels} ch with formats {found:?}"); - if narrowed.is_none() && found.len() < candidates.len() { - debug!("Narrowing the formats for the rest of the {samplerate} Hz sweep to {found:?}"); - narrowed = Some(found.clone()); + let mut preferred_mask = self.channel_masks.get(&channels).copied(); + if let Some(mask) = preferred_mask { + trace!("Probing {samplerate} Hz, {channels} ch using cached channel mask {mask:#010x}"); } - for candidate in found { - if !result.supported_candidates.contains(&candidate) { - result.supported_candidates.push(candidate); + for candidate in candidates { + let requested = WaveFormat::new( + candidate.storebits, + candidate.validbits, + &candidate.sample_type, + samplerate, + channels, + preferred_mask, + ); + if !covered_by_any(self.data_ranges, &requested) { + trace!("Skipping {samplerate} Hz, {channels} ch, format {candidate:?}, the driver declares no range for it"); + continue; + } + let Ok(accepted) = self.checker.check_exclusive(&requested) else { + trace!("Unsupported {samplerate} Hz, {channels} ch, format {candidate:?}"); + continue; + }; + trace!("Supported {samplerate} Hz, {channels} ch, format {candidate:?}"); + if accepted.wave_fmt.Format.wFormatTag == WAVE_FORMAT_EXTENSIBLE as u16 { + let mask = accepted.get_dwchannelmask(); + if self.channel_masks.insert(channels, mask) != Some(mask) { + debug!("Channel count {channels} will use channel mask {mask:#010x}"); + } + preferred_mask = Some(mask); + supported.push((*candidate, accepted)); + } else { + // The device only accepted the format in the simpler WAVEFORMATEX representation. + // That is a known driver quirk for one and two channel formats, + // and the format to use for streaming is still the WAVEFORMATEXTENSIBLE one. + trace!("Accepted as WAVEFORMATEX, reporting the WAVEFORMATEXTENSIBLE form"); + supported.push((*candidate, requested)); } } - result.channel_counts.insert(channels); - result - .formats - .extend(supported.into_iter().map(|(_, wave_fmt)| wave_fmt)); + supported } - result -} -/// Probe all the standard rates, up to the given channel count. -fn scan_all_rates( - checker: &C, - channel_masks: &mut ChannelMaskMap, - max_channels: usize, -) -> Vec { - debug!("Starting exclusive mode scan with channel ceiling {max_channels}"); - let mut formats = Vec::new(); - let mut channel_counts = BTreeSet::new(); - let mut learned: Option> = None; - - // Probe the two main families interleaved from the base rate upward. - // The first hit at any rate gives the channel limit for all the later probes. - // A family that has had a hit is deactivated by the first miss after it. - let families = [FAMILY_48_RATES, FAMILY_44_RATES]; - let mut channel_limit = 0; - let mut hit = [false; 2]; - let mut active = [true; 2]; - for step in 0..FAMILY_48_RATES.len().max(FAMILY_44_RATES.len()) { - if !active.iter().any(|is_active| *is_active) { - debug!("Stopping the upward scan, both families are inactive"); - break; - } - for (family_nbr, family) in families.iter().enumerate() { - if !active[family_nbr] { + /// Probe a single rate for the given channel counts. + /// With `narrow` the candidate formats are cut down as soon as a channel count + /// succeeds with fewer than all of them. + fn rate( + &mut self, + samplerate: usize, + channel_counts: I, + candidates: &[Candidate], + narrow: bool, + ) -> RateProbe + where + I: IntoIterator, + { + trace!("Probing {samplerate} Hz using sample formats {candidates:?}"); + let mut result = RateProbe { + formats: Vec::new(), + channel_counts: BTreeSet::new(), + supported_candidates: Vec::new(), + }; + let mut narrowed: Option> = None; + for channels in channel_counts { + let active = narrowed.clone(); + let active = active.as_deref().unwrap_or(candidates); + let supported = self.formats(samplerate, channels, active); + if supported.is_empty() { + trace!("No supported formats at {samplerate} Hz, {channels} ch"); continue; } - let Some(&rate) = family.get(step) else { - continue; - }; - let limit = if channel_limit > 0 { - channel_limit - } else { - max_channels - }; - let candidates = learned.as_deref().unwrap_or(CANDIDATE_FORMATS); - let result = probe_rate(checker, channel_masks, rate, 1..=limit, candidates); - if let Some(&highest) = result.channel_counts.iter().next_back() { - hit[family_nbr] = true; - channel_limit = channel_limit.max(highest); + let found: Vec = supported.iter().map(|(candidate, _)| *candidate).collect(); + debug!("Found support at {samplerate} Hz, {channels} ch with formats {found:?}"); + if narrow && narrowed.is_none() && found.len() < candidates.len() { debug!( - "Rate {rate} Hz gave at most {highest} channels, limit is now {channel_limit}" + "Narrowing the formats for the rest of the {samplerate} Hz sweep to {found:?}" ); - if learned.is_none() { - debug!( - "Learned the sample formats {:?} from {rate} Hz, reusing them", - result.supported_candidates - ); - learned = Some(result.supported_candidates); + narrowed = Some(found.clone()); + } + for candidate in found { + if !result.supported_candidates.contains(&candidate) { + result.supported_candidates.push(candidate); } - } else if hit[family_nbr] { - active[family_nbr] = false; - debug!("Stopping at {rate} Hz, this family had a miss after its earlier hits"); } - channel_counts.extend(&result.channel_counts); - formats.extend(result.formats); + result.channel_counts.insert(channels); + result + .formats + .extend(supported.into_iter().map(|(_, wave_fmt)| wave_fmt)); } + result } - // Probe the sub-multiples and the 32 kHz family. - // Reuse the channel counts found above, or take the full range if nothing was found. - let remaining_counts: Vec = if channel_counts.is_empty() { - debug!("Probing the remaining rates with the full channel range, nothing was found so far"); - (1..=max_channels).collect() - } else { - debug!("Probing the remaining rates with the channel counts {channel_counts:?}"); - channel_counts.iter().copied().collect() - }; - for &rate in REMAINING_RATES { - let candidates = learned.as_deref().unwrap_or(CANDIDATE_FORMATS); - let result = probe_rate( - checker, - channel_masks, - rate, - remaining_counts.iter().copied(), - candidates, + /// Probe all the standard rates, up to the given channel count. + fn all_rates(&mut self, max_channels: usize) -> Vec { + if !self.data_ranges.is_empty() { + return self.all_rates_within_ranges(max_channels); + } + self.all_rates_staged(max_channels) + } + + /// Probe the rates and channel counts that the driver declares support for. + /// The declared ranges are real bounds, so none of the guessing of the staged scan is needed. + fn all_rates_within_ranges(&mut self, max_channels: usize) -> Vec { + let declared_channels = self + .data_ranges + .iter() + .map(|range| range.max_channels as usize) + .max() + .unwrap_or(0); + let ceiling = declared_channels.min(max_channels); + debug!( + "Starting exclusive mode scan with channel ceiling {ceiling}, \ + the driver declares at most {declared_channels} channels" ); - if learned.is_none() && !result.supported_candidates.is_empty() { + let mut formats = Vec::new(); + for &rate in ALL_RATES { + let declared = self.data_ranges.iter().any(|range| { + (range.min_samplerate..=range.max_samplerate).contains(&(rate as u32)) + }); + if !declared { + trace!("Skipping {rate} Hz, the driver declares no range for it"); + continue; + } + let result = self.rate(rate, 1..=ceiling, CANDIDATE_FORMATS, false); + formats.extend(result.formats); + } + debug!("The exclusive mode scan found {} formats", formats.len()); + formats + } + + /// Probe all the standard rates in stages, pruning the search as it goes. + /// This is what is left when the driver declares nothing. + fn all_rates_staged(&mut self, max_channels: usize) -> Vec { + debug!("Starting staged exclusive mode scan with channel ceiling {max_channels}"); + let mut formats = Vec::new(); + let mut channel_counts = BTreeSet::new(); + let mut learned: Option> = None; + + // Probe the two main families interleaved from the base rate upward. + // The first hit at any rate gives the channel limit for all the later probes. + // A family that has had a hit is deactivated by the first miss after it. + let families = [FAMILY_48_RATES, FAMILY_44_RATES]; + let mut channel_limit = 0; + let mut hit = [false; 2]; + let mut active = [true; 2]; + for step in 0..FAMILY_48_RATES.len().max(FAMILY_44_RATES.len()) { + if !active.iter().any(|is_active| *is_active) { + debug!("Stopping the upward scan, both families are inactive"); + break; + } + for (family_nbr, family) in families.iter().enumerate() { + if !active[family_nbr] { + continue; + } + let Some(&rate) = family.get(step) else { + continue; + }; + let limit = if channel_limit > 0 { + channel_limit + } else { + max_channels + }; + let candidates = learned.clone(); + let candidates = candidates.as_deref().unwrap_or(CANDIDATE_FORMATS); + let result = self.rate(rate, 1..=limit, candidates, true); + if let Some(&highest) = result.channel_counts.iter().next_back() { + hit[family_nbr] = true; + channel_limit = channel_limit.max(highest); + debug!( + "Rate {rate} Hz gave at most {highest} channels, limit is now {channel_limit}" + ); + if learned.is_none() { + debug!( + "Learned the sample formats {:?} from {rate} Hz, reusing them", + result.supported_candidates + ); + learned = Some(result.supported_candidates); + } + } else if hit[family_nbr] { + active[family_nbr] = false; + debug!("Stopping at {rate} Hz, this family had a miss after its earlier hits"); + } + channel_counts.extend(&result.channel_counts); + formats.extend(result.formats); + } + } + + // Probe the sub-multiples and the 32 kHz family. + // Reuse the channel counts found above, or take the full range if nothing was found. + let remaining_counts: Vec = if channel_counts.is_empty() { debug!( - "Learned the sample formats {:?} from {rate} Hz, reusing them", - result.supported_candidates + "Probing the remaining rates with the full channel range, nothing was found so far" ); - learned = Some(result.supported_candidates); + (1..=max_channels).collect() + } else { + debug!("Probing the remaining rates with the channel counts {channel_counts:?}"); + channel_counts.iter().copied().collect() + }; + for &rate in REMAINING_RATES { + let candidates = learned.clone(); + let candidates = candidates.as_deref().unwrap_or(CANDIDATE_FORMATS); + let result = self.rate(rate, remaining_counts.iter().copied(), candidates, true); + if learned.is_none() && !result.supported_candidates.is_empty() { + debug!( + "Learned the sample formats {:?} from {rate} Hz, reusing them", + result.supported_candidates + ); + learned = Some(result.supported_candidates); + } + formats.extend(result.formats); } - formats.extend(result.formats); + debug!("The exclusive mode scan found {} formats", formats.len()); + formats } - debug!("The exclusive mode scan found {} formats", formats.len()); - formats +} + +/// The outcome of probing a single sample rate. +struct RateProbe { + /// All accepted formats. + formats: Vec, + /// The channel counts that had at least one accepted format. + channel_counts: BTreeSet, + /// The union of the candidates that were accepted at any channel count. + supported_candidates: Vec, } #[cfg(test)] @@ -382,6 +506,43 @@ mod tests { use crate::{make_channelmasks, WasapiError}; use std::cell::RefCell; + /// Build a probing state for a fake device, without any declared ranges. + fn probing<'a>( + device: &'a FakeDevice, + channel_masks: &'a mut ChannelMaskMap, + ) -> Probing<'a, FakeDevice> { + Probing { + checker: device, + channel_masks, + data_ranges: &[], + } + } + + /// Build a probing state for a fake device with declared ranges. + fn probing_with<'a>( + device: &'a FakeDevice, + channel_masks: &'a mut ChannelMaskMap, + data_ranges: &'a [DataRange], + ) -> Probing<'a, FakeDevice> { + Probing { + checker: device, + channel_masks, + data_ranges, + } + } + + /// A range of PCM formats, as a driver would declare it. + fn declared(max_channels: u32, bits: (u32, u32), rates: (u32, u32)) -> DataRange { + DataRange { + max_channels, + min_bits_per_sample: bits.0, + max_bits_per_sample: bits.1, + min_samplerate: rates.0, + max_samplerate: rates.1, + subformat: windows::Win32::Media::KernelStreaming::KSDATAFORMAT_SUBTYPE_PCM, + } + } + /// A single query made to the fake device. #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct Query { @@ -400,6 +561,7 @@ mod tests { max_channels: usize, skipped_channels: Vec, formats: Vec, + mono_only_formats: Vec, queries: RefCell>, } @@ -410,10 +572,17 @@ mod tests { max_channels, skipped_channels: Vec::new(), formats: formats.to_vec(), + mono_only_formats: Vec::new(), queries: RefCell::new(Vec::new()), } } + /// Make some formats work with a single channel only. + fn only_with_one_channel(mut self, formats: &[Candidate]) -> Self { + self.mono_only_formats = formats.to_vec(); + self + } + /// Punch a hole in the supported channel counts. fn without_channels(mut self, channels: &[usize]) -> Self { self.skipped_channels = channels.to_vec(); @@ -457,6 +626,7 @@ mod tests { || channels > self.max_channels || self.skipped_channels.contains(&channels) || !self.formats.contains(&candidate) + || (channels > 1 && self.mono_only_formats.contains(&candidate)) { return Err(WasapiError::UnsupportedFormat); } @@ -484,7 +654,7 @@ mod tests { fn probe_returns_the_supported_formats() { let device = FakeDevice::new(&[48000], 2, &[S16, S32]); let mut masks = ChannelMaskMap::new(); - let supported = probe_formats(&device, &mut masks, 48000, 2, CANDIDATE_FORMATS); + let supported = probing(&device, &mut masks).formats(48000, 2, CANDIDATE_FORMATS); let found: Vec = supported.iter().map(|(c, _)| *c).collect(); assert_eq!(found, vec![S16, S32]); @@ -498,9 +668,15 @@ mod tests { fn probe_returns_nothing_for_unsupported_rates_and_channel_counts() { let device = FakeDevice::new(&[48000], 2, &[S16]); let mut masks = ChannelMaskMap::new(); - assert!(probe_formats(&device, &mut masks, 44100, 2, CANDIDATE_FORMATS).is_empty()); - assert!(probe_formats(&device, &mut masks, 48000, 4, CANDIDATE_FORMATS).is_empty()); - assert!(probe_formats(&device, &mut masks, 48000, 0, CANDIDATE_FORMATS).is_empty()); + assert!(probing(&device, &mut masks) + .formats(44100, 2, CANDIDATE_FORMATS) + .is_empty()); + assert!(probing(&device, &mut masks) + .formats(48000, 4, CANDIDATE_FORMATS) + .is_empty()); + assert!(probing(&device, &mut masks) + .formats(48000, 0, CANDIDATE_FORMATS) + .is_empty()); } #[test] @@ -509,12 +685,12 @@ mod tests { let mut masks = ChannelMaskMap::new(); let accepted = FakeDevice::accepted_mask(2); - probe_formats(&device, &mut masks, 48000, 2, CANDIDATE_FORMATS); + probing(&device, &mut masks).formats(48000, 2, CANDIDATE_FORMATS); assert_eq!(masks.get(&2), Some(&accepted)); // The first query of the first probe still uses the default mask. assert_ne!(device.queries_for(48000, 2)[0].mask, accepted); - probe_formats(&device, &mut masks, 96000, 2, CANDIDATE_FORMATS); + probing(&device, &mut masks).formats(96000, 2, CANDIDATE_FORMATS); // The cached mask is used from the very first query of the second probe. assert!(device .queries_for(96000, 2) @@ -526,7 +702,7 @@ mod tests { fn the_formats_are_narrowed_after_the_first_channel_count() { let device = FakeDevice::new(&[48000], 4, &[S32]); let mut masks = ChannelMaskMap::new(); - let result = probe_rate(&device, &mut masks, 48000, 1..=4, CANDIDATE_FORMATS); + let result = probing(&device, &mut masks).rate(48000, 1..=4, CANDIDATE_FORMATS, true); assert_eq!(result.supported_candidates, vec![S32]); assert_eq!(result.channel_counts, BTreeSet::from([1, 2, 3, 4])); @@ -544,7 +720,8 @@ mod tests { // A device with a gap, it takes two and four channels but not three. let device = FakeDevice::new(&[48000], 4, &[S16]).without_channels(&[3]); let mut masks = ChannelMaskMap::new(); - let mut result = probe_rate(&device, &mut masks, 48000, [2, 3, 4], CANDIDATE_FORMATS); + let mut result = + probing(&device, &mut masks).rate(48000, [2, 3, 4], CANDIDATE_FORMATS, true); result.formats.retain(|fmt| fmt.get_nchannels() == 4); assert_eq!(result.channel_counts, BTreeSet::from([2, 4])); assert_eq!(result.formats.len(), 1); @@ -554,7 +731,8 @@ mod tests { fn the_full_scan_finds_all_the_supported_combinations() { let device = FakeDevice::new(&[44100, 48000, 96000, 32000], 2, &[S16, S24_3]); let mut masks = ChannelMaskMap::new(); - let mut found: Vec<(u32, u16, u16, u16)> = scan_all_rates(&device, &mut masks, 8) + let mut found: Vec<(u32, u16, u16, u16)> = probing(&device, &mut masks) + .all_rates(8) .iter() .map(describe) .collect(); @@ -576,7 +754,8 @@ mod tests { // 192 kHz is missing, so the 48 kHz family is dropped before 384 kHz. let device = FakeDevice::new(&[48000, 96000, 384000], 2, &[S32]); let mut masks = ChannelMaskMap::new(); - let found: Vec = scan_all_rates(&device, &mut masks, 8) + let found: Vec = probing(&device, &mut masks) + .all_rates(8) .iter() .map(|fmt| fmt.get_samplespersec()) .collect(); @@ -593,7 +772,7 @@ mod tests { fn the_full_scan_limits_the_channel_counts_of_the_later_rates() { let device = FakeDevice::new(&[48000, 44100, 32000], 2, &[S16]); let mut masks = ChannelMaskMap::new(); - scan_all_rates(&device, &mut masks, 8); + probing(&device, &mut masks).all_rates(8); // The first rate probes the full range, the ceiling drops to two after that. assert!(device.queries().iter().any(|q| q.channels == 8)); @@ -608,11 +787,98 @@ mod tests { .any(|q| q.samplerate == 32000 && q.channels > 2)); } + #[test] + fn the_declared_ranges_keep_the_scan_inside_them() { + let device = FakeDevice::new(&[44100, 48000], 2, &[S16, S32]); + let mut masks = ChannelMaskMap::new(); + // The driver only declares two channels, 16 bit, and the two rates. + let ranges = [declared(2, (16, 16), (44100, 48000))]; + let found: Vec<(u32, u16, u16, u16)> = probing_with(&device, &mut masks, &ranges) + .all_rates(DEFAULT_MAX_CHANNELS) + .iter() + .map(describe) + .collect(); + + assert_eq!( + found, + vec![ + (44100, 1, 16, 16), + (44100, 2, 16, 16), + (48000, 1, 16, 16), + (48000, 2, 16, 16) + ] + ); + // Nothing outside the declared ranges is even asked about. + assert!(device + .queries() + .iter() + .all(|q| q.channels <= 2 && q.candidate == S16)); + assert!(!device + .queries() + .iter() + .any(|q| q.samplerate < 44100 || q.samplerate > 48000)); + } + + #[test] + fn the_declared_ranges_find_a_rate_that_the_staged_scan_misses() { + // A device with a hole at 192 kHz, which cuts the staged scan short. + let device = FakeDevice::new(&[48000, 96000, 384000], 2, &[S32]); + let mut masks = ChannelMaskMap::new(); + let staged: Vec = probing(&device, &mut masks) + .all_rates(8) + .iter() + .map(|fmt| fmt.get_samplespersec()) + .collect(); + assert!(!staged.contains(&384000)); + + let device = FakeDevice::new(&[48000, 96000, 384000], 2, &[S32]); + let mut masks = ChannelMaskMap::new(); + let ranges = [declared(2, (16, 32), (48000, 384000))]; + let bounded: Vec = probing_with(&device, &mut masks, &ranges) + .all_rates(8) + .iter() + .map(|fmt| fmt.get_samplespersec()) + .collect(); + assert!(bounded.contains(&384000)); + } + + #[test] + fn the_declared_ranges_keep_all_the_formats_of_every_channel_count() { + // S32 only works with one channel, which the staged scan would narrow away. + let device = FakeDevice::new(&[48000], 4, &[S16, S32]).only_with_one_channel(&[S32]); + let mut masks = ChannelMaskMap::new(); + let ranges = [declared(4, (16, 32), (48000, 48000))]; + let found = probing_with(&device, &mut masks, &ranges).all_rates(4); + assert_eq!(describe(&found[0]), (48000, 1, 16, 16)); + assert_eq!(describe(&found[1]), (48000, 1, 32, 32)); + // Every channel count is probed with all four integer candidates that + // fit in the declared 16 to 32 bits, the float one is left out. + for channels in 1..=4 { + let queries = device.queries_for(48000, channels); + assert_eq!(queries.len(), 4); + assert!(queries + .iter() + .all(|q| q.candidate.sample_type == SampleType::Int)); + } + } + + #[test] + fn every_rate_is_in_the_combined_list() { + let mut combined: Vec = FAMILY_48_RATES + .iter() + .chain(FAMILY_44_RATES) + .chain(REMAINING_RATES) + .copied() + .collect(); + combined.sort_unstable(); + assert_eq!(combined, ALL_RATES); + } + #[test] fn the_full_scan_of_a_device_without_support_finds_nothing() { let device = FakeDevice::new(&[], 0, &[]); let mut masks = ChannelMaskMap::new(); - assert!(scan_all_rates(&device, &mut masks, 2).is_empty()); + assert!(probing(&device, &mut masks).all_rates(2).is_empty()); assert!(masks.is_empty()); // Nothing was found, so the low rates are probed with the full channel range. assert!(device diff --git a/src/dataranges.rs b/src/dataranges.rs new file mode 100644 index 0000000..6b694e4 --- /dev/null +++ b/src/dataranges.rs @@ -0,0 +1,486 @@ +//! Reading the capabilities that a driver declares for a device. +//! +//! A Wasapi endpoint is backed by a pin on a kernel streaming filter, +//! and a WDM audio driver declares what that pin accepts as a set of data ranges. +//! Getting at them means walking the device topology from the endpoint, +//! through the topology filter of the device, to the wave filter that does the streaming, +//! and then querying that filter directly. + +use std::collections::HashSet; +use std::mem::size_of; +use std::ptr::from_ref; + +use windows::core::{Interface, GUID, PCWSTR}; +use windows::Win32::Foundation::{CloseHandle, GENERIC_READ, GENERIC_WRITE, HANDLE}; +use windows::Win32::Media::Audio::{Connector, IConnector, IDeviceTopology, IMMDevice, IPart}; +use windows::Win32::Media::KernelStreaming::{ + KSPROPSETID_Pin, IOCTL_KS_PROPERTY, KSDATAFORMAT_0, KSDATAFORMAT_SUBTYPE_PCM, + KSDATAFORMAT_TYPE_AUDIO, KSIDENTIFIER_0_0, KSMULTIPLE_ITEM, KSPIN_DATAFLOW_IN, + KSPIN_DATAFLOW_OUT, KSPROPERTY_PIN, KSPROPERTY_PIN_CTYPES, KSPROPERTY_PIN_DATAFLOW, + KSPROPERTY_PIN_DATARANGES, KSPROPERTY_TYPE_GET, KSP_PIN, +}; +use windows::Win32::Media::Multimedia::KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; +use windows::Win32::Storage::FileSystem::{ + CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING, +}; +use windows::Win32::System::Com::CLSCTX_ALL; +use windows::Win32::System::IO::DeviceIoControl; + +use crate::{Direction, SampleType, WasapiRes, WaveFormat}; + +/// A KSDATAFORMAT is 64 bytes, the audio fields of a KSDATARANGE_AUDIO follow after it. +const AUDIO_RANGE_SIZE: usize = size_of::() + 5 * size_of::(); + +/// The error codes that mean the reply did not fit in the buffer. +const ERROR_MORE_DATA: u32 = 0x800700EA; +const ERROR_INSUFFICIENT_BUFFER: u32 = 0x8007007A; + +/// One capability range, as declared by a device driver. +/// +/// A range is a cross product and over-reports. +/// A device declaring two to eight channels at 44.1 to 192 kHz +/// is not promising that every combination in that box works, +/// so a range is an upper bound that still has to be confirmed with +/// [is_supported](crate::AudioClient::is_supported). +/// A driver may also declare several ranges, one per format or rate. +/// +/// This mirrors a +/// [KSDATARANGE_AUDIO](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ksmedia/ns-ksmedia-ksdatarange_audio). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct DataRange { + /// The largest number of channels. + pub max_channels: u32, + /// The smallest container size in bits. + pub min_bits_per_sample: u32, + /// The largest container size in bits. + pub max_bits_per_sample: u32, + /// The lowest sample rate. + pub min_samplerate: u32, + /// The highest sample rate. + pub max_samplerate: u32, + /// The subformat, normally PCM or IEEE float. + /// An all zero GUID is a wildcard that matches anything. + pub subformat: GUID, +} + +impl DataRange { + /// Get the sample type of the range. + /// Returns `None` for a wildcard, and for anything that is neither PCM nor float. + pub fn sample_type(&self) -> Option { + match self.subformat { + KSDATAFORMAT_SUBTYPE_PCM => Some(SampleType::Int), + KSDATAFORMAT_SUBTYPE_IEEE_FLOAT => Some(SampleType::Float), + _ => None, + } + } + + /// Check if a format falls inside this range. + /// A range that declares neither PCM nor float only matches on the other properties. + pub fn covers(&self, wave_fmt: &WaveFormat) -> bool { + let samplerate = wave_fmt.get_samplespersec(); + let storebits = wave_fmt.get_bitspersample() as u32; + let matching_type = match (self.sample_type(), wave_fmt.get_subformat()) { + (Some(declared), Ok(wanted)) => declared == wanted, + _ => true, + }; + matching_type + && wave_fmt.get_nchannels() as u32 <= self.max_channels + && (self.min_bits_per_sample..=self.max_bits_per_sample).contains(&storebits) + && (self.min_samplerate..=self.max_samplerate).contains(&samplerate) + } +} + +/// Check if a format falls inside any of the ranges. +/// An empty set of ranges means nothing is known, and everything is then accepted. +pub fn covered_by_any(ranges: &[DataRange], wave_fmt: &WaveFormat) -> bool { + ranges.is_empty() || ranges.iter().any(|range| range.covers(wave_fmt)) +} + +/// Read the data ranges that the driver declares for a device. +/// +/// This only works for devices that are backed by a WDM driver. +/// Devices that are implemented in software, such as some virtual +/// and remote devices, have no kernel streaming filter to ask. +pub(crate) fn read_data_ranges( + device: &IMMDevice, + direction: Direction, +) -> WasapiRes> { + let topology: IDeviceTopology = unsafe { device.Activate(CLSCTX_ALL, None)? }; + let endpoint_side = unsafe { topology.GetConnector(0)? }; + let device_side: IPart = unsafe { endpoint_side.GetConnectedTo()? }.cast()?; + let topology_filter = filter_id(&device_side); + + // The endpoint connects to the topology filter of the device, which holds + // the volume and mute controls. The wave filter that does the streaming is + // on the other side of it, upstream for a render device and downstream for a capture device. + let upstream = matches!(direction, Direction::Render); + let mut wave_filters: Vec<(String, HANDLE)> = Vec::new(); + // Some drivers put the endpoint on a filter of their own, and then the + // wave filter is in the other direction. Try both before giving up. + for upstream in [upstream, !upstream] { + let connectors = reachable_connectors(&device_side, upstream); + debug!( + "Found {} connectors {} of the endpoint", + connectors.len(), + if upstream { "upstream" } else { "downstream" } + ); + for connector in connectors { + let Ok(remote) = (unsafe { connector.GetConnectedTo() }) else { + continue; + }; + let Ok(remote) = remote.cast::() else { + continue; + }; + let Some(filter) = filter_id(&remote) else { + continue; + }; + if Some(&filter) == topology_filter.as_ref() + || wave_filters.iter().any(|(id, _)| *id == filter) + { + continue; + } + match open_filter(&filter) { + Ok(handle) => wave_filters.push((filter, handle)), + Err(err) => debug!("Could not open the filter {filter}, {err}"), + } + } + if !wave_filters.is_empty() { + break; + } + } + // Some drivers have no separate wave filter, and then the streaming pins + // are on the same filter as the endpoint connects to. + if wave_filters.is_empty() { + if let Some(filter) = topology_filter.clone() { + debug!("Found no wave filter, trying the filter of the endpoint itself"); + match open_filter(&filter) { + Ok(handle) => wave_filters.push((filter, handle)), + Err(err) => debug!("Could not open the filter {filter}, {err}"), + } + } + } + + // Take the pins that stream in the direction of the device, + // data goes into a render filter and out of a capture filter. + let wanted_flow = if upstream { + KSPIN_DATAFLOW_IN + } else { + KSPIN_DATAFLOW_OUT + }; + let mut ranges = Vec::new(); + for (id, filter) in &wave_filters { + let pins = query_u32(*filter, &pin_property(KSPROPERTY_PIN_CTYPES, 0)).unwrap_or(0); + debug!("The filter {id} has {pins} pins"); + for pin in 0..pins { + let flow = query_u32(*filter, &pin_property(KSPROPERTY_PIN_DATAFLOW, pin)); + if flow.unwrap_or(0) != wanted_flow.0 as u32 { + continue; + } + match query_bytes(*filter, &pin_property(KSPROPERTY_PIN_DATARANGES, pin)) { + Ok(reply) => { + for range in parse_data_ranges(&reply) { + if !ranges.contains(&range) { + ranges.push(range); + } + } + } + Err(err) => debug!("Could not read the data ranges of pin {pin}, {err}"), + } + } + } + for (_, filter) in wave_filters { + let _ = unsafe { CloseHandle(filter) }; + } + debug!("The driver declares {} data ranges", ranges.len()); + Ok(ranges) +} + +/// Get the device id of the filter that a part belongs to. +fn filter_id(part: &IPart) -> Option { + let topology: IDeviceTopology = unsafe { part.GetTopologyObject() }.ok()?; + unsafe { topology.GetDeviceId().ok()?.to_string() }.ok() +} + +/// Collect the connectors that can be reached from a part, +/// by walking through the subunits of the same filter. +fn reachable_connectors(start: &IPart, upstream: bool) -> Vec { + let mut connectors = Vec::new(); + let mut seen = HashSet::new(); + let mut queue = vec![start.clone()]; + while let Some(part) = queue.pop() { + let Ok(global_id) = (unsafe { part.GetGlobalId() }) else { + continue; + }; + if !seen.insert(unsafe { global_id.to_string() }.unwrap_or_default()) { + continue; + } + let next = if upstream { + unsafe { part.EnumPartsIncoming() } + } else { + unsafe { part.EnumPartsOutgoing() } + }; + let Ok(next) = next else { continue }; + for index in 0..unsafe { next.GetCount() }.unwrap_or(0) { + let Ok(part) = (unsafe { next.GetPart(index) }) else { + continue; + }; + if unsafe { part.GetPartType() } == Ok(Connector) { + if let Ok(connector) = part.cast::() { + connectors.push(connector); + } + } else { + queue.push(part); + } + } + } + connectors +} + +/// Open a kernel streaming filter by its device interface path. +/// The device ids from the topology have a `{2}.` prefix that has to go. +fn open_filter(device_id: &str) -> WasapiRes { + let path = match device_id.find("}.") { + Some(pos) if device_id.starts_with('{') => &device_id[pos + 2..], + _ => device_id, + }; + let wide: Vec = path.encode_utf16().chain(std::iter::once(0)).collect(); + let handle = unsafe { + CreateFileW( + PCWSTR(wide.as_ptr()), + GENERIC_READ.0 | GENERIC_WRITE.0, + FILE_SHARE_READ | FILE_SHARE_WRITE, + None, + OPEN_EXISTING, + FILE_FLAGS_AND_ATTRIBUTES(0), + None, + )? + }; + Ok(handle) +} + +/// Build a pin property request. +fn pin_property(id: KSPROPERTY_PIN, pin_id: u32) -> KSP_PIN { + let mut property = KSP_PIN::default(); + property.Property.Anonymous.Anonymous = KSIDENTIFIER_0_0 { + Set: KSPROPSETID_Pin, + Id: id.0 as u32, + Flags: KSPROPERTY_TYPE_GET, + }; + property.PinId = pin_id; + property +} + +/// Send a property request to an open filter. +/// Returns the number of bytes the driver has, or would have, written. +fn ks_property(filter: HANDLE, property: &KSP_PIN, buffer: Option<&mut [u8]>) -> WasapiRes { + let (data, size) = match buffer { + Some(buffer) => (Some(buffer.as_mut_ptr().cast()), buffer.len() as u32), + None => (None, 0), + }; + let mut returned = 0u32; + let result = unsafe { + DeviceIoControl( + filter, + IOCTL_KS_PROPERTY, + Some(from_ref(property).cast()), + size_of::() as u32, + data, + size, + Some(&mut returned), + None, + ) + }; + match result { + Ok(()) => Ok(returned), + // A buffer that is too small is not a failure here, + // the driver then reports the size it needs. + Err(err) + if matches!( + err.code().0 as u32, + ERROR_MORE_DATA | ERROR_INSUFFICIENT_BUFFER + ) => + { + Ok(returned) + } + Err(err) => Err(err.into()), + } +} + +/// Query a property that returns a single u32. +fn query_u32(filter: HANDLE, property: &KSP_PIN) -> WasapiRes { + let mut buffer = [0u8; size_of::()]; + ks_property(filter, property, Some(&mut buffer))?; + Ok(u32::from_le_bytes(buffer)) +} + +/// Query a property that returns a variable size reply. +/// The first call learns the size, the second one gets the data. +fn query_bytes(filter: HANDLE, property: &KSP_PIN) -> WasapiRes> { + let needed = ks_property(filter, property, None)?; + if needed as usize <= size_of::() { + return Ok(Vec::new()); + } + let mut buffer = vec![0u8; needed as usize]; + let returned = ks_property(filter, property, Some(&mut buffer))?; + buffer.truncate(returned as usize); + Ok(buffer) +} + +/// Pick the audio data ranges out of a KSMULTIPLE_ITEM reply. +/// The reply is a header followed by a list of KSDATARANGE structures +/// of varying size, each padded to a multiple of eight bytes. +fn parse_data_ranges(buffer: &[u8]) -> Vec { + let mut ranges = Vec::new(); + if buffer.len() < size_of::() { + return ranges; + } + let header: KSMULTIPLE_ITEM = unsafe { std::ptr::read_unaligned(buffer.as_ptr().cast()) }; + let mut offset = size_of::(); + for _ in 0..header.Count { + if offset + size_of::() > buffer.len() { + debug!("The list of data ranges is truncated at offset {offset}"); + break; + } + let format: KSDATAFORMAT_0 = + unsafe { std::ptr::read_unaligned(buffer[offset..].as_ptr().cast()) }; + let size = format.FormatSize as usize; + if format.MajorFormat == KSDATAFORMAT_TYPE_AUDIO + && size >= AUDIO_RANGE_SIZE + && offset + AUDIO_RANGE_SIZE <= buffer.len() + { + let field = |nbr: usize| { + let start = offset + size_of::() + 4 * nbr; + u32::from_le_bytes(buffer[start..start + 4].try_into().unwrap()) + }; + ranges.push(DataRange { + max_channels: field(0), + min_bits_per_sample: field(1), + max_bits_per_sample: field(2), + min_samplerate: field(3), + max_samplerate: field(4), + subformat: format.SubFormat, + }); + } + if size == 0 { + debug!("Got a data range of zero size, skipping the rest"); + break; + } + offset += size.div_ceil(8) * 8; + } + ranges +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Lay out a GUID the way it appears in a reply from the driver. + fn guid_bytes(guid: &GUID) -> [u8; 16] { + let mut bytes = [0u8; 16]; + bytes[0..4].copy_from_slice(&guid.data1.to_le_bytes()); + bytes[4..6].copy_from_slice(&guid.data2.to_le_bytes()); + bytes[6..8].copy_from_slice(&guid.data3.to_le_bytes()); + bytes[8..16].copy_from_slice(&guid.data4); + bytes + } + + fn range(channels: u32, bits: (u32, u32), rates: (u32, u32), subformat: GUID) -> DataRange { + DataRange { + max_channels: channels, + min_bits_per_sample: bits.0, + max_bits_per_sample: bits.1, + min_samplerate: rates.0, + max_samplerate: rates.1, + subformat, + } + } + + #[test] + fn a_range_covers_the_formats_inside_it() { + let declared = range(8, (16, 24), (44100, 192000), KSDATAFORMAT_SUBTYPE_PCM); + let inside = WaveFormat::new(24, 24, &SampleType::Int, 96000, 8, None); + assert!(declared.covers(&inside)); + + for outside in [ + WaveFormat::new(24, 24, &SampleType::Int, 96000, 9, None), + WaveFormat::new(32, 32, &SampleType::Int, 96000, 8, None), + WaveFormat::new(16, 16, &SampleType::Int, 22050, 8, None), + WaveFormat::new(16, 16, &SampleType::Int, 384000, 8, None), + WaveFormat::new(24, 24, &SampleType::Float, 96000, 8, None), + ] { + assert!(!declared.covers(&outside), "{outside:?}"); + } + } + + #[test] + fn a_wildcard_range_ignores_the_sample_type() { + let declared = range(2, (16, 16), (48000, 48000), GUID::zeroed()); + assert_eq!(declared.sample_type(), None); + assert!(declared.covers(&WaveFormat::new(16, 16, &SampleType::Int, 48000, 2, None))); + assert!(declared.covers(&WaveFormat::new(16, 16, &SampleType::Float, 48000, 2, None))); + } + + #[test] + fn a_format_is_covered_if_any_range_covers_it() { + let declared = [ + range(2, (24, 24), (48000, 48000), KSDATAFORMAT_SUBTYPE_PCM), + range(8, (16, 16), (44100, 48000), KSDATAFORMAT_SUBTYPE_PCM), + ]; + // Each format is outside one of the ranges but inside the other. + let packed_24 = WaveFormat::new(24, 24, &SampleType::Int, 48000, 2, None); + let eight_channels = WaveFormat::new(16, 16, &SampleType::Int, 44100, 8, None); + assert!(covered_by_any(&declared, &packed_24)); + assert!(covered_by_any(&declared, &eight_channels)); + // A combination that no single range covers. + let both = WaveFormat::new(24, 24, &SampleType::Int, 44100, 8, None); + assert!(!covered_by_any(&declared, &both)); + // Without any ranges nothing is known, so everything passes. + assert!(covered_by_any(&[], &both)); + } + + #[test] + fn a_reply_with_ranges_is_parsed() { + // Two ranges, an audio one and something else that must be skipped. + let mut reply = Vec::new(); + reply.extend_from_slice(&(8u32 + 88 + 72).to_le_bytes()); // Size + reply.extend_from_slice(&2u32.to_le_bytes()); // Count + + let mut audio = Vec::new(); + audio.extend_from_slice(&(AUDIO_RANGE_SIZE as u32).to_le_bytes()); // FormatSize + audio.extend_from_slice(&[0u8; 12]); // Flags, SampleSize, Reserved + audio.extend_from_slice(&guid_bytes(&KSDATAFORMAT_TYPE_AUDIO)); + audio.extend_from_slice(&guid_bytes(&KSDATAFORMAT_SUBTYPE_PCM)); + audio.extend_from_slice(&[0u8; 16]); // Specifier + for value in [6u32, 16, 32, 44100, 192000] { + audio.extend_from_slice(&value.to_le_bytes()); + } + audio.resize(audio.len().div_ceil(8) * 8, 0); + reply.extend_from_slice(&audio); + + let mut other = vec![0u8; 72]; + other[0..4].copy_from_slice(&72u32.to_le_bytes()); + reply.extend_from_slice(&other); + + let parsed = parse_data_ranges(&reply); + assert_eq!(parsed.len(), 1); + assert_eq!( + parsed[0], + range(6, (16, 32), (44100, 192000), KSDATAFORMAT_SUBTYPE_PCM) + ); + } + + #[test] + fn a_short_or_broken_reply_is_handled() { + assert!(parse_data_ranges(&[]).is_empty()); + assert!(parse_data_ranges(&[0u8; 4]).is_empty()); + // A count of one but no range following it. + let mut truncated = 8u32.to_le_bytes().to_vec(); + truncated.extend_from_slice(&1u32.to_le_bytes()); + assert!(parse_data_ranges(&truncated).is_empty()); + // A range of zero size must not loop forever. + let mut zero_size = 200u32.to_le_bytes().to_vec(); + zero_size.extend_from_slice(&50u32.to_le_bytes()); + zero_size.extend_from_slice(&[0u8; 200]); + assert!(parse_data_ranges(&zero_size).is_empty()); + } +} diff --git a/src/lib.rs b/src/lib.rs index e3076bf..480efdd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,11 +2,13 @@ mod api; mod capabilities; +mod dataranges; mod errors; mod events; mod waveformat; pub use api::*; pub use capabilities::*; +pub use dataranges::*; pub use errors::*; pub use events::*; pub use waveformat::*; From e98bacfeb9119ed46925fff0d9e782227b0d7bea Mon Sep 17 00:00:00 2001 From: HEnquist Date: Mon, 17 Aug 2026 22:11:51 +0200 Subject: [PATCH 4/7] Polish the capability probing API and the documentation - CapabilityProbe::new takes a Device and reads the data ranges of its driver. The constructor that took an AudioClient is gone, it forced the caller to hand over a client that the probe then kept. - The max_channels argument is gone as well. The ceiling now comes from the ranges that the driver declares, or DEFAULT_MAX_CHANNELS for a device that declares none, which is not something a caller should have to answer. - Re-export the 18 channel position constants, and document how to build a channel mask and how to read one. - Document the zero mask, the wildcard subformat, the channel ceiling, and that probing works on a device that is in use. - Give every example a description at the top. --- README.md | 9 +- examples/aec.rs | 6 ++ examples/capabilities.rs | 18 ++-- examples/dataranges.rs | 21 ++-- examples/device_notifications.rs | 8 +- examples/devices.rs | 3 + examples/loopback.rs | 5 + examples/playnoise_exclusive.rs | 7 ++ examples/playnoise_exclusive_poll.rs | 7 ++ examples/playsine.rs | 4 + examples/playsine_events.rs | 6 ++ examples/playsine_poll.rs | 5 + examples/processes.rs | 4 + examples/record.rs | 6 ++ examples/record_application.rs | 6 ++ src/api.rs | 32 ++++-- src/capabilities.rs | 141 ++++++++++++++++----------- src/dataranges.rs | 50 +++++----- src/waveformat.rs | 84 ++++++++++++++-- 19 files changed, 302 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index b574e44..b8e7ca0 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,10 @@ The following is a selection of the functionality currently available in the lib - Reading the capabilities that a driver declares for a device - …and additional features beyond this list - +The sharing modes (shared and exclusive) and timing modes (event-driven and polled) are described in +the documentation of +[`AudioClient::initialize_client`](https://docs.rs/wasapi/latest/wasapi/struct.AudioClient.html#method.initialize_client), +including how to choose between them. ## Included examples @@ -43,5 +46,5 @@ The following is a selection of the functionality currently available in the lib | `record_application` | Records audio from a single application, and saves the raw samples to a file. | | `aec` | Captures audio with Acoustic Echo Cancellation (AEC) enabled and saves the raw data to a file. | | `device_notifications` | Listens for devices being added, removed or changed, and for changes of the default device. | -| `capabilities` | Scans the default output device for the formats it supports in exclusive mode. | -| `dataranges` | Prints the capabilities each device driver declares, and verifies them with a scan. | +| `capabilities` | Lists the formats a single output device supports in exclusive mode. | +| `dataranges` | Checks the capabilities every driver declares against what its device really accepts. | diff --git a/examples/aec.rs b/examples/aec.rs index c2a0d92..55dc644 100644 --- a/examples/aec.rs +++ b/examples/aec.rs @@ -1,3 +1,9 @@ +// Record audio with Acoustic Echo Cancellation (AEC) enabled, +// and save the raw samples to the file 'aec-recorded.raw'. +// +// The AEC effect is applied by setting the stream category to +// communications, and capturing from the default communications device. + use std::collections::VecDeque; use std::error; use std::fs::File; diff --git a/examples/capabilities.rs b/examples/capabilities.rs index 764dc4a..c36fce5 100644 --- a/examples/capabilities.rs +++ b/examples/capabilities.rs @@ -1,3 +1,9 @@ +// Scan an output device for the formats it supports in exclusive mode. +// Give a device name as an argument, or nothing to use the default device. +// +// See the dataranges example to check what a driver declares +// against what its device really accepts. + use std::collections::BTreeMap; use std::time::Instant; use wasapi::*; @@ -19,7 +25,6 @@ fn format_name(wave_fmt: &WaveFormat) -> String { } } -// Scan the default output device for the formats it supports in exclusive mode. fn main() { let _ = SimpleLogger::init( LevelFilter::Info, @@ -42,19 +47,16 @@ fn main() { .unwrap(), None => enumerator.get_default_device(&Direction::Render).unwrap(), }; - println!( - "Scanning device {:?}, this takes a while..", - device.get_friendlyname().unwrap() - ); + println!("Scanning device {:?}..", device.get_friendlyname().unwrap()); // This uses the capabilities that the driver declares, when it has any. - let mut probe = CapabilityProbe::for_device(&device).unwrap(); + let mut probe = CapabilityProbe::new(&device).unwrap(); if !probe.data_ranges().is_empty() { println!("The driver declares {} ranges.", probe.data_ranges().len()); } let start = Instant::now(); - let formats = probe.supported_formats_all_rates(DEFAULT_MAX_CHANNELS); - println!("The scan took {:.1} s.", start.elapsed().as_secs_f32()); + let formats = probe.supported_formats_all_rates(); + println!("The scan took {} ms.", start.elapsed().as_millis()); // Group the formats by channel count and sample rate. let mut grouped: BTreeMap>> = BTreeMap::new(); diff --git a/examples/dataranges.rs b/examples/dataranges.rs index 8e5e1c9..8961fe0 100644 --- a/examples/dataranges.rs +++ b/examples/dataranges.rs @@ -1,10 +1,13 @@ // Compare the capabilities that a driver declares with what the device really accepts. // -// For every active output device this prints the declared data ranges, +// For every active output and input device this prints the declared data ranges, // then runs a full scan both with and without them, and compares the results. // It answers two questions, whether the declared ranges can be trusted, // and how much they help. // +// This is a check of the data ranges themselves. +// To simply list what a device supports, use the capabilities example instead. +// // Give a substring of a device name as an argument to only check the matching devices. use std::collections::BTreeSet; @@ -31,10 +34,10 @@ fn describe(wave_fmt: &WaveFormat) -> Described { ) } -fn scan(probe: &mut CapabilityProbe) -> (BTreeSet, f32) { +fn scan(probe: &mut CapabilityProbe) -> (BTreeSet, u128) { let start = Instant::now(); - let formats = probe.supported_formats_all_rates(DEFAULT_MAX_CHANNELS); - let elapsed = start.elapsed().as_secs_f32(); + let formats = probe.supported_formats_all_rates(); + let elapsed = start.elapsed().as_millis(); (formats.iter().map(describe).collect(), elapsed) } @@ -96,21 +99,23 @@ fn main() { } }; - let mut plain = CapabilityProbe::new(device.get_iaudioclient().unwrap()); + // Clear the ranges to get the staged scan, for comparison. + let mut plain = CapabilityProbe::new(&device).unwrap(); + plain.set_data_ranges(Vec::new()); let (staged, staged_time) = scan(&mut plain); println!( - " the staged scan found {} formats in {staged_time:.2} s", + " the staged scan found {} formats in {staged_time} ms", staged.len() ); if ranges.is_empty() { continue; } - let mut bounded = CapabilityProbe::new(device.get_iaudioclient().unwrap()); + let mut bounded = CapabilityProbe::new(&device).unwrap(); bounded.set_data_ranges(ranges); let (found, found_time) = scan(&mut bounded); println!( - " the bounded scan found {} formats in {found_time:.2} s", + " the bounded scan found {} formats in {found_time} ms", found.len() ); diff --git a/examples/device_notifications.rs b/examples/device_notifications.rs index de30832..01facf6 100644 --- a/examples/device_notifications.rs +++ b/examples/device_notifications.rs @@ -1,10 +1,12 @@ +// Listen to device change notifications for one minute. +// +// Plug or unplug a device, or change the default device in the +// Windows sound settings, to see the notifications arrive. + use std::thread; use std::time::Duration; use wasapi::*; -// Listen to device change notifications for one minute. -// Plug or unplug a device, or change the default device in the -// Windows sound settings, to see the notifications arrive. fn main() { initialize_mta().unwrap(); diff --git a/examples/devices.rs b/examples/devices.rs index 251f372..ee926f8 100644 --- a/examples/devices.rs +++ b/examples/devices.rs @@ -1,3 +1,6 @@ +// List all available audio devices with their state, +// and show the default device for each role. + use wasapi::*; fn main() { diff --git a/examples/loopback.rs b/examples/loopback.rs index 2d72949..647ba63 100644 --- a/examples/loopback.rs +++ b/examples/loopback.rs @@ -1,3 +1,8 @@ +// Capture and render sound simultaneously. +// +// Loops audio back from the default input device to the default output device, +// with separate threads for capture and render that are connected by a channel. + use std::collections::VecDeque; use std::error; use std::sync::mpsc; diff --git a/examples/playnoise_exclusive.rs b/examples/playnoise_exclusive.rs index 839eba7..520eaa4 100644 --- a/examples/playnoise_exclusive.rs +++ b/examples/playnoise_exclusive.rs @@ -1,3 +1,10 @@ +// Play white noise in exclusive mode on the default output device. +// +// Shows how to handle the HRESULT errors that initializing an +// exclusive mode stream can return. +// Uses event driven timing mode, see the playnoise_exclusive_poll +// example for polling. + use rand::prelude::*; use wasapi::*; diff --git a/examples/playnoise_exclusive_poll.rs b/examples/playnoise_exclusive_poll.rs index 9ad656c..b753360 100644 --- a/examples/playnoise_exclusive_poll.rs +++ b/examples/playnoise_exclusive_poll.rs @@ -1,3 +1,10 @@ +// Play white noise in exclusive mode on the default output device, +// using polling instead of event driven timing mode. +// +// Shows how to handle the HRESULT errors that initializing an +// exclusive mode stream can return. +// See the playnoise_exclusive example for the event driven version. + use rand::prelude::*; use std::{thread, time}; use wasapi::*; diff --git a/examples/playsine.rs b/examples/playsine.rs index 12b600f..44eb34c 100644 --- a/examples/playsine.rs +++ b/examples/playsine.rs @@ -1,3 +1,7 @@ +// Play a sine wave in shared mode on the default output device. +// +// Uses event driven timing mode, see the playsine_poll example for polling. + use std::f64::consts::PI; use wasapi::*; diff --git a/examples/playsine_events.rs b/examples/playsine_events.rs index 742ed68..1dc6a51 100644 --- a/examples/playsine_events.rs +++ b/examples/playsine_events.rs @@ -1,3 +1,9 @@ +// Play a sine wave in shared mode on the default output device, +// while listening to session notifications. +// +// Change the volume or mute the stream in the Windows volume mixer +// to see the notifications arrive. + use std::f64::consts::PI; use wasapi::*; diff --git a/examples/playsine_poll.rs b/examples/playsine_poll.rs index 51c81d7..249b8a4 100644 --- a/examples/playsine_poll.rs +++ b/examples/playsine_poll.rs @@ -1,3 +1,8 @@ +// Play a sine wave in shared mode on the default output device, +// using polling instead of event driven timing mode. +// +// See the playsine example for the event driven version. + use std::f64::consts::PI; use std::{thread, time}; use wasapi::*; diff --git a/examples/processes.rs b/examples/processes.rs index 4085068..9271980 100644 --- a/examples/processes.rs +++ b/examples/processes.rs @@ -1,3 +1,7 @@ +// List all audio devices and the processes that are using them. +// +// Prints the peak level of each device, and of every active session on it. + use wasapi::*; fn main() { diff --git a/examples/record.rs b/examples/record.rs index 76fba69..a5fa58d 100644 --- a/examples/record.rs +++ b/examples/record.rs @@ -1,3 +1,9 @@ +// Record audio from the default input device, and save the raw samples +// to the file 'recorded.raw'. +// +// The capture runs in a separate thread that sends the samples to the +// main thread over a channel. + use std::collections::VecDeque; use std::error; use std::fs::File; diff --git a/examples/record_application.rs b/examples/record_application.rs index 680a155..7388c6b 100644 --- a/examples/record_application.rs +++ b/examples/record_application.rs @@ -1,3 +1,9 @@ +// Record audio from a single application, and save the raw samples +// to the file 'recorded.raw'. +// +// This example captures from Firefox, edit the process name in main() +// to capture from another application. + use std::collections::VecDeque; use std::error::{self}; use std::ffi::OsStr; diff --git a/src/api.rs b/src/api.rs index b16318d..dd68b96 100644 --- a/src/api.rs +++ b/src/api.rs @@ -191,6 +191,10 @@ impl From for ERole { /// There are four main modes that can be specified, /// corresponding to the four possible combinations of sharing mode and timing. /// The enum variants only expose the parameters that can be set in each mode. +/// +/// See the documentation of [AudioClient::initialize_client()] +/// for a description of the sharing and timing modes, +/// and how to choose between them. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum StreamMode { /// Shared mode using polling for timing. @@ -221,14 +225,18 @@ pub enum StreamMode { EventsExclusive { period_hns: i64 }, } -/// Sharemode for device +/// Sharemode for device. +/// See the documentation of [AudioClient::initialize_client()] +/// for a description of the two sharing modes. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ShareMode { Shared, Exclusive, } -/// Timing mode for device +/// Timing mode for device. +/// See the documentation of [AudioClient::initialize_client()] +/// for a description of the two timing modes. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum TimingMode { Polling, @@ -570,15 +578,20 @@ impl Device { }) } - /// Get the [DataRange]s that the driver declares for this device. + /// Get the [DataRange](crate::DataRange)s that the driver declares for this device. /// /// The ranges are an upper bound on what the device supports, /// see [DataRange](crate::DataRange) for the details and the limitations. /// They can be used to narrow down the search of a [CapabilityProbe](crate::CapabilityProbe). /// - /// This only works for devices that are backed by a driver with a - /// kernel streaming filter. Devices that are implemented in software - /// have nothing to ask, and then this returns an error or an empty list. + /// This needs a device that is backed by a driver with a kernel streaming filter. + /// Being virtual is no obstacle, a virtual cable with a normal driver + /// declares its ranges like any sound card does. + /// A device without such a filter, a remote desktop endpoint for instance, + /// has nothing to ask, and then this returns an error or an empty list. + /// + /// This works even for a device that cannot be opened for streaming, + /// an unplugged headset for example, since it asks the driver and not the endpoint. pub fn get_data_ranges(&self) -> WasapiRes> { crate::dataranges::read_data_ranges(&self.device, self.direction) } @@ -966,12 +979,15 @@ impl AudioClient { /// - If one or two channels, try with the format as WAVEFORMATEX. /// This is skipped for formats that a WAVEFORMATEX cannot describe without ambiguity, /// such as 24 bit samples, see [WaveFormat::to_waveformatex]. - /// - Try with different channel masks: + /// - Try with different channel masks, see [make_channelmasks]: /// - If channels <= 8: Recommended mask(s) from ksmedia.h. /// - If channels <= 18: Simple mask. - /// - Zero mask. + /// - Zero mask, which assigns no speaker positions. + /// Few devices accept it, but for some it is the only one that works. /// /// If an accepted format is found, this is returned. + /// The returned format carries the mask that was accepted, which may differ + /// from the one that was asked for. /// An error means no accepted format was found. pub fn is_supported_exclusive_with_quirks( &self, diff --git a/src/capabilities.rs b/src/capabilities.rs index c0143d5..6675f48 100644 --- a/src/capabilities.rs +++ b/src/capabilities.rs @@ -5,7 +5,8 @@ use std::collections::{BTreeSet, HashMap}; use crate::{covered_by_any, AudioClient, DataRange, Device, SampleType, WasapiRes, WaveFormat}; use windows::Win32::Media::KernelStreaming::WAVE_FORMAT_EXTENSIBLE; -/// The channel count ceiling used when nothing better is known. +/// The channel count ceiling that a scan uses for a device +/// that declares no [DataRange]s of its own. pub const DEFAULT_MAX_CHANNELS: usize = 32; // Standard rates in each family, from the base rate upward through the multiples. @@ -91,9 +92,16 @@ impl FormatChecker for AudioClient { /// which avoids repeating the mask renegotiation of /// [is_supported_exclusive_with_quirks](AudioClient::is_supported_exclusive_with_quirks). /// +/// The channel counts run from one up to a ceiling. +/// With [DataRange]s that ceiling is the largest channel count the driver declares, +/// which is exact. Without them it is [DEFAULT_MAX_CHANNELS], a guess that is +/// deliberately generous, since a channel count above it would go unnoticed. +/// The staged scan below then lowers it to the highest count that worked, +/// as soon as any rate succeeds. +/// /// ## With the ranges the driver declares /// -/// When the probe has [DataRange]s, from [CapabilityProbe::for_device] or +/// When the probe has [DataRange]s, from [CapabilityProbe::new] or /// [CapabilityProbe::set_data_ranges], they give real bounds on the rates, /// channel counts and sample formats. /// The scan then only asks about the combinations that fall inside them, @@ -107,7 +115,13 @@ impl FormatChecker for AudioClient { /// /// ## Without them /// -/// A device that declares nothing gets a staged scan that guesses instead: +/// A probe without ranges, because the device declares none or because they were +/// cleared with [CapabilityProbe::set_data_ranges], falls back to a staged scan +/// that guesses instead. +/// Reading the ranges means walking the topology of the device, +/// and drivers build those in ways that are hard to cover in full, +/// so this is what keeps a device that cannot be walked from +/// looking like a device without any capabilities: /// /// - The 48 kHz and 44.1 kHz families are probed interleaved from the base rate upward. /// The first hit establishes an upper channel count limit, @@ -139,20 +153,25 @@ impl FormatChecker for AudioClient { /// A device may well accept several masks for the same channel count, /// for example both of the 5.1 layouts for six channels, /// but the probing stops at the first one and the others are never tried. -/// Use [is_supported_exclusive_with_quirks](AudioClient::is_supported_exclusive_with_quirks) -/// directly to find out whether a specific layout is accepted. +/// +/// To find every layout a device accepts, build the formats with +/// [WaveFormat::new] and a mask from [make_channelmasks](crate::make_channelmasks), +/// and query them one by one with [AudioClient::is_supported]. +/// That function returns the masks that are worth trying for a channel count, +/// with the most likely one first, and a mask of your own is built from the +/// [SPEAKER_FRONT_LEFT](crate::SPEAKER_FRONT_LEFT) and friends constants. /// /// ```no_run -/// use wasapi::{CapabilityProbe, Direction, DeviceEnumerator, DEFAULT_MAX_CHANNELS}; +/// use wasapi::{CapabilityProbe, Direction, DeviceEnumerator}; /// # fn main() -> Result<(), Box> { /// let device = DeviceEnumerator::new()?.get_default_device(&Direction::Render)?; -/// let mut probe = CapabilityProbe::for_device(&device)?; +/// let mut probe = CapabilityProbe::new(&device)?; /// -/// // Everything the device accepts at 48 kHz, for up to eight channels. -/// let formats = probe.supported_formats_at_rate(48000, 8); +/// // Everything the device accepts at 48 kHz. +/// let formats = probe.supported_formats_at_rate(48000); /// /// // Everything the device accepts, at any rate. -/// let all = probe.supported_formats_all_rates(DEFAULT_MAX_CHANNELS); +/// let all = probe.supported_formats_all_rates(); /// # Ok(()) /// # } /// ``` @@ -163,27 +182,16 @@ pub struct CapabilityProbe { } impl CapabilityProbe { - /// Create a new probe for the device of the given [AudioClient]. + /// Create a new probe for a [Device]. /// - /// The client must not have been initialized, - /// and it can not be used for streaming while the probing runs. + /// This gets an [AudioClient] of its own for the device, and reads the + /// [DataRange]s that the driver declares, which are used to narrow down the search. + /// A device that declares none, see [Device::get_data_ranges], + /// gets the staged scan instead. /// - /// Use [CapabilityProbe::for_device] instead to get the faster and - /// more thorough scan that the declared capabilities of the driver allow. - pub fn new(client: AudioClient) -> Self { - CapabilityProbe { - client, - channel_masks: ChannelMaskMap::new(), - data_ranges: Vec::new(), - } - } - - /// Create a new probe for a [Device], using the [DataRange]s - /// that its driver declares to narrow down the search. - /// - /// Falls back to a probe without any ranges if the device has none, - /// which is the case for devices that are implemented in software. - pub fn for_device(device: &Device) -> WasapiRes { + /// The probing only queries the client it holds, and never initializes it, + /// so it does not interfere with a client used for streaming. + pub fn new(device: &Device) -> WasapiRes { let client = device.get_iaudioclient()?; let data_ranges = device.get_data_ranges().unwrap_or_else(|err| { debug!("Could not read the data ranges of the device, {err}"); @@ -203,15 +211,12 @@ impl CapabilityProbe { } /// Set the [DataRange]s the probe uses to narrow down the search. + /// An empty list turns the narrowing off, + /// which is the way to ignore what the driver declares. pub fn set_data_ranges(&mut self, data_ranges: Vec) { self.data_ranges = data_ranges; } - /// Get a reference to the [AudioClient] the probe was created with. - pub fn client(&self) -> &AudioClient { - &self.client - } - /// Get the formats the device accepts at the given sample rate and channel count. /// /// This is the cheapest probe, at most one query per sample format. @@ -224,30 +229,38 @@ impl CapabilityProbe { } /// Get the formats the device accepts at the given sample rate, - /// for every channel count from one up to and including `max_channels`. - pub fn supported_formats_at_rate( - &mut self, - samplerate: usize, - max_channels: usize, - ) -> Vec { + /// for every channel count the device can have. + /// + /// The channel counts go up to the ceiling described in the + /// [struct documentation](CapabilityProbe). + /// A single rate gives nothing to learn from, unlike the full scan, + /// so a device that declares no [DataRange]s is probed all the way up to + /// [DEFAULT_MAX_CHANNELS] here. + /// Use [CapabilityProbe::supported_formats] instead + /// when only one channel count is of interest. + pub fn supported_formats_at_rate(&mut self, samplerate: usize) -> Vec { let narrow = self.data_ranges.is_empty(); - self.probing() - .rate(samplerate, 1..=max_channels, CANDIDATE_FORMATS, narrow) + let mut probing = self.probing(); + let ceiling = probing.channel_ceiling(); + probing + .rate(samplerate, 1..=ceiling, CANDIDATE_FORMATS, narrow) .formats } /// Get the formats the device accepts at any of the standard sample rates, - /// for channel counts up to and including `max_channels`. + /// for every channel count the device can have, + /// see the [struct documentation](CapabilityProbe) for the ceiling that is used. /// - /// This is the full scan. It is the most expensive probe by far. + /// This is the full scan, and the most expensive probe by far. /// Without any [DataRange]s it is also the one that leans hardest /// on the pruning heuristics, see the [struct documentation](CapabilityProbe). - /// Pass [DEFAULT_MAX_CHANNELS] unless the channel count is known to be lower. - pub fn supported_formats_all_rates(&mut self, max_channels: usize) -> Vec { - self.probing().all_rates(max_channels) + pub fn supported_formats_all_rates(&mut self) -> Vec { + let mut probing = self.probing(); + let ceiling = probing.channel_ceiling(); + probing.all_rates(ceiling) } - /// Borrow the parts that the probing needs. + /// Borrow the client, the channel mask cache and the data ranges as a [Probing]. fn probing(&mut self) -> Probing<'_, AudioClient> { Probing { checker: &self.client, @@ -257,7 +270,11 @@ impl CapabilityProbe { } } -/// The state that is shared between the probes. +/// The state that the probing logic works on, borrowed from a [CapabilityProbe]. +/// +/// The logic lives here instead of directly on [CapabilityProbe] so that it can be +/// generic over [FormatChecker]. That is what lets the unit tests run the real +/// pruning logic against a fake device, since the real one needs hardware. struct Probing<'a, C: FormatChecker> { checker: &'a C, channel_masks: &'a mut ChannelMaskMap, @@ -338,8 +355,7 @@ impl Probing<'_, C> { }; let mut narrowed: Option> = None; for channels in channel_counts { - let active = narrowed.clone(); - let active = active.as_deref().unwrap_or(candidates); + let active = narrowed.as_deref().unwrap_or(candidates); let supported = self.formats(samplerate, channels, active); if supported.is_empty() { trace!("No supported formats at {samplerate} Hz, {channels} ch"); @@ -366,12 +382,23 @@ impl Probing<'_, C> { result } + /// The highest channel count to probe. + /// The declared ranges give a real bound, and without them + /// there is nothing better than a generous guess. + fn channel_ceiling(&self) -> usize { + self.data_ranges + .iter() + .map(|range| range.max_channels as usize) + .max() + .unwrap_or(DEFAULT_MAX_CHANNELS) + } + /// Probe all the standard rates, up to the given channel count. - fn all_rates(&mut self, max_channels: usize) -> Vec { + fn all_rates(&mut self, ceiling: usize) -> Vec { if !self.data_ranges.is_empty() { - return self.all_rates_within_ranges(max_channels); + return self.all_rates_within_ranges(ceiling); } - self.all_rates_staged(max_channels) + self.all_rates_staged(ceiling) } /// Probe the rates and channel counts that the driver declares support for. @@ -436,8 +463,7 @@ impl Probing<'_, C> { } else { max_channels }; - let candidates = learned.clone(); - let candidates = candidates.as_deref().unwrap_or(CANDIDATE_FORMATS); + let candidates = learned.as_deref().unwrap_or(CANDIDATE_FORMATS); let result = self.rate(rate, 1..=limit, candidates, true); if let Some(&highest) = result.channel_counts.iter().next_back() { hit[family_nbr] = true; @@ -473,8 +499,7 @@ impl Probing<'_, C> { channel_counts.iter().copied().collect() }; for &rate in REMAINING_RATES { - let candidates = learned.clone(); - let candidates = candidates.as_deref().unwrap_or(CANDIDATE_FORMATS); + let candidates = learned.as_deref().unwrap_or(CANDIDATE_FORMATS); let result = self.rate(rate, remaining_counts.iter().copied(), candidates, true); if learned.is_none() && !result.supported_candidates.is_empty() { debug!( diff --git a/src/dataranges.rs b/src/dataranges.rs index 6b694e4..e8a531c 100644 --- a/src/dataranges.rs +++ b/src/dataranges.rs @@ -10,8 +10,10 @@ use std::collections::HashSet; use std::mem::size_of; use std::ptr::from_ref; -use windows::core::{Interface, GUID, PCWSTR}; -use windows::Win32::Foundation::{CloseHandle, GENERIC_READ, GENERIC_WRITE, HANDLE}; +use windows::core::{Interface, GUID, HRESULT, PCWSTR}; +use windows::Win32::Foundation::{ + CloseHandle, ERROR_INSUFFICIENT_BUFFER, ERROR_MORE_DATA, GENERIC_READ, GENERIC_WRITE, HANDLE, +}; use windows::Win32::Media::Audio::{Connector, IConnector, IDeviceTopology, IMMDevice, IPart}; use windows::Win32::Media::KernelStreaming::{ KSPROPSETID_Pin, IOCTL_KS_PROPERTY, KSDATAFORMAT_0, KSDATAFORMAT_SUBTYPE_PCM, @@ -31,9 +33,9 @@ use crate::{Direction, SampleType, WasapiRes, WaveFormat}; /// A KSDATAFORMAT is 64 bytes, the audio fields of a KSDATARANGE_AUDIO follow after it. const AUDIO_RANGE_SIZE: usize = size_of::() + 5 * size_of::(); -/// The error codes that mean the reply did not fit in the buffer. -const ERROR_MORE_DATA: u32 = 0x800700EA; -const ERROR_INSUFFICIENT_BUFFER: u32 = 0x8007007A; +/// The errors that mean the reply did not fit in the buffer. +const MORE_DATA: HRESULT = HRESULT::from_win32(ERROR_MORE_DATA.0); +const INSUFFICIENT_BUFFER: HRESULT = HRESULT::from_win32(ERROR_INSUFFICIENT_BUFFER.0); /// One capability range, as declared by a device driver. /// @@ -59,13 +61,20 @@ pub struct DataRange { /// The highest sample rate. pub max_samplerate: u32, /// The subformat, normally PCM or IEEE float. - /// An all zero GUID is a wildcard that matches anything. + /// An all zero GUID is a wildcard, see [DataRange::sample_type]. pub subformat: GUID, } impl DataRange { - /// Get the sample type of the range. - /// Returns `None` for a wildcard, and for anything that is neither PCM nor float. + /// Get the sample type of the range, if it has one. + /// + /// This returns `None` in two cases. + /// The first is a wildcard, an all zero GUID, + /// `KSDATAFORMAT_SUBTYPE_WILDCARD` in ksmedia.h. + /// A driver declares a wildcard for a property it does not want to restrict, + /// so a wildcard subformat means the pin takes any of them. + /// The second is a subformat that is neither PCM nor float, + /// a compressed one for instance, which has no [SampleType] to map to. pub fn sample_type(&self) -> Option { match self.subformat { KSDATAFORMAT_SUBTYPE_PCM => Some(SampleType::Int), @@ -75,7 +84,12 @@ impl DataRange { } /// Check if a format falls inside this range. - /// A range that declares neither PCM nor float only matches on the other properties. + /// + /// A range without a sample type of its own, see [DataRange::sample_type], + /// is matched on the other properties alone. + /// A range that cannot be interpreted then never excludes a format, + /// which is the safe direction to err in, + /// since the cost is a query that comes back negative. pub fn covers(&self, wave_fmt: &WaveFormat) -> bool { let samplerate = wave_fmt.get_samplespersec(); let storebits = wave_fmt.get_bitspersample() as u32; @@ -92,15 +106,14 @@ impl DataRange { /// Check if a format falls inside any of the ranges. /// An empty set of ranges means nothing is known, and everything is then accepted. -pub fn covered_by_any(ranges: &[DataRange], wave_fmt: &WaveFormat) -> bool { +pub(crate) fn covered_by_any(ranges: &[DataRange], wave_fmt: &WaveFormat) -> bool { ranges.is_empty() || ranges.iter().any(|range| range.covers(wave_fmt)) } /// Read the data ranges that the driver declares for a device. /// -/// This only works for devices that are backed by a WDM driver. -/// Devices that are implemented in software, such as some virtual -/// and remote devices, have no kernel streaming filter to ask. +/// This needs a device with a kernel streaming filter behind it. +/// A device without one has nothing to ask, and gets an empty list. pub(crate) fn read_data_ranges( device: &IMMDevice, direction: Direction, @@ -151,7 +164,7 @@ pub(crate) fn read_data_ranges( // Some drivers have no separate wave filter, and then the streaming pins // are on the same filter as the endpoint connects to. if wave_filters.is_empty() { - if let Some(filter) = topology_filter.clone() { + if let Some(filter) = topology_filter { debug!("Found no wave filter, trying the filter of the endpoint itself"); match open_filter(&filter) { Ok(handle) => wave_filters.push((filter, handle)), @@ -294,14 +307,7 @@ fn ks_property(filter: HANDLE, property: &KSP_PIN, buffer: Option<&mut [u8]>) -> Ok(()) => Ok(returned), // A buffer that is too small is not a failure here, // the driver then reports the size it needs. - Err(err) - if matches!( - err.code().0 as u32, - ERROR_MORE_DATA | ERROR_INSUFFICIENT_BUFFER - ) => - { - Ok(returned) - } + Err(err) if matches!(err.code(), MORE_DATA | INSUFFICIENT_BUFFER) => Ok(returned), Err(err) => Err(err.into()), } } diff --git a/src/waveformat.rs b/src/waveformat.rs index b447aa8..275b73b 100644 --- a/src/waveformat.rs +++ b/src/waveformat.rs @@ -4,15 +4,20 @@ use windows::{ Win32::Media::Audio::{ WAVEFORMATEX, WAVEFORMATEXTENSIBLE, WAVEFORMATEXTENSIBLE_0, WAVE_FORMAT_PCM, }, - Win32::Media::KernelStreaming::{ - KSDATAFORMAT_SUBTYPE_PCM, SPEAKER_BACK_CENTER, SPEAKER_BACK_LEFT, SPEAKER_BACK_RIGHT, - SPEAKER_FRONT_CENTER, SPEAKER_FRONT_LEFT, SPEAKER_FRONT_LEFT_OF_CENTER, - SPEAKER_FRONT_RIGHT, SPEAKER_FRONT_RIGHT_OF_CENTER, SPEAKER_LOW_FREQUENCY, - SPEAKER_SIDE_LEFT, SPEAKER_SIDE_RIGHT, WAVE_FORMAT_EXTENSIBLE, - }, + Win32::Media::KernelStreaming::{KSDATAFORMAT_SUBTYPE_PCM, WAVE_FORMAT_EXTENSIBLE}, Win32::Media::Multimedia::{KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, WAVE_FORMAT_IEEE_FLOAT}, }; +/// The [18 defined channel positions](https://docs.microsoft.com/en-us/windows/win32/api/mmreg/ns-mmreg-waveformatextensible) +/// of a channel mask, see [make_channelmasks] for how to use them. +pub use windows::Win32::Media::KernelStreaming::{ + SPEAKER_BACK_CENTER, SPEAKER_BACK_LEFT, SPEAKER_BACK_RIGHT, SPEAKER_FRONT_CENTER, + SPEAKER_FRONT_LEFT, SPEAKER_FRONT_LEFT_OF_CENTER, SPEAKER_FRONT_RIGHT, + SPEAKER_FRONT_RIGHT_OF_CENTER, SPEAKER_LOW_FREQUENCY, SPEAKER_SIDE_LEFT, SPEAKER_SIDE_RIGHT, + SPEAKER_TOP_BACK_CENTER, SPEAKER_TOP_BACK_LEFT, SPEAKER_TOP_BACK_RIGHT, SPEAKER_TOP_CENTER, + SPEAKER_TOP_FRONT_CENTER, SPEAKER_TOP_FRONT_LEFT, SPEAKER_TOP_FRONT_RIGHT, +}; + use crate::{SampleType, WasapiError, WasapiRes}; // Definitions from ksmedia.h of the windows sdk. @@ -139,7 +144,9 @@ impl WaveFormat { /// Build a [WAVEFORMATEXTENSIBLE](https://docs.microsoft.com/en-us/windows/win32/api/mmreg/ns-mmreg-waveformatextensible) struct for the given parameters. /// `channel_mask` is optional. If a mask is provided, it will be used. If not, a default mask will be created. /// This can be used to work around quirks for some device drivers. - /// If the default is not accepted, try again using a zero mask, `Some(0)`. + /// If the default is not accepted, try again using a zero mask, `Some(0)`, + /// which assigns no speaker positions. + /// See [make_channelmasks] for the masks that are worth trying, and in which order. pub fn new( storebits: usize, validbits: usize, @@ -295,6 +302,9 @@ impl WaveFormat { } /// Read dwChannelMask. + /// + /// The mask is a bit field of channel positions, + /// see [make_channelmasks] for how to read one. pub fn get_dwchannelmask(&self) -> u32 { self.wave_fmt.dwChannelMask } @@ -317,8 +327,61 @@ impl From for WaveFormat { } /// Return a vector with suggested channel masks for the given number of channels. -/// Used to find a format that a device accepts in exclusive mode. -/// The values are sorted according to how likely they are to be accepted, with the most likely first. +/// +/// Channel masks are one of the more awkward corners of Wasapi. +/// A mask is meant to describe where the channels are supposed to end up, +/// but in exclusive mode it also decides whether the device accepts the format at all, +/// and drivers do not agree on which masks are acceptable. +/// Since there is no way of asking a device what it wants, +/// finding a mask it likes comes down to trying them until one is accepted. +/// +/// This function gives the list worth trying for a channel count, +/// sorted according to how likely they are to be accepted, with the most likely first. +/// The masks are the recommended layouts from ksmedia.h where there is one, +/// then a simple mask with the lowest bits set, and last a zero mask. +/// +/// The zero mask at the end is a special case. +/// It assigns no speaker positions at all, `KSAUDIO_SPEAKER_DIRECTOUT` in ksmedia.h, +/// and leaves it unspecified where the channels are meant to end up. +/// It is last because few devices accept it, so it is only worth trying +/// when nothing else works, but for some devices it is the only one that works. +/// Which mask a device accepts can also differ between its channel counts, +/// so a mask that was accepted for two channels is no promise for six. +/// +/// A mask is a bit field of channel positions, so one is built by or-ing +/// the [SPEAKER_FRONT_LEFT] and friends constants together, +/// and a position is tested for with an and. +/// Build one yourself to ask a device about a layout that is not in the list. +/// +/// The samples of a frame come in the order the positions are defined, +/// which is the order of the bits from the least significant one and up, +/// no matter in which order the mask was written. +/// +/// ``` +/// use wasapi::{make_channelmasks, SPEAKER_FRONT_CENTER, SPEAKER_FRONT_LEFT, +/// SPEAKER_FRONT_RIGHT, SPEAKER_LOW_FREQUENCY}; +/// +/// // Every position is a single bit, and they are numbered in the order +/// // the samples of a frame come in. These are the four lowest ones. +/// assert_eq!(SPEAKER_FRONT_LEFT, 0x1); +/// assert_eq!(SPEAKER_FRONT_RIGHT, 0x2); +/// assert_eq!(SPEAKER_FRONT_CENTER, 0x4); +/// assert_eq!(SPEAKER_LOW_FREQUENCY, 0x8); +/// +/// // The most likely layout for three channels is 2.1. +/// let mask = make_channelmasks(3)[0]; +/// assert_eq!(mask, SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_LOW_FREQUENCY); +/// assert_eq!(mask, 0xb); +/// +/// // Ask which positions it holds. +/// assert!(mask & SPEAKER_LOW_FREQUENCY != 0); +/// assert!(mask & SPEAKER_FRONT_CENTER == 0); +/// +/// // The number of positions is the number of channels of the format. +/// // This layout skips the center channel, so the subwoofer bit 0x8 is the +/// // highest of the three, and its sample is the last one of a frame. +/// assert_eq!(mask.count_ones(), 3); +/// ``` pub fn make_channelmasks(channels: usize) -> Vec { match channels { 1 => vec![KSAUDIO_SPEAKER_MONO, make_simple_channelmask(channels), 0], @@ -361,7 +424,8 @@ pub fn make_channelmasks(channels: usize) -> Vec { /// Make a simple channel mask by adding the correct number of bits. /// Above the 18 channel positions [that are defined](https://docs.microsoft.com/en-us/windows/win32/api/mmreg/ns-mmreg-waveformatextensible) -/// it returns a zero. +/// it returns a zero, which is the only option left for such formats, +/// since there are no positions to assign, see [make_channelmasks]. pub fn make_simple_channelmask(channels: usize) -> u32 { match channels { 1..=18 => { From 64e7f7fb151eac3b561392757cd37640625d945d Mon Sep 17 00:00:00 2001 From: HEnquist Date: Mon, 17 Aug 2026 22:38:35 +0200 Subject: [PATCH 5/7] Switch to edition 2024 and correct the MSRV - Set edition to 2024 and rust-version to 1.85. The declared 1.76 was too low, windows 0.62 needs 1.82 and edition 2024 needs 1.85. - Rename the 'gen' variable in the playsine examples, it is a reserved keyword in edition 2024. - Reformat with the 2024 style edition, this only reorders imports. - Add a CI job that checks the library with the MSRV toolchain. - Document that the record_application example needs 1.88, since the sysinfo dev-dependency requires it. --- .github/workflows/windows-ci.yml | 17 +++++ Cargo.toml | 9 ++- README.md | 9 ++- examples/playnoise_exclusive.rs | 4 +- examples/playnoise_exclusive_poll.rs | 4 +- examples/playsine.rs | 4 +- examples/playsine_events.rs | 4 +- examples/playsine_poll.rs | 4 +- examples/record_application.rs | 4 ++ src/api.rs | 52 +++++++------- src/capabilities.rs | 100 ++++++++++++++++----------- src/dataranges.rs | 10 +-- src/events.rs | 25 +++---- src/waveformat.rs | 4 +- 14 files changed, 155 insertions(+), 95 deletions(-) diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 1e30bd0..9913735 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -46,6 +46,23 @@ jobs: if: ${{ always() }} run: cargo publish --dry-run + msrv: + if: ${{ github.event_name != 'release' }} + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + + # Keep this version in sync with 'rust-version' in Cargo.toml. + # Only the library is checked here, since some dev-dependencies + # require a newer compiler than the library itself. + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@1.85 + + - name: Cargo check (library only) + run: cargo check --lib + publish: if: ${{ github.event_name == 'release' && github.event.action == 'published' }} runs-on: windows-latest diff --git a/Cargo.toml b/Cargo.toml index ed9a9fa..455acce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,10 @@ [package] name = "wasapi" version = "0.24.0" -edition = "2021" -rust-version = "1.76" +edition = "2024" +# The library itself builds with 1.85. Note that some examples need a newer +# compiler, see the comments in [dev-dependencies]. +rust-version = "1.85" authors = ["HEnquist "] description = "Bindings for the Wasapi API on Windows" license = "MIT" @@ -40,6 +42,9 @@ thiserror = "2.0" [dev-dependencies] simplelog = "0.12" rand = "0.10" +# sysinfo 0.38 requires Rust 1.88, which is higher than the rust-version of the +# library. Only the 'record_application' example uses it, so building the library +# still works with 1.85. Building the examples and running the tests needs 1.88. sysinfo = "0.38" [package.metadata.docs.rs] diff --git a/README.md b/README.md index b8e7ca0..44a1d7d 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,13 @@ the documentation of [`AudioClient::initialize_client`](https://docs.rs/wasapi/latest/wasapi/struct.AudioClient.html#method.initialize_client), including how to choose between them. +## Minimum supported Rust version + +The library requires Rust 1.85, and uses edition 2024. + +The `record_application` example needs Rust 1.88, since it depends on `sysinfo`. +This also applies to `cargo test`, which builds all examples. + ## Included examples | Example | Description | @@ -43,7 +50,7 @@ including how to choose between them. | `record` | Records audio from the default device, and saves the raw samples to a file. | | `devices` | Lists all available audio devices and displays the default devices. | | `processes` | Lists all audio devices and the processes that are using them, with the peak level of each session. | -| `record_application` | Records audio from a single application, and saves the raw samples to a file. | +| `record_application` | Records audio from a single application, and saves the raw samples to a file. Needs Rust 1.88. | | `aec` | Captures audio with Acoustic Echo Cancellation (AEC) enabled and saves the raw data to a file. | | `device_notifications` | Listens for devices being added, removed or changed, and for changes of the default device. | | `capabilities` | Lists the formats a single output device supports in exclusive mode. | diff --git a/examples/playnoise_exclusive.rs b/examples/playnoise_exclusive.rs index 520eaa4..ce05e72 100644 --- a/examples/playnoise_exclusive.rs +++ b/examples/playnoise_exclusive.rs @@ -73,7 +73,9 @@ fn main() { match werr.code() { E_INVALIDARG => error!("IAudioClient::Initialize: Invalid argument"), AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED => { - warn!("IAudioClient::Initialize: Unaligned buffer, trying to adjust the period."); + warn!( + "IAudioClient::Initialize: Unaligned buffer, trying to adjust the period." + ); // Try to recover following the example in the docs. // https://learn.microsoft.com/en-us/windows/win32/api/audioclient/nf-audioclient-iaudioclient-initialize#examples // Just panic on errors to keep it short and simple. diff --git a/examples/playnoise_exclusive_poll.rs b/examples/playnoise_exclusive_poll.rs index b753360..7760143 100644 --- a/examples/playnoise_exclusive_poll.rs +++ b/examples/playnoise_exclusive_poll.rs @@ -77,7 +77,9 @@ fn main() { match werr.code() { E_INVALIDARG => error!("IAudioClient::Initialize: Invalid argument"), AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED => { - warn!("IAudioClient::Initialize: Unaligned buffer, trying to adjust the period."); + warn!( + "IAudioClient::Initialize: Unaligned buffer, trying to adjust the period." + ); // Try to recover following the example in the docs. // https://learn.microsoft.com/en-us/windows/win32/api/audioclient/nf-audioclient-iaudioclient-initialize#examples // Just panic on errors to keep it short and simple. diff --git a/examples/playsine.rs b/examples/playsine.rs index 44eb34c..ad78a92 100644 --- a/examples/playsine.rs +++ b/examples/playsine.rs @@ -49,7 +49,7 @@ fn main() { initialize_mta().unwrap(); - let mut gen = SineGenerator::new(1000.0, 44100.0, 0.1); + let mut sine = SineGenerator::new(1000.0, 44100.0, 0.1); let channels = 2; let enumerator = DeviceEnumerator::new().unwrap(); @@ -125,7 +125,7 @@ fn main() { let mut write_frames = |nbr_frames: usize| { let mut data = vec![0u8; nbr_frames * blockalign as usize]; for frame in data.chunks_exact_mut(blockalign as usize) { - let sample = gen.next().unwrap(); + let sample = sine.next().unwrap(); let sample_bytes = sample.to_le_bytes(); for value in frame.chunks_exact_mut(blockalign as usize / channels) { for (bufbyte, sinebyte) in value.iter_mut().zip(sample_bytes.iter()) { diff --git a/examples/playsine_events.rs b/examples/playsine_events.rs index 1dc6a51..14bf353 100644 --- a/examples/playsine_events.rs +++ b/examples/playsine_events.rs @@ -51,7 +51,7 @@ fn main() { initialize_mta().unwrap(); - let mut gen = SineGenerator::new(1000.0, 44100.0, 0.1); + let mut sine = SineGenerator::new(1000.0, 44100.0, 0.1); let channels = 2; let enumerator = DeviceEnumerator::new().unwrap(); @@ -81,7 +81,7 @@ fn main() { let mut write_frames = |nbr_frames: usize| { let mut data = vec![0u8; nbr_frames * blockalign as usize]; for frame in data.chunks_exact_mut(blockalign as usize) { - let sample = gen.next().unwrap(); + let sample = sine.next().unwrap(); let sample_bytes = sample.to_le_bytes(); for value in frame.chunks_exact_mut(blockalign as usize / channels) { for (bufbyte, sinebyte) in value.iter_mut().zip(sample_bytes.iter()) { diff --git a/examples/playsine_poll.rs b/examples/playsine_poll.rs index 249b8a4..071e843 100644 --- a/examples/playsine_poll.rs +++ b/examples/playsine_poll.rs @@ -51,7 +51,7 @@ fn main() { initialize_mta().unwrap(); - let mut gen = SineGenerator::new(1000.0, 44100.0, 0.1); + let mut sine = SineGenerator::new(1000.0, 44100.0, 0.1); let channels = 2; @@ -131,7 +131,7 @@ fn main() { let mut write_frames = |nbr_frames: usize| { let mut data = vec![0u8; nbr_frames * blockalign as usize]; for frame in data.chunks_exact_mut(blockalign as usize) { - let sample = gen.next().unwrap(); + let sample = sine.next().unwrap(); let sample_bytes = sample.to_le_bytes(); for value in frame.chunks_exact_mut(blockalign as usize / channels) { for (bufbyte, sinebyte) in value.iter_mut().zip(sample_bytes.iter()) { diff --git a/examples/record_application.rs b/examples/record_application.rs index 7388c6b..f91c309 100644 --- a/examples/record_application.rs +++ b/examples/record_application.rs @@ -3,6 +3,10 @@ // // This example captures from Firefox, edit the process name in main() // to capture from another application. +// +// Note: this example needs Rust 1.88, which is newer than the rust-version +// of the library. This is because it uses the 'sysinfo' crate to look up +// the process id of the application. use std::collections::VecDeque; use std::error::{self}; diff --git a/src/api.rs b/src/api.rs index dd68b96..350b01b 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,7 +1,7 @@ use num_integer::Integer; use std::cmp; use std::collections::VecDeque; -use std::mem::{size_of, ManuallyDrop}; +use std::mem::{ManuallyDrop, size_of}; use std::ops::Deref; use std::pin::Pin; use std::sync::{Arc, Condvar, Mutex}; @@ -9,21 +9,21 @@ use std::{fmt, ptr, slice}; use windows::Win32::Foundation::{CloseHandle, E_INVALIDARG, E_NOINTERFACE, FALSE, PROPERTYKEY}; use windows::Win32::Media::Audio::Endpoints::IAudioMeterInformation; use windows::Win32::Media::Audio::{ + AUDCLNT_STREAMOPTIONS, AUDCLNT_STREAMOPTIONS_AMBISONICS, AUDCLNT_STREAMOPTIONS_MATCH_FORMAT, + AUDCLNT_STREAMOPTIONS_NONE, AUDCLNT_STREAMOPTIONS_RAW, AUDIO_EFFECT, AUDIO_STREAM_CATEGORY, + AUDIOCLIENT_ACTIVATION_PARAMS, AUDIOCLIENT_ACTIVATION_PARAMS_0, + AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK, AUDIOCLIENT_PROCESS_LOOPBACK_PARAMS, ActivateAudioInterfaceAsync, AudioCategory_Alerts, AudioCategory_Communications, AudioCategory_FarFieldSpeech, AudioCategory_ForegroundOnlyMedia, AudioCategory_GameChat, AudioCategory_GameEffects, AudioCategory_GameMedia, AudioCategory_Media, AudioCategory_Movie, AudioCategory_Other, AudioCategory_SoundEffects, AudioCategory_Speech, - AudioCategory_UniformSpeech, AudioCategory_VoiceTyping, EDataFlow, ERole, - IAcousticEchoCancellationControl, IActivateAudioInterfaceAsyncOperation, - IActivateAudioInterfaceCompletionHandler, IActivateAudioInterfaceCompletionHandler_Impl, - IAudioClient2, IAudioEffectsManager, IAudioSessionControl2, IAudioSessionEnumerator, - IAudioSessionManager, IAudioSessionManager2, IMMEndpoint, PKEY_AudioEngine_DeviceFormat, - AUDCLNT_STREAMOPTIONS, AUDCLNT_STREAMOPTIONS_AMBISONICS, AUDCLNT_STREAMOPTIONS_MATCH_FORMAT, - AUDCLNT_STREAMOPTIONS_NONE, AUDCLNT_STREAMOPTIONS_RAW, AUDIOCLIENT_ACTIVATION_PARAMS, - AUDIOCLIENT_ACTIVATION_PARAMS_0, AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK, - AUDIOCLIENT_PROCESS_LOOPBACK_PARAMS, AUDIO_EFFECT, AUDIO_STREAM_CATEGORY, + AudioCategory_UniformSpeech, AudioCategory_VoiceTyping, EDataFlow, ENDPOINT_HARDWARE_SUPPORT_METER, ENDPOINT_HARDWARE_SUPPORT_MUTE, - ENDPOINT_HARDWARE_SUPPORT_VOLUME, PROCESS_LOOPBACK_MODE_EXCLUDE_TARGET_PROCESS_TREE, + ENDPOINT_HARDWARE_SUPPORT_VOLUME, ERole, IAcousticEchoCancellationControl, + IActivateAudioInterfaceAsyncOperation, IActivateAudioInterfaceCompletionHandler, + IActivateAudioInterfaceCompletionHandler_Impl, IAudioClient2, IAudioEffectsManager, + IAudioSessionControl2, IAudioSessionEnumerator, IAudioSessionManager, IAudioSessionManager2, + IMMEndpoint, PKEY_AudioEngine_DeviceFormat, PROCESS_LOOPBACK_MODE_EXCLUDE_TARGET_PROCESS_TREE, PROCESS_LOOPBACK_MODE_INCLUDE_TARGET_PROCESS_TREE, VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK, }; use windows::Win32::Media::KernelStreaming::AUDIO_EFFECT_TYPE_ACOUSTIC_ECHO_CANCELLATION; @@ -31,39 +31,39 @@ use windows::Win32::System::Com::CoTaskMemFree; use windows::Win32::System::Com::StructuredStorage::PropVariantClear; use windows::Win32::System::Variant::VT_BLOB; use windows::{ - core::{HRESULT, PCSTR}, Win32::Devices::FunctionDiscovery::{ - PKEY_DeviceInterface_FriendlyName, PKEY_Device_DeviceDesc, PKEY_Device_FriendlyName, + PKEY_Device_DeviceDesc, PKEY_Device_FriendlyName, PKEY_DeviceInterface_FriendlyName, }, Win32::Foundation::{HANDLE, WAIT_OBJECT_0}, Win32::Media::Audio::{ - eCapture, eCommunications, eConsole, eMultimedia, eRender, AudioSessionStateActive, - AudioSessionStateExpired, AudioSessionStateInactive, IAudioCaptureClient, IAudioClient, - IAudioClock, IAudioRenderClient, IAudioSessionControl, IAudioSessionEvents, IMMDevice, - IMMDeviceCollection, IMMDeviceEnumerator, IMMNotificationClient, MMDeviceEnumerator, AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY, AUDCLNT_BUFFERFLAGS_SILENT, AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR, AUDCLNT_SHAREMODE_EXCLUSIVE, AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, - AUDCLNT_STREAMFLAGS_LOOPBACK, AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY, DEVICE_STATE, + AUDCLNT_STREAMFLAGS_LOOPBACK, AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY, + AudioSessionStateActive, AudioSessionStateExpired, AudioSessionStateInactive, DEVICE_STATE, DEVICE_STATE_ACTIVE, DEVICE_STATE_DISABLED, DEVICE_STATE_NOTPRESENT, - DEVICE_STATE_UNPLUGGED, WAVEFORMATEX, WAVEFORMATEXTENSIBLE, + DEVICE_STATE_UNPLUGGED, IAudioCaptureClient, IAudioClient, IAudioClock, IAudioRenderClient, + IAudioSessionControl, IAudioSessionEvents, IMMDevice, IMMDeviceCollection, + IMMDeviceEnumerator, IMMNotificationClient, MMDeviceEnumerator, WAVEFORMATEX, + WAVEFORMATEXTENSIBLE, eCapture, eCommunications, eConsole, eMultimedia, eRender, }, Win32::Media::KernelStreaming::WAVE_FORMAT_EXTENSIBLE, Win32::System::Com::StructuredStorage::{ - PropVariantToStringAlloc, PROPVARIANT, PROPVARIANT_0, PROPVARIANT_0_0, PROPVARIANT_0_0_0, + PROPVARIANT, PROPVARIANT_0, PROPVARIANT_0_0, PROPVARIANT_0_0_0, PropVariantToStringAlloc, }, + Win32::System::Com::{BLOB, STGM_READ}, Win32::System::Com::{ - CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_ALL, COINIT_APARTMENTTHREADED, - COINIT_MULTITHREADED, + CLSCTX_ALL, COINIT_APARTMENTTHREADED, COINIT_MULTITHREADED, CoCreateInstance, + CoInitializeEx, CoUninitialize, }, - Win32::System::Com::{BLOB, STGM_READ}, Win32::System::Threading::{CreateEventA, WaitForSingleObject}, + core::{HRESULT, PCSTR}, }; -use windows_core::{implement, IUnknown, Interface, Ref, HSTRING, PCWSTR, PWSTR}; +use windows_core::{HSTRING, IUnknown, Interface, PCWSTR, PWSTR, Ref, implement}; use crate::{ - make_channelmasks, AudioSessionEvents, DeviceEventCallbacks, EventCallbacks, - NotificationClient, WasapiError, WaveFormat, + AudioSessionEvents, DeviceEventCallbacks, EventCallbacks, NotificationClient, WasapiError, + WaveFormat, make_channelmasks, }; pub(crate) type WasapiRes = Result; diff --git a/src/capabilities.rs b/src/capabilities.rs index 6675f48..0b0b27a 100644 --- a/src/capabilities.rs +++ b/src/capabilities.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeSet, HashMap}; -use crate::{covered_by_any, AudioClient, DataRange, Device, SampleType, WasapiRes, WaveFormat}; +use crate::{AudioClient, DataRange, Device, SampleType, WasapiRes, WaveFormat, covered_by_any}; use windows::Win32::Media::KernelStreaming::WAVE_FORMAT_EXTENSIBLE; /// The channel count ceiling that a scan uses for a device @@ -308,7 +308,9 @@ impl Probing<'_, C> { preferred_mask, ); if !covered_by_any(self.data_ranges, &requested) { - trace!("Skipping {samplerate} Hz, {channels} ch, format {candidate:?}, the driver declares no range for it"); + trace!( + "Skipping {samplerate} Hz, {channels} ch, format {candidate:?}, the driver declares no range for it" + ); continue; } let Ok(accepted) = self.checker.check_exclusive(&requested) else { @@ -528,7 +530,7 @@ struct RateProbe { #[cfg(test)] mod tests { use super::*; - use crate::{make_channelmasks, WasapiError}; + use crate::{WasapiError, make_channelmasks}; use std::cell::RefCell; /// Build a probing state for a fake device, without any declared ranges. @@ -693,15 +695,21 @@ mod tests { fn probe_returns_nothing_for_unsupported_rates_and_channel_counts() { let device = FakeDevice::new(&[48000], 2, &[S16]); let mut masks = ChannelMaskMap::new(); - assert!(probing(&device, &mut masks) - .formats(44100, 2, CANDIDATE_FORMATS) - .is_empty()); - assert!(probing(&device, &mut masks) - .formats(48000, 4, CANDIDATE_FORMATS) - .is_empty()); - assert!(probing(&device, &mut masks) - .formats(48000, 0, CANDIDATE_FORMATS) - .is_empty()); + assert!( + probing(&device, &mut masks) + .formats(44100, 2, CANDIDATE_FORMATS) + .is_empty() + ); + assert!( + probing(&device, &mut masks) + .formats(48000, 4, CANDIDATE_FORMATS) + .is_empty() + ); + assert!( + probing(&device, &mut masks) + .formats(48000, 0, CANDIDATE_FORMATS) + .is_empty() + ); } #[test] @@ -717,10 +725,12 @@ mod tests { probing(&device, &mut masks).formats(96000, 2, CANDIDATE_FORMATS); // The cached mask is used from the very first query of the second probe. - assert!(device - .queries_for(96000, 2) - .iter() - .all(|q| q.mask == accepted)); + assert!( + device + .queries_for(96000, 2) + .iter() + .all(|q| q.mask == accepted) + ); } #[test] @@ -801,15 +811,19 @@ mod tests { // The first rate probes the full range, the ceiling drops to two after that. assert!(device.queries().iter().any(|q| q.channels == 8)); - assert!(!device - .queries() - .iter() - .any(|q| q.samplerate == 44100 && q.channels > 2)); + assert!( + !device + .queries() + .iter() + .any(|q| q.samplerate == 44100 && q.channels > 2) + ); // The low rates only use the channel counts that were found. - assert!(!device - .queries() - .iter() - .any(|q| q.samplerate == 32000 && q.channels > 2)); + assert!( + !device + .queries() + .iter() + .any(|q| q.samplerate == 32000 && q.channels > 2) + ); } #[test] @@ -834,14 +848,18 @@ mod tests { ] ); // Nothing outside the declared ranges is even asked about. - assert!(device - .queries() - .iter() - .all(|q| q.channels <= 2 && q.candidate == S16)); - assert!(!device - .queries() - .iter() - .any(|q| q.samplerate < 44100 || q.samplerate > 48000)); + assert!( + device + .queries() + .iter() + .all(|q| q.channels <= 2 && q.candidate == S16) + ); + assert!( + !device + .queries() + .iter() + .any(|q| q.samplerate < 44100 || q.samplerate > 48000) + ); } #[test] @@ -881,9 +899,11 @@ mod tests { for channels in 1..=4 { let queries = device.queries_for(48000, channels); assert_eq!(queries.len(), 4); - assert!(queries - .iter() - .all(|q| q.candidate.sample_type == SampleType::Int)); + assert!( + queries + .iter() + .all(|q| q.candidate.sample_type == SampleType::Int) + ); } } @@ -906,9 +926,11 @@ mod tests { assert!(probing(&device, &mut masks).all_rates(2).is_empty()); assert!(masks.is_empty()); // Nothing was found, so the low rates are probed with the full channel range. - assert!(device - .queries() - .iter() - .any(|q| q.samplerate == 32000 && q.channels == 2)); + assert!( + device + .queries() + .iter() + .any(|q| q.samplerate == 32000 && q.channels == 2) + ); } } diff --git a/src/dataranges.rs b/src/dataranges.rs index e8a531c..c2f3b4a 100644 --- a/src/dataranges.rs +++ b/src/dataranges.rs @@ -10,16 +10,15 @@ use std::collections::HashSet; use std::mem::size_of; use std::ptr::from_ref; -use windows::core::{Interface, GUID, HRESULT, PCWSTR}; use windows::Win32::Foundation::{ CloseHandle, ERROR_INSUFFICIENT_BUFFER, ERROR_MORE_DATA, GENERIC_READ, GENERIC_WRITE, HANDLE, }; use windows::Win32::Media::Audio::{Connector, IConnector, IDeviceTopology, IMMDevice, IPart}; use windows::Win32::Media::KernelStreaming::{ - KSPROPSETID_Pin, IOCTL_KS_PROPERTY, KSDATAFORMAT_0, KSDATAFORMAT_SUBTYPE_PCM, - KSDATAFORMAT_TYPE_AUDIO, KSIDENTIFIER_0_0, KSMULTIPLE_ITEM, KSPIN_DATAFLOW_IN, - KSPIN_DATAFLOW_OUT, KSPROPERTY_PIN, KSPROPERTY_PIN_CTYPES, KSPROPERTY_PIN_DATAFLOW, - KSPROPERTY_PIN_DATARANGES, KSPROPERTY_TYPE_GET, KSP_PIN, + IOCTL_KS_PROPERTY, KSDATAFORMAT_0, KSDATAFORMAT_SUBTYPE_PCM, KSDATAFORMAT_TYPE_AUDIO, + KSIDENTIFIER_0_0, KSMULTIPLE_ITEM, KSP_PIN, KSPIN_DATAFLOW_IN, KSPIN_DATAFLOW_OUT, + KSPROPERTY_PIN, KSPROPERTY_PIN_CTYPES, KSPROPERTY_PIN_DATAFLOW, KSPROPERTY_PIN_DATARANGES, + KSPROPERTY_TYPE_GET, KSPROPSETID_Pin, }; use windows::Win32::Media::Multimedia::KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; use windows::Win32::Storage::FileSystem::{ @@ -27,6 +26,7 @@ use windows::Win32::Storage::FileSystem::{ }; use windows::Win32::System::Com::CLSCTX_ALL; use windows::Win32::System::IO::DeviceIoControl; +use windows::core::{GUID, HRESULT, Interface, PCWSTR}; use crate::{Direction, SampleType, WasapiRes, WaveFormat}; diff --git a/src/events.rs b/src/events.rs index b859ff3..759b778 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,16 +1,17 @@ use std::slice; use std::string::FromUtf16Error; use windows::{ - core::{implement, Result, GUID, PCWSTR}, Win32::Foundation::PROPERTYKEY, Win32::Media::Audio::{ AudioSessionDisconnectReason, AudioSessionState, AudioSessionStateActive, - AudioSessionStateExpired, AudioSessionStateInactive, DisconnectReasonDeviceRemoval, - DisconnectReasonExclusiveModeOverride, DisconnectReasonFormatChanged, - DisconnectReasonServerShutdown, DisconnectReasonSessionDisconnected, - DisconnectReasonSessionLogoff, EDataFlow, ERole, IAudioSessionEvents, - IAudioSessionEvents_Impl, IMMNotificationClient, IMMNotificationClient_Impl, DEVICE_STATE, + AudioSessionStateExpired, AudioSessionStateInactive, DEVICE_STATE, + DisconnectReasonDeviceRemoval, DisconnectReasonExclusiveModeOverride, + DisconnectReasonFormatChanged, DisconnectReasonServerShutdown, + DisconnectReasonSessionDisconnected, DisconnectReasonSessionLogoff, EDataFlow, ERole, + IAudioSessionEvents, IAudioSessionEvents_Impl, IMMNotificationClient, + IMMNotificationClient_Impl, }, + core::{GUID, PCWSTR, Result, implement}, }; use crate::{DeviceState, Direction, Role, SessionState}; @@ -286,10 +287,10 @@ impl IAudioSessionEvents_Impl for AudioSessionEvents_Impl { callback(changedchannel as usize, newvol, context); } else { warn!( - "OnChannelVolumeChanged: received unsupported changedchannel value {} for volume array length of {}", - changedchannel, - volslice.len() - ); + "OnChannelVolumeChanged: received unsupported changedchannel value {} for volume array length of {}", + changedchannel, + volslice.len() + ); return Ok(()); } } @@ -506,10 +507,10 @@ impl IMMNotificationClient_Impl for NotificationClient_Impl { mod tests { use super::*; use std::sync::{Arc, Mutex}; - use windows::core::HSTRING; use windows::Win32::Media::Audio::{ - eAll, eCapture, eConsole, eRender, DEVICE_STATE_ACTIVE, DEVICE_STATE_UNPLUGGED, + DEVICE_STATE_ACTIVE, DEVICE_STATE_UNPLUGGED, eAll, eCapture, eConsole, eRender, }; + use windows::core::HSTRING; const TEST_ID: &str = "{0.0.0.00000000}.{6e6f7420-6120-7265-616c-206465766963}"; diff --git a/src/waveformat.rs b/src/waveformat.rs index 275b73b..6568b68 100644 --- a/src/waveformat.rs +++ b/src/waveformat.rs @@ -1,11 +1,11 @@ use std::fmt; use windows::{ - core::GUID, Win32::Media::Audio::{ - WAVEFORMATEX, WAVEFORMATEXTENSIBLE, WAVEFORMATEXTENSIBLE_0, WAVE_FORMAT_PCM, + WAVE_FORMAT_PCM, WAVEFORMATEX, WAVEFORMATEXTENSIBLE, WAVEFORMATEXTENSIBLE_0, }, Win32::Media::KernelStreaming::{KSDATAFORMAT_SUBTYPE_PCM, WAVE_FORMAT_EXTENSIBLE}, Win32::Media::Multimedia::{KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, WAVE_FORMAT_IEEE_FLOAT}, + core::GUID, }; /// The [18 defined channel positions](https://docs.microsoft.com/en-us/windows/win32/api/mmreg/ns-mmreg-waveformatextensible) From efa0454d2790efac56c5056b91b7e2ea39f3ad06 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Mon, 17 Aug 2026 22:43:58 +0200 Subject: [PATCH 6/7] Fix two issues found in review - supported_formats_at_rate() no longer narrows the sample format candidates after the first channel count, which could drop a format that only works at some other count. - to_waveformatex() keeps the valid bits, subformat and channel mask instead of zeroing them, so the accessors describe the same format as the original. --- src/capabilities.rs | 25 +++++++++++++++++++++---- src/waveformat.rs | 23 ++++++++++++++--------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/capabilities.rs b/src/capabilities.rs index 0b0b27a..c47b012 100644 --- a/src/capabilities.rs +++ b/src/capabilities.rs @@ -126,7 +126,7 @@ impl FormatChecker for AudioClient { /// - The 48 kHz and 44.1 kHz families are probed interleaved from the base rate upward. /// The first hit establishes an upper channel count limit, /// and a reduced sample format set that all later probes reuse. -/// - Within a single rate, the sample format candidates are narrowed +/// - Within each rate of the scan, the sample format candidates are narrowed /// as soon as the first channel count succeeds with fewer than the full set. /// - Each family gets an early cutoff. Once a family has a hit, /// a miss at the next rate deactivates it, and the upward scan stops @@ -235,15 +235,16 @@ impl CapabilityProbe { /// [struct documentation](CapabilityProbe). /// A single rate gives nothing to learn from, unlike the full scan, /// so a device that declares no [DataRange]s is probed all the way up to - /// [DEFAULT_MAX_CHANNELS] here. + /// [DEFAULT_MAX_CHANNELS] here, and every sample format is tried for every + /// channel count. None of the pruning of the full scan is used, + /// so a format that only works at a single channel count is still found. /// Use [CapabilityProbe::supported_formats] instead /// when only one channel count is of interest. pub fn supported_formats_at_rate(&mut self, samplerate: usize) -> Vec { - let narrow = self.data_ranges.is_empty(); let mut probing = self.probing(); let ceiling = probing.channel_ceiling(); probing - .rate(samplerate, 1..=ceiling, CANDIDATE_FORMATS, narrow) + .rate(samplerate, 1..=ceiling, CANDIDATE_FORMATS, false) .formats } @@ -750,6 +751,22 @@ mod tests { } } + #[test] + fn without_narrowing_all_formats_are_probed_at_every_channel_count() { + // S32 only works with one channel, which narrowing would drop after the first count. + let device = FakeDevice::new(&[48000], 4, &[S16, S32]).only_with_one_channel(&[S32]); + let mut masks = ChannelMaskMap::new(); + let result = probing(&device, &mut masks).rate(48000, 1..=4, CANDIDATE_FORMATS, false); + + assert_eq!(result.supported_candidates, vec![S16, S32]); + for channels in 1..=4 { + assert_eq!( + device.queries_for(48000, channels).len(), + CANDIDATE_FORMATS.len() + ); + } + } + #[test] fn a_rate_probe_covers_all_channel_counts() { // A device with a gap, it takes two and four channels but not three. diff --git a/src/waveformat.rs b/src/waveformat.rs index 6568b68..924df21 100644 --- a/src/waveformat.rs +++ b/src/waveformat.rs @@ -5,7 +5,6 @@ use windows::{ }, Win32::Media::KernelStreaming::{KSDATAFORMAT_SUBTYPE_PCM, WAVE_FORMAT_EXTENSIBLE}, Win32::Media::Multimedia::{KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, WAVE_FORMAT_IEEE_FLOAT}, - core::GUID, }; /// The [18 defined channel positions](https://docs.microsoft.com/en-us/windows/win32/api/mmreg/ns-mmreg-waveformatextensible) @@ -228,6 +227,11 @@ impl WaveFormat { /// or padded in a four byte container, and the two cannot be told apart /// in a reliable way without `wValidBitsPerSample`. /// This method returns an error for any format that would be ambiguous. + /// + /// The returned value is still stored as a WAVEFORMATEXTENSIBLE, with `cbSize` set to zero + /// so that only the WAVEFORMATEX part of it is passed on to Wasapi. + /// The extensible fields are copied over unchanged, so that the accessors + /// keep describing the same format as the original. pub fn to_waveformatex(&self) -> WasapiRes { let blockalign = self.wave_fmt.Format.nBlockAlign; let samplerate = self.wave_fmt.Format.nSamplesPerSec; @@ -252,16 +256,13 @@ impl WaveFormat { wBitsPerSample: storebits, wFormatTag: sample_type as u16, }; - let sample = WAVEFORMATEXTENSIBLE_0 { - wValidBitsPerSample: 0, - }; - let subformat = GUID::zeroed(); - let mask = 0; let wave_fmt = WAVEFORMATEXTENSIBLE { Format: wave_format, - Samples: sample, - SubFormat: subformat, - dwChannelMask: mask, + Samples: WAVEFORMATEXTENSIBLE_0 { + wValidBitsPerSample: validbits, + }, + SubFormat: self.wave_fmt.SubFormat, + dwChannelMask: self.wave_fmt.dwChannelMask, }; Ok(WaveFormat { wave_fmt }) } @@ -456,6 +457,10 @@ mod tests { assert_eq!(fmtex.get_bitspersample(), storebits as u16); assert_eq!(fmtex.get_blockalign(), fmt.get_blockalign()); assert_eq!(fmtex.get_avgbytespersec(), fmt.get_avgbytespersec()); + // The accessors still describe the same format as the original. + assert_eq!(fmtex.get_validbitspersample(), storebits as u16); + assert_eq!(fmtex.get_subformat().unwrap(), sample_type); + assert_eq!(fmtex.get_dwchannelmask(), fmt.get_dwchannelmask()); } } From ffdcc0f826267ce9e6234f704f007894a725b359 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Mon, 17 Aug 2026 23:32:18 +0200 Subject: [PATCH 7/7] Open the kernel streaming filter with read access only Only get requests are sent to the filter, so write access is not needed, and asking for it can keep a read only filter from being opened at all. Falls back to read and write if the read only open fails. Also correct a doc comment that implied a data range has a lower bound on the channel count, it only declares a maximum. --- src/dataranges.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/dataranges.rs b/src/dataranges.rs index c2f3b4a..3d4d4c9 100644 --- a/src/dataranges.rs +++ b/src/dataranges.rs @@ -40,7 +40,7 @@ const INSUFFICIENT_BUFFER: HRESULT = HRESULT::from_win32(ERROR_INSUFFICIENT_BUFF /// One capability range, as declared by a device driver. /// /// A range is a cross product and over-reports. -/// A device declaring two to eight channels at 44.1 to 192 kHz +/// A device declaring up to eight channels at 44.1 to 192 kHz /// is not promising that every combination in that box works, /// so a range is an upper bound that still has to be confirmed with /// [is_supported](crate::AudioClient::is_supported). @@ -251,24 +251,34 @@ fn reachable_connectors(start: &IPart, upstream: bool) -> Vec { /// Open a kernel streaming filter by its device interface path. /// The device ids from the topology have a `{2}.` prefix that has to go. +/// +/// Only get requests are sent to the filter, so read access is enough, +/// and asking for less is what lets a filter that only allows reading be opened at all. +/// A driver that refuses that gets a second try with write access as well. fn open_filter(device_id: &str) -> WasapiRes { let path = match device_id.find("}.") { Some(pos) if device_id.starts_with('{') => &device_id[pos + 2..], _ => device_id, }; let wide: Vec = path.encode_utf16().chain(std::iter::once(0)).collect(); - let handle = unsafe { + let open = |access: u32| unsafe { CreateFileW( PCWSTR(wide.as_ptr()), - GENERIC_READ.0 | GENERIC_WRITE.0, + access, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, FILE_FLAGS_AND_ATTRIBUTES(0), None, - )? + ) }; - Ok(handle) + match open(GENERIC_READ.0) { + Ok(handle) => Ok(handle), + Err(err) => { + debug!("Could not open the filter for reading, {err}, retrying with write access"); + Ok(open(GENERIC_READ.0 | GENERIC_WRITE.0)?) + } + } } /// Build a pin property request.