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, play_interval_sequence, note_values, MidiWriter, } from './lib/midi' import { requestAudioContext, ftom, norm, dataURItoBlob, get_bounds, get_diff_bounds, } from './lib/util' import { update_value_on_change, update_radio_value_on_change, build_options } from './lib/ui' import * as data from './data' const DEFAULT_BPM = 60 const nx = window.nx = {} let recorder = null let recording = false let sendPitchBend = false midi_init() /* initialization */ let i = 0, datasets = {}, dataset = {}, bounds = {}, diff = [] let play_fn = play_sequence data.load().then(lists => { console.log(lists) datasets = lists.map(list => { list.shift() switch(list.name) { case 'gun_violence': return gun_violence_melody(list) case 'mass_shootings': return { ...list, lines: list.lines.map(line => { // 0 case name // 1 location // 2 date // 3 summary // 4 fatalities // 5 injured // 6 total_victims // 7 location // 8 age_of_shooter // 9 prior_signs_mental_health_issues // 10 mental_health_details // 11 weapons_obtained_legally // 12 where_obtained // 13 weapon_type // 14 weapon_details // 15 race // 16 gender // 17 sources // 18 mental_health_sources // 19 sources_additional_age // 20 latitude // 21 longitude // 22 type (Spree / Mass) // 23 year }) } break } }) pick_dataset('mass shootings') requestAudioContext(ready) }) function gun_violence_melody(list){ let melody = [] let lookup = {} let last = Date.now() let last_y = 2018 let last_m = 3 list.lines.forEach(line => { let [ incident_id, date, state, city_or_county, address, n_killed, n_injured, incident_url, source_url, incident_url_fields_missing, congressional_district, gun_stolen, gun_type, incident_characteristics, latitude, location_description, longitude, n_guns_involved, notes, participant_age, participant_age_group, participant_gender, participant_name, participant_relationship, participant_status, participant_type, sources, state_house_district, state_senate_district ] = line let [ y, m, d ] = date.split('-') }) return { ...list, lines: melody, } } /* 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, 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) 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 } /* build and bind the UI */ 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(dataset.name, bounds, 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') play_next() } /* keys */ keys.listen(index => { nx.offset.value = index nx.offset.update(index) })