Skip to content
Merged
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **ASIO**: Timestamps now include driver-reported hardware latency.
- **ASIO**: Hardware latency is now re-queried when the driver reports `kAsioLatenciesChanged`.
- **ASIO**: Stream error callback now receives `StreamError::BufferUnderrun` on `kAsioResyncRequest`.
- **ASIO**: Stream error callback now receives `StreamError::StreamInvalidated` when the driver reports a sample rate change (`sampleRateDidChange`) of 1 Hz or more from the configured rate.
- **ASIO**: Stream error callback now receives `StreamError::StreamInvalidated` when the driver reports a sample
rate change (`sampleRateDidChange`) of 1 Hz or more from the configured rate.
- **AudioWorklet**: `BufferSize::Fixed` now sets `renderSizeHint` on the `AudioContext`.
- **CoreAudio**: Timestamps now include device latency and safety offset.
- **JACK**: Timestamps now use the precise hardware deadline.
- **Linux/BSD**: Default host in order from first to last available now is: PipeWire, PulseAudio, ALSA.
Expand Down
23 changes: 18 additions & 5 deletions examples/custom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ struct StreamControls {
pause: AtomicBool,
}

const CHANNEL_COUNT: cpal::ChannelCount = 2;
const BUFFER_SIZE: cpal::FrameCount = 4096;

impl HostTrait for MyHost {
type Device = MyDevice;
type Devices = std::iter::Once<MyDevice>;
Expand Down Expand Up @@ -81,10 +84,13 @@ impl DeviceTrait for MyDevice {
&self,
) -> Result<Self::SupportedOutputConfigs, cpal::SupportedStreamConfigsError> {
Ok(std::iter::once(cpal::SupportedStreamConfigRange::new(
2,
CHANNEL_COUNT,
44100,
44100,
cpal::SupportedBufferSize::Unknown,
cpal::SupportedBufferSize::Range {
min: BUFFER_SIZE,
max: BUFFER_SIZE,
},
cpal::SampleFormat::F32,
)))
}
Expand All @@ -99,9 +105,12 @@ impl DeviceTrait for MyDevice {
&self,
) -> Result<cpal::SupportedStreamConfig, cpal::DefaultStreamConfigError> {
Ok(cpal::SupportedStreamConfig::new(
2,
CHANNEL_COUNT,
44100,
cpal::SupportedBufferSize::Unknown,
cpal::SupportedBufferSize::Range {
min: BUFFER_SIZE,
max: BUFFER_SIZE,
},
cpal::SampleFormat::I16,
))
}
Expand Down Expand Up @@ -145,7 +154,7 @@ impl DeviceTrait for MyDevice {
let start = Instant::now();
let thread_controls = controls.clone();
let handle = std::thread::spawn(move || {
let mut buffer = [0.0_f32; 4096];
let mut buffer = [0.0_f32; BUFFER_SIZE as usize * CHANNEL_COUNT as usize];
while !thread_controls.exit.load(Ordering::Relaxed) {
std::thread::sleep(std::time::Duration::from_secs_f32(
buffer.len() as f32 / 44100.0,
Expand Down Expand Up @@ -203,6 +212,10 @@ impl StreamTrait for MyStream {
let elapsed = self.start.elapsed();
cpal::StreamInstant::new(elapsed.as_secs(), elapsed.subsec_nanos())
}

fn buffer_size(&self) -> Result<cpal::FrameCount, cpal::StreamError> {
Ok(BUFFER_SIZE)
}
}

// streams are expected to stop when dropped
Expand Down
10 changes: 3 additions & 7 deletions src/host/aaudio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -721,19 +721,15 @@ impl StreamTrait for Stream {
now_stream_instant()
}

fn buffer_size(&self) -> Option<crate::FrameCount> {
let stream = self.inner.lock().ok()?;
fn buffer_size(&self) -> Result<crate::FrameCount, crate::StreamError> {
let stream = self.inner.lock().unwrap();

// frames_per_data_callback is only set for BufferSize::Fixed; for Default AAudio
// schedules callbacks at the burst size, so that is the best available estimate.
let frames = match stream.frames_per_data_callback() {
Some(size) if size > 0 => size,
_ => stream.frames_per_burst(),
};
if frames > 0 {
Some(frames as crate::FrameCount)
} else {
None
}
Ok(frames as crate::FrameCount)
}
}
4 changes: 2 additions & 2 deletions src/host/alsa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1289,8 +1289,8 @@ impl StreamTrait for Stream {
.expect("stream duration exceeded `StreamInstant` range")
}

fn buffer_size(&self) -> Option<FrameCount> {
Some(self.inner.period_frames as FrameCount)
fn buffer_size(&self) -> Result<FrameCount, crate::StreamError> {
Ok(self.inner.period_frames as FrameCount)
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/host/asio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ impl StreamTrait for Stream {
Stream::now(self)
}

fn buffer_size(&self) -> Option<crate::FrameCount> {
fn buffer_size(&self) -> Result<crate::FrameCount, crate::StreamError> {
Stream::buffer_size(self)
}
}
9 changes: 5 additions & 4 deletions src/host/asio/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,14 @@ impl Stream {
Ok(())
}

pub fn buffer_size(&self) -> Option<crate::FrameCount> {
let streams = self.asio_streams.lock().ok()?;
streams
pub fn buffer_size(&self) -> Result<crate::FrameCount, crate::StreamError> {
let streams = self.asio_streams.lock().unwrap();
Ok(streams
.output
.as_ref()
.or(streams.input.as_ref())
.map(|s| s.buffer_size as crate::FrameCount)
.expect("ASIO stream has neither input nor output")
.buffer_size as crate::FrameCount)
}
}

Expand Down
31 changes: 30 additions & 1 deletion src/host/audioworklet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

mod dependent_module;
use js_sys::wasm_bindgen;
use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};

use crate::dependent_module;
use wasm_bindgen::prelude::*;
Expand All @@ -30,6 +34,7 @@ pub struct Host;

pub struct Stream {
audio_context: web_sys::AudioContext,
buffer_size_frames: Arc<AtomicU64>,
}

pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs};
Expand All @@ -41,6 +46,9 @@ const MAX_SAMPLE_RATE: SampleRate = 96_000;
const DEFAULT_SAMPLE_RATE: SampleRate = 44_100;
const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32;

// https://webaudio.github.io/web-audio-api/#render-quantum-size
const DEFAULT_RENDER_SIZE: u64 = 128;

impl Host {
pub fn new() -> Result<Self, crate::HostUnavailable> {
if Self::is_available() {
Expand Down Expand Up @@ -195,6 +203,13 @@ impl DeviceTrait for Device {

let stream_opts = web_sys::AudioContextOptions::new();
stream_opts.set_sample_rate(config.sample_rate as f32);
if let crate::BufferSize::Fixed(n) = config.buffer_size {
let _ = js_sys::Reflect::set(
stream_opts.as_ref(),
&JsValue::from_str("renderSizeHint"),
&JsValue::from_f64(n as f64),
);
}

let audio_context = web_sys::AudioContext::new_with_context_options(&stream_opts).map_err(
|err| -> BuildStreamError {
Expand All @@ -213,6 +228,12 @@ impl DeviceTrait for Device {
destination.set_channel_count(config.channels as u32);
}

let initial_quantum = match config.buffer_size {
crate::BufferSize::Fixed(n) => n as u64,
crate::BufferSize::Default => DEFAULT_RENDER_SIZE,
};
let buffer_size_frames = Arc::new(AtomicU64::new(initial_quantum));
let buffer_size_frames_cb = buffer_size_frames.clone();
let ctx = audio_context.clone();
wasm_bindgen_futures::spawn_local(async move {
let result: Result<(), JsValue> = async move {
Expand Down Expand Up @@ -255,6 +276,7 @@ impl DeviceTrait for Device {
&wasm_bindgen::memory(),
&WasmAudioProcessor::new(Box::new(
move |interleaved_data, frame_size, sample_rate, now| {
buffer_size_frames_cb.store(frame_size as u64, Ordering::Relaxed);
let data = interleaved_data.as_mut_ptr() as *mut ();
let mut data = unsafe {
Data::from_parts(data, interleaved_data.len(), sample_format)
Expand Down Expand Up @@ -295,11 +317,18 @@ impl DeviceTrait for Device {
}
});

Ok(Stream { audio_context })
Ok(Stream {
audio_context,
buffer_size_frames,
})
}
}

impl StreamTrait for Stream {
fn buffer_size(&self) -> Result<crate::FrameCount, crate::StreamError> {
Ok(self.buffer_size_frames.load(Ordering::Relaxed) as crate::FrameCount)
}

fn play(&self) -> Result<(), PlayStreamError> {
match self.audio_context.resume() {
Ok(_) => Ok(()),
Expand Down
4 changes: 2 additions & 2 deletions src/host/coreaudio/ios/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,8 @@ impl StreamTrait for Stream {
host_time_to_stream_instant(m_host_time).expect("mach_timebase_info failed")
}

fn buffer_size(&self) -> Option<crate::FrameCount> {
Some(get_device_buffer_frames() as crate::FrameCount)
fn buffer_size(&self) -> Result<crate::FrameCount, crate::StreamError> {
Ok(get_device_buffer_frames() as crate::FrameCount)
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/host/coreaudio/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,12 @@ impl StreamTrait for Stream {
host_time_to_stream_instant(m_host_time).expect("mach_timebase_info failed")
}

fn buffer_size(&self) -> Option<crate::FrameCount> {
let stream = self.inner.lock().ok()?;
fn buffer_size(&self) -> Result<crate::FrameCount, crate::StreamError> {
let stream = self.inner.lock().unwrap();

device::get_device_buffer_frame_size(&stream.audio_unit)
.ok()
.map(|size| size as crate::FrameCount)
.map_err(|_| crate::StreamError::DeviceNotAvailable)
}
}

Expand Down
9 changes: 9 additions & 0 deletions src/host/custom/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ trait StreamErased: Send + Sync {
fn play(&self) -> Result<(), PlayStreamError>;
fn pause(&self) -> Result<(), PauseStreamError>;
fn now(&self) -> StreamInstant;
fn buffer_size(&self) -> Result<crate::FrameCount, crate::StreamError>;
}

fn device_to_erased(d: impl DeviceErased + 'static) -> Device {
Expand Down Expand Up @@ -317,6 +318,10 @@ where
fn now(&self) -> StreamInstant {
<T as StreamTrait>::now(self)
}

fn buffer_size(&self) -> Result<crate::FrameCount, crate::StreamError> {
<T as StreamTrait>::buffer_size(self)
}
}

// implementations of HostTrait, DeviceTrait, and StreamTrait for custom versions
Expand Down Expand Up @@ -444,4 +449,8 @@ impl StreamTrait for Stream {
fn now(&self) -> StreamInstant {
self.0.now()
}

fn buffer_size(&self) -> Result<crate::FrameCount, crate::StreamError> {
self.0.buffer_size()
}
}
33 changes: 20 additions & 13 deletions src/host/emscripten/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use web_sys::AudioContext;

use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
use crate::{
BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription,
DeviceDescriptionBuilder, DeviceId, DeviceIdError, DeviceNameError, DevicesError,
BufferSize, BuildStreamError, ChannelCount, Data, DefaultStreamConfigError, DeviceDescription,
DeviceDescriptionBuilder, DeviceId, DeviceIdError, DeviceNameError, DevicesError, FrameCount,
InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat,
SampleRate, StreamConfig, StreamError, StreamInstant, SupportedBufferSize,
SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError,
Expand All @@ -36,8 +36,8 @@ pub struct Device;
#[wasm_bindgen]
#[derive(Clone)]
pub struct Stream {
// A reference to an `AudioContext` object.
audio_ctxt: AudioContext,
buffer_size_frames: FrameCount,
}

// WASM runs in a single-threaded environment, so Send and Sync are safe by design.
Expand All @@ -50,14 +50,14 @@ crate::assert_stream_sync!(Stream);

pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs};

const MIN_CHANNELS: u16 = 1;
const MAX_CHANNELS: u16 = 32;
const MIN_CHANNELS: ChannelCount = 1;
const MAX_CHANNELS: ChannelCount = 32;
const MIN_SAMPLE_RATE: SampleRate = 8_000;
const MAX_SAMPLE_RATE: SampleRate = 96_000;
const DEFAULT_SAMPLE_RATE: SampleRate = 44_100;
const MIN_BUFFER_SIZE: u32 = 1;
const MAX_BUFFER_SIZE: u32 = u32::MAX;
const DEFAULT_BUFFER_SIZE: usize = 2048;
const MIN_BUFFER_SIZE: FrameCount = 1;
const MAX_BUFFER_SIZE: FrameCount = FrameCount::MAX;
const DEFAULT_BUFFER_SIZE: FrameCount = 2048;
const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32;

impl Host {
Expand Down Expand Up @@ -219,14 +219,17 @@ impl DeviceTrait for Device {
if !(MIN_BUFFER_SIZE..=MAX_BUFFER_SIZE).contains(&v) {
return Err(BuildStreamError::StreamConfigNotSupported);
}
v as usize
v
}
BufferSize::Default => DEFAULT_BUFFER_SIZE,
};

// Create the stream.
let audio_ctxt = AudioContext::new().expect("webaudio is not present on this system");
let stream = Stream { audio_ctxt };
let stream = Stream {
audio_ctxt,
buffer_size_frames,
};

// Use `set_timeout` to invoke a Rust callback repeatedly.
//
Expand All @@ -241,14 +244,18 @@ impl DeviceTrait for Device {
data_callback,
config,
sample_format,
buffer_size_frames as u32,
buffer_size_frames,
);

Ok(stream)
}
}

impl StreamTrait for Stream {
fn buffer_size(&self) -> Result<crate::FrameCount, crate::StreamError> {
Ok(self.buffer_size_frames)
}

fn play(&self) -> Result<(), PlayStreamError> {
let future = JsFuture::from(
self.audio_ctxt
Expand Down Expand Up @@ -286,7 +293,7 @@ impl StreamTrait for Stream {

fn audio_callback_fn<D>(
mut data_callback: AssertUnwindSafe<D>,
) -> impl FnOnce(Stream, StreamConfig, SampleFormat, u32)
) -> impl FnOnce(Stream, StreamConfig, SampleFormat, FrameCount)
where
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
{
Expand Down Expand Up @@ -368,7 +375,7 @@ fn set_timeout<D>(
data_callback: AssertUnwindSafe<D>,
config: StreamConfig,
sample_format: SampleFormat,
buffer_size_frames: u32,
buffer_size_frames: FrameCount,
) where
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
{
Expand Down
4 changes: 2 additions & 2 deletions src/host/jack/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,8 @@ impl StreamTrait for Stream {
micros_to_stream_instant(self.async_client.as_client().time())
}

fn buffer_size(&self) -> Option<crate::FrameCount> {
Some(self.async_client.as_client().buffer_size() as crate::FrameCount)
fn buffer_size(&self) -> Result<crate::FrameCount, crate::StreamError> {
Ok(self.async_client.as_client().buffer_size() as crate::FrameCount)
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/host/null/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ impl StreamTrait for Stream {
fn now(&self) -> crate::StreamInstant {
unimplemented!()
}

fn buffer_size(&self) -> Result<crate::FrameCount, crate::StreamError> {
unimplemented!()
}
}

impl Iterator for Devices {
Expand Down
Loading
Loading