WebAudio

View full source code or view the compiled example online

This example creates an FM oscillator using the WebAudio API and web-sys.

Cargo.toml

The Cargo.toml enables the types needed to use the relevant bits of the WebAudio API.

[package] authors = ["The wasm-bindgen Developers"] edition = "2021" name = "webaudio" publish = false version = "0.0.0" [lib] crate-type = ["cdylib"] [dependencies] wasm-bindgen = { path = "../../" } [dependencies.web-sys] features = [ 'AudioContext', 'AudioDestinationNode', 'AudioNode', 'AudioParam', 'GainNode', 'OscillatorNode', 'OscillatorType', ] path = "../../crates/web-sys" [lints] workspace = true

src/lib.rs

The Rust code implements the FM oscillator.

#![allow(unused)] fn main() { use wasm_bindgen::prelude::*; use web_sys::{AudioContext, OscillatorType}; /// Converts a midi note to frequency /// /// A midi note is an integer, generally in the range of 21 to 108 pub fn midi_to_freq(note: u8) -> f32 { 27.5 * 2f32.powf((note as f32 - 21.0) / 12.0) } #[wasm_bindgen] pub struct FmOsc { ctx: AudioContext, /// The primary oscillator. This will be the fundamental frequency primary: web_sys::OscillatorNode, /// Overall gain (volume) control gain: web_sys::GainNode, /// Amount of frequency modulation fm_gain: web_sys::GainNode, /// The oscillator that will modulate the primary oscillator's frequency fm_osc: web_sys::OscillatorNode, /// The ratio between the primary frequency and the fm_osc frequency. /// /// Generally fractional values like 1/2 or 1/4 sound best fm_freq_ratio: f32, fm_gain_ratio: f32, } impl Drop for FmOsc { fn drop(&mut self) { let _ = self.ctx.close(); } } #[wasm_bindgen] impl FmOsc { #[wasm_bindgen(constructor)] pub fn new() -> Result<FmOsc, JsValue> { let ctx = web_sys::AudioContext::new()?; // Create our web audio objects. let primary = ctx.create_oscillator()?; let fm_osc = ctx.create_oscillator()?; let gain = ctx.create_gain()?; let fm_gain = ctx.create_gain()?; // Some initial settings: primary.set_type(OscillatorType::Sine); primary.frequency().set_value(440.0); // A4 note gain.gain().set_value(0.0); // starts muted fm_gain.gain().set_value(0.0); // no initial frequency modulation fm_osc.set_type(OscillatorType::Sine); fm_osc.frequency().set_value(0.0); // Connect the nodes up! // The primary oscillator is routed through the gain node, so that // it can control the overall output volume. primary.connect_with_audio_node(&gain)?; // Then connect the gain node to the AudioContext destination (aka // your speakers). gain.connect_with_audio_node(&ctx.destination())?; // The FM oscillator is connected to its own gain node, so it can // control the amount of modulation. fm_osc.connect_with_audio_node(&fm_gain)?; // Connect the FM oscillator to the frequency parameter of the main // oscillator, so that the FM node can modulate its frequency. fm_gain.connect_with_audio_param(&primary.frequency())?; // Start the oscillators! primary.start()?; fm_osc.start()?; Ok(FmOsc { ctx, primary, gain, fm_gain, fm_osc, fm_freq_ratio: 0.0, fm_gain_ratio: 0.0, }) } /// Sets the gain for this oscillator, between 0.0 and 1.0. #[wasm_bindgen] pub fn set_gain(&self, mut gain: f32) { gain = gain.clamp(0.0, 1.0); self.gain.gain().set_value(gain); } #[wasm_bindgen] pub fn set_primary_frequency(&self, freq: f32) { self.primary.frequency().set_value(freq); // The frequency of the FM oscillator depends on the frequency of the // primary oscillator, so we update the frequency of both in this method. self.fm_osc.frequency().set_value(self.fm_freq_ratio * freq); self.fm_gain.gain().set_value(self.fm_gain_ratio * freq); } #[wasm_bindgen] pub fn set_note(&self, note: u8) { let freq = midi_to_freq(note); self.set_primary_frequency(freq); } /// This should be between 0 and 1, though higher values are accepted. #[wasm_bindgen] pub fn set_fm_amount(&mut self, amt: f32) { self.fm_gain_ratio = amt; self.fm_gain .gain() .set_value(self.fm_gain_ratio * self.primary.frequency().value()); } /// This should be between 0 and 1, though higher values are accepted. #[wasm_bindgen] pub fn set_fm_frequency(&mut self, amt: f32) { self.fm_freq_ratio = amt; self.fm_osc .frequency() .set_value(self.fm_freq_ratio * self.primary.frequency().value()); } } }

index.js

A small bit of JavaScript glues the rust module to input widgets and translates events into calls into Wasm code.

import('./pkg') .then(rust_module => { let fm = null; const play_button = document.getElementById("play"); play_button.addEventListener("click", event => { if (fm === null) { fm = new rust_module.FmOsc(); fm.set_note(50); fm.set_fm_frequency(0); fm.set_fm_amount(0); fm.set_gain(0.8); } else { fm.free(); fm = null; } }); const primary_slider = document.getElementById("primary_input"); primary_slider.addEventListener("input", event => { if (fm) { fm.set_note(parseInt(event.target.value)); } }); const fm_freq = document.getElementById("fm_freq"); fm_freq.addEventListener("input", event => { if (fm) { fm.set_fm_frequency(parseFloat(event.target.value)); } }); const fm_amount = document.getElementById("fm_amount"); fm_amount.addEventListener("input", event => { if (fm) { fm.set_fm_amount(parseFloat(event.target.value)); } }); }) .catch(console.error);