├── screenshot.png ├── .gitignore ├── .netlify └── state.json ├── postcss.config.js ├── ui ├── index.html ├── style.scss ├── components │ ├── Header.vue │ ├── Knob.vue │ ├── Envelope.vue │ ├── EnvelopeVisualizer.vue │ └── Subjam.vue ├── index.js └── App.vue ├── Cargo.toml ├── src ├── audio.rs ├── cv.rs ├── bus.rs └── lib.rs ├── README.md ├── netlify.toml ├── package.json ├── webpack.config.js └── Cargo.lock /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txus/jam/HEAD/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | /node_modules 4 | /dist 5 | /pkg 6 | -------------------------------------------------------------------------------- /.netlify/state.json: -------------------------------------------------------------------------------- 1 | { 2 | "siteId": "af90537b-cd09-496a-b2b4-5c5eb8b5fd4b" 3 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer') 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /ui/style.scss: -------------------------------------------------------------------------------- 1 | @import 'node_modules/bootstrap/scss/bootstrap'; 2 | @import 'node_modules/bootstrap-vue/src/index.scss'; 3 | 4 | $bluegrey: #2b3a42; 5 | 6 | pre { 7 | padding: 8px 16px; 8 | background: $bluegrey; 9 | color: #e1e6e9; 10 | font-family: Menlo, Courier, monospace; 11 | font-size: 13px; 12 | line-height: 1.5; 13 | text-shadow: 0 1px 0 rgba(23, 31, 35, 0.5); 14 | border-radius: 3px; 15 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "jam" 3 | version = "0.1.0" 4 | authors = ["Txus "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | wasm-bindgen = { version = "0.2.46", features = ['serde-serialize'] } 12 | js-sys = "0.3.23" 13 | 14 | [dependencies.web-sys] 15 | version = "0.3.23" 16 | features = [ 17 | 'console', 18 | 'AudioContext', 19 | 'AudioDestinationNode', 20 | 'AudioNode', 21 | 'AudioParam', 22 | 'GainNode', 23 | 'OscillatorNode', 24 | 'OscillatorType', 25 | 'BiquadFilterNode', 26 | 'BiquadFilterType', 27 | 'Window', 28 | 'Document', 29 | ] 30 | -------------------------------------------------------------------------------- /src/audio.rs: -------------------------------------------------------------------------------- 1 | use web_sys::AudioNode; 2 | 3 | pub trait AudioInput { 4 | fn input(&self) -> AudioNode; 5 | } 6 | 7 | pub trait AudioOutput { 8 | fn output(&self) -> AudioNode; 9 | } 10 | 11 | pub trait AudioInputs { 12 | fn inputs(&self) -> Vec; 13 | } 14 | 15 | pub fn connect(from: &F, to: &T) { 16 | from.output().connect_with_audio_node(&to.input()).unwrap(); 17 | } 18 | 19 | pub fn connect_to_one(from: &F, to: &T, at: usize) { 20 | let inputs = to.inputs(); 21 | assert!(at < inputs.len(), "index beyond limit"); 22 | from.output().connect_with_audio_node(&inputs[at]).unwrap(); 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jam 2 | 3 | ![Screenshot](/screenshot.png?raw=true "Screenshot") 4 | 5 | This will be hopefully a complete, extensible and playable modular synth running in the browser. 6 | 7 | It's written in Rust (interfacing with WebAudio API), compiled to WebAssembly and run in the browser -- the UI is a Vue.js application. If you have a MIDI controller, just plug it in and the browser should recognize it (probably just Chrome)! 8 | 9 | For now I'm just playing around! Definitely not very usable. 10 | 11 | ## Running 12 | 13 | ``` 14 | cargo build 15 | npm install 16 | npm run serve 17 | open localhost:8080 18 | ``` 19 | 20 | Connect your MIDI controller and turn the synth power ON! :) 21 | 22 | ## Modules 23 | 24 | ### Subjam 25 | 26 | A basic polyphonic 2-oscillator synth with an amp envelope and a low pass filter. 27 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # example netlify.toml 2 | [build] 3 | command = "npm run build" 4 | functions = "functions" 5 | publish = "dist" 6 | 7 | ## Uncomment to use this redirect for Single Page Applications like create-react-app. 8 | ## Not needed for static site generators. 9 | #[[redirects]] 10 | # from = "/*" 11 | # to = "/index.html" 12 | # status = 200 13 | 14 | ## (optional) Settings for Netlify Dev 15 | ## https://github.com/netlify/netlify-dev-plugin#project-detection 16 | #[dev] 17 | # command = "yarn start" # Command to start your dev server 18 | # port = 3000 # Port that the dev server will be listening on 19 | # publish = "dist" # Folder with the static content for _redirect file 20 | 21 | ## more info on configuring this file: https://www.netlify.com/docs/netlify-toml-reference/ 22 | -------------------------------------------------------------------------------- /ui/components/Header.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 33 | 34 | 35 | 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "webpack", 4 | "serve": "webpack-dev-server" 5 | }, 6 | "devDependencies": { 7 | "@babel/core": "^7.4.4", 8 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 9 | "@wasm-tool/wasm-pack-plugin": "0.2.1", 10 | "autoprefixer": "^9.5.1", 11 | "babel-loader": "^8.0.5", 12 | "css-loader": "^2.1.1", 13 | "html-webpack-plugin": "^3.2.0", 14 | "mini-css-extract-plugin": "^0.6.0", 15 | "node-sass": "^4.12.0", 16 | "postcss-loader": "^3.0.0", 17 | "sass-loader": "^7.1.0", 18 | "style-loader": "^0.23.1", 19 | "text-encoding": "^0.7.0", 20 | "vue-loader": "^15.7.0", 21 | "vue-template-compiler": "^2.6.10", 22 | "webpack": "^4.29.4", 23 | "webpack-cli": "^3.1.1", 24 | "webpack-dev-server": "^3.1.0" 25 | }, 26 | "dependencies": { 27 | "bootstrap": "^4.3.1", 28 | "bootstrap-vue": "^2.0.0-rc.19", 29 | "precision-inputs": "^0.2.5", 30 | "vue": "^2.5.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/cv.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | use web_sys::GainNode; 3 | use wasm_bindgen::prelude::*; 4 | 5 | pub trait Control { 6 | fn range() -> Range; 7 | fn set(&mut self, value: T); 8 | fn get(&self) -> T; 9 | fn default_value() -> T; 10 | } 11 | 12 | #[wasm_bindgen] 13 | pub struct GainControl { 14 | node: GainNode, 15 | value: f32 16 | } 17 | 18 | impl Control for GainControl { 19 | fn range() -> Range { 20 | 0.0..1.0 21 | } 22 | 23 | fn set(&mut self, value: f32) { 24 | self.value = value; 25 | } 26 | 27 | fn get(&self) -> f32 { 28 | self.value 29 | } 30 | 31 | fn default_value() -> f32 { 0.0 } 32 | } 33 | 34 | 35 | #[wasm_bindgen] 36 | #[derive(Clone, Copy)] 37 | pub struct MixControl { 38 | value: f32 39 | } 40 | 41 | impl Control for MixControl { 42 | fn range() -> Range { 43 | 0.0..1.0 44 | } 45 | 46 | fn set(&mut self, value: f32) { 47 | self.value = value; 48 | } 49 | 50 | fn get(&self) -> f32 { 51 | self.value 52 | } 53 | 54 | fn default_value() -> f32 { 0.5 } 55 | } 56 | -------------------------------------------------------------------------------- /ui/index.js: -------------------------------------------------------------------------------- 1 | import './style.scss'; 2 | import 'precision-inputs/css/precision-inputs.base.css'; 3 | import 'precision-inputs/css/precision-inputs.fl-controls.css'; 4 | 5 | import Vue from 'vue' 6 | import BootstrapVue from 'bootstrap-vue'; 7 | import App from './App.vue' 8 | 9 | Vue.use(BootstrapVue); 10 | 11 | import('./../pkg/jam') 12 | .then(rust_module => { 13 | let vue = new Vue({ 14 | el: '#app', 15 | props: ['rust'], 16 | components: { App }, 17 | render: h => h(App, {props: {rust: rust_module}}) 18 | }); 19 | 20 | // MIDI 21 | 22 | if (navigator.requestMIDIAccess) { 23 | console.log('This browser supports WebMIDI!'); 24 | } else { 25 | console.error('WebMIDI is not supported in this browser.'); 26 | } 27 | 28 | let midiInputs; 29 | 30 | const onMIDIMessage = ({data}) => { 31 | let cmd = data[0] >> 4; 32 | let channel = data[0] & 0xf; 33 | let type = data[0] & 0xf0; 34 | let note = data[1]; 35 | let velocity = data[2]; 36 | 37 | switch (type) { 38 | case 144: 39 | vue.$children[0].noteOn(note, velocity); 40 | break; 41 | case 128: 42 | vue.$children[0].noteOff(note, velocity); 43 | break; 44 | } 45 | }; 46 | 47 | navigator.requestMIDIAccess() 48 | .then( 49 | midiAccess => { 50 | midiInputs = midiAccess.inputs.values(); 51 | for (var input = midiInputs.next(); input && !input.done; input = midiInputs.next()) { 52 | // each time there is a midi message call the onMIDIMessage function 53 | input.value.onmidimessage = onMIDIMessage; 54 | } 55 | }, 56 | () => console.error('Could not access your MIDI devices') 57 | ); 58 | }) 59 | .catch(console.error); 60 | -------------------------------------------------------------------------------- /ui/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 65 | 66 | 76 | -------------------------------------------------------------------------------- /src/bus.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use wasm_bindgen::prelude::*; 3 | 4 | use web_sys::console; 5 | 6 | use std::rc::Rc; 7 | use std::cell::RefCell; 8 | 9 | pub struct EventBus { 10 | controls: Rc>>>, 11 | last_value: Rc>>, 12 | modulations: Rc>>> 13 | } 14 | 15 | impl EventBus { 16 | pub fn new() -> EventBus { 17 | EventBus { 18 | controls: Rc::new(RefCell::new(HashMap::new())), 19 | last_value: Rc::new(RefCell::new(HashMap::new())), 20 | modulations: Rc::new(RefCell::new(HashMap::new())) 21 | } 22 | 23 | } 24 | 25 | pub fn control(&self, id: String, initial_value: f32, on_change: Box) { 26 | let mut last_value = self.last_value.borrow_mut(); 27 | last_value.insert(id.clone(), initial_value); 28 | let mut controls = self.controls.borrow_mut(); 29 | if let Some(_) = controls.insert(id, on_change) { 30 | panic!("Adding control twice"); 31 | } 32 | } 33 | 34 | pub fn modulate(&self, from: String, to: String) { 35 | let mut modulations = self.modulations.borrow_mut(); 36 | let entries: &mut Vec = modulations.entry(from).or_insert(vec![]); 37 | entries.push(to); 38 | } 39 | 40 | pub fn value(&self, id: String) -> f32 { 41 | let last_value = self.last_value.borrow(); 42 | if let Some(v) = last_value.get(&id) { 43 | *v 44 | } else { 45 | 0.0 46 | } 47 | } 48 | 49 | pub fn trigger(&self, id: String, value: f32) { 50 | let controls = self.controls.borrow(); 51 | 52 | console::log_1(&"trying to trigger".into()); 53 | 54 | if let Some(f) = controls.get(&id) { 55 | let mut last_value = self.last_value.borrow_mut(); 56 | last_value.insert(id.clone(), value); 57 | f(value); 58 | } 59 | let modulations = self.modulations.borrow(); 60 | if let Some(modulations) = modulations.get(&id) { 61 | for modulated_id in modulations { 62 | self.trigger(modulated_id.to_string(), value); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ui/components/Knob.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 41 | 42 | -------------------------------------------------------------------------------- /ui/components/Envelope.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 56 | 57 | 78 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const webpack = require('webpack'); 4 | const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); 5 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 6 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 7 | 8 | module.exports = { 9 | entry: './ui/index.js', 10 | output: { 11 | path: path.resolve(__dirname, 'dist'), 12 | filename: 'index.js', 13 | }, 14 | resolve: { 15 | alias: { 16 | 'vue': 'vue/dist/vue.common.js', 17 | 'ui': path.resolve(__dirname, '../ui'), 18 | 'assets': path.resolve(__dirname, '../ui/assets'), 19 | 'components': path.resolve(__dirname, '../ui/components') 20 | } 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.js$/, 26 | exclude: /node_modules/, 27 | use: { 28 | loader: "babel-loader", 29 | options: { 30 | plugins: ['@babel/plugin-syntax-dynamic-import'] 31 | } 32 | } 33 | }, 34 | { 35 | test: /\.vue$/, 36 | loader: 'vue-loader' 37 | }, 38 | { 39 | test: /\.css$/, 40 | use: ['vue-style-loader', MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'] 41 | }, 42 | { 43 | test: /\.s[c|a]ss$/, 44 | use: ['style-loader', MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'] 45 | } 46 | ] 47 | }, 48 | plugins: [ 49 | new MiniCssExtractPlugin({ 50 | filename: 'style.[contenthash].css', 51 | }), 52 | new HtmlWebpackPlugin({ 53 | inject: false, 54 | template: './ui/index.html', 55 | filename: 'index.html' 56 | }), 57 | new VueLoaderPlugin(), 58 | new WasmPackPlugin({ 59 | crateDirectory: path.resolve(__dirname, ".") 60 | }), 61 | // Have this example work in Edge which doesn't ship `TextEncoder` or 62 | // `TextDecoder` at this time. 63 | new webpack.ProvidePlugin({ 64 | TextDecoder: ['text-encoding', 'TextDecoder'], 65 | TextEncoder: ['text-encoding', 'TextEncoder'] 66 | }) 67 | ], 68 | mode: 'development' 69 | }; 70 | -------------------------------------------------------------------------------- /ui/components/EnvelopeVisualizer.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 79 | 80 | 98 | -------------------------------------------------------------------------------- /ui/components/Subjam.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 181 | 182 | 221 | 222 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "autocfg" 5 | version = "0.1.2" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | 8 | [[package]] 9 | name = "backtrace" 10 | version = "0.3.15" 11 | source = "registry+https://github.com/rust-lang/crates.io-index" 12 | dependencies = [ 13 | "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 14 | "backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", 15 | "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 16 | "libc 0.2.53 (registry+https://github.com/rust-lang/crates.io-index)", 17 | "rustc-demangle 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", 18 | "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 19 | ] 20 | 21 | [[package]] 22 | name = "backtrace-sys" 23 | version = "0.1.28" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | dependencies = [ 26 | "cc 1.0.35 (registry+https://github.com/rust-lang/crates.io-index)", 27 | "libc 0.2.53 (registry+https://github.com/rust-lang/crates.io-index)", 28 | ] 29 | 30 | [[package]] 31 | name = "bumpalo" 32 | version = "2.4.1" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | 35 | [[package]] 36 | name = "cc" 37 | version = "1.0.35" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | 40 | [[package]] 41 | name = "cfg-if" 42 | version = "0.1.7" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | 45 | [[package]] 46 | name = "failure" 47 | version = "0.1.5" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | dependencies = [ 50 | "backtrace 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 51 | "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 52 | ] 53 | 54 | [[package]] 55 | name = "failure_derive" 56 | version = "0.1.5" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | dependencies = [ 59 | "proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)", 60 | "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", 61 | "syn 0.15.33 (registry+https://github.com/rust-lang/crates.io-index)", 62 | "synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", 63 | ] 64 | 65 | [[package]] 66 | name = "heck" 67 | version = "0.3.1" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | dependencies = [ 70 | "unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 71 | ] 72 | 73 | [[package]] 74 | name = "itoa" 75 | version = "0.4.3" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | 78 | [[package]] 79 | name = "jam" 80 | version = "0.1.0" 81 | dependencies = [ 82 | "js-sys 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", 83 | "wasm-bindgen 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)", 84 | "web-sys 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", 85 | ] 86 | 87 | [[package]] 88 | name = "js-sys" 89 | version = "0.3.23" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | dependencies = [ 92 | "wasm-bindgen 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)", 93 | ] 94 | 95 | [[package]] 96 | name = "lazy_static" 97 | version = "1.3.0" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | 100 | [[package]] 101 | name = "libc" 102 | version = "0.2.53" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | 105 | [[package]] 106 | name = "log" 107 | version = "0.4.6" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | dependencies = [ 110 | "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 111 | ] 112 | 113 | [[package]] 114 | name = "memchr" 115 | version = "2.2.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | 118 | [[package]] 119 | name = "nom" 120 | version = "4.2.3" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | dependencies = [ 123 | "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 124 | "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 125 | ] 126 | 127 | [[package]] 128 | name = "proc-macro2" 129 | version = "0.4.28" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | dependencies = [ 132 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 133 | ] 134 | 135 | [[package]] 136 | name = "quote" 137 | version = "0.6.12" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | dependencies = [ 140 | "proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)", 141 | ] 142 | 143 | [[package]] 144 | name = "rustc-demangle" 145 | version = "0.1.14" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | 148 | [[package]] 149 | name = "ryu" 150 | version = "0.2.7" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | 153 | [[package]] 154 | name = "serde" 155 | version = "1.0.90" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | 158 | [[package]] 159 | name = "serde_json" 160 | version = "1.0.39" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | dependencies = [ 163 | "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", 164 | "ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", 165 | "serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)", 166 | ] 167 | 168 | [[package]] 169 | name = "sourcefile" 170 | version = "0.1.4" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | 173 | [[package]] 174 | name = "syn" 175 | version = "0.15.33" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | dependencies = [ 178 | "proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)", 179 | "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", 180 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 181 | ] 182 | 183 | [[package]] 184 | name = "synstructure" 185 | version = "0.10.1" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | dependencies = [ 188 | "proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)", 189 | "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", 190 | "syn 0.15.33 (registry+https://github.com/rust-lang/crates.io-index)", 191 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 192 | ] 193 | 194 | [[package]] 195 | name = "unicode-segmentation" 196 | version = "1.2.1" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | 199 | [[package]] 200 | name = "unicode-xid" 201 | version = "0.1.0" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | 204 | [[package]] 205 | name = "version_check" 206 | version = "0.1.5" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | 209 | [[package]] 210 | name = "wasm-bindgen" 211 | version = "0.2.46" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | dependencies = [ 214 | "serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)", 215 | "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", 216 | "wasm-bindgen-macro 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)", 217 | ] 218 | 219 | [[package]] 220 | name = "wasm-bindgen-backend" 221 | version = "0.2.46" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | dependencies = [ 224 | "bumpalo 2.4.1 (registry+https://github.com/rust-lang/crates.io-index)", 225 | "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 226 | "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", 227 | "proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)", 228 | "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", 229 | "syn 0.15.33 (registry+https://github.com/rust-lang/crates.io-index)", 230 | "wasm-bindgen-shared 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)", 231 | ] 232 | 233 | [[package]] 234 | name = "wasm-bindgen-macro" 235 | version = "0.2.46" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | dependencies = [ 238 | "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", 239 | "wasm-bindgen-macro-support 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)", 240 | ] 241 | 242 | [[package]] 243 | name = "wasm-bindgen-macro-support" 244 | version = "0.2.46" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | dependencies = [ 247 | "proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)", 248 | "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", 249 | "syn 0.15.33 (registry+https://github.com/rust-lang/crates.io-index)", 250 | "wasm-bindgen-backend 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)", 251 | "wasm-bindgen-shared 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)", 252 | ] 253 | 254 | [[package]] 255 | name = "wasm-bindgen-shared" 256 | version = "0.2.46" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | 259 | [[package]] 260 | name = "wasm-bindgen-webidl" 261 | version = "0.2.46" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | dependencies = [ 264 | "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 265 | "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 266 | "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", 267 | "proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)", 268 | "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", 269 | "syn 0.15.33 (registry+https://github.com/rust-lang/crates.io-index)", 270 | "wasm-bindgen-backend 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)", 271 | "weedle 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", 272 | ] 273 | 274 | [[package]] 275 | name = "web-sys" 276 | version = "0.3.23" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | dependencies = [ 279 | "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 280 | "js-sys 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", 281 | "sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 282 | "wasm-bindgen 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)", 283 | "wasm-bindgen-webidl 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)", 284 | ] 285 | 286 | [[package]] 287 | name = "weedle" 288 | version = "0.9.0" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | dependencies = [ 291 | "nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 292 | ] 293 | 294 | [[package]] 295 | name = "winapi" 296 | version = "0.3.7" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | dependencies = [ 299 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 300 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 301 | ] 302 | 303 | [[package]] 304 | name = "winapi-i686-pc-windows-gnu" 305 | version = "0.4.0" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | 308 | [[package]] 309 | name = "winapi-x86_64-pc-windows-gnu" 310 | version = "0.4.0" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | 313 | [metadata] 314 | "checksum autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799" 315 | "checksum backtrace 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "f106c02a3604afcdc0df5d36cc47b44b55917dbaf3d808f71c163a0ddba64637" 316 | "checksum backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6" 317 | "checksum bumpalo 2.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4639720be048090544634e0402490838995ccdc9d2fe648f528f30d3c33ae71f" 318 | "checksum cc 1.0.35 (registry+https://github.com/rust-lang/crates.io-index)" = "5e5f3fee5eeb60324c2781f1e41286bdee933850fff9b3c672587fed5ec58c83" 319 | "checksum cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11d43355396e872eefb45ce6342e4374ed7bc2b3a502d1b28e36d6e23c05d1f4" 320 | "checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" 321 | "checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" 322 | "checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 323 | "checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b" 324 | "checksum js-sys 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)" = "9455b1e578bd8c833c4e2ef7eaaf0522be3c1f66a51c69cd1a83a57facdfac43" 325 | "checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" 326 | "checksum libc 0.2.53 (registry+https://github.com/rust-lang/crates.io-index)" = "ec350a9417dfd244dc9a6c4a71e13895a4db6b92f0b106f07ebbc3f3bc580cee" 327 | "checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" 328 | "checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39" 329 | "checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" 330 | "checksum proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)" = "ba92c84f814b3f9a44c5cfca7d2ad77fa10710867d2bbb1b3d175ab5f47daa12" 331 | "checksum quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "faf4799c5d274f3868a4aae320a0a182cbd2baee377b378f080e16a23e9d80db" 332 | "checksum rustc-demangle 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "ccc78bfd5acd7bf3e89cffcf899e5cb1a52d6fafa8dec2739ad70c9577a57288" 333 | "checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7" 334 | "checksum serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)" = "aa5f7c20820475babd2c077c3ab5f8c77a31c15e16ea38687b4c02d3e48680f4" 335 | "checksum serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "5a23aa71d4a4d43fdbfaac00eff68ba8a06a51759a89ac3304323e800c4dd40d" 336 | "checksum sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4bf77cb82ba8453b42b6ae1d692e4cdc92f9a47beaf89a847c8be83f4e328ad3" 337 | "checksum syn 0.15.33 (registry+https://github.com/rust-lang/crates.io-index)" = "ec52cd796e5f01d0067225a5392e70084acc4c0013fa71d55166d38a8b307836" 338 | "checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015" 339 | "checksum unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa6024fc12ddfd1c6dbc14a80fa2324d4568849869b779f6bd37e5e4c03344d1" 340 | "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" 341 | "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" 342 | "checksum wasm-bindgen 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)" = "5502982f20f1279d3a891c1cf8d6098df8a492e493c1228d2532fec8472e36ec" 343 | "checksum wasm-bindgen-backend 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)" = "97fbff1e66971afbdc4c7ec3783954c61442e3b89217626bc29952ee9583418a" 344 | "checksum wasm-bindgen-macro 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)" = "20c053d0cb5bf05f35f5db53e18a9363fe359cab0c65376a631d8e8028192883" 345 | "checksum wasm-bindgen-macro-support 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)" = "0578af634d3b25eb998a4ad587573be7e410e8a66b4a9262e62bda3a224c95b6" 346 | "checksum wasm-bindgen-shared 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)" = "9864292443a34a14464db49f0660202d3f7c9764d1fc9bbd49e354ad7ddd8c0a" 347 | "checksum wasm-bindgen-webidl 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)" = "8ab60aa7bfbf3b4c80cf53b93c0c995308425216b3840fd44b5e0ad7b1ee0ed6" 348 | "checksum web-sys 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)" = "81daa27c8b5674b72c3d54de349127ffead8658aff9c2433426b901068757a1f" 349 | "checksum weedle 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bcc44aa200daee8b1f3a004beaf16554369746f1b4486f0cf93b0caf8a3c2d1e" 350 | "checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770" 351 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 352 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 353 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | use web_sys::console; 3 | use web_sys::window; 4 | use js_sys; 5 | use web_sys::{AudioContext, AudioNode, BiquadFilterType, OscillatorType, OscillatorNode, GainNode, BiquadFilterNode, AudioParam}; 6 | mod audio; 7 | mod cv; 8 | mod bus; 9 | use audio::{AudioInput, AudioOutput, AudioInputs}; 10 | 11 | use bus::EventBus; 12 | 13 | /// Converts a midi note to frequency 14 | /// 15 | /// A midi note is an integer, generally in the range of 21 to 108 16 | pub fn midi_to_freq(note: u8) -> f32 { 17 | 27.5 * 2f32.powf((note as f32 - 21.0) / 12.0) 18 | } 19 | 20 | #[wasm_bindgen] 21 | #[derive(Clone)] 22 | pub struct Envelope { 23 | pub attack: u32, 24 | pub decay: u32, 25 | pub sustain: f32, 26 | pub release: u32, 27 | } 28 | 29 | impl Default for Envelope { 30 | fn default() -> Envelope { 31 | Envelope { 32 | attack: 30, // in milliseconds 33 | decay: 300, // in milliseconds 34 | sustain: 1.0, // out of 1 35 | release: 800, // in milliseconds 36 | } 37 | } 38 | } 39 | 40 | #[wasm_bindgen] 41 | pub fn default_envelope() -> Envelope { 42 | Default::default() 43 | } 44 | 45 | impl Envelope { 46 | pub fn adsr(&self) -> (u32, u32, f32, u32) { 47 | (self.attack, self.decay, self.sustain, self.release) 48 | } 49 | } 50 | 51 | pub struct Filter { 52 | ctx: AudioContext, 53 | filter_type: BiquadFilterType, 54 | pub frequency: u32, 55 | pub resonance: f32, 56 | filter: BiquadFilterNode, 57 | } 58 | 59 | impl Filter { 60 | pub fn new(ctx: AudioContext) -> Result { 61 | let filter = ctx.create_biquad_filter()?; 62 | filter.set_type(BiquadFilterType::Lowpass); 63 | let mut f = Filter { 64 | frequency: 8000, 65 | resonance: 0.0, 66 | ctx: ctx, 67 | filter: filter, 68 | filter_type: BiquadFilterType::Lowpass, 69 | }; 70 | f.set_frequency(f.frequency); 71 | f.set_resonance(f.resonance); 72 | Ok(f) 73 | } 74 | 75 | pub fn set_type(&mut self, filter_type: BiquadFilterType) { 76 | self.filter_type = filter_type; 77 | self.filter.set_type(filter_type); 78 | } 79 | 80 | pub fn set_frequency(&mut self, freq: u32) { 81 | self.frequency = freq; 82 | self.filter.frequency().set_value_at_time(freq as f32, self.ctx.current_time()).unwrap(); 83 | } 84 | 85 | pub fn set_resonance(&mut self, q: f32) { 86 | self.resonance = q; 87 | self.filter.q().set_value_at_time(q, self.ctx.current_time()).unwrap(); 88 | } 89 | } 90 | 91 | impl AudioInput for Filter { 92 | fn input(&self) -> AudioNode { 93 | self.filter.clone().into() 94 | } 95 | } 96 | 97 | impl AudioOutput for Filter { 98 | fn output(&self) -> AudioNode { 99 | self.filter.clone().into() 100 | } 101 | } 102 | 103 | impl AudioOutput for Oscillator { 104 | fn output(&self) -> AudioNode { 105 | self.amp.clone().into() 106 | } 107 | } 108 | 109 | impl AudioInput for Channel { 110 | fn input(&self) -> AudioNode { 111 | self.gain.clone().into() 112 | } 113 | } 114 | 115 | impl AudioOutput for Channel { 116 | fn output(&self) -> AudioNode { 117 | self.gain.clone().into() 118 | } 119 | } 120 | 121 | use std::collections::HashMap; 122 | 123 | pub struct Voice { 124 | pub unison: usize, 125 | pub oscs: Vec, 126 | pub gain: GainNode, 127 | pub filter: BiquadFilterNode 128 | } 129 | 130 | const TIME_PADDING: f64 = 0.003; 131 | const FILTER_MAX_FREQ: u32 = 7200; 132 | 133 | impl Voice { 134 | pub fn new(ctx: &AudioContext, unison: usize) -> Result { 135 | let f = ctx.create_biquad_filter()?; 136 | let g = ctx.create_gain()?; 137 | let mut oscs: Vec = vec![]; 138 | g.gain().set_value_at_time(0.0, ctx.current_time())?; 139 | f.connect_with_audio_node(&g)?; 140 | for i in 0..unison { 141 | let o = ctx.create_oscillator()?; 142 | o.detune().set_value(i as f32 * 0.5); 143 | o.connect_with_audio_node(&f)?; 144 | oscs.push(o); 145 | } 146 | Ok(Voice { unison: unison, oscs: oscs, gain: g, filter: f}) 147 | } 148 | 149 | pub fn start(&self) { 150 | for o in &self.oscs { 151 | o.start().unwrap(); 152 | } 153 | } 154 | 155 | pub fn stop(&self) { 156 | for o in &self.oscs { 157 | o.stop().unwrap(); 158 | } 159 | } 160 | 161 | pub fn set_waveform(&mut self, waveform: OscillatorType) { 162 | for o in &self.oscs { 163 | o.set_type(waveform); 164 | } 165 | } 166 | 167 | pub fn set_filter_frequency(&self, ctx: &AudioContext, freq: u32) { 168 | let now = ctx.current_time(); 169 | self.filter.frequency().set_value_at_time(freq as f32, now).unwrap(); 170 | } 171 | 172 | pub fn set_filter_resonance(&self, ctx: &AudioContext, q: f32) { 173 | let now = ctx.current_time(); 174 | self.filter.q().set_value_at_time(q, now).unwrap(); 175 | } 176 | 177 | pub fn set_freq(&mut self, ctx: &AudioContext, freq: f32) { 178 | let now = ctx.current_time(); 179 | for o in &self.oscs { 180 | o.frequency().set_value_at_time(freq, now + TIME_PADDING).unwrap(); 181 | } 182 | } 183 | 184 | pub fn amp_envelope_start(&self, ctx: &AudioContext, env: &Envelope, mut max_gain: f32, velocity: u8) { 185 | let now = ctx.current_time(); 186 | let vel = velocity as f32 / 127.0; 187 | max_gain = (vel * max_gain) / self.unison as f32; 188 | let attack_s = env.attack as f64 / 1000.0; 189 | let decay_s = env.decay as f64 / 1000.0; 190 | let gain: AudioParam = self.gain.gain(); 191 | 192 | // Init envelope (Set value to current value and quickly ramp to 0 to avoid clicks) 193 | gain.cancel_scheduled_values(now).unwrap(); 194 | gain.set_value_at_time(max_gain, now).unwrap(); 195 | gain.linear_ramp_to_value_at_time(0.0, now + TIME_PADDING).unwrap(); 196 | 197 | //Attack phase 198 | let attack_time = TIME_PADDING + attack_s; 199 | gain.linear_ramp_to_value_at_time(1.0, now + attack_time).unwrap(); 200 | 201 | //Decay phase (decay to sustain value) 202 | let decay_time = TIME_PADDING + decay_s; 203 | let sustain_value = env.sustain; 204 | gain.set_target_at_time(sustain_value, now + attack_time, decay_time).unwrap(); 205 | } 206 | 207 | pub fn amp_envelope_end(&self, ctx: &AudioContext, env: &Envelope) { 208 | let now = ctx.current_time(); 209 | let release_s = env.release as f64 / 1000.0; 210 | let gain: AudioParam = self.gain.gain(); 211 | //Release phase 212 | gain.cancel_scheduled_values(now).unwrap(); 213 | gain.set_value_at_time(gain.value(), now).unwrap(); 214 | gain.set_target_at_time(0.0, now, TIME_PADDING + release_s).unwrap(); 215 | } 216 | 217 | pub fn filter_envelope_start(&self, ctx: &AudioContext, env: &Envelope, filter_frequency: u32) { 218 | let attack_s = env.attack as f64 / 1000.0; 219 | let decay_s = env.decay as f64 / 1000.0; 220 | 221 | // Init 222 | let now = ctx.current_time(); 223 | self.filter.detune().cancel_scheduled_values(now).unwrap(); 224 | self.filter.detune().set_value_at_time(self.filter.detune().value(), now).unwrap(); 225 | 226 | // Attack 227 | let attack_time = TIME_PADDING + attack_s; 228 | let target_frequency = FILTER_MAX_FREQ; 229 | self.filter.detune().linear_ramp_to_value_at_time(target_frequency as f32, now + attack_time).unwrap(); 230 | 231 | // Decay 232 | let decay_time = TIME_PADDING + decay_s; 233 | 234 | // Calculate sustain 235 | let cutoff = filter_frequency as f32; 236 | let cutoff_pct = cutoff / (FILTER_MAX_FREQ as f32) * 100.0; 237 | let min_sustain = (FILTER_MAX_FREQ as f32 / 100.0) * cutoff_pct; 238 | let max_sustain = FILTER_MAX_FREQ as f32; 239 | 240 | let sustain_value = (env.sustain * (max_sustain - min_sustain) / 100.0) + min_sustain; 241 | self.filter.detune().set_target_at_time(sustain_value, now + attack_time, decay_time).unwrap(); 242 | } 243 | 244 | pub fn filter_envelope_end(&self, ctx: &AudioContext, env: &Envelope, filter_frequency: u32) { 245 | let now = ctx.current_time(); 246 | let release_s = env.release as f64 / 1000.0; 247 | self.filter.detune().cancel_scheduled_values(now).unwrap(); 248 | self.filter.detune().set_value_at_time(self.filter.detune().value(), now).unwrap(); 249 | self.filter.detune().set_target_at_time(filter_frequency as f32, now, TIME_PADDING + release_s).unwrap(); 250 | } 251 | 252 | pub fn connect_to_audio(&self, to: &AudioNode) { 253 | self.gain.connect_with_audio_node(&to).unwrap(); 254 | } 255 | } 256 | 257 | pub struct Oscillator { 258 | name: String, 259 | ctx: AudioContext, 260 | voices: Vec, 261 | last_voice: usize, 262 | pub amp_env: Envelope, 263 | pub filter_env: Envelope, 264 | pub osc_type: OscillatorType, 265 | pub polyphony: usize, 266 | pub filter_frequency: u32, 267 | pub filter_resonance: f32, 268 | playing_notes: HashMap, 269 | amp: GainNode, 270 | gain: Rc>, 271 | } 272 | 273 | use std::rc::Rc; 274 | use std::cell::RefCell; 275 | 276 | impl Oscillator { 277 | pub fn new(name: String, ctx: AudioContext, polyphony: usize, unison: usize, filter_frequency: u32, filter_resonance: f32) -> Result { 278 | let amp_env: Envelope = Default::default(); 279 | let filter_env: Envelope = Default::default(); 280 | let amp = ctx.create_gain()?; 281 | 282 | let mut voices: Vec = vec![]; 283 | for _ in 0..polyphony { 284 | let v = Voice::new(&ctx, unison)?; 285 | v.connect_to_audio(&); 286 | voices.push(v); 287 | } 288 | 289 | let gain_control = Rc::new(RefCell::new(0.9)); 290 | 291 | let bus = unsafe { get_bus() }; 292 | let control = gain_control.clone(); 293 | bus.control(format!("{}.gain", name), 0.9, Box::new(move |v| { 294 | let mut g = control.borrow_mut(); 295 | *g = v; 296 | })); 297 | 298 | let osc_type = OscillatorType::Sine; 299 | 300 | let mut o = Oscillator { 301 | name, 302 | playing_notes: HashMap::new(), 303 | gain: gain_control.clone(), 304 | polyphony, 305 | filter_frequency, 306 | filter_resonance, 307 | last_voice: 999, 308 | osc_type, 309 | ctx, 310 | voices, 311 | amp_env, 312 | filter_env, 313 | amp, 314 | }; 315 | 316 | o.set_waveform(osc_type); 317 | 318 | Ok(o) 319 | } 320 | 321 | pub fn on(&self) { 322 | for v in &self.voices { 323 | v.start(); 324 | } 325 | } 326 | 327 | pub fn off(&self) { 328 | for v in &self.voices { 329 | v.stop(); 330 | } 331 | } 332 | 333 | fn is_voice_free(&self, idx: usize) -> bool { 334 | let mut free = true; 335 | for (_note, voice) in &self.playing_notes { 336 | if *voice == idx { 337 | free = false; 338 | } 339 | } 340 | free 341 | } 342 | 343 | fn get_voice(&self) -> usize { 344 | let mut voice = if self.last_voice == 999 { 345 | 0 346 | } else { 347 | self.last_voice + 1 348 | }; 349 | if voice > self.polyphony-1 { 350 | voice = 0; 351 | } 352 | 353 | for _ in 0..self.polyphony { 354 | if !self.is_voice_free(voice) { 355 | voice += 1; 356 | if voice > self.polyphony-1 { 357 | voice = 0; 358 | } 359 | } 360 | } 361 | voice 362 | } 363 | 364 | pub fn note_on(&mut self, note: u8, velocity: u8) { 365 | let current_voice = self.get_voice(); 366 | self.last_voice = current_voice; 367 | self.playing_notes.insert(note, current_voice); 368 | 369 | let voice = &mut self.voices[current_voice]; 370 | voice.set_freq(&self.ctx, midi_to_freq(note)); 371 | let g = self.gain.borrow(); 372 | voice.amp_envelope_start(&self.ctx, &self.amp_env, *g, velocity); 373 | voice.filter_envelope_start(&self.ctx, &self.filter_env, self.filter_frequency); 374 | } 375 | 376 | pub fn note_off(&mut self, note: u8) { 377 | match self.playing_notes.get(¬e) { 378 | None => { panic!("Note off without note on") }, 379 | Some(idx) => { 380 | let voice = &self.voices[*idx]; 381 | voice.amp_envelope_end(&self.ctx, &self.amp_env); 382 | voice.filter_envelope_end(&self.ctx, &self.filter_env, self.filter_frequency); 383 | self.playing_notes.remove(¬e); 384 | } 385 | } 386 | } 387 | 388 | pub fn set_waveform(&mut self, waveform: OscillatorType) { 389 | self.osc_type = waveform; 390 | for v in &mut self.voices { 391 | v.set_waveform(waveform); 392 | } 393 | } 394 | 395 | pub fn set_amp_attack(&mut self, v: u32) { 396 | self.amp_env.attack = v; 397 | } 398 | pub fn set_amp_decay(&mut self, v: u32) { 399 | self.amp_env.decay = v; 400 | } 401 | pub fn set_amp_sustain(&mut self, v: f32) { 402 | self.amp_env.sustain = v; 403 | } 404 | pub fn set_amp_release(&mut self, v: u32) { 405 | self.amp_env.release = v; 406 | } 407 | 408 | pub fn set_filter_attack(&mut self, v: u32) { 409 | self.filter_env.attack = v; 410 | } 411 | pub fn set_filter_decay(&mut self, v: u32) { 412 | self.filter_env.decay = v; 413 | } 414 | pub fn set_filter_sustain(&mut self, v: f32) { 415 | self.filter_env.sustain = v; 416 | } 417 | pub fn set_filter_release(&mut self, v: u32) { 418 | self.filter_env.release = v; 419 | } 420 | 421 | pub fn set_filter_frequency(&mut self, f: u32) { 422 | self.filter_frequency = f; 423 | for v in &mut self.voices { 424 | v.set_filter_frequency(&self.ctx, f); 425 | } 426 | } 427 | 428 | pub fn set_filter_resonance(&mut self, q: f32) { 429 | self.filter_resonance = q; 430 | for v in &mut self.voices { 431 | v.set_filter_resonance(&self.ctx, q); 432 | } 433 | } 434 | 435 | pub fn connect_with_audio_node(&self, destination: &AudioNode) -> Result { 436 | let node = self.amp.connect_with_audio_node(&destination)?; 437 | Ok(node) 438 | } 439 | } 440 | 441 | #[wasm_bindgen] 442 | pub struct Channel { 443 | ctx: AudioContext, 444 | gain: GainNode, 445 | } 446 | 447 | #[wasm_bindgen] 448 | impl Channel { 449 | pub fn new(ctx: AudioContext) -> Result { 450 | let gain = ctx.create_gain()?; 451 | gain.gain().set_value_at_time(0.0, ctx.current_time())?; // start off 452 | Ok(Channel { 453 | ctx, 454 | gain, 455 | }) 456 | } 457 | 458 | #[wasm_bindgen] 459 | pub fn set_gain(&self, gain: f32) { 460 | self.gain.gain().set_value_at_time(gain, self.ctx.current_time()).unwrap(); 461 | } 462 | } 463 | 464 | #[wasm_bindgen] 465 | pub struct Mixer { 466 | channels: Vec, 467 | ctx: AudioContext, 468 | master: Channel, 469 | } 470 | 471 | #[wasm_bindgen] 472 | impl Mixer { 473 | #[wasm_bindgen(constructor)] 474 | pub fn new(ctx: AudioContext, channel_count: u8) -> Result { 475 | let master = Channel::new(ctx.clone()).unwrap(); 476 | 477 | let channels: Vec = (0..channel_count).map(|_| { 478 | let c = Channel::new(ctx.clone()).unwrap(); 479 | audio::connect(&c, &master); 480 | c 481 | }).collect(); 482 | 483 | let m = Mixer { 484 | channels, 485 | master, 486 | ctx, 487 | }; 488 | 489 | m.set_master_gain(0.9); 490 | for idx in 0..channel_count { 491 | m.set_gain(idx as usize, 0.8); 492 | } 493 | 494 | Ok(m) 495 | } 496 | 497 | #[wasm_bindgen] 498 | pub fn set_master_gain(&self, gain: f32) { 499 | self.master.set_gain(gain); 500 | } 501 | 502 | #[wasm_bindgen] 503 | pub fn set_gain(&self, idx: usize, gain: f32) { 504 | assert!(self.channels.len() > idx, "Not enough channels"); 505 | self.channels[idx].set_gain(gain); 506 | } 507 | 508 | #[wasm_bindgen] 509 | pub fn connect_to_speakers(&self) { 510 | self.master.output().connect_with_audio_node(&self.ctx.destination()).unwrap(); 511 | } 512 | } 513 | 514 | impl AudioInputs for Mixer { 515 | fn inputs(&self) -> Vec { 516 | self.channels.iter().map(|x| x.input()).collect() 517 | } 518 | } 519 | 520 | #[wasm_bindgen] 521 | pub struct Subjam { 522 | osc1: Oscillator, 523 | osc2: Oscillator, 524 | pub osc_mix: f32, 525 | pub filter_frequency: u32, 526 | pub filter_q: f32, 527 | out: GainNode, 528 | } 529 | 530 | impl AudioOutput for Subjam { 531 | fn output(&self) -> AudioNode { 532 | self.out.clone().into() 533 | } 534 | } 535 | 536 | #[wasm_bindgen] 537 | impl Subjam { 538 | #[wasm_bindgen(constructor)] 539 | pub fn new(ctx: AudioContext) -> Result { 540 | let bus = unsafe { get_bus() }; 541 | 542 | let polyphony = 16; 543 | let unison = 1; 544 | let filter_frequency = FILTER_MAX_FREQ; 545 | let filter_q = 0.0; 546 | let mut osc1 = Oscillator::new("subjam.osc1".to_string(), ctx.clone(), polyphony, unison, filter_frequency, filter_q)?; 547 | let mut osc2 = Oscillator::new("subjam.osc2".to_string(), ctx.clone(), polyphony, unison, filter_frequency, filter_q)?; 548 | 549 | let gain = ctx.clone().create_gain()?; 550 | gain.gain().set_value_at_time(0.5, ctx.current_time())?; 551 | 552 | osc1.set_waveform(OscillatorType::Sawtooth); 553 | osc2.set_waveform(OscillatorType::Square); 554 | 555 | bus.control("subjam.osc_mix".to_string(), 0.5, Box::new(|v| { 556 | let b = unsafe { get_bus() }; 557 | b.trigger("subjam.osc1.gain".to_string(), 1.0 - v); 558 | b.trigger("subjam.osc2.gain".to_string(), v); 559 | })); 560 | 561 | osc1.on(); 562 | osc2.on(); 563 | 564 | osc1.connect_with_audio_node(&gain)?; 565 | osc2.connect_with_audio_node(&gain)?; 566 | 567 | let subjam = Subjam { 568 | osc_mix: 0.5, 569 | osc1, 570 | osc2, 571 | filter_frequency, 572 | filter_q, 573 | out: gain, 574 | }; 575 | 576 | Ok(subjam) 577 | } 578 | 579 | #[wasm_bindgen] 580 | pub fn set_osc1_type(&mut self, waveform: OscillatorType) { 581 | self.osc1.set_waveform(waveform); 582 | } 583 | 584 | #[wasm_bindgen] 585 | pub fn set_osc2_type(&mut self, waveform: OscillatorType) { 586 | self.osc2.set_waveform(waveform); 587 | } 588 | 589 | #[wasm_bindgen] 590 | pub fn get_osc1_type(&self) -> OscillatorType { 591 | self.osc1.osc_type 592 | } 593 | 594 | #[wasm_bindgen] 595 | pub fn get_osc2_type(&self) -> OscillatorType { 596 | self.osc2.osc_type 597 | } 598 | 599 | #[wasm_bindgen] 600 | pub fn set_filter_frequency(&mut self, f: u32) { 601 | self.filter_frequency = f; 602 | self.osc1.set_filter_frequency(f); 603 | self.osc2.set_filter_frequency(f); 604 | } 605 | 606 | #[wasm_bindgen] 607 | pub fn set_filter_resonance(&mut self, q: f32) { 608 | self.filter_q = q; 609 | self.osc1.set_filter_resonance(q); 610 | self.osc2.set_filter_resonance(q); 611 | } 612 | 613 | #[wasm_bindgen] 614 | pub fn get_filter_frequency(&self) -> u32 { 615 | self.filter_frequency 616 | } 617 | 618 | #[wasm_bindgen] 619 | pub fn get_filter_resonance(&self) -> f32 { 620 | self.filter_q 621 | } 622 | 623 | #[wasm_bindgen] 624 | pub fn set_amp_attack(&mut self, v: u32) { 625 | self.osc1.set_amp_attack(v); 626 | self.osc2.set_amp_attack(v); 627 | } 628 | #[wasm_bindgen] 629 | pub fn set_amp_decay(&mut self, v: u32) { 630 | self.osc1.set_amp_decay(v); 631 | self.osc2.set_amp_decay(v); 632 | } 633 | #[wasm_bindgen] 634 | pub fn set_amp_sustain(&mut self, v: f32) { 635 | self.osc1.set_amp_sustain(v); 636 | self.osc2.set_amp_sustain(v); 637 | } 638 | #[wasm_bindgen] 639 | pub fn set_amp_release(&mut self, v: u32) { 640 | self.osc1.set_amp_release(v); 641 | self.osc2.set_amp_release(v); 642 | } 643 | 644 | #[wasm_bindgen] 645 | pub fn set_filter_attack(&mut self, v: u32) { 646 | self.osc1.set_filter_attack(v); 647 | self.osc2.set_filter_attack(v); 648 | } 649 | #[wasm_bindgen] 650 | pub fn set_filter_decay(&mut self, v: u32) { 651 | self.osc1.set_filter_decay(v); 652 | self.osc2.set_filter_decay(v); 653 | } 654 | #[wasm_bindgen] 655 | pub fn set_filter_sustain(&mut self, v: f32) { 656 | self.osc1.set_filter_sustain(v); 657 | self.osc2.set_filter_sustain(v); 658 | } 659 | #[wasm_bindgen] 660 | pub fn set_filter_release(&mut self, v: u32) { 661 | self.osc1.set_filter_release(v); 662 | self.osc2.set_filter_release(v); 663 | } 664 | 665 | #[wasm_bindgen] 666 | pub fn get_osc1_amp_env(&self) -> Envelope { 667 | self.osc1.amp_env.clone() 668 | } 669 | 670 | #[wasm_bindgen] 671 | pub fn get_osc2_amp_env(&self) -> Envelope { 672 | self.osc2.amp_env.clone() 673 | } 674 | 675 | #[wasm_bindgen] 676 | pub fn get_osc1_filter_env(&self) -> Envelope { 677 | self.osc1.filter_env.clone() 678 | } 679 | 680 | #[wasm_bindgen] 681 | pub fn note_on(&mut self, note: u8, velocity: u8) { 682 | self.osc1.note_on(note, velocity); 683 | self.osc2.note_on(note, velocity); 684 | } 685 | 686 | #[wasm_bindgen] 687 | pub fn note_off(&mut self, note: u8) { 688 | self.osc1.note_off(note); 689 | self.osc2.note_off(note); 690 | } 691 | 692 | #[wasm_bindgen] 693 | pub fn connect_to_mixer(&self, mixer: &Mixer, at: usize) { 694 | audio::connect_to_one(self, mixer, at); 695 | } 696 | } 697 | 698 | use std::ptr; 699 | use std::mem; 700 | 701 | static mut _EVENT_BUS_PTR:*const EventBus = 0 as *const EventBus; 702 | 703 | unsafe fn get_bus<'a>() -> &'a mut EventBus { 704 | if _EVENT_BUS_PTR == ptr::null::() { 705 | 706 | // Notice this is a Box, which is a *EventBus allocated on the heap 707 | // transmute(EventBus { v: 0 }) wouldn't work because once the stack scope ends 708 | // the instance would no longer be valid; Box lasts beyond the call to get() 709 | _EVENT_BUS_PTR = mem::transmute(Box::new(EventBus::new())); 710 | } 711 | return mem::transmute(_EVENT_BUS_PTR); 712 | } 713 | 714 | unsafe fn release() { 715 | ptr::read::(_EVENT_BUS_PTR); 716 | } 717 | 718 | #[wasm_bindgen(start)] 719 | pub fn run() -> Result<(), JsValue> { 720 | let window = window().expect("should have a window in this context"); 721 | let document = window.document().expect("window should have a document"); 722 | 723 | unsafe { 724 | let trigger = Closure::wrap(Box::new(|id: String, value: f32| { 725 | let bus = get_bus(); 726 | bus.trigger(id, value); 727 | }) as Box); 728 | 729 | js_sys::Reflect::set(&document, &"trigger".into(), &trigger.as_ref())?; 730 | 731 | mem::forget(trigger); 732 | } 733 | 734 | Ok(()) 735 | } 736 | --------------------------------------------------------------------------------