diff options
Diffstat (limited to 'client/index.js')
| -rw-r--r-- | client/index.js | 333 |
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) +}) |
