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
8 changes: 4 additions & 4 deletions .github/workflows/platforms.yml
Original file line number Diff line number Diff line change
Expand Up @@ -476,8 +476,8 @@ jobs:
- name: Check all features (no atomics)
run: cargo +${{ env.MSRV_WASM }} check --workspace --all-features --verbose --target ${{ env.TARGET }}

- name: Build wasm-beep example
working-directory: ./examples/wasm-beep
- name: Build webaudio example
working-directory: ./examples/webaudio
run: trunk build
env:
RUSTUP_TOOLCHAIN: ${{ env.MSRV_WASM }}
Expand Down Expand Up @@ -516,8 +516,8 @@ jobs:
- name: Check all features
run: cargo +nightly check --workspace --all-features --verbose -Z build-std=std,panic_abort --target ${{ env.TARGET }}

- name: Build audioworklet-beep example
working-directory: ./examples/audioworklet-beep
- name: Build audioworklet example
working-directory: ./examples/audioworklet
run: trunk build
env:
RUSTUP_TOOLCHAIN: nightly
Expand Down
8 changes: 5 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ 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.
- `DeviceTrait::build_duplex_stream()`, `build_duplex_stream_raw()`, and `supports_duplex()` for capture and playback from one device-level callback.
- **AudioWorklet**: Input and duplex streams are now supported.
- **WebAudio**: Input and duplex streams are now supported.

### Changed

Expand All @@ -22,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `StreamTrait::play` is renamed to `start`.
- `InputCallbackInfo`/`OutputCallbackInfo` merged into `CallbackInfo`.
- `InputStreamTimestamp`/`OutputStreamTimestamp` merged into `StreamTimestamp`; `capture`/`playback` renamed `device`.
- Renamed the `wasm-beep` and `audioworklet-beep` examples to `webaudio` and `audioworklet`.
- **ALSA**: Update `alsa` dependency to 0.12.
- **Linux**: `realtime` can now promote threads without requiring `realtime-dbus`.

Expand All @@ -38,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- **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`.
- **AudioWorklet**: Fix dropouts in output streams when the callback buffer grows.
- **JACK**: Channel enumeration is capped at the physical system port count again.
- **WASAPI**: Device enumeration no longer panics if the COM enumerator fails to initialize.

Expand Down Expand Up @@ -115,7 +118,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `realtime` feature for real-time audio thread scheduling without a D-Bus build dependency.
- `StreamTrait::now()` to query the current instant on the stream's clock.
- `StreamTrait::buffer_size()` to query the stream's current buffer size in frames per callback.
- `DeviceTrait::build_duplex_stream()`, `build_duplex_stream_raw()`, and `supports_duplex()` for synchronized capture and playback on a shared clock (no backend support yet).
- `SAMPLE_RATE_CD` (44100 Hz) and `SAMPLE_RATE_48K` (48000 Hz) constants.
- `SupportedStreamConfigRange::try_with_standard_sample_rate()` and `with_standard_sample_rate()`
to select 48 kHz or 44.1 kHz from a range.
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ name = "beep"
[[example]]
name = "enumerate"

[[example]]
name = "duplex"

[[example]]
name = "feedback"

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@ The `audioworklet` backend additionally requires `-Zbuild-std` with atomics supp
| Feature | Platform | Description |
| ------- | -------- | ----------- |
| `asio` | Windows | ASIO backend for low-latency audio, bypassing the Windows audio stack. Requires ASIO drivers and LLVM/Clang. See the [ASIO setup guide](#compiling-for-asio). |
| `audioworklet` | WebAssembly (`wasm32-unknown-unknown`) | Audio Worklet backend for lower-latency web audio than the default Web Audio API, running audio on a dedicated thread. Requires atomics support (`RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals"`) and `Cross-Origin` headers for `SharedArrayBuffer`. See the `audioworklet-beep` example. |
| `audioworklet` | WebAssembly (`wasm32-unknown-unknown`) | Audio Worklet backend for lower-latency web audio than the default Web Audio API, running audio on a dedicated thread. Requires atomics support (`RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals"`) and `Cross-Origin` headers for `SharedArrayBuffer`. See the `audioworklet` example. |
| `custom` | All | User-defined backend implementations for audio systems not natively supported by CPAL. See `examples/custom.rs`. |
| `jack` | Linux, BSD, macOS, Windows | JACK Audio Connection Kit backend for pro-audio routing and inter-application connectivity. Requires `libjack-jackd2-dev` (Debian/Ubuntu) or `jack-devel` (Fedora). |
| `pipewire` | Linux, BSD | PipeWire media server backend. Requires `libpipewire-0.3-dev` (Debian/Ubuntu) or `pipewire-devel` (Fedora). |
| `pulseaudio` | Linux, BSD | PulseAudio sound server backend. Requires `libpulse-dev` (Debian/Ubuntu) or `pulseaudio-libs-devel` (Fedora). |
| `realtime` | Android, Linux, Windows | Raises the audio callback thread to real-time or high-priority scheduling for lower latency. On Linux, requires `CAP_SYS_NICE`, root, or an `rtprio` limit granted via `limits.conf` or systemd, unless `realtime-dbus` is also enabled. |
| `realtime-dbus` | Linux | Uses `rtkit` via D-Bus for RT scheduling on Linux desktop systems. Implies `realtime` on all platforms. Requires `libdbus-1-dev` on Linux. |
| `wasm-bindgen` | WebAssembly (`wasm32-unknown-unknown`) | Web Audio API backend for browser-based audio; required for any WebAssembly audio support. See the `wasm-beep` example. |
| `wasm-bindgen` | WebAssembly (`wasm32-unknown-unknown`) | Web Audio API backend for browser-based audio; required for any WebAssembly audio support. See the `webaudio` example. |

See the [beep example](examples/beep.rs) for selecting the backend at runtime.

Expand Down
17 changes: 0 additions & 17 deletions examples/audioworklet-beep/index.html

This file was deleted.

File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "audioworklet-beep"
description = "cpal beep example for WebAssembly on an AudioWorklet"
name = "audioworklet"
description = "cpal AudioWorklet example for WebAssembly"
version = "0.2.0"
edition = "2024"
rust-version = "1.85"
Expand Down
File renamed without changes.
File renamed without changes.
28 changes: 28 additions & 0 deletions examples/audioworklet/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8">
<title>cpal AudioWorklet example</title>
</head>

<body>
<p>
<input id="play" type="button" value="beep" />
<input id="stop" type="button" value="stop" />
</p>
<p>
<input id="record" type="button" value="loopback (two streams)" />
<input id="stop-record" type="button" value="stop" />
</p>
<p>
<input id="duplex" type="button" value="loopback (duplex)" />
<input id="stop-duplex" type="button" value="stop" />
</p>
<p>Both loopback modes play your microphone back live. Wear headphones to avoid feedback howl.</p>
<p>The two-stream mode bridges an independent input and output stream with a ring buffer. The
duplex mode runs both directions from one callback on one clock, so it needs no ring buffer
and the round trip is audibly shorter.</p>
</body>

</html>
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::{cell::Cell, rc::Rc};

use cpal::{
Device, Error, ErrorKind, FromSample, HostId, Sample, SampleFormat, SizedSample, Stream,
StreamConfig,
Device, DuplexCallbackInfo, Error, ErrorKind, FromSample, HostId, Sample, SampleFormat,
SizedSample, Stream, StreamConfig,
traits::{DeviceTrait, HostTrait, StreamTrait},
};
use ringbuf::{
Expand All @@ -25,6 +25,8 @@ pub fn main_js() -> Result<(), JsValue> {
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();
let duplex_button = document.get_element_by_id("duplex").unwrap();
let stop_duplex_button = document.get_element_by_id("stop-duplex").unwrap();

// stream needs to be referenced from the "play" and "stop" closures
let stream = Rc::new(Cell::new(None));
Expand Down Expand Up @@ -76,6 +78,31 @@ pub fn main_js() -> Result<(), JsValue> {
closure.forget();
}

// duplex loopback is a single stream, so one slot holds the whole thing
let duplex_stream = Rc::new(Cell::new(None));

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

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

Ok(())
}

Expand Down Expand Up @@ -166,6 +193,56 @@ fn record() -> (Stream, Stream) {
(input_stream, output_stream)
}

/// The same live microphone loopback as [`record`], but as one duplex stream instead of two
/// independent ones. `AudioWorkletProcessor.process(inputs, outputs)` hands both directions to a
/// single callback on a single clock, so no ring buffer is needed to bridge them and the round
/// trip is a callback rather than a delay line. Wear headphones: routing a live mic to speakers
/// risks feedback howl.
fn duplex() -> Stream {
let host = cpal::host_from_id(HostId::AudioWorklet).expect("AudioWorklet host not available");

let device = host
.default_output_device()
.expect("failed to find a default output device");
assert!(
device.supports_duplex(),
"duplex streams need `navigator.mediaDevices`, which browsers expose only in a \
secure context: serve this page over HTTPS or from localhost"
);

let config = device.default_duplex_config().unwrap();

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()),
};

// WebAudio processes exclusively in f32, so there is no sample format to match on here.
let input_channels = config.input_channels as usize;
let output_channels = config.output_channels as usize;
let stream = device
.build_duplex_stream(
config,
move |input: &[f32], output: &mut [f32], _: &DuplexCallbackInfo| {
// The two directions can carry different channel counts, so mix each captured
// frame down to mono and fan it back out across the output frame.
for (captured, rendered) in input
.chunks(input_channels)
.zip(output.chunks_mut(output_channels))
{
rendered.fill(captured.iter().sum::<f32>() / input_channels as f32);
}
},
err_fn,
None,
)
.unwrap();
stream.start().unwrap();
stream
}

fn build_input<T>(device: &Device, config: StreamConfig, mut producer: HeapProd<f32>) -> Stream
where
T: Sample + SizedSample,
Expand Down
Loading
Loading