--- /dev/null
+// SETTINGS
+const INITIAL_LEAD_TIME = 0.05 // seconds delay at start of program for processing
+const LEAD_TIME = 6.0 // seconds ahead of time to queue up notes
+const BEAT_LENGTH = 0.5 // seconds per beat
+const INTERVAL_GOODNESS = [ // when played simultaniously
+ 2, // octave
+ 0.0, // 2nd/7th
+ 1.4, // 3rd/6th
+ 1.8, // 4th/5th
+ 1.8, // 4th/5th
+ 1.6, // 3rd/6th
+ 0.0, // 2nd/7th
+]
+const BEAT_LOUDNESS = [
+ 0.10,
+ 0.07,
+ 0.09,
+ 0.05,
+]
+
+function timeout (s, cb) {
+ return setTimeout(cb, s * 1000)
+}
+
+// This can make one musical note at a time
+class NoteChannel {
+ constructor (ctx) {
+ this.ctx = ctx
+ this.done_at = 0
+ this.gain = ctx.createGain()
+ this.gain.gain.setValueAtTime(0.00001, ctx.currentTime)
+ this.oscillator = ctx.createOscillator()
+ }
+ start () {
+ this.oscillator.connect(this.gain)
+ this.oscillator.start()
+ }
+ play (args) {
+ let duration = args.duration || 1
+ let amplitude = args.amplitude || 0.3
+ let grow, decay, end
+ if (args.grow != null) {
+ grow = args.grow
+ } else {
+ if (duration >= 0.1) {
+ grow = 0.05
+ } else {
+ grow = duration / 2
+ }
+ grow *= 0.7 + 440 / args.frequency * 0.6
+ }
+ if (args.decay != null) {
+ decay = args.decay
+ } else {
+ if (duration >= 1) {
+ decay = 0.5
+ } else {
+ decay = duration / 2
+ }
+ decay *= 0.7 + args.frequency / 440 * 0.6
+ }
+ if (grow + decay / 2 >= duration) {
+ end = args.time + grow + decay
+ } else {
+ end = args.time + duration + decay / 2
+ }
+ this.done_at = end
+ this.oscillator.frequency.setValueAtTime(args.frequency, args.time)
+ this.gain.gain.setValueAtTime(0.00001, args.time)
+ this.gain.gain.exponentialRampToValueAtTime(amplitude, args.time + grow)
+ this.gain.gain.setValueAtTime(amplitude, end - decay)
+ this.gain.gain.exponentialRampToValueAtTime(0.00001, end)
+ }
+}
+
+// This can play as many notes as you like at once
+class NotePlayer {
+ constructor (args) {
+ if (args == null) args = {}
+ if (args.context != null) {
+ this.ctx = args.context
+ } else if (window.AudioContext) {
+ this.ctx = new AudioContext()
+ } else if (window.webkitAudioContext) {
+ this.ctx = new webkitAudioContext()
+ } else {
+ console.log("Your browser doesn't support Web Audio")
+ throw("heck");
+ }
+ if (args.epoc) {
+ this.epoc = args.epoc
+ } else {
+ this.epoc = this.ctx.currentTime
+ }
+ this.channels = []
+ }
+ // return a channel that can be used starting at time
+ get_channel (time) {
+ let i
+ for (i in this.channels) {
+ if (time >= this.channels[i].done_at) {
+ return this.channels[i]
+ }
+ }
+ i = this.channels.length
+ this.channels[i] = new NoteChannel(this.ctx)
+ this.channels[i].gain.connect(this.ctx.destination)
+ this.channels[i].start()
+ // TODO use AudioContext.createDynamicsCompressor() to avoid clipping
+ return this.channels[i]
+ }
+ // required args: frequency, time
+ play (args) {
+ let channel = this.get_channel(args.time)
+ channel.play(args)
+ }
+ stop () {
+ while (this.channels.length) {
+ this.channels[0].gain.disconnect()
+ this.channels.shift()
+ }
+ }
+}
+
+// equal temperment: multiply by this to go up a half step
+const HALF_STEP = Math.pow(2, 1/12)
+const LOW_D = 110 * Math.pow(2, 5/12)
+let half_steps = []
+for (let f = LOW_D, i = 0; i < 37; f *= HALF_STEP, ++i) {
+ half_steps.push(f)
+}
+let scale = [
+ half_steps[0],
+ half_steps[2],
+ half_steps[4],
+ half_steps[5],
+ half_steps[7],
+ half_steps[9],
+ half_steps[11],
+ half_steps[12],
+ half_steps[14],
+ half_steps[16],
+ half_steps[17],
+ half_steps[19],
+ half_steps[21],
+ half_steps[23],
+ half_steps[24],
+ half_steps[26],
+ half_steps[28],
+ half_steps[29],
+ half_steps[31],
+ half_steps[33],
+ half_steps[35],
+ half_steps[36],
+]
+
+let CHR_a = 'a'.charCodeAt(0)
+
+class Choice {
+ constructor(value, priority) {
+ this.value = value
+ this.priority = priority
+ }
+}
+
+class NoteInScaleChoice extends Choice {
+ constructor(value, priority) {
+ super(value, priority)
+ this.freq = scale[value]
+ }
+}
+
+
+class setting {
+ constructor (...args) {
+ this.setting = new Map()
+ this.locked = false
+ this.chosen = null
+ this.chosen_key = ''
+ this.total_priority = 0
+ this.relationships = new Set()
+ this.init(...args)
+ }
+ set_to (key) {
+ if (this.chosen_key === key) {
+ return
+ }
+ this.chosen_key = key
+ this.chosen = this.setting.get(key).choice
+ }
+ choose () {
+ if (this.locked) {
+ return this.chosen_key
+ }
+ if (this.setting.length === 1) {
+ this.chosen = this.setting.get('a').value
+ return this.chosen_key = 'a'
+ }
+ let win = Math.random() * this.total_priority - 0.000001
+ let total = 0
+ for (let [key, c] of this.setting) {
+ total += c.priority
+ if (total > win) {
+ this.chosen = c.choice
+ return this.chosen_key = c.key
+ }
+ }
+ return 'WTF'
+ }
+ add (choice, priority) {
+ let key = String.fromCharCode(CHR_a + this.setting.size)
+ this.setting.set(key, {
+ key: key,
+ choice: choice,
+ priority: priority,
+ })
+ this.total_priority += priority
+ }
+ follow (relationship) {
+ this.relationships.add(relationship)
+ }
+ unfollow (relationship) {
+ this.relationships.delete(relationship)
+ }
+ lock () {
+ this.locked = true
+ this.relationships.clear()
+ }
+ destroy () {
+ this.relationships.clear()
+ }
+}
+
+class BassNoteSetting extends setting {
+ init () {
+ this.add(4, 1)
+ this.add(5, 0.4)
+ this.add(6, 5)
+ this.add(7, 2)
+ this.add(8, 0.7)
+ this.add(9, 1)
+ this.add(10, 0.5)
+ this.add(11, 1)
+ }
+}
+
+class HarmonyNoteSetting extends setting {
+ init () {
+ this.add(9, 1)
+ this.add(10, 0.5)
+ this.add(11, 1)
+ this.add(12, 0.4)
+ this.add(13, 5)
+ this.add(14, 2)
+ this.add(15, 0.7)
+ }
+}
+
+class MelodyNoteSetting extends setting {
+ init () {
+ this.add(11, 1)
+ this.add(12, 0.4)
+ this.add(13, 5)
+ this.add(14, 2)
+ this.add(15, 0.7)
+ this.add(16, 1)
+ this.add(17, 0.5)
+ this.add(18, 1)
+ }
+}
+
+class Relationship {
+ constructor (...settings) {
+ this.settings = settings
+ for (let setting of settings) {
+ setting.follow(this)
+ }
+ }
+ destroy () {
+ for (let setting of this.settings) {
+ setting.follow(this)
+ }
+ }
+}
+
+class NoopRel extends Relationship {
+ constructor (...settings) {
+ super() // don't pass args
+ }
+ score () {
+ return 0
+ }
+}
+
+// create a subclass of relationship that initializes with the last X items of
+// constructor arg
+const LastXMixin = (x, base_class) => class extends base_class {
+ constructor (notes) {
+ if (notes.length < x) {
+ return new NoopRel()
+ } else {
+ let settings = []
+ for (let i = 0; i < x; ++i) {
+ settings.unshift(notes[i].note)
+ }
+ super(...settings)
+ }
+ }
+}
+
+// pass two note numbers
+// rates how good they sound at the same time
+class Chord2Rel extends Relationship {
+ score () {
+ let a = this.settings[0].chosen
+ let b = this.settings[1].chosen
+ let diff = Math.abs((a % 7) - (b % 7))
+ return INTERVAL_GOODNESS[diff]
+ }
+}
+
+class HigherGoodRel extends Relationship {
+ score () {
+ if (this.settings[0].chosen > this.settings[1].chosen) {
+ return 1
+ }
+ return 0.5
+ }
+}
+
+class RangeGood8Rel extends Relationship {
+ score () {
+ let lowest, highest
+ lowest = highest = this.settings[0].chosen
+ for (let i = 1; i < this.settings.length; ++i) {
+ let chosen = this.settings[i].chosen
+ if (chosen < lowest) {
+ lowest = chosen
+ } else if (chosen > highest) {
+ highest = chosen
+ }
+ }
+ return (highest - lowest) / 9
+ }
+}
+const L8RangeGood8Rel = LastXMixin(8, RangeGood8Rel)
+
+class WalkRel extends Relationship {
+ score () {
+ let a = this.settings[0].chosen
+ let b = this.settings[1].chosen
+ if (a === b) {
+ return 0.2
+ }
+ if (Math.abs(a - b) === 1) {
+ return 1
+ }
+ let fives = [4, 7, 11]
+ // 1 <--> 5 is cool
+ if (a in fives && b in fives) {
+ return 0.8
+ }
+ // jumping a third to the third is pretty cool
+ if ((a === 7 || a === 11) && b === 9) {
+ return 0.8
+ }
+ // other thirds
+ if (Math.abs(a - b) === 2) {
+ if (a in fives || b in fives) {
+ return 0.4
+ }
+ return 0.3
+ }
+ return 0
+ }
+}
+const L2WalkRel = LastXMixin(2, WalkRel)
+
+class SameBad2Rel extends Relationship {
+ score () {
+ let ick = 0
+ for (let i = 0; i < 2; ++i) {
+ if (this.settings[i].chosen == this.settings[i+2].chosen) {
+ ick += 1
+ }
+ }
+ return 1 - ick
+ }
+}
+const L4SameBad2Rel = LastXMixin(4, SameBad2Rel)
+
+class SameBad4Rel extends Relationship {
+ score () {
+ let ick = 0
+ let prev_same = false
+ for (let i = 0; i < 4; ++i) {
+ if (this.settings[i].chosen == this.settings[i+4].chosen) {
+ ick += 1
+ if (prev_same) {
+ ick += 1
+ }
+ prev_same = true
+ } else {
+ prev_same = false
+ }
+ }
+ if (ick < 2) {
+ return 1
+ }
+ return Math.pow(0.9, ick)
+ }
+}
+const L8SameBad4Rel = LastXMixin(8, SameBad4Rel)
+
+class RunGood3Rel extends Relationship {
+ score () {
+ let a = this.settings[0].chosen
+ let b = this.settings[1].chosen
+ let c = this.settings[2].chosen
+ if (a !== b && Math.abs(a - b) == Math.abs(b - c)) {
+ return 1
+ } else {
+ return 0.7
+ }
+ }
+}
+const L3RunGood3Rel = LastXMixin(3, RunGood3Rel)
+
+class Beat {
+ constructor (song) {
+ this.settings = new Set()
+ this.actors = new Set()
+ this.song = song
+ // snapshot some song values
+ this.time = song.next_beat_at
+ this.number = song.beat_number
+ this.measure = song.measure_number
+ }
+ on_play (cb) {
+ this.actors.add(cb)
+ }
+ add_setting (key, setting) {
+ if (key != null) {
+ this[key] = setting
+ }
+ this.settings.add(setting)
+ }
+ resolve_and_play () {
+ let already_keyss = new Set()
+ let best_keys = ''
+ let best_score = 0
+ let relationships = new Set()
+ let settings = new Set()
+ // initialize
+ for (let setting of this.settings) {
+ if (setting.relationships.size === 0) {
+ setting.choose()
+ } else {
+ for (let r of setting.relationships) {
+ relationships.add(r)
+ // track setting on the other end
+ for (let setting of r.settings) {
+ if (!settings.has(setting)) {
+ // also choose and track
+ if (!setting.locked) {
+ best_keys += setting.choose()
+ settings.add(setting)
+ }
+ // TODO add relationships that relate this back to settings
+ }
+ }
+ }
+ }
+ }
+ for (let r of relationships) {
+ best_score += r.score()
+ }
+ already_keyss.add(best_keys)
+ // try some combinations (brute force, random everything each time)
+ // TODO be more efficient about which setting are changed
+ for (let i = 0; i < 50; ++i) {
+ let keys = ''
+ let score = 0
+ for (let setting of settings) {
+ keys += setting.choose()
+ }
+ if (already_keyss.has(keys)) {
+ continue
+ }
+ for (let r of relationships) {
+ score += r.score()
+ }
+ already_keyss.add(keys)
+ if (score > best_score) {
+ best_keys = keys
+ best_score = score
+ }
+ }
+ // set all setting to our favorite
+ let i = 0
+ for (let setting of settings) {
+ setting.set_to(best_keys.charAt(i++))
+ }
+ // lock only the ones in this beat
+ for (let setting of this.settings) {
+ setting.lock()
+ }
+ // play
+ for (let cb of this.actors) {
+ cb(this)
+ }
+ }
+}
+
+class Note {
+ constructor (song, ...args) {
+ this.song = song
+ this.beat = song.beat
+ this.init(...args)
+ this.beat.on_play(this.play.bind(this))
+ }
+}
+class BassNote extends Note {
+ init () {
+ if (this.beat.measure_number % 4 === 2) {
+ this.note = this.song.bass[7].note
+ } else {
+ this.note = new BassNoteSetting()
+ }
+ this.song.bass.unshift(this)
+ this.beat.add_setting('bass_note', this.note)
+ new L2WalkRel(this.song.bass)
+ new L3RunGood3Rel(this.song.bass)
+ new L4SameBad2Rel(this.song.bass)
+ new L4SameBad2Rel(this.song.bass)
+ new L8SameBad4Rel(this.song.bass)
+ }
+ play () {
+ let amp = BEAT_LOUDNESS[this.beat.number]
+ this.song.np.play({
+ frequency: scale[this.note.chosen],
+ time: this.beat.time,
+ duration: BEAT_LENGTH,
+ amplitude: amp
+ })
+ }
+}
+class QuickNote extends Note {
+ constructor (song, lag, ...args) {
+ super(song, lag, ...args)
+ this.time = this.beat.time
+ if (lag) {
+ this.time += BEAT_LENGTH / 2
+ }
+ }
+ play () {
+ let amp = this.amp * BEAT_LOUDNESS[this.beat.number]
+ this.song.np.play({
+ frequency: scale[this.note.chosen],
+ time: this.time,
+ duration: BEAT_LENGTH / 2,
+ amplitude: amp
+ })
+ }
+}
+
+class MelodyNote extends QuickNote {
+ init () {
+ if (this.beat.measure_number % 4 === 1 && this.beat.number < 3) {
+ this.note = this.song.melody[7].note
+ } else if (this.beat.measure_number % 4 === 2) {
+ this.note = this.song.melody[15].note
+ } else {
+ this.note = new MelodyNoteSetting()
+ }
+ this.song.melody.unshift(this)
+ this.beat.add_setting('melody_note', this.note)
+ new Chord2Rel(this.song.bass[0].note, this.note)
+ //new L2WalkRel(this.song.melody)
+ if (this.song.melody.length > 1) {
+ new WalkRel(this.song.melody[1].note, this.note)
+ }
+ new L3RunGood3Rel(this.song.melody)
+ new L4SameBad2Rel(this.song.melody)
+ new L8SameBad4Rel(this.song.melody)
+ new L8RangeGood8Rel(this.song.melody)
+ this.amp = 0.8
+ }
+}
+class HarmonyNote extends QuickNote {
+ init () {
+ if (this.beat.measure_number % 4 === 3) {
+ this.note = this.song.harmony[7].note
+ } else {
+ this.note = new HarmonyNoteSetting()
+ }
+ this.song.harmony.unshift(this)
+ this.beat.add_setting('harmony_note', this.note)
+ new Chord2Rel(this.song.bass[0].note, this.note)
+ new Chord2Rel(this.song.melody[0].note, this.note)
+ //new L2WalkRel(this.song.harmony)
+ //new L3RunGood3Rel(this.song.harmony)
+ new L4SameBad2Rel(this.song.harmony)
+ new L8SameBad4Rel(this.song.harmony)
+ new L8RangeGood8Rel(this.song.harmony)
+ new HigherGoodRel(this.note, this.song.bass[0].note)
+ new HigherGoodRel(this.song.melody[0].note, this.note)
+ this.amp = 0.5
+ }
+}
+
+class Song {
+ constructor () {
+ this.np = null
+ this.beat = null
+ this.next_beat_timeout = null
+ this.next_beat_at = 0
+ this.beats = []
+ this.bass = []
+ this.melody = []
+ this.harmony = []
+ this.beat_number = 0
+ this.measure_number = 0
+ }
+ play () {
+ this.np = new NotePlayer()
+ this.next_beat_at = this.np.ctx.currentTime + INITIAL_LEAD_TIME
+
+ this.scheduler()
+ }
+ scheduler () {
+ this.next_beat_timeout = null
+ this.next_beat()
+ let next_output_at = this.next_beat_at - (4 * BEAT_LENGTH)
+ let until_next = (next_output_at - LEAD_TIME) - this.np.ctx.currentTime
+ if (until_next < 0.001) {
+ this.scheduler()
+ } else {
+ this.next_beat_timeout = timeout(until_next, this.scheduler.bind(this))
+ }
+ }
+
+ stop () {
+ if (this.next_beat_timeout != null) {
+ clearTimeout(this.next_beat_timeout)
+ this.next_beat_timeout = null
+ }
+ this.np.stop()
+ }
+ next_beat () {
+ this.beat = new Beat(this)
+ this.beats.unshift(this.beat)
+ new BassNote(this)
+ new MelodyNote(this)
+ new HarmonyNote(this)
+ new MelodyNote(this, true)
+ new HarmonyNote(this, true)
+
+ if (this.beats.length >= 5) {
+ this.beats[4].resolve_and_play()
+ }
+
+ if (this.beats.length > 15) {
+ // FIXME
+ this.beats.pop()
+ this.bass.pop()
+ this.melody.pop()
+ this.melody.pop()
+ this.harmony.pop()
+ this.harmony.pop()
+ }
+
+ this.beat_number = (this.beat_number + 1) % 4
+ if (this.beat_number === 0) {
+ this.measure_number += 1
+ }
+ this.next_beat_at += BEAT_LENGTH
+ }
+}
+
+function main() {
+ let button = document.querySelector('button')
+ if (button == null) {
+ document.addEventListener('DOMContentLoaded', main)
+ return
+ }
+ let song = null
+ let start = () => {
+ window.song = song = new Song()
+ song.play()
+ button.childNodes[0].textContent = 'Stop'
+ button.onclick = stop
+ }
+ let stop = () => {
+ if (song != null) {
+ song.stop()
+ }
+ button.childNodes[0].textContent = 'Play'
+ button.onclick = start
+ }
+ if (document.location.hash == '#autoplay') {
+ start()
+ } else {
+ stop()
+ }
+}
+main()