├── 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 |
--------------------------------------------------------------------------------