import Tone from 'tone' import Nexus from 'nexusui' import { saveAs } from 'file-saver/FileSaver' import keys from './lib/keys' import scales from './lib/scales' import { midi_init, play_note, play_sequence, export_pattern_as_midi, note_values, MidiWriter, } from './lib/midi' import { requestAudioContext, norm, dataURItoBlob, get_bounds, get_diff_bounds, transpose, } from './lib/util' import { update_value_on_change, update_radio_value_on_change, build_options, nx } from './lib/ui' import * as data from './data' const DEFAULT_BPM = 60 let recorder = null let recording = false midi_init() /* initialization */ const mass_fields = [ "date", "timestamp", "fatalities", "injured", "total_victims", "age", "case", "weapon_type", "weapon_details" ].reduce((a,b,i) => { a[b] = i return a }, {}) let i = 0, mass_i = 0, datasets = {}, dataset = {}, bounds = {}, diff = [] let play_fn = play_sequence data.load().then(lists => { console.log(lists) transpose(lists.gun_violence_by_month.lines).forEach((row, i) => { const name = lists.gun_violence_by_month.h[i] if (name === 'Date') return console.log(name, row) datasets[name] = { name, h: [name], lines: [row.map(n => parseInt(n))], play_fn: play_sequence, } }) datasets["Mass Shootings"] = lists.mass_shootings_lite datasets["Mass Shootings"].name = "Mass Shootings" datasets["Mass Shootings"].play_fn = play_mass_shootings const lines = datasets["Mass Shootings"].lines.reverse() const [min_y, ...rest] = lines[0][mass_fields.date].split('/') datasets["Mass Shootings"].dates = lines.map(row => { const [y, m, d] = row[mass_fields.date].split('/') return (parseInt(y) - parseInt(min_y)) * 12 + parseInt(m) }) datasets["Mass Shootings"].lines = [lines.map(row => Math.log(row[mass_fields.total_victims]))] requestAudioContext(ready) }) /* play function for mass shooting data w/ custom timing */ let mass_rest = 0 // export 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], // ] function play_mass_shootings(i, bounds, diff, note_time, channel="all", exporting) { const { rows, min, max } = bounds const y = 0 const x = i % rows[0].length const n = rows[y][x] let notes = [], midi_notes = [] console.log(i, mass_i, dataset.dates[mass_i]) while (i === dataset.dates[mass_i]) { notes.push(dataset.lines[mass_i]) mass_i += 1 } switch (notes.length) { default: case 0: mass_rest += 1 break case 1: midi_notes.push(play_note( norm(n, min, max) * nx.multiply.value, 128, channel, exporting, mass_rest, 0)) break case 2: midi_notes.push(play_note( norm(n, min, max) * nx.multiply.value, 64, channel, exporting, mass_rest, 0)) midi_notes.push(play_note( norm(n, min, max) * nx.multiply.value, 64, channel, exporting, 0, 64)) break case 3: midi_notes.push(play_note( norm(n, min, max) * nx.multiply.value, 43, channel, exporting, mass_rest)) midi_notes.push(play_note( norm(n, min, max) * nx.multiply.value, 43, channel, exporting, 0, 43)) midi_notes.push(play_note( norm(n, min, max) * nx.multiply.value, 42, channel, exporting, 0, 85)) break case 4: midi_notes.push(play_note( norm(n, min, max) * nx.multiply.value, 32, channel, exporting, mass_rest)) midi_notes.push(play_note( norm(n, min, max) * nx.multiply.value, 32, channel, exporting, 0, 32)) midi_notes.push(play_note( norm(n, min, max) * nx.multiply.value, 32, channel, exporting, 0, 64)) midi_notes.push(play_note( norm(n, min, max) * nx.multiply.value, 32, channel, exporting, 0, 96)) break } if (mass_i > dataset.dates.length-1) { mass_rest = 0 mass_i = 0 i = 0 } else { i += 1 } if (notes.length) { mass_rest = 0 return [i, midi_notes] } mass_rest += 128 return [i, []] } /* play next note according to sonification */ 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, bounds, diff, 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 })) } } /* bind selects */ function pick_dataset(key){ console.log('pick dataset:', key, datasets[key]) i = 0 mass_i = 0 mass_rest = 0 dataset = datasets[key] bounds = get_bounds(dataset) diff = get_diff_bounds(bounds.rows) play_fn = dataset.play_fn } /* build and bind the UI */ function ready() { scales.build_options(document.querySelector('#scale')) build_options(document.querySelector('#dataset'), datasets, pick_dataset) const dial_size = [50, 50] Tone.Transport.bpm.value = DEFAULT_BPM nx.tempo = new Nexus.Dial('#tempo', { size: dial_size, 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', { size: dial_size, min: 0, max: 2, step: 0.01, value: 0.8, }) update_value_on_change(nx.duration, '#duration', false) nx.offset = new Nexus.Dial('#offset', { size: dial_size, min: -24, max: 24, step: 1, value: 0, }) update_value_on_change(nx.offset, '#offset', true) nx.octave = new Nexus.Dial('#octave', { size: dial_size, min: -4, max: 4, step: 1, value: 0, }) update_value_on_change(nx.octave, '#octave', true) nx.multiply = new Nexus.Dial('#multiply', { size: dial_size, min: -64, max: 64, step: 1, value: 7, }) update_value_on_change(nx.multiply, '#multiply', true) nx.interval = new Nexus.Dial('#interval', { size: dial_size, 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(dataset.name, bounds, diff, nx.tempo.value, nx.timing.active, play_fn) }) 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') document.querySelector('#dataset').value = 'Surrenders' pick_dataset('Mass Shootings') play_next() } /* keys */ keys.listen(index => { nx.offset.value = index nx.offset.update(index) })