import Tone from 'tone' import 'nexusui' import keys from './lib/keys' import color from './lib/color' import kalimba from './lib/kalimba' import scales from './lib/scales' import { mod, browser, requestAudioContext } from './lib/util' const nx = window.nx const noteCount = browser.isMobile ? 18 : 24 const stepCount = 16 const cellSize = browser.isMobile ? 22 : 27 let grid var loop = new Tone.Sequence(function(time, col){ var column = grid.matrix[col] grid.jumpToCol(col) for (var i = 0; i < noteCount; i++){ if (column[i] === 1){ const freq = scales.current().index(noteCount - i - 12) kalimba.play(freq) } } }, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "16n") nx.onload = () => requestAudioContext(ready) function ready () { grid = nx.widgets.grid grid.sequenceMode = true grid.bpm = 1 grid.col = stepCount grid.row = noteCount grid.init() grid.resize(cellSize * stepCount, cellSize * noteCount) grid.draw() nx.widgets.scale.choices = scales.names() nx.widgets.scale.init() nx.widgets.scale.on('*', e => scales.pick(e.value)) scales.onChange((s) => { buildLabels() }) buildLabels() grid.on('*', e => ('level' in e) && buildLabels()) nx.widgets.shiftUp.on('*', e => e.press && shiftUp()) nx.widgets.shiftDown.on('*', e => e.press && shiftDown()) nx.widgets.slideUp.on('*', e => e.press && slideUp()) nx.widgets.slideDown.on('*', e => e.press && slideDown()) nx.widgets.slideLeft.on('*', e => e.press && slideLeft()) nx.widgets.slideRight.on('*', e => e.press && slideRight()) nx.widgets.rotateUp.on('*', e => e.press && rotateVertical(-1)) nx.widgets.rotateDown.on('*', e => e.press && rotateVertical(1)) nx.widgets.rotateLeft.on('*', e => e.press && rotateHorizontal(-1)) nx.widgets.rotateRight.on('*', e => e.press && rotateHorizontal(1)) nx.widgets.flip.on('*', e => e.press && flip()) nx.widgets.flop.on('*', e => e.press && flop()) nx.colorize('#f4d142') loop.start() 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('*', e => Tone.Transport.bpm.value = nx.widgets.tempo.val.value ) Tone.Transport.start() } function draw () { grid.draw() buildLabels() } function buildLabels () { const labels = document.querySelector('#labels') const scaleCount = scales.current().scale.length const subScale = findSubScale() let str = '' for (let i = 0; i < noteCount; i++) { let index = noteCount - i - 12 let n = mod(index, scaleCount) let ns = (n+1) /* if (ns == 1) { let octave = (index / scaleCount) ns = ns + '' + octave + '' } */ if (subScale.includes(n)) { str += '' + (ns) + '
' } else { str += (ns) + '
' } } labels.innerHTML = str } function findSubScale (notes) { notes = notes || findNotes() const scaleCount = scales.current().scale.length return notes.reduce((acc, n) => { const scaleNote = mod(noteCount - n - 12, scaleCount) if (! acc.includes(scaleNote)) acc.push(scaleNote) return acc }, []).sort() } function shiftUp () { const originalNotes = findNotes() const subScale = findSubScale( originalNotes ) const scaleCount = scales.current().scale.length assignNotes( mapFunction(originalNotes, (n) => { let index = noteCount - n - 12 let note = mod(index, scaleCount) let scaleIndex = subScale.indexOf(note) + 1 let octave = Math.floor(index / scaleCount) while (scaleIndex >= subScale.length) { scaleIndex -= subScale.length octave += 1 } return 12 - (subScale[scaleIndex] + (octave * scaleCount)) })) } function shiftDown () { const originalNotes = findNotes() const subScale = findSubScale( originalNotes ) const scaleCount = scales.current().scale.length assignNotes( mapFunction(originalNotes, (n) => { let index = noteCount - n - 12 let note = mod(index, scaleCount) let scaleIndex = subScale.indexOf(note) - 1 let octave = Math.floor(index / scaleCount) while (scaleIndex < 0) { scaleIndex += subScale.length octave -= 1 } return 12 - (subScale[scaleIndex] + (octave * scaleCount)) })) } function slideUp () { assignNotes( mapFunction(findNotes(), (n) => { return (n - 1 + noteCount) % noteCount })) } function slideDown () { assignNotes( mapFunction(findNotes(), (n) => { return (n + 1 + noteCount) % noteCount })) } function slideLeft () { assignPositions( mapFunction(findPositions(), (n) => { return (n - 1 + stepCount) % stepCount })) } function slideRight () { assignPositions( mapFunction(findPositions(), (n) => { return (n + 1 + stepCount) % stepCount })) } function flip () { assignNotes( mapReverse(findNotes()) ) } function flop () { assignPositions( mapReverse(findPositions()) ) } function rotateHorizontal (n) { assignPositions( remapArray(findPositions(), n) ) } function rotateVertical (n) { assignNotes( remapArray(findNotes(), n) ) } function assignPositions (positions) { if (! positions) return const a = grid.matrix const b = iota() stride(a, (i,j,v) => { if (i in positions) { b[positions[i]][j] = v } }) assign(grid.matrix, b) draw() } function assignNotes (noteMap) { if (! noteMap) return const a = grid.matrix const b = iota() stride(a, (i,j,v) => { if (j in noteMap) { b[i][noteMap[j]] = v } }) assign(grid.matrix, b) draw() } function stride (a, f) { const w = stepCount, h = noteCount for (let i = 0; i < w; i++) { for (let j = 0; j < h; j++) { f(i, j, a[i][j]) } } return a } // assign b -> a function assign (a, b) { return stride(b, (i,j,v) => a[i][j] = b[i][j]) } function clone (a) { const b = iota(a.length, a[0].length) return assign(b, a) } function iota () { const w = grid.matrix.length, h = grid.matrix[0].length const a = new Array(w) for (let i = 0; i < w; i++) { a[i] = new Array(h) for (let j = 0; j < h; j++) { a[i][j] = 0 } } return a } function findNotes () { const a = new Array(noteCount) stride(grid.matrix, (i, j, v) => { if (v) a[j] = 1 }) return a.reduce((acc, v, i) => { if (v === 1) acc.push(i) return acc }, []) } function findPositions () { return grid.matrix.reduce((acc, row, i) => { if (row.some((x) => x === 1)) acc.push(i) return acc }, []) } function remapArray (a, n) { if (! a.length) return const h = {} const b = rotate(a, n) for (let i = 0; i < b.length; i++) { h[b[i]] = a[i] } return h } function rotate(a, n) { const b = a.slice(0) b.unshift.apply( b, b.splice( -n, b.length ) ) return b } function mapFunction (a, f) { if (! a.length) return const h = {} for (let i = 0; i < a.length; i++) { h[a[i]] = f(a[i]) } return h } function mapReverse (a) { if (! a.length) return const h = {} const b = a.slice(0).reverse() for (let i = 0; i < b.length; i++) { h[b[i]] = a[i] } return h } keys.listen(function(index){ const freq = scales.current().index(index) kalimba.play(freq) })