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
28 changes: 28 additions & 0 deletions .github/workflows/platforms.yml
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,34 @@ jobs:
env:
RUSTUP_TOOLCHAIN: nightly

# WebAssembly - Emscripten (WebAudio host)
wasm-emscripten:
runs-on: ubuntu-latest
env:
TARGET: wasm32-unknown-emscripten
steps:
- uses: actions/checkout@v5

- name: Install Rust MSRV (${{ env.MSRV_WASM }})
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ env.MSRV_WASM }}
targets: ${{ env.TARGET }}

- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
key: wasm-emscripten

- name: Check examples (default features)
run: cargo +${{ env.MSRV_WASM }} check --examples --workspace --verbose --target ${{ env.TARGET }}

- name: Check examples (wasm-bindgen feature)
run: cargo +${{ env.MSRV_WASM }} check --examples --features wasm-bindgen --workspace --verbose --target ${{ env.TARGET }}

- name: Check all features
run: cargo +${{ env.MSRV_WASM }} check --workspace --all-features --verbose --target ${{ env.TARGET }}

# WebAssembly - WASI Preview 1
wasm-wasip1:
runs-on: ubuntu-latest
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ jobs:
features: --features wasm-bindgen
os: ubuntu-latest

# WASM - Emscripten (WebAudio host)
- target: wasm32-unknown-emscripten
name: WASM-emscripten
features: --features wasm-bindgen
os: ubuntu-latest

# WASM - WASI
- target: wasm32-wasip1
name: WASI
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `DeviceTrait::build_duplex_stream()`, `build_duplex_stream_raw()`, `default_duplex_config()`, 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.
- **WebAudio**: Added support for Emscripten targets via [wasm-bindgen/Emscripten integration](https://github.com/wasm-bindgen/wasm-bindgen/issues/5237).

### Changed

Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,8 @@ objc2-avf-audio = { version = "0.3", default-features = false, features = [
"AVAudioSessionTypes",
] }

[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
wasm-bindgen = { version = "0.2", optional = true }
[target.'cfg(all(target_arch = "wasm32", any(target_os = "emscripten", target_os = "unknown")))'.dependencies]
wasm-bindgen = { version = "0.2.127", optional = true }
wasm-bindgen-futures = { version = "0.4", optional = true }
futures-channel = { version = "0.3", optional = true }
futures-util = { version = "0.3", default-features = false, optional = true, features = [
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ The minimum Rust version (MSRV) and minimum operating system / runtime version b
| PipeWire | Linux, BSD | 1.85 | PipeWire 0.3.53 |
| PulseAudio | Linux, BSD | 1.88 | — |
| WASAPI / ASIO | Windows | 1.85 | Windows 10 |
| WASM (`wasm32-unknown`) | WebAssembly | 1.85 | — |
| WASM (`wasm32-unknown-unknown`) | WebAssembly | 1.85 | — |
| WASM (`wasm32-unknown-emscripten`) | WebAssembly | 1.85 | Emscripten 6.0.3, wasm-bindgen 0.2.127 |
Comment thread
DouglasDwyer marked this conversation as resolved.
| WASM (`wasm32-wasip1`) | WebAssembly | 1.85 | — |
| WASM (`audioworklet`) | WebAssembly | nightly | — |

Expand All @@ -70,7 +71,7 @@ The `audioworklet` backend additionally requires `-Zbuild-std` with atomics supp
| `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 `webaudio` example. |
| `wasm-bindgen` | WebAssembly (`wasm32-unknown-emscripten`, `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
20 changes: 13 additions & 7 deletions src/host/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ pub(crate) mod wasapi;

#[cfg(all(
target_arch = "wasm32",
target_os = "unknown",
any(target_os = "emscripten", target_os = "unknown"),
feature = "wasm-bindgen"
))]
pub(crate) mod webaudio;
Expand All @@ -96,7 +96,7 @@ pub(crate) mod custom;
target_os = "android",
all(
target_arch = "wasm32",
target_os = "unknown",
any(target_os = "emscripten", target_os = "unknown"),
feature = "wasm-bindgen"
),
)))]
Expand Down Expand Up @@ -246,7 +246,13 @@ pub(crate) use error_emit::try_emit_error;
target_os = "netbsd",
target_os = "windows",
target_vendor = "apple",
feature = "audioworklet",
all(
target_arch = "wasm32",
target_os = "unknown",
feature = "wasm-bindgen",
feature = "audioworklet",
target_feature = "atomics"
),
))]
#[inline]
pub(crate) fn frames_to_duration(
Expand Down Expand Up @@ -333,7 +339,7 @@ where
/// <https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#exceptions>
#[cfg(all(
target_arch = "wasm32",
target_os = "unknown",
any(target_os = "emscripten", target_os = "unknown"),
feature = "wasm-bindgen"
))]
pub(crate) fn get_user_media_error(js_err: &wasm_bindgen::JsValue) -> crate::Error {
Expand Down Expand Up @@ -366,7 +372,7 @@ pub(crate) fn get_user_media_error(js_err: &wasm_bindgen::JsValue) -> crate::Err
/// <https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia>
#[cfg(all(
target_arch = "wasm32",
target_os = "unknown",
any(target_os = "emscripten", target_os = "unknown"),
feature = "wasm-bindgen"
))]
pub(crate) async fn request_microphone() -> Result<web_sys::MediaStream, wasm_bindgen::JsValue> {
Expand All @@ -387,7 +393,7 @@ pub(crate) async fn request_microphone() -> Result<web_sys::MediaStream, wasm_bi
/// microphone is actually present without asking for permission first via getUserMedia().
#[cfg(all(
target_arch = "wasm32",
target_os = "unknown",
any(target_os = "emscripten", target_os = "unknown"),
feature = "wasm-bindgen"
))]
pub(crate) fn is_get_user_media_available() -> bool {
Expand All @@ -398,7 +404,7 @@ pub(crate) fn is_get_user_media_available() -> bool {
/// browser's capture indicator. Dropping a WebAudio graph alone does not do this.
#[cfg(all(
target_arch = "wasm32",
target_os = "unknown",
any(target_os = "emscripten", target_os = "unknown"),
feature = "wasm-bindgen"
))]
pub(crate) fn stop_tracks(media_stream: &web_sys::MediaStream) {
Expand Down
142 changes: 142 additions & 0 deletions src/host/webaudio/main_thread.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//! Utility for invoking functions on the browser's main thread.
//! This allows for creating audio contexts within workers,
//! when normally having access to the window object is required.

#[cfg(target_os = "emscripten")]
pub use self::emscripten::*;

#[cfg(target_os = "unknown")]
pub use self::unknown::*;

/// Proxying implementation for `wasm32-unknown-emscripten`: will send
/// functions through Emscripten's queue to run on the main thread.
#[cfg(target_os = "emscripten")]
mod emscripten {
use std::ffi::c_void;

// Functions provided by `emscripten/proxying.h` and `emscripten/threading.h`
unsafe extern "C" {
/// Returns true if the current thread is the thread that hosts the Emscripten
/// runtime.
fn emscripten_is_main_runtime_thread() -> bool;

/// Returns the thread ID of the thread that hosts the Emscripten runtime.
fn emscripten_main_runtime_thread_id() -> usize;

/// Get the queue used for proxying low-level runtime work.
fn emscripten_proxy_get_system_queue() -> *mut c_void;

/// Enqueue `func` to be called with argument `arg` on the given queue
/// and thread then wait for `func` to be executed synchronously before returning.
fn emscripten_proxy_sync(
queue: *mut c_void,
target_thread: usize,
func: extern "C" fn(*mut c_void),
arg: *mut c_void,
) -> bool;
}

/// Runs `func` on the browser main thread. For the Emscripten target,
/// always succeeds with [`Some`].
pub fn try_run<F, R>(func: F) -> Option<R>
where
F: FnOnce() -> R + Send,
R: Send,
{
Some(run(func))
}

/// Run `func` on the browser main thread and return its result, blocking
/// the caller until it completes. Runs inline when the caller already is
/// the main thread.
fn run<F, R>(func: F) -> R
where
F: FnOnce() -> R + Send,
R: Send,
{
/// Data that gets sent between the two browser threads.
struct SyncSlot<F, R> {
/// The function to execute.
func: Option<F>,
/// The value that was returned.
ret: Option<R>,
}

/// Callback that gets invoked on the main thread.
extern "C" fn trampoline<F, R>(arg: *mut c_void)
where
F: FnOnce() -> R,
{
// SAFETY: `arg` points at a `SyncSlot` on the calling thread's stack.
// `emscripten_proxy_sync` keeps that thread blocked until this returns,
// so the pointer stays valid and unaliased for the call.
let slot = unsafe { &mut *arg.cast::<SyncSlot<F, R>>() };
let func = slot.func.take().expect("proxied task run twice");
slot.ret = Some(func());
}

// SAFETY: `func` and its return value are Send and can be moved
// between threads. `emscripten_proxy_sync` guarantees that it
// will invoke `trampoline` and not return until it is finished.
unsafe {
if emscripten_is_main_runtime_thread() {
return func();
}

let mut slot = SyncSlot {
func: Some(func),
ret: None,
};

let ok = emscripten_proxy_sync(
emscripten_proxy_get_system_queue(),
emscripten_main_runtime_thread_id(),
trampoline::<F, R>,
(&raw mut slot).cast(),
);

assert!(
ok,
"emscripten_proxy_sync to the browser main thread failed"
);
slot.ret.take().expect("proxied task did not run")
}
}
}

/// Proxying implementation for `wasm32-unknown-unknown`: will check to see
/// if closures are running on the main thread, and fail if ever called from a worker.
#[cfg(target_os = "unknown")]
mod unknown {
/// Attempts to run `func`. If this was not already the main browser thread,
/// then returns [`None`] because proxying is not possible on this target.
pub fn try_run<F, R>(func: F) -> Option<R>
where
F: FnOnce() -> R + Send,
R: Send,
{
if is_main_thread() {
Some(run(func))
} else {
None
}
}

/// Asserts that this is the main browser thread and runs `func`.
fn run<F, R>(func: F) -> R
where
F: FnOnce() -> R + Send,
R: Send,
{
assert!(
is_main_thread(),
"proxying closures is not supported on wasm32-unknown-unknown"
);
func()
}

/// Whether this is the main browser thread.
fn is_main_thread() -> bool {
web_sys::window().is_some()
}
}
Loading
Loading