├── README.md ├── css └── index.css ├── images └── favicon.ico ├── index.html └── js ├── index.js └── libs └── budio.req.js /README.md: -------------------------------------------------------------------------------- 1 | # music 2 | Procedural music with Javascript 3 | 4 | Demo: [wybiral.github.io/music](https://wybiral.github.io/music/) 5 | -------------------------------------------------------------------------------- /css/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | html, body { 7 | width: 100%; 8 | height: 100%; 9 | background: #fff; 10 | } 11 | 12 | #bg0, #bg1 { 13 | position: fixed; 14 | top: 0; 15 | right: 0; 16 | bottom: 0; 17 | left: 0; 18 | } 19 | 20 | #bg0, #bg1 { 21 | transition: opacity 5s linear; 22 | } 23 | 24 | h1 { 25 | font-family: sans-serif; 26 | text-align: center; 27 | } 28 | 29 | #bg0 { 30 | color: #0f0; 31 | background: #000; 32 | padding-top: 2em; 33 | } 34 | 35 | #bg1 { 36 | opacity: 0; 37 | padding-top: 4em; 38 | } -------------------------------------------------------------------------------- /images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wybiral/music/7c746e72c639cae97bc5e4ff22d4b3f1c51de645/images/favicon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Random Music 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

Click to start

16 |
17 |
18 |

19 |
20 | 21 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | const BudioContext = require('budio').Context; 2 | const Note = require('budio').Note; 3 | const Scale = require('budio').Scale; 4 | 5 | window.onload = () => { 6 | document.body.addEventListener('click', start); 7 | }; 8 | 9 | function start() { 10 | document.body.removeEventListener('click', start); 11 | const budio = new BudioContext(); 12 | const style = new Style(); 13 | let index = 0; 14 | const backgrounds = [ 15 | document.getElementById('bg0'), 16 | document.getElementById('bg1') 17 | ]; 18 | const titles = [ 19 | document.querySelector('#bg0 > h1'), 20 | document.querySelector('#bg1 > h1') 21 | ]; 22 | let time = budio.now; 23 | const loop = () => { 24 | // Less than 1 second queued up, generate more 25 | if (time - budio.now < 1.0) { 26 | backgrounds[index].style.opacity = 0; 27 | style.randomize(budio); 28 | const d = Math.random() * 365 | 0; 29 | const a = randomColor(); 30 | const b = randomColor(); 31 | const grad = 'linear-gradient(' + d + 'deg,' + a + ',' + b + ')'; 32 | index = 1 - index; 33 | titles[index].style.fontFamily = randomFont(); 34 | titles[index].innerText = style.key.note + ' ' + style.scale.name; 35 | backgrounds[index].style.background = grad; 36 | backgrounds[index].style.color = '#000'; 37 | backgrounds[index].style.opacity = 1; 38 | setTimeout(() => { 39 | let note = style.key; 40 | for (let i = 0; i < 40; i++) { 41 | [note, time] = style.play(budio, note, time); 42 | } 43 | style.key = note; 44 | }, 0); 45 | } 46 | setTimeout(loop, 100); 47 | }; 48 | loop(); 49 | } 50 | 51 | // Randomizing musical style class 52 | class Style { 53 | 54 | constructor() { 55 | this.key = randomNote(); 56 | } 57 | 58 | randomize(budio) { 59 | this.scale = randomScale(this.key); 60 | this.shape = randomShape(budio); 61 | this.harmonics = randomHarmonics(); 62 | this.timing = randomTiming(); 63 | this.flow = Math.random(); 64 | } 65 | 66 | next(note) { 67 | let interval = 1 + (3 * Math.pow(Math.random(), this.flow * 2)) | 0; 68 | if (Math.random() < 0.5) { 69 | interval = -interval; 70 | } 71 | note = this.scale.transpose(note, interval); 72 | if (note.octave < 3) { 73 | // Correct for octave being too low 74 | note = note.toOctave(note.octave + 1); 75 | } 76 | if (note.octave > 5) { 77 | // Correct for octave being too high 78 | note = note.toOctave(note.octave - 1); 79 | } 80 | return note 81 | } 82 | 83 | play(budio, note, time) { 84 | const duration = choice(this.timing); 85 | budio.play(this.hit(budio, note, duration * 1.5), time); 86 | return [this.next(note), time + duration]; 87 | } 88 | 89 | hit(budio, note, duration) { 90 | const harmonics = this.harmonics; 91 | const frequency = note.frequency; 92 | const vector = budio.silence(duration); 93 | for (let i = 0; i < harmonics.length; i++) { 94 | const wave = budio.sin(frequency * (i + 1), duration); 95 | vector.add(wave.mul(harmonics[i])); 96 | } 97 | return vector.mul(this.shape(duration)).mul(0.2 / harmonics.length); 98 | } 99 | 100 | } 101 | 102 | 103 | // Generate a random note 104 | const randomNote = () => new Note(choice(Note.NOTES)); 105 | 106 | // Generate a random scale 107 | const randomScale = key => { 108 | const scales = [ 109 | 'major', 110 | 'minor', 111 | 'wholetone', 112 | 'japanese', 113 | 'augmented', 114 | 'augmented fifth', 115 | 'diminished', 116 | 'blues major', 117 | 'blues minor', 118 | 'harmonic minor', 119 | 'pentatonic minor', 120 | 'pentatonic major', 121 | ]; 122 | return new Scale(key, choice(scales)); 123 | }; 124 | 125 | // Generate a random hit shape 126 | const randomShape = budio => { 127 | const choices = [ 128 | duration => { 129 | const shape = budio.range(duration); 130 | shape.div(shape.length); 131 | shape.map(x => 4 * Math.pow(x, 0.5) - 4 * x); 132 | return shape; 133 | }, 134 | duration => { 135 | const shape = budio.range(duration); 136 | shape.div(shape.length); 137 | shape.map(x => 3.53 * Math.pow(x, 0.227) - 3.53 * Math.pow(x, 0.5)); 138 | return shape; 139 | }, 140 | duration => { 141 | const shape = budio.range(duration); 142 | shape.div(shape.length); 143 | shape.map(x => 1.716 * Math.pow(x, 0.5) - 1.716 * Math.pow(x, 3)); 144 | return shape; 145 | }, 146 | duration => { 147 | const shape = budio.range(duration); 148 | shape.div(shape.length); 149 | shape.map(x => 1.315 * Math.pow(x, 0.5) - 1.315 * Math.pow(x, 7)); 150 | return shape; 151 | }, 152 | ]; 153 | if (Math.random() < 0.1) { 154 | // This one isn't always an option 155 | choices.push(duration => { 156 | const shape = budio.range(duration); 157 | shape.mul(7 * Math.PI / budio.rate); 158 | return shape.sin().abs().mul(0.75).add(0.25); 159 | }); 160 | } 161 | return choice(choices); 162 | }; 163 | 164 | // Generate random harmonic components 165 | const randomHarmonics = () => { 166 | const harmonics = []; 167 | for (let i = 0; i < 8; i++) { 168 | harmonics.push(Math.random()); 169 | } 170 | return harmonics; 171 | }; 172 | 173 | // Generate random timing components 174 | const randomTiming = () => { 175 | const timing = []; 176 | const speed = (Math.random() * 0.3) / 2 + 0.075; 177 | const options = [speed, speed * 2, speed * 2, speed * 4]; 178 | for (let i = 0; i < 7; i++) { 179 | timing.push(choice(options)); 180 | } 181 | return timing; 182 | }; 183 | 184 | // Pick a random value from an array 185 | const choice = array => { 186 | return array[Math.random() * array.length | 0]; 187 | }; 188 | 189 | const randomFont = () => { 190 | return choice([ 191 | 'serif', 192 | 'sans-serif', 193 | 'monospace', 194 | 'cursive', 195 | ]); 196 | }; 197 | 198 | const randomColor = () => { 199 | const r = Math.random() * 256 | 0; 200 | const g = Math.random() * 256 | 0; 201 | const b = Math.random() * 256 | 0; 202 | return 'rgb(' + r + ',' + g + ',' + b + ')'; 203 | }; -------------------------------------------------------------------------------- /js/libs/budio.req.js: -------------------------------------------------------------------------------- 1 | require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o i); 51 | } 52 | 53 | seconds(duration) { 54 | const rate = this.rate; 55 | return this.silence(duration).map((x, i) => i / rate); 56 | } 57 | 58 | sin(frequency, duration) { 59 | const factor = Math.PI * 2 * frequency; 60 | return this.seconds(duration).mul(factor).sin(); 61 | } 62 | 63 | saw(frequency, duration) { 64 | const period = Math.floor(this.rate / frequency); 65 | const vector = this.silence(duration); 66 | vector.map((x, i) => { 67 | return ((i % period) / period) * 2 - 1; 68 | }); 69 | return vector; 70 | } 71 | 72 | square(frequency, duration) { 73 | const period = Math.floor(this.rate / frequency); 74 | const halfPeriod = (period / 2) | 0; 75 | const vector = this.silence(duration); 76 | vector.map((x, i) => { 77 | return (i % period) < halfPeriod ? -1 : 1; 78 | }); 79 | return vector; 80 | } 81 | 82 | triangle(frequency, duration) { 83 | const period = Math.floor(this.rate / frequency); 84 | const hp = Math.floor(period / 2); 85 | const vector = this.silence(duration); 86 | vector.map((x, i) => { 87 | return ((hp - Math.abs(i % period - hp)) / hp) * 2 - 1; 88 | }); 89 | return vector; 90 | } 91 | 92 | }; 93 | 94 | },{"./vector":4}],2:[function(require,module,exports){ 95 | class Note { 96 | 97 | constructor(note) { 98 | if (typeof note === 'string') { 99 | note = parseNote(note); 100 | } 101 | this.index = note; 102 | } 103 | 104 | toString() { 105 | return this.note + this.octave; 106 | } 107 | 108 | get note() { 109 | return Note.NOTES[this.noteIndex]; 110 | } 111 | 112 | get noteIndex() { 113 | return this.index % 12; 114 | } 115 | 116 | get octave() { 117 | return Math.floor(this.index / 12); 118 | } 119 | 120 | get frequency() { 121 | const C0 = 16.35159783128741; 122 | return C0 * Math.pow(2.0, this.index / 12.0); 123 | } 124 | 125 | transpose(delta) { 126 | return new Note(this.index + delta); 127 | } 128 | 129 | toOctave(octave) { 130 | return new Note(this.noteIndex + 12 * octave); 131 | } 132 | 133 | }; 134 | module.exports = Note; 135 | 136 | Note.NOTES = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B']; 137 | 138 | const parseNote = note => { 139 | let octave = 4; 140 | note = note.trim().toUpperCase(); 141 | // Parse octave 142 | if (!isNaN(parseInt(note[note.length - 1]))) { 143 | octave = parseInt(note[note.length - 1]); 144 | note = note.substr(0, note.length - 1); 145 | } 146 | let index = Note.NOTES.indexOf(note[0]); 147 | // Parse accidentals 148 | for (let i = 1; i < note.length; i++) { 149 | if (note[i] === '#') { 150 | index++; 151 | } else if (note[i] === 'b') { 152 | index--; 153 | } 154 | } 155 | return (index % 12) + 12 * octave; 156 | }; 157 | 158 | },{}],3:[function(require,module,exports){ 159 | module.exports = class Scale { 160 | 161 | constructor(root, name) { 162 | this.root = root.toOctave(0); 163 | this.intervals = SCALES[name.replace('-', '').replace(' ', '')]; 164 | this.name = name; 165 | } 166 | 167 | get(index) { 168 | const intervals = this.intervals; 169 | let note = this.root; 170 | for (let i = 0; i < index; i++) { 171 | note = note.transpose(intervals[i % intervals.length]); 172 | } 173 | return note; 174 | } 175 | 176 | index(note) { 177 | const intervals = this.intervals; 178 | let i = 0; 179 | let x = this.root; 180 | for (; x.index < note.index; i++) { 181 | x = x.transpose(intervals[i % intervals.length]); 182 | } 183 | if (x.index !== note.index) { 184 | throw new Error('Note not in scale'); 185 | } 186 | return i; 187 | } 188 | 189 | transpose(note, interval) { 190 | return this.get(this.index(note) + interval); 191 | } 192 | 193 | } 194 | 195 | 196 | const SCALES = { 197 | 'major': [2, 2, 1, 2, 2, 2, 1], 198 | 'minor': [2, 1, 2, 2, 1, 2, 2], 199 | 'melodicminor': [2, 1, 2, 2, 2, 2, 1], 200 | 'harmonicminor': [2, 1, 2, 2, 1, 3, 1], 201 | 'pentatonicmajor': [2, 2, 3, 2, 3], 202 | 'bluesmajor': [3, 2, 1, 1, 2, 3], 203 | 'pentatonicminor': [3, 2, 2, 3, 2], 204 | 'bluesminor': [3, 2, 1, 1, 3, 2], 205 | 'augmented': [3, 1, 3, 1, 3, 1], 206 | 'diminished': [2, 1, 2, 1, 2, 1, 2, 1], 207 | 'chromatic': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 208 | 'wholehalf': [2, 1, 2, 1, 2, 1, 2, 1], 209 | 'halfwhole': [1, 2, 1, 2, 1, 2, 1, 2], 210 | 'wholetone': [2, 2, 2, 2, 2, 2], 211 | 'augmentedfifth': [2, 2, 1, 2, 1, 1, 2, 1], 212 | 'japanese': [1, 4, 2, 1, 4], 213 | 'oriental': [1, 3, 1, 1, 3, 1, 2], 214 | 'ionian': [2, 2, 1, 2, 2, 2, 1], 215 | 'dorian': [2, 1, 2, 2, 2, 1, 2], 216 | 'phrygian': [1, 2, 2, 2, 1, 2, 2], 217 | 'lydian': [2, 2, 2, 1, 2, 2, 1], 218 | 'mixolydian': [2, 2, 1, 2, 2, 1, 2], 219 | 'aeolian': [2, 1, 2, 2, 1, 2, 2], 220 | 'locrian': [1, 2, 2, 1, 2, 2, 2] 221 | } 222 | 223 | },{}],4:[function(require,module,exports){ 224 | module.exports = class Vector { 225 | 226 | constructor(context, array) { 227 | this.context = context; 228 | this.array = array; 229 | } 230 | 231 | get length() { 232 | return this.array.length; 233 | } 234 | 235 | map(fn) { 236 | const array = this.array; 237 | const length = array.length; 238 | for (let i = 0; i < length; i++) { 239 | array[i] = fn(array[i], i); 240 | } 241 | return this; 242 | } 243 | 244 | map2(that, fn) { 245 | const a = this.array; 246 | const b = that.array; 247 | const length = Math.min(a.length, b.length); 248 | for (let i = 0; i < length; i++) { 249 | a[i] = fn(a[i], b[i], i); 250 | } 251 | return this; 252 | } 253 | 254 | add(that) { 255 | if (typeof that === 'number') { 256 | return this.map(x => x + that); 257 | } else { 258 | return this.map2(that, (x, y) => x + y); 259 | } 260 | } 261 | 262 | sub(that) { 263 | if (typeof that === 'number') { 264 | return this.map(x => x - that); 265 | } else { 266 | return this.map2(that, (x, y) => x - y); 267 | } 268 | } 269 | 270 | mul(that) { 271 | if (typeof that === 'number') { 272 | return this.map(x => x * that); 273 | } else { 274 | return this.map2(that, (x, y) => x * y); 275 | } 276 | } 277 | 278 | div(that) { 279 | if (typeof that === 'number') { 280 | return this.map(x => x / that); 281 | } else { 282 | return this.map2(that, (x, y) => x / y); 283 | } 284 | } 285 | 286 | pow(that) { 287 | if (typeof that === 'number') { 288 | return this.map(x => Math.pow(x, that)); 289 | } else { 290 | return this.map2(that, (x, y) => Math.pow(x, y)); 291 | } 292 | } 293 | 294 | sin() { 295 | return this.map(Math.sin); 296 | } 297 | 298 | cos() { 299 | return this.map(Math.cos); 300 | } 301 | 302 | sqrt() { 303 | return this.map(Math.sqrt); 304 | } 305 | 306 | abs() { 307 | return this.map(Math.abs); 308 | } 309 | 310 | }; 311 | 312 | },{}],"budio":[function(require,module,exports){ 313 | module.exports = { 314 | Context: require('./context'), 315 | Vector: require('./vector'), 316 | Note: require('./music/note'), 317 | Scale: require('./music/scale'), 318 | }; 319 | },{"./context":1,"./music/note":2,"./music/scale":3,"./vector":4}]},{},[]); 320 | --------------------------------------------------------------------------------