Skip to content

Add exclusive mode capability probing - #64

Open
HEnquist wants to merge 7 commits into
masterfrom
rel0.25
Open

Add exclusive mode capability probing#64
HEnquist wants to merge 7 commits into
masterfrom
rel0.25

Conversation

@HEnquist

Copy link
Copy Markdown
Owner

Collects the changes for the next release, one commit per issue.

  • WaveFormat::to_waveformatex now returns an error for formats that a WAVEFORMATEX
    cannot describe without ambiguity, and the fallback query is skipped for them (WAVEFORMATEX fallback is unsafe for 24 bit formats #62).
    This is a behavior break, 24 bit formats used to get a wrong result instead.
  • New CapabilityProbe, for finding the formats a device supports in exclusive mode,
    at one rate and channel count, at one rate, or at all rates (Add exclusive mode capability probing #63).
  • New Device::get_data_ranges, for reading the capabilities that a driver declares.
    The probe uses them to bound the search, which is both faster and free of the blind
    spots of the staged scan.
  • Re-export the 18 channel position constants.

🤖 Generated with Claude Code

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.
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.
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.
- 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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the wasapi crate with structured probing of exclusive-mode format support, backed by optional driver-declared KS data ranges to bound the search space and improve both performance and coverage. It also hardens the WAVEFORMATEX fallback path to avoid unsafe/ambiguous 24-bit behavior.

Changes:

  • Add CapabilityProbe for querying supported exclusive-mode formats at (rate, channels), at a rate across channels, or across all standard rates.
  • Add Device::get_data_ranges plus KS topology/IOCTL plumbing to read driver-declared KSDATARANGE_AUDIO bounds and use them to constrain probing.
  • Tighten WaveFormat::to_waveformatex and the exclusive-mode quirk fallback to avoid ambiguous 24-bit WAVEFORMATEX queries; re-export speaker position constants and expand docs/examples.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/waveformat.rs Adds channel-position re-exports and strengthens to_waveformatex ambiguity handling; expands channel-mask documentation and tests.
src/lib.rs Exposes new capabilities and dataranges modules from the crate root.
src/dataranges.rs Implements KS topology traversal + KSPROPERTY_PIN_DATARANGES querying and parsing into DataRange.
src/capabilities.rs Introduces CapabilityProbe and probing logic (staged scan + bounded scan using DataRanges) with unit tests.
src/api.rs Adds Device::get_data_ranges and makes WAVEFORMATEX fallback conditional on to_waveformatex() success (avoids ambiguous formats).
README.md Documents new capabilities/data-ranges functionality and adds new examples.
examples/*.rs Adds new capabilities/dataranges examples and improves headers/comments across existing examples.
Cargo.toml Enables additional Windows feature flags needed for new IO and filesystem APIs.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/capabilities.rs
Comment on lines +241 to +248
pub fn supported_formats_at_rate(&mut self, samplerate: usize) -> Vec<WaveFormat> {
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)
.formats
}
Comment thread src/waveformat.rs
Comment on lines +239 to 243
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,
- 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.
- 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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/dataranges.rs:44

  • The doc comment implies a range can declare a lower channel-count bound ("two to eight channels"), but DataRange only models max_channels and covers() only checks <= max_channels. Consider rewording to avoid implying a minimum-channel constraint that the code does not enforce.
/// 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,

src/dataranges.rs:271

  • open_filter requests GENERIC_WRITE even though all IOCTLs issued are KSPROPERTY_TYPE_GET. Requesting write access can cause CreateFileW to fail on devices that allow read-only access to the KS filter. Consider opening read-only here (or at least falling back to read-only on access denied).
    let handle = unsafe {
        CreateFileW(
            PCWSTR(wide.as_ptr()),
            GENERIC_READ.0 | GENERIC_WRITE.0,
            FILE_SHARE_READ | FILE_SHARE_WRITE,
            None,

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants