diff --git a/CHANGELOG.md b/CHANGELOG.md index b163261e7..4491b55dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,9 +37,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - **ALSA**: Fix a remaining timestamp segfault on 32-bit platforms with a 64-bit kernel `time_t`. +- **ALSA**: Improved enumeration accuracy for supported format, channel and rate combinations. +- **ASIO**: `Stream` no longer risks blocking or panicking in the driver callback while another stream is being created or destroyed. +- **ASIO**: Output streams now start with silence instead of undefined content in the first buffer period. - **AudioWorklet**: Fix processor construction failures not being reported to `error_callback`. +- **CoreAudio**: Fix the device running at a different sample rate from the stream on hardware that reports a continuous rate range. +- **CoreAudio**: Fix `supported_configs()` only reporting `F32`, even on hardware that also supports other sample formats. - **JACK**: Channel enumeration is capped at the physical system port count again. +- **JACK**: Streams no longer panic when the server delivers a larger period than the negotiated buffer size. +- **PipeWire**: Fix an empty chunk being emitted when a cycle requests no frames. +- **PipeWire**: Fix capture reading from the wrong offset in the buffer on some devices. - **WASAPI**: Device enumeration no longer panics if the COM enumerator fails to initialize. +- **WASAPI**: Output streams now start with silence instead of undefined content in the render buffer. +- **WASAPI**: Fix `I64` and `F64` incorrectly reported as supported output formats. ## [0.18.2] - 2026-08-16 diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 076306d29..c98a8386d 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -535,64 +535,46 @@ impl Device { //SND_PCM_FORMAT_U18_3BE, ]; - let min_rate = hw_params.get_rate_min()?; - let max_rate = hw_params.get_rate_max()?; - - let sample_rates = if min_rate == max_rate || hw_params.test_rate(min_rate + 1).is_ok() { - // Fixed rate or continuous range. - vec![(min_rate, max_rate)] - } else { - // Discrete rates: probe the standard list plus the hardware's own min and max so - // that rates outside `COMMON_SAMPLE_RATES` are not missed. - let mut probe: Vec = COMMON_SAMPLE_RATES.to_vec(); - probe.push(min_rate); - probe.push(max_rate); - probe.sort_unstable(); - probe.dedup(); - probe - .into_iter() - .filter(|&r| (min_rate..=max_rate).contains(&r) && hw_params.test_rate(r).is_ok()) - .map(|r| (r, r)) - .collect() - }; - - let min_channels = hw_params.get_channels_min()?; // 64 = AES10 (MADI) maximum; also prevents spinning on plugins like plughw that report u32::MAX. const CHANNEL_ENUM_CAP: u32 = 64; - let max_channels = hw_params - .get_channels_max()? - .min(CHANNEL_ENUM_CAP) - .min(ChannelCount::MAX as u32); - - let supported_channels: Vec = - if min_channels == max_channels || hw_params.test_channels(min_channels + 1).is_ok() { - (min_channels..=max_channels) - .map(|c| c as ChannelCount) - .collect() - } else { - (min_channels..=max_channels) - .filter(|&c| hw_params.test_channels(c).is_ok()) - .map(|c| c as ChannelCount) - .collect() - }; - let mut output = - Vec::with_capacity(FORMATS.len() * supported_channels.len() * sample_rates.len()); + let mut output = Vec::new(); let mut seen_formats: Vec = Vec::with_capacity(FORMATS.len()); // Key: (channels, physical width in bits) with 4 physical widths (8/16/32/64 bits) let mut buffer_size_cache: HashMap<(ChannelCount, u32), SupportedBufferSize> = - HashMap::with_capacity(supported_channels.len() * 4); + HashMap::new(); + // `test_*` checks a value, it doesn't apply it, so format/channels/rate are each set on a + // clone in sequence rather than tested independently against the same unconstrained params. for &(sample_format, alsa_format) in FORMATS.iter() { - if seen_formats.contains(&sample_format) || hw_params.test_format(alsa_format).is_err() - { + if seen_formats.contains(&sample_format) { + continue; + } + let format_params = hw_params.clone(); + if format_params.set_format(alsa_format).is_err() { continue; } seen_formats.push(sample_format); let width = alsa_format.physical_width().unwrap_or(0) as u32; - for &channels in &supported_channels { + let (Ok(min_channels), Ok(max_channels)) = ( + format_params.get_channels_min(), + format_params.get_channels_max(), + ) else { + continue; + }; + let max_channels = max_channels + .min(CHANNEL_ENUM_CAP) + .min(ChannelCount::MAX as u32); + + for raw_channels in min_channels..=max_channels { + let channel_params = format_params.clone(); + if channel_params.set_channels(raw_channels).is_err() { + continue; + } + let channels = raw_channels as ChannelCount; + let buffer_size = *buffer_size_cache .entry((channels, width)) @@ -600,6 +582,34 @@ impl Device { supported_period_size_range(&hw_params, alsa_format, channels) }); + let (Ok(min_rate), Ok(max_rate)) = + (channel_params.get_rate_min(), channel_params.get_rate_max()) + else { + continue; + }; + + let sample_rates = + if min_rate == max_rate || channel_params.test_rate(min_rate + 1).is_ok() { + // Fixed rate or continuous range. + vec![(min_rate, max_rate)] + } else { + // Discrete rates: probe the standard list plus the hardware's own min and max + // so that rates outside `COMMON_SAMPLE_RATES` are not missed. + let mut probe: Vec = COMMON_SAMPLE_RATES.to_vec(); + probe.push(min_rate); + probe.push(max_rate); + probe.sort_unstable(); + probe.dedup(); + probe + .into_iter() + .filter(|&r| { + (min_rate..=max_rate).contains(&r) + && channel_params.test_rate(r).is_ok() + }) + .map(|r| (r, r)) + .collect() + }; + for &(min_rate, max_rate) in sample_rates.iter() { output.push(SupportedStreamConfigRange { channels, diff --git a/src/host/asio/stream.rs b/src/host/asio/stream.rs index ed316b2cb..5adb6cd08 100644 --- a/src/host/asio/stream.rs +++ b/src/host/asio/stream.rs @@ -223,8 +223,9 @@ impl Device { } last_buffer_index = callback_info.buffer_index; - // There is 0% chance of lock contention the host only locks when recreating streams. - let stream_lock = asio_streams.lock().unwrap(); + let Ok(stream_lock) = asio_streams.lock() else { + return; + }; let asio_stream = match stream_lock.input { Some(ref asio_stream) => asio_stream, None => return, @@ -448,6 +449,8 @@ impl Device { let driver = Arc::new(driver); let asio_streams = driver.streams(); + prefill_output_silence(&driver, &asio_streams); + if let Err(e) = driver.start() { driver.remove_event_callback(driver_event_callback_id); driver.remove_callback(callback_id); @@ -577,8 +580,9 @@ impl Device { } last_buffer_index = callback_info.buffer_index; - // There is 0% chance of lock contention the host only locks when recreating streams. - let mut stream_lock = asio_streams.lock().unwrap(); + let Ok(mut stream_lock) = asio_streams.lock() else { + return; + }; let asio_stream = match stream_lock.output { Some(ref mut asio_stream) => asio_stream, None => return, @@ -854,6 +858,8 @@ impl Device { let driver = Arc::new(driver); let asio_streams = driver.streams(); + prefill_output_silence(&driver, &asio_streams); + if let Err(e) = driver.start() { driver.remove_event_callback(driver_event_callback_id); driver.remove_callback(callback_id); @@ -1273,6 +1279,27 @@ unsafe fn asio_channel_slice_mut( unsafe { std::slice::from_raw_parts_mut(buff_ptr, channel_length) } } +// ASIOStart() plays buffer half 1 immediately, before the first bufferSwitch can fill it; +// half 0 is covered by that first callback. +fn prefill_output_silence(driver: &sys::Driver, asio_streams: &Mutex) { + if let Ok(mut streams) = asio_streams.lock() { + if let Some(ref mut output) = streams.output { + if let Some(sample_format) = driver + .output_data_type() + .ok() + .and_then(|ty| super::device::convert_data_type(&ty)) + { + let byte_len = output.buffer_size as usize * sample_format.sample_size(); + for ch_ix in 0..output.buffer_infos.len() { + let channel = + unsafe { asio_channel_slice_mut::(output, 1, ch_ix, Some(byte_len)) }; + fill_equilibrium(channel, sample_format); + } + } + } + } +} + fn load_driver_err(e: sys::LoadDriverError) -> Error { match e { sys::LoadDriverError::LoadDriverFailed | sys::LoadDriverError::DriverAlreadyExists => { diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index bde36af6b..7aec4cc87 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -15,7 +15,7 @@ use coreaudio::audio_unit::{ audio_format::LinearPcmFlags, macos_helpers::{ RateListener, audio_unit_from_device_id_uninitialized, find_matching_physical_format, - get_device_name, set_device_physical_stream_format, + get_device_name, get_supported_physical_stream_formats, set_device_physical_stream_format, }, render_callback::{self, data}, }; @@ -526,18 +526,7 @@ impl Device { n_channels += buf.mNumberChannels as usize; } - // TODO: macOS should support U8, I16, I32, F32 and F64. This should allow for using - // I16 but just use F32 for now as it's the default anyway. - let sample_format = SampleFormat::F32; - // Get available sample rate ranges. - // The property "kAudioDevicePropertyAvailableNominalSampleRates" returns a list of pairs of - // minimum and maximum sample rates but most of the devices returns pairs of same values though the underlying mechanism is unclear. - // This may cause issues when, for example, sorting the configs by the sample rates. - // We follows the implementation of RtAudio, which returns single element of config - // when all the pairs have the same values and returns multiple elements otherwise. - // See https://github.com/thestk/rtaudio/blob/master/RtAudio.cpp#L1369C1-L1375C39 - property_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; let mut data_size = 0u32; let status = AudioObjectGetPropertyDataSize( @@ -575,19 +564,55 @@ impl Device { } let buffer_size = get_io_buffer_frame_size_range(self.audio_device_id)?; - // Most hardware reports discrete rates (mMinimum == mMaximum); some aggregate or - // virtual devices report continuous ranges. - let fmts: Vec<_> = ranges - .iter() - .map(|range| SupportedStreamConfigRange { - channels: n_channels as ChannelCount, - min_sample_rate: range.mMinimum as u32, - max_sample_rate: range.mMaximum as u32, - buffer_size, - sample_format, - }) - .collect(); - Ok(fmts.into_iter()) + // AUHAL always converts to and from F32 regardless of the physical format, so + // advertise it at every nominal rate (most hardware reports discrete rates, i.e. + // mMinimum == mMaximum; some aggregate or virtual devices report continuous ranges). + let f32_fmts = ranges.iter().map(|range| SupportedStreamConfigRange { + channels: n_channels as ChannelCount, + min_sample_rate: range.mMinimum as u32, + max_sample_rate: range.mMaximum as u32, + buffer_size, + sample_format: SampleFormat::F32, + }); + + // The hardware's own physical formats, so integer-only devices advertise their + // bit-perfect paths instead of only the AUHAL-converted F32 one. + let physical_fmts = get_supported_physical_stream_formats(self.audio_device_id) + .unwrap_or_default() + .into_iter() + .filter_map(|fmt| { + let Some(coreaudio::audio_unit::AudioFormat::LinearPCM(flags)) = + coreaudio::audio_unit::AudioFormat::from_format_and_flag( + fmt.mFormat.mFormatID, + Some(fmt.mFormat.mFormatFlags), + ) + else { + return None; + }; + let sample_format = match CoreAudioSampleFormat::from_flags_and_bits_per_sample( + flags, + fmt.mFormat.mBitsPerChannel, + )? { + CoreAudioSampleFormat::I8 => SampleFormat::I8, + CoreAudioSampleFormat::I16 => SampleFormat::I16, + CoreAudioSampleFormat::I24 => SampleFormat::I24, + CoreAudioSampleFormat::I32 => SampleFormat::I32, + // Already covered by f32_fmts at every rate, not just this row's range. + CoreAudioSampleFormat::F32 => return None, + }; + Some(SupportedStreamConfigRange { + channels: fmt.mFormat.mChannelsPerFrame as ChannelCount, + min_sample_rate: fmt.mSampleRateRange.mMinimum as u32, + max_sample_rate: fmt.mSampleRateRange.mMaximum as u32, + buffer_size, + sample_format, + }) + }); + + Ok(f32_fmts + .chain(physical_fmts) + .collect::>() + .into_iter()) } } @@ -711,14 +736,15 @@ impl Device { // Set the physical stream format (bit depth + sample rate) on the hardware device. // This avoids unnecessary format conversions, which is especially important on aggregate - // devices. Falls back to sample-rate-only if no matching physical format is available. - if set_physical_format( + // devices. Falls back to sample-rate-only if no matching physical format is available, or + // if the closest match found doesn't actually run at the requested rate. + if !set_physical_format( self.audio_device_id, config.sample_rate, config.channels, sample_format, ) - .is_err() + .is_ok_and(|asbd| (asbd.mSampleRate - config.sample_rate as f64).abs() < 1.0) { set_sample_rate(self.audio_device_id, config.sample_rate, timeout)?; } @@ -845,14 +871,15 @@ impl Device { // Best-effort: set the physical stream format (bit depth + sample rate) on the hardware. // This avoids unnecessary conversions, especially on aggregate devices. Not an error if - // it fails — the AudioUnit will handle format conversion as before. - if set_physical_format( + // it fails: the AudioUnit will handle format conversion as before. Also falls back if the + // closest match found doesn't actually run at the requested rate. + if !set_physical_format( self.audio_device_id, config.sample_rate, config.channels, sample_format, ) - .is_err() + .is_ok_and(|asbd| (asbd.mSampleRate - config.sample_rate as f64).abs() < 1.0) { set_sample_rate(self.audio_device_id, config.sample_rate, timeout)?; } diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index 79da78084..f88306af7 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -4,7 +4,6 @@ use std::sync::{ }; use super::JACK_SAMPLE_FORMAT; -#[cfg(feature = "realtime")] use crate::host::try_emit_error; use crate::{ CallbackInfo, ChannelCount, Data, Error, ErrorKind, FrameCount, ResultExt, Sample, SampleRate, @@ -79,7 +78,6 @@ impl Stream { None, playback_state.clone(), pending_xrun.clone(), - #[cfg(feature = "realtime")] error_callback_ptr.clone(), ); @@ -138,7 +136,6 @@ impl Stream { Some(Box::new(data_callback)), playback_state.clone(), pending_xrun.clone(), - #[cfg(feature = "realtime")] error_callback_ptr.clone(), ); @@ -285,8 +282,8 @@ struct LocalProcessHandler { temp_output_buffer: Vec, playback_state: Arc, pending_xrun: Arc, - #[cfg(feature = "realtime")] error_callback: ErrorCallbackArc, + oversized_reported: bool, #[cfg(feature = "realtime")] rt_checked: bool, } @@ -302,7 +299,7 @@ impl LocalProcessHandler { output_data_callback: Option, playback_state: Arc, pending_xrun: Arc, - #[cfg(feature = "realtime")] error_callback: ErrorCallbackArc, + error_callback: ErrorCallbackArc, ) -> Self { let temp_input_buffer = vec![f32::EQUILIBRIUM; in_ports.len() * buffer_size]; let temp_output_buffer = vec![f32::EQUILIBRIUM; out_ports.len() * buffer_size]; @@ -318,8 +315,8 @@ impl LocalProcessHandler { temp_output_buffer, playback_state, pending_xrun, - #[cfg(feature = "realtime")] error_callback, + oversized_reported: false, #[cfg(feature = "realtime")] rt_checked: false, } @@ -414,9 +411,25 @@ impl jack::ProcessHandler for LocalProcessHandler { } } - // This should be equal to self.buffer_size, but the implementation will - // work even if it is less. Will panic in `temp_buffer_to_data` if greater. - let current_frame_count = process_scope.n_frames() as usize; + // This should be equal to self.buffer_size, but the implementation will work even if + // it is less. A greater count is truncated to the temp buffers' capacity. + let requested_frame_count = process_scope.n_frames() as usize; + let current_frame_count = requested_frame_count.min(self.buffer_size); + if requested_frame_count > self.buffer_size { + if !self.oversized_reported { + let message = format!( + "JACK delivered a {requested_frame_count}-frame period, exceeding the configured buffer size of {}; truncated", + self.buffer_size + ); + self.oversized_reported = try_emit_error( + &self.error_callback, + Error::with_message(ErrorKind::BackendError, message), + ) + .is_ok(); + } + } else { + self.oversized_reported = false; + } // Get timestamp data let (current_start_usecs, next_usecs_opt) = match process_scope.cycle_times() { @@ -517,6 +530,8 @@ impl jack::ProcessHandler for LocalProcessHandler { for i in 0..current_frame_count { output_channel[i] = self.temp_output_buffer[ch_ix + i * num_out_channels]; } + // A truncated cycle leaves the tail of JACK's port buffer unwritten. + output_channel[current_frame_count..requested_frame_count].fill(f32::EQUILIBRIUM); } } @@ -618,7 +633,7 @@ impl jack::NotificationHandler for JackNotificationHandler { } fn xrun(&mut self, _: &jack::Client) -> jack::Control { - if StreamState::load(&self.playback_state, Ordering::Relaxed) != StreamState::Starting { + if StreamState::load(&self.playback_state, Ordering::Relaxed) == StreamState::Playing { self.pending_xrun.store(true, Ordering::Relaxed); } jack::Control::Continue diff --git a/src/host/mod.rs b/src/host/mod.rs index b109acb59..d52ccff17 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -216,7 +216,6 @@ pub(crate) use error_emit::emit_error; target_os = "android", all( feature = "jack", - feature = "realtime", any( target_os = "linux", target_os = "dragonfly", diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index e9351dc97..1e0d4a834 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -816,8 +816,12 @@ where } if let Some(mut buffer) = stream.dequeue_buffer() { - // Read the requested frame count before mutably borrowing datas_mut(). - let requested = buffer.requested() as usize; + // Read the requested frame count before mutably borrowing datas_mut(); fall back + // to the last negotiated quantum when a cycle outside the driver's schedule reports 0. + let requested = match buffer.requested() as usize { + 0 => user_data.last_quantum.load(Ordering::Relaxed) as usize, + requested => requested, + }; let datas = buffer.datas_mut(); if datas.is_empty() { return; @@ -1092,16 +1096,23 @@ where return; } let data = &mut datas[0]; - let n_samples = data.chunk().size() / user_data.sample_format.sample_size() as u32; - let frames = n_samples / n_channels; + let stride = user_data.sample_format.sample_size() * n_channels as usize; + let offset = data.chunk().offset() as usize; + let size = data.chunk().size() as usize; let Some(samples) = data.data() else { return; }; - let data = samples.as_mut_ptr() as *mut (); - let data = - unsafe { Data::from_parts(data, n_samples as usize, user_data.sample_format) }; - user_data.publish_data_in(stream, frames as usize, &data, xrun); + // offset/size semantics: spa/buffer/buffer.h. + let maxsize = samples.len(); + let offset = offset % maxsize; + let frames = size.min(maxsize - offset) / stride; + let valid = &mut samples[offset..offset + frames * stride]; + + let ptr = valid.as_mut_ptr() as *mut (); + let n_samples = frames * n_channels as usize; + let data = unsafe { Data::from_parts(ptr, n_samples, user_data.sample_format) }; + user_data.publish_data_in(stream, frames, &data, xrun); } }) .register()?; diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 3068e29db..74faa29b0 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -187,19 +187,15 @@ unsafe fn data_flow_from_immendpoint(endpoint: &Audio::IMMEndpoint) -> Audio::ED } // Given the audio client and format, returns whether the audio engine supports it natively in -// shared mode without format conversion. +// the given share mode without format conversion. pub unsafe fn is_format_supported( client: &Audio::IAudioClient, waveformatex_ptr: *const Audio::WAVEFORMATEX, + share_mode: Audio::AUDCLNT_SHAREMODE, ) -> Result { let mut closest_match: *mut Audio::WAVEFORMATEX = ptr::null_mut(); - let hr = unsafe { - client.IsFormatSupported( - Audio::AUDCLNT_SHAREMODE_SHARED, - waveformatex_ptr, - Some(&mut closest_match), - ) - }; + let hr = + unsafe { client.IsFormatSupported(share_mode, waveformatex_ptr, Some(&mut closest_match)) }; if !closest_match.is_null() { let _free = WaveFormatExPtr(closest_match); } @@ -646,7 +642,11 @@ impl Device { .context("Failed to get mix format")?; // If the default format can't succeed we have no hope of finding other formats. - if !is_format_supported(client, default_waveformatex_ptr.0)? { + if !is_format_supported( + client, + default_waveformatex_ptr.0, + Audio::AUDCLNT_SHAREMODE_SHARED, + )? { return Err(Error::with_message( ErrorKind::UnsupportedConfig, "Could not determine support for default audio format", @@ -717,11 +717,13 @@ impl Device { buffer_size: BufferSize::Default, }, sample_format, + None, ) { let usable = is_output || is_format_supported( client, &waveformat.Format as *const Audio::WAVEFORMATEX, + Audio::AUDCLNT_SHAREMODE_SHARED, )?; if usable { supported_formats.push(SupportedStreamConfigRange { @@ -867,7 +869,7 @@ impl Device { // Computing the format and initializing the device. let waveformatex = { - let format_attempt = config_to_waveformatextensible(config, sample_format) + let format_attempt = config_to_waveformatextensible(config, sample_format, None) .ok_or_else(|| { Error::with_message( ErrorKind::UnsupportedConfig, @@ -970,7 +972,7 @@ impl Device { // Computing the format and initializing the device. let waveformatex = { - let format_attempt = config_to_waveformatextensible(config, sample_format) + let format_attempt = config_to_waveformatextensible(config, sample_format, None) .ok_or_else(|| { Error::with_message( ErrorKind::UnsupportedConfig, @@ -1349,14 +1351,12 @@ const OUTPUT_MAX_SAMPLE_RATE: SampleRate = 384_000; // Formats encodable as WAVEFORMATEXTENSIBLE. U8/I16 map to WAVE_FORMAT_PCM; the rest use // WAVE_FORMAT_EXTENSIBLE. Unsigned formats wider than 8 bits are omitted: KSDATAFORMAT_SUBTYPE_PCM // is always signed for 16-bit and wider, so submitting unsigned data would produce a DC offset. -const WAVEFORMATEXTENSIBLE_SAMPLE_FORMATS: [SampleFormat; 7] = [ +const WAVEFORMATEXTENSIBLE_SAMPLE_FORMATS: [SampleFormat; 5] = [ SampleFormat::U8, SampleFormat::I16, SampleFormat::I24, SampleFormat::I32, - SampleFormat::I64, SampleFormat::F32, - SampleFormat::F64, ]; // Turns a `Format` into a `WAVEFORMATEXTENSIBLE`. @@ -1365,6 +1365,7 @@ const WAVEFORMATEXTENSIBLE_SAMPLE_FORMATS: [SampleFormat; 7] = [ fn config_to_waveformatextensible( config: StreamConfig, sample_format: SampleFormat, + channel_mask: Option, ) -> Option { let format_tag = match sample_format { SampleFormat::U8 | SampleFormat::I16 => Audio::WAVE_FORMAT_PCM, @@ -1405,8 +1406,9 @@ fn config_to_waveformatextensible( cbSize: cb_size, }; - // CPAL does not care about speaker positions, so pass audio right through. - let channel_mask = KernelStreaming::KSAUDIO_SPEAKER_DIRECTOUT; + // By default CPAL does not care about speaker positions, so pass audio right through. + // Exclusive mode negotiation supplies its own mask instead. + let channel_mask = channel_mask.unwrap_or(KernelStreaming::KSAUDIO_SPEAKER_DIRECTOUT); let sub_format = match sample_format { SampleFormat::U8 diff --git a/src/host/wasapi/stream.rs b/src/host/wasapi/stream.rs index 873e2cf11..b19b129bb 100644 --- a/src/host/wasapi/stream.rs +++ b/src/host/wasapi/stream.rs @@ -577,6 +577,19 @@ fn process_commands(run_context: &mut RunContext) -> Result { match command { Command::PlayStream => unsafe { if !run_context.stream.playing { + // Start() needs a primed buffer, or there's an audible gap until the engine + // gets real data from the first callback. + if let AudioClientFlow::Render { ref render_client } = + run_context.stream.client_flow + { + // PlayStream also fires on resume from pause, where the buffer wasn't + // reset and may already be full. + let frames = get_available_frames(&run_context.stream)?; + if frames > 0 { + write_silence(render_client, &run_context.stream, frames)?; + } + } + run_context .stream .audio_client @@ -652,6 +665,22 @@ fn get_available_frames(stream: &StreamInner) -> Result { } } +// Fills `frames` of the render buffer with silence and releases it. +unsafe fn write_silence( + render_client: &Audio::IAudioRenderClient, + stream: &StreamInner, + frames: FrameCount, +) -> Result<(), Error> { + unsafe { + let buffer = render_client.GetBuffer(frames)?; + debug_assert!(!buffer.is_null()); + let byte_count = frames as usize * stream.bytes_per_frame as usize; + let buffer_slice = std::slice::from_raw_parts_mut(buffer, byte_count); + fill_equilibrium(buffer_slice, stream.sample_format); + render_client.ReleaseBuffer(frames, 0).map_err(Into::into) + } +} + fn run_input( mut run_ctxt: RunContext, data_callback: &mut dyn FnMut(&Data, &CallbackInfo),