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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/windows-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 10 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 <henrik.enquist@gmail.com>"]
description = "Bindings for the Wasapi API on Windows"
license = "MIT"
Expand All @@ -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"
Expand All @@ -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]
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- 鈥nd 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

Expand All @@ -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. |
6 changes: 6 additions & 0 deletions examples/aec.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
82 changes: 82 additions & 0 deletions examples/capabilities.rs
Original file line number Diff line number Diff line change
@@ -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<u16, BTreeMap<u32, Vec<String>>> = 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(", "));
}
}
}
132 changes: 132 additions & 0 deletions examples/dataranges.rs
Original file line number Diff line number Diff line change
@@ -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<Described>, 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");
}
}
}
8 changes: 5 additions & 3 deletions examples/device_notifications.rs
Original file line number Diff line number Diff line change
@@ -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();

Expand Down
3 changes: 3 additions & 0 deletions examples/devices.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// List all available audio devices with their state,
// and show the default device for each role.

use wasapi::*;

fn main() {
Expand Down
5 changes: 5 additions & 0 deletions examples/loopback.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
11 changes: 10 additions & 1 deletion examples/playnoise_exclusive.rs
Original file line number Diff line number Diff line change
@@ -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::*;

Expand Down Expand Up @@ -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.
Expand Down
11 changes: 10 additions & 1 deletion examples/playnoise_exclusive_poll.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand Down Expand Up @@ -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.
Expand Down
Loading