├── 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 | 
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 |
2 |
15 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 |
2 |
3 |
4 |
5 | {{label}}
6 |
7 |
8 |
9 |
10 |
41 |
42 |
--------------------------------------------------------------------------------
/ui/components/Envelope.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
2 |
3 |
11 |
12 |
13 |
14 |
79 |
80 |
98 |
--------------------------------------------------------------------------------
/ui/components/Subjam.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
18 |
19 |
20 |
LP Filter
21 |
22 |
23 |
24 |
25 |
31 |
32 |
33 |
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 |
--------------------------------------------------------------------------------