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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- `StreamTrait::stop` ends a stream gracefully, draining buffered audio before halting (blocking up to a caller-supplied timeout). Dropping a stream still halts immediately without draining.
- `CallbackInfo::xrun()` reports buffer over/underruns via the data callback.
- **AudioWorklet**: Input streams are now supported.
- **WebAudio**: Input streams are now supported.

### Changed

Expand All @@ -35,6 +37,7 @@ 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`.
- **AudioWorklet**: Fix processor construction failures not being reported to `error_callback`.

## [Unreleased] (v0.18.2)

Expand Down
12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ audioworklet = [
"web-sys/AudioWorklet",
"web-sys/AudioWorkletNode",
"web-sys/AudioWorkletNodeOptions",
"web-sys/ChannelCountMode",
"web-sys/Event",
]

# Support for user-defined custom hosts, devices, and streams
Expand Down Expand Up @@ -87,6 +89,16 @@ wasm-bindgen = [
"dep:wasm-bindgen-futures",
"dep:futures-channel",
"dep:futures-util",
"web-sys/Navigator",
"web-sys/MediaDevices",
"web-sys/MediaStream",
"web-sys/MediaStreamConstraints",
"web-sys/MediaStreamTrack",
"web-sys/MediaStreamAudioSourceNode",
"web-sys/ScriptProcessorNode",
"web-sys/AudioProcessingEvent",
"web-sys/GainNode",
"web-sys/AudioParam",
]

[dependencies]
Expand Down
2 changes: 2 additions & 0 deletions examples/audioworklet-beep/.cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ rustflags = [
"link-arg=--export=__tls_align",
"-C",
"link-arg=--export=__tls_base",
"-C",
"link-arg=--export=__heap_base",
]

[unstable]
Expand Down
3 changes: 3 additions & 0 deletions examples/audioworklet-beep/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ wasm-bindgen = "0.2"
# logging them with `console.error`.
console_error_panic_hook = "0.1"

# The `ringbuf` crate provides a lock-free ring buffer for passing audio between streams.
ringbuf = "0.4"

# The `web-sys` crate allows you to interact with the various browser APIs,
# like the DOM.
[dependencies.web-sys]
Expand Down
3 changes: 3 additions & 0 deletions examples/audioworklet-beep/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
<body>
<input id="play" type="button" value="beep" />
<input id="stop" type="button" value="stop" />
<input id="record" type="button" value="record" />
<input id="stop-record" type="button" value="stop recording" />
<p>Recording plays your microphone back live. Wear headphones to avoid feedback howl.</p>
</body>

</html>
128 changes: 125 additions & 3 deletions examples/audioworklet-beep/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use std::{cell::Cell, rc::Rc};

use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait},
Device, Error, ErrorKind, FromSample, HostId, Sample, SampleFormat, SizedSample, Stream,
StreamConfig,
traits::{DeviceTrait, HostTrait, StreamTrait},
};
use ringbuf::{
HeapCons, HeapProd, HeapRb,
traits::{Consumer, Producer, Split},
};
use wasm_bindgen::prelude::*;
use web_sys::console;
Expand All @@ -19,6 +23,8 @@ pub fn main_js() -> Result<(), JsValue> {
let document = gloo::utils::document();
let play_button = document.get_element_by_id("play").unwrap();
let stop_button = document.get_element_by_id("stop").unwrap();
let record_button = document.get_element_by_id("record").unwrap();
let stop_record_button = document.get_element_by_id("stop-record").unwrap();

// stream needs to be referenced from the "play" and "stop" closures
let stream = Rc::new(Cell::new(None));
Expand All @@ -45,12 +51,36 @@ pub fn main_js() -> Result<(), JsValue> {
closure.forget();
}

// input stream needs its own slot; recording and playback run independently
let record_stream = Rc::new(Cell::new(None));

// set up record button
{
let record_stream = record_stream.clone();
let closure = Closure::<dyn FnMut(_)>::new(move |_event: web_sys::MouseEvent| {
record_stream.set(Some(record()));
});
record_button
.add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())?;
closure.forget();
}

// set up stop-record button
{
let closure = Closure::<dyn FnMut(_)>::new(move |_event: web_sys::MouseEvent| {
// stop the stream by dropping it; releases the microphone
record_stream.take();
});
stop_record_button
.add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())?;
closure.forget();
}

Ok(())
}

fn beep() -> Stream {
let host =
cpal::host_from_id(HostId::AudioWorklet).expect("AudioWorklet host not available");
let host = cpal::host_from_id(HostId::AudioWorklet).expect("AudioWorklet host not available");

let device = host
.default_output_device()
Expand Down Expand Up @@ -98,6 +128,98 @@ where
stream
}

/// Captures microphone input into a ring buffer and immediately plays it back, so you can hear
/// your own voice. Wear headphones: routing a live mic to speakers risks feedback howl.
fn record() -> (Stream, Stream) {
let host = cpal::host_from_id(HostId::AudioWorklet).expect("AudioWorklet host not available");

let input_device = host
.default_input_device()
.expect("failed to find a default input device");
let output_device = host
.default_output_device()
.expect("failed to find a default output device");

let input_config = input_device.default_input_config().unwrap();
let output_config = output_device.default_output_config().unwrap();

// Bound end-to-end latency; once full, the producer drops the newest samples instead of
// blocking.
let max_buffered_samples =
output_config.sample_rate() as usize * output_config.channels() as usize / 2;
let ring = HeapRb::<f32>::new(max_buffered_samples);
let (producer, consumer) = ring.split();

let input_stream = match input_config.sample_format() {
SampleFormat::F32 => build_input::<f32>(&input_device, input_config.into(), producer),
SampleFormat::I16 => build_input::<i16>(&input_device, input_config.into(), producer),
SampleFormat::U16 => build_input::<u16>(&input_device, input_config.into(), producer),
_ => panic!("unsupported sample format"),
};
let output_stream = match output_config.sample_format() {
SampleFormat::F32 => build_output::<f32>(&output_device, output_config.into(), consumer),
SampleFormat::I16 => build_output::<i16>(&output_device, output_config.into(), consumer),
SampleFormat::U16 => build_output::<u16>(&output_device, output_config.into(), consumer),
_ => panic!("unsupported sample format"),
};

(input_stream, output_stream)
}

fn build_input<T>(device: &Device, config: StreamConfig, mut producer: HeapProd<f32>) -> Stream
where
T: Sample + SizedSample,
f32: FromSample<T>,
{
let err_fn = |err: Error| match err.kind() {
ErrorKind::DeviceChanged | ErrorKind::RealtimeDenied => {
console::log_1(&format!("{err}").into())
}
_ => console::error_1(&format!("Stream error: {err}").into()),
};

let stream = device
.build_input_stream(
config,
move |data: &[T], _| {
producer.push_iter(data.iter().map(|&s| f32::from_sample(s)));
},
err_fn,
None,
)
.unwrap();
stream.start().unwrap();
stream
}

fn build_output<T>(device: &Device, config: StreamConfig, mut consumer: HeapCons<f32>) -> Stream
where
T: Sample + SizedSample + FromSample<f32>,
{
let err_fn = |err: Error| match err.kind() {
ErrorKind::DeviceChanged | ErrorKind::RealtimeDenied => {
console::log_1(&format!("{err}").into())
}
_ => console::error_1(&format!("Stream error: {err}").into()),
};

let stream = device
.build_output_stream(
config,
move |data: &mut [T], _| {
for sample in data.iter_mut() {
let value = consumer.try_pop().unwrap_or(f32::EQUILIBRIUM);
*sample = T::from_sample(value);
}
},
err_fn,
None,
)
.unwrap();
stream.start().unwrap();
stream
}

fn write_data<T>(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32)
where
T: Sample + FromSample<f32>,
Expand Down
3 changes: 3 additions & 0 deletions examples/wasm-beep/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ wasm-bindgen = "0.2"
# logging them with `console.error`.
console_error_panic_hook = "0.1"

# The `ringbuf` crate provides a lock-free ring buffer for passing audio between streams.
ringbuf = "0.4"

# The `web-sys` crate allows you to interact with the various browser APIs,
# like the DOM.
[dependencies.web-sys]
Expand Down
3 changes: 3 additions & 0 deletions examples/wasm-beep/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@
<body>
<input id="play" type="button" value="beep"/>
<input id="stop" type="button" value="stop"/>
<input id="record" type="button" value="record"/>
<input id="stop-record" type="button" value="stop recording"/>
<p>Recording plays your microphone back live. Wear headphones to avoid feedback howl.</p>
</body>
</html>
Loading