summaryrefslogtreecommitdiff
path: root/client/index.js
diff options
context:
space:
mode:
authorJules Laplace <julescarbon@gmail.com>2018-10-05 16:36:12 +0200
committerJules Laplace <julescarbon@gmail.com>2018-10-05 16:36:12 +0200
commit852ed2e007deac47292d3e83a374070683c29894 (patch)
treecacff78f2bc63c77cdb458863fc576043561adb1 /client/index.js
parent0ca3983dd9e00a93cc1ed1c55b2ad7a4a6b14bf2 (diff)
inequality client
Diffstat (limited to 'client/index.js')
-rw-r--r--client/index.js333
1 files changed, 333 insertions, 0 deletions
diff --git a/client/index.js b/client/index.js
new file mode 100644
index 0000000..e6f0716
--- /dev/null
+++ b/client/index.js
@@ -0,0 +1,333 @@
+import Tone from 'tone'
+import WebMidi from 'webmidi'
+import Nexus from 'nexusui'
+import keys from './lib/keys'
+import kalimba from './lib/kalimba'
+import scales from './lib/scales'
+import { requestAudioContext, ftom, dataURItoBlob } from './lib/util'
+import { saveAs } from 'file-saver/FileSaver'
+
+import * as data from './data'
+
+const MidiWriter = require('midi-writer-js')
+
+const DEFAULT_BPM = 60
+
+const nx = window.nx = {}
+
+let midi
+let exporting = false
+let recording = false
+let recorder = null
+
+const note_values = [
+ [8, '8 measures', 8 * 512],
+ [4, '4 measures', 4 * 512],
+ [2, '2 measures', 2 * 512],
+ [1, 'whole note', 512],
+ [1/2, 'half note', 256],
+ [1/3, 'third note', [170, 170, 171]],
+ [1/4, 'quarter note', 128],
+ [1/5, 'fifth note', [51,51,51,51,52]],
+ [1/6, 'sixth note', [85, 85, 86, 85, 85, 86]],
+ [1/8, 'eighth note', 64],
+ [1/10, 'tenth note', [25,26,26,25,26,25,26,26,25,26]],
+ [1/12, 'twelfth note', [21,21,22, 21,21,22, 21,21,22, 21,21,22]],
+ [1/16, 'sixteenth note', 32],
+ [1/32, 'thirtysecond note', 16],
+]
+
+WebMidi.enable(midi_ready)
+function midi_ready(err) {
+ if (err) {
+ console.error('webmidi failed to initialize')
+ return
+ }
+ if (!WebMidi.outputs.length) {
+ console.error('no MIDI output found')
+ return
+ }
+ console.log(WebMidi.inputs)
+ console.log(WebMidi.outputs)
+ if (WebMidi.outputs.length > 1) {
+ const filtered = WebMidi.outputs.filter(output => output.name.match(/prodipe/i))
+ if (filtered.length) {
+ // midi = filtered[0]
+ }
+ }
+ // midi = midi || WebMidi.outputs[0]
+ // console.log(midi.name)
+}
+
+let i = 0, datasets = {}, dataset = {}, bounds = {}, diff = []
+let play_fn = play_sequence
+data.load().then(lists => {
+ // nx.dataset.choices = Object.keys(lists)
+ console.log(lists)
+ datasets = lists
+ pick_dataset('housing costs and income inequality')
+ requestAudioContext(ready)
+})
+
+function pick_dataset(key){
+ console.log('pick dataset:', key)
+ i = 0
+ dataset = datasets[key]
+ bounds = get_bounds(dataset)
+ diff = get_diff_bounds(bounds.rows)
+}
+var behaviors = {
+ sequence: { name: 'Sequence', fn: play_sequence },
+ interval: { name: 'Intervals', fn: play_interval_sequence },
+}
+function pick_behavior(name){
+ play_fn = behaviors[name].fn
+}
+function play_next(){
+ let note_time = 120000 / Tone.Transport.bpm.value * note_values[nx.timing.active][0] * nx.duration.value
+ setTimeout(play_next, note_time)
+ let [new_i, notes] = play_fn(i, note_time)
+ i = new_i
+ if (recording) {
+ let timing = note_values[nx.timing.active][2]
+ if (timing.length) timing = timing[i % timing.length]
+ recorder.addEvent(new MidiWriter.NoteEvent({ pitch: notes, duration: 't' + timing }))
+ }
+}
+function play_sequence(i, note_time) {
+ const { rows, min, max } = bounds
+ const count = rows.length * rows[0].length
+ if (i >= count) i = 0
+ const y = Math.floor(i / rows[0].length)
+ const x = i % rows[0].length
+ if (!x) console.log(y)
+ const n = rows[y][x]
+ i += 1
+ if (i >= count) i = 0
+ const midi_note = play( norm(n, min, max) * nx.multiply.value, note_time)
+ return [i, [midi_note]]
+}
+function play_interval_sequence(i, note_time) {
+ const { rows, min, max } = bounds
+ const count = rows.length
+ if (i >= count) i = 0
+ const y = i % count
+ const row = rows[y]
+ if (! row) { i = 0; return }
+ const row_min = Math.min.apply(Math, row)
+ // const row_max = Math.max.apply(Math, row)
+ const row_f0 = norm(row_min, min, max)
+ const row_root = row_f0 * nx.multiply.value
+ const notes = row.map(n => {
+ const note = row_root + norm(n - row_min, diff.min, diff.max) * nx.interval.value
+ play(note, note_time)
+ })
+ i += 1
+ return [i, notes]
+}
+
+function norm(n, min, max){
+ return (n - min) / (max - min)
+}
+function get_diff_bounds(rows){
+ const diffs = rows.map(row => {
+ const row_min = Math.min.apply(Math, row)
+ const row_max = Math.max.apply(Math, row)
+ return row_max - row_min
+ })
+ const min = Math.min.apply(Math, diffs)
+ const max = Math.max.apply(Math, diffs)
+ return { min, max }
+}
+function get_bounds(dataset){
+ let rows = dataset.lines
+ rows.forEach(row => row.shift())
+ rows = rows.map(a => a.map(n => parseFloat(n)))
+ const max = rows.reduce((a,b) => {
+ return b.reduce((z,bb) => {
+ return Math.max(z, bb)
+ }, a)
+ }, -Infinity)
+ const min = rows.reduce((a,b) => {
+ return b.reduce((z,bb) => {
+ return Math.min(z, bb)
+ }, a)
+ }, Infinity)
+ return { rows, max, min }
+}
+function play(index, duration){
+ // console.log(index)
+ const scale = scales.current()
+ const freq = scale.index(index + Math.round(nx.offset.value), nx.octave.value)
+ let midi_note = ftom(freq)
+ let cents = midi_note % 1
+ if (cents > 0.5) {
+ midi_note += 1
+ cents -= 1
+ }
+ cents *= 2
+ midi_note = Math.floor(midi_note)
+ if ((midi || exporting) && midi_note > 127) return 0
+ const note = Tone.Frequency(Math.floor(midi_note), "midi").toNote()
+ if (exporting || midi) {
+ duration = duration || 60000 / Tone.Transport.bpm.value
+ if (! exporting) {
+ midi.playNote(note, "all", { duration })
+ midi.sendPitchBend(cents, "all")
+ }
+ } else {
+ kalimba.play(freq)
+ }
+ return note
+}
+
+function update_value_on_change(el, id, is_int, fn) {
+ const label = document.querySelector(id + ' + .val')
+ const update = v => {
+ label.innerHTML = is_int ? parseInt(v) : v.toFixed(2)
+ fn && fn(v)
+ }
+ el.on('change', update)
+ update(el.value)
+ el.update = update
+}
+function update_radio_value_on_change(el, id, values, fn) {
+ let old_v = el.active
+ const label = document.querySelector(id + ' + .val')
+ const update = v => {
+ if (v === -1) {
+ v = el.active = old_v
+ } else {
+ old_v = v
+ }
+ label.innerHTML = values[v][1]
+ fn && fn(v)
+ }
+ el.on('change', update)
+ update(el.active)
+ el.update = update
+}
+function build_options(el, lists, fn) {
+ Object.keys(lists).forEach(key => {
+ const list = lists[key]
+ const option = document.createElement('option')
+ option.innerHTML = list.name
+ option.value = key
+ el.appendChild(option)
+ })
+ el.addEventListener('input', function(e){
+ fn(e.target.value)
+ })
+}
+function ready () {
+ scales.build_options(document.querySelector('#scale'))
+ build_options(document.querySelector('#dataset'), datasets, pick_dataset)
+ build_options(document.querySelector('#behavior'), behaviors, pick_behavior)
+ // nx.colorize('#f4d142')
+
+ Tone.Transport.bpm.value = DEFAULT_BPM
+ nx.tempo = new Nexus.Dial('#tempo', {
+ min: 10,
+ max: 300,
+ step: 1,
+ value: DEFAULT_BPM,
+ })
+ update_value_on_change(nx.tempo, '#tempo', true, v => Tone.Transport.bpm.value = v)
+
+ nx.timing = new Nexus.RadioButton('#timing', {
+ size: [400,25],
+ numberOfButtons: note_values.length,
+ active: 6,
+ })
+ update_radio_value_on_change(nx.timing, '#timing', note_values)
+
+ nx.duration = new Nexus.Dial('#duration', {
+ min: 0,
+ max: 2,
+ step: 0.01,
+ value: 0.8,
+ })
+ update_value_on_change(nx.duration, '#duration', false)
+
+ nx.offset = new Nexus.Dial('#offset', {
+ min: -24,
+ max: 24,
+ step: 1,
+ value: 0,
+ })
+ update_value_on_change(nx.offset, '#offset', true)
+
+ nx.octave = new Nexus.Dial('#octave', {
+ min: -4,
+ max: 4,
+ step: 1,
+ value: 0,
+ })
+ update_value_on_change(nx.octave, '#octave', true)
+
+ nx.multiply = new Nexus.Dial('#multiply', {
+ min: -64,
+ max: 64,
+ step: 1,
+ value: 7,
+ })
+ update_value_on_change(nx.multiply, '#multiply', true)
+
+ nx.interval = new Nexus.Dial('#interval', {
+ min: -64,
+ max: 64,
+ step: 1,
+ value: 10,
+ })
+ update_value_on_change(nx.interval, '#interval', true)
+
+ const export_midi_button = document.querySelector('#export_midi')
+ export_midi_button.addEventListener('click', export_pattern_as_midi)
+
+ const record_midi_button = document.querySelector('#record_midi')
+ record_midi_button.addEventListener('click', () => {
+ if (recording) {
+ record_midi_button.innerHTML = 'Record MIDI'
+ document.body.classList.remove('recording')
+ recording = false
+ const writer = new MidiWriter.Writer([recorder])
+ const blob = dataURItoBlob(writer.dataUri())
+ saveAs(blob, 'Recording - ' + dataset.name + '.mid')
+ } else {
+ record_midi_button.innerHTML = 'Save Recording'
+ document.body.classList.add('recording')
+ recording = true
+ recorder = new MidiWriter.Track()
+ recorder.setTempo(nx.tempo.value)
+ }
+ })
+
+ document.querySelector('.loading').classList.remove('loading')
+ play_next()
+}
+function export_pattern_as_midi(){
+ const behavior = document.querySelector('#behavior').value
+ const { rows } = bounds
+ let count = behavior === 'sequence' ? rows[0].length * rows.length : rows.length
+ let notes
+ let note_time
+ let timing = note_values[nx.timing.active][2]
+ exporting = true
+ let midi_track = new MidiWriter.Track()
+ midi_track.setTempo(nx.tempo.value)
+ for (let i = 0, len = count; i < len; i++) {
+ notes = play_fn(i)[1]
+ if (timing.length) note_time = timing[i % timing.length]
+ else note_time = timing
+ midi_track.addEvent(new MidiWriter.NoteEvent({ pitch: notes, duration: 't' + note_time }))
+ }
+ const writer = new MidiWriter.Writer([midi_track])
+ const blob = dataURItoBlob(writer.dataUri())
+ saveAs(blob, 'Recording - ' + dataset.name + '.mid')
+ exporting = false
+}
+
+keys.listen(index => {
+ nx.offset.value = index
+ nx.offset.update(index)
+})