diff options
Diffstat (limited to 'app/app.js')
| -rw-r--r-- | app/app.js | 364 |
1 files changed, 0 insertions, 364 deletions
diff --git a/app/app.js b/app/app.js deleted file mode 100644 index 17cdb18..0000000 --- a/app/app.js +++ /dev/null @@ -1,364 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { css, pos, width, height } from './style'; -import * as notes from './notes'; -import { useAppState, useMIDI, Toolbar } from './config'; - -const statbyte = (stat, chan) => (stat << 4) | chan; -const CHANNEL = 0b1011; -const ALL_NOTES_OFF = 0x7B; -const ALL_SOUND_OFF = 0x78; -const NOTE_ON = 0b1001; -const NOTE_OFF = 0b1000; -const CCHANGE = 0b1011; - -const clamp = (val, min=0, max=1) => Math.max(min, Math.min(val, max)); -const ramp = i => new Array(i).fill(true).map((_, i) => i); -const mix = (i, a, b) => i * a + (1-i) * b; - -const parse = message => ({ - cmd: message.data[0] >> 4, - chan: message.data[0] & 0xf, - note: message.data[1], - val: message.data[2] / 127, -}); - -const Hexagon = ({ x, y, children, state, note, major, scale, noteon, noteoff }) => { - if (major == 'row') { - if (y % 2 == 0) - x += 0.5; - } else { - if (x % 2 == 0) - y += 0.5; - } - - return ( - <div - className={`hexagon ${state ? ' active' : ''} ${scale}`} - style={pos([x, y], major)} - > - <div - onMouseDown={() => noteon(note)} - onMouseUp={() => noteoff(note)} - > - {children} - </div> - </div> - ); -}; - -// B A -// A B A C B -// o C o o -// C - - -const layouts = { - wicki_hayden: { - major: 'row', - map: ([x, y], [w, h]) => { - // A = 5 - // B = 7 - // C = 2 = x - // A + B = 12 = y - let note = 5 + 2 * x + 6 * y; - if (y % 2 == 0) note += 1; - return note; - }, - }, - janko: { - major: 'row', - map: ([x, y], [w, h]) => { - let note = 5 + 2 * x; - if (y % 2 == 0) note += 1; - return note; - }, - }, - trad_piano: { - major: 'row', - map: ([x, y], [w, h]) => { - const pitches = [0, 1, 2, 3, 4, -1, 5, 6, 7, 8, 9, 10, 11, -1]; - - let i = 2 * x; - if (y % 2 == 1) i -= 1; - - const pitch = pitches[i % 14]; - if (pitch < 0) return pitch; - - const oct = Math.floor(i / 14) + Math.floor(y / 2); - return 10 + pitch + 12 * oct; - }, - }, - harmonic: { - // A = 3 - // B = 7 = y - // C = 4 - // C - A = 1 = x - - // A = 7 = y - // B = 4 - // C = -3 - // B + C = 1 = x - major: 'col', - map: ([x, y], [w, h]) => { - let note = 48 + x * 0.5 + y * 7; - if (x % 2 === 1) note -= 3.5; - return note; - }, - }, - gerhard: { - // A = -3 - // B = 1 - // C = 4 - // - // A = 1 = y - // B = 4 - // C = 3 - // B + C = 7 = x - major: 'col', - map: ([x, y], [w, h]) => { - let note = 48 + x * 3.5 + y; - if (x % 2 === 1) note -= 0.5; - return note; - }, - }, -}; - -const scales = { - major: [2, 2, 1, 2, 2, 2, 1], - minor_nat: [2, 1, 2, 2, 1, 2, 2], - minor_harm: [2, 1, 2, 2, 1, 3, 1], - minor_mel: [2, 1, 2, 2, 2, 2, 1], - minor_hung: [2, 1, 3, 1, 1, 3, 1], - whole: [2, 2, 2, 2, 2, 2], - penta: [2, 2, 3, 2, 3], -}; - -const range = i => new Array(i).fill(true); -const tallyup = (steps, offset=0) => { - const notes = []; - - let sum = 0; - for (let i = 0; i < steps.length; i++) { - notes[i] = offset + sum; - sum += steps[i]; - } - - if (sum != 12) throw new Error(`scale doesn't tally up: ${steps} / ${notes}`); - - return { - notes, - modulo: notes.map(n => n % 12), - }; -}; - -const Keyboard = ({ w, h, layout: { major, map }, state, scale, transpose, labels, noteon, noteoff }) => ( - <div - className={`keyboard ${major}-major`} - style={{ - height: height(h, major), - width: width(w, major), - }} - > - {range(w).map((_, x) => ( - range(h).map((_, y) => { - const rawNote = map([x, y], [w, h]); - const disabled = rawNote < 0; - const note = disabled ? -1 : rawNote + transpose; - const onCore = !disabled && scale && scale.notes.indexOf(note) >= 0; - const onScale = !disabled && scale && scale.modulo.indexOf(note % 12) >= 0; - return ( - <Hexagon - key={x + ',' + y} - x={x} y={y} note={note} - state={state[note]} - scale={onCore ? "core" : (onScale ? "" : "disabled")} - noteon={noteon} - noteoff={noteoff} - major={major} - > - {!disabled && (labels ? labels[note % 12] : note)} - </Hexagon> - ); - }) - ))} - </div> -); - -const ChordView = ({ chord }) => { - const min = chord[0]; - - return ( - <div className="chord"> - {range(13).map((_,i) => ( - <div - key={i} - className="blip" - /> - ))} - {chord.map((n) => ( - <div - key={n} - className="note" - style={{ bottom: `${((n - min) % 12) / 12 * 100 - 3}%` }} - > - {(n - min) && (<span>+{n - min}</span>)} - </div> - ))} - </div> - ); -}; -; - -css(` -main { - position: relative; - padding: 0.5em; - margin: 1em 0; - - display: flex; - justify-content: space-evenly; -} - -main .focus-msg { - position: absolute; - inset: 0; - - display: flex; - font-size: 5em; - justify-content: space-around; - align-items: center; - text-align: center; - - border: 4px solid white; - background: #212121; - pointer-events: none; - - opacity: 0.8; - transition: opacity 0.4s; -} - -main:focus-within .focus-msg { - opacity: 0; -} -`); - -export default () => { - const [settings, setSettings] = useAppState({ - layout: 'wicki_hayden', - scale: 'major', - labels: 'english', - offset: 60, - transpose: 3*12, - w: 12, - h: 4, - }); - const [state, setState] = useState({}); - const midi = useMIDI(); - - const ref = React.createRef(); - - const midiin = midi.inputs.get(settings.midiin); - const midiout = midi.outputs.get(settings.midiout); - - const send = (command, note, vel=127) => { - if (!midiout) return; - - const msg = [statbyte(command, 0), note, vel]; - midiout.send(msg); - }; - - const noteon = (note) => { - if (settings.offset === null) { - setSettings((s) => ({ ...s, offset: note })); - } - - setState((s) => ({ ...s, [note]: true })); - send(NOTE_ON, note); - } - - const noteoff = (note) => { - setState((s) => ({ ...s, [note]: false })); - send(NOTE_OFF, note); - } - - useEffect(() => { - ref.current.onkeydown = (e) => { - if (e.repeat) return; - - const note = notes.key2midi[e.code]; - if (!note) return; - e.preventDefault(); - noteon(note); - } - - ref.current.onkeyup = (e) => { - if (e.repeat) return; - - const note = notes.key2midi[e.code]; - if (!note) return; - e.preventDefault(); - noteoff(note); - } - }); - - useEffect(() => { - send(CHANNEL, ALL_SOUND_OFF, 0); - send(CHANNEL, ALL_NOTES_OFF, 0); - }, [midiout]); - - useEffect(() => { - if (!midiin) return; - - midiin.onmidimessage = (e) => { - const message = parse(e); - - switch (message.cmd) { - case NOTE_ON: - if (settings.offset === null) { - setSettings((s) => ({ ...s, offset: message.note })); - } - - setState((s) => ({ ...s, [message.note]: true })); - break; - - case NOTE_OFF: - setState((s) => ({ ...s, [message.note]: false })); - break; - } - } - - return () => { - midiin.onmidimessage = null; - }; - }, [midiin]); - - const { scale, layout, labels, offset } = settings; - const chord = Object.entries(state).filter(([note, on]) => on).map(([k, _]) => k); - chord.sort(); - - return ( - <div className="app"> - <Toolbar - state={settings} - setState={setSettings} - midi={midi} - /> - - <main ref={ref} tabIndex="0"> - <Keyboard - {...settings} - noteon={noteon} - noteoff={noteoff} - layout={layouts[layout]} - scale={scale !== "none" && offset !== null && tallyup(scales[scale], offset)} - labels={notes.labels[labels]} - state={state} - /> - <ChordView chord={chord} /> - <div className="focus-msg"> - <span>click here to activate<br/>keyboard input</span> - </div> - </main> - </div> - ); -}; |
