├── 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 | Fork me on GitHub 26 | 27 | 28 |
29 | Turing Tunes 30 |
31 | 32 |
33 | Click "generate" to produce a new random machine/tune. 34 |
35 | 36 |
37 |
38 |
39 | Machine options 40 | Number of states: 41 | 43 | Number of symbols: 44 | 46 | Filter out duds: 47 | 48 |
49 | 50 |
51 | Musical scale 52 | Root note: 53 | 55 | Scale type: 56 | 58 |
59 | 60 |
61 | Octaves covered 62 | C2 63 | C3 64 | C4 65 | C5 (middle C) 66 | C6 67 | C7 68 |
69 | 70 |
71 | Note durations 72 | Eight 73 | Quarter 74 | Half 75 | Whole 76 |
77 | 78 |
79 | Drum samples 80 |
81 | 82 |
83 | Time signature 84 | Tempo: 85 | 86 | Time signature: 87 | 88 | 89 |
90 | 91 |
92 | Actions 93 | 94 |      95 | 96 |      97 | 98 |
99 |
100 | 101 |
102 | 103 | Your browser does not support the canvas element. 104 | 105 |
106 | 107 |
108 |
109 | Share this online 110 | Give this creation a name: 111 | 112 |

113 | 114 | 115 |
116 |
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 | 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 | --------------------------------------------------------------------------------