aboutsummaryrefslogtreecommitdiffstats
path: root/app/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/app.js')
-rw-r--r--app/app.js364
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>
- );
-};