web-sys: WebAudio - The `wasm-bindgen` Guide

## Keyboard shortcuts

Press `←` or `→` to navigate between chapters

Press `S` or `/` to search in the book

Press `?` to show this help

Press `Esc` to hide this help

# 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);