├── README.md ├── canvas.js ├── envelope.js ├── forkme.png ├── index.html ├── main.js ├── melody.js ├── mixer.js ├── music.js ├── piece.js ├── publish.sh ├── sampling.js ├── stylesheet.css ├── synth.js ├── utility.js └── vanalog.js /README.md: -------------------------------------------------------------------------------- 1 | Melodique 2 | ========= 3 | 4 | Melodique is one of my first ventures into musical programming. It uses 5 | simple music theory concepts to generate short melodic phrases. The melodic 6 | phrases are rendered visually using the HTML5 canvas, and audio output is 7 | produced by a built-in virtual analog sound synthesis engine. This project is 8 | distributed under a modified BSD license. 9 | 10 | You can try Melodique at the following URL: 11 | [http://maximecb.github.io/Melodique/](http://maximecb.github.io/Melodique/) 12 | 13 | 14 | -------------------------------------------------------------------------------- /canvas.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * 3 | * This file is part of the Melodique project. The project is 4 | * distributed at: 5 | * https://github.com/maximecb/Melodique 6 | * 7 | * Copyright (c) 2013, Maxime Chevalier-Boisvert. All rights reserved. 8 | * 9 | * This software is licensed under the following license (Modified BSD 10 | * License): 11 | * 12 | * Redistribution and use in source and binary forms, with or without 13 | * modification, are permitted provided that the following conditions are 14 | * met: 15 | * 1. Redistributions of source code must retain the above copyright 16 | * notice, this list of conditions and the following disclaimer. 17 | * 2. Redistributions in binary form must reproduce the above copyright 18 | * notice, this list of conditions and the following disclaimer in the 19 | * documentation and/or other materials provided with the distribution. 20 | * 3. The name of the author may not be used to endorse or promote 21 | * products derived from this software without specific prior written 22 | * permission. 23 | * 24 | * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED 25 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 26 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 27 | * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 28 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 29 | * NOT LIMITED TO PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 30 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 31 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 33 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | * 35 | *****************************************************************************/ 36 | 37 | /** 38 | Draw a graph in a canvas 39 | */ 40 | function drawGraph(ctx, startX, startY, width, height, data, yMin, yMax) 41 | { 42 | assert ( 43 | data.length >= 2, 44 | 'too few samples' 45 | ); 46 | 47 | // Compute the number of samples and lines 48 | var numSamples = Math.min(data.length, width); 49 | var numLines = numSamples - 1; 50 | 51 | // Resample the input data 52 | var samples = resample( 53 | data, 54 | numSamples, 55 | startY + height - 1, 56 | startY, 57 | yMin, 58 | yMax 59 | ); 60 | 61 | var xSpread = width / numLines; 62 | 63 | ctx.beginPath(); 64 | 65 | // For each line to draw 66 | for (var i = 0; i < numLines; ++i) 67 | { 68 | var v0 = samples[i]; 69 | var v1 = samples[i+1]; 70 | 71 | var x0 = startX + (i * xSpread); 72 | var x1 = x0 + xSpread; 73 | 74 | ctx.moveTo(x0, v0); 75 | ctx.lineTo(x1, v1); 76 | } 77 | 78 | ctx.stroke(); 79 | } 80 | 81 | -------------------------------------------------------------------------------- /envelope.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * 3 | * This file is part of the Melodique project. The project is 4 | * distributed at: 5 | * https://github.com/maximecb/Melodique 6 | * 7 | * Copyright (c) 2013, Maxime Chevalier-Boisvert. All rights reserved. 8 | * 9 | * This software is licensed under the following license (Modified BSD 10 | * License): 11 | * 12 | * Redistribution and use in source and binary forms, with or without 13 | * modification, are permitted provided that the following conditions are 14 | * met: 15 | * 1. Redistributions of source code must retain the above copyright 16 | * notice, this list of conditions and the following disclaimer. 17 | * 2. Redistributions in binary form must reproduce the above copyright 18 | * notice, this list of conditions and the following disclaimer in the 19 | * documentation and/or other materials provided with the distribution. 20 | * 3. The name of the author may not be used to endorse or promote 21 | * products derived from this software without specific prior written 22 | * permission. 23 | * 24 | * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED 25 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 26 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 27 | * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 28 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 29 | * NOT LIMITED TO PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 30 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 31 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 33 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | * 35 | *****************************************************************************/ 36 | 37 | /** 38 | @class Attack-Decay-Sustain-Release envelope implementation 39 | */ 40 | function ADSREnv(a, d, s, r) 41 | { 42 | /** 43 | Attack time 44 | */ 45 | this.a = a; 46 | 47 | /** 48 | Decay time 49 | */ 50 | this.d = d; 51 | 52 | /** 53 | Sustain amplitude [0,1] 54 | */ 55 | this.s = s; 56 | 57 | /** 58 | Release time 59 | */ 60 | this.r = r; 61 | 62 | /** 63 | Attack curve exponent 64 | */ 65 | this.aExp = 2; 66 | 67 | /** 68 | Decay curve exponent 69 | */ 70 | this.dExp = 2; 71 | 72 | /** 73 | Release curve exponent 74 | */ 75 | this.rExp = 2; 76 | } 77 | 78 | /** 79 | Get the envelope value at a given time 80 | */ 81 | ADSREnv.prototype.getValue = function (curTime, onTime, offTime, onAmp, offAmp) 82 | { 83 | // Interpolation function: 84 | // x ranges from 0 to 1 85 | function interp(x, yL, yR, exp) 86 | { 87 | // If the curve is increasing 88 | if (yR > yL) 89 | { 90 | return yL + Math.pow(x, exp) * (yR - yL); 91 | } 92 | else 93 | { 94 | return yR + Math.pow(1 - x, exp) * (yL - yR); 95 | } 96 | } 97 | 98 | if (offTime === 0) 99 | { 100 | var noteTime = curTime - onTime; 101 | 102 | if (noteTime < this.a) 103 | { 104 | return interp(noteTime / this.a, onAmp, 1, this.aExp); 105 | } 106 | else if (noteTime < this.a + this.d) 107 | { 108 | return interp((noteTime - this.a) / this.d , 1, this.s, this.dExp); 109 | } 110 | else 111 | { 112 | return this.s; 113 | } 114 | } 115 | else 116 | { 117 | var relTime = curTime - offTime; 118 | 119 | if (relTime < this.r) 120 | { 121 | return interp(relTime / this.r, offAmp, 0, this.rExp); 122 | } 123 | else 124 | { 125 | return 0; 126 | } 127 | } 128 | } 129 | 130 | -------------------------------------------------------------------------------- /forkme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maximecb/Melodique/1f832f144ed145c7d91fae19944efb7d32463c1e/forkme.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Melodique 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Fork me on GitHub 23 | 24 | 25 |
26 | Melodique: Algorithmic Melodic Phrase Generator 27 |
28 | 29 |
30 |
31 | 32 |
33 | Scale 34 | Root note: 35 | 37 | Scale type: 38 | 40 |
41 |
42 | 43 |
44 | Duration and timing 45 | Duration (bars): 46 | 47 | Tempo: 48 | 49 | Time signature: 50 | 51 | 52 |
53 | 54 |
55 | Chord types allowed 56 |
57 | 58 |
59 | Chord options 60 | Root inv. 61 | End on I 62 |
63 | 64 |
65 | Melodic patterns allowed 66 |
67 | 68 |
69 | Actions 70 | 71 |      72 | 73 |      74 | 75 |      76 | 77 |
78 | 79 |
80 |
81 | 82 |
83 | 84 |
85 | 86 |
87 |
88 | 89 | Your browser does not support the canvas element. 90 | 91 |
92 |
93 | 94 |
95 | Melodique is a tool to generate melodic phrases procedurally based on random 96 | chord progressions. It is intended to help those who wish to learn about chord 97 | progressions and the basics of compositions as well as those looking for new 98 | melody ideas. The controls at the top of the page can be used to adjust various 99 | parameters affecting the way in which the melodic phrases are generated. 100 |
101 | 102 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * 3 | * This file is part of the Melodique project. The project is 4 | * distributed at: 5 | * https://github.com/maximecb/Melodique 6 | * 7 | * Copyright (c) 2013, Maxime Chevalier-Boisvert. All rights reserved. 8 | * 9 | * This software is licensed under the following license (Modified BSD 10 | * License): 11 | * 12 | * Redistribution and use in source and binary forms, with or without 13 | * modification, are permitted provided that the following conditions are 14 | * met: 15 | * 1. Redistributions of source code must retain the above copyright 16 | * notice, this list of conditions and the following disclaimer. 17 | * 2. Redistributions in binary form must reproduce the above copyright 18 | * notice, this list of conditions and the following disclaimer in the 19 | * documentation and/or other materials provided with the distribution. 20 | * 3. The name of the author may not be used to endorse or promote 21 | * products derived from this software without specific prior written 22 | * permission. 23 | * 24 | * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED 25 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 26 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 27 | * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 28 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 29 | * NOT LIMITED TO PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 30 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 31 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 33 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | * 35 | *****************************************************************************/ 36 | 37 | //============================================================================ 38 | // Page interface code 39 | //============================================================================ 40 | 41 | /** 42 | Called after page load to initialize needed resources 43 | */ 44 | function init() 45 | { 46 | // Get a reference to the canvas 47 | canvas = findElementById("canvas"); 48 | 49 | // Get a 2D context for the drawing canvas 50 | canvasCtx = canvas.getContext("2d"); 51 | 52 | //console.log('Got page elements'); 53 | 54 | // Set the canvas size 55 | canvas.width = 600; 56 | canvas.height = 350; 57 | 58 | // Create an audio context 59 | if (this.hasOwnProperty('AudioContext') === true) 60 | { 61 | //console.log('Audio context found'); 62 | audioCtx = new AudioContext(); 63 | } 64 | else if (this.hasOwnProperty('webkitAudioContext') === true) 65 | { 66 | //console.log('WebKit audio context found'); 67 | audioCtx = new webkitAudioContext(); 68 | } 69 | else 70 | { 71 | audioCtx = undefined; 72 | } 73 | 74 | // If an audio context was created 75 | if (audioCtx !== undefined) 76 | { 77 | // Get the sample rate for the audio context 78 | var sampleRate = audioCtx.sampleRate; 79 | 80 | console.log('Sample rate: ' + audioCtx.sampleRate); 81 | 82 | // Size of the audio generation buffer 83 | var bufferSize = 2048; 84 | } 85 | else 86 | { 87 | alert( 88 | 'No Web Audio API support. Sound will be disabled. ' + 89 | 'Try this page in the latest version of Chrome' 90 | ); 91 | 92 | var sampleRate = 44100; 93 | } 94 | 95 | // Create a synthesis network 96 | var synthNet = new SynthNet(sampleRate); 97 | 98 | // Create a piece 99 | var piece = new Piece(synthNet); 100 | 101 | // Initialize the synth network 102 | initSynth(synthNet, piece); 103 | 104 | // Initialize the form 105 | initForm(); 106 | 107 | // Create an audio generation event handler 108 | var genAudio = piece.makeHandler(); 109 | 110 | // JS audio node to produce audio output 111 | var jsAudioNode = undefined; 112 | 113 | // Drawing interval 114 | var drawInterv = undefined; 115 | 116 | drawTrack = function () 117 | { 118 | drawMelody(canvas, canvasCtx, piece, piece.tracks[0]); 119 | } 120 | 121 | // Draw the empty track 122 | drawTrack(); 123 | 124 | genMelody = function () 125 | { 126 | makeMelody(piece); 127 | } 128 | 129 | playAudio = function () 130 | { 131 | // If audio is disabled, stop 132 | if (audioCtx === undefined) 133 | return; 134 | 135 | // If the audio isn't stopped, stop it 136 | if (jsAudioNode !== undefined) 137 | stopAudio() 138 | 139 | // Set the playback time on the piece to 0 (start) 140 | piece.setTime(0); 141 | 142 | // Create a JS audio node and connect it to the destination 143 | jsAudioNode = audioCtx.createScriptProcessor(bufferSize, 2, 2); 144 | jsAudioNode.onaudioprocess = genAudio; 145 | jsAudioNode.connect(audioCtx.destination); 146 | 147 | drawInterv = setInterval(drawTrack, 100); 148 | } 149 | 150 | stopAudio = function () 151 | { 152 | // If audio is disabled, stop 153 | if (audioCtx === undefined) 154 | return; 155 | 156 | if (jsAudioNode === undefined) 157 | return; 158 | 159 | // Notify the piece that we are stopping playback 160 | piece.stop(); 161 | 162 | // Disconnect the audio node 163 | jsAudioNode.disconnect(); 164 | jsAudioNode = undefined; 165 | 166 | // Clear the drawing interval 167 | clearInterval(drawInterv); 168 | } 169 | 170 | exportMIDI = function () 171 | { 172 | var midiData = piece.getMIDIData(piece.tracks[0]); 173 | 174 | console.log('MIDI data length: ' + midiData.length); 175 | 176 | var base64Data = encodeBase64(midiData); 177 | 178 | console.log(base64Data); 179 | 180 | window.location = "data:audio/sp-midi;base64," + base64Data; 181 | } 182 | } 183 | 184 | // Attach the init function to the load event 185 | if (window.addEventListener) 186 | window.addEventListener('load', init, false); 187 | else if (document.addEventListener) 188 | document.addEventListener('load', init, false); 189 | else if (window.attachEvent) 190 | window.attachEvent('onload', init); 191 | 192 | // Default console logging function implementation 193 | if (!window.console) console = {}; 194 | console.log = console.log || function(){}; 195 | console.warn = console.warn || function(){}; 196 | console.error = console.error || function(){}; 197 | console.info = console.info || function(){}; 198 | 199 | // Check for typed array support 200 | if (!this.Float64Array) 201 | { 202 | console.log('No Float64Array support'); 203 | Float64Array = Array; 204 | } 205 | 206 | -------------------------------------------------------------------------------- /melody.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * 3 | * This file is part of the Melodique project. The project is 4 | * distributed at: 5 | * https://github.com/maximecb/Melodique 6 | * 7 | * Copyright (c) 2013, Maxime Chevalier-Boisvert. All rights reserved. 8 | * 9 | * This software is licensed under the following license (Modified BSD 10 | * License): 11 | * 12 | * Redistribution and use in source and binary forms, with or without 13 | * modification, are permitted provided that the following conditions are 14 | * met: 15 | * 1. Redistributions of source code must retain the above copyright 16 | * notice, this list of conditions and the following disclaimer. 17 | * 2. Redistributions in binary form must reproduce the above copyright 18 | * notice, this list of conditions and the following disclaimer in the 19 | * documentation and/or other materials provided with the distribution. 20 | * 3. The name of the author may not be used to endorse or promote 21 | * products derived from this software without specific prior written 22 | * permission. 23 | * 24 | * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED 25 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 26 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 27 | * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 28 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 29 | * NOT LIMITED TO PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 30 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 31 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 33 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | * 35 | *****************************************************************************/ 36 | 37 | function initSynth(synthNet, piece) 38 | { 39 | var outNode = synthNet.addNode(new OutNode(2)); 40 | 41 | var mixer = synthNet.addNode(new Mixer()); 42 | mixer.inVolume[0] = 0.3; 43 | mixer.outVolume = 1.0; 44 | 45 | // Synth piano 46 | var vanalog = synthNet.addNode(new VAnalog(2)); 47 | vanalog.name = 'synth piano'; 48 | vanalog.oscs[0].type = 'sawtooth'; 49 | vanalog.oscs[0].detune = 0; 50 | vanalog.oscs[0].volume = 0.75; 51 | 52 | vanalog.oscs[0].env.a = 0; 53 | vanalog.oscs[0].env.d = 0.67; 54 | vanalog.oscs[0].env.s = 0.25; 55 | vanalog.oscs[0].env.r = 0.50; 56 | 57 | vanalog.oscs[1].type = 'pulse'; 58 | vanalog.oscs[1].duty = 0.15; 59 | vanalog.oscs[1].detune = 1400; 60 | vanalog.oscs[1].volume = 1; 61 | 62 | vanalog.oscs[1].sync = true; 63 | vanalog.oscs[1].syncDetune = 0; 64 | 65 | vanalog.oscs[1].env = vanalog.oscs[0].env; 66 | 67 | vanalog.cutoff = 0.1; 68 | vanalog.resonance = 0; 69 | 70 | vanalog.filterEnv.a = 0; 71 | vanalog.filterEnv.d = 5.22; 72 | vanalog.filterEnv.s = 0; 73 | vanalog.filterEnv.r = 5; 74 | vanalog.filterEnvAmt = 0.75; 75 | 76 | // Connect all synth nodes and topologically order them 77 | vanalog.output.connect(mixer.input0); 78 | mixer.output.connect(outNode.signal); 79 | synthNet.orderNodes(); 80 | 81 | // Create a track for the instrument 82 | var track = new Track(vanalog); 83 | piece.addTrack(track); 84 | } 85 | 86 | function initForm() 87 | { 88 | var scaleRoot = findElementById('scaleRoot'); 89 | for (var name in NOTE_NAME_PC) 90 | { 91 | var opt = document.createElement("option"); 92 | opt.text = name; 93 | opt.value = NOTE_NAME_PC[name]; 94 | scaleRoot.appendChild(opt); 95 | } 96 | 97 | var scaleType = findElementById('scaleType'); 98 | for (var scale in scaleIntervs) 99 | { 100 | var opt = document.createElement("option"); 101 | opt.text = scale; 102 | opt.value = scale; 103 | scaleType.appendChild(opt); 104 | } 105 | 106 | var chordTypes = findElementById('chordTypes'); 107 | for (var type in chordIntervs) 108 | { 109 | var text = document.createTextNode(type); 110 | chordTypes.appendChild(text); 111 | 112 | var checked = (type === 'maj' || type === 'min'); 113 | 114 | var box = document.createElement('input'); 115 | box.type = 'checkbox'; 116 | box.name = type; 117 | box.checked = checked; 118 | chordTypes.appendChild(box); 119 | } 120 | 121 | var melPatterns = findElementById('melPatterns'); 122 | 123 | for (var key in patterns) 124 | { 125 | var pattern = patterns[key]; 126 | 127 | var text = document.createTextNode(pattern.name); 128 | melPatterns.appendChild(text); 129 | 130 | var box = document.createElement('input'); 131 | box.type = 'checkbox'; 132 | box.name = key; 133 | box.checked = pattern.checked; 134 | melPatterns.appendChild(box); 135 | } 136 | } 137 | 138 | function makeMelody(piece) 139 | { 140 | // 141 | // Extract form information 142 | // 143 | 144 | // Extract the scale root note 145 | var scaleRoot = document.getElementById('scaleRoot'); 146 | for (var i = 0; i < scaleRoot.length; ++i) 147 | { 148 | if (scaleRoot[i].selected === true) 149 | { 150 | var pc = Number(scaleRoot[i].value); 151 | var rootNo = (new Note('C4')).noteNo + pc; 152 | var scaleRoot = new Note(rootNo); 153 | break; 154 | } 155 | } 156 | 157 | if ((scaleRoot instanceof Note) === false) 158 | error('Invalid scale root'); 159 | 160 | // Extract the scale type 161 | var scaleType = findElementById('scaleType'); 162 | for (var i = 0; i < scaleType.length; ++i) 163 | { 164 | if (scaleType[i].selected === true) 165 | { 166 | var scaleType = scaleType[i].value; 167 | break; 168 | } 169 | } 170 | 171 | if (scaleIntervs[scaleType] === undefined) 172 | error('Invalid scale type'); 173 | 174 | // Extract the duration in bars 175 | var duration = findElementById('duration'); 176 | var numBars = Number(duration.value); 177 | if (isPosInt(numBars) === false) 178 | error('Invalid duration in bars: ' + numBars); 179 | 180 | // Extract the tempo 181 | var tempo = findElementById('tempo'); 182 | var beatsPerMin = Number(tempo.value); 183 | if (isPosInt(beatsPerMin) === false) 184 | error('Invalid tempo: ' + beatsPerMin); 185 | 186 | // Extract the time signature 187 | var timeSigNum = findElementById('timeSigNum'); 188 | var timeSigDenom = findElementById('timeSigDenom'); 189 | var beatsPerBar = Number(timeSigNum.value); 190 | var noteVal = Number(timeSigDenom.value); 191 | if (isPosInt(beatsPerBar) === false || isNaN(noteVal) === true) 192 | error('Invalid time signature'); 193 | 194 | // Extract a list of chord types allowed 195 | var chordTypesElem = findElementById('chordTypes'); 196 | var chordTypes = []; 197 | for (var i = 0; i < chordTypesElem.children.length; ++i) 198 | { 199 | var chordElem = chordTypesElem.children[i]; 200 | if (chordElem.checked === true) 201 | chordTypes.push(chordElem.name); 202 | } 203 | if (chordTypes.length === 0) 204 | error('Must allow at least one chord type'); 205 | 206 | // Extract the chord options 207 | var rootInv = Boolean(findElementById('rootInv').value); 208 | var endOnI = Boolean(findElementById('endOnI').value); 209 | 210 | // Extract a list of melodic patterns allowed 211 | var melPatternsElem = findElementById('melPatterns'); 212 | var melPatterns = []; 213 | for (var i = 0; i < melPatternsElem.children.length; ++i) 214 | { 215 | var patElem = melPatternsElem.children[i]; 216 | if (patElem.checked === true) 217 | melPatterns.push(patterns[patElem.name]); 218 | } 219 | if (melPatterns.length === 0) 220 | error('Must allow at least one melodic pattern type'); 221 | 222 | // 223 | // Initialize the piece/track 224 | // 225 | 226 | // Get the melody track and clear it 227 | var track = piece.tracks[0]; 228 | track.clear(); 229 | 230 | // Set the tempo 231 | piece.beatsPerMin = beatsPerMin; 232 | 233 | // Set the time signature 234 | piece.beatsPerBar = beatsPerBar; 235 | piece.noteVal = noteVal; 236 | 237 | // 238 | // Melodic phrase generation 239 | // 240 | 241 | // TODO 242 | // - first inversion -> move the root an octave higher 243 | //- open chords -> move the root an octave lower 244 | 245 | // TODO 246 | // - Play with pauses, tonic (center tone), longer pauses on down phases? 247 | // - Chords sound better if they only use notes from the scale? 248 | 249 | // Usually people write songs that heavily use the I, IV, and V chords, 250 | // as well as the relative minor (vi) 251 | // TODO: give I, IV, V, vi chords higher probability? 252 | 253 | // Compute the total number of beats 254 | var numBeats = numBars * beatsPerBar; 255 | 256 | // Generate the scale notes 257 | var scaleNotes = genScale(scaleRoot, scaleType); 258 | 259 | console.log('num beats: ' + numBeats); 260 | 261 | // Get the major chord type to use 262 | var majType; 263 | if (chordTypes.indexOf('maj') !== -1) 264 | majType = 'maj'; 265 | else if (chordTypes.indexOf('maj7') !== -1) 266 | majType = 'maj7'; 267 | else 268 | majType = chordTypes[0]; 269 | 270 | // Get the minor chord type to use 271 | var minType; 272 | if (chordTypes.indexOf('min') !== -1) 273 | minType = 'min'; 274 | else if (chordTypes.indexOf('min7') !== -1) 275 | minType = 'min7'; 276 | else 277 | minType = chordTypes[0]; 278 | 279 | // Chord description string 280 | var chordNames = ''; 281 | 282 | // Until the melodic phrase is complete 283 | for (var beatNo = 0; lastBeat !== true;) 284 | { 285 | var chordDegree; 286 | var chordType; 287 | 288 | //console.log('end time : ' + track.endTime()); 289 | //console.log('beat time: ' + endBeatTime); 290 | 291 | // Test if this is the last beat 292 | var lastBeat = beatNo > numBeats - 2; 293 | 294 | // Choose the scale degree for this chord 295 | if (endOnI === true && lastBeat === true) 296 | { 297 | chordDegree = 0; 298 | chordType = majType; 299 | } 300 | else 301 | { 302 | var r0 = randomInt(0, (scaleNotes.length > 5)? 6:5); 303 | 304 | switch (r0) 305 | { 306 | // Random chord 307 | case 0: 308 | chordDegree = randomInt(0, scaleNotes.length - 1); 309 | chordType = randomChoice.apply(null, chordTypes); 310 | break; 311 | 312 | // I 313 | case 1: 314 | chordDegree = 0; 315 | chordType = majType; 316 | break; 317 | 318 | // IV 319 | case 2: 320 | case 3: 321 | chordDegree = 3; 322 | chordType = majType; 323 | break; 324 | 325 | // V 326 | case 4: 327 | case 5: 328 | chordDegree = 4; 329 | chordType = majType; 330 | break; 331 | 332 | // vi 333 | case 6: 334 | chordDegree = 5; 335 | chordType = minType; 336 | break; 337 | } 338 | } 339 | 340 | // Get the chord root note 341 | var chordRoot = scaleNotes[chordDegree]; 342 | 343 | // Add the chord name to the name string 344 | chordNames += ((beatNo !== 0)? ' ':'') + chordRoot + chordType; 345 | 346 | // Generate the chord notes 347 | var chordNotes = genChord(chordRoot, chordType); 348 | 349 | // Randomly choose if the root should be inverted 350 | if (rootInv === true && randomBool() === true) 351 | chordNotes[0] = chordNotes[0].shift(1); 352 | 353 | // Sort the chord notes by ascending pitch 354 | chordNotes.sort(Note.sortFn); 355 | 356 | // Choose a melodic pattern 357 | if (endOnI === true && lastBeat === true) 358 | var pattern = patterns.allNotesOn; 359 | else 360 | var pattern = randomChoice.apply(null, melPatterns); 361 | 362 | // Make the notes for this pattern 363 | beatNo = pattern.makeNotes(piece, track, chordNotes, beatNo); 364 | } 365 | 366 | console.log('chord names: ' + chordNames); 367 | 368 | // Output the chord names on the page 369 | var chordText = findElementById('chordText'); 370 | chordText.value = chordNames; 371 | 372 | // Draw the track with the generated notes 373 | drawTrack(); 374 | } 375 | 376 | function drawMelody(canvas, canvasCtx, piece, track) 377 | { 378 | piece.drawTrack( 379 | track, 380 | canvasCtx, 381 | 0, 382 | 0, 383 | canvas.width, 384 | canvas.height, 385 | new Note('C3'), 386 | 4 387 | ); 388 | } 389 | 390 | /** 391 | @namespace Melodic patterns 392 | */ 393 | var patterns = {}; 394 | 395 | patterns.allNotesOn = {}; 396 | patterns.allNotesOn.name = 'All notes on'; 397 | patterns.allNotesOn.checked = true; 398 | patterns.allNotesOn.makeNotes = function (piece, track, notes, beatNo) 399 | { 400 | for (var i = 0; i < notes.length; ++i) 401 | piece.makeNote(track, beatNo, notes[i], 1); 402 | 403 | return beatNo + 1; 404 | } 405 | 406 | patterns.doubleNotes = {}; 407 | patterns.doubleNotes.name = 'Double notes'; 408 | patterns.doubleNotes.checked = true; 409 | patterns.doubleNotes.makeNotes = function (piece, track, notes, beatNo) 410 | { 411 | for (var i = 0; i < notes.length; ++i) 412 | piece.makeNote(track, beatNo, notes[i], 0.5); 413 | 414 | for (var i = 0; i < notes.length; ++i) 415 | piece.makeNote(track, beatNo + 0.5, notes[i], 0.5); 416 | 417 | return beatNo + 1; 418 | } 419 | 420 | patterns.shortNotes = {}; 421 | patterns.shortNotes.name = 'Short notes'; 422 | patterns.shortNotes.checked = true; 423 | patterns.shortNotes.makeNotes = function (piece, track, notes, beatNo) 424 | { 425 | for (var i = 0; i < notes.length; ++i) 426 | piece.makeNote(track, beatNo, notes[i], 0.5); 427 | 428 | return beatNo + 1; 429 | } 430 | 431 | patterns.ascArp = {}; 432 | patterns.ascArp.name = 'Asc. arp.'; 433 | patterns.ascArp.checked = true; 434 | patterns.ascArp.makeNotes = function (piece, track, notes, beatNo) 435 | { 436 | var noteLen = 1 / notes.length; 437 | 438 | for (var i = 0; i < notes.length; ++i) 439 | { 440 | var note = notes[i]; 441 | piece.makeNote(track, beatNo + i * noteLen, note, noteLen); 442 | } 443 | 444 | return beatNo + 1; 445 | } 446 | 447 | patterns.descArp = {}; 448 | patterns.descArp.name = 'Desc. arp.'; 449 | patterns.descArp.checked = true; 450 | patterns.descArp.makeNotes = function (piece, track, notes, beatNo) 451 | { 452 | var noteLen = 1 / notes.length; 453 | 454 | for (var i = notes.length - 1; i >= 0; --i) 455 | { 456 | var note = notes[i]; 457 | piece.makeNote(track, beatNo + i * noteLen, note, noteLen); 458 | } 459 | 460 | return beatNo + 1; 461 | } 462 | 463 | patterns.randArp = {}; 464 | patterns.randArp.name = 'Rand. arp.'; 465 | patterns.randArp.checked = true; 466 | patterns.randArp.makeNotes = function (piece, track, notes, beatNo) 467 | { 468 | var noteLen = 1 / notes.length; 469 | 470 | notes = notes.slice(0); 471 | 472 | for (var i = notes.length - 1; i >= 0; --i) 473 | { 474 | var n = randomInt(0, notes.length - 1); 475 | var note = notes[n]; 476 | notes.splice(n, 1); 477 | 478 | piece.makeNote(track, beatNo + i * noteLen, note, noteLen); 479 | } 480 | 481 | return beatNo + 1; 482 | } 483 | 484 | /* 485 | patterns.halfBeatGap = {}; 486 | patterns.halfBeatGap.name = '1/2-beat gap'; 487 | patterns.halfBeatGap.checked = false; 488 | patterns.halfBeatGap.makeNotes = function (piece, track, notes, beatNo) 489 | { 490 | beatNo += 0.5; 491 | return beatNo; 492 | } 493 | */ 494 | -------------------------------------------------------------------------------- /mixer.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * 3 | * This file is part of the Melodique project. The project is 4 | * distributed at: 5 | * https://github.com/maximecb/Melodique 6 | * 7 | * Copyright (c) 2013, Maxime Chevalier-Boisvert. All rights reserved. 8 | * 9 | * This software is licensed under the following license (Modified BSD 10 | * License): 11 | * 12 | * Redistribution and use in source and binary forms, with or without 13 | * modification, are permitted provided that the following conditions are 14 | * met: 15 | * 1. Redistributions of source code must retain the above copyright 16 | * notice, this list of conditions and the following disclaimer. 17 | * 2. Redistributions in binary form must reproduce the above copyright 18 | * notice, this list of conditions and the following disclaimer in the 19 | * documentation and/or other materials provided with the distribution. 20 | * 3. The name of the author may not be used to endorse or promote 21 | * products derived from this software without specific prior written 22 | * permission. 23 | * 24 | * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED 25 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 26 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 27 | * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 28 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 29 | * NOT LIMITED TO PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 30 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 31 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 33 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | * 35 | *****************************************************************************/ 36 | 37 | /** 38 | @class Simple multi-input mixer 39 | */ 40 | function Mixer(numInputs, numChans) 41 | { 42 | if (numInputs === undefined) 43 | numInputs = 8; 44 | 45 | if (numChans === undefined) 46 | numChans = 2; 47 | 48 | /** 49 | Number of input/output channels 50 | */ 51 | this.numChans = numChans; 52 | 53 | /** 54 | Input volume(s), one value per input 55 | */ 56 | this.inVolume = new Float64Array(numInputs); 57 | 58 | /** 59 | Input panning settings, one value per input in [-1, 1] 60 | */ 61 | this.inPanning = new Float64Array(numInputs); 62 | 63 | /** 64 | Output volume 65 | */ 66 | this.outVolume = 1; 67 | 68 | /** 69 | List of inputs 70 | */ 71 | this.inputs = new Array(numInputs); 72 | 73 | // For each input 74 | for (var i = 0; i < numInputs; ++i) 75 | { 76 | // Initialize the volume to 1 77 | this.inVolume[i] = 1; 78 | 79 | // Initialize the panning to 0 (centered) 80 | this.inPanning[i] = 0; 81 | 82 | // Audio input signal 83 | this.inputs[i] = new SynthInput(this, 'input' + i, numChans); 84 | } 85 | 86 | // Audio output 87 | new SynthOutput(this, 'output', numChans); 88 | 89 | // Default name for this node 90 | this.name = 'mixer'; 91 | } 92 | Mixer.prototype = new SynthNode(); 93 | 94 | /** 95 | Update the outputs based on the inputs 96 | */ 97 | Mixer.prototype.update = function (time, sampleRate) 98 | { 99 | // Count the number of inputs having produced data 100 | var actCount = 0; 101 | for (var inIdx = 0; inIdx < this.inputs.length; ++inIdx) 102 | if (this.inputs[inIdx].hasData() === true) 103 | ++actCount; 104 | 105 | // If there are no active inputs, do nothing 106 | if (actCount === 0) 107 | return; 108 | 109 | // Initialize the output to 0 110 | for (var chIdx = 0; chIdx < this.numChans; ++chIdx) 111 | { 112 | var outBuf = this.output.getBuffer(chIdx); 113 | for (var i = 0; i < outBuf.length; ++i) 114 | outBuf[i] = 0; 115 | } 116 | 117 | // For each input 118 | for (var inIdx = 0; inIdx < this.inputs.length; ++inIdx) 119 | { 120 | // Get the input 121 | var input = this.inputs[inIdx]; 122 | 123 | // If this input has no available data, skip it 124 | if (input.hasData() === false) 125 | continue; 126 | 127 | // For each channel 128 | for (var chIdx = 0; chIdx < this.numChans; ++chIdx) 129 | { 130 | // Get the input buffer 131 | var inBuf = input.getBuffer(chIdx); 132 | 133 | // Get the volume for this input 134 | var inVolume = this.inVolume[inIdx]; 135 | 136 | // Get the output buffer 137 | var outBuf = this.output.getBuffer(chIdx); 138 | 139 | // If we are operating in stereo 140 | if (this.numChans === 2) 141 | { 142 | var inPanning = this.inPanning[inIdx]; 143 | 144 | // Scale the channel volumes based on the panning level 145 | if (chIdx === 0) 146 | inVolume *= (1 - inPanning) / 2; 147 | else if (chIdx === 1) 148 | inVolume *= (1 + inPanning) / 2; 149 | } 150 | 151 | // Scale the input and add it to the output 152 | for (var i = 0; i < inBuf.length; ++i) 153 | outBuf[i] += inBuf[i] * inVolume; 154 | } 155 | } 156 | 157 | // Scale the output according to the output volume 158 | for (var chIdx = 0; chIdx < this.numChans; ++chIdx) 159 | { 160 | var outBuf = this.output.getBuffer(chIdx); 161 | for (var i = 0; i < outBuf.length; ++i) 162 | outBuf[i] *= this.outVolume; 163 | } 164 | } 165 | 166 | -------------------------------------------------------------------------------- /music.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * 3 | * This file is part of the Melodique project. The project is 4 | * distributed at: 5 | * https://github.com/maximecb/Melodique 6 | * 7 | * Copyright (c) 2013, Maxime Chevalier-Boisvert. All rights reserved. 8 | * 9 | * This software is licensed under the following license (Modified BSD 10 | * License): 11 | * 12 | * Redistribution and use in source and binary forms, with or without 13 | * modification, are permitted provided that the following conditions are 14 | * met: 15 | * 1. Redistributions of source code must retain the above copyright 16 | * notice, this list of conditions and the following disclaimer. 17 | * 2. Redistributions in binary form must reproduce the above copyright 18 | * notice, this list of conditions and the following disclaimer in the 19 | * documentation and/or other materials provided with the distribution. 20 | * 3. The name of the author may not be used to endorse or promote 21 | * products derived from this software without specific prior written 22 | * permission. 23 | * 24 | * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED 25 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 26 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 27 | * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 28 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 29 | * NOT LIMITED TO PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 30 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 31 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 33 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | * 35 | *****************************************************************************/ 36 | 37 | //============================================================================ 38 | // Note representation 39 | //============================================================================ 40 | 41 | /** 42 | Number of MIDI notes 43 | */ 44 | var NUM_NOTES = 128; 45 | 46 | /** 47 | Number of notes per octave 48 | */ 49 | var NOTES_PER_OCTAVE = 12; 50 | 51 | /** 52 | Number of cents per octave 53 | */ 54 | var CENTS_PER_OCTAVE = 1200; 55 | 56 | /** 57 | Frequency of the A4 note 58 | */ 59 | var A4_NOTE_FREQ = 440; 60 | 61 | /** 62 | Note number of the A4 note 63 | */ 64 | var A4_NOTE_NO = 69; 65 | 66 | /** 67 | Note number of the C4 note 68 | */ 69 | var C4_NOTE_NO = 71; 70 | 71 | /** 72 | Mapping from note names to pitch classes 73 | */ 74 | var NOTE_NAME_PC = { 75 | 'C' : 0, 76 | 'C#': 1, 77 | 'D' : 2, 78 | 'D#': 3, 79 | 'E' : 4, 80 | 'F' : 5, 81 | 'F#': 6, 82 | 'G' : 7, 83 | 'G#': 8, 84 | 'A' : 9, 85 | 'A#': 10, 86 | 'B' : 11 87 | }; 88 | 89 | /** 90 | Mapping from pitch classes to note names 91 | */ 92 | var NOTE_PC_NAME = { 93 | 0 : 'C', 94 | 1 : 'C#', 95 | 2 : 'D', 96 | 3 : 'D#', 97 | 4 : 'E', 98 | 5 : 'F', 99 | 6 : 'F#', 100 | 7 : 'G', 101 | 8 : 'G#', 102 | 9 : 'A', 103 | 10 : 'A#', 104 | 11 : 'B' 105 | }; 106 | 107 | /** 108 | @class Represents note values. 109 | 110 | Midi note numbers go from 0 to 127. 111 | 112 | A4 is tuned to 440Hz, and corresponds to midi note 69. 113 | 114 | F(n) = 440 * (2^(1/12))^(n - 69) 115 | = 440 * 2 ^ ((n-69)/12) 116 | */ 117 | function Note(val) 118 | { 119 | // If we got a note name, convert it to a note number 120 | if (typeof val === 'string') 121 | val = Note.nameToNo(val); 122 | 123 | assert ( 124 | typeof val === 'number', 125 | 'invalid note number' 126 | ); 127 | 128 | if (Note.notesByNo[val] !== undefined) 129 | return Note.notesByNo[val]; 130 | 131 | this.noteNo = val; 132 | 133 | Note.notesByNo[val] = this; 134 | } 135 | 136 | /** 137 | Array of note numbers to note objects 138 | */ 139 | Note.notesByNo = []; 140 | 141 | /** 142 | Get the note number for a note name 143 | */ 144 | Note.nameToNo = function (name) 145 | { 146 | // Use a regular expression to parse the name 147 | var matches = name.match(/([A-G]#?)([0-9])/i); 148 | 149 | assert ( 150 | matches !== null, 151 | 'invalid note name: "' + name + '"' 152 | ); 153 | 154 | var namePart = matches[1]; 155 | var numPart = matches[2]; 156 | 157 | var pc = NOTE_NAME_PC[namePart]; 158 | 159 | assert ( 160 | typeof pc === 'number', 161 | 'invalid note name: ' + namePart 162 | ); 163 | 164 | var octNo = parseInt(numPart); 165 | 166 | // Compute the note number 167 | var noteNo = (octNo + 1) * NOTES_PER_OCTAVE + pc; 168 | 169 | assert ( 170 | noteNo >= 0 || noteNo < NUM_NOTES, 171 | 'note parsing failed' 172 | ); 173 | 174 | return noteNo; 175 | } 176 | 177 | /** 178 | Sorting function for note objects 179 | */ 180 | Note.sortFn = function (n1, n2) 181 | { 182 | return n1.noteNo - n2.noteNo; 183 | } 184 | 185 | /** 186 | Get the pitch class 187 | */ 188 | Note.prototype.getPC = function () 189 | { 190 | return this.noteNo % NOTES_PER_OCTAVE; 191 | } 192 | 193 | /** 194 | Get the octave number 195 | */ 196 | Note.prototype.getOctNo = function () 197 | { 198 | return Math.floor(this.noteNo / NOTES_PER_OCTAVE) - 1; 199 | } 200 | 201 | /** 202 | Get the name for a note 203 | */ 204 | Note.prototype.getName = function () 205 | { 206 | // Compute the octave number of the note 207 | var octNo = this.getOctNo(); 208 | 209 | // Get the pitch class for this note 210 | var pc = this.getPC(); 211 | 212 | var name = NOTE_PC_NAME[pc]; 213 | 214 | // Add the octave number to the note name 215 | name += String(octNo); 216 | 217 | return name; 218 | } 219 | 220 | /** 221 | The string representation of a note is its name 222 | */ 223 | Note.prototype.toString = Note.prototype.getName; 224 | 225 | /** 226 | Get the frequency for a note 227 | @param offset detuning offset in cents 228 | */ 229 | Note.prototype.getFreq = function (offset) 230 | { 231 | if (offset === undefined) 232 | offset = 0; 233 | 234 | // F(n) = 440 * 2 ^ ((n-69)/12) 235 | var noteExp = (this.noteNo - A4_NOTE_NO) / NOTES_PER_OCTAVE; 236 | 237 | // b = a * 2 ^ (o / 1200) 238 | var offsetExp = offset / CENTS_PER_OCTAVE; 239 | 240 | // Compute the note frequency 241 | return A4_NOTE_FREQ * Math.pow( 242 | 2, 243 | noteExp + offsetExp 244 | ); 245 | } 246 | 247 | /** 248 | Shift a note to higher or lower octaves 249 | */ 250 | Note.prototype.shift = function (numOcts) 251 | { 252 | var shiftNo = this.noteNo + (numOcts * NOTES_PER_OCTAVE); 253 | 254 | assert ( 255 | shiftNo >= 0 && shiftNo < NUM_NOTES, 256 | 'invalid note number after shift' 257 | ); 258 | 259 | return new Note(shiftNo); 260 | } 261 | 262 | //============================================================================ 263 | // Chord generation 264 | //============================================================================ 265 | 266 | /** 267 | Semitone intervals for different scales 268 | */ 269 | var scaleIntervs = { 270 | 271 | // Major scale (2 2 1 2 2 2 1) 272 | 'major': [0, 2, 4, 5, 7, 9, 11, 12], 273 | 274 | // Natural Minor scale (2 1 2 2 1 2 2) 275 | 'natural minor': [0, 2, 3, 5, 7, 8, 10, 12], 276 | 277 | // Major pentatonic scale (2 2 3 2 3) 278 | 'major pentatonic': [0, 2, 4, 7, 9, 12], 279 | 280 | // Blues scale (3 2 1 1 3 2) 281 | 'blues scale': [0, 3, 5, 6, 7, 10, 12], 282 | 283 | // Chromatic scale (1 1 1 1 1 1 1 1 1 1 1) 284 | 'chromatic': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] 285 | }; 286 | 287 | /** 288 | Generate the notes of a scale based on a root note 289 | */ 290 | function genScale(rootNote, scale) 291 | { 292 | if ((rootNote instanceof Note) === false) 293 | rootNote = new Note(rootNote); 294 | 295 | var rootNo = rootNote.noteNo; 296 | 297 | // Get the intervals for this type of chord 298 | var intervs = scaleIntervs[scale]; 299 | 300 | assert ( 301 | intervs instanceof Array, 302 | 'invalid scale name: ' + scale 303 | ); 304 | 305 | // Compute the note numbers for the notes 306 | var noteNos = intervs.map(function (i) { return rootNo + i; }); 307 | 308 | // Get note objects for the chord nodes 309 | var notes = noteNos.map(function (no) { return new Note(no); }); 310 | 311 | return notes; 312 | } 313 | 314 | /** 315 | Semitone intervals for different kinds of chords 316 | */ 317 | var chordIntervs = { 318 | 319 | // Major chord 320 | 'maj': [0, 4, 7], 321 | 322 | // Minor chord 323 | 'min': [0, 3, 7], 324 | 325 | // Major 7th 326 | 'maj7': [0, 4, 7, 11], 327 | 328 | // Minor 7th 329 | 'min7': [0, 3, 7, 10], 330 | 331 | // Dominant 7th 332 | '7': [0, 4, 7, 10], 333 | 334 | // Suspended 4th 335 | 'sus4': [0, 5, 7], 336 | 337 | // Suspended second 338 | 'sus2': [0, 2, 7] 339 | }; 340 | 341 | /** 342 | Generate a list of notes for a chord 343 | */ 344 | function genChord(rootNote, type) 345 | { 346 | if ((rootNote instanceof Note) === false) 347 | rootNote = new Note(rootNote); 348 | 349 | // Get the intervals for this type of chord 350 | var intervs = chordIntervs[type]; 351 | 352 | assert ( 353 | intervs instanceof Array, 354 | 'invalid chord type: ' + type 355 | ); 356 | 357 | // Get the root note number 358 | var rootNo = rootNote.noteNo; 359 | 360 | // Compute the note numbers for the notes 361 | var notes = intervs.map(function (i) { return new Note(rootNo + i); }); 362 | 363 | return notes; 364 | } 365 | 366 | -------------------------------------------------------------------------------- /piece.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * 3 | * This file is part of the Melodique project. The project is 4 | * distributed at: 5 | * https://github.com/maximecb/Melodique 6 | * 7 | * Copyright (c) 2013, Maxime Chevalier-Boisvert. All rights reserved. 8 | * 9 | * This software is licensed under the following license (Modified BSD 10 | * License): 11 | * 12 | * Redistribution and use in source and binary forms, with or without 13 | * modification, are permitted provided that the following conditions are 14 | * met: 15 | * 1. Redistributions of source code must retain the above copyright 16 | * notice, this list of conditions and the following disclaimer. 17 | * 2. Redistributions in binary form must reproduce the above copyright 18 | * notice, this list of conditions and the following disclaimer in the 19 | * documentation and/or other materials provided with the distribution. 20 | * 3. The name of the author may not be used to endorse or promote 21 | * products derived from this software without specific prior written 22 | * permission. 23 | * 24 | * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED 25 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 26 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 27 | * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 28 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 29 | * NOT LIMITED TO PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 30 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 31 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 33 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | * 35 | *****************************************************************************/ 36 | 37 | //============================================================================ 38 | // Music piece implementation 39 | //============================================================================ 40 | 41 | /** 42 | @class Musical piece implementation. 43 | */ 44 | function Piece(synthNet) 45 | { 46 | assert ( 47 | synthNet instanceof SynthNet || synthNet === undefined, 48 | 'invalid synth net' 49 | ); 50 | 51 | /** 52 | Synthesis network used by this piece 53 | */ 54 | this.synthNet = synthNet; 55 | 56 | /** 57 | Music/info tracks 58 | */ 59 | this.tracks = []; 60 | 61 | /** 62 | Current playback time/position 63 | */ 64 | this.playTime = 0; 65 | 66 | /** 67 | Previous update time 68 | */ 69 | this.prevTime = 0; 70 | 71 | /** 72 | Tempo in beats per minute 73 | */ 74 | this.beatsPerMin = 120; 75 | 76 | /** 77 | Time signature numerator, beats per bar 78 | */ 79 | this.beatsPerBar = 4; 80 | 81 | /** 82 | Time signature denominator, note value for each beat 83 | */ 84 | this.noteVal = 4; 85 | } 86 | 87 | /** 88 | Add a track to the piece 89 | */ 90 | Piece.prototype.addTrack = function (track) 91 | { 92 | assert ( 93 | track instanceof Track, 94 | 'invalid track' 95 | ); 96 | 97 | this.tracks.push(track); 98 | } 99 | 100 | /** 101 | Get the time offset for a beat number. This number can be fractional. 102 | */ 103 | Piece.prototype.beatTime = function (beatNo) 104 | { 105 | var beatLen = 60 / this.beatsPerMin; 106 | 107 | return beatLen * beatNo; 108 | } 109 | 110 | /** 111 | Get the length in seconds for a note value multiple 112 | */ 113 | Piece.prototype.noteLen = function (len) 114 | { 115 | // By default, use the default note value 116 | if (len === undefined) 117 | len = 1; 118 | 119 | var beatLen = 60 / this.beatsPerMin; 120 | 121 | var barLen = beatLen * this.beatsPerBar; 122 | 123 | var noteLen = barLen / this.noteVal; 124 | 125 | return len * noteLen * 0.99; 126 | } 127 | 128 | /** 129 | Helper methods to add notes to the track. 130 | Produces a note-on and note-off event pair. 131 | */ 132 | Piece.prototype.makeNote = function (track, beatNo, note, len, vel) 133 | { 134 | assert ( 135 | note instanceof Note || 136 | typeof note === 'string', 137 | 'invalid note' 138 | ); 139 | 140 | if (typeof note === 'string') 141 | note = new Note(note); 142 | 143 | // By default, the velocity is 100% 144 | if (len === undefined) 145 | vel = 1; 146 | 147 | // Convert the note time to a beat number 148 | var time = this.beatTime(beatNo); 149 | 150 | // Get the note length in seconds 151 | var noteLen = this.noteLen(len); 152 | 153 | // Create the note on and note off events 154 | var noteOn = new NoteOnEvt(time, note, vel); 155 | var noteOff = new NoteOffEvt(time + noteLen, note); 156 | 157 | // Add the events to the track 158 | track.addEvent(noteOn); 159 | track.addEvent(noteOff); 160 | } 161 | 162 | /** 163 | Set the playback position/time 164 | */ 165 | Piece.prototype.setTime = function (time) 166 | { 167 | this.playTime = time; 168 | } 169 | 170 | /** 171 | Dispatch synthesis events up to the current time 172 | */ 173 | Piece.prototype.dispatch = function (curTime) 174 | { 175 | // Do the dispatch for each track 176 | for (var i = 0; i < this.tracks.length; ++i) 177 | { 178 | var track = this.tracks[i]; 179 | 180 | track.dispatch(this.prevTime, curTime); 181 | } 182 | 183 | // Store the last update time/position 184 | this.prevTime = curTime; 185 | } 186 | 187 | /** 188 | Called when stopping the playback of a piece 189 | */ 190 | Piece.prototype.stop = function () 191 | { 192 | // If a synthesis network is attached to this piece 193 | if (this.synthNet !== undefined) 194 | { 195 | // Send an all notes off event to all synthesis nodes 196 | var notesOffEvt = new AllNotesOffEvt(); 197 | for (var i = 0; i < this.synthNet.nodes.length; ++i) 198 | { 199 | var node = this.synthNet.nodes[i]; 200 | node.processEvent(notesOffEvt); 201 | } 202 | } 203 | 204 | // Set the playback position past all events 205 | this.playTime = Infinity; 206 | } 207 | 208 | /** 209 | Create a handler for real-time audio generation 210 | */ 211 | Piece.prototype.makeHandler = function () 212 | { 213 | var synthNet = this.synthNet; 214 | var piece = this; 215 | 216 | var sampleRate = synthNet.sampleRate; 217 | 218 | // Output node of the synthesis network 219 | var outNode = synthNet.outNode; 220 | 221 | // Current playback time 222 | var curTime = piece.playTime; 223 | 224 | // Audio generation function 225 | function genAudio(evt) 226 | { 227 | var startTime = (new Date()).getTime(); 228 | 229 | var numChans = evt.outputBuffer.numberOfChannels 230 | var numSamples = evt.outputBuffer.getChannelData(0).length; 231 | 232 | // If the playback position changed, update the current time 233 | if (piece.playTime !== curTime) 234 | { 235 | console.log('playback time updated'); 236 | curTime = piece.playTime; 237 | } 238 | 239 | assert ( 240 | numChans === outNode.numChans, 241 | 'mismatch in the number of output channels' 242 | ); 243 | 244 | assert ( 245 | numSamples % SYNTH_BUF_SIZE === 0, 246 | 'the output buffer size must be a multiple of the synth buffer size' 247 | ); 248 | 249 | // Until all samples are produced 250 | for (var smpIdx = 0; smpIdx < numSamples; smpIdx += SYNTH_BUF_SIZE) 251 | { 252 | // Update the piece, dispatch track events 253 | piece.dispatch(curTime); 254 | 255 | // Generate the sample values 256 | var values = synthNet.genOutput(curTime); 257 | 258 | // Copy the values for each channel 259 | for (var chnIdx = 0; chnIdx < numChans; ++chnIdx) 260 | { 261 | var srcBuf = outNode.getBuffer(chnIdx); 262 | var dstBuf = evt.outputBuffer.getChannelData(chnIdx); 263 | 264 | for (var i = 0; i < SYNTH_BUF_SIZE; ++i) 265 | dstBuf[smpIdx + i] = srcBuf[i]; 266 | } 267 | 268 | // Update the current time based on sample rate 269 | curTime += SYNTH_BUF_SIZE / sampleRate; 270 | 271 | // Update the current playback position 272 | piece.playTime = curTime; 273 | } 274 | 275 | var endTime = (new Date()).getTime(); 276 | var compTime = (endTime - startTime) / 1000; 277 | var realTime = (numSamples / synthNet.sampleRate); 278 | var cpuUse = (100 * compTime / realTime).toFixed(1); 279 | 280 | //console.log('cpu use: ' + cpuUse + '%'); 281 | } 282 | 283 | // Return the handler function 284 | return genAudio; 285 | } 286 | 287 | /** 288 | Draw the notes of a track using the canvas API 289 | */ 290 | Piece.prototype.drawTrack = function ( 291 | track, 292 | canvasCtx, 293 | topX, 294 | topY, 295 | width, 296 | height, 297 | minNote, 298 | numOcts 299 | ) 300 | { 301 | // Compute the bottom-right corner coordinates 302 | var botX = topX + width; 303 | var botY = topY + height; 304 | 305 | // Get the last event time 306 | var maxTime = track.endTime(); 307 | 308 | // Compute the number of beats 309 | var numBeats = Math.ceil((maxTime / 60) * this.beatsPerMin); 310 | 311 | // Compute the total time for the beats 312 | var totalTime = (numBeats / this.beatsPerMin) * 60; 313 | 314 | //console.log('max time : ' + maxTime); 315 | //console.log('num beats : ' + numBeats); 316 | //console.log('total time: ' + totalTime); 317 | 318 | var minNoteNo = Math.floor(minNote.noteNo / NOTES_PER_OCTAVE) * NOTES_PER_OCTAVE; 319 | 320 | var numNotes = numOcts * NOTES_PER_OCTAVE; 321 | 322 | var numWhites = numOcts * 7; 323 | 324 | var whiteHeight = height / numWhites; 325 | 326 | var blackHeight = whiteHeight / 2; 327 | 328 | var pianoWidth = 40; 329 | 330 | var blackWidth = (pianoWidth / 4) * 3; 331 | 332 | var beatWidth = (width - pianoWidth) / numBeats; 333 | 334 | canvasCtx.fillStyle = "grey" 335 | canvasCtx.fillRect(topX, topY, width, height); 336 | 337 | canvasCtx.fillStyle = "white" 338 | canvasCtx.fillRect(topX, topY, pianoWidth, height); 339 | 340 | canvasCtx.strokeStyle = "black"; 341 | canvasCtx.beginPath(); 342 | canvasCtx.moveTo(topX, topY); 343 | canvasCtx.lineTo(topX + pianoWidth, topY); 344 | canvasCtx.lineTo(topX + pianoWidth, botY); 345 | canvasCtx.lineTo(topX, botY); 346 | canvasCtx.closePath(); 347 | canvasCtx.stroke(); 348 | 349 | var noteExts = new Array(numNotes); 350 | var noteIdx = 0; 351 | 352 | // For each white note 353 | for (var i = 0; i < numWhites; ++i) 354 | { 355 | var whiteBot = botY - (whiteHeight * i); 356 | var whiteTop = whiteBot - whiteHeight; 357 | 358 | var whiteExts = noteExts[noteIdx++] = { bot: whiteBot, top: whiteTop }; 359 | 360 | if (i > 0) 361 | { 362 | var prevExts = noteExts[noteIdx - 2]; 363 | whiteExts.bot = Math.min(whiteExts.bot, prevExts.top); 364 | } 365 | 366 | canvasCtx.strokeStyle = "black"; 367 | canvasCtx.beginPath(); 368 | canvasCtx.moveTo(topX, whiteBot); 369 | canvasCtx.lineTo(topX + pianoWidth, whiteBot); 370 | canvasCtx.closePath(); 371 | canvasCtx.stroke(); 372 | 373 | if ((i % 7) !== 2 && (i % 7) !== 6) 374 | { 375 | var blackTop = whiteTop - (blackHeight / 2); 376 | var blackBot = whiteTop + (blackHeight / 2); 377 | 378 | var blackExts = noteExts[noteIdx++] = { bot:blackBot, top:blackTop }; 379 | whiteExts.top = blackExts.bot; 380 | 381 | canvasCtx.fillStyle = "black"; 382 | canvasCtx.beginPath(); 383 | canvasCtx.moveTo(topX, blackTop); 384 | canvasCtx.lineTo(topX + blackWidth, blackTop); 385 | canvasCtx.lineTo(topX + blackWidth, blackBot); 386 | canvasCtx.lineTo(topX, blackBot); 387 | canvasCtx.lineTo(topX, blackTop); 388 | canvasCtx.closePath(); 389 | canvasCtx.fill(); 390 | } 391 | } 392 | 393 | // Draw the horizontal note separation lines 394 | for (var i = 0; i < noteExts.length; ++i) 395 | { 396 | var exts = noteExts[i]; 397 | 398 | canvasCtx.strokeStyle = "rgb(0, 0, 125)"; 399 | canvasCtx.beginPath(); 400 | canvasCtx.moveTo(topX + pianoWidth + 1, exts.top); 401 | canvasCtx.lineTo(botX, exts.top); 402 | canvasCtx.closePath(); 403 | canvasCtx.stroke(); 404 | } 405 | 406 | // Draw the vertical beat separation lines 407 | for (var i = 1; i < numBeats; ++i) 408 | { 409 | var xCoord = topX + pianoWidth + (i * beatWidth); 410 | 411 | var color; 412 | if (i % this.beatsPerBar === 0) 413 | color = "rgb(25, 25, 255)" 414 | else 415 | color = "rgb(0, 0, 125)"; 416 | 417 | canvasCtx.strokeStyle = color; 418 | canvasCtx.beginPath(); 419 | canvasCtx.moveTo(xCoord, topY); 420 | canvasCtx.lineTo(xCoord, botY); 421 | canvasCtx.closePath(); 422 | canvasCtx.stroke(); 423 | } 424 | 425 | // For each track event 426 | for (var i = 0; i < track.events.length; ++i) 427 | { 428 | var event = track.events[i]; 429 | 430 | // If this is a note on event 431 | if (event instanceof NoteOnEvt) 432 | { 433 | var noteNo = event.note.noteNo; 434 | var startTime = event.time; 435 | 436 | // Try to find the note end time 437 | //var endTime = startTime + (60 / this.beatsPerMin); 438 | var endTime = undefined; 439 | for (var j = i + 1; j < track.events.length; ++j) 440 | { 441 | var e2 = track.events[j]; 442 | 443 | if (e2 instanceof NoteOffEvt && 444 | e2.note.noteNo === noteNo && 445 | e2.time > event.time) 446 | { 447 | endTime = e2.time; 448 | break; 449 | } 450 | } 451 | 452 | if (endTime === undefined) 453 | error('COULD NOT FIND NOTE OFF'); 454 | 455 | var startFrac = startTime / totalTime; 456 | var endFrac = endTime / totalTime; 457 | 458 | var xStart = topX + pianoWidth + startFrac * (width - pianoWidth); 459 | var xEnd = topX + pianoWidth + endFrac * (width - pianoWidth); 460 | 461 | var noteIdx = noteNo - minNoteNo; 462 | 463 | if (noteIdx >= noteExts.length) 464 | { 465 | console.log('note above limit'); 466 | continue; 467 | } 468 | 469 | //console.log(noteIdx + ': ' + xStart + ' => ' + xEnd); 470 | 471 | var exts = noteExts[noteIdx]; 472 | 473 | canvasCtx.fillStyle = "red"; 474 | canvasCtx.strokeStyle = "black"; 475 | canvasCtx.beginPath(); 476 | canvasCtx.moveTo(xStart, exts.top); 477 | canvasCtx.lineTo(xEnd , exts.top); 478 | canvasCtx.lineTo(xEnd , exts.bot); 479 | canvasCtx.lineTo(xStart, exts.bot); 480 | canvasCtx.lineTo(xStart, exts.top); 481 | canvasCtx.closePath(); 482 | canvasCtx.fill(); 483 | canvasCtx.stroke(); 484 | } 485 | } 486 | 487 | // If playback is ongoing 488 | if (this.playTime !== 0 && maxTime !== 0) 489 | { 490 | // Compute the cursor line position 491 | var cursorFrac = this.playTime / maxTime; 492 | var cursorPos = topX + pianoWidth + cursorFrac * (width - pianoWidth); 493 | 494 | // Draw the cursor line 495 | canvasCtx.strokeStyle = "white"; 496 | canvasCtx.beginPath(); 497 | canvasCtx.moveTo(cursorPos, topY); 498 | canvasCtx.lineTo(cursorPos, botY); 499 | canvasCtx.closePath(); 500 | canvasCtx.stroke(); 501 | } 502 | } 503 | 504 | /** 505 | Produce MIDI file data for a track of this piece. 506 | The data is written into a byte array. 507 | */ 508 | Piece.prototype.getMIDIData = function (track) 509 | { 510 | var data = []; 511 | 512 | var writeIdx = 0; 513 | 514 | function writeByte(val) 515 | { 516 | assert ( 517 | val <= 0xFF, 518 | 'invalid value in writeByte' 519 | ); 520 | 521 | data[writeIdx++] = val; 522 | } 523 | 524 | function writeWORD(val) 525 | { 526 | assert ( 527 | val <= 0xFFFF, 528 | 'invalid value in writeWORD' 529 | ); 530 | 531 | writeByte((val >> 8) & 0xFF); 532 | writeByte((val >> 0) & 0xFF); 533 | } 534 | 535 | function writeDWORD(val) 536 | { 537 | assert ( 538 | val <= 0xFFFFFFFF, 539 | 'invalid value in writeDWORD: ' + val 540 | ); 541 | 542 | writeByte((val >> 24) & 0xFF); 543 | writeByte((val >> 16) & 0xFF); 544 | writeByte((val >> 8) & 0xFF); 545 | writeByte((val >> 0) & 0xFF); 546 | } 547 | 548 | function writeVarLen(val) 549 | { 550 | // Higher bits must be written first 551 | 552 | var bytes = []; 553 | 554 | do 555 | { 556 | var bits = val & 0x7F; 557 | 558 | val >>= 7; 559 | 560 | bytes.push(bits); 561 | 562 | } while (val !== 0); 563 | 564 | for (var i = bytes.length - 1; i >= 0; --i) 565 | { 566 | var bits = bytes[i]; 567 | 568 | if (i > 0) 569 | bits = 0x80 | bits; 570 | 571 | writeByte(bits); 572 | } 573 | } 574 | 575 | // Number of clock ticks per beat 576 | var ticksPerBeat = 500; 577 | 578 | // Write the file header 579 | writeDWORD(0x4D546864); // MThd 580 | writeDWORD(0x00000006); // Chunk size 581 | writeWORD(0); // Type 0 MIDI file (one track) 582 | writeWORD(1); // One track 583 | writeWORD(ticksPerBeat); // Time division 584 | 585 | // Write the track header 586 | writeDWORD(0x4D54726B) // MTrk 587 | writeDWORD(0); // Chunk size, written later 588 | 589 | // Save the track size index 590 | var trackSizeIdx = data.length - 4; 591 | 592 | // Delta time conversion ratio 593 | var ticksPerSec = (this.beatsPerMin / 60) * ticksPerBeat; 594 | 595 | console.log('ticks per sec: ' + ticksPerSec); 596 | 597 | // Set the tempo in microseconds per quarter node 598 | var usPerMin = 60000000; 599 | var mpqn = usPerMin / this.beatsPerMin; 600 | writeVarLen(0); 601 | writeByte(0xFF) 602 | writeByte(0x51); 603 | writeVarLen(3); 604 | writeByte((mpqn >> 16) & 0xFF); 605 | writeByte((mpqn >> 8) & 0xFF); 606 | writeByte((mpqn >> 0) & 0xFF); 607 | 608 | // Set the time signature 609 | var num32Nds = Math.floor(8 * (4 / this.noteVal)); 610 | writeVarLen(0); 611 | writeByte(0xFF) 612 | writeByte(0x58); 613 | writeVarLen(4); 614 | writeByte(this.beatsPerBar); // Num 615 | writeByte(2); // Denom 2^2 = 4 616 | writeByte(24); // Metronome rate 617 | writeByte(num32Nds); // 32nds per quarter note 618 | 619 | console.log('beats per bar: ' + this.beatsPerBar); 620 | console.log('num 32nds: ' + num32Nds); 621 | 622 | // Set the piano program 623 | writeVarLen(0); 624 | writeByte(0xC0); 625 | writeByte(0); 626 | 627 | // For each track event 628 | for (var i = 0; i < track.events.length; ++i) 629 | { 630 | var event = track.events[i]; 631 | var prevEvent = track.events[i-1]; 632 | 633 | // Event format: 634 | // Delta Time 635 | // Event Type Value 636 | // MIDI Channel 637 | // Parameter 1 638 | // Parameter 2 639 | 640 | var deltaTime = prevEvent? (event.time - prevEvent.time):0; 641 | 642 | var deltaTicks = Math.ceil(ticksPerSec * deltaTime); 643 | 644 | assert ( 645 | isNonNegInt(deltaTicks), 646 | 'invalid delta ticks: ' + deltaTicks 647 | ); 648 | 649 | console.log(event.toString()) 650 | console.log('delta ticks: ' + deltaTicks); 651 | 652 | // Write the event delta time 653 | writeVarLen(deltaTicks); 654 | 655 | if (event instanceof NoteOnEvt) 656 | { 657 | writeByte(0x90); 658 | 659 | writeByte(event.note.noteNo); 660 | 661 | // Velocity 662 | var vel = Math.min(Math.floor(event.vel * 127), 127); 663 | writeByte(vel); 664 | } 665 | 666 | else if (event instanceof NoteOffEvt) 667 | { 668 | writeByte(0x80); 669 | 670 | writeByte(event.note.noteNo); 671 | 672 | // Velocity 673 | writeByte(0); 674 | } 675 | } 676 | 677 | // Write the end of track event 678 | writeVarLen(0); 679 | writeByte(0xFF) 680 | writeByte(0x2F); 681 | writeVarLen(0); 682 | 683 | // Write the track chunk size 684 | var trackSize = data.length - (trackSizeIdx + 4); 685 | console.log('track size: ' + trackSize); 686 | writeIdx = trackSizeIdx 687 | writeDWORD(trackSize); 688 | 689 | return data; 690 | } 691 | 692 | /** 693 | @class Synthesis event track implementation. Produces events and sends them 694 | to a target synthesis node. 695 | */ 696 | function Track(target) 697 | { 698 | assert ( 699 | target instanceof SynthNode || target === undefined, 700 | 'invalid target node' 701 | ); 702 | 703 | /** 704 | Target synthesis node to send events to 705 | */ 706 | this.target = target; 707 | 708 | /** 709 | Events for this track 710 | */ 711 | this.events = []; 712 | } 713 | 714 | /** 715 | Add an event to the track 716 | */ 717 | Track.prototype.addEvent = function (evt) 718 | { 719 | this.events.push(evt); 720 | 721 | // If the event is being added at the end of the track, stop 722 | if (this.events.length === 1 || 723 | evt.time >= this.events[this.events.length-2].time) 724 | return; 725 | 726 | // Sort the events 727 | this.events.sort(function (a, b) { return a.time - b.time; }); 728 | } 729 | 730 | /** 731 | Get the dispatch time of the last event 732 | */ 733 | Track.prototype.endTime = function () 734 | { 735 | if (this.events.length === 0) 736 | return 737 | else 738 | return this.events[this.events.length-1].time; 739 | } 740 | 741 | /** 742 | Dispatch the events between the previous update time and 743 | the current time, inclusively. 744 | */ 745 | Track.prototype.dispatch = function (prevTime, curTime) 746 | { 747 | if (this.target === undefined) 748 | return; 749 | 750 | if (this.events.length === 0) 751 | return; 752 | 753 | // Must play all events from the previous time (inclusive) up to the 754 | // current time (exclusive). 755 | // 756 | // Find the mid idx where we are at or just past the previous time. 757 | 758 | var minIdx = 0; 759 | var maxIdx = this.events.length - 1; 760 | 761 | var midIdx = 0; 762 | 763 | while (minIdx <= maxIdx) 764 | { 765 | midIdx = Math.floor((minIdx + maxIdx) / 2); 766 | 767 | //console.log(midIdx); 768 | 769 | var midTime = this.events[midIdx].time; 770 | 771 | var leftTime = (midIdx === 0)? -Infinity:this.events[midIdx-1].time; 772 | 773 | if (leftTime < prevTime && midTime >= prevTime) 774 | break; 775 | 776 | if (midTime < prevTime) 777 | minIdx = midIdx + 1; 778 | else 779 | maxIdx = midIdx - 1; 780 | } 781 | 782 | // If no event to dispatch was fount, stop 783 | if (minIdx > maxIdx) 784 | return; 785 | 786 | // Dispatch all events up to the current time (exclusive) 787 | for (var idx = midIdx; idx < this.events.length; ++idx) 788 | { 789 | var evt = this.events[idx]; 790 | 791 | if (evt.time >= curTime) 792 | break; 793 | 794 | console.log('Dispatch: ' + evt); 795 | 796 | this.target.processEvent(evt, curTime); 797 | } 798 | } 799 | 800 | /** 801 | Clear all the events from this track 802 | */ 803 | Track.prototype.clear = function () 804 | { 805 | this.events = []; 806 | } 807 | 808 | //============================================================================ 809 | // Synthesis events 810 | //============================================================================ 811 | 812 | /** 813 | @class Base class for all synthesis events. 814 | */ 815 | function SynthEvt() 816 | { 817 | /** 818 | Event occurrence time 819 | */ 820 | this.time = 0; 821 | } 822 | 823 | /** 824 | Format a synthesis event string representation 825 | */ 826 | SynthEvt.formatStr = function (evt, str) 827 | { 828 | return evt.time.toFixed(2) + ': ' + str; 829 | } 830 | 831 | /** 832 | Default string representation for events 833 | */ 834 | SynthEvt.prototype.toString = function () 835 | { 836 | return SynthEvt.formatStr(this, 'event'); 837 | } 838 | 839 | /** 840 | @class Note on event 841 | */ 842 | function NoteOnEvt(time, note, vel) 843 | { 844 | // By default, use the C4 note 845 | if (note === undefined) 846 | note = new Note(C4_NOTE_NO); 847 | 848 | // By default, 50% velocity 849 | if (vel === undefined) 850 | vel = 0.5; 851 | 852 | /** 853 | Note 854 | */ 855 | this.note = note; 856 | 857 | /** 858 | Velocity 859 | */ 860 | this.vel = vel; 861 | 862 | // Set the event time 863 | this.time = time; 864 | } 865 | NoteOnEvt.prototype = new SynthEvt(); 866 | 867 | /** 868 | Default string representation for events 869 | */ 870 | NoteOnEvt.prototype.toString = function () 871 | { 872 | return SynthEvt.formatStr(this, 'note-on ' + this.note); 873 | } 874 | 875 | /** 876 | @class Note off event 877 | */ 878 | function NoteOffEvt(time, note) 879 | { 880 | // By default, use the C4 note 881 | if (note === undefined) 882 | note = new Note(C4_NOTE_NO); 883 | 884 | /** 885 | Note 886 | */ 887 | this.note = note; 888 | 889 | // Set the event time 890 | this.time = time; 891 | } 892 | NoteOffEvt.prototype = new SynthEvt(); 893 | 894 | /** 895 | Default string representation for events 896 | */ 897 | NoteOffEvt.prototype.toString = function () 898 | { 899 | return SynthEvt.formatStr(this, 'note-off ' + this.note); 900 | } 901 | 902 | /** 903 | @class All notes off event. Silences instruments. 904 | */ 905 | function AllNotesOffEvt() 906 | { 907 | } 908 | AllNotesOffEvt.prototype = new SynthEvt(); 909 | 910 | /** 911 | Default string representation for events 912 | */ 913 | AllNotesOffEvt.prototype.toString = function () 914 | { 915 | return SynthEvt.formatStr(this, 'all notes off'); 916 | } 917 | 918 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | git push -f origin master:gh-pages 2 | -------------------------------------------------------------------------------- /sampling.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * 3 | * This file is part of the Melodique project. The project is 4 | * distributed at: 5 | * https://github.com/maximecb/Melodique 6 | * 7 | * Copyright (c) 2013, Maxime Chevalier-Boisvert. All rights reserved. 8 | * 9 | * This software is licensed under the following license (Modified BSD 10 | * License): 11 | * 12 | * Redistribution and use in source and binary forms, with or without 13 | * modification, are permitted provided that the following conditions are 14 | * met: 15 | * 1. Redistributions of source code must retain the above copyright 16 | * notice, this list of conditions and the following disclaimer. 17 | * 2. Redistributions in binary form must reproduce the above copyright 18 | * notice, this list of conditions and the following disclaimer in the 19 | * documentation and/or other materials provided with the distribution. 20 | * 3. The name of the author may not be used to endorse or promote 21 | * products derived from this software without specific prior written 22 | * permission. 23 | * 24 | * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED 25 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 26 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 27 | * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 28 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 29 | * NOT LIMITED TO PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 30 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 31 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 33 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | * 35 | *****************************************************************************/ 36 | 37 | /** 38 | @class Loads a sample asynchronously from a URL 39 | */ 40 | function Sample(url) 41 | { 42 | /** 43 | Sample URL 44 | */ 45 | this.url = url; 46 | 47 | /** 48 | Audio data buffer, undefined until loaded 49 | */ 50 | this.buffer = undefined; 51 | 52 | console.log('loading sample "' + url + '"'); 53 | 54 | var xhr = new XMLHttpRequest(); 55 | xhr.open("GET", url, true); 56 | xhr.responseType = "arraybuffer"; 57 | 58 | var that = this; 59 | xhr.onload = function() 60 | { 61 | var audioBuffer = audioCtx.createBuffer(xhr.response, true); 62 | var f32buffer = audioBuffer.getChannelData(0); 63 | 64 | var f64buffer = new Float64Array(f32buffer.length); 65 | for (var i = 0; i < f32buffer.length; ++i) 66 | f64buffer[i] = f32buffer[i]; 67 | 68 | that.buffer = f64buffer; 69 | 70 | //console.log('loaded sample "' + url + '" (' + that.buffer.length + ')'); 71 | }; 72 | 73 | xhr.send(); 74 | } 75 | 76 | /** 77 | @class Basic sample-mapping instrument 78 | @extends SynthNode 79 | */ 80 | function SampleKit() 81 | { 82 | /** 83 | Array of samples, indexed by MIDI note numbers 84 | */ 85 | this.samples = []; 86 | 87 | /** 88 | Array of active (currently playing) samples 89 | */ 90 | this.actSamples = []; 91 | 92 | // Sound output 93 | new SynthOutput(this, 'output'); 94 | 95 | this.name = 'sample-kit'; 96 | } 97 | SampleKit.prototype = new SynthNode(); 98 | 99 | /** 100 | Map a sample to a given note 101 | */ 102 | SampleKit.prototype.mapSample = function (note, sample, volume) 103 | { 104 | if (typeof note === 'string') 105 | note = new Note(note); 106 | 107 | if (typeof sample === 'string') 108 | sample = new Sample(sample); 109 | 110 | if (volume === undefined) 111 | volume = 1; 112 | 113 | this.samples[note.noteNo] = { 114 | data: sample, 115 | volume: volume 116 | } 117 | } 118 | 119 | /** 120 | Process an event 121 | */ 122 | SampleKit.prototype.processEvent = function (evt, time) 123 | { 124 | // Note-on event 125 | if (evt instanceof NoteOnEvt) 126 | { 127 | // Get the note 128 | var note = evt.note; 129 | 130 | var sample = this.samples[note.noteNo]; 131 | 132 | // If no sample is mapped to this note, do nothing 133 | if (sample === undefined) 134 | return; 135 | 136 | // If the sample is not yet loaded, do nothing 137 | if (sample.data.buffer === undefined) 138 | return; 139 | 140 | // Add a new instance to the active list 141 | this.actSamples.push({ 142 | sample: sample, 143 | pos: 0 144 | }); 145 | } 146 | 147 | // All notes off event 148 | else if (evt instanceof AllNotesOffEvt) 149 | { 150 | this.actSamples = []; 151 | } 152 | 153 | // By default, do nothing 154 | } 155 | 156 | /** 157 | Update the outputs based on the inputs 158 | */ 159 | SampleKit.prototype.update = function (time, sampleRate) 160 | { 161 | // If there are no active samples, do nothing 162 | if (this.actSamples.length === 0) 163 | return; 164 | 165 | // Get the output buffer 166 | var outBuf = this.output.getBuffer(0); 167 | 168 | // Initialize the output to 0 169 | for (var i = 0; i < outBuf.length; ++i) 170 | outBuf[i] = 0; 171 | 172 | // For each active sample instance 173 | for (var i = 0; i < this.actSamples.length; ++i) 174 | { 175 | var actSample = this.actSamples[i]; 176 | 177 | var inBuf = actSample.sample.data.buffer; 178 | 179 | var volume = actSample.sample.volume; 180 | 181 | assert ( 182 | inBuf instanceof Float64Array, 183 | 'invalid input buffer' 184 | ); 185 | 186 | var playLen = Math.min(outBuf.length, inBuf.length - actSample.pos); 187 | 188 | for (var outIdx = 0; outIdx < playLen; ++outIdx) 189 | outBuf[outIdx] += inBuf[actSample.pos + outIdx] * volume; 190 | 191 | actSample.pos += playLen; 192 | 193 | // If this sample is done playing 194 | if (actSample.pos === inBuf.length) 195 | { 196 | // Remove the sample from the active list 197 | this.actSamples.splice(i, 1); 198 | --i; 199 | } 200 | } 201 | } 202 | 203 | /** 204 | @class Sample-based pitch-shifting instrument 205 | @extends SynthNode 206 | */ 207 | function SampleInstr(sample, centerNote) 208 | { 209 | if (typeof sample === 'string') 210 | sample = new Sample(sample); 211 | 212 | if (typeof centerNote === 'string') 213 | centerNote = new Note(centerNote); 214 | 215 | /** 216 | Sample data 217 | */ 218 | this.sample = sample; 219 | 220 | /** 221 | Center note/pitch for the sample 222 | */ 223 | this.centerNote = centerNote; 224 | 225 | /** 226 | List of active notes 227 | */ 228 | this.actNotes = []; 229 | 230 | // TODO: loop points 231 | 232 | // Sound output 233 | new SynthOutput(this, 'output'); 234 | 235 | this.name = 'sample-instr'; 236 | } 237 | SampleInstr.prototype = new SynthNode(); 238 | 239 | /** 240 | Process an event 241 | */ 242 | SampleInstr.prototype.processEvent = function (evt, time) 243 | { 244 | // Note-on event 245 | if (evt instanceof NoteOnEvt) 246 | { 247 | // If the sample is not yet loaded, stop 248 | if (this.sample.buffer === undefined) 249 | return; 250 | 251 | // Get the note 252 | var note = evt.note; 253 | 254 | var centerFreq = this.centerNote.getFreq(); 255 | var noteFreq = note.getFreq(); 256 | var freqRatio = noteFreq / centerFreq; 257 | 258 | // Add an entry to the active note list 259 | this.actNotes.push({ 260 | pos: 0, 261 | freqRatio: freqRatio 262 | }); 263 | } 264 | 265 | // Note-off event 266 | if (evt instanceof NoteOffEvt) 267 | { 268 | // Get the note 269 | var note = evt.note; 270 | 271 | // TODO: loop points 272 | } 273 | 274 | // All notes off event 275 | else if (evt instanceof AllNotesOffEvt) 276 | { 277 | this.actNotes = []; 278 | } 279 | 280 | // By default, do nothing 281 | } 282 | 283 | /** 284 | Update the outputs based on the inputs 285 | */ 286 | SampleInstr.prototype.update = function (time, sampleRate) 287 | { 288 | // If there are no active notes, do nothing 289 | if (this.actNotes.length === 0) 290 | return; 291 | 292 | // Get the output buffer 293 | var outBuf = this.output.getBuffer(0); 294 | 295 | // Initialize the output to 0 296 | for (var i = 0; i < outBuf.length; ++i) 297 | outBuf[i] = 0; 298 | 299 | // Get the sample buffer 300 | var inBuf = this.sample.buffer; 301 | 302 | // For each active note 303 | for (var i = 0; i < this.actNotes.length; ++i) 304 | { 305 | var actNote = this.actNotes[i]; 306 | 307 | // Compute the displacement between sample points 308 | var disp = actNote.freqRatio; 309 | 310 | var pos = actNote.pos; 311 | 312 | // For each output sample to produce 313 | for (var outIdx = 0; outIdx < outBuf.length; ++outIdx) 314 | { 315 | var lIdx = Math.floor(pos); 316 | var rIdx = lIdx + 1; 317 | 318 | if (rIdx >= inBuf.length) 319 | break; 320 | 321 | var lVal = inBuf[lIdx]; 322 | var rVal = inBuf[rIdx]; 323 | var oVal = lVal * (rIdx - pos) + rVal * (pos - lIdx); 324 | 325 | outBuf[outIdx] = oVal; 326 | 327 | // Update the sample position 328 | pos += disp; 329 | } 330 | 331 | // Store the final sample position 332 | actNote.pos = pos; 333 | 334 | // If the note is done playing 335 | if (pos >= inBuf.length) 336 | { 337 | // Remove the note from the active list 338 | this.actNotes.splice(i, 1); 339 | --i; 340 | } 341 | } 342 | } 343 | 344 | -------------------------------------------------------------------------------- /stylesheet.css: -------------------------------------------------------------------------------- 1 | body 2 | { 3 | background: #000000; 4 | 5 | color: white; 6 | font-family: "Arial", sans-serif; 7 | font-weight: normal; 8 | } 9 | 10 | a 11 | { 12 | color: rgb(160,160,160); 13 | font-family: "Arial", sans-serif; 14 | } 15 | a:link {text-decoration: none; } 16 | a:visited {text-decoration: none; } 17 | a:active {text-decoration: none; } 18 | a:hover {text-decoration: underline; } 19 | 20 | div.title 21 | { 22 | margin-top: 12px; 23 | margin-bottom: 12px; 24 | 25 | text-align:center; 26 | color: white; 27 | font-family: "Arial", sans-serif; 28 | font-weight: bold; 29 | font-size: 30px; 30 | } 31 | 32 | div.faq_link 33 | { 34 | margin-top: -5px; 35 | margin-bottom: 12px; 36 | 37 | text-align: center; 38 | font-weight: bold; 39 | font-size: 15px; 40 | } 41 | 42 | div.canvas_frame 43 | { 44 | border: 2px solid rgb(255,255,255); 45 | 46 | text-align: center; 47 | vertical-align: middle; 48 | } 49 | 50 | div.text_frame 51 | { 52 | border: 2px solid rgb(255,255,255); 53 | padding-top: 3px; 54 | padding-bottom: 3px; 55 | padding-left: 5px; 56 | padding-right: 5px; 57 | 58 | vertical-align: text-top; 59 | text-align: left; 60 | color: white; 61 | font-family: "Courier", sans-serif; 62 | font-weight: normal; 63 | font-size: 15px; 64 | } 65 | 66 | div.faq_q 67 | { 68 | margin-top: 12px; 69 | margin-bottom: 10px; 70 | 71 | text-align:left; 72 | color: red; 73 | font-family: "Arial", sans-serif; 74 | font-weight: bold; 75 | font-size: 20px; 76 | } 77 | 78 | div.ad 79 | { 80 | width:100%; 81 | margin-top: 12px; 82 | margin-bottom: 12px; 83 | 84 | text-align:center; 85 | } 86 | 87 | div.copyright 88 | { 89 | margin-top: 8px; 90 | margin-bottom: 8px; 91 | 92 | text-align:center; 93 | color: white; 94 | font-family: "Arial", sans-serif; 95 | font-weight: normal; 96 | font-size: 15px; 97 | } 98 | 99 | form 100 | { 101 | color: white; 102 | font-family: "Arial", sans-serif; 103 | } 104 | 105 | -------------------------------------------------------------------------------- /synth.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * 3 | * This file is part of the Melodique project. The project is 4 | * distributed at: 5 | * https://github.com/maximecb/Melodique 6 | * 7 | * Copyright (c) 2013, Maxime Chevalier-Boisvert. All rights reserved. 8 | * 9 | * This software is licensed under the following license (Modified BSD 10 | * License): 11 | * 12 | * Redistribution and use in source and binary forms, with or without 13 | * modification, are permitted provided that the following conditions are 14 | * met: 15 | * 1. Redistributions of source code must retain the above copyright 16 | * notice, this list of conditions and the following disclaimer. 17 | * 2. Redistributions in binary form must reproduce the above copyright 18 | * notice, this list of conditions and the following disclaimer in the 19 | * documentation and/or other materials provided with the distribution. 20 | * 3. The name of the author may not be used to endorse or promote 21 | * products derived from this software without specific prior written 22 | * permission. 23 | * 24 | * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED 25 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 26 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 27 | * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 28 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 29 | * NOT LIMITED TO PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 30 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 31 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 33 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | * 35 | *****************************************************************************/ 36 | 37 | //============================================================================ 38 | // Synthesis network core 39 | //============================================================================ 40 | 41 | /** 42 | Buffer size used by the synthesis network 43 | */ 44 | var SYNTH_BUF_SIZE = 256; 45 | 46 | /** 47 | Buffer containing only zero data 48 | */ 49 | var SYNTH_ZERO_BUF = new Float64Array(SYNTH_BUF_SIZE); 50 | 51 | /** 52 | @class Synthesis node output 53 | */ 54 | function SynthOutput(node, name, numChans) 55 | { 56 | assert ( 57 | node[name] === undefined, 58 | 'node already has property with this name' 59 | ); 60 | 61 | // By default, one output channel 62 | if (numChans === undefined) 63 | numChans = 1; 64 | 65 | /** 66 | Parent synthesis node 67 | */ 68 | this.node = node; 69 | 70 | /** 71 | Output name 72 | */ 73 | this.name = name; 74 | 75 | /** 76 | Number of output channels 77 | */ 78 | this.numChans = numChans; 79 | 80 | /** 81 | Output buffers, one per channel 82 | */ 83 | this.buffers = new Array(numChans); 84 | 85 | /** 86 | Flag to indicate output was produced in the current iteration 87 | */ 88 | this.hasData = false; 89 | 90 | /** 91 | Connected destination nodes 92 | */ 93 | this.dsts = []; 94 | 95 | // Allocate the output buffers 96 | for (var i = 0; i < numChans; ++i) 97 | this.buffers[i] = new Float64Array(SYNTH_BUF_SIZE); 98 | 99 | // Create a field in the parent node for this output 100 | node[name] = this; 101 | } 102 | 103 | /** 104 | Get the buffer for a given channel 105 | */ 106 | SynthOutput.prototype.getBuffer = function (chanIdx) 107 | { 108 | assert ( 109 | !(chanIdx === undefined && this.numChans > 1), 110 | 'channel idx must be specified when more than 1 channel' 111 | ); 112 | 113 | if (chanIdx === undefined) 114 | chanIdx = 0; 115 | 116 | // Mark this output as having data 117 | this.hasData = true; 118 | 119 | return this.buffers[chanIdx]; 120 | } 121 | 122 | /** 123 | Connect to a synthesis input 124 | */ 125 | SynthOutput.prototype.connect = function (dst) 126 | { 127 | assert ( 128 | dst instanceof SynthInput, 129 | 'invalid dst' 130 | ); 131 | 132 | assert ( 133 | this.dsts.indexOf(dst) === -1, 134 | 'already connected to input' 135 | ); 136 | 137 | assert ( 138 | dst.src === undefined, 139 | 'dst already connected to an output' 140 | ); 141 | 142 | assert ( 143 | this.numChans === dst.numChans || 144 | this.numChans === 1, 145 | 'mismatch in the channel count' 146 | ); 147 | 148 | //console.log('connecting'); 149 | 150 | this.dsts.push(dst); 151 | dst.src = this; 152 | } 153 | 154 | /** 155 | @class Synthesis node input 156 | */ 157 | function SynthInput(node, name, numChans) 158 | { 159 | assert ( 160 | node[name] === undefined, 161 | 'node already has property with this name' 162 | ); 163 | 164 | this.node = node; 165 | 166 | this.name = name; 167 | 168 | this.numChans = numChans; 169 | 170 | this.src = undefined; 171 | 172 | node[name] = this; 173 | } 174 | 175 | /** 176 | Test if data is available 177 | */ 178 | SynthInput.prototype.hasData = function () 179 | { 180 | if (this.src === undefined) 181 | return false; 182 | 183 | return this.src.hasData; 184 | } 185 | 186 | /** 187 | Get the buffer for a given channel 188 | */ 189 | SynthInput.prototype.getBuffer = function (chanIdx) 190 | { 191 | assert ( 192 | this.src instanceof SynthOutput, 193 | 'synth input not connected to any output' 194 | ); 195 | 196 | assert ( 197 | !(chanIdx === undefined && this.numChans > 1), 198 | 'channel idx must be specified when more than 1 channel' 199 | ); 200 | 201 | assert ( 202 | chanIdx < this.src.numChans || this.src.numChans === 1, 203 | 'invalid chan idx: ' + chanIdx 204 | ); 205 | 206 | // If the source has no data, return the zero buffer 207 | if (this.src.hasData === false) 208 | return SYNTH_ZERO_BUF; 209 | 210 | if (chanIdx === undefined) 211 | chanIdx = 0; 212 | 213 | if (chanIdx >= this.src.numChans) 214 | chanIdx = 0; 215 | 216 | return this.src.buffers[chanIdx]; 217 | } 218 | 219 | /** 220 | @class Synthesis network node 221 | */ 222 | function SynthNode() 223 | { 224 | /** 225 | Node name 226 | */ 227 | this.name = ''; 228 | } 229 | 230 | /** 231 | Process an event 232 | */ 233 | SynthNode.prototype.processEvent = function (evt, time) 234 | { 235 | // By default, do nothing 236 | } 237 | 238 | /** 239 | Update the outputs based on the inputs 240 | */ 241 | SynthNode.prototype.update = function (time, sampleRate) 242 | { 243 | // By default, do nothing 244 | } 245 | 246 | /** 247 | Audio synthesis network 248 | */ 249 | function SynthNet(sampleRate) 250 | { 251 | console.log('Creating synth network'); 252 | 253 | assert ( 254 | isPosInt(sampleRate), 255 | 'invalid sample rate' 256 | ); 257 | 258 | /** 259 | Sample rate 260 | */ 261 | this.sampleRate = sampleRate; 262 | 263 | /** 264 | List of nodes 265 | */ 266 | this.nodes = []; 267 | 268 | /** 269 | Output node 270 | */ 271 | this.outNode = null; 272 | 273 | /** 274 | Topological ordering of nodes 275 | */ 276 | this.order = undefined; 277 | } 278 | 279 | /** 280 | Add a node to the network 281 | */ 282 | SynthNet.prototype.addNode = function (node) 283 | { 284 | assert ( 285 | this.nodes.indexOf(node) === -1, 286 | 'node already in network' 287 | ); 288 | 289 | assert ( 290 | !(node instanceof OutNode && this.outNode !== null), 291 | 'output node already in network' 292 | ); 293 | 294 | if (node instanceof OutNode) 295 | this.outNode = node; 296 | 297 | // Add the node to the network 298 | this.nodes.push(node); 299 | 300 | // Invalidate any existing node ordering 301 | this.order = undefined; 302 | 303 | return node; 304 | } 305 | 306 | /** 307 | Produce a topological ordering of the nodes 308 | */ 309 | SynthNet.prototype.orderNodes = function () 310 | { 311 | console.log('Computing node ordering'); 312 | 313 | // Set of nodes with no outgoing edges 314 | var S = []; 315 | 316 | // List sorted in reverse topological order 317 | var L = []; 318 | 319 | // Total count of input edges 320 | var numEdges = 0; 321 | 322 | // For each graph node 323 | for (var i = 0; i < this.nodes.length; ++i) 324 | { 325 | var node = this.nodes[i]; 326 | 327 | //console.log('Graph node: ' + node.name); 328 | 329 | // List of input edges for this node 330 | node.inEdges = []; 331 | 332 | // Collect all inputs for this node 333 | for (k in node) 334 | { 335 | if (node[k] instanceof SynthInput) 336 | { 337 | var synthIn = node[k]; 338 | 339 | //console.log('Input port: ' + synthIn.name); 340 | //console.log(synthIn.src); 341 | 342 | if (synthIn.src instanceof SynthOutput) 343 | { 344 | //console.log(node.name + ': ' + synthIn.name); 345 | 346 | node.inEdges.push(synthIn.src); 347 | ++numEdges; 348 | } 349 | } 350 | } 351 | 352 | // If this node has no input edges, add it to S 353 | if (node.inEdges.length === 0) 354 | S.push(node); 355 | } 356 | 357 | console.log('Num edges: ' + numEdges); 358 | 359 | // While S not empty 360 | while (S.length > 0) 361 | { 362 | var node = S.pop(); 363 | 364 | console.log('Graph node: ' + node.name); 365 | 366 | L.push(node); 367 | 368 | // For each output port of this node 369 | for (k in node) 370 | { 371 | if (node[k] instanceof SynthOutput) 372 | { 373 | var synthOut = node[k]; 374 | 375 | // For each destination of this port 376 | for (var i = 0; i < synthOut.dsts.length; ++i) 377 | { 378 | var dstIn = synthOut.dsts[i]; 379 | var dstNode = dstIn.node; 380 | 381 | //console.log('dst: ' + dstNode.name); 382 | 383 | var idx = dstNode.inEdges.indexOf(synthOut); 384 | 385 | assert ( 386 | idx !== -1, 387 | 'input port not found' 388 | ); 389 | 390 | // Remove this edge 391 | dstNode.inEdges.splice(idx, 1); 392 | numEdges--; 393 | 394 | // If this node now has no input edges, add it to S 395 | if (dstNode.inEdges.length === 0) 396 | S.push(dstNode); 397 | } 398 | } 399 | } 400 | } 401 | 402 | assert ( 403 | numEdges === 0, 404 | 'cycle in graph' 405 | ); 406 | 407 | assert ( 408 | L.length === this.nodes.length, 409 | 'invalid ordering length' 410 | ); 411 | 412 | console.log('Ordering computed'); 413 | 414 | // Store the ordering 415 | this.order = L; 416 | } 417 | 418 | /** 419 | Generate audio for each output channel. 420 | @returns An array of audio samples (one per channel). 421 | */ 422 | SynthNet.prototype.genOutput = function (time) 423 | { 424 | assert ( 425 | this.order instanceof Array, 426 | 'node ordering not found' 427 | ); 428 | 429 | assert ( 430 | this.outNode instanceof SynthNode, 431 | 'genSample: output node not found' 432 | ); 433 | 434 | // For each node in the order 435 | for (var i = 0; i < this.order.length; ++i) 436 | { 437 | var node = this.order[i]; 438 | 439 | // Reset the outputs for this node 440 | for (k in node) 441 | if (node[k] instanceof SynthOutput) 442 | node[k].hasData = false; 443 | 444 | // Update this node 445 | node.update(time, this.sampleRate); 446 | } 447 | 448 | // Return the output node 449 | return this.outNode; 450 | } 451 | 452 | //============================================================================ 453 | // Output node 454 | //============================================================================ 455 | 456 | /** 457 | @class Output node 458 | @extends SynthNode 459 | */ 460 | function OutNode(numChans) 461 | { 462 | if (numChans === undefined) 463 | numChans = 2; 464 | 465 | /** 466 | Number of output channels 467 | */ 468 | this.numChans = numChans; 469 | 470 | // Audio input signal 471 | new SynthInput(this, 'signal', numChans); 472 | 473 | this.name = 'output'; 474 | } 475 | OutNode.prototype = new SynthNode(); 476 | 477 | /** 478 | Get the buffer for a given output channel 479 | */ 480 | OutNode.prototype.getBuffer = function (chanIdx) 481 | { 482 | return this.signal.getBuffer(chanIdx); 483 | } 484 | 485 | -------------------------------------------------------------------------------- /utility.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * 3 | * This file is part of the Melodique project. The project is 4 | * distributed at: 5 | * https://github.com/maximecb/Melodique 6 | * 7 | * Copyright (c) 2013, Maxime Chevalier-Boisvert. All rights reserved. 8 | * 9 | * This software is licensed under the following license (Modified BSD 10 | * License): 11 | * 12 | * Redistribution and use in source and binary forms, with or without 13 | * modification, are permitted provided that the following conditions are 14 | * met: 15 | * 1. Redistributions of source code must retain the above copyright 16 | * notice, this list of conditions and the following disclaimer. 17 | * 2. Redistributions in binary form must reproduce the above copyright 18 | * notice, this list of conditions and the following disclaimer in the 19 | * documentation and/or other materials provided with the distribution. 20 | * 3. The name of the author may not be used to endorse or promote 21 | * products derived from this software without specific prior written 22 | * permission. 23 | * 24 | * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED 25 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 26 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 27 | * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 28 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 29 | * NOT LIMITED TO PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 30 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 31 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 33 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | * 35 | *****************************************************************************/ 36 | 37 | //============================================================================ 38 | // Misc utility code 39 | //============================================================================ 40 | 41 | /** 42 | Assert that a condition holds true 43 | */ 44 | function assert(condition, errorText) 45 | { 46 | if (!condition) 47 | { 48 | error(errorText); 49 | } 50 | } 51 | 52 | /** 53 | Abort execution because a critical error occurred 54 | */ 55 | function error(errorText) 56 | { 57 | alert('ERROR: ' + errorText); 58 | 59 | throw errorText; 60 | } 61 | 62 | /** 63 | Test that a value is integer 64 | */ 65 | function isInt(val) 66 | { 67 | return ( 68 | Math.floor(val) === val 69 | ); 70 | } 71 | 72 | /** 73 | Test that a value is a nonnegative integer 74 | */ 75 | function isNonNegInt(val) 76 | { 77 | return ( 78 | isInt(val) && 79 | val >= 0 80 | ); 81 | } 82 | 83 | /** 84 | Test that a value is a strictly positive (nonzero) integer 85 | */ 86 | function isPosInt(val) 87 | { 88 | return ( 89 | isInt(val) && 90 | val > 0 91 | ); 92 | } 93 | 94 | /** 95 | Get the current time in seconds 96 | */ 97 | function getTimeSecs() 98 | { 99 | return (new Date()).getTime() / 1000; 100 | } 101 | 102 | /** 103 | Generate a random integer within [a, b] 104 | */ 105 | function randomInt(a, b) 106 | { 107 | assert ( 108 | isInt(a) && isInt(b) && a <= b, 109 | 'invalid params to randomInt' 110 | ); 111 | 112 | var range = b - a; 113 | 114 | var rnd = a + Math.floor(Math.random() * (range + 1)); 115 | 116 | return rnd; 117 | } 118 | 119 | /** 120 | Generate a random boolean 121 | */ 122 | function randomBool() 123 | { 124 | return (randomInt(0, 1) === 1); 125 | } 126 | 127 | /** 128 | Choose a random argument value uniformly randomly 129 | */ 130 | function randomChoice() 131 | { 132 | assert ( 133 | arguments.length > 0, 134 | 'must supply at least one possible choice' 135 | ); 136 | 137 | var idx = randomInt(0, arguments.length - 1); 138 | 139 | return arguments[idx]; 140 | } 141 | 142 | /** 143 | Generate a random floating-point number within [a, b] 144 | */ 145 | function randomFloat(a, b) 146 | { 147 | if (a === undefined) 148 | a = 0; 149 | if (b === undefined) 150 | b = 1; 151 | 152 | assert ( 153 | a <= b, 154 | 'invalid params to randomFloat' 155 | ); 156 | 157 | var range = b - a; 158 | 159 | var rnd = a + Math.random() * range; 160 | 161 | return rnd; 162 | } 163 | 164 | /** 165 | Generate a random value from a normal distribution 166 | */ 167 | function randomNorm(mean, variance) 168 | { 169 | // Declare variables for the points and radius 170 | var x1, x2, w; 171 | 172 | // Repeat until suitable points are found 173 | do 174 | { 175 | x1 = 2.0 * randomFloat() - 1.0; 176 | x2 = 2.0 * randomFloat() - 1.0; 177 | w = x1 * x1 + x2 * x2; 178 | } while (w >= 1.0 || w == 0); 179 | 180 | // compute the multiplier 181 | w = Math.sqrt((-2.0 * Math.log(w)) / w); 182 | 183 | // compute the gaussian-distributed value 184 | var gaussian = x1 * w; 185 | 186 | // Shift the gaussian value according to the mean and variance 187 | return (gaussian * variance) + mean; 188 | } 189 | 190 | /** 191 | Escape a string for valid HTML formatting 192 | */ 193 | function escapeHTML(str) 194 | { 195 | str = str.replace(/\n/g, '
'); 196 | str = str.replace(/ /g, ' '); 197 | str = str.replace(/\t/g, '    '); 198 | 199 | return str; 200 | } 201 | 202 | /** 203 | Find an element in the HTML DOM tree by its id 204 | */ 205 | function findElementById(id, elem) 206 | { 207 | if (elem === undefined) 208 | elem = document 209 | 210 | for (k in elem.childNodes) 211 | { 212 | var child = elem.childNodes[k]; 213 | 214 | if (child.attributes) 215 | { 216 | var childId = child.getAttribute('id'); 217 | 218 | if (childId == id) 219 | return child; 220 | } 221 | 222 | var nestedElem = findElementById(id, child); 223 | 224 | if (nestedElem) 225 | return nestedElem; 226 | } 227 | 228 | return null; 229 | } 230 | 231 | /** 232 | Encode an array of bytes into base64 string format 233 | */ 234 | function encodeBase64(data) 235 | { 236 | assert ( 237 | data instanceof Array, 238 | 'invalid data array' 239 | ); 240 | 241 | var str = ''; 242 | 243 | function encodeChar(bits) 244 | { 245 | //console.log(bits); 246 | 247 | var ch; 248 | 249 | if (bits < 26) 250 | ch = String.fromCharCode(65 + bits); 251 | else if (bits < 52) 252 | ch = String.fromCharCode(97 + (bits - 26)); 253 | else if (bits < 62) 254 | ch = String.fromCharCode(48 + (bits - 52)); 255 | else if (bits === 62) 256 | ch = '+'; 257 | else 258 | ch = '/'; 259 | 260 | str += ch; 261 | } 262 | 263 | for (var i = 0; i < data.length; i += 3) 264 | { 265 | var numRem = data.length - i; 266 | 267 | // 3 bytes -> 4 base64 chars 268 | var b0 = data[i]; 269 | var b1 = (numRem >= 2)? data[i+1]:0 270 | var b2 = (numRem >= 3)? data[i+2]:0 271 | 272 | var bits = (b0 << 16) + (b1 << 8) + b2; 273 | 274 | encodeChar((bits >> 18) & 0x3F); 275 | encodeChar((bits >> 12) & 0x3F); 276 | 277 | if (numRem >= 2) 278 | { 279 | encodeChar((bits >> 6) & 0x3F); 280 | 281 | if (numRem >= 3) 282 | encodeChar((bits >> 0) & 0x3F); 283 | else 284 | str += '='; 285 | } 286 | else 287 | { 288 | str += '=='; 289 | } 290 | } 291 | 292 | return str; 293 | } 294 | 295 | /** 296 | Resample and normalize an array of data points 297 | */ 298 | function resample(data, numSamples, outLow, outHigh, inLow, inHigh) 299 | { 300 | // Compute the number of data points per samples 301 | var ptsPerSample = data.length / numSamples; 302 | 303 | // Compute the number of samples 304 | var numSamples = Math.floor(data.length / ptsPerSample); 305 | 306 | // Allocate an array for the output samples 307 | var samples = new Array(numSamples); 308 | 309 | // Extract the samples 310 | for (var i = 0; i < numSamples; ++i) 311 | { 312 | samples[i] = 0; 313 | 314 | var startI = Math.floor(i * ptsPerSample); 315 | var endI = Math.min(Math.ceil((i+1) * ptsPerSample), data.length); 316 | var numPts = endI - startI; 317 | 318 | for (var j = startI; j < endI; ++j) 319 | samples[i] += data[j]; 320 | 321 | samples[i] /= numPts; 322 | } 323 | 324 | // If the input range is not specified 325 | if (inLow === undefined && inHigh === undefined) 326 | { 327 | // Min and max sample values 328 | var inLow = Infinity; 329 | var inHigh = -Infinity; 330 | 331 | // Compute the min and max sample values 332 | for (var i = 0; i < numSamples; ++i) 333 | { 334 | inLow = Math.min(inLow, samples[i]); 335 | inHigh = Math.max(inHigh, samples[i]); 336 | } 337 | } 338 | 339 | // Compute the input range 340 | var iRange = (inHigh > inLow)? (inHigh - inLow):1; 341 | 342 | // Compute the output range 343 | var oRange = outHigh - outLow; 344 | 345 | // Normalize the samples 346 | samples.forEach( 347 | function (v, i) 348 | { 349 | var normVal = (v - inLow) / iRange; 350 | samples[i] = outLow + (normVal * oRange); 351 | } 352 | ); 353 | 354 | // Return the normalized samples 355 | return samples; 356 | } 357 | 358 | -------------------------------------------------------------------------------- /vanalog.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * 3 | * This file is part of the Melodique project. The project is 4 | * distributed at: 5 | * https://github.com/maximecb/Melodique 6 | * 7 | * Copyright (c) 2013, Maxime Chevalier-Boisvert. All rights reserved. 8 | * 9 | * This software is licensed under the following license (Modified BSD 10 | * License): 11 | * 12 | * Redistribution and use in source and binary forms, with or without 13 | * modification, are permitted provided that the following conditions are 14 | * met: 15 | * 1. Redistributions of source code must retain the above copyright 16 | * notice, this list of conditions and the following disclaimer. 17 | * 2. Redistributions in binary form must reproduce the above copyright 18 | * notice, this list of conditions and the following disclaimer in the 19 | * documentation and/or other materials provided with the distribution. 20 | * 3. The name of the author may not be used to endorse or promote 21 | * products derived from this software without specific prior written 22 | * permission. 23 | * 24 | * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED 25 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 26 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 27 | * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 28 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 29 | * NOT LIMITED TO PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 30 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 31 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 33 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | * 35 | *****************************************************************************/ 36 | 37 | /** 38 | @class Simple virtual analog synthesizer 39 | @extends SynthNode 40 | */ 41 | function VAnalog(numOscs) 42 | { 43 | if (numOscs === undefined) 44 | numOscs = 1; 45 | 46 | /** 47 | Array of oscillator parameters 48 | */ 49 | this.oscs = new Array(numOscs); 50 | 51 | // Initialize the oscillator parameters 52 | for (var i = 0; i < numOscs; ++i) 53 | { 54 | var osc = this.oscs[i] = {}; 55 | 56 | // Oscillator type 57 | osc.type = 'sine'; 58 | 59 | // Duty cycle, for pulse wave 60 | osc.duty = 0.5; 61 | 62 | // Oscillator detuning, in cents 63 | osc.detune = 0; 64 | 65 | // ADSR amplitude envelope 66 | osc.env = new ADSREnv(0.05, 0.05, 0.2, 0.1); 67 | 68 | // Mixing volume 69 | osc.volume = 1; 70 | 71 | // Oscillator sync flag 72 | osc.sync = false; 73 | 74 | // Syncing oscillator detuning 75 | osc.syncDetune = 0; 76 | } 77 | 78 | /** 79 | Filter cutoff [0,1] 80 | */ 81 | this.cutoff = 1; 82 | 83 | /** 84 | Filter resonance [0,1] 85 | */ 86 | this.resonance = 0; 87 | 88 | /** 89 | Filter envelope 90 | */ 91 | this.filterEnv = new ADSREnv(0, 0, 1, Infinity); 92 | 93 | /** 94 | Filter envelope modulation amount 95 | */ 96 | this.filterEnvAmt = 1; 97 | 98 | /** 99 | Active/on note array 100 | */ 101 | this.actNotes = []; 102 | 103 | /** 104 | Temporary oscillator buffer, for intermediate processing 105 | */ 106 | this.oscBuf = new Float64Array(SYNTH_BUF_SIZE); 107 | 108 | /** 109 | Temporary note buffer, for intermediate processing 110 | */ 111 | this.noteBuf = new Float64Array(SYNTH_BUF_SIZE); 112 | 113 | // Sound output 114 | new SynthOutput(this, 'output'); 115 | } 116 | VAnalog.prototype = new SynthNode(); 117 | 118 | /** 119 | Process an event 120 | */ 121 | VAnalog.prototype.processEvent = function (evt, time) 122 | { 123 | // Note-on event 124 | if (evt instanceof NoteOnEvt) 125 | { 126 | // Get the note 127 | var note = evt.note; 128 | 129 | // Try to find the note among the active list 130 | var noteState = undefined; 131 | for (var i = 0; i < this.actNotes.length; ++i) 132 | { 133 | var state = this.actNotes[i]; 134 | 135 | if (state.note === note) 136 | { 137 | noteState = state; 138 | break; 139 | } 140 | } 141 | 142 | // If the note was not active before 143 | if (noteState === undefined) 144 | { 145 | noteState = {}; 146 | 147 | // Note being played 148 | noteState.note = note; 149 | 150 | // Time a note-on was received 151 | noteState.onTime = time; 152 | 153 | // Time a note-off was received 154 | noteState.offTime = 0; 155 | 156 | // Initialize the oscillator states 157 | noteState.oscs = new Array(this.oscs.length); 158 | for (var i = 0; i < this.oscs.length; ++i) 159 | { 160 | var oscState = {}; 161 | noteState.oscs[i] = oscState; 162 | 163 | // Cycle position 164 | oscState.cyclePos = 0; 165 | 166 | // Sync cycle position 167 | oscState.syncCyclePos = 0; 168 | 169 | // Envelope amplitude at note-on and note-off time 170 | oscState.onAmp = 0; 171 | oscState.offAmp = 0; 172 | } 173 | 174 | // Initialize the filter state values 175 | noteState.filterSt = new Array(8); 176 | for (var i = 0; i < noteState.filterSt.length; ++i) 177 | noteState.filterSt[i] = 0; 178 | 179 | // Filter envelope value at note-on and note-off time 180 | noteState.filterOnEnv = 0; 181 | noteState.filterOffEnv = 0; 182 | 183 | // Add the note to the active list 184 | this.actNotes.push(noteState); 185 | } 186 | 187 | // If the note was active before 188 | else 189 | { 190 | // Store the oscillator amplitudes at note-on time 191 | for (var i = 0; i < this.oscs.length; ++i) 192 | { 193 | var oscState = noteState.oscs[i]; 194 | 195 | oscState.onAmp = this.oscs[i].env.getValue( 196 | time, 197 | noteState.onTime, 198 | noteState.offTime, 199 | oscState.onAmp, 200 | oscState.offAmp 201 | ); 202 | 203 | //console.log('on amp: ' + oscState.onAmp); 204 | } 205 | 206 | // Filter envelope value at note-on time 207 | noteState.filterOnEnv = this.filterEnv.getValue( 208 | time, 209 | noteState.onTime, 210 | noteState.offTime, 211 | noteState.filterOnEnv, 212 | noteState.filterOffEnv 213 | ); 214 | 215 | // Set the on and off times 216 | noteState.onTime = time; 217 | noteState.offTime = 0; 218 | } 219 | 220 | //console.log('on time: ' + noteState.onTime); 221 | } 222 | 223 | // Note-off event 224 | else if (evt instanceof NoteOffEvt) 225 | { 226 | // Get the note 227 | var note = evt.note; 228 | 229 | // Try to find the note among the active list 230 | var noteState = undefined; 231 | for (var i = 0; i < this.actNotes.length; ++i) 232 | { 233 | var state = this.actNotes[i]; 234 | 235 | if (state.note === note) 236 | { 237 | noteState = state; 238 | break; 239 | } 240 | } 241 | 242 | // If the note is active 243 | if (noteState !== undefined) 244 | { 245 | // Store the oscillator amplitudes at note-off time 246 | for (var i = 0; i < this.oscs.length; ++i) 247 | { 248 | var oscState = noteState.oscs[i]; 249 | 250 | oscState.offAmp = this.oscs[i].env.getValue( 251 | time, 252 | noteState.onTime, 253 | noteState.offTime, 254 | oscState.onAmp, 255 | oscState.offAmp 256 | ); 257 | } 258 | 259 | // Filter envelope value at note-off time 260 | noteState.filterOffEnv = this.filterEnv.getValue( 261 | time, 262 | noteState.onTime, 263 | noteState.offTime, 264 | noteState.filterOnEnv, 265 | noteState.filterOffEnv 266 | ); 267 | 268 | // Set the note-off time 269 | noteState.offTime = time; 270 | } 271 | } 272 | 273 | // All notes off event 274 | else if (evt instanceof AllNotesOffEvt) 275 | { 276 | this.actNotes = []; 277 | } 278 | 279 | // By default, do nothing 280 | } 281 | 282 | /** 283 | Update the outputs based on the inputs 284 | */ 285 | VAnalog.prototype.update = function (time, sampleRate) 286 | { 287 | // If there are no active notes, do nothing 288 | if (this.actNotes.length === 0) 289 | return; 290 | 291 | // Get the output buffer 292 | var outBuf = this.output.getBuffer(0); 293 | 294 | // Initialize the output to 0 295 | for (var i = 0; i < outBuf.length; ++i) 296 | outBuf[i] = 0; 297 | 298 | // Get the time at the end of the buffer 299 | var endTime = time + ((outBuf.length - 1) / sampleRate); 300 | 301 | // For each active note 302 | for (var i = 0; i < this.actNotes.length; ++i) 303 | { 304 | var noteState = this.actNotes[i]; 305 | 306 | // Initialize the note buffer to 0 307 | for (var smpIdx = 0; smpIdx < outBuf.length; ++smpIdx) 308 | this.noteBuf[smpIdx] = 0; 309 | 310 | // Maximum end amplitude value 311 | var maxEndAmp = 0; 312 | 313 | // For each oscillator 314 | for (var oscNo = 0; oscNo < this.oscs.length; ++oscNo) 315 | { 316 | var oscParams = this.oscs[oscNo]; 317 | var oscState = noteState.oscs[oscNo]; 318 | 319 | // Generate the oscillator signal 320 | this.genOsc( 321 | this.oscBuf, 322 | oscParams, 323 | oscState, 324 | noteState.note, 325 | sampleRate 326 | ); 327 | 328 | // Get the amplitude value at the start of the buffer 329 | var ampStart = oscParams.volume * oscParams.env.getValue( 330 | time, 331 | noteState.onTime, 332 | noteState.offTime, 333 | oscState.onAmp, 334 | oscState.offAmp 335 | ); 336 | 337 | // Get the envelope value at the end of the buffer 338 | var ampEnd = oscParams.volume * oscParams.env.getValue( 339 | endTime, 340 | noteState.onTime, 341 | noteState.offTime, 342 | oscState.onAmp, 343 | oscState.offAmp 344 | ); 345 | 346 | //console.log('start time: ' + time); 347 | //console.log('start env: ' + envStart); 348 | //console.log('end: ' + envEnd); 349 | 350 | // Update the maximum end envelope value 351 | maxEndAmp = Math.max(maxEndAmp, ampEnd); 352 | 353 | // Modulate the output based on the amplitude envelope 354 | for (var smpIdx = 0; smpIdx < outBuf.length; ++smpIdx) 355 | { 356 | var ratio = (smpIdx / (outBuf.length - 1)); 357 | this.oscBuf[smpIdx] *= ampStart + ratio * (ampEnd - ampStart); 358 | } 359 | 360 | // Accumulate the sample values in the note buffer 361 | for (var smpIdx = 0; smpIdx < outBuf.length; ++smpIdx) 362 | this.noteBuf[smpIdx] += this.oscBuf[smpIdx]; 363 | } 364 | 365 | // Apply the filter to the temp buffer 366 | this.applyFilter(time, noteState, this.noteBuf); 367 | 368 | // Accumulate the sample values in the output buffer 369 | for (var smpIdx = 0; smpIdx < outBuf.length; ++smpIdx) 370 | outBuf[smpIdx] += this.noteBuf[smpIdx]; 371 | 372 | // If all envelopes have fallen to 0, remove the note from the active list 373 | if (maxEndAmp === 0) 374 | { 375 | this.actNotes.splice(i, 1); 376 | i--; 377 | } 378 | } 379 | } 380 | 381 | /** 382 | Generate output for an oscillator and update its position 383 | */ 384 | VAnalog.prototype.genOsc = function (outBuf, oscParams, oscState, note, sampleRate) 385 | { 386 | // Get the oscillator frequency 387 | var freq = note.getFreq(oscParams.detune); 388 | 389 | // Get the initial cycle position 390 | var cyclePos = oscState.cyclePos; 391 | 392 | // Compute the cycle position change between samples 393 | var deltaPos = freq / sampleRate; 394 | 395 | // Get the sync oscillator frequency 396 | var syncFreq = note.getFreq(oscParams.syncDetune); 397 | 398 | // Get the initial sync cycle position 399 | var syncCyclePos = oscState.syncCyclePos; 400 | 401 | // Compute the cycle position change between samples 402 | var syncDeltaPos = syncFreq / sampleRate; 403 | 404 | // For each sample to be produced 405 | for (var i = 0; i < outBuf.length; ++i) 406 | { 407 | // Switch on the oscillator type/waveform 408 | switch (oscParams.type) 409 | { 410 | // Sine wave 411 | case 'sine': 412 | outBuf[i] = Math.sin(2 * Math.PI * cyclePos); 413 | break; 414 | 415 | // Triangle wave 416 | case 'triangle': 417 | if (cyclePos < 0.5) 418 | outBuf[i] = (4 * cyclePos) - 1; 419 | else 420 | outBuf[i] = 1 - (4 * (cyclePos - 0.5)); 421 | break; 422 | 423 | // Sawtooth wave 424 | case 'sawtooth': 425 | outBuf[i] = -1 + (2 * cyclePos); 426 | break; 427 | 428 | // Pulse wave 429 | case 'pulse': 430 | if (cyclePos < oscParams.duty) 431 | outBuf[i] = -1; 432 | else 433 | outBuf[i] = 1; 434 | break; 435 | 436 | // Noise 437 | case 'noise': 438 | outBuf[i] = 1 - 2 * Math.random(); 439 | break; 440 | 441 | default: 442 | error('invalid waveform: ' + oscParams.type); 443 | } 444 | 445 | cyclePos += deltaPos; 446 | 447 | if (cyclePos > 1) 448 | cyclePos -= 1; 449 | 450 | if (oscParams.sync === true) 451 | { 452 | syncCyclePos += syncDeltaPos; 453 | 454 | if (syncCyclePos > 1) 455 | { 456 | syncCyclePos -= 1; 457 | cyclePos = 0; 458 | } 459 | } 460 | } 461 | 462 | // Set the final cycle position 463 | oscState.cyclePos = cyclePos; 464 | 465 | // Set the final sync cycle position 466 | oscState.syncCyclePos = syncCyclePos; 467 | } 468 | 469 | /** 470 | Apply a filter to a buffer of samples 471 | IIR, 2-pole, resonant Low Pass Filter (LPF) 472 | */ 473 | VAnalog.prototype.applyFilter = function (time, noteState, buffer) 474 | { 475 | assert ( 476 | this.cutoff >= 0 && this.cutoff <= 1, 477 | 'invalid filter cutoff' 478 | ); 479 | 480 | assert ( 481 | this.resonance >= 0 && this.resonance <= 1, 482 | 'invalid filter resonance' 483 | ); 484 | 485 | var filterEnvVal = this.filterEnv.getValue( 486 | time, 487 | noteState.onTime, 488 | noteState.offTime, 489 | noteState.filterOnEnv, 490 | noteState.filterOffEnv 491 | ); 492 | 493 | var filterEnvMag = (1 - this.cutoff) * this.filterEnvAmt; 494 | 495 | var cutoff = this.cutoff + filterEnvMag * filterEnvVal; 496 | 497 | var c = Math.pow(0.5, (1 - cutoff) / 0.125); 498 | var r = Math.pow(0.5, (this.resonance + 0.125) / 0.125); 499 | 500 | var mrc = 1 - r * c; 501 | 502 | var v0 = noteState.filterSt[0]; 503 | var v1 = noteState.filterSt[1]; 504 | 505 | for (var i = 0; i < buffer.length; ++i) 506 | { 507 | v0 = (mrc * v0) - (c * v1) + (c * buffer[i]); 508 | v1 = (mrc * v1) + (c * v0); 509 | 510 | buffer[i] = v1; 511 | } 512 | 513 | noteState.filterSt[0] = v0; 514 | noteState.filterSt[1] = v1; 515 | } 516 | 517 | --------------------------------------------------------------------------------