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 = 't' + timing[i % timing.length] else note_time = timing midi_track.addEvent(new MidiWriter.NoteEvent({ pitch: notes, duration: 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) })