diff --git a/Cargo.toml b/Cargo.toml index d76eb37f..cf918adc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,7 @@ members = [ "examples/performance", "examples/smorgasboard", "examples/wasm-in-wasm", + "examples/webaudio", "tests/no-std", ] diff --git a/examples/webaudio/.gitignore b/examples/webaudio/.gitignore new file mode 100644 index 00000000..89f520ca --- /dev/null +++ b/examples/webaudio/.gitignore @@ -0,0 +1,4 @@ +package-lock.json +node_modules/ +webaudio.js +webaudio_bg.wasm diff --git a/examples/webaudio/Cargo.toml b/examples/webaudio/Cargo.toml new file mode 100644 index 00000000..6d71cc84 --- /dev/null +++ b/examples/webaudio/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "webaudio" +version = "0.1.0" +authors = ["Andrew Chin "] + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = { path = "../.." } +web-sys = { path = "../../crates/web-sys" } \ No newline at end of file diff --git a/examples/webaudio/README.md b/examples/webaudio/README.md new file mode 100644 index 00000000..98c9f0c6 --- /dev/null +++ b/examples/webaudio/README.md @@ -0,0 +1,14 @@ +# Web Audio example + +This directory is an example of how to use the Web Audio APIs from Rust. It creates a very simple +FM (frequency modulation) synth, and let's you control the primary frequency, the modulation amount, +and the modulation frequency. + +To run, first install some utilities via npm: + + > npm install + + Then build the project with either `build.bat` or `build.sh`. + + Finally, run a development web server with `npm run serve` and then open + [http://localhost:8080/](http://localhost:8080/) in a browser! \ No newline at end of file diff --git a/examples/webaudio/build.bat b/examples/webaudio/build.bat new file mode 100644 index 00000000..de86353e --- /dev/null +++ b/examples/webaudio/build.bat @@ -0,0 +1,2 @@ +cargo +nightly build --target wasm32-unknown-unknown +cargo +nightly run --manifest-path ../../crates/cli/Cargo.toml --bin wasm-bindgen -- ../../target/wasm32-unknown-unknown/debug/webaudio.wasm --out-dir . diff --git a/examples/webaudio/build.sh b/examples/webaudio/build.sh new file mode 100755 index 00000000..fa6c0c12 --- /dev/null +++ b/examples/webaudio/build.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# For more coments about what's going on here, see the `hello_world` example + +set -ex + +cargo +nightly build --target wasm32-unknown-unknown +cargo +nightly run --manifest-path ../../crates/cli/Cargo.toml \ + --bin wasm-bindgen -- \ + ../../target/wasm32-unknown-unknown/debug/webaudio.wasm --out-dir . diff --git a/examples/webaudio/index.html b/examples/webaudio/index.html new file mode 100644 index 00000000..d091f797 --- /dev/null +++ b/examples/webaudio/index.html @@ -0,0 +1,31 @@ + + + + + + + + + + (headphone users, please make sure your volume is not too loud!) + +
+ Primary frequency: +
+ +
+ Modulation frequency: +
+ +
+ Modulation amount: +
+ + + diff --git a/examples/webaudio/index.js b/examples/webaudio/index.js new file mode 100644 index 00000000..6488f5de --- /dev/null +++ b/examples/webaudio/index.js @@ -0,0 +1,40 @@ +const rust = import('./webaudio'); + + +// Most browsers don't let WebAudio autoplay without some interaction from the user. So once the module is loaded, +// it's passed to this function which will set up the UI elements for the user to interact with +function setup(rust_module) { + play = function() { + console.log("About to create some music!"); + fm = new rust_module.FmOsc(); + + fm.set_note(50); + fm.set_fm_frequency(0); + fm.set_fm_amount(0); + fm.set_gain(0.8); + + }; + + // create some UI elements + const primary_slider = document.getElementById("primary_input"); + primary_slider.oninput = (e) => { + fm.set_note(e.target.value); + }; + + const fm_freq = document.getElementById("fm_freq"); + fm_freq.oninput = (e) => { + fm.set_fm_frequency(e.target.value); + }; + + const fm_amount = document.getElementById("fm_amount"); + fm_amount.oninput = (e) => { + fm.set_fm_amount(e.target.value); + }; + + console.log("Ready! Press the play button!"); +} + + +rust.then(m => { + setup(m); +}); diff --git a/examples/webaudio/package.json b/examples/webaudio/package.json new file mode 100644 index 00000000..07da0131 --- /dev/null +++ b/examples/webaudio/package.json @@ -0,0 +1,9 @@ +{ + "scripts": { + "serve": "webpack-serve ./webpack.config.js" + }, + "devDependencies": { + "webpack": "^4.16.5", + "webpack-serve": "^2.0.2" + } +} diff --git a/examples/webaudio/src/lib.rs b/examples/webaudio/src/lib.rs new file mode 100644 index 00000000..bca2256d --- /dev/null +++ b/examples/webaudio/src/lib.rs @@ -0,0 +1,147 @@ +#![feature(use_extern_macros, nll)] + +extern crate wasm_bindgen; +extern crate web_sys; + +use wasm_bindgen::prelude::*; +use web_sys::{AudioContext, BaseAudioContext, AudioNode, AudioScheduledSourceNode, 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, + + +} + +#[wasm_bindgen] +impl FmOsc { + #[wasm_bindgen(constructor)] + pub fn new() -> FmOsc { + // TODO, how to throw from a constructor? + + let ctx = web_sys::AudioContext::new().unwrap(); + let base: &BaseAudioContext = ctx.as_ref(); + + // create our web audio objects + let primary = base.create_oscillator().unwrap(); + let fm_osc = base.create_oscillator().unwrap(); + let gain = base.create_gain().unwrap(); + let fm_gain = base.create_gain().unwrap(); + + // 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); + + + // Create base class references: + let primary_node: &AudioNode = primary.as_ref(); + let gain_node: &AudioNode = gain.as_ref(); + let fm_osc_node: &AudioNode = fm_osc.as_ref(); + let fm_gain_node: &AudioNode = fm_gain.as_ref(); + let destination = base.destination(); + let destination_node: &AudioNode = destination.as_ref(); + + + // connect them up: + + // The primary oscillator is routed through the gain node, so that it can control the overall output volume + primary_node.connect_with_destination_and_output_and_input_using_destination(gain.as_ref()); + // Then connect the gain node to the AudioContext destination (aka your speakers) + gain_node.connect_with_destination_and_output_and_input_using_destination(destination_node); + + // the FM oscillator is connected to its own gain node, so it can control the amount of modulation + fm_osc_node.connect_with_destination_and_output_and_input_using_destination(fm_gain.as_ref()); + + // Connect the FM oscillator to the frequency parameter of the main oscillator, so that the + // FM node can modulate its frequency + fm_gain_node.connect_with_destination_and_output_using_destination(&primary.frequency()); + + + // start the oscillators! + AsRef::::as_ref(&primary).start(); + AsRef::::as_ref(&fm_osc).start(); + + 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) { + if gain > 1.0 { gain = 1.0; } + if gain < 0.0 { gain = 0.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()); + } + + +} \ No newline at end of file diff --git a/examples/webaudio/webpack.config.js b/examples/webaudio/webpack.config.js new file mode 100644 index 00000000..dce27149 --- /dev/null +++ b/examples/webaudio/webpack.config.js @@ -0,0 +1,10 @@ +const path = require('path'); + +module.exports = { + entry: './index.js', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'index.js', + }, + mode: 'development' +};