aboutsummaryrefslogtreecommitdiffstats
path: root/app/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/app.js')
-rw-r--r--app/app.js286
1 files changed, 98 insertions, 188 deletions
diff --git a/app/app.js b/app/app.js
index 5074285..9af30ee 100644
--- a/app/app.js
+++ b/app/app.js
@@ -1,6 +1,7 @@
-import React from 'react';
+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;
@@ -36,9 +37,8 @@ const Hexagon = ({ x, y, children, state, note, major, scale, noteon, noteoff })
style={pos([x, y], major)}
>
<div
- className="inner"
onMouseDown={() => noteon(note)}
- onMouseUp ={() => noteoff(note)}
+ onMouseUp={() => noteoff(note)}
>
{children}
</div>
@@ -60,7 +60,7 @@ const layouts = {
// B = 7
// C = 2 = x
// A + B = 12 = y
- let note = 41 + 2 * x + 6 * y;
+ let note = 5 + 2 * x + 6 * y;
if (y % 2 == 0) note += 1;
return note;
},
@@ -128,7 +128,7 @@ const tallyup = (steps, offset=0) => {
};
};
-const Keyboard = ({ w, h, layout: { major, map }, state, scale, labels, noteon, noteoff }) => (
+const Keyboard = ({ w, h, layout: { major, map }, state, scale, transpose, labels, noteon, noteoff }) => (
<div
className={`keyboard ${major}-major`}
style={{
@@ -138,7 +138,7 @@ const Keyboard = ({ w, h, layout: { major, map }, state, scale, labels, noteon,
>
{range(w).map((_, x) => (
range(h).map((_, y) => {
- const note = map([x, y], [w, h]);
+ const note = map([x, y], [w, h]) + transpose;
const onCore = scale && scale.notes.indexOf(note) >= 0;
const onScale = scale && scale.modulo.indexOf(note % 12) >= 0;
return (
@@ -182,30 +182,7 @@ const ChordView = ({ chord }) => {
</div>
);
};
-
-css(`
-nav {
- display: flex;
- flex-wrap: wrap;
- justfiy-content: space-between;
-}
-
-nav > .group, nav > .spacer {
- display: flex;
- align-items: baseline;
- padding: 0.25em 1em;
- gap: 1em;
-
- border: 0 solid #363636;
- border-width: 1px 0 1px 0;
-}
-
-nav > .group:nth-child(2n) {
- background: #363636;
-}
-
-nav > .spacer { flex: 1; }
-`);
+;
css(`
main {
@@ -240,190 +217,123 @@ main:focus-within .focus-msg {
}
`);
-export default class App extends React.Component {
- state = {
+export default () => {
+ const [settings, setSettings] = useAppState({
layout: 'wicki_hayden',
scale: 'major',
labels: 'english',
offset: 60,
- state: {},
+ transpose: 3*12,
w: 12,
h: 4,
- };
-
- ref = React.createRef();
-
- componentDidMount() {
- this.ref.current.addEventListener("keydown", this.onkeydown);
- this.ref.current.addEventListener("keyup", this.onkeyup);
- }
-
- componentDidUpdate(prevProps) {
- this.send(CHANNEL, ALL_SOUND_OFF, 0);
- this.send(CHANNEL, ALL_NOTES_OFF, 0);
-
- if (prevProps.midiin)
- prevProps.midiin.onmidimessage = null;
-
- if (this.props.midiin)
- this.props.midiin.onmidimessage = this.onmessage;
- }
+ });
+ const [state, setState] = useState({});
+ const midi = useMIDI();
- blink = (note) => {
- this.onmessage({ cmd: NOTE_ON, note });
- setTimeout(() => this.onmessage({ cmd: NOTE_OFF, note }), 250);
- }
+ const ref = React.createRef();
- send(command, note, vel=127) {
- if (command === NOTE_ON && this.state.offset === null)
- this.setState({ offset: note });
+ const midiin = midi.inputs.get(settings.midiin);
+ const midiout = midi.outputs.get(settings.midiout);
- if (!this.props.midiout)
- return;
+ const send = (command, note, vel=127) => {
+ if (!midiout) return;
const msg = [statbyte(command, 0), note, vel];
- this.props.midiout.send(msg);
- }
+ midiout.send(msg);
+ };
- noteon(note) {
- if (this.state.offset === null)
- this.setState({ offset: note });
+ const noteon = (note) => {
+ if (settings.offset === null) {
+ setSettings((s) => ({ ...s, offset: note }));
+ }
- this.setState(({ state }) => ({ state: { ...state, [note]: true } }));
- this.send(NOTE_ON, note);
+ setState((s) => ({ ...s, [note]: true }));
+ send(NOTE_ON, note);
}
- noteoff(note) {
- this.setState(({ state }) => ({ state: { ...state, [note]: false } }));
- this.send(NOTE_OFF, note);
+ const noteoff = (note) => {
+ setState((s) => ({ ...s, [note]: false }));
+ send(NOTE_OFF, note);
}
- set = (e) => {
- const key = e.target.name;
- let value = e.target.value;
- if (key === 'w' || key === 'h') value = +value;
- this.setState({ [key]: value });
- };
+ useEffect(() => {
+ ref.current.onkeydown = (e) => {
+ if (e.repeat) return;
- onoffset = (offset) => (e) => this.setState({ offset });
+ const note = notes.key2midi[e.code];
+ if (!note) return;
+ e.preventDefault();
+ noteon(note);
+ }
- onmessage = (e) => {
- const message = parse(e);
+ ref.current.onkeyup = (e) => {
+ if (e.repeat) return;
- switch (message.cmd) {
- case NOTE_ON:
- console.info(message.note);
- break;
+ const note = notes.key2midi[e.code];
+ if (!note) return;
+ e.preventDefault();
+ noteoff(note);
}
+ });
- switch (message.cmd) {
- case NOTE_ON:
- this.setState(({ state }) => ({ state: { ...state, [message.note]: true } }));
- break;
+ useEffect(() => {
+ send(CHANNEL, ALL_SOUND_OFF, 0);
+ send(CHANNEL, ALL_NOTES_OFF, 0);
+ }, [midiout]);
- case NOTE_OFF:
- this.setState(({ state }) => ({ state: { ...state, [message.note]: false } }));
- break;
- }
- }
+ useEffect(() => {
+ if (!midiin) return;
- onkeydown = (e) => {
- if (e.repeat) return;
+ midiin.onmidimessage = (e) => {
+ const message = parse(e);
- const note = notes.key2midi[e.code];
- if (!note) return;
- e.preventDefault();
- this.noteon(note);
- }
+ switch (message.cmd) {
+ case NOTE_ON:
+ if (settings.offset === null) {
+ setSettings((s) => ({ ...s, offset: note }));
+ }
- onkeyup = (e) => {
- if (e.repeat) return;
+ setState((s) => ({ ...s, [message.note]: true }));
+ break;
- const note = notes.key2midi[e.code];
- if (!note) return;
- e.preventDefault();
- this.noteoff(note);
- }
+ case NOTE_OFF:
+ setState((s) => ({ ...s, [message.note]: false }));
+ break;
+ }
+ }
- render() {
- const { configure } = this.props;
- const { w, h, scale, layout, labels, offset, state } = this.state;
-
- const chord = Object.entries(state).filter(([note, on]) => on).map(([k, _]) => k);
- chord.sort();
-
- return (
- <div className="app">
- <nav>
- <div className="group">
- <button onClick={configure}>midi settings</button>
- </div>
- <div className="group">
- <label>layout:</label>
- <select name="layout" value={layout} onChange={this.set}>
- <option value="wicki_hayden">Wicki-Hayden</option>
- <option value="harmonic">Harmonic Table</option>
- <option value="gerhard">Gerhard</option>
- </select>
- </div>
- <div className="group">
- <label>note format:</label>
- <select name="labels" value={labels} onChange={this.set}>
- <option value="english">English</option>
- <option value="german">German</option>
- <option value="sol">Solfège</option>
- <option value="midi">MIDI no</option>
- </select>
- </div>
- <div className="group">
- <label>scale:</label>
- <button onClick={this.onoffset(null)}>
- {labels ? notes.labels[labels][offset % 12] : offset}
- </button>
- <select name="scale" value={scale} onChange={this.set}>
- <option value="none">None</option>
- <option value="major">Major</option>
- <option value="minor_nat">Natural Minor</option>
- <option value="minor_harm">Harmonic Minor</option>
- <option value="minor_mel">Melodic Minor</option>
- <option value="minor_hung">Hungarian Minor</option>
- <option value="whole">Whole-Tone</option>
- <option value="penta">Pentatonic</option>
- </select>
- </div>
- <div className="group">
- <label>octave:</label>
- <button onClick={this.onoffset(offset - 12)}>-</button>
- {Math.floor(offset / 12)}
- <button onClick={this.onoffset(offset + 12)}>+</button>
- </div>
- <div className="group">
- <label>size:</label>
- <input className="small" type="number" min="1" name="w" value={w} onChange={this.set} />
- {'x'}
- <input className="small" type="number" min="1" name="h" value={h} onChange={this.set} />
- </div>
- <div className="spacer" />
- </nav>
-
- <main ref={this.ref} tabIndex="0">
- <Keyboard
- w={w}
- h={h}
- noteon={this.noteon.bind(this)}
- noteoff={this.noteoff.bind(this)}
- layout={layouts[layout]}
- scale={scale !== "none" && offset !== null && tallyup(scales[scale], offset)}
- labels={notes.labels[this.state.labels]}
- offset={offset}
- state={state}
- />
- <ChordView chord={chord} />
- <div className="focus-msg">
- <span>click here to activate<br/>keyboard input</span>
- </div>
- </main>
- </div>
- );
- }
+ 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>
+ );
};