summaryrefslogtreecommitdiff
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/data.js10
-rw-r--r--client/index.js212
-rw-r--r--client/lib/scales.js53
3 files changed, 252 insertions, 23 deletions
diff --git a/client/data.js b/client/data.js
index 15792c9..eb5d41f 100644
--- a/client/data.js
+++ b/client/data.js
@@ -2,6 +2,8 @@ const files = [
"housing-costs-and-income-inequality",
"income-inequality-over-time",
"shares-of-wealth",
+ "weekly-earnings",
+ "household-wealth",
]
const dataPromises = files.map(name => {
@@ -10,11 +12,15 @@ const dataPromises = files.map(name => {
}).then(text => {
let lines = text.split('\n').map(line => line.split(','))
const h = lines.shift()
- return { name, h, lines, }
+ return {
+ name,
+ h,
+ lines: lines.filter(s => !!s)
+ }
})
})
const allPromises = Promise.all(dataPromises).then(data => {
- return data.reduce((a,b) => { a[b.name]=b, a }, {})
+ return data.reduce((a,b) => { a[b.name.replace(/-/g, '_')] = b; return a }, {})
})
const load = () => {
return allPromises
diff --git a/client/index.js b/client/index.js
index f9a4902..29ec55b 100644
--- a/client/index.js
+++ b/client/index.js
@@ -1,6 +1,6 @@
import Tone from 'tone'
import WebMidi from 'webmidi'
-import 'nexusui'
+import Nexus from 'nexusui'
import keys from './lib/keys'
import kalimba from './lib/kalimba'
import scales from './lib/scales'
@@ -8,9 +8,28 @@ import { mod, browser, requestAudioContext, ftom, mtof, tap } from './lib/util'
import * as data from './data'
-const nx = window.nx
+const DEFAULT_BPM = 60
+
+const nx = window.nx = {}
let midi
+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'],
+]
+
WebMidi.enable(midi_ready)
@@ -21,14 +40,106 @@ function midi_ready(err) {
}
if (!WebMidi.outputs.length) {
console.error('no MIDI output found')
+ return
}
console.log(WebMidi.inputs)
console.log(WebMidi.outputs)
- midi = WebMidi.outputs[0]
+ if (WebMidi.outputs.length > 1) {
+ const filtered = WebMidi.outputs.filter(output => output.name.match(/prodipe/i))
+ if (filtered.length) {
+ midi = filtered[0]
+ }
+ } else {
+ midi = WebMidi.outputs[0]
+ }
+ console.log(midi.name)
+}
+let i = 0
+data.load().then(lists => {
+ // nx.dataset.choices = Object.keys(lists)
+ // console.log(lists)
+ const list = lists.household_wealth
+ document.querySelector('#dataset_name').innerHTML = list.name.replace(/-/g, ' ')
+ // playSequence(list)
+ playIntervalSequence(list)
+})
+
+function playSequence(list){
+ let { rows, min, max } = get_bounds(list)
+ let count = rows.length * rows[0].length
+ playNext()
+ function playNext() {
+ let note_time = 30000 / Tone.Transport.bpm.value
+ setTimeout(playNext, note_time)
+ 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)
+ }
+}
+
+function playIntervalSequence(list){
+ let { rows, min, max } = get_bounds(list)
+ let diff = get_diff_bounds(rows)
+ let count = rows.length
+ playNext()
+ function playNext() {
+ let note_time = 120000 / Tone.Transport.bpm.value * note_values[nx.timing.active][0]
+ setTimeout(playNext, note_time)
+ const y = i % count
+ const row = rows[y]
+ 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
+ rows[y].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
+ if (i > count) i = 0;
+ }
}
-function play(index){
- const freq = scales.current().index(index)
+// function pick_dataset(name){
+// i = 0
+// // dataset
+// }
+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 freq = scales.current().index(index + Math.round(nx.offset.value))
let midi_note = ftom(freq)
let cents = midi_note % 1
if (cents > 0.5) {
@@ -39,30 +150,93 @@ function play(index){
midi_note = Math.floor(midi_note)
// console.log(freq, midi_note, cents.foFixed(1))
if (midi) {
- midi.playNote(Tone.Frequency(Math.floor(midi_note), "midi").toNote(), "all", { duration: 90000 / Tone.Transport.bpm.value })
+ duration = duration || 60000 / Tone.Transport.bpm.value
+ midi.playNote(Tone.Frequency(Math.floor(midi_note), "midi").toNote(), "all", { duration })
// cents
- midi.sendPitchBend(cents, "all")
+ // midi.sendPitchBend(cents, "all")
} else {
kalimba.play(freq)
}
}
-nx.onload = () => requestAudioContext(ready)
+requestAudioContext(ready)
function ready () {
- nx.widgets.scale.choices = scales.names()
- nx.widgets.scale.init()
- nx.widgets.scale.on('*', e => scales.pick(e.value))
+ scales.build_options(document.querySelector('#scale'))
+ // 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.widgets.shiftUp.on('*', tap(shiftUp))
+ nx.multiply = new Nexus.Dial('#multiply', {
+ min: -64,
+ max: 64,
+ step: 1,
+ value: 7,
+ })
+ update_value_on_change(nx.multiply, '#multiply', true)
- nx.colorize('#f4d142')
+ nx.interval = new Nexus.Dial('#interval', {
+ min: -64,
+ max: 64,
+ step: 1,
+ value: 10,
+ })
+ update_value_on_change(nx.interval, '#interval', true)
- Tone.Transport.bpm.value = 108
- nx.widgets.tempo.min = 10
- nx.widgets.tempo.max = 1000
- nx.widgets.tempo.set({ value: 108 })
- nx.widgets.tempo.on('*', () => Tone.Transport.bpm.value = nx.widgets.tempo.val.value )
Tone.Transport.start()
+ document.querySelector('.loading').classList.remove('loading')
+}
+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)
+}
+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)
}
-
keys.listen(play)
diff --git a/client/lib/scales.js b/client/lib/scales.js
index cb36b28..2d59f8a 100644
--- a/client/lib/scales.js
+++ b/client/lib/scales.js
@@ -19,6 +19,38 @@ const meantone = `! meanquar.scl
2/1
`
+const shares = `! shares.scl
+!
+A scale based on shares of wealth
+!
+1.
+5.
+15.
+32.
+52.
+78.
+116.
+182.
+521.
+1000.
+`
+
+const shares_sum = `! shares_sum.scl
+!
+A scale based on summing shares of wealth
+!
+1
+6.0
+21.0
+53.0
+105.0
+183.0
+299.0
+481.0
+1002.0
+2/1
+`
+
const mavila = `! mavila12.scl
!
A 12-note mavila scale (for warping meantone-based music), 5-limit TOP
@@ -137,6 +169,12 @@ const scales = [
{
scl: colundi,
},
+ {
+ scl: shares,
+ },
+ {
+ scl: shares_sum,
+ },
].map( (opt) => new Intonation(opt) )
let scale = scales[0]
@@ -152,7 +190,18 @@ function build () {
})
scale_list.appendChild(scale.heading)
})
-
+ pick(0)
+}
+function build_options(el) {
+ scales.forEach( (scale, i) => {
+ const option = document.createElement('option')
+ option.innerHTML = scale.name
+ option.value = i
+ el.appendChild(option)
+ })
+ el.addEventListener('input', function(e){
+ pick(e.target.value)
+ })
pick(0)
}
@@ -178,4 +227,4 @@ function names () {
}
-export default { scales, current, build, pick, names, onChange }
+export default { scales, current, build, build_options, pick, names, onChange }