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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **WASAPI**: Output streams now start with real audio immediately instead of undefined content in the render buffer.
- **WASAPI**: A stream paused immediately after starting no longer plays silence before real audio on resume.
- **WASAPI**: Fix `I64` and `F64` incorrectly reported as supported output formats.
- **WASAPI**: Reduced risk of `stop()` draining short when it races a callback that just wrote more audio.
- **WASAPI**: Input streams now deliver silence for packets the driver flags as silent instead of undefined data.

## [0.18.2] - 2026-08-16

Expand Down
2 changes: 1 addition & 1 deletion src/host/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ pub(crate) fn secs_to_nanos(secs: f64) -> u64 {
/// `Some(Duration::ZERO)` returns immediately.
///
/// [`StreamTrait::stop`]: crate::traits::StreamTrait::stop
#[cfg(any(all(windows, feature = "asio"), target_vendor = "apple"))]
#[cfg(any(windows, target_vendor = "apple"))]
pub(crate) fn wait_for_drain(window: std::time::Duration, timeout: Option<std::time::Duration>) {
let wait = timeout.map_or(window, |t| window.min(t));
if !wait.is_zero() {
Expand Down
21 changes: 7 additions & 14 deletions src/host/wasapi/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,6 @@ unsafe fn format_from_waveformatex_ptr(
(8, Audio::WAVE_FORMAT_PCM) => SampleFormat::U8,
(16, Audio::WAVE_FORMAT_PCM) => SampleFormat::I16,
(32, Multimedia::WAVE_FORMAT_IEEE_FLOAT) => SampleFormat::F32,
(64, Multimedia::WAVE_FORMAT_IEEE_FLOAT) => SampleFormat::F64,
(n_bits, KernelStreaming::WAVE_FORMAT_EXTENSIBLE) => {
let waveformatextensible_ptr = waveformatex_ptr as *const Audio::WAVEFORMATEXTENSIBLE;
let sub = unsafe { (*waveformatextensible_ptr).SubFormat };
Expand All @@ -235,13 +234,11 @@ unsafe fn format_from_waveformatex_ptr(
24 => SampleFormat::I24,
32 if valid_bits == 24 => SampleFormat::I24,
32 => SampleFormat::I32,
64 => SampleFormat::I64,
_ => return None,
}
} else if cmp_guid(&sub, &Multimedia::KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) {
match n_bits {
32 => SampleFormat::F32,
64 => SampleFormat::F64,
_ => return None,
}
} else {
Expand Down Expand Up @@ -1370,11 +1367,9 @@ fn config_to_waveformatextensible(
let format_tag = match sample_format {
SampleFormat::U8 | SampleFormat::I16 => Audio::WAVE_FORMAT_PCM,

SampleFormat::I24
| SampleFormat::I32
| SampleFormat::I64
| SampleFormat::F32
| SampleFormat::F64 => KernelStreaming::WAVE_FORMAT_EXTENSIBLE,
SampleFormat::I24 | SampleFormat::I32 | SampleFormat::F32 => {
KernelStreaming::WAVE_FORMAT_EXTENSIBLE
}

_ => return None,
};
Expand Down Expand Up @@ -1411,13 +1406,11 @@ fn config_to_waveformatextensible(
let channel_mask = channel_mask.unwrap_or(KernelStreaming::KSAUDIO_SPEAKER_DIRECTOUT);

let sub_format = match sample_format {
SampleFormat::U8
| SampleFormat::I16
| SampleFormat::I24
| SampleFormat::I32
| SampleFormat::I64 => KernelStreaming::KSDATAFORMAT_SUBTYPE_PCM,
SampleFormat::U8 | SampleFormat::I16 | SampleFormat::I24 | SampleFormat::I32 => {
KernelStreaming::KSDATAFORMAT_SUBTYPE_PCM
}

SampleFormat::F32 | SampleFormat::F64 => Multimedia::KSDATAFORMAT_SUBTYPE_IEEE_FLOAT,
SampleFormat::F32 => Multimedia::KSDATAFORMAT_SUBTYPE_IEEE_FLOAT,
_ => return None,
};

Expand Down
141 changes: 90 additions & 51 deletions src/host/wasapi/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ use windows::Win32::{
use crate::{
CallbackInfo, Data, Error, ErrorKind, FrameCount, ResultExt, SampleFormat, SampleRate,
StreamConfig, StreamInstant, StreamTimestamp,
host::{ErrorCallbackArc, emit_error, equilibrium::fill_equilibrium, latch::Latch},
host::{
ErrorCallbackArc, emit_error, equilibrium::fill_equilibrium, latch::Latch, wait_for_drain,
},
traits::StreamTrait,
};

Expand Down Expand Up @@ -534,13 +536,10 @@ impl StreamTrait for Stream {
fn stop(&self, timeout: Option<Duration>) -> Result<(), Error> {
self.skip_callback.store(true, Ordering::Relaxed);

if timeout != Some(Duration::ZERO) {
let fill = Duration::from_micros(self.fill_usec.load(Ordering::Relaxed));
let wait = timeout.map_or(fill, |t| fill.min(t));
if !wait.is_zero() {
std::thread::sleep(wait);
}
}
wait_for_drain(
Duration::from_micros(self.fill_usec.load(Ordering::Relaxed)),
timeout,
);

self.push_command(Command::StopStream).map_err(|_| {
Error::with_message(
Expand Down Expand Up @@ -694,27 +693,27 @@ fn run_input(
}

let stream = &run_ctxt.stream;
let scratch_len = if stream.sample_format == SampleFormat::I24 {
stream.max_frames_in_buffer as usize * stream.bytes_per_frame as usize / size_of::<i32>()
} else {
// The scratch buffer won't be used in this case.
0 // Vec::with_capacity(0) does not allocate.

// Create a scratch buffer for holding converted I24 data and silence for
// packets flagged `AUDCLNT_BUFFERFLAGS_SILENT`.
let scratch_len = (stream.max_frames_in_buffer as usize * stream.bytes_per_frame as usize)
.div_ceil(size_of::<i32>());
let mut scratch_buffer = vec![0i32; scratch_len].into_boxed_slice();

let capture_client = match stream.client_flow {
AudioClientFlow::Capture { ref capture_client } => capture_client.clone(),
_ => unreachable!(),
};
let mut scratch_buffer = vec![0; scratch_len].into_boxed_slice();

loop {
match process_commands_and_await_signal(&mut run_ctxt, error_callback) {
ControlFlow::Break(()) => break,
ControlFlow::Continue(false) => continue,
ControlFlow::Continue(true) => {}
}
let capture_client = match run_ctxt.stream.client_flow {
AudioClientFlow::Capture { ref capture_client } => capture_client.clone(),
_ => unreachable!(),
};
if let Err(err) = process_input(
&run_ctxt.stream,
capture_client,
&capture_client,
data_callback,
&mut scratch_buffer,
) {
Expand Down Expand Up @@ -875,10 +874,12 @@ fn process_commands_and_await_signal(
// The loop for processing pending input data.
fn process_input(
stream: &StreamInner,
capture_client: Audio::IAudioCaptureClient,
capture_client: &Audio::IAudioCaptureClient,
data_callback: &mut dyn FnMut(&Data, &CallbackInfo),
scratch_buffer: &mut [i32],
) -> Result<(), Error> {
let sample_size = stream.sample_format.sample_size();

unsafe {
// Get the available data in the shared buffer.
let mut buffer: *mut u8 = ptr::null_mut();
Expand Down Expand Up @@ -914,26 +915,15 @@ fn process_input(

debug_assert!(!buffer.is_null());
let byte_count = frames_available as usize * stream.bytes_per_frame as usize;
let data = if stream.sample_format == SampleFormat::I24 {
// WASAPI stores i24 in the upper bits
let source_data =
slice::from_raw_parts(buffer.cast(), byte_count / size_of::<i32>());
// use a scratch buffer since the capture buffer isn't meant to be written
let dst = &mut scratch_buffer[..source_data.len()];
dst.copy_from_slice(source_data);
for sample in dst.iter_mut() {
// On signed integers, >> is an arithmetic shift,
// which ensures the correct upper bits get shifted in
*sample >>= 8;
}

dst.as_mut_ptr().cast()
} else {
buffer.cast()
};

let len = byte_count / stream.sample_format.sample_size();
let data = Data::from_parts(data, len, stream.sample_format);
let silent = flags & Audio::AUDCLNT_BUFFERFLAGS_SILENT.0 as u32 != 0;
let data = packet_data(
buffer,
byte_count,
silent,
stream.sample_format,
scratch_buffer,
);
let data = Data::from_parts(data, byte_count / sample_size, stream.sample_format);

if !stream.skip_callback.load(Ordering::Relaxed) {
// The `qpc_position` is in 100 nanosecond units. Convert it to nanoseconds.
Expand All @@ -949,6 +939,43 @@ fn process_input(
}
}

// Returns the packet data to hand to the callback: silence for a packet flagged silent, converted
// samples for I24, or the packet itself.
//
// Safety: `packet` must point to `byte_count` bytes that stay valid until the packet is released.
#[inline]
unsafe fn packet_data(
packet: *mut u8,
byte_count: usize,
silent: bool,
sample_format: SampleFormat,
scratch_buffer: &mut [i32],
) -> *mut () {
unsafe {
if silent {
// The packet's data values must be ignored, so hand out silence instead.
let words = &mut scratch_buffer[..byte_count.div_ceil(size_of::<i32>())];
let dst = slice::from_raw_parts_mut(words.as_mut_ptr().cast::<u8>(), byte_count);
fill_equilibrium(dst, sample_format);
dst.as_mut_ptr().cast()
} else if sample_format == SampleFormat::I24 {
// WASAPI stores i24 in the upper bits
let source_data = slice::from_raw_parts(packet.cast(), byte_count / size_of::<i32>());
// use a scratch buffer since the capture buffer isn't meant to be written
let dst = &mut scratch_buffer[..source_data.len()];
dst.copy_from_slice(source_data);
for sample in dst.iter_mut() {
// On signed integers, >> is an arithmetic shift,
// which ensures the correct upper bits get shifted in
*sample >>= 8;
}
dst.as_mut_ptr().cast()
} else {
packet.cast()
}
}
}

// The loop for writing output data.
fn process_output(
stream: &StreamInner,
Expand All @@ -964,17 +991,9 @@ fn process_output(
};

let padding = stream.max_frames_in_buffer - frames_available;
let fill_usec = (padding as u64)
.saturating_mul(1_000_000)
.saturating_div(stream.config.sample_rate as u64)
.saturating_add(
stream
.stream_latency
.as_micros()
.try_into()
.unwrap_or(u64::MAX),
);
stream.fill_usec.store(fill_usec, Ordering::Relaxed);
stream
.fill_usec
.store(fill_usec(stream, padding), Ordering::Relaxed);

if stream.skip_callback.load(Ordering::Relaxed) {
// Skip the period instead of queuing silence a future resume would replay.
Expand Down Expand Up @@ -1021,9 +1040,29 @@ fn process_output(
*frames_written += frames_available as u64;
}

// Republish now the write has landed, so a concurrent stop() drains the whole tail.
stream.fill_usec.store(
fill_usec(stream, padding + frames_available),
Ordering::Relaxed,
);

Ok(())
}

// Time until the device has played out `frames` of queued audio, including its own latency.
fn fill_usec(stream: &StreamInner, frames: FrameCount) -> u64 {
(frames as u64)
.saturating_mul(1_000_000)
.saturating_div(stream.config.sample_rate as u64)
.saturating_add(
stream
.stream_latency
.as_micros()
.try_into()
.unwrap_or(u64::MAX),
)
}

/// Reads the stream's `IAudioClock` in a single `GetPosition` call, returning the callback
/// [`StreamInstant`] together with the device position from that same snapshot.
#[inline]
Expand Down
Loading