From 582452e79ab43e174ff33641cf7deb81fe25fcb4 Mon Sep 17 00:00:00 2001 From: s-ol Date: Tue, 4 Jan 2022 20:47:01 +0100 Subject: add scale highlighting --- app/app.js | 97 +++++++++++++++++++++++++++++++++++++++++++++++++----------- app/style.js | 15 +++++++++- 2 files changed, 93 insertions(+), 19 deletions(-) diff --git a/app/app.js b/app/app.js index 00b18bb..522cb99 100644 --- a/app/app.js +++ b/app/app.js @@ -3,6 +3,9 @@ import { pos, width, height } from './style'; import * as notes from './notes'; 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; @@ -24,7 +27,7 @@ const parse = message => ({ val: message.data[2] / 127, }); -const Hexagon = ({ x, y, children, state, note, send, major }) => { +const Hexagon = ({ x, y, children, state, note, send, major, scale }) => { if (major == 'row') { if (y % 2 == 0) x += 0.5; @@ -35,7 +38,7 @@ const Hexagon = ({ x, y, children, state, note, send, major }) => { return (
new Array(i).fill(true); -const Keyboard = ({ w, h, send, layout: { major, map }, showMidi, state }) => ( +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, send, layout: { major, map }, showMidi, state, scale }) => (
( {range(w).map((_, x) => ( range(h).map((_, y) => { const note = map([x, y], [w, h]); + const onCore = scale && scale.notes.indexOf(note) >= 0; + const onScale = scale && scale.modulo.indexOf(note % 12) >= 0; return ( ( state={state[note]} send={send} major={major} + scale={onCore ? "core" : (onScale ? "" : "disabled")} > {showMidi ? note : notes.music[note % 12]} @@ -133,7 +165,9 @@ const ChordView = ({ chord }) => { export default class App extends React.Component { state = { layout: 'wicki_hayden', - notes: {}, + scale: 'major', + offset: 60, + state: {}, }; componentDidMount() { @@ -147,6 +181,9 @@ export default class App extends React.Component { } componentDidUpdate(prevProps) { + this.send(CHANNEL, ALL_SOUND_OFF, 0); + this.send(CHANNEL, ALL_NOTES_OFF, 0); + if (prevProps.midiin) prevProps.midiin.onmidimessage = null; @@ -159,17 +196,21 @@ export default class App extends React.Component { setTimeout(() => this.onmessage({ cmd: NOTE_OFF, note }), 250); } - send = (command, note) => { + send = (command, note, vel=127) => { + if (command === NOTE_ON && this.state.offset === null) + this.setState({ offset: note }); + if (!this.props.midiout) return; - const msg = [statbyte(command, 0), note, 127]; + const msg = [statbyte(command, 0), note, vel]; this.props.midiout.send(msg); } - onlayout = (e) => { - this.setState({ layout: e.target.value }); - } + onlayout = (e) => this.setState({ layout: e.target.value }) + onscale = (e) => this.setState({ scale: e.target.value }) + + onoffset = (e) => this.setState({ offset: null }); onmessage = (e) => { const message = parse(e); @@ -182,11 +223,11 @@ export default class App extends React.Component { switch (message.cmd) { case NOTE_ON: - this.setState(({ notes }) => ({ notes: { ...notes, [message.note]: true } })); + this.setState(({ state }) => ({ state: { ...state, [message.note]: true } })); break; case NOTE_OFF: - this.setState(({ notes }) => ({ notes: { ...notes, [message.note]: false } })); + this.setState(({ state }) => ({ state: { ...state, [message.note]: false } })); break; } } @@ -197,7 +238,11 @@ export default class App extends React.Component { const note = notes.key2midi[e.code]; if (!note) return; e.preventDefault(); - this.setState(({ notes }) => ({ notes: { ...notes, [note]: true } })); + + if (this.state.offset === null) + this.setState({ offset: note }); + + this.setState(({ state }) => ({ state: { ...state, [note]: true } })); this.send(NOTE_ON, note); } @@ -207,36 +252,52 @@ export default class App extends React.Component { const note = notes.key2midi[e.code]; if (!note) return; e.preventDefault(); - this.setState(({ notes }) => ({ notes: { ...notes, [note]: false } })); + + this.setState(({ state }) => ({ state: { ...state, [note]: false } })); this.send(NOTE_OFF, note); } render() { const { configure, showMidi } = this.props; - const { layout, notes } = this.state; + const { scale, offset, layout, state } = this.state; - const chord = Object.entries(notes).filter(([note, on]) => on).map(([k, _]) => k); + const chord = Object.entries(state).filter(([note, on]) => on).map(([k, _]) => k); chord.sort(); return (
-