From 9f4a54afa2fa3ec60d2b0946dabfcc198b43b44f Mon Sep 17 00:00:00 2001 From: s-ol Date: Tue, 22 Mar 2022 00:45:16 +0100 Subject: move MIDI settings in toolbar, reorganize --- app/app.js | 286 ++++++++++++++++++++-------------------------------------- app/config.js | 278 ++++++++++++++++++++++++++++---------------------------- app/index.js | 3 +- app/style.js | 5 +- 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)} >
noteon(note)} - onMouseUp ={() => noteoff(note)} + onMouseUp={() => noteoff(note)} > {children}
@@ -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 }) => (
{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 }) => {
); }; - -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 ( -
- - -
- - -
- click here to activate
keyboard input
-
-
-
- ); - } + 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 ( +
+ + +
+ + +
+ click here to activate
keyboard input
+
+
+
+ ); }; 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 }) => ( -
- - -
+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 }) => ( + ); -const Checkbox = ({ name, set, value }) => ( -
- - set(e, !value)} - checked={value} - /> -
-); +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 ( - - ); - } - - return ( -
-

Display

- -

MIDI I/O

- - - - -
- ); - }; - }; +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 ( + + ); +}; 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; } -- cgit v1.2.3