├── README.md
├── TODO.txt
├── chrome.sh
├── envelope.js
├── graph.js
├── images
└── forkme.png
├── index.html
├── machine.js
├── main.js
├── mixer.js
├── music.js
├── piece.js
├── publish.sh
├── samples
├── biab_trance_clap_2.wav
├── biab_trance_hat_6.wav
├── biab_trance_kick_4.wav
├── biab_trance_snare_2.wav
├── closed_hat.wav
└── one-two-three-four.wav
├── sampling.js
├── stylesheet.css
├── utils-html.js
├── utils-misc.js
└── vanalog.js
/README.md:
--------------------------------------------------------------------------------
1 | Turing-Tunes
2 | ============
3 |
4 | Turing Tunes is a procedural music generation engine that uses randomly
5 | generated Turing machines to produce streams of musical notes in real-time.
6 | Requires a browser with Web Audio API support such as Chrome. This project
7 | is distributed under a modified BSD license.
8 |
9 | You can try Turing Tunes at the following URL:
10 | [http://maximecb.github.io/Turing-Tunes/](http://maximecb.github.io/Turing-Tunes/)
11 |
12 | Below is a sample of the music Turing Tunes can generate:
13 |
14 | ***List to be populated***
15 |
16 | If running locally in Chrome, run with "--allow-file-access-from-files" option.
17 |
--------------------------------------------------------------------------------
/TODO.txt:
--------------------------------------------------------------------------------
1 | Turing-Tunes
2 | ------------
3 |
4 | TODO: canvas visualization, streaming piano roll
5 | - Second roll for drum samples?
6 |
7 | TODO: mutate button
8 |
9 | TODO: back button
10 | - history list
11 |
12 | TODO: show iteration count, memory position?
13 |
14 |
15 |
16 |
17 | [DONE] TODO: publish on github
18 |
19 | [DONE] TODO: shareable URL encoding
20 | - Encoding needs to specify notes used, note lengths for output symbols
21 |
22 | [DONE] TODO: create git repo, readme file
23 |
24 | [DONE] TODO: generate notes with drum samples
25 | - Must have null note, pause
26 |
27 | [DONE] TODO: drum samples, form generation with list
28 |
29 | [DONE] TODO: fix synth glitches, improve patch
30 |
31 | [DONE] TODO: basic filtering, log filtering results
32 | - Run machine for many iterations (10K?). If no output, or less than 10 symbols, filter out.
33 | - Compute Shannon entropy of output stream
34 | - Loop detection? Look for loops in notes generated
35 | - Repeats of current note
36 |
37 | [DONE] TODO: name validation
38 |
39 | [DONE] TODO: generate notes on track as needed, based on current playback time
40 | - Never actually needs to terminate!
41 |
42 | [DONE] TODO: note generation function?
43 |
44 | [DONE] TODO: generate notes from root note, scale type, octaves
45 | - Log number of notes generated
46 |
47 | [DONE] TODO: fix midi note numbers... don't want to have octave -1
48 |
49 | [DONE] TODO: octave selection instead of octaves covered
50 |
51 | [DONE] TODO: improve gen URL feature, add button to force naming
52 |
53 | [DONE] TODO: add num octaves to form
54 |
55 | [DONE] TODO: setup synth, audio generation code
56 |
57 | [DONE] TODO: shareable URL on page layout
58 | - Name your piece
59 |
60 | [DONE] TODO: fill in form options
61 | - make defaults selected
62 |
63 | [DONE] TODO: revise intro paragraph, see Turing-Drawings
64 |
65 | [DONE] TODO: basic form layout, generative options
66 | - Musical scale (default pentatonic)
67 | - Possible notes (checkboxes)
68 | - choosing a musical scale resets this
69 | - Number of octaves (default one)
70 | - Note lengths (default 1/4 or 1/8 only)
71 |
72 | [DONE] TODO: Machine class
73 | - One memory tape, large size, loops over
74 | - e.g.: 64K or 256K cells
75 |
76 | [DONE] TODO: import music code
77 |
78 | -------------------------------------------------------------------------
79 |
80 | Music tape:
81 | - Symbols are musical notes or blanks
82 | - Never appear on the left-side of a transition rule
83 | - Can have multiple note or blank lengths
84 | - Can never erase, action is either write (W) or noop (N)
85 |
86 | Working tape:
87 | - Working memory for the system
88 | - Left and Right actions (L/R)
89 |
90 | N states
91 | K mem symbols
92 | S output symbols
93 | 2 mem actions
94 | 2 output actions
95 |
96 | N x K -> N x K x S x 2 x 2
97 | - 8 states x 8 mem symbols means 64 table entries of 5 values each
98 | - If output action is N, keep
99 |
100 | def entropy_ideal(length):
101 | "Calculates the ideal Shannon entropy of a string with given length"
102 | prob = 1.0 / length
103 | return -1.0 * length * prob * math.log(prob) / math.log(2.0)
104 |
105 |
--------------------------------------------------------------------------------
/chrome.sh:
--------------------------------------------------------------------------------
1 | google-chrome --allow-file-access-from-files index.html
2 |
--------------------------------------------------------------------------------
/envelope.js:
--------------------------------------------------------------------------------
1 | /*****************************************************************************
2 | *
3 | * This file is part of the Turing-Tunes project. The project is
4 | * distributed at:
5 | * https://github.com/maximecb/Turing-Tunes
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 |
--------------------------------------------------------------------------------
/graph.js:
--------------------------------------------------------------------------------
1 | /*****************************************************************************
2 | *
3 | * This file is part of the Turing-Tunes project. The project is
4 | * distributed at:
5 | * https://github.com/maximecb/Turing-Tunes
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 | // Audio graph core
39 | //============================================================================
40 |
41 | /**
42 | Buffer size used by the audio graph
43 | */
44 | var AUDIO_BUF_SIZE = 256;
45 |
46 | /**
47 | Buffer containing only zero data
48 | */
49 | var AUDIO_ZERO_BUF = new Float64Array(AUDIO_BUF_SIZE);
50 |
51 | /**
52 | @class Audio node output
53 | */
54 | function AudioOutput(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 audio 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(AUDIO_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 | AudioOutput.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 an audio input
124 | */
125 | AudioOutput.prototype.connect = function (dst)
126 | {
127 | assert (
128 | dst instanceof AudioInput,
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 Audio node input
156 | */
157 | function AudioInput(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 | AudioInput.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 | AudioInput.prototype.getBuffer = function (chanIdx)
190 | {
191 | assert (
192 | this.src instanceof AudioOutput,
193 | 'audio 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 AUDIO_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 Audio graph node
221 | */
222 | function AudioNode()
223 | {
224 | /**
225 | Node name
226 | */
227 | this.name = '';
228 | }
229 |
230 | /**
231 | Process an event
232 | */
233 | AudioNode.prototype.processEvent = function (evt, time)
234 | {
235 | // By default, do nothing
236 | }
237 |
238 | /**
239 | Update the outputs based on the inputs
240 | */
241 | AudioNode.prototype.update = function (time, sampleRate)
242 | {
243 | // By default, do nothing
244 | }
245 |
246 | /**
247 | Audio synthesis graph
248 | */
249 | function AudioGraph(sampleRate)
250 | {
251 | console.log('Creating audio graph');
252 |
253 | assert (
254 | isPosInt(sampleRate),
255 | 'invalid sample rate'
256 | );
257 |
258 | /**
259 | Sample rate
260 | */
261 | this.sampleRate = sampleRate;
262 |
263 | /**
264 | Output node
265 | */
266 | this.outNode = null;
267 |
268 | /**
269 | Topological ordering of nodes
270 | */
271 | this.order = undefined;
272 | }
273 |
274 | /**
275 | Set the output node for the graph
276 | */
277 | AudioGraph.prototype.setOutNode = function (node)
278 | {
279 | assert (
280 | !(node instanceof OutNode && this.outNode !== null),
281 | 'output node already set'
282 | );
283 |
284 | this.outNode = node;
285 |
286 | return node;
287 | }
288 |
289 | /**
290 | Produce a topological ordering of the nodes
291 | */
292 | AudioGraph.prototype.orderNodes = function ()
293 | {
294 | console.log('Computing node ordering');
295 |
296 | // Set of nodes with no outgoing edges
297 | var S = [];
298 |
299 | // List sorted in reverse topological order
300 | var L = [];
301 |
302 | // Total count of input edges
303 | var numEdges = 0;
304 |
305 | var visited = [];
306 |
307 | function visit(node)
308 | {
309 | // If this node was visited, stop
310 | if (visited.indexOf(node) !== -1)
311 | return;
312 |
313 | visited.push(node);
314 |
315 | // List of input edges for this node
316 | node.inEdges = [];
317 |
318 | // Collect all inputs for this node
319 | for (k in node)
320 | {
321 | // If this is an input
322 | if (node[k] instanceof AudioInput)
323 | {
324 | var audioIn = node[k];
325 |
326 | // If this input is connected
327 | if (audioIn.src instanceof AudioOutput)
328 | {
329 | //console.log(node.name + ': ' + audioIn.name);
330 |
331 | node.inEdges.push(audioIn.src);
332 | ++numEdges;
333 |
334 | // Visit the node for this input
335 | visit(audioIn.src.node);
336 | }
337 | }
338 | }
339 |
340 | // If this node has no input edges, add it to S
341 | if (node.inEdges.length === 0)
342 | S.push(node);
343 | }
344 |
345 | // Visit nodes starting from the output node
346 | visit(this.outNode);
347 |
348 | console.log('Num edges: ' + numEdges);
349 | console.log('Num nodes: ' + visited.length);
350 |
351 | // While S not empty
352 | while (S.length > 0)
353 | {
354 | var node = S.pop();
355 |
356 | console.log('Graph node: ' + node.name);
357 |
358 | L.push(node);
359 |
360 | // For each output port of this node
361 | for (k in node)
362 | {
363 | if (node[k] instanceof AudioOutput)
364 | {
365 | var audioOut = node[k];
366 |
367 | // For each destination of this port
368 | for (var i = 0; i < audioOut.dsts.length; ++i)
369 | {
370 | var dstIn = audioOut.dsts[i];
371 | var dstNode = dstIn.node;
372 |
373 | //console.log('dst: ' + dstNode.name);
374 |
375 | var idx = dstNode.inEdges.indexOf(audioOut);
376 |
377 | assert (
378 | idx !== -1,
379 | 'input port not found'
380 | );
381 |
382 | // Remove this edge
383 | dstNode.inEdges.splice(idx, 1);
384 | numEdges--;
385 |
386 | // If this node now has no input edges, add it to S
387 | if (dstNode.inEdges.length === 0)
388 | S.push(dstNode);
389 | }
390 | }
391 | }
392 | }
393 |
394 | assert (
395 | numEdges === 0,
396 | 'cycle in graph'
397 | );
398 |
399 | assert (
400 | L.length >= 1,
401 | 'invalid node ordering'
402 | );
403 |
404 | console.log('Ordering computed');
405 |
406 | // Store the ordering
407 | this.order = L;
408 | }
409 |
410 | /**
411 | Generate audio for each output channel.
412 | @returns An array of audio samples (one per channel).
413 | */
414 | AudioGraph.prototype.genOutput = function (time)
415 | {
416 | assert (
417 | this.order instanceof Array,
418 | 'node ordering not found'
419 | );
420 |
421 | assert (
422 | this.outNode instanceof AudioNode,
423 | 'genSample: output node not found'
424 | );
425 |
426 | // For each node in the order
427 | for (var i = 0; i < this.order.length; ++i)
428 | {
429 | var node = this.order[i];
430 |
431 | // Reset the outputs for this node
432 | for (k in node)
433 | if (node[k] instanceof AudioOutput)
434 | node[k].hasData = false;
435 |
436 | // Update this node
437 | node.update(time, this.sampleRate);
438 | }
439 |
440 | // Return the output node
441 | return this.outNode;
442 | }
443 |
444 | //============================================================================
445 | // Output node
446 | //============================================================================
447 |
448 | /**
449 | @class Output node
450 | @extends AudioNode
451 | */
452 | function OutNode(numChans)
453 | {
454 | if (numChans === undefined)
455 | numChans = 2;
456 |
457 | /**
458 | Number of output channels
459 | */
460 | this.numChans = numChans;
461 |
462 | // Audio input signal
463 | new AudioInput(this, 'signal', numChans);
464 |
465 | this.name = 'output';
466 | }
467 | OutNode.prototype = new AudioNode();
468 |
469 | /**
470 | Get the buffer for a given output channel
471 | */
472 | OutNode.prototype.getBuffer = function (chanIdx)
473 | {
474 | return this.signal.getBuffer(chanIdx);
475 | }
476 |
477 |
--------------------------------------------------------------------------------
/images/forkme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maximecb/Turing-Tunes/f1cca5220e2fa88bb09693b55eb3f554e0d5c4bc/images/forkme.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Turing Tunes
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
32 |
33 | Click "generate" to produce a new random machine/tune.
34 |
35 |
36 |
37 |
100 |
101 |
102 |
103 | Your browser does not support the canvas element.
104 |
105 |
106 |
107 |
117 |
118 |
119 | Turing Tunes uses randomly generated Turing machines to produce
120 | sequences of musical notes, as a form of generative art. The musical
121 | pieces generated are potentially infinite in length. You can create
122 | your own pieces by pressing the "Generate" button above. The controls at
123 | the top of the page can be used to adjust various parameters affecting
124 | the way in which the music is generated. Don't know anything about
125 | music theory? Don't worry: sensible defaults are provided.
126 | Should you find a tune you like, and would like to share it online,
127 | you can do so by copying the custom shareable URL above.
128 |
129 |
130 |
131 |
132 | Copyright © 2013 Maxime Chevalier-Boisvert
133 |
134 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/machine.js:
--------------------------------------------------------------------------------
1 | /*****************************************************************************
2 | *
3 | * This file is part of the Turing-Tunes project. The project is
4 | * distributed at:
5 | * https://github.com/maximecb/Turing-Tunes
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 | // Memory actions
38 | var ACTION_LEFT = 0;
39 | var ACTION_RIGHT = 1;
40 |
41 | // Output actions
42 | var OUT_WRITE = 0;
43 | var OUT_NOOP = 1;
44 |
45 | /*
46 | N states (one start state)
47 | K memory symbols
48 | S output symbols
49 | 2 memory actions
50 | 2 output actions
51 |
52 | N x K -> N x K x S x 2 x 2
53 | */
54 | function Machine(numStates, numSymbols, outSymbols, memSize)
55 | {
56 | assert (
57 | numStates >= 1,
58 | 'must have at least 1 state'
59 | );
60 |
61 | assert (
62 | numSymbols >= 1,
63 | 'must have at least 1 memory symbol'
64 | );
65 |
66 | assert (
67 | outSymbols.length >= 1,
68 | 'must have at least 1 output symbol'
69 | );
70 |
71 | assert (
72 | memSize >= 1,
73 | 'must have at least 1 memory cell'
74 | );
75 |
76 | /// Number of states
77 | this.numStates = numStates;
78 |
79 | /// Number of memory symbols
80 | this.numSymbols = numSymbols;
81 |
82 | /// Number of outputs
83 | this.outSymbols = outSymbols;
84 |
85 | /// Transition table
86 | this.table = new Int32Array(numStates * numSymbols * 5);
87 |
88 | /// Memory tape
89 | this.memory = new Uint16Array(memSize);
90 |
91 | // Generate random transitions
92 | for (var st = 0; st < numStates; ++st)
93 | {
94 | for (var sy = 0; sy < numSymbols; ++sy)
95 | {
96 | var idx = this.getTransIdx(st, sy);
97 | this.table[idx + 0] = randomInt(0, numStates - 1); // New state
98 | this.table[idx + 1] = randomInt(0, numSymbols - 1); // Mem symbol
99 | this.table[idx + 2] = randomInt(0, outSymbols.length - 1); // Out symbol
100 | this.table[idx + 3] = randomInt(0, 1); // Mem action
101 | this.table[idx + 4] = randomInt(0, 1); // Out action
102 | }
103 | }
104 |
105 | // Initialize the state
106 | this.reset();
107 | }
108 |
109 | Machine.prototype.getTransIdx = function (st, sy)
110 | {
111 | return (this.numStates * sy + st) * 5;
112 | }
113 |
114 | Machine.prototype.reset = function ()
115 | {
116 | /// Start state
117 | this.state = 0;
118 |
119 | /// Memory position
120 | this.memPos = 0;
121 |
122 | // Initialize the memory tape
123 | for (var i = 0; i < this.memory.length; ++i)
124 | this.memory[i] = 0;
125 |
126 | /// Iteration count
127 | this.itrCount = 0;
128 | }
129 |
130 | Machine.prototype.toString = function ()
131 | {
132 | var str = '';
133 |
134 | str += this.outSymbols.length;
135 | for (var i = 0; i < this.outSymbols.length; ++i)
136 | {
137 | var sym = this.outSymbols[i];
138 | str += ',' + sym.note;
139 | str += ',' + sym.frac;
140 | str += ',' + ((sym.drumNote !== null)? sym.drumNote:'');
141 | }
142 | str += ',';
143 |
144 | str += this.numStates + ',' + this.numSymbols + ',' + this.memory.length;
145 | for (var i = 0; i < this.table.length; ++i)
146 | {
147 | str += ',' + this.table[i];
148 | }
149 |
150 | //print(str);
151 |
152 | return str;
153 | }
154 |
155 | Machine.fromString = function (str)
156 | {
157 | function extract()
158 | {
159 | var subStr = str.split(',', 1)[0];
160 | str = str.substr(subStr.length+1);
161 | return subStr;
162 | }
163 |
164 | print('str: ' + str);
165 |
166 | var numSymbols = parseInt(extract());
167 |
168 | print('numSymbols: ' + numSymbols);
169 |
170 | var outSymbols = new Array(numSymbols);
171 | for (var i = 0; i < outSymbols.length; ++i)
172 | {
173 | var note = extract();
174 | var frac = extract();
175 | var drumNote = extract();
176 |
177 | //print(i + ' / ' + outSymbols.length);
178 |
179 | outSymbols[i] = {
180 | note: Note(note),
181 | frac: parseFloat(frac),
182 | drumNote: drumNote? parseInt(drumNote):null
183 | };
184 |
185 | print(outSymbols[i].frac);
186 | }
187 |
188 | var numStates = parseInt(extract());
189 | var numSymbols = parseInt(extract());
190 | var memSize = parseInt(extract());
191 |
192 | var machine = new Machine(
193 | numStates,
194 | numSymbols,
195 | outSymbols,
196 | memSize
197 | );
198 |
199 | for (var i = 0; i < machine.table.length; ++i)
200 | machine.table[i] = parseInt(extract());
201 |
202 | return machine;
203 | }
204 |
205 | /**
206 | Perform one update iteration
207 | */
208 | Machine.prototype.iterate = function()
209 | {
210 | var idx = this.getTransIdx(this.state, this.memory[this.memPos]);
211 | var st = this.table[idx + 0];
212 | var ms = this.table[idx + 1];
213 | var os = this.table[idx + 2];
214 | var ma = this.table[idx + 3];
215 | var oa = this.table[idx + 4];
216 |
217 | // Update the current state
218 | this.state = st;
219 |
220 | // Write the new symbol to the memory tape
221 | this.memory[this.memPos] = ms;
222 |
223 | assert (
224 | this.state >= 0 && this.state < this.numStates,
225 | 'invalid state'
226 | );
227 |
228 | assert (
229 | os >= 0 && os < this.outSymbols.length,
230 | 'invalid output symbol'
231 | );
232 |
233 | // Perform the memory action
234 | switch (ma)
235 | {
236 | case ACTION_LEFT:
237 | this.memPos += 1;
238 | if (this.memPos >= this.memory.length)
239 | this.memPos -= this.memory.length;
240 | break;
241 |
242 | case ACTION_RIGHT:
243 | this.memPos -= 1;
244 | if (this.memPos < 0)
245 | this.memPos += this.memory.length;
246 | break;
247 |
248 | default:
249 | error('invalid memory action');
250 | }
251 |
252 | assert (
253 | this.memPos >= 0 && this.memPos < this.memory.length,
254 | 'invalid memory position'
255 | );
256 |
257 | var output;
258 |
259 | // Perform the output action
260 | switch (oa)
261 | {
262 | case OUT_WRITE:
263 | output = this.outSymbols[os];
264 | break;
265 |
266 | case OUT_NOOP:
267 | output = null;
268 | break;
269 |
270 | default:
271 | error('invalid output action');
272 | }
273 |
274 | ++this.itrCount;
275 |
276 | return output;
277 | }
278 |
279 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | /*****************************************************************************
2 | *
3 | * This file is part of the Turing-Tunes project. The project is
4 | * distributed at:
5 | * https://github.com/maximecb/Turing-Tunes
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 | Called after page load to initialize needed resources
39 | */
40 | function init()
41 | {
42 | // Initialize the form options
43 | initForm();
44 |
45 | // Get a reference to the canvas
46 | canvas = document.getElementById("canvas");
47 |
48 | // Get a 2D context for the drawing canvas
49 | canvas.ctx = canvas.getContext("2d");
50 |
51 | // Initialize the audio subsystem
52 | initAudio();
53 |
54 | // If a location hash is specified
55 | if (location.hash !== '')
56 | {
57 | console.log('parsing machine from hash string');
58 |
59 | // Extract the machine name
60 | var str = location.hash.substr(1);
61 |
62 | print(str);
63 |
64 | var args = str.split(',', 4);
65 | var name = args[0];
66 | piece.beatsPerMin = args[1];
67 | piece.beatsPerBar = args[2];
68 | piece.noteVal = args[3];
69 | str = str.substr(args.toString().length+1);
70 |
71 | setTitle(name);
72 |
73 | machine = Machine.fromString(str);
74 |
75 | playAudio();
76 | }
77 | }
78 | window.addEventListener("load", init, false);
79 |
80 | /// Drum samples. These will get mapped to the note number
81 | /// corresponding to their position in the list
82 | var DRUM_SAMPLES = [
83 | { name: 'kick' , path: 'samples/biab_trance_kick_4.wav' , vol: 3 },
84 | { name: 'snare' , path: 'samples/biab_trance_snare_2.wav' , vol: 1.5 },
85 | { name: 'hat 1' , path: 'samples/biab_trance_hat_6.wav' , vol: 1.5 },
86 | { name: 'hat 2' , path: 'samples/closed_hat.wav' , vol: 1.5 },
87 | { name: 'clap' , path: 'samples/biab_trance_clap_2.wav' , vol: 2 },
88 | ];
89 |
90 | /// Maximum number of machine iterations to produce one note
91 | var ITRS_PER_NOTE = 2000;
92 |
93 | /// Web audio context
94 | var audioCtx = undefined;
95 |
96 | /// JS audio generation node
97 | var jsAudioNode = undefined;
98 |
99 | /// Audio generation event handler
100 | var genAudio = undefined;
101 |
102 | var drawInterv = undefined;
103 |
104 | var piece = undefined;
105 |
106 | var leadTrack = undefined;
107 |
108 | var drumTrack = undefined;
109 |
110 | var machine = undefined;
111 |
112 | /**
113 | Initialize the form options
114 | */
115 | function initForm()
116 | {
117 | var numStates = document.getElementById('numStates');
118 | for (var i = 5; i <= 25; ++i)
119 | {
120 | var opt = document.createElement("option");
121 | opt.text = String(i);
122 | opt.value = String(i);
123 | numStates.appendChild(opt);
124 |
125 | if (i === 10)
126 | opt.selected = true;
127 | }
128 |
129 | var numSymbols = document.getElementById('numSymbols');
130 | for (var i = 5; i <= 25; ++i)
131 | {
132 | var opt = document.createElement("option");
133 | opt.text = String(i);
134 | opt.value = String(i);
135 | numSymbols.appendChild(opt);
136 |
137 | if (i === 10)
138 | opt.selected = true;
139 | }
140 |
141 | var scaleRoot = document.getElementById('scaleRoot');
142 | for (var name in NOTE_NAME_PC)
143 | {
144 | var opt = document.createElement("option");
145 | opt.text = name;
146 | opt.value = name;
147 | scaleRoot.appendChild(opt);
148 |
149 | if (name === 'C')
150 | opt.selected = true;
151 | }
152 |
153 | var scaleType = document.getElementById('scaleType');
154 | for (var scale in scaleIntervs)
155 | {
156 | var opt = document.createElement("option");
157 | opt.text = scale;
158 | opt.value = scale;
159 | scaleType.appendChild(opt);
160 |
161 | if (scale == 'blues')
162 | opt.selected = true;
163 | }
164 |
165 | var drumSamples = document.getElementById('drumSamples');
166 | for (var i = 0; i < DRUM_SAMPLES.length; ++i)
167 | {
168 | var sample = DRUM_SAMPLES[i];
169 |
170 | var text = document.createTextNode(capitalize(sample.name));
171 | drumSamples.appendChild(text);
172 |
173 | var input = document.createElement("input");
174 | input.type = 'checkbox';
175 | input.value = String(i);
176 | drumSamples.appendChild(input);
177 |
178 | if (sample.name === 'kick')
179 | input.checked = true;
180 | }
181 | }
182 |
183 | /**
184 | Initialize the audio subsystem
185 | */
186 | function initAudio()
187 | {
188 | // Create an audio context
189 | if (this.hasOwnProperty('AudioContext') === true)
190 | {
191 | //console.log('Audio context found');
192 | audioCtx = new AudioContext();
193 | }
194 | else if (this.hasOwnProperty('webkitAudioContext') === true)
195 | {
196 | //console.log('WebKit audio context found');
197 | audioCtx = new webkitAudioContext();
198 | }
199 | else
200 | {
201 | audioCtx = undefined;
202 | }
203 |
204 | // If no audio context was created
205 | if (audioCtx === undefined)
206 | {
207 | error(
208 | 'No Web Audio API support. Sound will be disabled. ' +
209 | 'Try this page in the latest version of Chrome'
210 | );
211 | }
212 |
213 | // Get the sample rate for the audio context
214 | var sampleRate = audioCtx.sampleRate;
215 |
216 | // Create the audio graph
217 | var graph = new AudioGraph(sampleRate);
218 |
219 | // Create a stereo sound output node
220 | var outNode = new OutNode(2);
221 | graph.setOutNode(outNode);
222 |
223 | // Create the piece
224 | piece = new Piece(graph);
225 |
226 | // Lead patch
227 | var lead = new VAnalog(2);
228 | lead.name = 'lead';
229 | lead.oscs[0].type = 'pulse';
230 | lead.oscs[0].duty = 0.5;
231 | lead.oscs[0].detune = -1195;
232 | lead.oscs[0].volume = 1;
233 | lead.oscs[0].env.a = 0;
234 | lead.oscs[0].env.d = 0.1;
235 | lead.oscs[0].env.s = 0.1;
236 | lead.oscs[0].env.r = 0.1;
237 |
238 | lead.oscs[1].type = 'pulse';
239 | lead.oscs[1].duty = 0.5;
240 | lead.oscs[1].detune = -1205;
241 | lead.oscs[1].volume = 1;
242 | lead.oscs[1].env = lead.oscs[0].env;
243 |
244 | lead.cutoff = 0.3;
245 | lead.resonance = 0;
246 | lead.filterEnv.a = 0;
247 | lead.filterEnv.d = 0.2;
248 | lead.filterEnv.s = 0.0;
249 | lead.filterEnv.r = 0;
250 | lead.filterEnvAmt = 0.85;
251 |
252 | // Drum kit
253 | var drumKit = new SampleKit();
254 |
255 | // Load the drum samples
256 | for (var i = 0; i < DRUM_SAMPLES.length; ++i)
257 | {
258 | var sample = DRUM_SAMPLES[i];
259 | drumKit.mapSample(Note(i), sample.path, sample.vol);
260 | }
261 |
262 | // Mixer with 8 channels
263 | mixer = new Mixer(8);
264 | mixer.inVolume[0] = 1.0;
265 | mixer.inVolume[1] = 1.0;
266 | mixer.outVolume = 0.5;
267 |
268 | // Connect all synth nodes and topologically order them
269 | lead.output.connect(mixer.input0);
270 | drumKit.output.connect(mixer.input1);
271 | mixer.output.connect(outNode.signal);
272 |
273 | // Create new tracks for the instruments
274 | leadTrack = piece.addTrack(new Track(lead));
275 | drumTrack = piece.addTrack(new Track(drumKit));
276 |
277 | // Order the audio graph nodes
278 | graph.orderNodes();
279 |
280 | // Create an audio generation event handler
281 | genAudio = piece.makeHandler();
282 | }
283 |
284 | /**
285 | Generate a new random machine
286 | */
287 | function genMachine()
288 | {
289 | // Get the machine options
290 | var numStates = parseInt(document.getElementById("numStates").value);
291 | var numSymbols = parseInt(document.getElementById("numSymbols").value);
292 | var filterDuds = document.getElementById("filterDuds").checked;
293 |
294 | // Extract the scale root note
295 | var rootElem = document.getElementById('scaleRoot');
296 | var scaleRoot = undefined;
297 | for (var i = 0; i < rootElem.length; ++i)
298 | {
299 | if (rootElem[i].selected === true)
300 | {
301 | var scaleRoot = String(rootElem[i].value);
302 | break;
303 | }
304 | }
305 |
306 | if (NOTE_NAME_PC.hasOwnProperty(scaleRoot) === false)
307 | error('Invalid scale root');
308 |
309 | // Extract the scale type
310 | var typeElem = document.getElementById('scaleType');
311 | var scaleType = undefined;
312 | for (var i = 0; i < typeElem.length; ++i)
313 | {
314 | if (typeElem[i].selected === true)
315 | {
316 | var scaleType = String(typeElem[i].value);
317 | break;
318 | }
319 | }
320 |
321 | if (scaleIntervs[scaleType] === undefined)
322 | error('Invalid scale type');
323 |
324 | // Extract a list of octaves covered
325 | var octavesElem = document.getElementById('octaves');
326 | var octaves = [];
327 | for (var i = 0; i < octavesElem.children.length; ++i)
328 | {
329 | var octElem = octavesElem.children[i];
330 | if (octElem.checked === true)
331 | octaves.push(parseInt(octElem.value));
332 | }
333 |
334 | if (octaves.length === 0)
335 | error('Must cover at least one octave');
336 |
337 | // Extract a list of note durations
338 | var durationsElem = document.getElementById('durations');
339 | var durations = [];
340 | for (var i = 0; i < durationsElem.children.length; ++i)
341 | {
342 | var durElem = durationsElem.children[i];
343 | if (durElem.checked === true)
344 | durations.push(parseInt(durElem.value));
345 | }
346 |
347 | if (durations.length === 0)
348 | error('Must allow at least one note duration');
349 |
350 | // Extract a list of drum samples
351 | var drumsElem = document.getElementById('drumSamples');
352 | var drumNotes = [null];
353 | for (var i = 0; i < drumsElem.children.length; ++i)
354 | {
355 | var elem = drumsElem.children[i];
356 | if (elem.checked === true)
357 | drumNotes.push(parseInt(elem.value));
358 | }
359 |
360 | // Get the tempo and time signature
361 | var tempo = parseInt(document.getElementById("tempo").value);
362 | var timeSigNum = parseInt(document.getElementById("timeSigNum").value);
363 | var timeSigDenom = parseInt(document.getElementById("timeSigDenom").value);
364 |
365 | if (!(tempo > 0 && tempo <= 400))
366 | error('invalid tempo')
367 | if (!(timeSigNum > 0 && timeSigNum <= 32) &&
368 | !(timeSigDenom > 0 && timeSigDenom <= 32))
369 | error('invalid time signature');
370 |
371 | // Generate the list of scale notes
372 | var noteList = [];
373 | for (var i = 0; i < octaves.length; ++i)
374 | {
375 | var octNo = octaves[i];
376 | var octRoot = Note(scaleRoot + octNo);
377 | var scaleNotes = genScale(octRoot, scaleType);
378 |
379 | if (i + 1 < octaves.length && octaves[i+1] == octNo + 1)
380 | scaleNotes.pop();
381 |
382 | noteList = noteList.concat(scaleNotes)
383 | }
384 |
385 | // Generate the list of note value pairs
386 | var noteVals = [];
387 | for (var i = 0; i < noteList.length; ++i)
388 | {
389 | var note = noteList[i];
390 |
391 | for (var j = 0; j < durations.length; ++j)
392 | {
393 | var dur = durations[j];
394 | var noteFrac = timeSigDenom * (1 / dur);
395 |
396 | for (var k = 0; k < drumNotes.length; ++k)
397 | {
398 | var drumNote = drumNotes[k];
399 | noteVals.push({ note:note, frac:noteFrac, drumNote:drumNote });
400 | }
401 | }
402 | }
403 |
404 | // Set the timing configuration
405 | piece.beatsPerMin = tempo;
406 | piece.beatsPerBar = timeSigNum;
407 | piece.noteVal = timeSigDenom;
408 |
409 | console.log('num states: ' + numStates);
410 | console.log('num symbols: ' + numSymbols);
411 | console.log('Note list: ' + noteList.toString());
412 | console.log('Num output symbols: ' + noteVals.length);
413 |
414 | for (var attemptNo = 1; attemptNo < 50; attemptNo++)
415 | {
416 | console.log('Attempt #' + attemptNo);
417 |
418 | // Create a new random machine
419 | machine = new Machine(
420 | numStates,
421 | numSymbols,
422 | noteVals, // Output symbols
423 | 50000 // Memory size
424 | );
425 |
426 | var filter = testMachine(machine);
427 |
428 | if (filterDuds === false || filter === true)
429 | break;
430 | }
431 |
432 | // Clear the current hash tag to avoid confusion
433 | location.hash = '';
434 |
435 | // Clear the name from the page title
436 | setTitle('');
437 |
438 | // Start playing audio
439 | playAudio();
440 | }
441 |
442 | /**
443 | Test the fitness of a machine
444 | */
445 | function testMachine(machine)
446 | {
447 | var NUM_TEST_ITRS = 20 * ITRS_PER_NOTE;
448 |
449 | var MAX_LOOP_LEN = 20;
450 | var NUM_LOOP_REPEATS = 5;
451 |
452 | // Generate output for a large number of iterations
453 | var notes = [];
454 | for (var i = 0; i < NUM_TEST_ITRS; ++i)
455 | {
456 | outSym = machine.iterate();
457 | if (outSym !== null)
458 | notes.push(outSym);
459 | }
460 |
461 | // If too few notes were generated, filter out
462 | if (notes.length < NUM_TEST_ITRS / ITRS_PER_NOTE)
463 | return false;
464 |
465 | // Test for loops
466 | LOOP_TEST:
467 | for (var loopLen = 2; loopLen < MAX_LOOP_LEN; ++loopLen)
468 | {
469 | for (var ofs = 0; ofs < MAX_LOOP_LEN; ++ofs)
470 | {
471 | var loopIdx = notes.length - loopLen - ofs;
472 |
473 | for (var i = 0; i < NUM_LOOP_REPEATS; ++i)
474 | {
475 | var loopIdx2 = loopIdx - (i+1) * loopLen;
476 |
477 | for (var j = 0; j < loopLen; ++j)
478 | if (notes[loopIdx2 + j] !== notes[loopIdx+j])
479 | continue LOOP_TEST;
480 | }
481 | }
482 |
483 | console.log("LOOP FOUND!!!!!!!!!!!");
484 |
485 | // Loop found
486 | return false;
487 | }
488 |
489 | // Filter test passed
490 | return true;
491 | }
492 |
493 | function playAudio()
494 | {
495 | console.log('playAudio()');
496 |
497 | // Size of the audio generation buffer
498 | var bufferSize = 2048;
499 |
500 | // If audio is disabled, stop
501 | if (audioCtx === undefined)
502 | return;
503 |
504 | // If the audio isn't stopped, stop it
505 | if (jsAudioNode !== undefined)
506 | stopAudio()
507 |
508 | audioCtx.resume().then(function ()
509 | {
510 | // Reset the machine state
511 | machine.reset();
512 |
513 | // Clear the instrument tracks
514 | leadTrack.clear();
515 | drumTrack.clear();
516 |
517 | // Set the playback time on the piece to 0 (start)
518 | piece.setTime(0);
519 |
520 | var nextNoteBeat = 0;
521 |
522 | console.log('first beat time: ' + piece.beatTime(nextNoteBeat));
523 |
524 | function audioCB(evt)
525 | {
526 | // Generate audio data
527 | genAudio(evt);
528 |
529 | for (var i = 0; i < ITRS_PER_NOTE; ++i)
530 | {
531 | if (piece.beatTime(nextNoteBeat) > piece.playTime + 1)
532 | return;
533 |
534 | outSym = machine.iterate();
535 |
536 | if (outSym === null)
537 | continue;
538 |
539 | piece.makeNote(leadTrack, nextNoteBeat, outSym.note, outSym.frac);
540 |
541 | if (outSym.drumNote !== null)
542 | piece.makeNote(drumTrack, nextNoteBeat, outSym.drumNote, outSym.frac);
543 |
544 | nextNoteBeat += piece.noteLen(outSym.frac);
545 |
546 | //console.log('mem pos: ' + machine.memPos);
547 | //console.log('itr count: ' + machine.itrCount);
548 | //console.log('next note beat: ' + nextNoteBeat);
549 | }
550 | }
551 |
552 | // Create a JS audio node and connect it to the destination
553 | jsAudioNode = audioCtx.createScriptProcessor(bufferSize, 2, 2);
554 | jsAudioNode.onaudioprocess = audioCB;
555 | jsAudioNode.connect(audioCtx.destination);
556 | });
557 | }
558 |
559 | function stopAudio()
560 | {
561 | console.log('stopAudio()');
562 |
563 | // If audio is disabled, stop
564 | if (audioCtx === undefined)
565 | return;
566 |
567 | if (jsAudioNode === undefined)
568 | return;
569 |
570 | // Notify the piece that we are stopping playback
571 | piece.stop();
572 |
573 | // Disconnect the audio node
574 | jsAudioNode.disconnect();
575 | jsAudioNode = undefined;
576 | }
577 |
578 | /**
579 | Restart the audio playback from the start
580 | */
581 | function restartAudio()
582 | {
583 | stopAudio();
584 | playAudio();
585 | }
586 |
587 | /**
588 | Set the tune name in the page title
589 | */
590 | function setTitle(name)
591 | {
592 | var titleHeader = document.getElementById('titleHeader');
593 | while (titleHeader.childNodes.length > 1)
594 | titleHeader.removeChild(titleHeader.lastChild);
595 |
596 | var pageTitle = document.getElementById('pageTitle');
597 | while (pageTitle.childNodes.length > 1)
598 | pageTitle.removeChild(pageTitle.firstChild);
599 |
600 | if (name)
601 | {
602 | titleHeader.appendChild(document.createTextNode(' - ' + name));
603 | pageTitle.insertBefore(document.createTextNode(name + ' - '), pageTitle.firstChild);
604 | }
605 | }
606 |
607 | /**
608 | Validate a name string
609 | */
610 | function validName(name)
611 | {
612 | return (
613 | name.length <= 40 &&
614 | /^[\w ]+$/.test(name)
615 | );
616 | }
617 |
618 | /**
619 | Generate a shareable URL for the current piece
620 | */
621 | function genURL()
622 | {
623 | if (machine === undefined)
624 | error('need to generate a machine before sharing it');
625 |
626 | var name = document.getElementById('shareName').value;
627 | if (name.length === 0)
628 | error('Please enter a name for your creation');
629 | if (validName(name) === false)
630 | error('Invalid name');
631 |
632 | // Generate the encoding string
633 | var coding = '';
634 | coding += name + ',';
635 | coding += piece.beatsPerMin + ',';
636 | coding += piece.beatsPerBar + ',';
637 | coding += piece.noteVal + ',';
638 | coding += machine.toString();
639 |
640 | // Set the sharing URL
641 | var shareURL = (
642 | location.protocol + '//' + location.host +
643 | location.pathname + '#' + coding
644 | );
645 | document.getElementById("shareURL").value = shareURL;
646 |
647 | // Set the page title
648 | setTitle(name);
649 | }
650 |
--------------------------------------------------------------------------------
/mixer.js:
--------------------------------------------------------------------------------
1 | /*****************************************************************************
2 | *
3 | * This file is part of the Turing-Tunes project. The project is
4 | * distributed at:
5 | * https://github.com/maximecb/Turing-Tunes
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 AudioInput(this, 'input' + i, numChans);
84 | }
85 |
86 | // Audio output
87 | new AudioOutput(this, 'output', numChans);
88 |
89 | // Default name for this node
90 | this.name = 'mixer';
91 | }
92 | Mixer.prototype = new AudioNode();
93 |
94 | Mixer.prototype.connect = function (output, volume)
95 | {
96 | if (volume === undefined)
97 | volume = 1;
98 |
99 | for (var inIdx = 0; inIdx < this.inputs.length; ++inIdx)
100 | {
101 | var input = this.inputs[inIdx];
102 |
103 | if (!input.src)
104 | {
105 | output.connect(input);
106 | this.inVolume[inIdx] = volume;
107 | return;
108 | }
109 | }
110 |
111 | error('no mixer inputs available');
112 | }
113 |
114 | /**
115 | Update the outputs based on the inputs
116 | */
117 | Mixer.prototype.update = function (time, sampleRate)
118 | {
119 | // Count the number of inputs having produced data
120 | var actCount = 0;
121 | for (var inIdx = 0; inIdx < this.inputs.length; ++inIdx)
122 | if (this.inputs[inIdx].hasData() === true)
123 | ++actCount;
124 |
125 | // If there are no active inputs, do nothing
126 | if (actCount === 0)
127 | return;
128 |
129 | // Initialize the output to 0
130 | for (var chIdx = 0; chIdx < this.numChans; ++chIdx)
131 | {
132 | var outBuf = this.output.getBuffer(chIdx);
133 | for (var i = 0; i < outBuf.length; ++i)
134 | outBuf[i] = 0;
135 | }
136 |
137 | // For each input
138 | for (var inIdx = 0; inIdx < this.inputs.length; ++inIdx)
139 | {
140 | // Get the input
141 | var input = this.inputs[inIdx];
142 |
143 | // If this input has no available data, skip it
144 | if (input.hasData() === false)
145 | continue;
146 |
147 | // For each channel
148 | for (var chIdx = 0; chIdx < this.numChans; ++chIdx)
149 | {
150 | // Get the input buffer
151 | var inBuf = input.getBuffer(chIdx);
152 |
153 | // Get the volume for this input
154 | var inVolume = this.inVolume[inIdx];
155 |
156 | // Get the output buffer
157 | var outBuf = this.output.getBuffer(chIdx);
158 |
159 | // If we are operating in stereo
160 | if (this.numChans === 2)
161 | {
162 | var inPanning = this.inPanning[inIdx];
163 |
164 | // Scale the channel volumes based on the panning level
165 | if (chIdx === 0)
166 | inVolume *= (1 - inPanning) / 2;
167 | else if (chIdx === 1)
168 | inVolume *= (1 + inPanning) / 2;
169 | }
170 |
171 | // Scale the input and add it to the output
172 | for (var i = 0; i < inBuf.length; ++i)
173 | outBuf[i] += inBuf[i] * inVolume;
174 | }
175 | }
176 |
177 | // Scale the output according to the output volume
178 | for (var chIdx = 0; chIdx < this.numChans; ++chIdx)
179 | {
180 | var outBuf = this.output.getBuffer(chIdx);
181 | for (var i = 0; i < outBuf.length; ++i)
182 | outBuf[i] *= this.outVolume;
183 | }
184 | }
185 |
186 |
--------------------------------------------------------------------------------
/music.js:
--------------------------------------------------------------------------------
1 | /*****************************************************************************
2 | *
3 | * This file is part of the Turing-Tunes project. The project is
4 | * distributed at:
5 | * https://github.com/maximecb/Turing-Tunes
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 | Note number of the C5 note (middle C)
58 | */
59 | var C5_NOTE_NO = 60;
60 |
61 | /**
62 | Frequency of the A5 note
63 | */
64 | var A5_NOTE_FREQ = 440;
65 |
66 | /**
67 | Note number of the A5 note
68 | */
69 | var A5_NOTE_NO = 69;
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 PC_NOTE_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 | A5 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 | // Create a note object
132 | var note = Object.create(Note.prototype);
133 | note.noteNo = val;
134 |
135 | // Store the note object in the note table
136 | Note.notesByNo[val] = note;
137 |
138 | return note;
139 | }
140 |
141 | /**
142 | Array of note numbers to note objects
143 | */
144 | Note.notesByNo = [];
145 |
146 | /**
147 | Get the note number for a note name
148 | */
149 | Note.nameToNo = function (name)
150 | {
151 | // Use a regular expression to parse the name
152 | var matches = name.match(/([A-G]#?)([0-9])/i);
153 |
154 | assert (
155 | matches !== null,
156 | 'invalid note name: "' + name + '"'
157 | );
158 |
159 | var namePart = matches[1];
160 | var numPart = matches[2];
161 |
162 | var pc = NOTE_NAME_PC[namePart];
163 |
164 | assert (
165 | typeof pc === 'number',
166 | 'invalid note name: ' + namePart
167 | );
168 |
169 | var octNo = parseInt(numPart);
170 |
171 | // Compute the note number
172 | var noteNo = octNo * NOTES_PER_OCTAVE + pc;
173 |
174 | assert (
175 | noteNo >= 0 || noteNo < NUM_NOTES,
176 | 'note parsing failed'
177 | );
178 |
179 | return noteNo;
180 | }
181 |
182 | /**
183 | Sorting function for note objects
184 | */
185 | Note.sortFn = function (n1, n2)
186 | {
187 | return n1.noteNo - n2.noteNo;
188 | }
189 |
190 | /**
191 | Get the pitch class
192 | */
193 | Note.prototype.getPC = function ()
194 | {
195 | return this.noteNo % NOTES_PER_OCTAVE;
196 | }
197 |
198 | /**
199 | Get the octave number
200 | */
201 | Note.prototype.getOctNo = function ()
202 | {
203 | return Math.floor(this.noteNo / NOTES_PER_OCTAVE);
204 | }
205 |
206 | /**
207 | Get the name for a note
208 | */
209 | Note.prototype.getName = function ()
210 | {
211 | // Compute the octave number of the note
212 | var octNo = this.getOctNo();
213 |
214 | // Get the pitch class for this note
215 | var pc = this.getPC();
216 |
217 | var name = PC_NOTE_NAME[pc];
218 |
219 | // Add the octave number to the note name
220 | name += String(octNo);
221 |
222 | return name;
223 | }
224 |
225 | /**
226 | The string representation of a note is its name
227 | */
228 | Note.prototype.toString = Note.prototype.getName;
229 |
230 | /**
231 | Get the frequency for a note
232 | @param offset detuning offset in cents
233 | */
234 | Note.prototype.getFreq = function (offset)
235 | {
236 | if (offset === undefined)
237 | offset = 0;
238 |
239 | // F(n) = 440 * 2 ^ ((n-69)/12)
240 | var noteExp = (this.noteNo - A5_NOTE_NO) / NOTES_PER_OCTAVE;
241 |
242 | // b = a * 2 ^ (o / 1200)
243 | var offsetExp = offset / CENTS_PER_OCTAVE;
244 |
245 | // Compute the note frequency
246 | return A5_NOTE_FREQ * Math.pow(
247 | 2,
248 | noteExp + offsetExp
249 | );
250 | }
251 |
252 | /**
253 | Offset a note by a number of semitones
254 | */
255 | Note.prototype.offset = function (numSemis)
256 | {
257 | var offNo = this.noteNo + numSemis;
258 |
259 | assert (
260 | offNo >= 0 && offNo < NUM_NOTES,
261 | 'invalid note number after offset'
262 | );
263 |
264 | return new Note(offNo);
265 | }
266 |
267 | /**
268 | Shift a note to higher or lower octaves
269 | */
270 | Note.prototype.shift = function (numOcts)
271 | {
272 | return this.offset(numOcts * NOTES_PER_OCTAVE);
273 | }
274 |
275 | /**
276 | Interval consonance table
277 | */
278 | var intervCons = {
279 | 0 : 3, // Unison
280 | 1 : -3, // Minor second
281 | 2 : -1, // Major second
282 |
283 | 3 : 3, // Minor third
284 | 4 : 3, // Major third
285 | 5 : 1, // Perfect fourth
286 |
287 | 6 : -1, // Tritone
288 | 7 : 3, // Perfect fifth
289 | 8 : 1, // Minor sixth
290 |
291 | 9 : 2, // Major sixth
292 | 10: -1, // Minor seventh
293 | 11: -2 // Major seventh
294 | };
295 |
296 | /**
297 | Consonance rating function for two notes
298 | */
299 | function consonance(n1, n2)
300 | {
301 | var no1 = n1.noteNo;
302 | var no2 = n2.noteNo;
303 |
304 | var diff = Math.max(no1 - no2, no2 - no1);
305 |
306 | // Compute the simple interval between the two notes
307 | var interv = diff % 12;
308 |
309 | return intervCons[interv];
310 | }
311 |
312 | //============================================================================
313 | // Scale and chord generation
314 | //============================================================================
315 |
316 | /**
317 | Semitone intervals for different scales
318 | */
319 | var scaleIntervs = {
320 |
321 | // Major scale
322 | 'major': [2, 2, 1, 2, 2, 2],
323 |
324 | // Natural minor scale
325 | 'natural minor': [2, 1, 2, 2, 1, 2],
326 |
327 | // Major pentatonic scale
328 | 'major pentatonic': [2, 2, 3, 2],
329 |
330 | // Minor pentatonic scale
331 | 'minor pentatonic': [3, 2, 2, 3],
332 |
333 | // Blues scale
334 | 'blues': [3, 2, 1, 1, 3],
335 |
336 | // Chromatic scale
337 | 'chromatic': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
338 | };
339 |
340 | /**
341 | Generate the notes of a scale based on a root note
342 | */
343 | function genScale(rootNote, scale, numOctaves)
344 | {
345 | if ((rootNote instanceof Note) === false)
346 | rootNote = new Note(rootNote);
347 |
348 | if (numOctaves === undefined)
349 | numOctaves = 1;
350 |
351 | // Get the intervals for this type of chord
352 | var intervs = scaleIntervs[scale];
353 |
354 | assert (
355 | intervs instanceof Array,
356 | 'invalid scale name: ' + scale
357 | );
358 |
359 | // List of generated notes
360 | var notes = [];
361 |
362 | // For each octave
363 | for (var octNo = 0; octNo < numOctaves; ++octNo)
364 | {
365 | var octRoot = rootNote.shift(octNo);
366 |
367 | // Add the root note to the scale
368 | notes.push(octRoot);
369 |
370 | // Add the scale notes
371 | for (var i = 0; i < intervs.length; ++i)
372 | {
373 | var prevNote = notes[notes.length-1];
374 |
375 | var interv = intervs[i];
376 |
377 | notes.push(prevNote.offset(interv));
378 | }
379 | }
380 |
381 | // Add the note closing the last octave
382 | notes.push(rootNote.shift(numOctaves));
383 |
384 | return notes;
385 | }
386 |
387 | /**
388 | Semitone intervals for different kinds of chords
389 | */
390 | var chordIntervs = {
391 |
392 | // Major chord
393 | 'maj': [0, 4, 7],
394 |
395 | // Minor chord
396 | 'min': [0, 3, 7],
397 |
398 | // Diminished chord
399 | 'dim': [0, 3, 6],
400 |
401 | // Major 7th
402 | 'maj7': [0, 4, 7, 11],
403 |
404 | // Minor 7th
405 | 'min7': [0, 3, 7, 10],
406 |
407 | // Diminished 7th chord
408 | 'dim7': [0, 3, 6, 9],
409 |
410 | // Dominant 7th
411 | '7': [0, 4, 7, 10],
412 |
413 | // Suspended 4th
414 | 'sus4': [0, 5, 7],
415 |
416 | // Suspended second
417 | 'sus2': [0, 2, 7]
418 | };
419 |
420 | /**
421 | Generate a list of notes for a chord
422 | */
423 | function genChord(rootNote, type)
424 | {
425 | if ((rootNote instanceof Note) === false)
426 | rootNote = new Note(rootNote);
427 |
428 | // Get the intervals for this type of chord
429 | var intervs = chordIntervs[type];
430 |
431 | assert (
432 | intervs instanceof Array,
433 | 'invalid chord type: ' + type
434 | );
435 |
436 | // Get the root note number
437 | var rootNo = rootNote.noteNo;
438 |
439 | // Compute the note numbers for the notes
440 | var notes = intervs.map(function (i) { return new Note(rootNo + i); });
441 |
442 | return notes;
443 | }
444 |
445 |
--------------------------------------------------------------------------------
/piece.js:
--------------------------------------------------------------------------------
1 | /*****************************************************************************
2 | *
3 | * This file is part of the Turing-Tunes project. The project is
4 | * distributed at:
5 | * https://github.com/maximecb/Turing-Tunes
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(graph)
45 | {
46 | assert (
47 | graph instanceof AudioGraph || graph === undefined,
48 | 'invalid synth net'
49 | );
50 |
51 | /**
52 | Audio graph used by this piece
53 | */
54 | this.graph = graph;
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 | Loop time
68 | */
69 | this.loopTime = undefined;
70 |
71 | /**
72 | Previous update time
73 | */
74 | this.prevTime = 0;
75 |
76 | /**
77 | Tempo in beats per minute
78 | */
79 | this.beatsPerMin = 140;
80 |
81 | /**
82 | Time signature numerator, beats per bar
83 | */
84 | this.beatsPerBar = 4;
85 |
86 | /**
87 | Time signature denominator, note value for each beat
88 | */
89 | this.noteVal = 4;
90 | }
91 |
92 | /**
93 | Add a track to the piece
94 | */
95 | Piece.prototype.addTrack = function (track)
96 | {
97 | assert (
98 | track instanceof Track,
99 | 'invalid track'
100 | );
101 |
102 | this.tracks.push(track);
103 |
104 | return track;
105 | }
106 |
107 | /**
108 | Get the time offset for a beat number. This number can be fractional.
109 | */
110 | Piece.prototype.beatTime = function (beatNo)
111 | {
112 | var beatLen = 60 / this.beatsPerMin;
113 |
114 | return beatLen * beatNo;
115 | }
116 |
117 | /**
118 | Get the length in seconds for a note value multiple
119 | */
120 | Piece.prototype.noteLen = function (len)
121 | {
122 | // By default, use the default note value
123 | if (len === undefined)
124 | len = 1;
125 |
126 | var beatLen = 60 / this.beatsPerMin;
127 |
128 | var barLen = beatLen * this.beatsPerBar;
129 |
130 | var noteLen = barLen / this.noteVal;
131 |
132 | return len * noteLen * 0.99;
133 | }
134 |
135 | /**
136 | Helper methods to add notes to the track.
137 | Produces a note-on and note-off event pair.
138 | */
139 | Piece.prototype.makeNote = function (track, beatNo, note, len, vel)
140 | {
141 | if ((note instanceof Note) == false)
142 | note = new Note(note);
143 |
144 | // By default, the velocity is 100%
145 | if (vel === undefined)
146 | vel = 1;
147 |
148 | // Convert the note time to a beat number
149 | var time = this.beatTime(beatNo);
150 |
151 | // Get the note length in seconds
152 | var noteLen = this.noteLen(len);
153 |
154 | // Create the note on and note off events
155 | var noteOn = new NoteOnEvt(time, note, vel);
156 | var noteOff = new NoteOffEvt(time + noteLen, note);
157 |
158 | // Add the events to the track
159 | track.addEvent(noteOn);
160 | track.addEvent(noteOff);
161 | }
162 |
163 | /**
164 | Set the playback position/time
165 | */
166 | Piece.prototype.setTime = function (time)
167 | {
168 | this.playTime = time;
169 | }
170 |
171 | /**
172 | Dispatch synthesis events up to the current time
173 | */
174 | Piece.prototype.dispatch = function (curTime, realTime)
175 | {
176 | // Do the dispatch for each track
177 | for (var i = 0; i < this.tracks.length; ++i)
178 | {
179 | var track = this.tracks[i];
180 |
181 | track.dispatch(this.prevTime, curTime, realTime);
182 | }
183 |
184 | // Store the last update time/position
185 | this.prevTime = curTime;
186 | }
187 |
188 | /**
189 | Called when stopping the playback of a piece
190 | */
191 | Piece.prototype.stop = function ()
192 | {
193 | // If a synthesis network is attached to this piece
194 | if (this.graph !== undefined)
195 | {
196 | // Send an all notes off event to all synthesis nodes
197 | var notesOffEvt = new AllNotesOffEvt();
198 | for (var i = 0; i < this.graph.order.length; ++i)
199 | {
200 | var node = this.graph.order[i];
201 | node.processEvent(notesOffEvt);
202 | }
203 | }
204 |
205 | // Set the playback position past all events
206 | this.playTime = Infinity;
207 | }
208 |
209 | /**
210 | Create a handler for real-time audio generation
211 | */
212 | Piece.prototype.makeHandler = function ()
213 | {
214 | var graph = this.graph;
215 | var piece = this;
216 |
217 | var sampleRate = graph.sampleRate;
218 |
219 | // Output node of the audio graph
220 | var outNode = graph.outNode;
221 |
222 | // Current playback time
223 | var curTime = piece.playTime;
224 | var realTime = piece.playTime;
225 |
226 | // Audio generation function
227 | function genAudio(evt)
228 | {
229 | var startTime = (new Date()).getTime();
230 |
231 | var numChans = evt.outputBuffer.numberOfChannels
232 | var numSamples = evt.outputBuffer.getChannelData(0).length;
233 |
234 | // If the playback position changed, update the current time
235 | if (piece.playTime !== curTime)
236 | {
237 | console.log('playback time updated');
238 | curTime = piece.playTime;
239 | }
240 |
241 | assert (
242 | numChans === outNode.numChans,
243 | 'mismatch in the number of output channels'
244 | );
245 |
246 | assert (
247 | numSamples % AUDIO_BUF_SIZE === 0,
248 | 'the output buffer size must be a multiple of the synth buffer size'
249 | );
250 |
251 | // Until all samples are produced
252 | for (var smpIdx = 0; smpIdx < numSamples; smpIdx += AUDIO_BUF_SIZE)
253 | {
254 | // Update the piece, dispatch track events
255 | piece.dispatch(curTime, realTime);
256 |
257 | // Generate the sample values
258 | var values = graph.genOutput(realTime);
259 |
260 | // Copy the values for each channel
261 | for (var chnIdx = 0; chnIdx < numChans; ++chnIdx)
262 | {
263 | var srcBuf = outNode.getBuffer(chnIdx);
264 | var dstBuf = evt.outputBuffer.getChannelData(chnIdx);
265 |
266 | for (var i = 0; i < AUDIO_BUF_SIZE; ++i)
267 | dstBuf[smpIdx + i] = srcBuf[i];
268 | }
269 |
270 | // Update the current time based on sample rate
271 | curTime += AUDIO_BUF_SIZE / sampleRate;
272 | realTime += AUDIO_BUF_SIZE / sampleRate;
273 |
274 | // If we lust passed the loop time, go back to the start
275 | if (piece.playTime <= piece.loopTime &&
276 | curTime > piece.loopTime)
277 | {
278 | piece.dispatch(piece.loopTime + 0.01, realTime);
279 |
280 | curTime = 0;
281 | piece.prevTime = 0;
282 | }
283 |
284 | // Update the current playback position
285 | piece.playTime = curTime;
286 | }
287 |
288 | var endTime = (new Date()).getTime();
289 | var compTime = (endTime - startTime) / 1000;
290 | var soundTime = (numSamples / graph.sampleRate);
291 | var cpuUse = (100 * compTime / soundTime).toFixed(1);
292 |
293 | //console.log('cpu use: ' + cpuUse + '%');
294 | }
295 |
296 | // Return the handler function
297 | return genAudio;
298 | }
299 |
300 | /**
301 | Draw the notes of a track using the canvas API
302 | */
303 | Piece.prototype.drawTrack = function (
304 | track,
305 | canvasCtx,
306 | topX,
307 | topY,
308 | width,
309 | height,
310 | minNote,
311 | numOcts,
312 | numBeats
313 | )
314 | {
315 | // Compute the bottom-right corner coordinates
316 | var botX = topX + width;
317 | var botY = topY + height;
318 |
319 |
320 |
321 |
322 |
323 | // Compute the total time for the beats
324 | var totalTime = (numBeats / this.beatsPerMin) * 60;
325 |
326 | var minTime = Math.max(0, this.playTime - (totalTime / 2));
327 |
328 | var maxTime = Math.min(minTime + totalTime, track.endTime());
329 |
330 | /*
331 | console.log(totalTime);
332 | console.log(minTime);
333 | console.log(maxTime);
334 | */
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 | var minNoteNo = Math.floor(minNote.noteNo / NOTES_PER_OCTAVE) * NOTES_PER_OCTAVE;
343 |
344 | var numNotes = numOcts * NOTES_PER_OCTAVE;
345 |
346 | var numWhites = numOcts * 7;
347 |
348 | var whiteHeight = height / numWhites;
349 |
350 | var blackHeight = whiteHeight / 2;
351 |
352 | var pianoWidth = 40;
353 |
354 | var blackWidth = (pianoWidth / 4) * 3;
355 |
356 | var beatWidth = (width - pianoWidth) / numBeats;
357 |
358 | canvasCtx.fillStyle = "grey"
359 | canvasCtx.fillRect(topX, topY, width, height);
360 |
361 | // Fill the piano area with white
362 | canvasCtx.fillStyle = "white"
363 | canvasCtx.fillRect(topX, topY, pianoWidth, height);
364 |
365 | canvasCtx.strokeStyle = "black";
366 | canvasCtx.beginPath();
367 | canvasCtx.moveTo(topX, topY);
368 | canvasCtx.lineTo(topX + pianoWidth, topY);
369 | canvasCtx.lineTo(topX + pianoWidth, botY);
370 | canvasCtx.lineTo(topX, botY);
371 | canvasCtx.closePath();
372 | canvasCtx.stroke();
373 |
374 | var noteExts = new Array(numNotes);
375 | var noteIdx = 0;
376 |
377 | // For each white note
378 | for (var i = 0; i < numWhites; ++i)
379 | {
380 | var whiteBot = botY - (whiteHeight * i);
381 | var whiteTop = whiteBot - whiteHeight;
382 |
383 | var whiteExts = noteExts[noteIdx++] = { bot: whiteBot, top: whiteTop };
384 |
385 | if (i > 0)
386 | {
387 | var prevExts = noteExts[noteIdx - 2];
388 | whiteExts.bot = Math.min(whiteExts.bot, prevExts.top);
389 | }
390 |
391 | canvasCtx.strokeStyle = "black";
392 | canvasCtx.beginPath();
393 | canvasCtx.moveTo(topX, whiteBot);
394 | canvasCtx.lineTo(topX + pianoWidth, whiteBot);
395 | canvasCtx.closePath();
396 | canvasCtx.stroke();
397 |
398 | if ((i % 7) !== 2 && (i % 7) !== 6)
399 | {
400 | var blackTop = whiteTop - (blackHeight / 2);
401 | var blackBot = whiteTop + (blackHeight / 2);
402 |
403 | var blackExts = noteExts[noteIdx++] = { bot:blackBot, top:blackTop };
404 | whiteExts.top = blackExts.bot;
405 |
406 | canvasCtx.fillStyle = "black";
407 | canvasCtx.beginPath();
408 | canvasCtx.moveTo(topX, blackTop);
409 | canvasCtx.lineTo(topX + blackWidth, blackTop);
410 | canvasCtx.lineTo(topX + blackWidth, blackBot);
411 | canvasCtx.lineTo(topX, blackBot);
412 | canvasCtx.lineTo(topX, blackTop);
413 | canvasCtx.closePath();
414 | canvasCtx.fill();
415 | }
416 | }
417 |
418 | // Draw the horizontal note separation lines
419 | for (var i = 0; i < noteExts.length; ++i)
420 | {
421 | var exts = noteExts[i];
422 |
423 | canvasCtx.strokeStyle = "rgb(0, 0, 125)";
424 | canvasCtx.beginPath();
425 | canvasCtx.moveTo(topX + pianoWidth + 1, exts.top);
426 | canvasCtx.lineTo(botX, exts.top);
427 | canvasCtx.closePath();
428 | canvasCtx.stroke();
429 | }
430 |
431 |
432 |
433 | /*
434 | var minTimeBeat = this.beatTime(minTime);
435 | // beatWidth
436 |
437 | var firstBeatLine = minBeatTime +
438 | */
439 |
440 |
441 | /*
442 | // Draw the vertical beat separation lines
443 | for (var i = 1; i < numBeats; ++i)
444 | {
445 | var xCoord = topX + pianoWidth + (i * beatWidth);
446 |
447 | var color;
448 | if (i % this.beatsPerBar === 0)
449 | color = "rgb(25, 25, 255)"
450 | else
451 | color = "rgb(0, 0, 125)";
452 |
453 | canvasCtx.strokeStyle = color;
454 | canvasCtx.beginPath();
455 | canvasCtx.moveTo(xCoord, topY);
456 | canvasCtx.lineTo(xCoord, botY);
457 | canvasCtx.closePath();
458 | canvasCtx.stroke();
459 | }
460 | */
461 |
462 |
463 |
464 | // TODO: method for binary search
465 |
466 | // TODO: method to find corresponding note off
467 |
468 |
469 |
470 | // For each track event
471 | for (var i = 0; i < track.events.length; ++i)
472 | {
473 | var event = track.events[i];
474 |
475 | // If this is a note on event
476 | if (event instanceof NoteOnEvt)
477 | {
478 | var noteNo = event.note.noteNo;
479 | var startTime = event.time;
480 |
481 | // Try to find the note end time
482 | var endTime = undefined;
483 | for (var j = i + 1; j < track.events.length; ++j)
484 | {
485 | var e2 = track.events[j];
486 |
487 | if (e2 instanceof NoteOffEvt &&
488 | e2.note.noteNo === noteNo &&
489 | e2.time > event.time)
490 | {
491 | endTime = e2.time;
492 | break;
493 | }
494 | }
495 |
496 | if (endTime === undefined)
497 | error('COULD NOT FIND NOTE OFF');
498 |
499 | var startFrac = startTime / totalTime;
500 | var endFrac = endTime / totalTime;
501 |
502 | var xStart = topX + pianoWidth + startFrac * (width - pianoWidth);
503 | var xEnd = topX + pianoWidth + endFrac * (width - pianoWidth);
504 |
505 | var noteIdx = noteNo - minNoteNo;
506 |
507 | if (noteIdx >= noteExts.length)
508 | {
509 | console.log('note above limit');
510 | continue;
511 | }
512 |
513 | //console.log(noteIdx + ': ' + xStart + ' => ' + xEnd);
514 |
515 | var exts = noteExts[noteIdx];
516 |
517 | canvasCtx.fillStyle = "red";
518 | canvasCtx.strokeStyle = "black";
519 | canvasCtx.beginPath();
520 | canvasCtx.moveTo(xStart, exts.top);
521 | canvasCtx.lineTo(xEnd , exts.top);
522 | canvasCtx.lineTo(xEnd , exts.bot);
523 | canvasCtx.lineTo(xStart, exts.bot);
524 | canvasCtx.lineTo(xStart, exts.top);
525 | canvasCtx.closePath();
526 | canvasCtx.fill();
527 | canvasCtx.stroke();
528 | }
529 | }
530 |
531 |
532 |
533 |
534 |
535 | /*
536 | // If playback is ongoing
537 | if (this.playTime !== 0 && maxTime !== 0)
538 | {
539 | // Compute the cursor line position
540 | var cursorFrac = this.playTime / maxTime;
541 | var cursorPos = topX + pianoWidth + cursorFrac * (width - pianoWidth);
542 |
543 | // Draw the cursor line
544 | canvasCtx.strokeStyle = "white";
545 | canvasCtx.beginPath();
546 | canvasCtx.moveTo(cursorPos, topY);
547 | canvasCtx.lineTo(cursorPos, botY);
548 | canvasCtx.closePath();
549 | canvasCtx.stroke();
550 | }
551 | */
552 |
553 |
554 |
555 |
556 | }
557 |
558 | /**
559 | Produce MIDI file data for a track of this piece.
560 | The data is written into a byte array.
561 | */
562 | Piece.prototype.getMIDIData = function (track)
563 | {
564 | var data = [];
565 |
566 | var writeIdx = 0;
567 |
568 | function writeByte(val)
569 | {
570 | assert (
571 | val <= 0xFF,
572 | 'invalid value in writeByte'
573 | );
574 |
575 | data[writeIdx++] = val;
576 | }
577 |
578 | function writeWORD(val)
579 | {
580 | assert (
581 | val <= 0xFFFF,
582 | 'invalid value in writeWORD'
583 | );
584 |
585 | writeByte((val >> 8) & 0xFF);
586 | writeByte((val >> 0) & 0xFF);
587 | }
588 |
589 | function writeDWORD(val)
590 | {
591 | assert (
592 | val <= 0xFFFFFFFF,
593 | 'invalid value in writeDWORD: ' + val
594 | );
595 |
596 | writeByte((val >> 24) & 0xFF);
597 | writeByte((val >> 16) & 0xFF);
598 | writeByte((val >> 8) & 0xFF);
599 | writeByte((val >> 0) & 0xFF);
600 | }
601 |
602 | function writeVarLen(val)
603 | {
604 | // Higher bits must be written first
605 |
606 | var bytes = [];
607 |
608 | do
609 | {
610 | var bits = val & 0x7F;
611 |
612 | val >>= 7;
613 |
614 | bytes.push(bits);
615 |
616 | } while (val !== 0);
617 |
618 | for (var i = bytes.length - 1; i >= 0; --i)
619 | {
620 | var bits = bytes[i];
621 |
622 | if (i > 0)
623 | bits = 0x80 | bits;
624 |
625 | writeByte(bits);
626 | }
627 | }
628 |
629 | // Number of clock ticks per beat
630 | var ticksPerBeat = 500;
631 |
632 | // Write the file header
633 | writeDWORD(0x4D546864); // MThd
634 | writeDWORD(0x00000006); // Chunk size
635 | writeWORD(0); // Type 0 MIDI file (one track)
636 | writeWORD(1); // One track
637 | writeWORD(ticksPerBeat); // Time division
638 |
639 | // Write the track header
640 | writeDWORD(0x4D54726B) // MTrk
641 | writeDWORD(0); // Chunk size, written later
642 |
643 | // Save the track size index
644 | var trackSizeIdx = data.length - 4;
645 |
646 | // Delta time conversion ratio
647 | var ticksPerSec = (this.beatsPerMin / 60) * ticksPerBeat;
648 |
649 | console.log('ticks per sec: ' + ticksPerSec);
650 |
651 | // Set the tempo in microseconds per quarter node
652 | var usPerMin = 60000000;
653 | var mpqn = usPerMin / this.beatsPerMin;
654 | writeVarLen(0);
655 | writeByte(0xFF)
656 | writeByte(0x51);
657 | writeVarLen(3);
658 | writeByte((mpqn >> 16) & 0xFF);
659 | writeByte((mpqn >> 8) & 0xFF);
660 | writeByte((mpqn >> 0) & 0xFF);
661 |
662 | // Set the time signature
663 | var num32Nds = Math.floor(8 * (4 / this.noteVal));
664 | writeVarLen(0);
665 | writeByte(0xFF)
666 | writeByte(0x58);
667 | writeVarLen(4);
668 | writeByte(this.beatsPerBar); // Num
669 | writeByte(2); // Denom 2^2 = 4
670 | writeByte(24); // Metronome rate
671 | writeByte(num32Nds); // 32nds per quarter note
672 |
673 | console.log('beats per bar: ' + this.beatsPerBar);
674 | console.log('num 32nds: ' + num32Nds);
675 |
676 | // Set the piano program
677 | writeVarLen(0);
678 | writeByte(0xC0);
679 | writeByte(0);
680 |
681 | // For each track event
682 | for (var i = 0; i < track.events.length; ++i)
683 | {
684 | var event = track.events[i];
685 | var prevEvent = track.events[i-1];
686 |
687 | // Event format:
688 | // Delta Time
689 | // Event Type Value
690 | // MIDI Channel
691 | // Parameter 1
692 | // Parameter 2
693 |
694 | var deltaTime = prevEvent? (event.time - prevEvent.time):0;
695 |
696 | var deltaTicks = Math.ceil(ticksPerSec * deltaTime);
697 |
698 | assert (
699 | isNonNegInt(deltaTicks),
700 | 'invalid delta ticks: ' + deltaTicks
701 | );
702 |
703 | console.log(event.toString())
704 | console.log('delta ticks: ' + deltaTicks);
705 |
706 | // Write the event delta time
707 | writeVarLen(deltaTicks);
708 |
709 | if (event instanceof NoteOnEvt)
710 | {
711 | writeByte(0x90);
712 |
713 | writeByte(event.note.noteNo);
714 |
715 | // Velocity
716 | var vel = Math.min(Math.floor(event.vel * 127), 127);
717 | writeByte(vel);
718 | }
719 |
720 | else if (event instanceof NoteOffEvt)
721 | {
722 | writeByte(0x80);
723 |
724 | writeByte(event.note.noteNo);
725 |
726 | // Velocity
727 | writeByte(0);
728 | }
729 | }
730 |
731 | // Write the end of track event
732 | writeVarLen(0);
733 | writeByte(0xFF)
734 | writeByte(0x2F);
735 | writeVarLen(0);
736 |
737 | // Write the track chunk size
738 | var trackSize = data.length - (trackSizeIdx + 4);
739 | console.log('track size: ' + trackSize);
740 | writeIdx = trackSizeIdx
741 | writeDWORD(trackSize);
742 |
743 | return data;
744 | }
745 |
746 | /**
747 | @class Synthesis event track implementation. Produces events and sends them
748 | to a target synthesis node.
749 | */
750 | function Track(target)
751 | {
752 | assert (
753 | target instanceof AudioNode || target === undefined,
754 | 'invalid target node'
755 | );
756 |
757 | /**
758 | Target audio node to send events to
759 | */
760 | this.target = target;
761 |
762 | /**
763 | Events for this track
764 | */
765 | this.events = [];
766 | }
767 |
768 | /**
769 | Add an event to the track
770 | */
771 | Track.prototype.addEvent = function (evt)
772 | {
773 | this.events.push(evt);
774 |
775 | // If the event is being added at the end of the track, stop
776 | if (this.events.length === 1 ||
777 | evt.time >= this.events[this.events.length-2].time)
778 | return;
779 |
780 | // Sort the events
781 | this.events.sort(function (a, b) { return a.time - b.time; });
782 | }
783 |
784 | /**
785 | Get the dispatch time of the last event
786 | */
787 | Track.prototype.endTime = function ()
788 | {
789 | if (this.events.length === 0)
790 | return
791 | else
792 | return this.events[this.events.length-1].time;
793 | }
794 |
795 | /**
796 | Dispatch the events between the previous update time and
797 | the current time, inclusively.
798 | */
799 | Track.prototype.dispatch = function (prevTime, curTime, realTime)
800 | {
801 | if (this.target === undefined)
802 | return;
803 |
804 | if (this.events.length === 0)
805 | return;
806 |
807 | // Must play all events from the previous time (inclusive) up to the
808 | // current time (exclusive).
809 | //
810 | // Find the mid idx where we are at or just past the previous time.
811 |
812 | var minIdx = 0;
813 | var maxIdx = this.events.length - 1;
814 |
815 | var midIdx = 0;
816 |
817 | while (minIdx <= maxIdx)
818 | {
819 | midIdx = Math.floor((minIdx + maxIdx) / 2);
820 |
821 | //console.log(midIdx);
822 |
823 | var midTime = this.events[midIdx].time;
824 |
825 | var leftTime = (midIdx === 0)? -Infinity:this.events[midIdx-1].time;
826 |
827 | if (leftTime < prevTime && midTime >= prevTime)
828 | break;
829 |
830 | if (midTime < prevTime)
831 | minIdx = midIdx + 1;
832 | else
833 | maxIdx = midIdx - 1;
834 | }
835 |
836 | // If no event to dispatch were found, stop
837 | if (minIdx > maxIdx)
838 | return;
839 |
840 | // Dispatch all events up to the current time (exclusive)
841 | for (var idx = midIdx; idx < this.events.length; ++idx)
842 | {
843 | var evt = this.events[idx];
844 |
845 | if (evt.time >= curTime)
846 | break;
847 |
848 | //console.log('Dispatch: ' + evt);
849 |
850 | this.target.processEvent(evt, realTime);
851 | }
852 | }
853 |
854 | /**
855 | Clear all the events from this track
856 | */
857 | Track.prototype.clear = function ()
858 | {
859 | this.events = [];
860 | }
861 |
862 | //============================================================================
863 | // Synthesis events
864 | //============================================================================
865 |
866 | /**
867 | @class Base class for all synthesis events.
868 | */
869 | function SynthEvt()
870 | {
871 | /**
872 | Event occurrence time
873 | */
874 | this.time = 0;
875 | }
876 |
877 | /**
878 | Format a synthesis event string representation
879 | */
880 | SynthEvt.formatStr = function (evt, str)
881 | {
882 | return evt.time.toFixed(2) + ': ' + str;
883 | }
884 |
885 | /**
886 | Default string representation for events
887 | */
888 | SynthEvt.prototype.toString = function ()
889 | {
890 | return SynthEvt.formatStr(this, 'event');
891 | }
892 |
893 | /**
894 | @class Note on event
895 | */
896 | function NoteOnEvt(time, note, vel)
897 | {
898 | // By default, use the C4 note
899 | if (note === undefined)
900 | note = new Note(C4_NOTE_NO);
901 |
902 | // By default, 50% velocity
903 | if (vel === undefined)
904 | vel = 0.5;
905 |
906 | /**
907 | Note
908 | */
909 | this.note = note;
910 |
911 | /**
912 | Velocity
913 | */
914 | this.vel = vel;
915 |
916 | // Set the event time
917 | this.time = time;
918 | }
919 | NoteOnEvt.prototype = new SynthEvt();
920 |
921 | /**
922 | Default string representation for events
923 | */
924 | NoteOnEvt.prototype.toString = function ()
925 | {
926 | return SynthEvt.formatStr(this, 'note-on ' + this.note);
927 | }
928 |
929 | /**
930 | @class Note off event
931 | */
932 | function NoteOffEvt(time, note)
933 | {
934 | // By default, use the C4 note
935 | if (note === undefined)
936 | note = new Note(C4_NOTE_NO);
937 |
938 | /**
939 | Note
940 | */
941 | this.note = note;
942 |
943 | // Set the event time
944 | this.time = time;
945 | }
946 | NoteOffEvt.prototype = new SynthEvt();
947 |
948 | /**
949 | Default string representation for events
950 | */
951 | NoteOffEvt.prototype.toString = function ()
952 | {
953 | return SynthEvt.formatStr(this, 'note-off ' + this.note);
954 | }
955 |
956 | /**
957 | @class All notes off event. Silences instruments.
958 | */
959 | function AllNotesOffEvt(time)
960 | {
961 | this.time = time;
962 | }
963 | AllNotesOffEvt.prototype = new SynthEvt();
964 |
965 | /**
966 | Default string representation for events
967 | */
968 | AllNotesOffEvt.prototype.toString = function ()
969 | {
970 | return SynthEvt.formatStr(this, 'all notes off');
971 | }
972 |
973 |
--------------------------------------------------------------------------------
/publish.sh:
--------------------------------------------------------------------------------
1 | git push -f origin master:gh-pages
2 |
--------------------------------------------------------------------------------
/samples/biab_trance_clap_2.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maximecb/Turing-Tunes/f1cca5220e2fa88bb09693b55eb3f554e0d5c4bc/samples/biab_trance_clap_2.wav
--------------------------------------------------------------------------------
/samples/biab_trance_hat_6.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maximecb/Turing-Tunes/f1cca5220e2fa88bb09693b55eb3f554e0d5c4bc/samples/biab_trance_hat_6.wav
--------------------------------------------------------------------------------
/samples/biab_trance_kick_4.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maximecb/Turing-Tunes/f1cca5220e2fa88bb09693b55eb3f554e0d5c4bc/samples/biab_trance_kick_4.wav
--------------------------------------------------------------------------------
/samples/biab_trance_snare_2.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maximecb/Turing-Tunes/f1cca5220e2fa88bb09693b55eb3f554e0d5c4bc/samples/biab_trance_snare_2.wav
--------------------------------------------------------------------------------
/samples/closed_hat.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maximecb/Turing-Tunes/f1cca5220e2fa88bb09693b55eb3f554e0d5c4bc/samples/closed_hat.wav
--------------------------------------------------------------------------------
/samples/one-two-three-four.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maximecb/Turing-Tunes/f1cca5220e2fa88bb09693b55eb3f554e0d5c4bc/samples/one-two-three-four.wav
--------------------------------------------------------------------------------
/sampling.js:
--------------------------------------------------------------------------------
1 | /*****************************************************************************
2 | *
3 | * CodeBeats : Online Music Coding Platform
4 | *
5 | * This file is part of the CodeBeats project. The project is distributed at:
6 | * https://github.com/maximecb/CodeBeats
7 | *
8 | * Copyright (c) 2012, Maxime Chevalier-Boisvert
9 | * All rights reserved.
10 | *
11 | * This software is licensed under the following license (Modified BSD
12 | * License):
13 | *
14 | * Redistribution and use in source and binary forms, with or without
15 | * modification, are permitted provided that the following conditions are
16 | * met:
17 | * * Redistributions of source code must retain the above copyright
18 | * notice, this list of conditions and the following disclaimer.
19 | * * Redistributions in binary form must reproduce the above copyright
20 | * notice, this list of conditions and the following disclaimer in the
21 | * documentation and/or other materials provided with the distribution.
22 | * * Neither the name of the Universite de Montreal nor the names of its
23 | * contributors may be used to endorse or promote products derived
24 | * from this software without specific prior written permission.
25 | *
26 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
27 | * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
28 | * TO, THE IMPLIED WARRANTIES OF MERCHApNTABILITY AND FITNESS FOR A
29 | * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL UNIVERSITE DE
30 | * MONTREAL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
31 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
32 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
33 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
34 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
35 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
36 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37 | *
38 | *****************************************************************************/
39 |
40 | /**
41 | @class Loads a sample asynchronously from a URL
42 | */
43 | function Sample(url)
44 | {
45 | /**
46 | Sample URL
47 | */
48 | this.url = url;
49 |
50 | /**
51 | Audio data buffer, undefined until loaded
52 | */
53 | this.buffer = undefined;
54 |
55 | console.log('loading sample "' + url + '"');
56 |
57 | var xhr = new XMLHttpRequest();
58 | xhr.open("GET", url, true);
59 | xhr.responseType = "arraybuffer";
60 |
61 | var that = this;
62 | xhr.onload = function()
63 | {
64 | try
65 | {
66 | audioCtx.decodeAudioData(
67 | xhr.response,
68 | function (audioBuffer)
69 | {
70 | var f32buffer = audioBuffer.getChannelData(0);
71 | var f64buffer = new Float64Array(f32buffer.length);
72 | for (var i = 0; i < f32buffer.length; ++i)
73 | f64buffer[i] = f32buffer[i];
74 |
75 | that.buffer = f64buffer;
76 | }
77 | );
78 | }
79 |
80 | catch (e)
81 | {
82 | console.error('failed to load "' + url + '"');
83 | console.error(e.toString());
84 | }
85 |
86 | //console.log('loaded sample "' + url + '" (' + that.buffer.length + ')');
87 | };
88 |
89 | xhr.send();
90 | }
91 |
92 | /**
93 | @class Basic sample-mapping instrument
94 | @extends AudioNode
95 | */
96 | function SampleKit()
97 | {
98 | /**
99 | Array of samples, indexed by MIDI note numbers
100 | */
101 | this.samples = [];
102 |
103 | /**
104 | Array of active (currently playing) samples
105 | */
106 | this.actSamples = [];
107 |
108 | // Sound output
109 | new AudioOutput(this, 'output');
110 |
111 | this.name = 'sample-kit';
112 | }
113 | SampleKit.prototype = new AudioNode();
114 |
115 | /**
116 | Map a sample to a given note
117 | */
118 | SampleKit.prototype.mapSample = function (note, sample, volume)
119 | {
120 | if ((note instanceof Note) == false)
121 | note = new Note(note);
122 |
123 | if (typeof sample === 'string')
124 | sample = new Sample(sample);
125 |
126 | if (volume === undefined)
127 | volume = 1;
128 |
129 | this.samples[note.noteNo] = {
130 | data: sample,
131 | volume: volume
132 | }
133 | }
134 |
135 | /**
136 | Process an event
137 | */
138 | SampleKit.prototype.processEvent = function (evt, time)
139 | {
140 | // Note-on event
141 | if (evt instanceof NoteOnEvt)
142 | {
143 | // Get the note
144 | var note = evt.note;
145 |
146 | var sample = this.samples[note.noteNo];
147 |
148 | // If no sample is mapped to this note, do nothing
149 | if (sample === undefined)
150 | return;
151 |
152 | // If the sample is not yet loaded, do nothing
153 | if (sample.data.buffer === undefined)
154 | return;
155 |
156 | // Add a new instance to the active list
157 | this.actSamples.push({
158 | sample: sample,
159 | pos: 0
160 | });
161 | }
162 |
163 | // All notes off event
164 | else if (evt instanceof AllNotesOffEvt)
165 | {
166 | this.actSamples = [];
167 | }
168 |
169 | // By default, do nothing
170 | }
171 |
172 | /**
173 | Update the outputs based on the inputs
174 | */
175 | SampleKit.prototype.update = function (time, sampleRate)
176 | {
177 | // If there are no active samples, do nothing
178 | if (this.actSamples.length === 0)
179 | return;
180 |
181 | // Get the output buffer
182 | var outBuf = this.output.getBuffer(0);
183 |
184 | // Initialize the output to 0
185 | for (var i = 0; i < outBuf.length; ++i)
186 | outBuf[i] = 0;
187 |
188 | // For each active sample instance
189 | for (var i = 0; i < this.actSamples.length; ++i)
190 | {
191 | var actSample = this.actSamples[i];
192 |
193 | var inBuf = actSample.sample.data.buffer;
194 |
195 | var volume = actSample.sample.volume;
196 |
197 | assert (
198 | inBuf instanceof Float64Array,
199 | 'invalid input buffer'
200 | );
201 |
202 | var playLen = Math.min(outBuf.length, inBuf.length - actSample.pos);
203 |
204 | for (var outIdx = 0; outIdx < playLen; ++outIdx)
205 | outBuf[outIdx] += inBuf[actSample.pos + outIdx] * volume;
206 |
207 | actSample.pos += playLen;
208 |
209 | // If this sample is done playing
210 | if (actSample.pos === inBuf.length)
211 | {
212 | // Remove the sample from the active list
213 | this.actSamples.splice(i, 1);
214 | --i;
215 | }
216 | }
217 | }
218 |
219 | /**
220 | @class Sample-based pitch-shifting instrument
221 | @extends AudioNode
222 | */
223 | function SampleInstr(sample, centerNote)
224 | {
225 | if (typeof sample === 'string')
226 | sample = new Sample(sample);
227 |
228 | if (typeof centerNote === 'string')
229 | centerNote = new Note(centerNote);
230 |
231 | /**
232 | Sample data
233 | */
234 | this.sample = sample;
235 |
236 | /**
237 | Center note/pitch for the sample
238 | */
239 | this.centerNote = centerNote;
240 |
241 | /**
242 | List of active notes
243 | */
244 | this.actNotes = [];
245 |
246 | // TODO: loop points
247 |
248 | // Sound output
249 | new AudioOutput(this, 'output');
250 |
251 | this.name = 'sample-instr';
252 | }
253 | SampleInstr.prototype = new AudioNode();
254 |
255 | /**
256 | Process an event
257 | */
258 | SampleInstr.prototype.processEvent = function (evt, time)
259 | {
260 | // Note-on event
261 | if (evt instanceof NoteOnEvt)
262 | {
263 | // If the sample is not yet loaded, stop
264 | if (this.sample.buffer === undefined)
265 | return;
266 |
267 | // Get the note
268 | var note = evt.note;
269 |
270 | var centerFreq = this.centerNote.getFreq();
271 | var noteFreq = note.getFreq();
272 | var freqRatio = noteFreq / centerFreq;
273 |
274 | // Add an entry to the active note list
275 | this.actNotes.push({
276 | pos: 0,
277 | freqRatio: freqRatio
278 | });
279 | }
280 |
281 | // Note-off event
282 | if (evt instanceof NoteOffEvt)
283 | {
284 | // Get the note
285 | var note = evt.note;
286 |
287 | // TODO: loop points
288 | }
289 |
290 | // All notes off event
291 | else if (evt instanceof AllNotesOffEvt)
292 | {
293 | this.actNotes = [];
294 | }
295 |
296 | // By default, do nothing
297 | }
298 |
299 | /**
300 | Update the outputs based on the inputs
301 | */
302 | SampleInstr.prototype.update = function (time, sampleRate)
303 | {
304 | // If there are no active notes, do nothing
305 | if (this.actNotes.length === 0)
306 | return;
307 |
308 | // Get the output buffer
309 | var outBuf = this.output.getBuffer(0);
310 |
311 | // Initialize the output to 0
312 | for (var i = 0; i < outBuf.length; ++i)
313 | outBuf[i] = 0;
314 |
315 | // Get the sample buffer
316 | var inBuf = this.sample.buffer;
317 |
318 | // For each active note
319 | for (var i = 0; i < this.actNotes.length; ++i)
320 | {
321 | var actNote = this.actNotes[i];
322 |
323 | // Compute the displacement between sample points
324 | var disp = actNote.freqRatio;
325 |
326 | var pos = actNote.pos;
327 |
328 | // For each output sample to produce
329 | for (var outIdx = 0; outIdx < outBuf.length; ++outIdx)
330 | {
331 | var lIdx = Math.floor(pos);
332 | var rIdx = lIdx + 1;
333 |
334 | if (rIdx >= inBuf.length)
335 | break;
336 |
337 | var lVal = inBuf[lIdx];
338 | var rVal = inBuf[rIdx];
339 | var oVal = lVal * (rIdx - pos) + rVal * (pos - lIdx);
340 |
341 | outBuf[outIdx] = oVal;
342 |
343 | // Update the sample position
344 | pos += disp;
345 | }
346 |
347 | // Store the final sample position
348 | actNote.pos = pos;
349 |
350 | // If the note is done playing
351 | if (pos >= inBuf.length)
352 | {
353 | // Remove the note from the active list
354 | this.actNotes.splice(i, 1);
355 | --i;
356 | }
357 | }
358 | }
359 |
360 |
--------------------------------------------------------------------------------
/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 | #titleHeader
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.faqLink
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 |
--------------------------------------------------------------------------------
/utils-html.js:
--------------------------------------------------------------------------------
1 | /*****************************************************************************
2 | *
3 | * This file is part of the Turing-Tunes project. The project is
4 | * distributed at:
5 | * https://github.com/maximecb/Turing-Tunes
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 | // Default console logging function implementation
38 | if (!window.console) console = {};
39 | console.log = console.log || function() {};
40 | console.warn = console.warn || function() {};
41 | console.error = console.error || function() {};
42 | console.info = console.info || function() {};
43 | print = function (v) { console.log(String(v)); }
44 |
45 | // Check for typed array support
46 | if (!this.Int8Array)
47 | {
48 | console.log('No Int8Array support');
49 | Int8Array = Array;
50 | }
51 | if (!this.Uint16Array)
52 | {
53 | console.log('No Uint16Array support');
54 | Uint16Array = Array;
55 | }
56 | if (!this.Int32Array)
57 | {
58 | console.log('No Int32Array support');
59 | Int32Array = Array;
60 | }
61 | if (!this.Float64Array)
62 | {
63 | console.log('No Float64Array support');
64 | Float64Array = Array;
65 | }
66 |
67 | /**
68 | Escape a string for valid HTML formatting
69 | */
70 | function escapeHTML(str)
71 | {
72 | str = str.replace(/\n/g, ' ');
73 | str = str.replace(/ /g, ' ');
74 | str = str.replace(/\t/g, ' ');
75 |
76 | return str;
77 | }
78 |
79 | /**
80 | Encode an array of bytes into base64 string format
81 | */
82 | function encodeBase64(data)
83 | {
84 | assert (
85 | data instanceof Array,
86 | 'invalid data array'
87 | );
88 |
89 | var str = '';
90 |
91 | function encodeChar(bits)
92 | {
93 | //console.log(bits);
94 |
95 | var ch;
96 |
97 | if (bits < 26)
98 | ch = String.fromCharCode(65 + bits);
99 | else if (bits < 52)
100 | ch = String.fromCharCode(97 + (bits - 26));
101 | else if (bits < 62)
102 | ch = String.fromCharCode(48 + (bits - 52));
103 | else if (bits === 62)
104 | ch = '+';
105 | else
106 | ch = '/';
107 |
108 | str += ch;
109 | }
110 |
111 | for (var i = 0; i < data.length; i += 3)
112 | {
113 | var numRem = data.length - i;
114 |
115 | // 3 bytes -> 4 base64 chars
116 | var b0 = data[i];
117 | var b1 = (numRem >= 2)? data[i+1]:0
118 | var b2 = (numRem >= 3)? data[i+2]:0
119 |
120 | var bits = (b0 << 16) + (b1 << 8) + b2;
121 |
122 | encodeChar((bits >> 18) & 0x3F);
123 | encodeChar((bits >> 12) & 0x3F);
124 |
125 | if (numRem >= 2)
126 | {
127 | encodeChar((bits >> 6) & 0x3F);
128 |
129 | if (numRem >= 3)
130 | encodeChar((bits >> 0) & 0x3F);
131 | else
132 | str += '=';
133 | }
134 | else
135 | {
136 | str += '==';
137 | }
138 | }
139 |
140 | return str;
141 | }
142 |
143 | // TODO
144 | // TODO: decodeBase64(str)
145 | // TODO
146 |
147 |
--------------------------------------------------------------------------------
/utils-misc.js:
--------------------------------------------------------------------------------
1 | /*****************************************************************************
2 | *
3 | * This file is part of the Turing-Tunes project. The project is
4 | * distributed at:
5 | * https://github.com/maximecb/Turing-Tunes
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 | Assert that a condition holds true
39 | */
40 | function assert(condition, errorText)
41 | {
42 | if (!condition)
43 | {
44 | error(errorText);
45 | }
46 | }
47 |
48 | /**
49 | Abort execution because a critical error occurred
50 | */
51 | function error(errorText)
52 | {
53 | alert('ERROR: ' + errorText);
54 |
55 | throw errorText;
56 | }
57 |
58 | /**
59 | Test that a value is integer
60 | */
61 | function isInt(val)
62 | {
63 | return (
64 | Math.floor(val) === val
65 | );
66 | }
67 |
68 | /**
69 | Test that a value is a nonnegative integer
70 | */
71 | function isNonNegInt(val)
72 | {
73 | return (
74 | isInt(val) &&
75 | val >= 0
76 | );
77 | }
78 |
79 | /**
80 | Test that a value is a strictly positive (nonzero) integer
81 | */
82 | function isPosInt(val)
83 | {
84 | return (
85 | isInt(val) &&
86 | val > 0
87 | );
88 | }
89 |
90 | /**
91 | Get the current time in millisseconds
92 | */
93 | function getTimeMillis()
94 | {
95 | return (new Date()).getTime();
96 | }
97 |
98 | /**
99 | Get the current time in seconds
100 | */
101 | function getTimeSecs()
102 | {
103 | return (new Date()).getTime() / 1000;
104 | }
105 |
106 | /**
107 | Generate a random integer within [a, b]
108 | */
109 | function randomInt(a, b)
110 | {
111 | assert (
112 | isInt(a) && isInt(b) && a <= b,
113 | 'invalid params to randomInt'
114 | );
115 |
116 | var range = b - a;
117 |
118 | var rnd = a + Math.floor(Math.random() * (range + 1));
119 |
120 | return rnd;
121 | }
122 |
123 | /**
124 | Generate a random boolean
125 | */
126 | function randomBool()
127 | {
128 | return (randomInt(0, 1) === 1);
129 | }
130 |
131 | /**
132 | Generate a random floating-point number within [a, b]
133 | */
134 | function randomFloat(a, b)
135 | {
136 | if (a === undefined)
137 | a = 0;
138 | if (b === undefined)
139 | b = 1;
140 |
141 | assert (
142 | a <= b,
143 | 'invalid params to randomFloat'
144 | );
145 |
146 | var range = b - a;
147 |
148 | var rnd = a + Math.random() * range;
149 |
150 | return rnd;
151 | }
152 |
153 | /**
154 | Generate a random value from a normal distribution
155 | */
156 | function randomNorm(mean, variance)
157 | {
158 | // Declare variables for the points and radius
159 | var x1, x2, w;
160 |
161 | // Repeat until suitable points are found
162 | do
163 | {
164 | x1 = 2.0 * randomFloat() - 1.0;
165 | x2 = 2.0 * randomFloat() - 1.0;
166 | w = x1 * x1 + x2 * x2;
167 | } while (w >= 1.0 || w == 0);
168 |
169 | // compute the multiplier
170 | w = Math.sqrt((-2.0 * Math.log(w)) / w);
171 |
172 | // compute the gaussian-distributed value
173 | var gaussian = x1 * w;
174 |
175 | // Shift the gaussian value according to the mean and variance
176 | return (gaussian * variance) + mean;
177 | }
178 |
179 | /**
180 | Choose a random argument value uniformly randomly
181 | */
182 | function randomChoice()
183 | {
184 | assert (
185 | arguments.length > 0,
186 | 'must supply at least one possible choice'
187 | );
188 |
189 | var idx = randomInt(0, arguments.length - 1);
190 |
191 | return arguments[idx];
192 | }
193 |
194 | /**
195 | Generate a weighed random choice function
196 | */
197 | function genChoiceFn()
198 | {
199 | assert (
200 | arguments.length > 0 && arguments.length % 2 === 0,
201 | 'invalid argument count: ' + arguments.length
202 | );
203 |
204 | var numChoices = arguments.length / 2;
205 |
206 | var choices = [];
207 | var weights = [];
208 | var weightSum = 0;
209 |
210 | for (var i = 0; i < numChoices; ++i)
211 | {
212 | var choice = arguments[2*i];
213 | var weight = arguments[2*i + 1];
214 |
215 | choices.push(choice);
216 | weights.push(weight);
217 |
218 | weightSum += weight;
219 | }
220 |
221 | assert (
222 | weightSum > 0,
223 | 'weight sum must be positive'
224 | );
225 |
226 | var limits = [];
227 | var limitSum = 0;
228 |
229 | for (var i = 0; i < weights.length; ++i)
230 | {
231 | var normWeight = weights[i] / weightSum;
232 |
233 | limitSum += normWeight;
234 |
235 | limits[i] = limitSum;
236 | }
237 |
238 | function choiceFn()
239 | {
240 | var r = Math.random();
241 |
242 | for (var i = 0; i < numChoices; ++i)
243 | {
244 | if (r < limits[i])
245 | return choices[i];
246 | }
247 |
248 | return choices[numChoices-1];
249 | }
250 |
251 | return choiceFn;
252 | }
253 |
254 | /**
255 | Left-pad a string to a minimum length
256 | */
257 | function leftPadStr(str, minLen, padStr)
258 | {
259 | if (padStr === undefined)
260 | padStr = ' ';
261 |
262 | var str = String(str);
263 |
264 | while (str.length < minLen)
265 | str = padStr + str;
266 |
267 | return str;
268 | }
269 |
270 | /**
271 | Capitalize a string
272 | */
273 | function capitalize(str)
274 | {
275 | if (str.length === 0)
276 | return str;
277 |
278 | return str[0].toUpperCase() + str.substr(1);
279 | }
280 |
281 | /**
282 | Resample and normalize an array of data points
283 | */
284 | function resample(data, numSamples, outLow, outHigh, inLow, inHigh)
285 | {
286 | // Compute the number of data points per samples
287 | var ptsPerSample = data.length / numSamples;
288 |
289 | // Compute the number of samples
290 | var numSamples = Math.floor(data.length / ptsPerSample);
291 |
292 | // Allocate an array for the output samples
293 | var samples = new Array(numSamples);
294 |
295 | // Extract the samples
296 | for (var i = 0; i < numSamples; ++i)
297 | {
298 | samples[i] = 0;
299 |
300 | var startI = Math.floor(i * ptsPerSample);
301 | var endI = Math.min(Math.ceil((i+1) * ptsPerSample), data.length);
302 | var numPts = endI - startI;
303 |
304 | for (var j = startI; j < endI; ++j)
305 | samples[i] += data[j];
306 |
307 | samples[i] /= numPts;
308 | }
309 |
310 | // If the input range is not specified
311 | if (inLow === undefined && inHigh === undefined)
312 | {
313 | // Min and max sample values
314 | var inLow = Infinity;
315 | var inHigh = -Infinity;
316 |
317 | // Compute the min and max sample values
318 | for (var i = 0; i < numSamples; ++i)
319 | {
320 | inLow = Math.min(inLow, samples[i]);
321 | inHigh = Math.max(inHigh, samples[i]);
322 | }
323 | }
324 |
325 | // Compute the input range
326 | var iRange = (inHigh > inLow)? (inHigh - inLow):1;
327 |
328 | // Compute the output range
329 | var oRange = outHigh - outLow;
330 |
331 | // Normalize the samples
332 | samples.forEach(
333 | function (v, i)
334 | {
335 | var normVal = (v - inLow) / iRange;
336 | samples[i] = outLow + (normVal * oRange);
337 | }
338 | );
339 |
340 | // Return the normalized samples
341 | return samples;
342 | }
343 |
344 |
--------------------------------------------------------------------------------
/vanalog.js:
--------------------------------------------------------------------------------
1 | /*****************************************************************************
2 | *
3 | * This file is part of the Turing-Tunes project. The project is
4 | * distributed at:
5 | * https://github.com/maximecb/Turing-Tunes
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 AudioNode
40 | */
41 | function VAnalog(numOscs)
42 | {
43 | if (numOscs === undefined)
44 | numOscs = 1;
45 |
46 | this.name = 'vanalog';
47 |
48 | /**
49 | Array of oscillator parameters
50 | */
51 | this.oscs = new Array(numOscs);
52 |
53 | // Initialize the oscillator parameters
54 | for (var i = 0; i < numOscs; ++i)
55 | {
56 | var osc = this.oscs[i] = {};
57 |
58 | // Oscillator type
59 | osc.type = 'sine';
60 |
61 | // Duty cycle, for pulse wave
62 | osc.duty = 0.5;
63 |
64 | // Oscillator detuning, in cents
65 | osc.detune = 0;
66 |
67 | // ADSR amplitude envelope
68 | osc.env = new ADSREnv(0.05, 0.05, 0.2, 0.1);
69 |
70 | // Mixing volume
71 | osc.volume = 1;
72 |
73 | // Oscillator sync flag
74 | osc.sync = false;
75 |
76 | // Syncing oscillator detuning
77 | osc.syncDetune = 0;
78 | }
79 |
80 | /**
81 | Filter cutoff [0,1]
82 | */
83 | this.cutoff = 1;
84 |
85 | /**
86 | Filter resonance [0,1]
87 | */
88 | this.resonance = 0;
89 |
90 | /**
91 | Filter envelope
92 | */
93 | this.filterEnv = new ADSREnv(0, 0, 1, Infinity);
94 |
95 | /**
96 | Filter envelope modulation amount
97 | */
98 | this.filterEnvAmt = 1;
99 |
100 | /**
101 | Pitch envelope
102 | */
103 | this.pitchEnv = new ADSREnv(0, 0, 1, Infinity);
104 |
105 | /**
106 | Pitch envelope amount
107 | */
108 | this.pitchEnvAmt = 0;
109 |
110 | /**
111 | Active/on note array
112 | */
113 | this.actNotes = [];
114 |
115 | /**
116 | Temporary oscillator buffer, for intermediate processing
117 | */
118 | this.oscBuf = new Float64Array(AUDIO_BUF_SIZE);
119 |
120 | /**
121 | Temporary note buffer, for intermediate processing
122 | */
123 | this.noteBuf = new Float64Array(AUDIO_BUF_SIZE);
124 |
125 | // Sound output
126 | new AudioOutput(this, 'output');
127 | }
128 | VAnalog.prototype = new AudioNode();
129 |
130 | /**
131 | Process an event
132 | */
133 | VAnalog.prototype.processEvent = function (evt, time)
134 | {
135 | // Note-on event
136 | if (evt instanceof NoteOnEvt)
137 | {
138 | // Get the note
139 | var note = evt.note;
140 |
141 | noteState = {};
142 |
143 | // Note being played
144 | noteState.note = note;
145 |
146 | // Note velocity
147 | noteState.vel = evt.vel;
148 |
149 | // Time a note-on was received
150 | noteState.onTime = time;
151 |
152 | // Time a note-off was received
153 | noteState.offTime = 0;
154 |
155 | // Initialize the oscillator states
156 | noteState.oscs = new Array(this.oscs.length);
157 | for (var i = 0; i < this.oscs.length; ++i)
158 | {
159 | var oscState = {};
160 | noteState.oscs[i] = oscState;
161 |
162 | // Cycle position
163 | oscState.cyclePos = 0;
164 |
165 | // Sync cycle position
166 | oscState.syncCyclePos = 0;
167 |
168 | // Envelope amplitude at note-on and note-off time
169 | oscState.onAmp = 0;
170 | oscState.offAmp = 0;
171 | }
172 |
173 | // Initialize the filter state values
174 | noteState.filterSt = new Array(8);
175 | for (var i = 0; i < noteState.filterSt.length; ++i)
176 | noteState.filterSt[i] = 0;
177 |
178 | // Filter envelope value at note-on and note-off time
179 | noteState.filterOnEnv = 0;
180 | noteState.filterOffEnv = 0;
181 |
182 | // Pitch envelope value at note-on and note-off time
183 | noteState.pitchOnEnv = 0;
184 | noteState.pitchOffEnv = 0;
185 |
186 | // Add the note to the active list
187 | this.actNotes.push(noteState);
188 |
189 | //console.log('on time: ' + noteState.onTime);
190 | }
191 |
192 | // Note-off event
193 | else if (evt instanceof NoteOffEvt)
194 | {
195 | // Get the note
196 | var note = evt.note;
197 |
198 | // Try to find the note among the active list
199 | for (var i = 0; i < this.actNotes.length; ++i)
200 | {
201 | var noteState = this.actNotes[i];
202 |
203 | // If this is the note we are looking for
204 | if (noteState.note === note)
205 | {
206 | // Store the oscillator amplitudes at note-off time
207 | for (var j = 0; j < this.oscs.length; ++j)
208 | {
209 | var oscState = noteState.oscs[j];
210 |
211 | oscState.offAmp = this.oscs[j].env.getValue(
212 | time,
213 | noteState.onTime,
214 | noteState.offTime,
215 | oscState.onAmp,
216 | oscState.offAmp
217 | );
218 | }
219 |
220 | // Filter envelope value at note-off time
221 | noteState.filterOffEnv = this.filterEnv.getValue(
222 | time,
223 | noteState.onTime,
224 | noteState.offTime,
225 | noteState.filterOnEnv,
226 | noteState.filterOffEnv
227 | );
228 |
229 | // Pitch envelope value at note-off time
230 | noteState.pitchOffEnv = this.pitchEnv.getValue(
231 | time,
232 | noteState.onTime,
233 | noteState.offTime,
234 | noteState.pitchOnEnv,
235 | noteState.pitchOffEnv
236 | );
237 |
238 | // Set the note-off time
239 | noteState.offTime = time;
240 | }
241 | }
242 | }
243 |
244 | // All notes off event
245 | else if (evt instanceof AllNotesOffEvt)
246 | {
247 | this.actNotes = [];
248 | }
249 |
250 | // By default, do nothing
251 | }
252 |
253 | /**
254 | Update the outputs based on the inputs
255 | */
256 | VAnalog.prototype.update = function (time, sampleRate)
257 | {
258 | // If there are no active notes, do nothing
259 | if (this.actNotes.length === 0)
260 | return;
261 |
262 | // Get the output buffer
263 | var outBuf = this.output.getBuffer(0);
264 |
265 | // Initialize the output to 0
266 | for (var i = 0; i < outBuf.length; ++i)
267 | outBuf[i] = 0;
268 |
269 | // Get the time at the end of the buffer
270 | var endTime = time + ((outBuf.length - 1) / sampleRate);
271 |
272 | // For each active note
273 | for (var i = 0; i < this.actNotes.length; ++i)
274 | {
275 | var noteState = this.actNotes[i];
276 |
277 | // Initialize the note buffer to 0
278 | for (var smpIdx = 0; smpIdx < outBuf.length; ++smpIdx)
279 | this.noteBuf[smpIdx] = 0;
280 |
281 | // Maximum end amplitude value
282 | var maxEndAmp = 0;
283 |
284 | // For each oscillator
285 | for (var oscNo = 0; oscNo < this.oscs.length; ++oscNo)
286 | {
287 | var oscParams = this.oscs[oscNo];
288 | var oscState = noteState.oscs[oscNo];
289 |
290 | // Generate the oscillator signal
291 | this.genOsc(
292 | time,
293 | this.oscBuf,
294 | oscParams,
295 | oscState,
296 | noteState,
297 | sampleRate
298 | );
299 |
300 | // Compute the note volume
301 | var noteVol = noteState.vel * oscParams.volume;
302 |
303 | // Get the amplitude value at the start of the buffer
304 | var ampStart = noteVol * oscParams.env.getValue(
305 | time,
306 | noteState.onTime,
307 | noteState.offTime,
308 | oscState.onAmp,
309 | oscState.offAmp
310 | );
311 |
312 | // Get the envelope value at the end of the buffer
313 | var ampEnd = noteVol * oscParams.env.getValue(
314 | endTime,
315 | noteState.onTime,
316 | noteState.offTime,
317 | oscState.onAmp,
318 | oscState.offAmp
319 | );
320 |
321 | //console.log('start time: ' + time);
322 | //console.log('start env: ' + envStart);
323 | //console.log('end: ' + envEnd);
324 |
325 | // Update the maximum end envelope value
326 | maxEndAmp = Math.max(maxEndAmp, ampEnd);
327 |
328 | // Modulate the output based on the amplitude envelope
329 | for (var smpIdx = 0; smpIdx < outBuf.length; ++smpIdx)
330 | {
331 | var ratio = (smpIdx / (outBuf.length - 1));
332 | this.oscBuf[smpIdx] *= ampStart + ratio * (ampEnd - ampStart);
333 | }
334 |
335 | // Accumulate the sample values in the note buffer
336 | for (var smpIdx = 0; smpIdx < outBuf.length; ++smpIdx)
337 | this.noteBuf[smpIdx] += this.oscBuf[smpIdx];
338 | }
339 |
340 | // Apply the filter to the temp buffer
341 | this.applyFilter(time, noteState, this.noteBuf);
342 |
343 | // Accumulate the sample values in the output buffer
344 | for (var smpIdx = 0; smpIdx < outBuf.length; ++smpIdx)
345 | outBuf[smpIdx] += this.noteBuf[smpIdx];
346 |
347 | // If all envelopes have fallen to 0, remove the note from the active list
348 | if (maxEndAmp === 0)
349 | {
350 | this.actNotes.splice(i, 1);
351 | i--;
352 | }
353 | }
354 | }
355 |
356 | /**
357 | Generate output for an oscillator and update its position
358 | */
359 | VAnalog.prototype.genOsc = function (
360 | time,
361 | outBuf,
362 | oscParams,
363 | oscState,
364 | noteState,
365 | sampleRate
366 | )
367 | {
368 | // Get the pitch envelope detuning value
369 | var envDetune = this.pitchEnvAmt * this.pitchEnv.getValue(
370 | time,
371 | noteState.onTime,
372 | noteState.offTime,
373 | noteState.pitchOnEnv,
374 | noteState.pitchOffEnv
375 | );
376 |
377 | // Get the note
378 | var note = noteState.note;
379 |
380 | // Get the oscillator frequency
381 | var freq = note.getFreq(oscParams.detune + envDetune);
382 |
383 | // Get the initial cycle position
384 | var cyclePos = oscState.cyclePos;
385 |
386 | // Compute the cycle position change between samples
387 | var deltaPos = freq / sampleRate;
388 |
389 | // Get the sync oscillator frequency
390 | var syncFreq = note.getFreq(oscParams.syncDetune);
391 |
392 | // Get the initial sync cycle position
393 | var syncCyclePos = oscState.syncCyclePos;
394 |
395 | // Compute the cycle position change between samples
396 | var syncDeltaPos = syncFreq / sampleRate;
397 |
398 | // For each sample to be produced
399 | for (var i = 0; i < outBuf.length; ++i)
400 | {
401 | // Switch on the oscillator type/waveform
402 | switch (oscParams.type)
403 | {
404 | // Sine wave
405 | case 'sine':
406 | outBuf[i] = Math.sin(2 * Math.PI * cyclePos);
407 | break;
408 |
409 | // Triangle wave
410 | case 'triangle':
411 | if (cyclePos < 0.5)
412 | outBuf[i] = (4 * cyclePos) - 1;
413 | else
414 | outBuf[i] = 1 - (4 * (cyclePos - 0.5));
415 | break;
416 |
417 | // Sawtooth wave
418 | case 'sawtooth':
419 | outBuf[i] = -1 + (2 * cyclePos);
420 | break;
421 |
422 | // Pulse wave
423 | case 'pulse':
424 | if (cyclePos < oscParams.duty)
425 | outBuf[i] = -1;
426 | else
427 | outBuf[i] = 1;
428 | break;
429 |
430 | // Noise
431 | case 'noise':
432 | outBuf[i] = 1 - 2 * Math.random();
433 | break;
434 |
435 | default:
436 | error('invalid waveform: ' + oscParams.type);
437 | }
438 |
439 | cyclePos += deltaPos;
440 |
441 | if (cyclePos > 1)
442 | cyclePos -= 1;
443 |
444 | if (oscParams.sync === true)
445 | {
446 | syncCyclePos += syncDeltaPos;
447 |
448 | if (syncCyclePos > 1)
449 | {
450 | syncCyclePos -= 1;
451 | cyclePos = 0;
452 | }
453 | }
454 | }
455 |
456 | // Set the final cycle position
457 | oscState.cyclePos = cyclePos;
458 |
459 | // Set the final sync cycle position
460 | oscState.syncCyclePos = syncCyclePos;
461 | }
462 |
463 | /**
464 | Apply a filter to a buffer of samples
465 | IIR, 2-pole, resonant Low Pass Filter (LPF)
466 | */
467 | VAnalog.prototype.applyFilter = function (time, noteState, buffer)
468 | {
469 | assert (
470 | this.cutoff >= 0 && this.cutoff <= 1,
471 | 'invalid filter cutoff'
472 | );
473 |
474 | assert (
475 | this.resonance >= 0 && this.resonance <= 1,
476 | 'invalid filter resonance'
477 | );
478 |
479 | var filterEnvVal = this.filterEnv.getValue(
480 | time,
481 | noteState.onTime,
482 | noteState.offTime,
483 | noteState.filterOnEnv,
484 | noteState.filterOffEnv
485 | );
486 |
487 | var filterEnvMag = (1 - this.cutoff) * this.filterEnvAmt;
488 |
489 | var cutoff = this.cutoff + filterEnvMag * filterEnvVal;
490 |
491 | var c = Math.pow(0.5, (1 - cutoff) / 0.125);
492 | var r = Math.pow(0.5, (this.resonance + 0.125) / 0.125);
493 |
494 | var mrc = 1 - r * c;
495 |
496 | var v0 = noteState.filterSt[0];
497 | var v1 = noteState.filterSt[1];
498 |
499 | for (var i = 0; i < buffer.length; ++i)
500 | {
501 | v0 = (mrc * v0) - (c * v1) + (c * buffer[i]);
502 | v1 = (mrc * v1) + (c * v0);
503 |
504 | buffer[i] = v1;
505 | }
506 |
507 | noteState.filterSt[0] = v0;
508 | noteState.filterSt[1] = v1;
509 | }
510 |
511 |
--------------------------------------------------------------------------------