Skip to content

Commit

Permalink
Merge pull request rustwasm#716 from eminence/webaudio
Browse files Browse the repository at this point in the history
Initial example of using the WebAudio APIs from web-sys
  • Loading branch information
alexcrichton authored Aug 17, 2018
2 parents 57693ee + 4f18e21 commit 7a08da9
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ members = [
"examples/performance",
"examples/smorgasboard",
"examples/wasm-in-wasm",
"examples/webaudio",
"tests/no-std",
]

Expand Down
4 changes: 4 additions & 0 deletions examples/webaudio/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package-lock.json
node_modules/
webaudio.js
webaudio_bg.wasm
11 changes: 11 additions & 0 deletions examples/webaudio/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "webaudio"
version = "0.1.0"
authors = ["Andrew Chin <achin@eminence32.net>"]

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = { path = "../.." }
web-sys = { path = "../../crates/web-sys" }
14 changes: 14 additions & 0 deletions examples/webaudio/README.md
Original file line number Diff line number Diff line change
@@ -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!
2 changes: 2 additions & 0 deletions examples/webaudio/build.bat
Original file line number Diff line number Diff line change
@@ -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 .
10 changes: 10 additions & 0 deletions examples/webaudio/build.sh
Original file line number Diff line number Diff line change
@@ -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 .
31 changes: 31 additions & 0 deletions examples/webaudio/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
</head>
<body>
<script>
// Global variable, so a user can play with it via the JS console
var fm;
function play() {
console.log("Rust module not loaded yet!");
}
</script>
<script src='./index.js'></script>

<input type="button" value="Click me first to turn on audio" onclick="javascript: play();" />
(headphone users, please make sure your volume is not too loud!)

<div>
Primary frequency: <input type="range" min="30" max="80" value="50" style="width: 400px" id="primary_input"/>
</div>

<div>
Modulation frequency: <input type="range" min="0" max="3" value="0" step="0.05" style="width: 400px" id="fm_freq"/>
</div>

<div>
Modulation amount: <input type="range" min="0" max="3" value="0" step="0.05" style="width: 400px" id="fm_amount"/>
</div>

</body>
</html>
40 changes: 40 additions & 0 deletions examples/webaudio/index.js
Original file line number Diff line number Diff line change
@@ -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);
});
9 changes: 9 additions & 0 deletions examples/webaudio/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"scripts": {
"serve": "webpack-serve ./webpack.config.js"
},
"devDependencies": {
"webpack": "^4.16.5",
"webpack-serve": "^2.0.2"
}
}
147 changes: 147 additions & 0 deletions examples/webaudio/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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::<AudioScheduledSourceNode>::as_ref(&primary).start();
AsRef::<AudioScheduledSourceNode>::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());
}


}
10 changes: 10 additions & 0 deletions examples/webaudio/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const path = require('path');

module.exports = {
entry: './index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
},
mode: 'development'
};

0 comments on commit 7a08da9

Please sign in to comment.