From 930c3a7c40e1b1e5fa834167d4fc79a067c322f4 Mon Sep 17 00:00:00 2001 From: s-ol Date: Mon, 20 Dec 2021 16:22:57 +0100 Subject: add chord display --- app/app.js | 189 +++++++++++++++++++++++++++++++++++------------------------ app/notes.js | 54 ++++++++++++++++- app/style.js | 59 +++++++++++++++++-- 3 files changed, 222 insertions(+), 80 deletions(-) diff --git a/app/app.js b/app/app.js index 5fa04b4..172a908 100644 --- a/app/app.js +++ b/app/app.js @@ -1,6 +1,6 @@ import React from 'react'; -import { pos, height } from './style'; -import notes from './notes'; +import { pos, width, height } from './style'; +import * as notes from './notes'; const statbyte = (stat, chan) => (stat << 4) | chan; const NOTE_ON = 0b1001; @@ -78,61 +78,74 @@ const layouts = { const range = i => new Array(i).fill(true); -class Keyboard extends React.Component { - state = {}; +const Keyboard = ({ w, h, send, layout: { major, map }, showMidi, state }) => ( +
+ {range(w).map((_, x) => ( + range(h).map((_, y) => { + const note = map([x, y], [w, h]); + return ( + + {showMidi ? note : notes.music[note % 12]} + + ); + }) + ))} +
+); + +const ChordView = ({ chord }) => { + const min = chord[0]; - onmessage = (message) => { - switch (message.cmd) { - case NOTE_ON: - this.setState(old => ({ ...old, [message.note]: true })); - break; - - case NOTE_OFF: - this.setState(old => ({ ...old, [message.note]: false })); - break; - } - } - - render() { - const { w, h, send, layout, showMidi } = this.props; - const { major, map } = layouts[layout]; - - return ( -
- {range(w).map((_, x) => ( - range(h).map((_, y) => { - const note = map([x, y], [w, h]); - return ( - - {showMidi ? notes[note % 12] : note} - - ); - }) - ))} -
- ); - } -} + return ( +
+ {range(13).map((_,i) => ( +
+ ))} + {chord.map((n) => ( +
+ {(n - min) && (+{n - min})} +
+ ))} +
+ ); +}; export default class App extends React.Component { - keyboardRef = React.createRef(); - state = { layout: 'wicki_hayden', + notes: {}, }; + componentDidMount() { + document.addEventListener("keydown", this.onkeydown); + document.addEventListener("keyup", this.onkeyup); + } + + componentWillUnmount() { + document.removeEventListener("keydown", this.onkeydown); + document.removeEventListener("keyup", this.onkeyup); + } + componentDidUpdate(prevProps) { if (prevProps.midiin) prevProps.midiin.onmidimessage = null; @@ -142,41 +155,62 @@ export default class App extends React.Component { } blink = (note) => { - if (this.keyboardRef.current) { - const { current } = this.keyboardRef; - current.onmessage({ cmd: NOTE_ON, note }); - setTimeout(() => current.onmessage({ cmd: NOTE_OFF, note }), 250); - } + this.onmessage({ cmd: NOTE_ON, note }); + setTimeout(() => this.onmessage({ cmd: NOTE_OFF, note }), 250); + } + + send = (command, note) => { + if (!this.props.midiout) + return; + + const msg = [statbyte(command, 0), note, 127]; + this.props.midiout.send(msg); + } + + onlayout = (e) => { + this.setState({ layout: e.target.value }); } onmessage = (e) => { const message = parse(e); - if (this.keyboardRef.current) - this.keyboardRef.current.onmessage(message); - switch (message.cmd) { case NOTE_ON: console.info(message.note); break; } - } - send = (command, note) => { - if (!this.props.midiout) - return; + switch (message.cmd) { + case NOTE_ON: + this.setState(({ notes }) => ({ notes: { ...notes, [message.note]: true } })); + break; - const msg = [statbyte(command, 0), note, 127]; - this.props.midiout.send(msg); + case NOTE_OFF: + this.setState(({ notes }) => ({ notes: { ...notes, [message.note]: false } })); + break; + } } - onlayout = (e) => { - this.setState({ layout: e.target.value }); + onkeydown = (e) => { + const note = notes.key2midi[e.code]; + if (!note) return; + e.preventDefault(); + this.setState(({ notes }) => ({ notes: { ...notes, [note]: true } })); + } + + onkeyup = (e) => { + const note = notes.key2midi[e.code]; + if (!note) return; + e.preventDefault(); + this.setState(({ notes }) => ({ notes: { ...notes, [note]: false } })); } render() { const { configure, showMidi } = this.props; - const { layout } = this.state; + const { layout, notes } = this.state; + + const chord = Object.entries(notes).filter(([note, on]) => on).map(([k, _]) => k); + chord.sort(); return (
@@ -189,14 +223,19 @@ export default class App extends React.Component { - +
+ + +
); } diff --git a/app/notes.js b/app/notes.js index 3d0e659..0e3e92d 100644 --- a/app/notes.js +++ b/app/notes.js @@ -1,4 +1,4 @@ -export default { +export const music = { 11: 'B', 10: 'A#', 9: 'A', @@ -12,3 +12,55 @@ export default { 1: 'C#', 0: 'C', }; + +export const key2midi = { + 'KeyZ': 49, + 'KeyX': 51, + 'KeyC': 53, + 'KeyV': 55, + 'KeyB': 57, + 'KeyN': 59, + 'KeyM': 61, + 'Comma': 63, + 'Period': 65, + 'Slash': 67, + + 'KeyA': 54, + 'KeyS': 56, + 'KeyD': 58, + 'KeyF': 60, + 'KeyG': 62, + 'KeyH': 64, + 'KeyJ': 66, + 'KeyK': 68, + 'KeyL': 70, + 'Semicolon': 72, + 'Quote': 74, + 'Backslash': 76, + + 'KeyQ': 61, + 'KeyW': 63, + 'KeyE': 65, + 'KeyR': 67, + 'KeyT': 69, + 'KeyY': 71, + 'KeyU': 73, + 'KeyI': 75, + 'KeyO': 77, + 'KeyP': 79, + 'BracketLeft': 81, + 'BracketRight': 83, + + 'Digit1': 66, + 'Digit2': 68, + 'Digit3': 70, + 'Digit4': 72, + 'Digit5': 74, + 'Digit6': 76, + 'Digit7': 78, + 'Digit8': 80, + 'Digit9': 82, + 'Digit0': 84, + 'Minus': 86, + 'Equal': 88, +}; diff --git a/app/style.js b/app/style.js index f2e1b7c..ea59e75 100644 --- a/app/style.js +++ b/app/style.js @@ -14,7 +14,7 @@ export const css = (code) => { const longSize = 3; const sizeA = longSize * Math.sqrt(3); -const sizeB = longSize * 2; +const sizeB = longSize * 1.5; const strideA = longSize * Math.sqrt(3); const strideB = longSize * 1.5; const rem = (n) => `${n}rem`; @@ -24,14 +24,21 @@ export const pos = ([x, y], major) => { ? [strideA, strideB] : [strideB, strideA]; return { - bottom: rem(y * sy), + bottom: rem(y * sy), left: rem(x * sx), }; }; +export const width = (w, major) => { + if (major === 'row') + return rem((w + 0.5) * strideA); + + return rem((w + 0.5) * strideB); +}; + export const height = (h, major) => { if (major === 'row') - return rem(h * strideA); + return rem((h - 0.5) * strideA); return rem((h + 1.5) * strideB); }; @@ -45,10 +52,15 @@ body { font-family: sans-serif; } -.app, .keyboard { +.app, .keyboard, .chord { position: relative; } +.main { + display: flex; + justify-content: space-evenly; +} + .hexagon { position: absolute; width: ${rem(sizeA)}; @@ -83,4 +95,43 @@ body { background: #eeeeee; color: #696969; } + +.chord { + display: flex; + flex-direction: column; + justify-content: space-between; + margin: 2rem 0; + width: 2rem; +} + +.chord .blip { + height: 1px; + margin: auto 0 0; + background: #eeeeee; + opacity: 0.75; +} + +.chord .blip:nth-child(2n) { + margin: auto 0.3rem 0; + opacity: 0.5; +} +.chord .blip:first-child { + margin-top: 0; +} + +.chord .note { + position: absolute; + left: 0; + right: 0; + margin: auto; + height: 6%; + aspect-ratio: 1; + background: #eeeeee; + border-radius: 100%; +} + +.chord .note span { + position: absolute; + left: 2rem; +} `); -- cgit v1.2.3