aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2022-03-21 23:45:16 +0000
committers-ol <s+removethis@s-ol.nu>2022-03-21 23:55:10 +0000
commit9f4a54afa2fa3ec60d2b0946dabfcc198b43b44f (patch)
tree701f5725f89f35623045cad3e654b5d1ef8332d5
parentsimpler key styling (diff)
downloadisomorphic-kb-explorer-9f4a54afa2fa3ec60d2b0946dabfcc198b43b44f.tar.gz
isomorphic-kb-explorer-9f4a54afa2fa3ec60d2b0946dabfcc198b43b44f.zip
move MIDI settings in toolbar, reorganize
-rw-r--r--app/app.js286
-rw-r--r--app/config.js278
-rw-r--r--app/index.js3
-rw-r--r--app/style.js5
4 files changed, 244 insertions, 328 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>
+ );
};
diff --git a/app/config.js b/app/config.js
index a59d989..eac34f8 100644
--- a/app/config.js
+++ b/app/config.js
@@ -1,14 +1,13 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
+import React, { useState, useCallback, useEffect } from 'react';
+import { labels as noteLabels } from './notes';
import { css } from './style';
-const setState = (key, state) => {
+const saveState = (key, state) => {
try {
localStorage.setItem(key, JSON.stringify(state));
- return true;
}
catch (e) {
- return false;
+ console.error(e);
}
};
@@ -22,141 +21,146 @@ const loadState = (key, defaultState=null) => {
}
};
-css(`
- .settings {
- width: 500px;
- margin: auto;
- padding: 1em;
- text-align: left;
- }
-
- .setting {
- margin: 0.4em 0;
- }
-
- .setting > label {
- display: inline-block;
- width: 200px;
- }
-
- .setting > select, .setting > input, .settings button {
- width: 200px;
- margin-left: 20px;
- }
+export const useAppState = (defaultState) => {
+ const [state, setState] = useState(defaultState);
- .setting > select:disabled {
- color: #aaa;
- background: #444;
- }
+ useEffect(() => setState(loadState('settings', defaultState)), []);
+ useEffect(() => saveState('settings', state), [state]);
- .settings > button {
- text-align: center;
- margin-right: 20px;
- }
-`);
+ return [state, setState];
+};
-const Dropdown = ({ name, list, set, disabled, value }) => (
- <div className="setting">
- <label>{name}</label>
- <select value={value} onChange={e => set(e, e.target.value)} disabled={!list || disabled}>
- <option>(none)</option>
- {list && ([...list.values()]).map((port) => (
- <option value={port.id} key={port.id}>{port.name}</option>
- ))}
- </select>
- </div>
+export const useMIDI = () => {
+ const [inputs, setInputs] = useState(new Map());
+ const [outputs, setOutputs] = useState(new Map());
+
+ const reload = useCallback(() => {
+ navigator.requestMIDIAccess &&
+ navigator.requestMIDIAccess({ sysex: true })
+ .then((access) => {
+ setInputs(access.inputs);
+ setOutputs(access.outputs);
+ });
+ });
+
+ useEffect(reload, []);
+
+ return { inputs, outputs, reload };
+}
+
+const Dropdown = ({ list, name, onChange, value }) => (
+ <select value={value} onChange={onChange} disabled={!list || !list.size}>
+ <option>(none)</option>
+ {list && ([...list.values()]).map((port) => (
+ <option value={port.id} key={port.id}>{port.name}</option>
+ ))}
+ </select>
);
-const Checkbox = ({ name, set, value }) => (
- <div className="setting">
- <label>{name}</label>
- <input
- type="checkbox"
- onChange={e => set(e, !value)}
- checked={value}
- />
- </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; }
+`);
-export default WrappedComponent =>
- class SettingsStore extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = loadState('settings', { configured: false, midiSupport: false, showMidi: false });
- this.configure = () => this.setState({ configured: false });
-
- this.reload = () => {
- navigator.requestMIDIAccess({ sysex: true })
- .then(access => {
- this.inputs = access.inputs;
- this.outputs = access.outputs;
- this.setState({
- midiSupport: true,
- });
- this.forceUpdate();
- });
- }
- }
-
- componentWillUpdate(props, state) {
- setState('settings', state);
- }
-
- componentDidMount() {
- if (navigator.requestMIDIAccess)
- this.reload();
- }
-
- set(k, v) {
- return (e, vv) => {
- e.stopPropagation();
-
- this.setState({ [k]: v === undefined
- ? vv
- : v });
- }
- }
-
- render() {
- const { configured, midiin, midiout, showMidi } = this.state;
-
- if (configured) {
- return (
- <WrappedComponent
- {...this.props}
- {...this.state}
- midiin={this.inputs && this.inputs.get(midiin)}
- midiout={this.outputs && this.outputs.get(midiout)}
- configure={this.configure}
- />
- );
- }
-
- return (
- <div className="panel settings">
- <h3>Display</h3>
- <Checkbox
- name="Show notes as MIDI number"
- set={this.set('showMidi')}
- value={showMidi}
- />
- <h3>MIDI I/O</h3>
- <Dropdown
- name="MIDI Keyboard Input"
- list={this.inputs}
- set={this.set('midiin')}
- value={midiin}
- />
- <Dropdown
- name="MIDI Output"
- list={this.outputs}
- set={this.set('midiout')}
- value={midiout}
- />
- <button onClick={this.reload}>reload MIDI device list</button>
- <button onClick={this.set('configured', true)}>start</button>
- </div>
- );
- };
- };
+export const Toolbar = ({ state, setState, midi }) => {
+ const { labels, offset, transpose } = state;
+
+ const track = (key) => ({
+ name: key,
+ value: state[key],
+ onChange: (e) => {
+ e.stopPropagation();
+ let value = e.target.value;
+ if (key === 'w' || key === 'h') value = +value;
+ setState({ ...state, [key]: value });
+ },
+ });
+
+ return (
+ <nav>
+ <div className="group">
+ <label>layout:</label>
+ <select {...track('layout')}>
+ <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 {...track('labels')}>
+ <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={() => setState({ ...state, offset: null })}>
+ {offset === null
+ ? "listening..."
+ : (noteLabels[labels] ? noteLabels[labels][offset % 12] : offset)}
+ </button>
+ <select {...track('scale')}>
+ <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={() => setState({ ...state, transpose: transpose - 12 })}>-</button>
+ {Math.floor(transpose / 12)}
+ <button onClick={() => setState({ ...state, transpose: transpose + 12 })}>+</button>
+ </div>
+ <div className="group">
+ <label>size:</label>
+ <input className="small" type="number" min="1" {...track('w')} />
+ {'x'}
+ <input className="small" type="number" min="1" {...track('h')} />
+ </div>
+ <div className="group">
+ <label>midi</label>
+ in:
+ <Dropdown
+ name="midiin"
+ list={midi.inputs}
+ {...track('midiin')}
+ />
+ out:
+ <Dropdown
+ name="midiout"
+ list={midi.outputs}
+ {...track('midiout')}
+ />
+ <button onClick={midi.reload}>↻</button>
+ </div>
+ <div className="spacer" />
+ </nav>
+ );
+};
diff --git a/app/index.js b/app/index.js
index 6e2a51e..75a2a9f 100644
--- a/app/index.js
+++ b/app/index.js
@@ -1,9 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';
-import config from './config';
-const $App = config(App);
+const $App = App;
const node = document.createElement('div');
ReactDOM.render(<$App />, node);
diff --git a/app/style.js b/app/style.js
index a20510e..26a6ce6 100644
--- a/app/style.js
+++ b/app/style.js
@@ -59,6 +59,9 @@ button, input, select {
padding: 0.25em;
border-radius: 0.5em;
}
+button:disabled, input:disabled, select:disabled {
+ opacity: 0.75;
+}
input.small {
width: 4em;
@@ -103,7 +106,7 @@ label {
border-color: #363636;
color: #848484;
}
-.hexagon.core > .inner {
+.hexagon.core {
border-color: #eeeeee;
}