summaryrefslogtreecommitdiff
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/index.js173
-rw-r--r--client/lib/util.js26
2 files changed, 139 insertions, 60 deletions
diff --git a/client/index.js b/client/index.js
index 3e0e171..9db4e32 100644
--- a/client/index.js
+++ b/client/index.js
@@ -4,35 +4,40 @@ import Nexus from 'nexusui'
import keys from './lib/keys'
import kalimba from './lib/kalimba'
import scales from './lib/scales'
-import { mod, browser, requestAudioContext, ftom, mtof, tap } from './lib/util'
+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'],
- [4, '4 measures'],
- [2, '2 measures'],
- [1, 'whole note'],
- [1/2, 'half note'],
- [1/3, 'third note'],
- [1/4, 'quarter note'],
- [1/5, 'fifth note'],
- [1/6, 'sixth note'],
- [1/8, 'eighth note'],
- [1/10, 'tenth note'],
- [1/12, 'twelfth note'],
- [1/16, 'sixteenth note'],
- [1/32, 'thirtysecond note'],
+ [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')
@@ -53,15 +58,17 @@ function midi_ready(err) {
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
- requestAudioContext(ready)
pick_dataset('housing costs and income inequality')
+ requestAudioContext(ready)
})
+
function pick_dataset(key){
console.log('pick dataset:', key)
i = 0
@@ -74,45 +81,49 @@ var behaviors = {
interval: { name: 'Intervals', fn: play_interval_sequence },
}
function pick_behavior(name){
- behaviors[name].fn()
+ play_fn = behaviors[name].fn
}
function play_next(){
- let note_time = 120000 / Tone.Transport.bpm.value * note_values[nx.timing.active][0]
+ let note_time = 120000 / Tone.Transport.bpm.value * note_values[nx.timing.active][0] * nx.duration.value
setTimeout(play_next, note_time)
- play_fn(note_time)
-}
-function play_sequence(){
- play_fn = (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;
- play( norm(n, min, max) * nx.multiply.value, note_time * nx.duration.value)
+ 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_interval_sequence(){
- play_fn = (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
- row.forEach(n => {
- const note = row_root + norm(n - row_min, diff.min, diff.max) * nx.interval.value
- play(note, note_time * nx.duration.value)
- })
- i += 1
- }
+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){
@@ -156,16 +167,18 @@ function play(index, duration){
}
cents *= 2
midi_note = Math.floor(midi_note)
- if (midi) {
- if (midi_note > 127) return
- const note = Tone.Frequency(Math.floor(midi_note), "midi").toNote()
+ 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
- midi.playNote(note, "all", { duration })
- // cents
- // midi.sendPitchBend(cents, "all")
+ 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) {
@@ -195,7 +208,7 @@ function update_radio_value_on_change(el, id, values, fn) {
el.update = update
}
function build_options(el, lists, fn) {
- Object.keys(lists).forEach( (key, i) => {
+ Object.keys(lists).forEach(key => {
const list = lists[key]
const option = document.createElement('option')
option.innerHTML = list.name
@@ -268,9 +281,51 @@ function ready () {
})
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
diff --git a/client/lib/util.js b/client/lib/util.js
index 31c3d3a..e47b343 100644
--- a/client/lib/util.js
+++ b/client/lib/util.js
@@ -54,6 +54,30 @@ function requestAudioContext (fn) {
}
}
+function dataURItoBlob(dataURI) {
+ // convert base64 to raw binary data held in a string
+ // doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
+ var byteString = atob(dataURI.split(',')[1]);
+
+ // separate out the mime component
+ var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]
+
+ // write the bytes of the string to an ArrayBuffer
+ var ab = new ArrayBuffer(byteString.length);
+
+ // create a view into the buffer
+ var ia = new Uint8Array(ab);
+
+ // set the bytes of the buffer to the correct values
+ for (var i = 0; i < byteString.length; i++) {
+ ia[i] = byteString.charCodeAt(i);
+ }
+
+ // write the ArrayBuffer to a blob, and you're done
+ var blob = new Blob([ab], {type: mimeString});
+ return blob;
+
+}
function ftom(f) {
// return (Math.log(f) - Math.log(261.626)) / Math.log(2) + 4.0
return 69 + 12 * Math.log2(f / 440)
@@ -68,5 +92,5 @@ function tap (fn) {
}
}
-export { choice, mod, browser, requestAudioContext, ftom, mtof, tap }
+export { choice, mod, browser, requestAudioContext, ftom, mtof, tap, dataURItoBlob }