├── .gitignore
├── README.md
├── css
├── controls.css
├── keyboard.css
└── page.css
├── img
├── adsr.png
├── input.png
├── key-black.png
├── key.png
├── sawtooth.png
├── sine.png
└── square.png
├── index.html
├── js
├── Controls.js
├── Keyboard.js
├── KeyboardListener.js
├── MediatorMixin.js
├── MidiListener.js
├── Note.js
├── SineModulator.js
├── Synth.js
├── app.js
├── effects
│ └── Delay.js
├── prod
│ └── app.js
├── synthMixins
│ ├── ADSR.js
│ ├── PitchShifter.js
│ └── WaveForm.js
└── waveforms
│ ├── sawtooth.js
│ ├── sine.js
│ └── square.js
├── package.json
├── spec
├── DelaySpec.js
├── KeyboardListenerSpec.js
├── KeyboardSpec.js
├── MediatorMixinSpec.js
├── MidiListenerSpec.js
├── NoteSpec.js
├── SineModulatorSpec.js
├── SynthMixinsSpec.js
├── SynthSpec.js
├── WaveFormSpec.js
└── support
│ └── jasmine.json
└── tpl
└── controls.html
/.gitignore:
--------------------------------------------------------------------------------
1 | log
2 | node_modules
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Build:
2 |
3 | `npm run-script build`
4 |
5 | Test:
6 |
7 | `npm test`
8 |
9 | Demo:
10 |
11 | http://miroshko.github.io/Synzer
12 |
--------------------------------------------------------------------------------
/css/controls.css:
--------------------------------------------------------------------------------
1 | @import url(https://fonts.googleapis.com/css?family=Indie+Flower);
2 |
3 | .controls {
4 | width: 790px;
5 | font-family: 'Indie Flower';
6 | font-size: 22px;
7 | }
8 |
9 | .col2-container, .col3-container, .col5-container, .col6-container {
10 | display: inline-block;
11 | margin-top: 10px;
12 | text-align: right;
13 | vertical-align: top;
14 | }
15 |
16 | .col2-container {
17 | width: 100px;
18 | }
19 |
20 | .col3-container {
21 | width: 150px;
22 | }
23 |
24 | .col5-container {
25 | width: 250px;
26 | }
27 |
28 | .col6-container {
29 | width: 300px;
30 | }
31 |
32 |
33 |
34 | .adsr-image {
35 | width: 120px;
36 | height: 90px;
37 | }
38 |
39 | .wave-form {
40 | display: inline-block;
41 | width: 80px;
42 | height: 80px;
43 | text-align: center;
44 | }
45 |
46 | .wave-form .img {
47 | display: inline-block;
48 | width: 50px;
49 | height: 50px;
50 | background-size: 100%;
51 | }
52 |
53 | .sine .img {
54 | background-image: url(../img/sine.png);
55 | }
56 |
57 | .square .img {
58 | background-image: url(../img/square.png);
59 | }
60 |
61 | .sawtooth .img {
62 | background-image: url(../img/sawtooth.png);
63 | }
64 |
65 | [type="number"] {
66 | width: 45px;
67 | padding: 5px;
68 | border: 0;
69 | background: url(../img/input.png) 50% 50% no-repeat;
70 | background-size: 100%;
71 | }
72 |
73 | h1, h2, h3 {
74 | margin: 0;
75 | }
76 |
77 | /* hide outline on focus */
78 | input[type=text]:focus,
79 | input[type=number]:focus {
80 | outline: none;
81 | }
82 |
83 | /* hide radio input, not for wave-form inputs, yet */
84 | label:not(.wave-form) input[type=radio] {
85 | opacity: 0; /* display:none breaks all */
86 | }
87 |
88 | label span {
89 | cursor: pointer;
90 | }
91 |
92 | label:not(.wave-form) input[type=radio]:checked + span {
93 | font-weight: bold;
94 | }
95 |
--------------------------------------------------------------------------------
/css/keyboard.css:
--------------------------------------------------------------------------------
1 | .key {
2 | display: inline-block;
3 | position: relative;
4 | vertical-align: top;
5 | width: 40px;
6 | height: 120px;
7 | margin: 0 -1px;
8 | background: url(../img/key.png);
9 | user-drag: none;
10 | -moz-user-select: none;
11 | -webkit-user-drag: none;
12 | }
13 |
14 | .key-black {
15 | z-index: 10;
16 | height: 72px;
17 | margin: 0 -20px 0 -20px;
18 | background: url(../img/key-black.png);
19 | }
20 |
21 | .pressed {
22 | top: 4px;
23 | }
--------------------------------------------------------------------------------
/css/page.css:
--------------------------------------------------------------------------------
1 | .synzer {
2 | position: relative;
3 | top: 100px;
4 | width: 900px;
5 | margin: auto;
6 | }
7 |
--------------------------------------------------------------------------------
/img/adsr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miroshko/Synzer/0161ccd100d41b4dc695c15929cd6eb4763dd6d9/img/adsr.png
--------------------------------------------------------------------------------
/img/input.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miroshko/Synzer/0161ccd100d41b4dc695c15929cd6eb4763dd6d9/img/input.png
--------------------------------------------------------------------------------
/img/key-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miroshko/Synzer/0161ccd100d41b4dc695c15929cd6eb4763dd6d9/img/key-black.png
--------------------------------------------------------------------------------
/img/key.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miroshko/Synzer/0161ccd100d41b4dc695c15929cd6eb4763dd6d9/img/key.png
--------------------------------------------------------------------------------
/img/sawtooth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miroshko/Synzer/0161ccd100d41b4dc695c15929cd6eb4763dd6d9/img/sawtooth.png
--------------------------------------------------------------------------------
/img/sine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miroshko/Synzer/0161ccd100d41b4dc695c15929cd6eb4763dd6d9/img/sine.png
--------------------------------------------------------------------------------
/img/square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miroshko/Synzer/0161ccd100d41b4dc695c15929cd6eb4763dd6d9/img/square.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Synz. Javascript synthesizer
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/js/Controls.js:
--------------------------------------------------------------------------------
1 | var MediatorMixin = require('./MediatorMixin');
2 | var template = require('../tpl/controls.html');
3 |
4 | function Controls(el) {
5 | this.el = el;
6 | MediatorMixin.call(this);
7 |
8 | this.el.innerHTML = template;
9 | }
10 |
11 | Controls.prototype.activate = function() {
12 | var this_ = this;
13 | this.el.addEventListener('change', function(e) {
14 | this_.emit(e.target.name + '-change', e.target.value);
15 | });
16 |
17 | // senging out initial values
18 | Array.prototype.forEach.call(this.el.querySelectorAll('[name]'), function(el) {
19 | if (el.type == 'radio' && el.checked === false) {
20 | return;
21 | }
22 | this_.emit(el.name + '-change', el.value);
23 | });
24 | }
25 |
26 | module.exports = Controls;
--------------------------------------------------------------------------------
/js/Keyboard.js:
--------------------------------------------------------------------------------
1 | var Note = require('./Note');
2 | var MediatorMixin = require('./MediatorMixin');
3 |
4 | function Keyboard(el) {
5 | this.el = el;
6 | this._keysPressed = {};
7 | MediatorMixin.call(this);
8 | }
9 |
10 | Keyboard.prototype.draw = function(lowestNote, highestNote) {
11 | var key;
12 | this._keyEls = {};
13 | for(var i = lowestNote; i <= highestNote; i++) {
14 | this._keyEls[i] = key = document.createElement('div');
15 | key.dataset.pitch = i;
16 | key.classList.add('key');
17 | if (['C#', 'D#', 'F#', 'G#', 'B'].indexOf(new Note(i).letter) > -1) {
18 | key.classList.add('key-black');
19 | }
20 | this.el.appendChild(key);
21 | }
22 | };
23 |
24 | Keyboard.prototype.press = function(pitch) {
25 | var el = this._keyEls[pitch];
26 | el.classList.add('pressed');
27 | this.emit('notePressed', new Note(el.dataset.pitch));
28 | };
29 |
30 | Keyboard.prototype.release = function(pitch) {
31 | var el = this._keyEls[pitch];
32 | el.classList.remove('pressed');
33 | this.emit('noteReleased', new Note(el.dataset.pitch));
34 | };
35 |
36 | Keyboard.prototype.startMouseListening = function() {
37 | this.el.addEventListener('mousedown', (e) => {
38 | if (!e.target.classList.contains('key'))
39 | return;
40 | this._mouseDown = true;
41 | this.press(e.target.dataset.pitch);
42 | });
43 |
44 | this.el.addEventListener('mouseover', (e) => {
45 | if (!e.target.classList.contains('key'))
46 | return;
47 | if (this._mouseDown) {
48 | this.press(e.target.dataset.pitch);
49 | }
50 | });
51 |
52 | this.el.addEventListener('mouseleave', (e) => {
53 | if (!e.target.classList.contains('key'))
54 | return;
55 | this._mouseDown = false;
56 | });
57 |
58 | this.el.addEventListener('mouseout', (e) => {
59 | if (!e.target.classList.contains('key'))
60 | return;
61 | if (this._mouseDown) {
62 | this.release(e.target.dataset.pitch);
63 | }
64 | });
65 |
66 | this.el.addEventListener('mouseup', (e) => {
67 | if (!e.target.classList.contains('key'))
68 | return;
69 | if (this._mouseDown) {
70 | this.release(e.target.dataset.pitch);
71 | }
72 | this._mouseDown = false;
73 | });
74 | };
75 |
76 | module.exports = Keyboard;
77 |
--------------------------------------------------------------------------------
/js/KeyboardListener.js:
--------------------------------------------------------------------------------
1 | var MediatorMixin = require('./MediatorMixin');
2 |
3 | const KEYCODE_TO_PITCH_MAP = {
4 | 81: 48,
5 | 50: 49,
6 | 87: 50,
7 | 51: 51,
8 | 69: 52,
9 | 82: 53,
10 | 53: 54,
11 | 84: 55,
12 | 54: 56,
13 | 90: 57,
14 | 55: 58,
15 | 85: 59,
16 | 73: 60,
17 | 57: 61,
18 | 79: 62,
19 | 48: 63,
20 | 80: 64,
21 | 186: 65,
22 | 65: 66,
23 | 89: 67,
24 | 83: 68,
25 | 88: 69,
26 | 68: 70,
27 | 67: 71,
28 | 86: 72,
29 | 71: 73,
30 | 66: 74,
31 | 72: 75,
32 | 78: 76,
33 | 77: 77,
34 | 75: 78,
35 | 188: 79,
36 | 76: 80,
37 | 190: 81,
38 | 192: 82,
39 | 189: 83
40 | };
41 |
42 | KeyboardListener.prototype.KEYCODE_TO_PITCH_MAP;
43 |
44 | function KeyboardListener (options) {
45 | options = Object.assign({}, options);
46 | MediatorMixin.call(this);
47 |
48 | if (!options.startNote || options.startNote < 48) {
49 | throw new Error('startNote must be a number greater or equal than 48');
50 | }
51 |
52 | if (!options.endNote || options.endNote > 83) {
53 | throw new Error('endNote must be a number less or equal than 83');
54 | }
55 |
56 | this._options = options;
57 | this._buttonStatuses = {};
58 |
59 | var emitPitch = (name) => (e) => {
60 | var pitch = KEYCODE_TO_PITCH_MAP[e.keyCode];
61 | if (pitch && pitch >= this._options.startNote && pitch <= this._options.endNote && this._buttonStatuses[pitch] != name) {
62 | this.emit(name, pitch);
63 | }
64 | this._buttonStatuses[pitch] = name;
65 | }
66 |
67 | global.window.addEventListener('keydown', emitPitch('keyPressed'));
68 | global.window.addEventListener('keyup', emitPitch('keyReleased'));
69 | }
70 |
71 | module.exports = KeyboardListener;
72 |
--------------------------------------------------------------------------------
/js/MediatorMixin.js:
--------------------------------------------------------------------------------
1 | function MediatorMixin() {
2 | this._events = {};
3 | this.on = function(eventName, callback) {
4 | this._events[eventName] = this._events[eventName] || [];
5 | this._events[eventName].push(callback);
6 | };
7 |
8 | this.emit = function(eventName) {
9 | var args = Array.prototype.slice.call(arguments, 1);
10 |
11 | if (this._events[eventName]) {
12 | this._events[eventName].forEach(function(callback) {
13 | callback.apply(null, args);
14 | });
15 | }
16 | };
17 | };
18 |
19 | module.exports = MediatorMixin;
--------------------------------------------------------------------------------
/js/MidiListener.js:
--------------------------------------------------------------------------------
1 | var MediatorMixin = require('./MediatorMixin');
2 |
3 | MidiListener.prototype._emitMidiMessage = function(event) {
4 | var eventName = {
5 | 148: 'keyPressed',
6 | 132: 'keyReleased'
7 | }[event.data[0]];
8 | this.emit(eventName, event.data[1]);
9 | };
10 |
11 | function MidiListener () {
12 | MediatorMixin.call(this);
13 |
14 | if (window.navigator.requestMIDIAccess) {
15 | window.navigator.requestMIDIAccess({sysex:false})
16 | .then((midiAccess) => {
17 | var midiInputs = midiAccess.inputs.values();
18 | var input = midiInputs.next();
19 | do {
20 | input.value.onmidimessage = (event) => this._emitMidiMessage(event);
21 | input = midiInputs.next();
22 | } while(!input.done)
23 | }, () => console.log('Failed to get access to the MIDI device'));
24 | } else {
25 | console.log('MIDI API is not available in the browser');
26 | }
27 | }
28 |
29 | module.exports = MidiListener;
30 |
--------------------------------------------------------------------------------
/js/Note.js:
--------------------------------------------------------------------------------
1 | function Note(letterWithOctaveOrPitch) {
2 | if (!this._parsePitch(letterWithOctaveOrPitch) && !this._parseLetter(letterWithOctaveOrPitch)) {
3 | throw new Error('Can not parse ' + letterWithOctaveOrPitch);
4 | }
5 | }
6 |
7 | Note.prototype._NOTES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'B', 'H'];
8 |
9 | Note.prototype._parsePitch = function(pitch) {
10 | // 21 == A0
11 | pitch = parseInt(pitch);
12 | if (isNaN(pitch) || pitch < 21 || pitch > 108)
13 | return false;
14 |
15 | this.letter = this._NOTES[(pitch - 21 + 9) % 12];
16 | this.octave = Math.floor((pitch - 12) / 12);
17 | this.pitch = pitch;
18 | this.frequency = this._freq(this.pitch);
19 | return true;
20 | };
21 |
22 | Note.prototype._parseLetter = function(letterOctave) {
23 | var match = letterOctave.match(/([ABCDEFGH]#?)(\d+)/);
24 | if (!match.length)
25 | return false;
26 | this.letter = match[1];
27 | this.octave = parseInt(match[2]);
28 | this.pitch = this._NOTES.indexOf(this.letter) + 12 * (this.octave + 1);
29 | this.frequency = this._freq(this.pitch);
30 | return true;
31 | };
32 |
33 | Note.prototype._freq = function(pitch) {
34 | return Math.pow(2, (pitch - 20 - 49) / 12) * 440;
35 | };
36 |
37 | module.exports = Note;
--------------------------------------------------------------------------------
/js/SineModulator.js:
--------------------------------------------------------------------------------
1 | function SineModulator (options) {
2 | options = options || {};
3 | this._frequency = options.frequency || 0;
4 | this._phaseOffset = 0;
5 | this._startedAt = 0;
6 | this._interval = null;
7 | this._prevValue = 0;
8 | this.depth = options.depth || 0;
9 |
10 | Object.defineProperty(this, "frequency", {
11 | set: function (frequency) {
12 | // the offset is needed in order to have seamless
13 | // transition between different frequencies
14 | frequency = parseFloat(frequency);
15 | this._phaseOffset = this._phaseNow();
16 | this._startedAt = Date.now();
17 | this._frequency = frequency;
18 | },
19 | get: function() {
20 | return this._frequency;
21 | }
22 | });
23 | }
24 |
25 | SineModulator.prototype.modulate = function(object, property) {
26 | this._objToModulate = object;
27 | this._propertyToModulate = property;
28 | return this;
29 | };
30 |
31 | SineModulator.prototype.start = function() {
32 | this._startedAt = Date.now();
33 | var this_ = this;
34 | this._interval = setInterval(function() {
35 | var value = this_._modValueNow();
36 | var diff = value - this_._prevValue;
37 | this_._objToModulate[this_._propertyToModulate] += diff;
38 | this_._prevValue = value;
39 | }, 10);
40 | };
41 |
42 | SineModulator.prototype._phaseNow = function() {
43 | var timeDiff = (Date.now() - this._startedAt) / 1000;
44 | var phase = this._phaseOffset + timeDiff * this.frequency % 1;
45 | return phase;
46 | };
47 |
48 | SineModulator.prototype._modValueNow = function() {
49 | var phase = this._phaseNow();
50 | return Math.sin((phase) * 2 * Math.PI) * this.depth;
51 | };
52 |
53 | SineModulator.prototype.stop = function() {
54 | clearInterval(this._interval);
55 | }
56 |
57 | module.exports = SineModulator;
58 |
--------------------------------------------------------------------------------
/js/Synth.js:
--------------------------------------------------------------------------------
1 | var WaveForm = require('./synthMixins/WaveForm')
2 | var PitchShifter = require('./synthMixins/PitchShifter')
3 | var ADSR = require('./synthMixins/ADSR')
4 |
5 | function Synth(context) {
6 | this.audioContext = context;
7 | this.output = context.createGain();
8 |
9 | this._oscillators = {};
10 |
11 | WaveForm.apply(this, arguments);
12 | PitchShifter.apply(this, arguments);
13 | ADSR.apply(this, arguments);
14 | }
15 |
16 | Synth.prototype.play = function(note) {
17 | var oscillator;
18 |
19 | oscillator = this._oscillators[note.pitch];
20 | if (oscillator) {
21 | this.stop(note);
22 | }
23 |
24 | oscillator = this._oscillators[note.pitch] = this.audioContext.createOscillator();
25 | oscillator.frequency.value = note.frequency;
26 | oscillator.connect(this.output);
27 | oscillator.start(0);
28 | return oscillator;
29 | };
30 |
31 | Synth.prototype.stop = function(note) {
32 | this._oscillators[note.pitch].disconnect(this.output);
33 | this._oscillators[note.pitch].stop(0);
34 | delete this._oscillators[note.pitch];
35 | };
36 |
37 | Synth.prototype.connect = function(output) {
38 | this.output.connect(output);
39 | };
40 |
41 | module.exports = Synth;
42 |
--------------------------------------------------------------------------------
/js/app.js:
--------------------------------------------------------------------------------
1 | var ScreenKeyboard = require('./Keyboard');
2 | var KeyboardListener = require('./KeyboardListener');
3 | var MidiListener = require('./MidiListener');
4 | var Controls = require('./Controls');
5 | var Synth = require('./Synth');
6 | var Delay = require('./effects/Delay');
7 | var SineModulator = require('./SineModulator');
8 |
9 | var audioCtx = new global.AudioContext();
10 | var synth = new Synth(audioCtx);
11 | var volume = audioCtx.createGain();
12 | var pan = audioCtx.createStereoPanner();
13 | var delay = new Delay(audioCtx);
14 |
15 | synth.connect(volume);
16 | volume.connect(delay.input);
17 | delay.connect(pan);
18 | pan.connect(audioCtx.destination);
19 |
20 | var tremolo = new SineModulator().modulate(volume.gain, 'value');
21 | var vibrato = new SineModulator().modulate(synth, 'pitchShift');
22 | var controls = new Controls(document.querySelector('.controls'));
23 |
24 | controls.on('wave-form-change', function(type) {
25 | synth.waveForm = type;
26 | });
27 |
28 | controls.on('volume-change', function(value) {
29 | volume.gain.value = value;
30 | });
31 |
32 | controls.on('pan-change', function(value) {
33 | pan.pan.value = value;
34 | });
35 |
36 | controls.on('tremolo-on-change', function(value) {
37 | parseInt(value) ? tremolo.start() : tremolo.stop();
38 | });
39 |
40 | controls.on('tremolo-depth-change', function(value) {
41 | tremolo.depth = value;
42 | });
43 |
44 | controls.on('tremolo-freq-change', function(value) {
45 | tremolo.frequency = value;
46 | });
47 |
48 | controls.on('vibrato-on-change', function(value) {
49 | parseInt(value) ? vibrato.start() : vibrato.stop();
50 | });
51 |
52 | controls.on('vibrato-depth-change', function(value) {
53 | vibrato.depth = value;
54 | });
55 |
56 | controls.on('vibrato-freq-change', function(value) {
57 | vibrato.frequency = value;
58 | });
59 |
60 | controls.on('delay-on-change', function(value) {
61 | parseInt(value) ? delay.start() : delay.stop();
62 | });
63 |
64 | controls.on('delay-feedback-change', function(value) {
65 | delay.feedback = value;
66 | });
67 |
68 | controls.on('delay-taps-change', function(value) {
69 | delay.taps = value;
70 | });
71 |
72 | controls.on('delay-latency-change', function(value) {
73 | delay.latency = value;
74 | });
75 |
76 | controls.on('delay-latency-change', function(value) {
77 | delay.latency = value;
78 | });
79 |
80 | controls.on('adsr-a-change', function(value) {
81 | synth.ADSR.A = value;
82 | });
83 |
84 | controls.on('adsr-d-change', function(value) {
85 | synth.ADSR.D = value;
86 | });
87 |
88 | controls.on('adsr-s-change', function(value) {
89 | synth.ADSR.S = value;
90 | });
91 |
92 | controls.on('adsr-r-change', function(value) {
93 | synth.ADSR.R = value;
94 | });
95 |
96 | controls.activate();
97 |
98 | var screenKeyboard = new ScreenKeyboard(document.querySelector('.keyboard'));
99 | screenKeyboard.draw(48, 83);
100 | screenKeyboard.startMouseListening();
101 |
102 | screenKeyboard.on('notePressed', function(note) {
103 | synth.play(note);
104 | });
105 |
106 | screenKeyboard.on('noteReleased', function(note) {
107 | synth.stop(note);
108 | });
109 |
110 | var keyboardListener = new KeyboardListener({startNote: 48, endNote: 83});
111 | keyboardListener.on('keyPressed', (pitch) => screenKeyboard.press(pitch));
112 | keyboardListener.on('keyReleased', (pitch) => screenKeyboard.release(pitch));
113 |
114 | var midiListener = new MidiListener({startNote: 48, endNote: 83});
115 | midiListener.on('keyPressed', (pitch) => screenKeyboard.press(pitch));
116 | midiListener.on('keyReleased', (pitch) => screenKeyboard.release(pitch));
117 |
--------------------------------------------------------------------------------
/js/effects/Delay.js:
--------------------------------------------------------------------------------
1 | function Delay(audioCtx) {
2 | this._audioCtx = audioCtx;
3 | this.input = audioCtx.createGain();
4 | this._delayLines = [];
5 | this._gainNodes = [];
6 | this._delayLinesInput = audioCtx.createGain();
7 | this._output = audioCtx.createGain();
8 |
9 | this._taps = 0;
10 | this._latency = 0;
11 | this._feedback = 0;
12 |
13 | Object.defineProperty(this, "feedback", {
14 | set: function (freq) {
15 | this._feedback = freq;
16 | this._applyParams();
17 | },
18 | get: function() {
19 | return this._feedback;
20 | }
21 | });
22 |
23 | Object.defineProperty(this, "latency", {
24 | set: function (freq) {
25 | this._latency = freq;
26 | this._applyParams();
27 | },
28 | get: function() {
29 | return this._latency;
30 | }
31 | });
32 |
33 | Object.defineProperty(this, "taps", {
34 | set: function (value) {
35 | var prevTaps = this._taps;
36 | var diff = value - this._taps;
37 | for(var i = 0; i < diff; i++) {
38 | diff < 0 ? this._popTap() : this._pushTap();
39 | }
40 | this._taps = value;
41 | },
42 | get: function() {
43 | return this._taps;
44 | }
45 | });
46 |
47 | this.input.connect(this._output);
48 | }
49 |
50 | Delay.prototype._applyParams = function() {
51 | for(var i = 0; i < this._delayLines.length; i++) {
52 | this._delayLines[i].delayTime.value = this._latency / 1000 * (i + 1);
53 | this._gainNodes[i].gain.value = Math.pow(this._feedback, (1 + i))
54 | }
55 | };
56 |
57 | Delay.prototype._pushTap = function() {
58 | var delay = this._audioCtx.createDelay(10.0);
59 | this._delayLines.push(delay);
60 |
61 | var gainNode = this._audioCtx.createGain();
62 | this._gainNodes.push(gainNode);
63 |
64 | gainNode.connect(this._output);
65 | delay.connect(gainNode);
66 | this._delayLinesInput.connect(delay);
67 | };
68 |
69 | Delay.prototype._popTap = function() {
70 | var lastDelayLine = this._delayLines.pop();
71 | var lastGainNode = this._gainNodes.pop();
72 |
73 | lastDelayLine.disconnect(lastGainNode);
74 | lastGainNode.disconnect(this._output);
75 | this._delayLinesInput.disconnect(lastDelayLine);
76 | };
77 |
78 | Delay.prototype.start = function() {
79 | if (!this._started) {
80 | this.input.connect(this._delayLinesInput);
81 | this._started = true;
82 | }
83 | }
84 |
85 | Delay.prototype.stop = function() {
86 | if (this._started) {
87 | this.input.disconnect(this._delayLinesInput);
88 | this._started = false;
89 | }
90 | };
91 |
92 | Delay.prototype.connect = function(target) {
93 | this._output.connect(target);
94 | };
95 |
96 | module.exports = Delay;
97 |
--------------------------------------------------------------------------------
/js/prod/app.js:
--------------------------------------------------------------------------------
1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o -1) {
46 | key.classList.add('key-black');
47 | }
48 | this.el.appendChild(key);
49 | }
50 | };
51 |
52 | Keyboard.prototype.press = function(pitch) {
53 | var el = this._keyEls[pitch];
54 | el.classList.add('pressed');
55 | this.emit('notePressed', new Note(el.dataset.pitch));
56 | };
57 |
58 | Keyboard.prototype.release = function(pitch) {
59 | var el = this._keyEls[pitch];
60 | el.classList.remove('pressed');
61 | this.emit('noteReleased', new Note(el.dataset.pitch));
62 | };
63 |
64 | Keyboard.prototype.startMouseListening = function() {
65 | this.el.addEventListener('mousedown', (e) => {
66 | if (!e.target.classList.contains('key'))
67 | return;
68 | this._mouseDown = true;
69 | this.press(e.target.dataset.pitch);
70 | });
71 |
72 | this.el.addEventListener('mouseover', (e) => {
73 | if (!e.target.classList.contains('key'))
74 | return;
75 | if (this._mouseDown) {
76 | this.press(e.target.dataset.pitch);
77 | }
78 | });
79 |
80 | this.el.addEventListener('mouseleave', (e) => {
81 | if (!e.target.classList.contains('key'))
82 | return;
83 | this._mouseDown = false;
84 | });
85 |
86 | this.el.addEventListener('mouseout', (e) => {
87 | if (!e.target.classList.contains('key'))
88 | return;
89 | if (this._mouseDown) {
90 | this.release(e.target.dataset.pitch);
91 | }
92 | });
93 |
94 | this.el.addEventListener('mouseup', (e) => {
95 | if (!e.target.classList.contains('key'))
96 | return;
97 | if (this._mouseDown) {
98 | this.release(e.target.dataset.pitch);
99 | }
100 | this._mouseDown = false;
101 | });
102 | };
103 |
104 | module.exports = Keyboard;
105 |
106 | },{"./MediatorMixin":4,"./Note":6}],3:[function(require,module,exports){
107 | (function (global){
108 | var MediatorMixin = require('./MediatorMixin');
109 |
110 | const KEYCODE_TO_PITCH_MAP = {
111 | 81: 48,
112 | 50: 49,
113 | 87: 50,
114 | 51: 51,
115 | 69: 52,
116 | 82: 53,
117 | 53: 54,
118 | 84: 55,
119 | 54: 56,
120 | 90: 57,
121 | 55: 58,
122 | 85: 59,
123 | 73: 60,
124 | 57: 61,
125 | 79: 62,
126 | 48: 63,
127 | 80: 64,
128 | 186: 65,
129 | 65: 66,
130 | 89: 67,
131 | 83: 68,
132 | 88: 69,
133 | 68: 70,
134 | 67: 71,
135 | 86: 72,
136 | 71: 73,
137 | 66: 74,
138 | 72: 75,
139 | 78: 76,
140 | 77: 77,
141 | 75: 78,
142 | 188: 79,
143 | 76: 80,
144 | 190: 81,
145 | 192: 82,
146 | 189: 83
147 | };
148 |
149 | KeyboardListener.prototype.KEYCODE_TO_PITCH_MAP;
150 |
151 | function KeyboardListener (options) {
152 | options = Object.assign({}, options);
153 | MediatorMixin.call(this);
154 |
155 | if (!options.startNote || options.startNote < 48) {
156 | throw new Error('startNote must be a number greater or equal than 48');
157 | }
158 |
159 | if (!options.endNote || options.endNote > 83) {
160 | throw new Error('endNote must be a number less or equal than 83');
161 | }
162 |
163 | this._options = options;
164 | this._buttonStatuses = {};
165 |
166 | var emitPitch = (name) => (e) => {
167 | var pitch = KEYCODE_TO_PITCH_MAP[e.keyCode];
168 | if (pitch && pitch >= this._options.startNote && pitch <= this._options.endNote && this._buttonStatuses[pitch] != name) {
169 | this.emit(name, pitch);
170 | }
171 | this._buttonStatuses[pitch] = name;
172 | }
173 |
174 | global.window.addEventListener('keydown', emitPitch('keyPressed'));
175 | global.window.addEventListener('keyup', emitPitch('keyReleased'));
176 | }
177 |
178 | module.exports = KeyboardListener;
179 |
180 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
181 | },{"./MediatorMixin":4}],4:[function(require,module,exports){
182 | function MediatorMixin() {
183 | this._events = {};
184 | this.on = function(eventName, callback) {
185 | this._events[eventName] = this._events[eventName] || [];
186 | this._events[eventName].push(callback);
187 | };
188 |
189 | this.emit = function(eventName) {
190 | var args = Array.prototype.slice.call(arguments, 1);
191 |
192 | if (this._events[eventName]) {
193 | this._events[eventName].forEach(function(callback) {
194 | callback.apply(null, args);
195 | });
196 | }
197 | };
198 | };
199 |
200 | module.exports = MediatorMixin;
201 | },{}],5:[function(require,module,exports){
202 | var MediatorMixin = require('./MediatorMixin');
203 |
204 | MidiListener.prototype._emitMidiMessage = function(event) {
205 | var eventName = {
206 | 148: 'keyPressed',
207 | 132: 'keyReleased'
208 | }[event.data[0]];
209 | this.emit(eventName, event.data[1]);
210 | };
211 |
212 | function MidiListener () {
213 | MediatorMixin.call(this);
214 |
215 | if (window.navigator.requestMIDIAccess) {
216 | window.navigator.requestMIDIAccess({sysex:false})
217 | .then((midiAccess) => {
218 | var midiInputs = midiAccess.inputs.values();
219 | var input = midiInputs.next();
220 | do {
221 | input.value.onmidimessage = (event) => this._emitMidiMessage(event);
222 | input = midiInputs.next();
223 | } while(!input.done)
224 | }, () => console.log('Failed to get access to the MIDI device'));
225 | } else {
226 | console.log('MIDI API is not available in the browser');
227 | }
228 | }
229 |
230 | module.exports = MidiListener;
231 |
232 | },{"./MediatorMixin":4}],6:[function(require,module,exports){
233 | function Note(letterWithOctaveOrPitch) {
234 | if (!this._parsePitch(letterWithOctaveOrPitch) && !this._parseLetter(letterWithOctaveOrPitch)) {
235 | throw new Error('Can not parse ' + letterWithOctaveOrPitch);
236 | }
237 | }
238 |
239 | Note.prototype._NOTES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'B', 'H'];
240 |
241 | Note.prototype._parsePitch = function(pitch) {
242 | // 21 == A0
243 | pitch = parseInt(pitch);
244 | if (isNaN(pitch) || pitch < 21 || pitch > 108)
245 | return false;
246 |
247 | this.letter = this._NOTES[(pitch - 21 + 9) % 12];
248 | this.octave = Math.floor((pitch - 12) / 12);
249 | this.pitch = pitch;
250 | this.frequency = this._freq(this.pitch);
251 | return true;
252 | };
253 |
254 | Note.prototype._parseLetter = function(letterOctave) {
255 | var match = letterOctave.match(/([ABCDEFGH]#?)(\d+)/);
256 | if (!match.length)
257 | return false;
258 | this.letter = match[1];
259 | this.octave = parseInt(match[2]);
260 | this.pitch = this._NOTES.indexOf(this.letter) + 12 * (this.octave + 1);
261 | this.frequency = this._freq(this.pitch);
262 | return true;
263 | };
264 |
265 | Note.prototype._freq = function(pitch) {
266 | return Math.pow(2, (pitch - 20 - 49) / 12) * 440;
267 | };
268 |
269 | module.exports = Note;
270 | },{}],7:[function(require,module,exports){
271 | function SineModulator (options) {
272 | options = options || {};
273 | this._frequency = options.frequency || 0;
274 | this._phaseOffset = 0;
275 | this._startedAt = 0;
276 | this._interval = null;
277 | this._prevValue = 0;
278 | this.depth = options.depth || 0;
279 |
280 | Object.defineProperty(this, "frequency", {
281 | set: function (frequency) {
282 | // the offset is needed in order to have seamless
283 | // transition between different frequencies
284 | frequency = parseFloat(frequency);
285 | this._phaseOffset = this._phaseNow();
286 | this._startedAt = Date.now();
287 | this._frequency = frequency;
288 | },
289 | get: function() {
290 | return this._frequency;
291 | }
292 | });
293 | }
294 |
295 | SineModulator.prototype.modulate = function(object, property) {
296 | this._objToModulate = object;
297 | this._propertyToModulate = property;
298 | return this;
299 | };
300 |
301 | SineModulator.prototype.start = function() {
302 | this._startedAt = Date.now();
303 | var this_ = this;
304 | this._interval = setInterval(function() {
305 | var value = this_._modValueNow();
306 | var diff = value - this_._prevValue;
307 | this_._objToModulate[this_._propertyToModulate] += diff;
308 | this_._prevValue = value;
309 | }, 10);
310 | };
311 |
312 | SineModulator.prototype._phaseNow = function() {
313 | var timeDiff = (Date.now() - this._startedAt) / 1000;
314 | var phase = this._phaseOffset + timeDiff * this.frequency % 1;
315 | return phase;
316 | };
317 |
318 | SineModulator.prototype._modValueNow = function() {
319 | var phase = this._phaseNow();
320 | return Math.sin((phase) * 2 * Math.PI) * this.depth;
321 | };
322 |
323 | SineModulator.prototype.stop = function() {
324 | clearInterval(this._interval);
325 | }
326 |
327 | module.exports = SineModulator;
328 |
329 | },{}],8:[function(require,module,exports){
330 | var WaveForm = require('./synthMixins/WaveForm')
331 | var PitchShifter = require('./synthMixins/PitchShifter')
332 | var ADSR = require('./synthMixins/ADSR')
333 |
334 | function Synth(context) {
335 | this.audioContext = context;
336 | this.output = context.createGain();
337 |
338 | this._oscillators = {};
339 |
340 | WaveForm.apply(this, arguments);
341 | PitchShifter.apply(this, arguments);
342 | ADSR.apply(this, arguments);
343 | }
344 |
345 | Synth.prototype.play = function(note) {
346 | var oscillator;
347 |
348 | oscillator = this._oscillators[note.pitch];
349 | if (oscillator) {
350 | this.stop(note);
351 | }
352 |
353 | oscillator = this._oscillators[note.pitch] = this.audioContext.createOscillator();
354 | oscillator.frequency.value = note.frequency;
355 | oscillator.connect(this.output);
356 | oscillator.start(0);
357 | return oscillator;
358 | };
359 |
360 | Synth.prototype.stop = function(note) {
361 | this._oscillators[note.pitch].disconnect(this.output);
362 | this._oscillators[note.pitch].stop(0);
363 | delete this._oscillators[note.pitch];
364 | };
365 |
366 | Synth.prototype.connect = function(output) {
367 | this.output.connect(output);
368 | };
369 |
370 | module.exports = Synth;
371 |
372 | },{"./synthMixins/ADSR":11,"./synthMixins/PitchShifter":12,"./synthMixins/WaveForm":13}],9:[function(require,module,exports){
373 | (function (global){
374 | var ScreenKeyboard = require('./Keyboard');
375 | var KeyboardListener = require('./KeyboardListener');
376 | var MidiListener = require('./MidiListener');
377 | var Controls = require('./Controls');
378 | var Synth = require('./Synth');
379 | var Delay = require('./effects/Delay');
380 | var SineModulator = require('./SineModulator');
381 |
382 | var audioCtx = new global.AudioContext();
383 | var synth = new Synth(audioCtx);
384 | var volume = audioCtx.createGain();
385 | var pan = audioCtx.createStereoPanner();
386 | var delay = new Delay(audioCtx);
387 |
388 | synth.connect(volume);
389 | volume.connect(delay.input);
390 | delay.connect(pan);
391 | pan.connect(audioCtx.destination);
392 |
393 | var tremolo = new SineModulator().modulate(volume.gain, 'value');
394 | var vibrato = new SineModulator().modulate(synth, 'pitchShift');
395 | var controls = new Controls(document.querySelector('.controls'));
396 |
397 | controls.on('wave-form-change', function(type) {
398 | synth.waveForm = type;
399 | });
400 |
401 | controls.on('volume-change', function(value) {
402 | volume.gain.value = value;
403 | });
404 |
405 | controls.on('pan-change', function(value) {
406 | pan.pan.value = value;
407 | });
408 |
409 | controls.on('tremolo-on-change', function(value) {
410 | parseInt(value) ? tremolo.start() : tremolo.stop();
411 | });
412 |
413 | controls.on('tremolo-depth-change', function(value) {
414 | tremolo.depth = value;
415 | });
416 |
417 | controls.on('tremolo-freq-change', function(value) {
418 | tremolo.frequency = value;
419 | });
420 |
421 | controls.on('vibrato-on-change', function(value) {
422 | parseInt(value) ? vibrato.start() : vibrato.stop();
423 | });
424 |
425 | controls.on('vibrato-depth-change', function(value) {
426 | vibrato.depth = value;
427 | });
428 |
429 | controls.on('vibrato-freq-change', function(value) {
430 | vibrato.frequency = value;
431 | });
432 |
433 | controls.on('delay-on-change', function(value) {
434 | parseInt(value) ? delay.start() : delay.stop();
435 | });
436 |
437 | controls.on('delay-feedback-change', function(value) {
438 | delay.feedback = value;
439 | });
440 |
441 | controls.on('delay-taps-change', function(value) {
442 | delay.taps = value;
443 | });
444 |
445 | controls.on('delay-latency-change', function(value) {
446 | delay.latency = value;
447 | });
448 |
449 | controls.on('delay-latency-change', function(value) {
450 | delay.latency = value;
451 | });
452 |
453 | controls.on('adsr-a-change', function(value) {
454 | synth.ADSR.A = value;
455 | });
456 |
457 | controls.on('adsr-d-change', function(value) {
458 | synth.ADSR.D = value;
459 | });
460 |
461 | controls.on('adsr-s-change', function(value) {
462 | synth.ADSR.S = value;
463 | });
464 |
465 | controls.on('adsr-r-change', function(value) {
466 | synth.ADSR.R = value;
467 | });
468 |
469 | controls.activate();
470 |
471 | var screenKeyboard = new ScreenKeyboard(document.querySelector('.keyboard'));
472 | screenKeyboard.draw(48, 83);
473 | screenKeyboard.startMouseListening();
474 |
475 | screenKeyboard.on('notePressed', function(note) {
476 | synth.play(note);
477 | });
478 |
479 | screenKeyboard.on('noteReleased', function(note) {
480 | synth.stop(note);
481 | });
482 |
483 | var keyboardListener = new KeyboardListener({startNote: 48, endNote: 83});
484 | keyboardListener.on('keyPressed', (pitch) => screenKeyboard.press(pitch));
485 | keyboardListener.on('keyReleased', (pitch) => screenKeyboard.release(pitch));
486 |
487 | var midiListener = new MidiListener({startNote: 48, endNote: 83});
488 | midiListener.on('keyPressed', (pitch) => screenKeyboard.press(pitch));
489 | midiListener.on('keyReleased', (pitch) => screenKeyboard.release(pitch));
490 |
491 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
492 | },{"./Controls":1,"./Keyboard":2,"./KeyboardListener":3,"./MidiListener":5,"./SineModulator":7,"./Synth":8,"./effects/Delay":10}],10:[function(require,module,exports){
493 | function Delay(audioCtx) {
494 | this._audioCtx = audioCtx;
495 | this.input = audioCtx.createGain();
496 | this._delayLines = [];
497 | this._gainNodes = [];
498 | this._delayLinesInput = audioCtx.createGain();
499 | this._output = audioCtx.createGain();
500 |
501 | this._taps = 0;
502 | this._latency = 0;
503 | this._feedback = 0;
504 |
505 | Object.defineProperty(this, "feedback", {
506 | set: function (freq) {
507 | this._feedback = freq;
508 | this._applyParams();
509 | },
510 | get: function() {
511 | return this._feedback;
512 | }
513 | });
514 |
515 | Object.defineProperty(this, "latency", {
516 | set: function (freq) {
517 | this._latency = freq;
518 | this._applyParams();
519 | },
520 | get: function() {
521 | return this._latency;
522 | }
523 | });
524 |
525 | Object.defineProperty(this, "taps", {
526 | set: function (value) {
527 | var prevTaps = this._taps;
528 | var diff = value - this._taps;
529 | for(var i = 0; i < diff; i++) {
530 | diff < 0 ? this._popTap() : this._pushTap();
531 | }
532 | this._taps = value;
533 | },
534 | get: function() {
535 | return this._taps;
536 | }
537 | });
538 |
539 | this.input.connect(this._output);
540 | }
541 |
542 | Delay.prototype._applyParams = function() {
543 | for(var i = 0; i < this._delayLines.length; i++) {
544 | this._delayLines[i].delayTime.value = this._latency / 1000 * (i + 1);
545 | this._gainNodes[i].gain.value = Math.pow(this._feedback, (1 + i))
546 | }
547 | };
548 |
549 | Delay.prototype._pushTap = function() {
550 | var delay = this._audioCtx.createDelay(10.0);
551 | this._delayLines.push(delay);
552 |
553 | var gainNode = this._audioCtx.createGain();
554 | this._gainNodes.push(gainNode);
555 |
556 | gainNode.connect(this._output);
557 | delay.connect(gainNode);
558 | this._delayLinesInput.connect(delay);
559 | };
560 |
561 | Delay.prototype._popTap = function() {
562 | var lastDelayLine = this._delayLines.pop();
563 | var lastGainNode = this._gainNodes.pop();
564 |
565 | lastDelayLine.disconnect(lastGainNode);
566 | lastGainNode.disconnect(this._output);
567 | this._delayLinesInput.disconnect(lastDelayLine);
568 | };
569 |
570 | Delay.prototype.start = function() {
571 | if (!this._started) {
572 | this.input.connect(this._delayLinesInput);
573 | this._started = true;
574 | }
575 | }
576 |
577 | Delay.prototype.stop = function() {
578 | if (this._started) {
579 | this.input.disconnect(this._delayLinesInput);
580 | this._started = false;
581 | }
582 | };
583 |
584 | Delay.prototype.connect = function(target) {
585 | this._output.connect(target);
586 | };
587 |
588 | module.exports = Delay;
589 |
590 | },{}],11:[function(require,module,exports){
591 | function ADSR() {
592 | this.ADSR = {
593 | A: null,
594 | D: null,
595 | S: null,
596 | R: null
597 | };
598 |
599 | var oscillators = {};
600 | var gainNodes = {};
601 | var asdIntervals = {};
602 | var rIntervals = {};
603 |
604 | var old = {
605 | play: this.play,
606 | stop: this.stop
607 | };
608 |
609 | this.play = function(note) {
610 | var gain = gainNodes[note.pitch];
611 | if (!gain) {
612 | gain = gainNodes[note.pitch] = this.audioContext.createGain();
613 | gain.connect(this.output);
614 | gain.gain.value = 0;
615 | }
616 |
617 | var startedAt = Date.now();
618 | var startedAtGain = gain.gain.value;
619 |
620 | if (oscillators[note.pitch]) {
621 | this._finalize(note);
622 | }
623 |
624 | var osc = oscillators[note.pitch] = old.play.call(this, note);
625 | osc.disconnect(this.output);
626 | osc.connect(gain);
627 |
628 | this.ADSR.A = parseInt(this.ADSR.A);
629 | this.ADSR.D = parseInt(this.ADSR.D);
630 | this.ADSR.S = parseFloat(this.ADSR.S);
631 | this.ADSR.R = parseInt(this.ADSR.R);
632 |
633 | asdIntervals[note.pitch] = setInterval(() => {
634 | var diff = Date.now() - startedAt;
635 | if (diff < this.ADSR.A) {
636 | gain.gain.value = startedAtGain + (1 - startedAtGain) * (diff / this.ADSR.A);
637 | } else if (diff < this.ADSR.A + this.ADSR.D) {
638 | gain.gain.value = 1 - (diff - this.ADSR.A) / (this.ADSR.D / (1 - this.ADSR.S));
639 | } else {
640 | gain.gain.value = this.ADSR.S;
641 | clearInterval(asdIntervals[note.pitch]);
642 | }
643 | }, 10);
644 |
645 | return osc;
646 | };
647 |
648 | this._finalize = function(note) {
649 | var osc = oscillators[note.pitch];
650 | var gain = gainNodes[note.pitch];
651 | clearInterval(rIntervals[note.pitch]);
652 | gain.gain.value = 0;
653 | osc.disconnect(gain);
654 | osc.connect(this.output);
655 | delete oscillators[note.pitch];
656 | old.stop.apply(this, arguments);
657 | }
658 |
659 | this.stop = function(note) {
660 | var releasedAt = Date.now();
661 | var this_ = this;
662 | var arguments_ = arguments;
663 | var gain = gainNodes[note.pitch];
664 | var gainOnRelease = gain.gain.value;
665 | rIntervals[note.pitch] = setInterval(() => {
666 | var diff = Date.now() - releasedAt;
667 | if (diff < this_.ADSR.R) {
668 | gain.gain.value = gainOnRelease * (1 - diff / this_.ADSR.R);
669 | } else {
670 | this._finalize(note);
671 | }
672 | }, 10);
673 | };
674 | }
675 |
676 | module.exports = ADSR;
677 |
678 | },{}],12:[function(require,module,exports){
679 | function PitchShifter() {
680 | this._pitchShift = 0;
681 | var oscillators = {};
682 |
683 | Object.defineProperty(this, "pitchShift", {
684 | set: function (ps) {
685 | this._pitchShift = ps;
686 | for(var pitch in oscillators) {
687 | oscillators[pitch].frequency.value =
688 | oscillators[pitch].baseFrequency * Math.pow(2, this._pitchShift/1200);
689 | }
690 | },
691 | get: function() {
692 | return this._pitchShift;
693 | }
694 | });
695 |
696 | var old = {
697 | play: this.play,
698 | stop: this.stop
699 | };
700 |
701 | this.play = function(note) {
702 | var osc = oscillators[note.pitch] = old.play.call(this, note);
703 | osc.baseFrequency = note.frequency;
704 | osc.frequency.value = osc.baseFrequency * Math.pow(2, this._pitchShift/1200);
705 | return osc;
706 | };
707 |
708 | this.stop = function(note) {
709 | delete oscillators[note.pitch];
710 | old.stop.apply(this, arguments);
711 | };
712 |
713 | }
714 |
715 | module.exports = PitchShifter;
716 | },{}],13:[function(require,module,exports){
717 | var sawtooth = require('../waveforms/sawtooth');
718 | var square = require('../waveforms/square');
719 | var sine = require('../waveforms/sine');
720 |
721 | function WaveForm() {
722 | Object.defineProperty(this, "waveForm", {
723 | set: function(waveForm) {
724 | this._waveForm = {
725 | 'sawtooth': sawtooth,
726 | 'square': square,
727 | 'sine': sine
728 | }[waveForm];
729 | },
730 | get: function() {
731 | return this._waveForm;
732 | }
733 | });
734 |
735 | var old = {
736 | play: this.play,
737 | stop: this.stop
738 | };
739 |
740 | this.play = function() {
741 | var osc = old.play.apply(this, arguments);
742 | osc.setPeriodicWave(this._waveForm || sine);
743 | return osc;
744 | }
745 |
746 | this.stop = function() {
747 | old.stop.apply(this, arguments);
748 | }
749 | }
750 |
751 | module.exports = WaveForm;
752 | },{"../waveforms/sawtooth":14,"../waveforms/sine":15,"../waveforms/square":16}],14:[function(require,module,exports){
753 | (function (global){
754 | var context = new global.AudioContext();
755 | var steps = 128;
756 | var real = new global.Float32Array(steps);
757 | var imag = new global.Float32Array(steps);
758 |
759 | for (var i = 1; i < steps; i++) {
760 | imag[i] = 1 / (i * Math.PI);
761 | }
762 |
763 | var wave = context.createPeriodicWave(real, imag);
764 |
765 | module.exports = wave;
766 |
767 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
768 | },{}],15:[function(require,module,exports){
769 | (function (global){
770 | var context = new global.AudioContext();
771 | var realCoeffs = new global.Float32Array([0,0]);
772 | var imagCoeffs = new global.Float32Array([0,1]);
773 | var wave = context.createPeriodicWave(realCoeffs, imagCoeffs);
774 |
775 | module.exports = wave;
776 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
777 | },{}],16:[function(require,module,exports){
778 | (function (global){
779 | var context = new global.AudioContext();
780 | var approaches = 128;
781 | var real = new global.Float32Array(approaches);
782 | var imag = new global.Float32Array(approaches);
783 |
784 | real[0] = 0;
785 | for (var i = 1; i < approaches; i++) {
786 | imag[i] = i % 2 == 0 ? 0 : 4 / (i * Math.PI);
787 | }
788 |
789 | var wave = context.createPeriodicWave(real, imag);
790 |
791 | module.exports = wave;
792 |
793 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
794 | },{}],17:[function(require,module,exports){
795 | module.exports = "\n\n\n
Tremolo
\n \n \n \n \n\n\n\n
Vibrato
\n \n \n \n \n\n\n\n
Delay
\n \n \n \n \n \n\n\n\n\n\n \n \n
\n\n\n \n \n \n
\n";
796 |
797 | },{}]},{},[9]);
798 |
--------------------------------------------------------------------------------
/js/synthMixins/ADSR.js:
--------------------------------------------------------------------------------
1 | function ADSR() {
2 | this.ADSR = {
3 | A: null,
4 | D: null,
5 | S: null,
6 | R: null
7 | };
8 |
9 | var oscillators = {};
10 | var gainNodes = {};
11 | var asdIntervals = {};
12 | var rIntervals = {};
13 |
14 | var old = {
15 | play: this.play,
16 | stop: this.stop
17 | };
18 |
19 | this.play = function(note) {
20 | var gain = gainNodes[note.pitch];
21 | if (!gain) {
22 | gain = gainNodes[note.pitch] = this.audioContext.createGain();
23 | gain.connect(this.output);
24 | gain.gain.value = 0;
25 | }
26 |
27 | var startedAt = Date.now();
28 | var startedAtGain = gain.gain.value;
29 |
30 | if (oscillators[note.pitch]) {
31 | this._finalize(note);
32 | }
33 |
34 | var osc = oscillators[note.pitch] = old.play.call(this, note);
35 | osc.disconnect(this.output);
36 | osc.connect(gain);
37 |
38 | this.ADSR.A = parseInt(this.ADSR.A);
39 | this.ADSR.D = parseInt(this.ADSR.D);
40 | this.ADSR.S = parseFloat(this.ADSR.S);
41 | this.ADSR.R = parseInt(this.ADSR.R);
42 |
43 | asdIntervals[note.pitch] = setInterval(() => {
44 | var diff = Date.now() - startedAt;
45 | if (diff < this.ADSR.A) {
46 | gain.gain.value = startedAtGain + (1 - startedAtGain) * (diff / this.ADSR.A);
47 | } else if (diff < this.ADSR.A + this.ADSR.D) {
48 | gain.gain.value = 1 - (diff - this.ADSR.A) / (this.ADSR.D / (1 - this.ADSR.S));
49 | } else {
50 | gain.gain.value = this.ADSR.S;
51 | clearInterval(asdIntervals[note.pitch]);
52 | }
53 | }, 10);
54 |
55 | return osc;
56 | };
57 |
58 | this._finalize = function(note) {
59 | var osc = oscillators[note.pitch];
60 | var gain = gainNodes[note.pitch];
61 | clearInterval(rIntervals[note.pitch]);
62 | gain.gain.value = 0;
63 | osc.disconnect(gain);
64 | osc.connect(this.output);
65 | delete oscillators[note.pitch];
66 | old.stop.apply(this, arguments);
67 | }
68 |
69 | this.stop = function(note) {
70 | var releasedAt = Date.now();
71 | var this_ = this;
72 | var arguments_ = arguments;
73 | var gain = gainNodes[note.pitch];
74 | var gainOnRelease = gain.gain.value;
75 | rIntervals[note.pitch] = setInterval(() => {
76 | var diff = Date.now() - releasedAt;
77 | if (diff < this_.ADSR.R) {
78 | gain.gain.value = gainOnRelease * (1 - diff / this_.ADSR.R);
79 | } else {
80 | this._finalize(note);
81 | }
82 | }, 10);
83 | };
84 | }
85 |
86 | module.exports = ADSR;
87 |
--------------------------------------------------------------------------------
/js/synthMixins/PitchShifter.js:
--------------------------------------------------------------------------------
1 | function PitchShifter() {
2 | this._pitchShift = 0;
3 | var oscillators = {};
4 |
5 | Object.defineProperty(this, "pitchShift", {
6 | set: function (ps) {
7 | this._pitchShift = ps;
8 | for(var pitch in oscillators) {
9 | oscillators[pitch].frequency.value =
10 | oscillators[pitch].baseFrequency * Math.pow(2, this._pitchShift/1200);
11 | }
12 | },
13 | get: function() {
14 | return this._pitchShift;
15 | }
16 | });
17 |
18 | var old = {
19 | play: this.play,
20 | stop: this.stop
21 | };
22 |
23 | this.play = function(note) {
24 | var osc = oscillators[note.pitch] = old.play.call(this, note);
25 | osc.baseFrequency = note.frequency;
26 | osc.frequency.value = osc.baseFrequency * Math.pow(2, this._pitchShift/1200);
27 | return osc;
28 | };
29 |
30 | this.stop = function(note) {
31 | delete oscillators[note.pitch];
32 | old.stop.apply(this, arguments);
33 | };
34 |
35 | }
36 |
37 | module.exports = PitchShifter;
--------------------------------------------------------------------------------
/js/synthMixins/WaveForm.js:
--------------------------------------------------------------------------------
1 | var sawtooth = require('../waveforms/sawtooth');
2 | var square = require('../waveforms/square');
3 | var sine = require('../waveforms/sine');
4 |
5 | function WaveForm() {
6 | Object.defineProperty(this, "waveForm", {
7 | set: function(waveForm) {
8 | this._waveForm = {
9 | 'sawtooth': sawtooth,
10 | 'square': square,
11 | 'sine': sine
12 | }[waveForm];
13 | },
14 | get: function() {
15 | return this._waveForm;
16 | }
17 | });
18 |
19 | var old = {
20 | play: this.play,
21 | stop: this.stop
22 | };
23 |
24 | this.play = function() {
25 | var osc = old.play.apply(this, arguments);
26 | osc.setPeriodicWave(this._waveForm || sine);
27 | return osc;
28 | }
29 |
30 | this.stop = function() {
31 | old.stop.apply(this, arguments);
32 | }
33 | }
34 |
35 | module.exports = WaveForm;
--------------------------------------------------------------------------------
/js/waveforms/sawtooth.js:
--------------------------------------------------------------------------------
1 | var context = new global.AudioContext();
2 | var steps = 128;
3 | var real = new global.Float32Array(steps);
4 | var imag = new global.Float32Array(steps);
5 |
6 | for (var i = 1; i < steps; i++) {
7 | imag[i] = 1 / (i * Math.PI);
8 | }
9 |
10 | var wave = context.createPeriodicWave(real, imag);
11 |
12 | module.exports = wave;
13 |
--------------------------------------------------------------------------------
/js/waveforms/sine.js:
--------------------------------------------------------------------------------
1 | var context = new global.AudioContext();
2 | var realCoeffs = new global.Float32Array([0,0]);
3 | var imagCoeffs = new global.Float32Array([0,1]);
4 | var wave = context.createPeriodicWave(realCoeffs, imagCoeffs);
5 |
6 | module.exports = wave;
--------------------------------------------------------------------------------
/js/waveforms/square.js:
--------------------------------------------------------------------------------
1 | var context = new global.AudioContext();
2 | var approaches = 128;
3 | var real = new global.Float32Array(approaches);
4 | var imag = new global.Float32Array(approaches);
5 |
6 | real[0] = 0;
7 | for (var i = 1; i < approaches; i++) {
8 | imag[i] = i % 2 == 0 ? 0 : 4 / (i * Math.PI);
9 | }
10 |
11 | var wave = context.createPeriodicWave(real, imag);
12 |
13 | module.exports = wave;
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Synzer",
3 | "version": "0.0.0",
4 | "devDependencies": {
5 | "atob": "^1.1.2",
6 | "browserify": "^11.1.0",
7 | "btoa": "^1.1.2",
8 | "jasmine": "*",
9 | "node-dom": "^0.1.0",
10 | "proxyquire": "^1.7.2",
11 | "stringify": "^3.1.0"
12 | },
13 | "scripts": {
14 | "test": "node_modules/.bin/jasmine",
15 | "build": "node_modules/.bin/browserify js/app.js -t stringify -o js/prod/app.js"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/spec/DelaySpec.js:
--------------------------------------------------------------------------------
1 | var Delay = require('../js/effects/Delay');
2 |
3 | describe("Delay", function() {
4 | var delay, delayNodes, audioContext;
5 |
6 | beforeEach(function() {
7 | audioContext = {
8 | createDelay: function() {},
9 | createGain: function() {},
10 | destination: {}
11 | };
12 | delayNodes = [];
13 | gainNodes = [];
14 |
15 | for(var i = 0; i < 10; i++) {
16 | var delayNode = jasmine.createSpyObj('delayNode' + i, ['connect', 'disconnect']);
17 | delayNode.delayTime = {};
18 | delayNodes.push(delayNode);
19 |
20 | var gainNode = jasmine.createSpyObj('gainNode' + i, ['connect', 'disconnect']);
21 | gainNode.gain = {};
22 | gainNode.i = i;
23 | gainNodes.push(gainNode);
24 | }
25 |
26 | var delayNodeCounter = 0;
27 | spyOn(audioContext, 'createDelay').and.callFake(function() {
28 | return delayNodes[delayNodeCounter++];
29 | });
30 |
31 | var gainNodeCounter = 0;
32 | spyOn(audioContext, 'createGain').and.callFake(function() {
33 | return gainNodes[gainNodeCounter++];
34 | });
35 |
36 | delay = new Delay(audioContext);
37 | });
38 |
39 | it('connects input node to delays on start', function() {
40 | delay.start();
41 | expect(gainNodes[0].connect).toHaveBeenCalled();
42 | });
43 |
44 | it('disconnects input note from delays on stop', function() {
45 | delay.start();
46 | delay.stop();
47 | expect(gainNodes[0].disconnect).toHaveBeenCalled();
48 | });
49 |
50 | it('changes delays on frequency change', function() {
51 | delay.taps = 3;
52 | delay.latency = 200;
53 | expect(delayNodes[0].delayTime.value).toBeCloseTo(0.2);
54 | expect(delayNodes[1].delayTime.value).toBeCloseTo(0.4);
55 | expect(delayNodes[2].delayTime.value).toBeCloseTo(0.6);
56 | });
57 |
58 | it('changes delay lines, gain nodes amount on taps change', function() {
59 | delay.taps = 3;
60 | expect(delayNodes[0].connect).toHaveBeenCalled();
61 | expect(delayNodes[1].connect).toHaveBeenCalled();
62 | expect(delayNodes[2].connect).toHaveBeenCalled();
63 | expect(delayNodes[3].connect).not.toHaveBeenCalled();
64 |
65 | expect(gainNodes[3].connect).toHaveBeenCalled();
66 | expect(gainNodes[4].connect).toHaveBeenCalled();
67 | expect(gainNodes[5].connect).toHaveBeenCalled();
68 | expect(gainNodes[6].connect).not.toHaveBeenCalled();
69 |
70 | });
71 |
72 | it('changes feedback', function() {
73 | delay.taps = 3;
74 | delay.feedback = 0.5;
75 | expect(gainNodes[3].gain.value).toBeCloseTo(0.5);
76 | expect(gainNodes[4].gain.value).toBeCloseTo(0.25);
77 | expect(gainNodes[5].gain.value).toBeCloseTo(0.125);
78 | });
79 | });
--------------------------------------------------------------------------------
/spec/KeyboardListenerSpec.js:
--------------------------------------------------------------------------------
1 | var KeyboardListener = require('../js/KeyboardListener');
2 |
3 | describe('KeyboardListener', function() {
4 | beforeEach(function() {
5 | global.window = {
6 | addEventListener: jasmine.createSpy('window.addEventListener')
7 | };
8 | });
9 |
10 | afterEach(function() {
11 | delete global.window;
12 | });
13 |
14 | it('throws if specified notes range is out of allowed range (48 - 83)', function() {
15 | expect(function() {
16 | new KeyboardListener({startNote: 47, endNote: 81});
17 | }).toThrow();
18 | expect(function() {
19 | new KeyboardListener({startNote: 51, endNote: 85});
20 | }).toThrow();
21 | });
22 |
23 | it('does not throw when notes are within 48 - 83 range', function() {
24 | expect(function() {
25 | new KeyboardListener({startNote: 48, endNote: 83})
26 | }).not.toThrow();
27 | expect(function() {
28 | new KeyboardListener({startNote: 52, endNote: 63})
29 | }).not.toThrow();
30 | });
31 |
32 | describe('', function() {
33 | var keyboardListener;
34 | var keypressCallback;
35 | var keyupCallback;
36 | var onKeyPress;
37 | var onKeyRelease;
38 |
39 | beforeEach(function() {
40 | global.window.addEventListener.and.callFake(function (name, fn) {
41 | if (name == 'keydown') {
42 | keypressCallback = fn;
43 | }
44 | if (name == 'keyup') {
45 | keyupCallback = fn;
46 | }
47 | });
48 | keyboardListener = new KeyboardListener({startNote: 60, endNote: 72});
49 | onKeyPress = jasmine.createSpy('keyPress');
50 | onKeyRelease = jasmine.createSpy('keyRelease');
51 | keyboardListener.on('keyPressed', onKeyPress);
52 | keyboardListener.on('keyReleased', onKeyRelease);
53 | });
54 |
55 | it('emits keyPressed and keyReleased for keys within range', function() {
56 | keypressCallback({
57 | keyCode: 73
58 | });
59 | expect(onKeyPress).toHaveBeenCalledWith(60);
60 | expect(onKeyRelease).not.toHaveBeenCalled();
61 | keyupCallback({
62 | keyCode: 73
63 | });
64 | expect(onKeyRelease).toHaveBeenCalledWith(60);
65 | });
66 |
67 | it('does not emit keyPressed and keyReleased for keys outside the range', function() {
68 | keypressCallback({
69 | keyCode: 53
70 | });
71 | expect(onKeyPress).not.toHaveBeenCalled();
72 | keyupCallback({
73 | keyCode: 53
74 | });
75 | expect(onKeyRelease).not.toHaveBeenCalled();
76 | });
77 |
78 | it('does emit after second keyPressed with the same keyCode', function() {
79 | keypressCallback({
80 | keyCode: 73
81 | });
82 | onKeyPress.calls.reset();
83 | keypressCallback({
84 | keyCode: 73
85 | });
86 | expect(onKeyPress).not.toHaveBeenCalled();
87 | });
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/spec/KeyboardSpec.js:
--------------------------------------------------------------------------------
1 | var Keyboard = require('../js/Keyboard');
2 |
3 | describe('Keyboard', function() {
4 | var keyboard;
5 |
6 | beforeEach(function() {
7 | keyboard = new Keyboard;
8 | });
9 |
10 | afterEach(function() {
11 | });
12 | });
--------------------------------------------------------------------------------
/spec/MediatorMixinSpec.js:
--------------------------------------------------------------------------------
1 | var MediatorMixin = require('../js/MediatorMixin')
2 |
3 | describe('Mediator', function() {
4 | var obj = {};
5 |
6 | beforeEach(function() {
7 | MediatorMixin.call(obj);
8 | });
9 |
10 | it('subscribes', function() {
11 | expect(function() {
12 | obj.on('event1');
13 | }).not.toThrow();
14 | });
15 |
16 | it('reaches listener if emitted', function(done) {
17 | obj.on('ev1', function(value) {
18 | expect(value).toBe(1);
19 | done();
20 | });
21 | obj.emit('ev1', 1)
22 | });
23 |
24 | it('reaches multiple listeners', function(done) {
25 | var counter = 0;
26 |
27 | obj.on('ev1', function(a, b) {
28 | expect(a).toBe(1);
29 | expect(b).toBe(2);
30 | if (++counter == 2) done();
31 | });
32 |
33 | obj.on('ev1', function(a, b, c, d) {
34 | expect(a).toBe(1);
35 | expect(b).toBe(2);
36 | expect(c).toBe(3);
37 | expect(d).toBe(4);
38 | if (++counter == 2) done();
39 | });
40 |
41 | obj.emit('ev1', 1, 2, 3, 4);
42 | });
43 |
44 | it('does not reach not corresponding listeners', function(done) {
45 | obj.on('ev1', function() {
46 | done.fail();
47 | });
48 | obj.emit('ev2');
49 | setTimeout(done);
50 | });
51 |
52 | it('does not throw if no listener', function() {
53 | expect(function() { obj.emit('ev1'); }).not.toThrow();
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/spec/MidiListenerSpec.js:
--------------------------------------------------------------------------------
1 | var MidiListener = require('../js/MidiListener');
2 |
3 | describe('MidiListener', function() {
4 | it('', function() {
5 |
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/spec/NoteSpec.js:
--------------------------------------------------------------------------------
1 | var Note = require('../js/Note');
2 |
3 | describe('Note', function() {
4 | it('can be created with pitch', function() {
5 | var note1 = new Note(21);
6 | expect(note1.letter).toEqual("A");
7 | expect(note1.pitch).toEqual(21);
8 | expect(note1.octave).toEqual(0);
9 | });
10 |
11 | it('can be created with letter notation', function() {
12 | var note1 = new Note("C#4");
13 | expect(note1.letter).toEqual("C#");
14 | expect(note1.pitch).toEqual(61);
15 | expect(note1.octave).toEqual(4);
16 | });
17 |
18 | it('throws if invalid input', function() {
19 | expect(function() {new Note();}).toThrow();
20 | expect(function() {new Note(2);}).toThrow();
21 | expect(function() {new Note("nn");}).toThrow();
22 | expect(function() {new Note("N#6");}).toThrow();
23 | });
24 |
25 | it('calculates frequencies correctly', function() {
26 | expect(new Note('A4').frequency).toEqual(440);
27 | expect(new Note('F6').frequency).toBeCloseTo(1396.91);
28 | });
29 | });
--------------------------------------------------------------------------------
/spec/SineModulatorSpec.js:
--------------------------------------------------------------------------------
1 | var SineModulator = require('../js/SineModulator');
2 |
3 | describe('Sine Modulator', function() {
4 | var sineModulator;
5 | var modulatedObj;
6 | var prevVal;
7 |
8 | beforeEach(function() {
9 | modulatedObj = {
10 | param1: 100
11 | };
12 |
13 | sineModulator = new SineModulator({
14 | depth: 0.5,
15 | frequency: 1
16 | });
17 |
18 | prevVal = modulatedObj.param1;
19 |
20 | sineModulator.modulate(modulatedObj, 'param1');
21 | jasmine.clock().install();
22 | jasmine.clock().mockDate();
23 | sineModulator.start();
24 | });
25 |
26 | afterEach(function() {
27 | jasmine.clock().uninstall();
28 | });
29 |
30 | it('modulate returns the object', function() {
31 | expect(sineModulator.modulate(modulatedObj, 'param1')).toBe(sineModulator);
32 | });
33 |
34 | it('starts modulating', function() {
35 | jasmine.clock().tick(250);
36 | expect(modulatedObj.param1).toBeCloseTo(prevVal + 0.5);
37 | jasmine.clock().tick(250);
38 | expect(modulatedObj.param1).toBeCloseTo(prevVal);
39 | jasmine.clock().tick(250);
40 | expect(modulatedObj.param1).toBeCloseTo(prevVal - 0.5);
41 | jasmine.clock().tick(250);
42 | expect(modulatedObj.param1).toBeCloseTo(prevVal);
43 | });
44 |
45 | it('depth can be changed in runtime', function() {
46 | jasmine.clock().tick(250);
47 | expect(modulatedObj.param1).toBeCloseTo(prevVal + 0.5);
48 | jasmine.clock().tick(250);
49 | sineModulator.depth = 0.8;
50 | jasmine.clock().tick(250);
51 | expect(modulatedObj.param1).toBeCloseTo(prevVal - 0.8);
52 | });
53 |
54 | it('frequency can be changed in runtime', function() {
55 | jasmine.clock().tick(250);
56 | expect(modulatedObj.param1).toBeCloseTo(prevVal + 0.5);
57 | jasmine.clock().tick(250);
58 | sineModulator.frequency = 2;
59 | jasmine.clock().tick(125);
60 | expect(modulatedObj.param1).toBeCloseTo(prevVal - 0.5);
61 | jasmine.clock().tick(125);
62 | expect(modulatedObj.param1).toBeCloseTo(prevVal);
63 | });
64 |
65 | it('doesn\'t block the param from being changed by something else', function() {
66 | jasmine.clock().tick(500);
67 | modulatedObj.param1 += 50;
68 | jasmine.clock().tick(250);
69 | expect(modulatedObj.param1).toBeCloseTo(prevVal + 50 - 0.5);
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/spec/SynthMixinsSpec.js:
--------------------------------------------------------------------------------
1 | var proxyquire = require('proxyquire').noCallThru();
2 |
3 | var WaveForm = proxyquire('../js/synthMixins/WaveForm', {
4 | '../waveforms/sine': {periodicWave: true, sine: true},
5 | '../waveforms/sawtooth': {periodicWave: true, sawtooth: true},
6 | '../waveforms/square': {periodicWave: true, square: true}
7 | });
8 | var PitchShifter = require('../js/synthMixins/PitchShifter');
9 | var ADSR = require('../js/synthMixins/ADSR');
10 |
11 | describe('synthMixins', function() {
12 | var oscillator, audioContext, note, note2;
13 | beforeEach(function() {
14 | oscillator = jasmine.createSpyObj('oscillator', ['setPeriodicWave', 'connect', 'disconnect', 'start', 'stop']);
15 | oscillator.frequency = {};
16 | gainNode = jasmine.createSpyObj('gainNode', ['connect', 'disconnect']);
17 | gainNode.gain = {};
18 |
19 | audioContext = {
20 | createOscillator: function() {},
21 | createGain: function() {},
22 | };
23 | spyOn(audioContext, 'createOscillator').and.returnValue(oscillator);
24 | spyOn(audioContext, 'createGain').and.returnValue(gainNode);
25 |
26 | synth = {
27 | audioContext: audioContext,
28 | output: gainNode,
29 | play: function(note) {
30 | oscillator.frequency.value = note.frequency;
31 | return oscillator;
32 | },
33 | stop: function() {}
34 | };
35 | note = {pitch:44, frequency:100};
36 | note2 = {pitch:46, frequency:121};
37 | });
38 |
39 | describe("WaveForm", function() {
40 | beforeEach(function() {
41 | WaveForm.call(synth);
42 | });
43 |
44 | it('play method returns oscillator', function() {
45 | expect(synth.play(note)).toBe(oscillator);
46 | });
47 |
48 | it('waveForm property can be set', function() {
49 | synth.waveForm = 'square';
50 | synth.play(note);
51 | expect(oscillator.setPeriodicWave).toHaveBeenCalledWith(
52 | jasmine.objectContaining({periodicWave: true, square: true})
53 | );
54 | });
55 |
56 | it('uses sine if no wave form is set', function() {
57 | synth.play(note);
58 | expect(oscillator.setPeriodicWave).toHaveBeenCalledWith(
59 | jasmine.objectContaining({periodicWave: true, sine: true})
60 | );
61 | });
62 | });
63 |
64 | describe('Pitch shift', function() {
65 | beforeEach(function() {
66 | PitchShifter.call(synth);
67 | });
68 |
69 | it('play method returns oscillator', function() {
70 | expect(synth.play(note)).toBe(oscillator);
71 | });
72 |
73 | it('pitchShift property can be set', function() {
74 | synth.pitchShift = 1;
75 | synth.play(note);
76 | expect(oscillator.frequency.value).toBeCloseTo(note.frequency * (Math.pow(2, 1/1200)));
77 |
78 | synth.pitchShift = 10;
79 | expect(oscillator.frequency.value).toBeCloseTo(note.frequency * (Math.pow(2, 10/1200)));
80 |
81 | synth.pitchShift = 1200;
82 | expect(oscillator.frequency.value).toBeCloseTo(note.frequency * 2);
83 |
84 | synth.pitchShift = -1200;
85 | expect(oscillator.frequency.value).toBeCloseTo(note.frequency / 2);
86 | });
87 | });
88 |
89 | describe('ADSR', function () {
90 | beforeEach(function() {
91 | ADSR.call(synth);
92 | synth.ADSR.A = 100;
93 | synth.ADSR.D = 100;
94 | synth.ADSR.S = 0.5;
95 | synth.ADSR.R = 100;
96 | jasmine.clock().install();
97 | jasmine.clock().mockDate();
98 | });
99 |
100 | afterEach(function() {
101 | jasmine.clock().uninstall();
102 | })
103 |
104 | it('play method returns an oscillator', function() {
105 | expect(synth.play(note)).toBe(oscillator);
106 | });
107 |
108 | it('wires in a gain node between oscillator and output', function() {
109 | synth.play(note);
110 | expect(gainNode.connect).toHaveBeenCalledWith(gainNode);
111 | expect(oscillator.disconnect).toHaveBeenCalledWith(gainNode);
112 | expect(oscillator.connect).toHaveBeenCalledWith(gainNode);
113 | });
114 |
115 | it('A (Attack) property sets the duration of the A linear gain phase', function() {
116 | synth.ADSR.A = 500;
117 | synth.play(note);
118 | expect(gainNode.gain.value).toBe(0);
119 | jasmine.clock().tick(250);
120 | expect(gainNode.gain.value).toBe(0.5);
121 | jasmine.clock().tick(250)
122 | expect(gainNode.gain.value).toBe(1);
123 | });
124 |
125 | it('D (Decay) property can be set', function() {
126 | synth.ADSR.S = 0.2;
127 | synth.ADSR.A = 100;
128 | synth.ADSR.D = 500;
129 | synth.play(note);
130 | jasmine.clock().tick(100);
131 | expect(gainNode.gain.value).toBeCloseTo(1);
132 | jasmine.clock().tick(250);
133 | expect(gainNode.gain.value).toBeCloseTo(0.6);
134 | jasmine.clock().tick(250);
135 | expect(gainNode.gain.value).toBeCloseTo(0.2);
136 | });
137 |
138 | it('S (Sustain) property can be set', function() {
139 | synth.ADSR.S = 0.4;
140 | synth.ADSR.A = 200;
141 | synth.ADSR.D = 300;
142 | synth.play(note);
143 | jasmine.clock().tick(500);
144 | expect(gainNode.gain.value).toBeCloseTo(0.4);
145 | jasmine.clock().tick(1000);
146 | expect(gainNode.gain.value).toBeCloseTo(0.4);
147 | });
148 |
149 | it('R (Release) property can be set', function() {
150 | synth.ADSR.A = 100;
151 | synth.ADSR.D = 100;
152 | synth.ADSR.S = 0.1;
153 | synth.ADSR.R = 400;
154 | synth.play(note);
155 | jasmine.clock().tick(1000);
156 | synth.stop(note);
157 | expect(gainNode.gain.value).toBeCloseTo(0.1);
158 | jasmine.clock().tick(200);
159 | expect(gainNode.gain.value).toBeCloseTo(0.05);
160 | jasmine.clock().tick(200);
161 | expect(gainNode.gain.value).toBeCloseTo(0);
162 | });
163 |
164 | it('Release before A or D is finished cases decrease from the current level', function() {
165 | synth.ADSR.A = 100;
166 | synth.ADSR.D = 100;
167 | synth.ADSR.S = 0.6;
168 | synth.ADSR.R = 600;
169 | synth.play(note);
170 | jasmine.clock().tick(80);
171 | synth.stop(note);
172 | expect(gainNode.gain.value).toBeCloseTo(0.8);
173 | jasmine.clock().tick(300);
174 | expect(gainNode.gain.value).toBeCloseTo(0.4);
175 | jasmine.clock().tick(300);
176 | expect(gainNode.gain.value).toBeCloseTo(0);
177 | });
178 |
179 | it('Starting a new note during the R phase cancels R', function() {
180 | synth.ADSR.A = 100;
181 | synth.ADSR.D = 100;
182 | synth.ADSR.S = 0.6;
183 | synth.ADSR.R = 600;
184 | synth.play(note);
185 | jasmine.clock().tick(300);
186 | synth.stop(note);
187 | jasmine.clock().tick(300);
188 | synth.play(note);
189 | jasmine.clock().tick(50);
190 | expect(gainNode.gain.value).toBeCloseTo(0.65);
191 | jasmine.clock().tick(50);
192 | expect(gainNode.gain.value).toBeCloseTo(1);
193 | });
194 |
195 | it('Plays second note as expected when first if completely finished', function() {
196 | synth.ADSR.A = 100;
197 | synth.ADSR.D = 100;
198 | synth.ADSR.S = 0.6;
199 | synth.ADSR.R = 600;
200 | synth.play(note);
201 | jasmine.clock().tick(500);
202 | synth.stop(note);
203 | jasmine.clock().tick(800);
204 | synth.play(note);
205 | jasmine.clock().tick(50);
206 | expect(gainNode.gain.value).toBeCloseTo(0.5);
207 | jasmine.clock().tick(50);
208 | expect(gainNode.gain.value).toBeCloseTo(1);
209 | jasmine.clock().tick(50);
210 | expect(gainNode.gain.value).toBeCloseTo(0.8);
211 | jasmine.clock().tick(50);
212 | expect(gainNode.gain.value).toBeCloseTo(0.6);
213 | jasmine.clock().tick(50);
214 | expect(gainNode.gain.value).toBeCloseTo(0.6);
215 | synth.stop(note);
216 | jasmine.clock().tick(300);
217 | expect(gainNode.gain.value).toBeCloseTo(0.3);
218 | jasmine.clock().tick(300);
219 | expect(gainNode.gain.value).toBeCloseTo(0);
220 | });
221 | });
222 | });
223 |
--------------------------------------------------------------------------------
/spec/SynthSpec.js:
--------------------------------------------------------------------------------
1 | var proxyquire = require('proxyquire').noCallThru();
2 | var Synth = proxyquire('../js/Synth', {
3 | './synthMixins/ADSR': function() {},
4 | './synthMixins/PitchShifter': function() {},
5 | './synthMixins/WaveForm': function() {}
6 | });
7 |
8 | describe('Synth', function() {
9 | var synth, gainNode, oscillator, audioContext, aNote = {pitch: 81, frequency: 440}, aNote2 = {pitch: 93, frequency: 880};
10 |
11 | beforeEach(function() {
12 | oscillator = jasmine.createSpyObj('oscillator', ['setPeriodicWave', 'connect', 'disconnect', 'start', 'stop']);
13 | oscillator.frequency = {};
14 | gainNode = jasmine.createSpyObj('gainNode', ['connect']);
15 | gainNode.gain = {};
16 | stereoPanner = jasmine.createSpyObj('stereoPanner', ['connect']);
17 | stereoPanner.pan = {};
18 |
19 | audioContext = {
20 | createOscillator: function() {},
21 | createPeriodicWave: function() {},
22 | createStereoPanner: function() {},
23 | createGain: function() {},
24 | destination: {}
25 | };
26 |
27 | spyOn(audioContext, 'createOscillator').and.returnValue(oscillator);
28 | spyOn(audioContext, 'createGain').and.returnValue(gainNode);
29 | spyOn(audioContext, 'createStereoPanner').and.returnValue(stereoPanner);
30 |
31 | synth = new Synth(audioContext);
32 | });
33 |
34 | afterEach(function() {
35 | delete global.AudioContext;
36 | });
37 |
38 | it('constructs', function() {
39 | expect(synth).toEqual(jasmine.any(Object));
40 | });
41 |
42 | it('starts playing', function() {
43 | synth.play(aNote);
44 | expect(oscillator.start).toHaveBeenCalledWith(0);
45 | expect(oscillator.frequency.value).toEqual(aNote.frequency)
46 | });
47 |
48 | it('stops playing', function() {
49 | synth.play(aNote);
50 | synth.stop(aNote);
51 | expect(oscillator.stop).toHaveBeenCalledWith(0);
52 | });
53 |
54 | it('plays note several times', function() {
55 | synth.play(aNote);
56 | synth.stop(aNote);
57 | synth.play(aNote);
58 | expect(oscillator.start.calls.count()).toBe(2);
59 | });
60 |
61 | it('connects all oscillators when connect is called', function() {
62 | synth.play(aNote);
63 | synth.play(aNote2);
64 | expect(oscillator.connect).toHaveBeenCalledWith(gainNode);
65 | expect(oscillator.connect.calls.count()).toBe(2);
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/spec/WaveFormSpec.js:
--------------------------------------------------------------------------------
1 | var proxyquire = require('proxyquire').noCallThru();
2 |
3 | var WaveForm = proxyquire('../js/synthMixins/WaveForm', {
4 | '../waveforms/sine': {periodicWave: true, sine: true},
5 | '../waveforms/sawtooth': {periodicWave: true, sawtooth: true},
6 | '../waveforms/square': {periodicWave: true, square: true}
7 | });
8 | var PitchShifter = require('../js/synthMixins/PitchShifter');
9 |
10 | var oscillator, audioContext, note;
11 |
12 | describe("Mixins", function() {
13 | beforeEach(function() {
14 | oscillator = jasmine.createSpyObj('oscillator', ['setPeriodicWave', 'connect', 'start', 'stop']);
15 | audioContext = {
16 | createOscillator: function() {},
17 | };
18 | spyOn(audioContext, 'createOscillator').and.returnValue(oscillator);
19 |
20 | synth = {
21 | play: function() { return oscillator; },
22 | stop: function() {}
23 | };
24 |
25 | note = {pitch:44, frequency:100};
26 | });
27 |
28 | describe('WaveForm', function() {
29 | beforeEach(function() {
30 | WaveForm.call(synth);
31 | });
32 |
33 | it('sets wave form', function() {
34 | synth.waveForm = 'square';
35 | synth.play(note);
36 | expect(oscillator.setPeriodicWave).toHaveBeenCalledWith(
37 | jasmine.objectContaining({periodicWave: true, square: true})
38 | );
39 | });
40 |
41 | it('uses sine if no wave form is set', function() {
42 | synth.play(note);
43 | expect(oscillator.setPeriodicWave).toHaveBeenCalledWith(
44 | jasmine.objectContaining({periodicWave: true, sine: true})
45 | );
46 | });
47 | });
48 |
49 | describe('', function() {
50 | beforeEach(function() {
51 | PitchShifter.call(synth);
52 | });
53 | });
54 | });
--------------------------------------------------------------------------------
/spec/support/jasmine.json:
--------------------------------------------------------------------------------
1 | {
2 | "spec_dir": "spec",
3 | "spec_files": [
4 | "**/*[sS]pec.js"
5 | ],
6 | "helpers": [
7 | "helpers/**/*.js"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/tpl/controls.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Tremolo
5 |
8 |
11 |
15 |
19 |
20 |
21 |
22 |
Vibrato
23 |
26 |
29 |
33 |
37 |
38 |
39 |
40 |
Delay
41 |
44 |
47 |
51 |
55 |
59 |
60 |
61 |
83 |
84 |
85 |
89 |
93 |
94 |
95 |
96 |
100 |
104 |
108 |
109 |
--------------------------------------------------------------------------------