JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
style footer
[mimpro.git] / main.js
1 // SETTINGS
2 const INITIAL_LEAD_TIME = 0.05 // seconds delay at start of program for processing
3 const LEAD_TIME = 6.0 // seconds ahead of time to queue up notes
4 const BEAT_LENGTH = 0.5 // seconds per beat
5 const INTERVAL_GOODNESS = [ // when played simultaniously
6         2, // octave
7         0.0, // 2nd/7th
8         1.4, // 3rd/6th
9         1.8, // 4th/5th
10         1.8, // 4th/5th
11         1.6, // 3rd/6th
12     0.0, // 2nd/7th
13 ]
14 const BEAT_LOUDNESS = [
15         0.10,
16         0.07,
17         0.09,
18         0.05,
19 ]
20
21 function timeout (s, cb) {
22         return setTimeout(cb, s * 1000)
23 }
24
25 // This can make one musical note at a time
26 class NoteChannel {
27         constructor (ctx) {
28                 this.ctx = ctx
29                 this.done_at = 0
30                 this.gain = ctx.createGain()
31                 this.gain.gain.setValueAtTime(0.00001, ctx.currentTime)
32                 this.oscillator = ctx.createOscillator()
33         }
34         start () {
35                 this.oscillator.connect(this.gain)
36                 this.oscillator.start()
37         }
38         play (args) {
39                 let duration = args.duration || 1
40                 let amplitude = args.amplitude || 0.3
41                 let grow, decay, end
42                 if (args.grow != null) {
43                         grow = args.grow
44                 } else {
45                         if (duration >= 0.1) {
46                                 grow = 0.05
47                         } else {
48                                 grow = duration / 2
49                         }
50                         grow *= 0.7 + 440 / args.frequency * 0.6
51                 }
52                 if (args.decay != null) {
53                         decay = args.decay
54                 } else {
55                         if (duration >= 1) {
56                                 decay = 0.5
57                         } else {
58                                 decay = duration / 2
59                         }
60                         decay *= 0.7 + args.frequency / 440 * 0.6
61                 }
62                 if (grow + decay / 2 >= duration) {
63                         end = args.time + grow + decay
64                 } else {
65                         end = args.time + duration + decay / 2
66                 }
67                 this.done_at = end
68                 this.oscillator.frequency.setValueAtTime(args.frequency, args.time)
69                 this.gain.gain.setValueAtTime(0.00001, args.time)
70                 this.gain.gain.exponentialRampToValueAtTime(amplitude, args.time + grow)
71                 this.gain.gain.setValueAtTime(amplitude, end - decay)
72                 this.gain.gain.exponentialRampToValueAtTime(0.00001, end)
73         }
74 }
75
76 // This can play as many notes as you like at once
77 class NotePlayer {
78         constructor (args) {
79                 if (args == null) args = {}
80                 if (args.context != null) {
81                         this.ctx = args.context
82                 } else if (window.AudioContext) {
83                         this.ctx = new AudioContext()
84                 } else if (window.webkitAudioContext) {
85                         this.ctx = new webkitAudioContext()
86                 } else {
87                         console.log("Your browser doesn't support Web Audio")
88                         throw("heck");
89                 }
90                 if (args.epoc) {
91                         this.epoc = args.epoc
92                 } else {
93                         this.epoc = this.ctx.currentTime
94                 }
95                 this.channels = []
96         }
97         // return a channel that can be used starting at time
98         get_channel (time) {
99                 let i
100                 for (i in this.channels) {
101                         if (time >= this.channels[i].done_at) {
102                                 return this.channels[i]
103                         }
104                 }
105                 i = this.channels.length
106                 this.channels[i] = new NoteChannel(this.ctx)
107                 this.channels[i].gain.connect(this.ctx.destination)
108                 this.channels[i].start()
109                 // TODO use AudioContext.createDynamicsCompressor() to avoid clipping
110                 return this.channels[i]
111         }
112         // required args: frequency, time
113         play (args) {
114                 let channel = this.get_channel(args.time)
115                 channel.play(args)
116         }
117         stop () {
118                 while (this.channels.length) {
119                         this.channels[0].gain.disconnect()
120                         this.channels.shift()
121                 }
122         }
123 }
124
125 // equal temperment: multiply by this to go up a half step
126 const HALF_STEP = Math.pow(2, 1/12)
127 const LOW_D = 110 * Math.pow(2, 5/12)
128 let half_steps = []
129 for (let f = LOW_D, i = 0; i < 37; f *= HALF_STEP, ++i) {
130         half_steps.push(f)
131 }
132 let scale = [
133         half_steps[0],
134         half_steps[2],
135         half_steps[4],
136         half_steps[5],
137         half_steps[7],
138         half_steps[9],
139         half_steps[11],
140         half_steps[12],
141         half_steps[14],
142         half_steps[16],
143         half_steps[17],
144         half_steps[19],
145         half_steps[21],
146         half_steps[23],
147         half_steps[24],
148         half_steps[26],
149         half_steps[28],
150         half_steps[29],
151         half_steps[31],
152         half_steps[33],
153         half_steps[35],
154         half_steps[36],
155 ]
156
157 let CHR_a = 'a'.charCodeAt(0)
158
159 class Choice {
160         constructor(value, priority) {
161                 this.value = value
162                 this.priority = priority
163         }
164 }
165
166 class NoteInScaleChoice extends Choice {
167         constructor(value, priority) {
168                 super(value, priority)
169                 this.freq = scale[value]
170         }
171 }
172
173
174 class setting {
175         constructor (...args) {
176                 this.setting = new Map()
177                 this.locked = false
178                 this.chosen = null
179                 this.chosen_key = ''
180                 this.total_priority = 0
181                 this.relationships = new Set()
182                 this.init(...args)
183         }
184         set_to (key) {
185                 if (this.chosen_key === key) {
186                         return
187                 }
188                 this.chosen_key = key
189                 this.chosen = this.setting.get(key).choice
190         }
191         choose () {
192                 if (this.locked) {
193                         return this.chosen_key
194                 }
195                 if (this.setting.length === 1) {
196                         this.chosen = this.setting.get('a').value
197                         return this.chosen_key = 'a'
198                 }
199                 let win = Math.random() * this.total_priority - 0.000001
200                 let total = 0
201                 for (let [key, c] of this.setting) {
202                         total += c.priority
203                         if (total > win) {
204                                 this.chosen = c.choice
205                                 return this.chosen_key = c.key
206                         }
207                 }
208                 return 'WTF'
209         }
210         add (choice, priority) {
211                 let key = String.fromCharCode(CHR_a + this.setting.size)
212                 this.setting.set(key, {
213                         key: key,
214                         choice: choice,
215                         priority: priority,
216                 })
217                 this.total_priority += priority
218         }
219         follow (relationship) {
220                 this.relationships.add(relationship)
221         }
222         unfollow (relationship) {
223                 this.relationships.delete(relationship)
224         }
225         lock () {
226                 this.locked = true
227                 this.relationships.clear()
228         }
229         destroy () {
230                 this.relationships.clear()
231         }
232 }
233
234 class BassNoteSetting extends setting {
235         init () {
236                 this.add(4, 1)
237                 this.add(5, 0.4)
238                 this.add(6, 5)
239                 this.add(7, 2)
240                 this.add(8, 0.7)
241                 this.add(9, 1)
242                 this.add(10, 0.5)
243                 this.add(11, 1)
244         }
245 }
246
247 class HarmonyNoteSetting extends setting {
248         init () {
249                 this.add(9, 1)
250                 this.add(10, 0.5)
251                 this.add(11, 1)
252                 this.add(12, 0.4)
253                 this.add(13, 5)
254                 this.add(14, 2)
255                 this.add(15, 0.7)
256         }
257 }
258
259 class MelodyNoteSetting extends setting {
260         init () {
261                 this.add(11, 1)
262                 this.add(12, 0.4)
263                 this.add(13, 5)
264                 this.add(14, 2)
265                 this.add(15, 0.7)
266                 this.add(16, 1)
267                 this.add(17, 0.5)
268                 this.add(18, 1)
269         }
270 }
271
272 class Relationship {
273         constructor (...settings) {
274                 this.settings = settings
275                 for (let setting of settings) {
276                         setting.follow(this)
277                 }
278         }
279         destroy () {
280                 for (let setting of this.settings) {
281                         setting.follow(this)
282                 }
283         }
284 }
285
286 class NoopRel extends Relationship {
287         constructor (...settings) {
288                 super() // don't pass args
289         }
290         score () {
291                 return 0
292         }
293 }
294
295 // create a subclass of relationship that initializes with the last X items of
296 // constructor arg
297 const LastXMixin = (x, base_class) => class extends base_class {
298         constructor (notes) {
299                 if (notes.length < x) {
300                         return new NoopRel()
301                 } else {
302                         let settings = []
303                         for (let i = 0; i < x; ++i) {
304                                 settings.unshift(notes[i].note)
305                         }
306                         super(...settings)
307                 }
308         }
309 }
310
311 // pass two note numbers
312 // rates how good they sound at the same time
313 class Chord2Rel extends Relationship {
314         score () {
315                 let a = this.settings[0].chosen
316                 let b = this.settings[1].chosen
317                 let diff = Math.abs((a % 7) - (b % 7))
318                 return INTERVAL_GOODNESS[diff]
319         }
320 }
321
322 class HigherGoodRel extends Relationship {
323         score () {
324                 if (this.settings[0].chosen > this.settings[1].chosen) {
325                         return 1
326                 }
327                 return 0.5
328         }
329 }
330
331 class RangeGood8Rel extends Relationship {
332         score () {
333                 let lowest, highest
334                 lowest = highest = this.settings[0].chosen
335                 for (let i = 1; i < this.settings.length; ++i) {
336                         let chosen = this.settings[i].chosen
337                         if (chosen < lowest) {
338                                 lowest = chosen
339                         } else if (chosen > highest) {
340                                 highest = chosen
341                         }
342                 }
343                 return (highest - lowest) / 9
344         }
345 }
346 const L8RangeGood8Rel = LastXMixin(8, RangeGood8Rel)
347
348 class WalkRel extends Relationship {
349         score () {
350                 let a = this.settings[0].chosen
351                 let b = this.settings[1].chosen
352                 if (a === b) {
353                         return 0.2
354                 }
355                 if (Math.abs(a - b) === 1) {
356                         return 1
357                 }
358                 let fives = [4, 7, 11]
359                 // 1 <--> 5 is cool
360                 if (a in fives && b in fives) {
361                         return 0.8
362                 }
363                 // jumping a third to the third is pretty cool
364                 if ((a === 7 || a === 11) && b === 9) {
365                         return 0.8
366                 }
367                 // other thirds
368                 if (Math.abs(a - b) === 2) {
369                         if (a in fives || b in fives) {
370                                 return 0.4
371                         }
372                         return 0.3
373                 }
374                 return 0
375         }
376 }
377 const L2WalkRel = LastXMixin(2, WalkRel)
378
379 class SameBad2Rel extends Relationship {
380         score () {
381                 let ick = 0
382                 for (let i = 0; i < 2; ++i) {
383                         if (this.settings[i].chosen == this.settings[i+2].chosen) {
384                                 ick += 1
385                         }
386                 }
387                 return 1 - ick
388         }
389 }
390 const L4SameBad2Rel = LastXMixin(4, SameBad2Rel)
391
392 class SameBad4Rel extends Relationship {
393         score () {
394                 let ick = 0
395                 let prev_same = false
396                 for (let i = 0; i < 4; ++i) {
397                         if (this.settings[i].chosen == this.settings[i+4].chosen) {
398                                 ick += 1
399                                 if (prev_same) {
400                                         ick += 1
401                                 }
402                                 prev_same = true
403                         } else {
404                                 prev_same = false
405                         }
406                 }
407                 if (ick < 2) {
408                         return 1
409                 }
410                 return Math.pow(0.9, ick)
411         }
412 }
413 const L8SameBad4Rel = LastXMixin(8, SameBad4Rel)
414
415 class RunGood3Rel extends Relationship {
416         score () {
417                 let a = this.settings[0].chosen
418                 let b = this.settings[1].chosen
419                 let c = this.settings[2].chosen
420                 if (a !== b && Math.abs(a - b) == Math.abs(b - c)) {
421                         return 1
422                 } else {
423                         return 0.7
424                 }
425         }
426 }
427 const L3RunGood3Rel = LastXMixin(3, RunGood3Rel)
428
429 class Beat {
430         constructor (song) {
431                 this.settings = new Set()
432                 this.actors = new Set()
433                 this.song = song
434                 // snapshot some song values
435                 this.time = song.next_beat_at
436                 this.number = song.beat_number
437                 this.measure = song.measure_number
438         }
439         on_play (cb) {
440                 this.actors.add(cb)
441         }
442         add_setting (key, setting) {
443                 if (key != null) {
444                         this[key] = setting
445                 }
446                 this.settings.add(setting)
447         }
448         resolve_and_play () {
449                 let already_keyss = new Set()
450                 let best_keys = ''
451                 let best_score = 0
452                 let relationships = new Set()
453                 let settings = new Set()
454                 // initialize
455                 for (let setting of this.settings) {
456                         if (setting.relationships.size === 0) {
457                                 setting.choose()
458                         } else {
459                                 for (let r of setting.relationships) {
460                                         relationships.add(r)
461                                         // track setting on the other end
462                                         for (let setting of r.settings) {
463                                                 if (!settings.has(setting)) {
464                                                         // also choose and track
465                                                         if (!setting.locked) {
466                                                                 best_keys += setting.choose()
467                                                                 settings.add(setting)
468                                                         }
469                                                         // TODO add relationships that relate this back to settings
470                                                 }
471                                         }
472                                 }
473                         }
474                 }
475                 for (let r of relationships) {
476                         best_score += r.score()
477                 }
478                 already_keyss.add(best_keys)
479                 // try some combinations (brute force, random everything each time)
480                 // TODO be more efficient about which setting are changed
481                 for (let i = 0; i < 50; ++i) {
482                         let keys = ''
483                         let score = 0
484                         for (let setting of settings) {
485                                 keys += setting.choose()
486                         }
487                         if (already_keyss.has(keys)) {
488                                 continue
489                         }
490                         for (let r of relationships) {
491                                 score += r.score()
492                         }
493                         already_keyss.add(keys)
494                         if (score > best_score) {
495                                 best_keys = keys
496                                 best_score = score
497                         }
498                 }
499                 // set all setting to our favorite
500                 let i = 0
501                 for (let setting of settings) {
502                         setting.set_to(best_keys.charAt(i++))
503                 }
504                 // lock only the ones in this beat
505                 for (let setting of this.settings) {
506                         setting.lock()
507                 }
508                 // play
509                 for (let cb of this.actors) {
510                         cb(this)
511                 }
512         }
513 }
514
515 class Note {
516         constructor (song, ...args) {
517                 this.song = song
518                 this.beat = song.beat
519                 this.init(...args)
520                 this.beat.on_play(this.play.bind(this))
521         }
522 }
523 class BassNote extends Note {
524         init () {
525                 if (this.beat.measure_number % 4 === 2) {
526                         this.note = this.song.bass[7].note
527                 } else {
528                         this.note = new BassNoteSetting()
529                 }
530                 this.song.bass.unshift(this)
531                 this.beat.add_setting('bass_note', this.note)
532                 new L2WalkRel(this.song.bass)
533                 new L3RunGood3Rel(this.song.bass)
534                 new L4SameBad2Rel(this.song.bass)
535                 new L4SameBad2Rel(this.song.bass)
536                 new L8SameBad4Rel(this.song.bass)
537         }
538         play () {
539                 let amp = BEAT_LOUDNESS[this.beat.number]
540                 this.song.np.play({
541                         frequency: scale[this.note.chosen],
542                         time: this.beat.time,
543                         duration: BEAT_LENGTH,
544                         amplitude: amp
545                 })
546         }
547 }
548 class QuickNote extends Note {
549         constructor (song, lag, ...args) {
550                 super(song, lag, ...args)
551                 this.time = this.beat.time
552                 if (lag) {
553                         this.time += BEAT_LENGTH / 2
554                 }
555         }
556         play () {
557                 let amp = this.amp * BEAT_LOUDNESS[this.beat.number]
558                 this.song.np.play({
559                         frequency: scale[this.note.chosen],
560                         time: this.time,
561                         duration: BEAT_LENGTH / 2,
562                         amplitude: amp
563                 })
564         }
565 }
566
567 class MelodyNote extends QuickNote {
568         init () {
569                 if (this.beat.measure_number % 4 === 1 && this.beat.number < 3) {
570                         this.note = this.song.melody[7].note
571                 } else if (this.beat.measure_number % 4 === 2) {
572                         this.note = this.song.melody[15].note
573                 } else {
574                         this.note = new MelodyNoteSetting()
575                 }
576                 this.song.melody.unshift(this)
577                 this.beat.add_setting('melody_note', this.note)
578                 new Chord2Rel(this.song.bass[0].note, this.note)
579                 //new L2WalkRel(this.song.melody)
580                 if (this.song.melody.length > 1) {
581                         new WalkRel(this.song.melody[1].note, this.note)
582                 }
583                 new L3RunGood3Rel(this.song.melody)
584                 new L4SameBad2Rel(this.song.melody)
585                 new L8SameBad4Rel(this.song.melody)
586                 new L8RangeGood8Rel(this.song.melody)
587                 this.amp = 0.8
588         }
589 }
590 class HarmonyNote extends QuickNote {
591         init () {
592                 if (this.beat.measure_number % 4 === 3) {
593                         this.note = this.song.harmony[7].note
594                 } else {
595                         this.note = new HarmonyNoteSetting()
596                 }
597                 this.song.harmony.unshift(this)
598                 this.beat.add_setting('harmony_note', this.note)
599                 new Chord2Rel(this.song.bass[0].note, this.note)
600                 new Chord2Rel(this.song.melody[0].note, this.note)
601                 //new L2WalkRel(this.song.harmony)
602                 //new L3RunGood3Rel(this.song.harmony)
603                 new L4SameBad2Rel(this.song.harmony)
604                 new L8SameBad4Rel(this.song.harmony)
605                 new L8RangeGood8Rel(this.song.harmony)
606                 new HigherGoodRel(this.note, this.song.bass[0].note)
607                 new HigherGoodRel(this.song.melody[0].note, this.note)
608                 this.amp = 0.5
609         }
610 }
611
612 class Song {
613         constructor () {
614                 this.np = null
615                 this.beat = null
616                 this.next_beat_timeout = null
617                 this.next_beat_at = 0
618                 this.beats = []
619                 this.bass = []
620                 this.melody = []
621                 this.harmony = []
622                 this.beat_number = 0
623                 this.measure_number = 0
624         }
625         play () {
626                 this.np = new NotePlayer()
627                 this.next_beat_at = this.np.ctx.currentTime + INITIAL_LEAD_TIME
628
629                 this.scheduler()
630         }
631         scheduler () {
632                 this.next_beat_timeout = null
633                 this.next_beat()
634                 let next_output_at = this.next_beat_at - (4 * BEAT_LENGTH)
635                 let until_next = (next_output_at - LEAD_TIME) - this.np.ctx.currentTime
636                 if (until_next < 0.001) {
637                         this.scheduler()
638                 } else {
639                         this.next_beat_timeout = timeout(until_next, this.scheduler.bind(this))
640                 }
641         }
642
643         stop () {
644                 if (this.next_beat_timeout != null) {
645                         clearTimeout(this.next_beat_timeout)
646                         this.next_beat_timeout = null
647                 }
648                 this.np.stop()
649         }
650         next_beat () {
651                 this.beat = new Beat(this)
652                 this.beats.unshift(this.beat)
653                 new BassNote(this)
654                 new MelodyNote(this)
655                 new HarmonyNote(this)
656                 new MelodyNote(this, true)
657                 new HarmonyNote(this, true)
658
659                 if (this.beats.length >= 5) {
660                         this.beats[4].resolve_and_play()
661                 }
662
663                 if (this.beats.length > 15) {
664                         // FIXME
665                         this.beats.pop()
666                         this.bass.pop()
667                         this.melody.pop()
668                         this.melody.pop()
669                         this.harmony.pop()
670                         this.harmony.pop()
671                 }
672
673                 this.beat_number = (this.beat_number + 1) % 4
674                 if (this.beat_number === 0) {
675                         this.measure_number += 1
676                 }
677                 this.next_beat_at += BEAT_LENGTH
678         }
679 }
680
681 function main() {
682         let button = document.querySelector('button')
683         if (button == null) {
684                 document.addEventListener('DOMContentLoaded', main)
685                 return
686         }
687         let song = null
688         let start = () => {
689                 window.song = song = new Song()
690                 song.play()
691                 button.childNodes[0].textContent = 'Stop'
692                 button.onclick = stop
693         }
694         let stop = () => {
695                 if (song != null) {
696                         song.stop()
697                 }
698                 button.childNodes[0].textContent = 'Play'
699                 button.onclick = start
700         }
701         if (document.location.hash == '#autoplay') {
702                 start()
703         } else {
704                 stop()
705         }
706 }
707 main()