├── 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 |
23 |
24 |
25 |
26 | Melodique: Algorithmic Melodic Phrase Generator
27 |
28 |
29 |
30 |
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 |
103 | Copyright © 2012 Maxime Chevalier-Boisvert
104 |
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 |
--------------------------------------------------------------------------------