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 29451f0..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" @@ -27,7 +29,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" @@ -38,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 dbb1dc4..44a1d7d 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,21 @@ 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 +- 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. +## 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 @@ -38,6 +50,8 @@ The following is a selection of the functionality currently available in the lib | `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. | +| `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 new file mode 100644 index 0000000..c36fce5 --- /dev/null +++ b/examples/capabilities.rs @@ -0,0 +1,82 @@ +// 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::*; + +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}") + } +} + +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(); + // 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 {:?}..", device.get_friendlyname().unwrap()); + + // This uses the capabilities that the driver declares, when it has any. + 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(); + println!("The scan took {} ms.", start.elapsed().as_millis()); + + // 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/examples/dataranges.rs b/examples/dataranges.rs new file mode 100644 index 0000000..8961fe0 --- /dev/null +++ b/examples/dataranges.rs @@ -0,0 +1,132 @@ +// Compare the capabilities that a driver declares with what the device really accepts. +// +// 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; +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, u128) { + let start = Instant::now(); + let formats = probe.supported_formats_all_rates(); + let elapsed = start.elapsed().as_millis(); + (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() + } + }; + + // 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} ms", + staged.len() + ); + + if ranges.is_empty() { + continue; + } + 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} ms", + 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/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..ce05e72 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::*; @@ -66,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 9ad656c..7760143 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::*; @@ -70,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 12b600f..ad78a92 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::*; @@ -45,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(); @@ -121,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 742ed68..14bf353 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::*; @@ -45,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(); @@ -75,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 51c81d7..071e843 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::*; @@ -46,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; @@ -126,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/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..f91c309 100644 --- a/examples/record_application.rs +++ b/examples/record_application.rs @@ -1,3 +1,13 @@ +// 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. +// +// 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}; use std::ffi::OsStr; diff --git a/src/api.rs b/src/api.rs index 371c8b7..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; @@ -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,6 +578,24 @@ impl 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 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) + } + /// Gets an [IAudioSessionManager] from an [IMMDevice] pub fn get_iaudiosessionmanager(&self) -> WasapiRes { let session_manager = unsafe { @@ -877,6 +903,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,12 +977,17 @@ impl AudioClient { /// The alternatives it tries are: /// - The format as given. /// - If one or two channels, try with the format as WAVEFORMATEX. - /// - Try with different channel masks: + /// 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, 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, @@ -966,14 +1000,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/capabilities.rs b/src/capabilities.rs new file mode 100644 index 0000000..c47b012 --- /dev/null +++ b/src/capabilities.rs @@ -0,0 +1,953 @@ +//! Probing of the formats a device supports in exclusive mode. + +use std::collections::{BTreeSet, HashMap}; + +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 +/// 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. +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, +]; + +/// 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 { + 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 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). +/// +/// 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::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, +/// 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 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, +/// and a reduced sample format set that all later probes reuse. +/// - 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 +/// 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. +/// 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. +/// +/// # 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. +/// +/// 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}; +/// # fn main() -> Result<(), Box> { +/// let device = DeviceEnumerator::new()?.get_default_device(&Direction::Render)?; +/// let mut probe = CapabilityProbe::new(&device)?; +/// +/// // 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(); +/// # Ok(()) +/// # } +/// ``` +pub struct CapabilityProbe { + client: AudioClient, + channel_masks: ChannelMaskMap, + data_ranges: Vec, +} + +impl CapabilityProbe { + /// Create a new probe for a [Device]. + /// + /// 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. + /// + /// 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}"); + 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. + /// 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 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 { + 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, + /// 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, 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 mut probing = self.probing(); + let ceiling = probing.channel_ceiling(); + probing + .rate(samplerate, 1..=ceiling, CANDIDATE_FORMATS, false) + .formats + } + + /// Get the formats the device accepts at any of the standard sample rates, + /// for every channel count the device can have, + /// see the [struct documentation](CapabilityProbe) for the ceiling that is used. + /// + /// 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). + 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 client, the channel mask cache and the data ranges as a [Probing]. + fn probing(&mut self) -> Probing<'_, AudioClient> { + Probing { + checker: &self.client, + channel_masks: &mut self.channel_masks, + data_ranges: &self.data_ranges, + } + } +} + +/// 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, + data_ranges: &'a [DataRange], +} + +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 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 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)); + } + } + supported + } + + /// 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.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 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!( + "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 + } + + /// 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, ceiling: usize) -> Vec { + if !self.data_ranges.is_empty() { + return self.all_rates_within_ranges(ceiling); + } + self.all_rates_staged(ceiling) + } + + /// 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" + ); + 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.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!( + "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 = 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); + } + 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)] +mod tests { + use super::*; + use crate::{WasapiError, make_channelmasks}; + 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 { + 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, + mono_only_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(), + 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(); + 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) + || (channels > 1 && self.mono_only_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 = probing(&device, &mut masks).formats(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!( + 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] + 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); + + 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); + + 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) + ); + } + + #[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 = 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])); + // 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 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. + let device = FakeDevice::new(&[48000], 4, &[S16]).without_channels(&[3]); + let mut masks = ChannelMaskMap::new(); + 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); + } + + #[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)> = probing(&device, &mut masks) + .all_rates(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 = probing(&device, &mut masks) + .all_rates(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(); + 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)); + 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_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!(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) + ); + } +} diff --git a/src/dataranges.rs b/src/dataranges.rs new file mode 100644 index 0000000..3d4d4c9 --- /dev/null +++ b/src/dataranges.rs @@ -0,0 +1,502 @@ +//! 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::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::{ + 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::{ + 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 windows::core::{GUID, HRESULT, Interface, PCWSTR}; + +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 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. +/// +/// A range is a cross product and over-reports. +/// 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). +/// 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, see [DataRange::sample_type]. + pub subformat: GUID, +} + +impl DataRange { + /// 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), + KSDATAFORMAT_SUBTYPE_IEEE_FLOAT => Some(SampleType::Float), + _ => None, + } + } + + /// Check if a format falls inside this range. + /// + /// 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; + 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(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 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, +) -> 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 { + 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. +/// +/// 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 open = |access: u32| unsafe { + CreateFileW( + PCWSTR(wide.as_ptr()), + access, + FILE_SHARE_READ | FILE_SHARE_WRITE, + None, + OPEN_EXISTING, + FILE_FLAGS_AND_ATTRIBUTES(0), + None, + ) + }; + 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. +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(), MORE_DATA | 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/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/lib.rs b/src/lib.rs index 4024bfc..480efdd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,14 @@ #![doc = include_str!("../README.md")] 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::*; diff --git a/src/waveformat.rs b/src/waveformat.rs index 79dd82e..924df21 100644 --- a/src/waveformat.rs +++ b/src/waveformat.rs @@ -1,18 +1,22 @@ use std::fmt; use windows::{ - core::GUID, 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, + 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}, }; +/// 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 +143,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, @@ -213,12 +219,29 @@ 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. + /// + /// 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; 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, @@ -233,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 }) } @@ -283,6 +303,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 } @@ -305,8 +328,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], @@ -349,7 +425,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 => { @@ -359,3 +436,41 @@ 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()); + // 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()); + } + } + + #[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()); + } +}