diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bb44cf3b..90d8eaa7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **WASAPI**: Capture streams no longer report a spurious xrun on the first buffer after starting. - **WASAPI**: Output streams no longer reject formats that the built-in resampler can convert. - **WASAPI**: Device enumeration no longer panics if the COM enumerator fails to initialize. +- **WASAPI**: Fix 24-bit samples not getting shifted from/to MSB. - **WebAudio**: Fix stale audio output when a data callback wrote a partial buffer. - **WebAudio**: Fix unsound `Send + Sync` on `Stream` when compiled with `+atomics`. - **WebAudio**: Fix `Host::is_available()` always returning `true`, even in non-window contexts. diff --git a/src/host/wasapi/stream.rs b/src/host/wasapi/stream.rs index 40563e125..873e2cf11 100644 --- a/src/host/wasapi/stream.rs +++ b/src/host/wasapi/stream.rs @@ -1,7 +1,7 @@ use std::{ mem, ops::ControlFlow, - ptr, + ptr, slice, sync::{ Arc, atomic::{AtomicBool, AtomicU64, Ordering}, @@ -665,6 +665,15 @@ fn run_input( emit_error(error_callback, err); } + 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::() + } else { + // The scratch buffer won't be used in this case. + 0 // Vec::with_capacity(0) does not allocate. + }; + 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, @@ -675,7 +684,12 @@ fn run_input( AudioClientFlow::Capture { ref capture_client } => capture_client.clone(), _ => unreachable!(), }; - if let Err(err) = process_input(&run_ctxt.stream, capture_client, data_callback) { + if let Err(err) = process_input( + &run_ctxt.stream, + capture_client, + data_callback, + &mut scratch_buffer, + ) { emit_error(error_callback, err); break; } @@ -815,6 +829,7 @@ fn process_input( stream: &StreamInner, capture_client: Audio::IAudioCaptureClient, data_callback: &mut dyn FnMut(&Data, &CallbackInfo), + scratch_buffer: &mut [i32], ) -> Result<(), Error> { unsafe { // Get the available data in the shared buffer. @@ -850,10 +865,26 @@ fn process_input( && flags & Audio::AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY.0 as u32 != 0; 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::()); + // 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; + } - let data = buffer as *mut (); - let len = frames_available as usize * stream.bytes_per_frame as usize - / stream.sample_format.sample_size(); + 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); if !stream.draining.load(Ordering::Relaxed) { @@ -921,6 +952,18 @@ fn process_output( xrun: false, }, ); + + if stream.sample_format == SampleFormat::I24 { + // WASAPI stores i24 in the upper bits + #[expect( + clippy::cast_ptr_alignment, + reason = "WASAPI guarantees the buffer to be aligned to a frame boundary" + )] + let buffer_slice_i32 = slice::from_raw_parts_mut(buffer.cast::(), len); + for sample in buffer_slice_i32 { + *sample <<= 8; + } + } } render_client.ReleaseBuffer(frames_available, 0)?;