From e447f2d9ca1de1a33d0d25c0a8395d72a4035553 Mon Sep 17 00:00:00 2001 From: s-ol Date: Fri, 5 May 2023 21:54:08 +0200 Subject: restart with v2 --- app/app.js | 364 ---------------------------------------------------------- app/config.js | 168 --------------------------- app/css.js | 1 - app/index.js | 9 -- app/notes.js | 57 --------- app/style.js | 162 -------------------------- 6 files changed, 761 deletions(-) delete mode 100644 app/app.js delete mode 100644 app/config.js delete mode 100644 app/css.js delete mode 100644 app/index.js delete mode 100644 app/notes.js delete mode 100644 app/style.js (limited to 'app') diff --git a/app/app.js b/app/app.js deleted file mode 100644 index 17cdb18..0000000 --- a/app/app.js +++ /dev/null @@ -1,364 +0,0 @@ -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; -const ALL_NOTES_OFF = 0x7B; -const ALL_SOUND_OFF = 0x78; -const NOTE_ON = 0b1001; -const NOTE_OFF = 0b1000; -const CCHANGE = 0b1011; - -const clamp = (val, min=0, max=1) => Math.max(min, Math.min(val, max)); -const ramp = i => new Array(i).fill(true).map((_, i) => i); -const mix = (i, a, b) => i * a + (1-i) * b; - -const parse = message => ({ - cmd: message.data[0] >> 4, - chan: message.data[0] & 0xf, - note: message.data[1], - val: message.data[2] / 127, -}); - -const Hexagon = ({ x, y, children, state, note, major, scale, noteon, noteoff }) => { - if (major == 'row') { - if (y % 2 == 0) - x += 0.5; - } else { - if (x % 2 == 0) - y += 0.5; - } - - return ( -
-
noteon(note)} - onMouseUp={() => noteoff(note)} - > - {children} -
-
- ); -}; - -// B A -// A B A C B -// o C o o -// C - - -const layouts = { - wicki_hayden: { - major: 'row', - map: ([x, y], [w, h]) => { - // A = 5 - // B = 7 - // C = 2 = x - // A + B = 12 = y - let note = 5 + 2 * x + 6 * y; - if (y % 2 == 0) note += 1; - return note; - }, - }, - janko: { - major: 'row', - map: ([x, y], [w, h]) => { - let note = 5 + 2 * x; - if (y % 2 == 0) note += 1; - return note; - }, - }, - trad_piano: { - major: 'row', - map: ([x, y], [w, h]) => { - const pitches = [0, 1, 2, 3, 4, -1, 5, 6, 7, 8, 9, 10, 11, -1]; - - let i = 2 * x; - if (y % 2 == 1) i -= 1; - - const pitch = pitches[i % 14]; - if (pitch < 0) return pitch; - - const oct = Math.floor(i / 14) + Math.floor(y / 2); - return 10 + pitch + 12 * oct; - }, - }, - harmonic: { - // A = 3 - // B = 7 = y - // C = 4 - // C - A = 1 = x - - // A = 7 = y - // B = 4 - // C = -3 - // B + C = 1 = x - major: 'col', - map: ([x, y], [w, h]) => { - let note = 48 + x * 0.5 + y * 7; - if (x % 2 === 1) note -= 3.5; - return note; - }, - }, - gerhard: { - // A = -3 - // B = 1 - // C = 4 - // - // A = 1 = y - // B = 4 - // C = 3 - // B + C = 7 = x - major: 'col', - map: ([x, y], [w, h]) => { - let note = 48 + x * 3.5 + y; - if (x % 2 === 1) note -= 0.5; - return note; - }, - }, -}; - -const scales = { - major: [2, 2, 1, 2, 2, 2, 1], - minor_nat: [2, 1, 2, 2, 1, 2, 2], - minor_harm: [2, 1, 2, 2, 1, 3, 1], - minor_mel: [2, 1, 2, 2, 2, 2, 1], - minor_hung: [2, 1, 3, 1, 1, 3, 1], - whole: [2, 2, 2, 2, 2, 2], - penta: [2, 2, 3, 2, 3], -}; - -const range = i => new Array(i).fill(true); -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, layout: { major, map }, state, scale, transpose, labels, noteon, noteoff }) => ( -
- {range(w).map((_, x) => ( - range(h).map((_, y) => { - const rawNote = map([x, y], [w, h]); - const disabled = rawNote < 0; - const note = disabled ? -1 : rawNote + transpose; - const onCore = !disabled && scale && scale.notes.indexOf(note) >= 0; - const onScale = !disabled && scale && scale.modulo.indexOf(note % 12) >= 0; - return ( - - {!disabled && (labels ? labels[note % 12] : note)} - - ); - }) - ))} -
-); - -const ChordView = ({ chord }) => { - const min = chord[0]; - - return ( -
- {range(13).map((_,i) => ( -
- ))} - {chord.map((n) => ( -
- {(n - min) && (+{n - min})} -
- ))} -
- ); -}; -; - -css(` -main { - position: relative; - padding: 0.5em; - margin: 1em 0; - - display: flex; - justify-content: space-evenly; -} - -main .focus-msg { - position: absolute; - inset: 0; - - display: flex; - font-size: 5em; - justify-content: space-around; - align-items: center; - text-align: center; - - border: 4px solid white; - background: #212121; - pointer-events: none; - - opacity: 0.8; - transition: opacity 0.4s; -} - -main:focus-within .focus-msg { - opacity: 0; -} -`); - -export default () => { - const [settings, setSettings] = useAppState({ - layout: 'wicki_hayden', - scale: 'major', - labels: 'english', - offset: 60, - transpose: 3*12, - w: 12, - h: 4, - }); - const [state, setState] = useState({}); - const midi = useMIDI(); - - const ref = React.createRef(); - - const midiin = midi.inputs.get(settings.midiin); - const midiout = midi.outputs.get(settings.midiout); - - const send = (command, note, vel=127) => { - if (!midiout) return; - - const msg = [statbyte(command, 0), note, vel]; - midiout.send(msg); - }; - - const noteon = (note) => { - if (settings.offset === null) { - setSettings((s) => ({ ...s, offset: note })); - } - - setState((s) => ({ ...s, [note]: true })); - send(NOTE_ON, note); - } - - const noteoff = (note) => { - setState((s) => ({ ...s, [note]: false })); - send(NOTE_OFF, note); - } - - useEffect(() => { - ref.current.onkeydown = (e) => { - if (e.repeat) return; - - const note = notes.key2midi[e.code]; - if (!note) return; - e.preventDefault(); - noteon(note); - } - - ref.current.onkeyup = (e) => { - if (e.repeat) return; - - const note = notes.key2midi[e.code]; - if (!note) return; - e.preventDefault(); - noteoff(note); - } - }); - - useEffect(() => { - send(CHANNEL, ALL_SOUND_OFF, 0); - send(CHANNEL, ALL_NOTES_OFF, 0); - }, [midiout]); - - useEffect(() => { - if (!midiin) return; - - midiin.onmidimessage = (e) => { - const message = parse(e); - - switch (message.cmd) { - case NOTE_ON: - if (settings.offset === null) { - setSettings((s) => ({ ...s, offset: message.note })); - } - - setState((s) => ({ ...s, [message.note]: true })); - break; - - case NOTE_OFF: - setState((s) => ({ ...s, [message.note]: false })); - break; - } - } - - 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 deleted file mode 100644 index 7bf3a1b..0000000 --- a/app/config.js +++ /dev/null @@ -1,168 +0,0 @@ -import React, { useState, useCallback, useEffect } from 'react'; -import { labels as noteLabels } from './notes'; -import { css } from './style'; - -const saveState = (key, state) => { - try { - localStorage.setItem(key, JSON.stringify(state)); - } - catch (e) { - console.error(e); - } -}; - -const loadState = (key, defaultState=null) => { - try { - const data = window.localStorage.getItem(key); - return Object.assign({}, defaultState, data && JSON.parse(data)); - } - catch (e) { - return defaultState; - } -}; - -export const useAppState = (defaultState) => { - const [state, setState] = useState(defaultState); - - useEffect(() => setState(loadState('settings', defaultState)), []); - useEffect(() => saveState('settings', state), [state]); - - return [state, setState]; -}; - -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 }) => ( - -); - -css(` -nav { - display: flex; - flex-wrap: wrap; - justfiy-content: space-between; -} - -nav > .group { - flex: 1 0 auto; - - display: flex; - justify-content: center; - 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; -} -`); - -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/css.js b/app/css.js deleted file mode 100644 index 8b13789..0000000 --- a/app/css.js +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/index.js b/app/index.js deleted file mode 100644 index 75a2a9f..0000000 --- a/app/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './app'; - -const $App = App; - -const node = document.createElement('div'); -ReactDOM.render(<$App />, node); -document.body.appendChild(node); diff --git a/app/notes.js b/app/notes.js deleted file mode 100644 index bd124a5..0000000 --- a/app/notes.js +++ /dev/null @@ -1,57 +0,0 @@ -export const labels = { - english: 'C C# D D# E F F# G G# A A# B'.split(' '), - german: 'C C# D D# E F F# G G# A A# H'.split(' '), - sol: 'do do# re re# mi fa fa# sol sol# la la# si'.split(' '), -}; - -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': 59, - 'KeyW': 61, - 'KeyE': 63, - 'KeyR': 65, - 'KeyT': 67, - 'KeyY': 69, - 'KeyU': 71, - 'KeyI': 73, - 'KeyO': 75, - 'KeyP': 77, - 'BracketLeft': 79, - 'BracketRight': 81, - - 'Digit1': 64, - 'Digit2': 66, - 'Digit3': 68, - 'Digit4': 70, - 'Digit5': 72, - 'Digit6': 74, - 'Digit7': 76, - 'Digit8': 78, - 'Digit9': 80, - 'Digit0': 82, - 'Minus': 84, - 'Equal': 86, -}; diff --git a/app/style.js b/app/style.js deleted file mode 100644 index 26a6ce6..0000000 --- a/app/style.js +++ /dev/null @@ -1,162 +0,0 @@ -export const css = (code) => { - const style = document.createElement('style'); - document.head.appendChild(style); - style.appendChild(document.createTextNode('')); - - let i = 0; - for (const v of code.split('}')) { - if (v.indexOf('{') < 0) continue; - style.sheet.insertRule(v + '}', i++); - } - - return style; -}; - -const longSize = 3; -const sizeA = longSize * Math.sqrt(3); -const sizeB = longSize * 1.5; -const strideA = longSize * Math.sqrt(3); -const strideB = longSize * 1.5; -const rem = (n) => `${n}rem`; - -export const pos = ([x, y], major) => { - let [sx, sy] = major === 'row' - ? [strideA, strideB] - : [strideB, strideA]; - return { - 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 - 0.5) * strideA); - - return rem((h + 1.5) * strideB); -}; - -css(` -body { - color: #eeeeee; - background: #212121; - margin: 2rem; - - font-family: sans-serif; -} - -button, input, select { - background: #363636; - border: 1px solid #eeeeee; - color: #eeeeee; - padding: 0.25em; - border-radius: 0.5em; -} -button:disabled, input:disabled, select:disabled { - opacity: 0.75; -} - -input.small { - width: 4em; -} - -button { - padding: 0.25em 0.5em; -} - -label { - display: inline-block; - font-weight: bold; -} - -.app, .keyboard, .chord { - position: relative; -} - -.hexagon { - position: absolute; - width: ${rem(longSize * 1.4)}; - height: ${rem(longSize * 1.4)}; - border-radius: ${rem(longSize)}; - - box-sizing: border-box; - font-size: ${rem(longSize * 0.5)}; - text-align: center; - background: #696969; - border: 3px solid #696969; - color: #eeeeee; - cursor: pointer; - - display: flex; - justify-content: center; - align-items: center; - - transition: background 300ms, border 300ms; -} - -.hexagon.disabled { - background: #363636 !important; - border-color: #363636; - color: #848484; -} -.hexagon.core { - border-color: #eeeeee; -} - -.hexagon:hover { - background: #848484; - border-color: #848484; -} -.hexagon:active, -.hexagon.active { - background: #eeeeee; - border-color: #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