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 ? 20 : 27
const baseTone = browser.isMobile ? 8 : 12
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 - baseTone)
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('*', tap(shiftUp))
nx.widgets.shiftDown.on('*', tap(shiftDown))
nx.widgets.slideUp.on('*', tap(slideUp))
nx.widgets.slideDown.on('*', tap(slideDown))
nx.widgets.slideLeft.on('*', tap(slideLeft))
nx.widgets.slideRight.on('*', tap(slideRight))
nx.widgets.rotateUp.on('*', tap(() => rotateVertical(-1)))
nx.widgets.rotateDown.on('*', tap(() => rotateVertical(1)))
nx.widgets.rotateLeft.on('*', tap(() => rotateHorizontal(-1)))
nx.widgets.rotateRight.on('*', tap(() => rotateHorizontal(1)))
nx.widgets.flip.on('*', tap(flip))
nx.widgets.flop.on('*', tap(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 tap (fn) {
return (e) => {
if (browser.isMobile) fn()
else if (e.press) fn()
}
}
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 - baseTone
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 - baseTone, scaleCount)
if (! acc.includes(scaleNote)) acc.push(scaleNote)
return acc
}, []).sort()
}
function shiftUp () {
const notes = findNotes()
const subScale = findSubScale( notes )
const scaleCount = scales.current().scale.length
assignNotes( mapFunction(notes, (n) => {
let index = noteCount - n - baseTone
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 noteCount - baseTone - (subScale[scaleIndex] + (octave * scaleCount))
}))
}
function shiftDown () {
const notes = findNotes()
const subScale = findSubScale( notes )
const scaleCount = scales.current().scale.length
assignNotes( mapFunction(notes, (n) => {
let index = noteCount - n - baseTone
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 noteCount - baseTone - (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)
})