Conversation
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.
There was a problem hiding this comment.
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
CapabilityProbefor querying supported exclusive-mode formats at (rate, channels), at a rate across channels, or across all standard rates. - Add
Device::get_data_rangesplus KS topology/IOCTL plumbing to read driver-declaredKSDATARANGE_AUDIObounds and use them to constrain probing. - Tighten
WaveFormat::to_waveformatexand 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 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 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.
There was a problem hiding this comment.
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
DataRangeonly modelsmax_channelsandcovers()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_filterrequestsGENERIC_WRITEeven though all IOCTLs issued areKSPROPERTY_TYPE_GET. Requesting write access can causeCreateFileWto 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Collects the changes for the next release, one commit per issue.
WaveFormat::to_waveformatexnow returns an error for formats that a WAVEFORMATEXcannot 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.
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).
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.
🤖 Generated with Claude Code