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
14 const BEAT_LOUDNESS = [
21 function timeout (s, cb) {
22 return setTimeout(cb, s * 1000)
25 // This can make one musical note at a time
30 this.gain = ctx.createGain()
31 this.gain.gain.setValueAtTime(0.00001, ctx.currentTime)
32 this.oscillator = ctx.createOscillator()
35 this.oscillator.connect(this.gain)
36 this.oscillator.start()
39 let duration = args.duration || 1
40 let amplitude = args.amplitude || 0.3
42 if (args.grow != null) {
45 if (duration >= 0.1) {
50 grow *= 0.7 + 440 / args.frequency * 0.6
52 if (args.decay != null) {
60 decay *= 0.7 + args.frequency / 440 * 0.6
62 if (grow + decay / 2 >= duration) {
63 end = args.time + grow + decay
65 end = args.time + duration + decay / 2
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)
76 // This can play as many notes as you like at once
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()
87 console.log("Your browser doesn't support Web Audio")
93 this.epoc = this.ctx.currentTime
97 // return a channel that can be used starting at time
100 for (i in this.channels) {
101 if (time >= this.channels[i].done_at) {
102 return this.channels[i]
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]
112 // required args: frequency, time
114 let channel = this.get_channel(args.time)
118 while (this.channels.length) {
119 this.channels[0].gain.disconnect()
120 this.channels.shift()
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)
129 for (let f = LOW_D, i = 0; i < 37; f *= HALF_STEP, ++i) {
157 let CHR_a = 'a'.charCodeAt(0)
160 constructor(value, priority) {
162 this.priority = priority
166 class NoteInScaleChoice extends Choice {
167 constructor(value, priority) {
168 super(value, priority)
169 this.freq = scale[value]
175 constructor (...args) {
176 this.setting = new Map()
180 this.total_priority = 0
181 this.relationships = new Set()
185 if (this.chosen_key === key) {
188 this.chosen_key = key
189 this.chosen = this.setting.get(key).choice
193 return this.chosen_key
195 if (this.setting.length === 1) {
196 this.chosen = this.setting.get('a').value
197 return this.chosen_key = 'a'
199 let win = Math.random() * this.total_priority - 0.000001
201 for (let [key, c] of this.setting) {
204 this.chosen = c.choice
205 return this.chosen_key = c.key
210 add (choice, priority) {
211 let key = String.fromCharCode(CHR_a + this.setting.size)
212 this.setting.set(key, {
217 this.total_priority += priority
219 follow (relationship) {
220 this.relationships.add(relationship)
222 unfollow (relationship) {
223 this.relationships.delete(relationship)
227 this.relationships.clear()
230 this.relationships.clear()
234 class BassNoteSetting extends setting {
247 class HarmonyNoteSetting extends setting {
259 class MelodyNoteSetting extends setting {
273 constructor (...settings) {
274 this.settings = settings
275 for (let setting of settings) {
280 for (let setting of this.settings) {
286 class NoopRel extends Relationship {
287 constructor (...settings) {
288 super() // don't pass args
295 // create a subclass of relationship that initializes with the last X items of
297 const LastXMixin = (x, base_class) => class extends base_class {
298 constructor (notes) {
299 if (notes.length < x) {
303 for (let i = 0; i < x; ++i) {
304 settings.unshift(notes[i].note)
311 // pass two note numbers
312 // rates how good they sound at the same time
313 class Chord2Rel extends Relationship {
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]
322 class HigherGoodRel extends Relationship {
324 if (this.settings[0].chosen > this.settings[1].chosen) {
331 class RangeGood8Rel extends Relationship {
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) {
339 } else if (chosen > highest) {
343 return (highest - lowest) / 9
346 const L8RangeGood8Rel = LastXMixin(8, RangeGood8Rel)
348 class WalkRel extends Relationship {
350 let a = this.settings[0].chosen
351 let b = this.settings[1].chosen
355 if (Math.abs(a - b) === 1) {
358 let fives = [4, 7, 11]
360 if (a in fives && b in fives) {
363 // jumping a third to the third is pretty cool
364 if ((a === 7 || a === 11) && b === 9) {
368 if (Math.abs(a - b) === 2) {
369 if (a in fives || b in fives) {
377 const L2WalkRel = LastXMixin(2, WalkRel)
379 class SameBad2Rel extends Relationship {
382 for (let i = 0; i < 2; ++i) {
383 if (this.settings[i].chosen == this.settings[i+2].chosen) {
390 const L4SameBad2Rel = LastXMixin(4, SameBad2Rel)
392 class SameBad4Rel extends Relationship {
395 let prev_same = false
396 for (let i = 0; i < 4; ++i) {
397 if (this.settings[i].chosen == this.settings[i+4].chosen) {
410 return Math.pow(0.9, ick)
413 const L8SameBad4Rel = LastXMixin(8, SameBad4Rel)
415 class RunGood3Rel extends Relationship {
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)) {
427 const L3RunGood3Rel = LastXMixin(3, RunGood3Rel)
431 this.settings = new Set()
432 this.actors = new Set()
434 // snapshot some song values
435 this.time = song.next_beat_at
436 this.number = song.beat_number
437 this.measure = song.measure_number
442 add_setting (key, setting) {
446 this.settings.add(setting)
448 resolve_and_play () {
449 let already_keyss = new Set()
452 let relationships = new Set()
453 let settings = new Set()
455 for (let setting of this.settings) {
456 if (setting.relationships.size === 0) {
459 for (let r of setting.relationships) {
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)
469 // TODO add relationships that relate this back to settings
475 for (let r of relationships) {
476 best_score += r.score()
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) {
484 for (let setting of settings) {
485 keys += setting.choose()
487 if (already_keyss.has(keys)) {
490 for (let r of relationships) {
493 already_keyss.add(keys)
494 if (score > best_score) {
499 // set all setting to our favorite
501 for (let setting of settings) {
502 setting.set_to(best_keys.charAt(i++))
504 // lock only the ones in this beat
505 for (let setting of this.settings) {
509 for (let cb of this.actors) {
516 constructor (song, ...args) {
518 this.beat = song.beat
520 this.beat.on_play(this.play.bind(this))
523 class BassNote extends Note {
525 if (this.beat.measure_number % 4 === 2) {
526 this.note = this.song.bass[7].note
528 this.note = new BassNoteSetting()
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)
539 let amp = BEAT_LOUDNESS[this.beat.number]
541 frequency: scale[this.note.chosen],
542 time: this.beat.time,
543 duration: BEAT_LENGTH,
548 class QuickNote extends Note {
549 constructor (song, lag, ...args) {
550 super(song, lag, ...args)
551 this.time = this.beat.time
553 this.time += BEAT_LENGTH / 2
557 let amp = this.amp * BEAT_LOUDNESS[this.beat.number]
559 frequency: scale[this.note.chosen],
561 duration: BEAT_LENGTH / 2,
567 class MelodyNote extends QuickNote {
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
574 this.note = new MelodyNoteSetting()
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)
583 new L3RunGood3Rel(this.song.melody)
584 new L4SameBad2Rel(this.song.melody)
585 new L8SameBad4Rel(this.song.melody)
586 new L8RangeGood8Rel(this.song.melody)
590 class HarmonyNote extends QuickNote {
592 if (this.beat.measure_number % 4 === 3) {
593 this.note = this.song.harmony[7].note
595 this.note = new HarmonyNoteSetting()
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)
616 this.next_beat_timeout = null
617 this.next_beat_at = 0
623 this.measure_number = 0
626 this.np = new NotePlayer()
627 this.next_beat_at = this.np.ctx.currentTime + INITIAL_LEAD_TIME
632 this.next_beat_timeout = null
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) {
639 this.next_beat_timeout = timeout(until_next, this.scheduler.bind(this))
644 if (this.next_beat_timeout != null) {
645 clearTimeout(this.next_beat_timeout)
646 this.next_beat_timeout = null
651 this.beat = new Beat(this)
652 this.beats.unshift(this.beat)
655 new HarmonyNote(this)
656 new MelodyNote(this, true)
657 new HarmonyNote(this, true)
659 if (this.beats.length >= 5) {
660 this.beats[4].resolve_and_play()
663 if (this.beats.length > 15) {
673 this.beat_number = (this.beat_number + 1) % 4
674 if (this.beat_number === 0) {
675 this.measure_number += 1
677 this.next_beat_at += BEAT_LENGTH
682 let button = document.querySelector('button')
683 if (button == null) {
684 document.addEventListener('DOMContentLoaded', main)
689 window.song = song = new Song()
691 button.childNodes[0].textContent = 'Stop'
692 button.onclick = stop
698 button.childNodes[0].textContent = 'Play'
699 button.onclick = start
701 if (document.location.hash == '#autoplay') {