From 9f874147b5a2fb57f673d8f00bd1baebb24f59d7 Mon Sep 17 00:00:00 2001 From: Tim Stirrat Date: Thu, 29 Aug 2024 21:02:23 +1000 Subject: [PATCH] Version 1.0 - Basic waveform editor (very crude) - Send patch to mGB directly via MIDI - Download .syx file to store and use from some other software --- .gitignore | 24 ++++++ README.md | 12 +++ bun.lockb | Bin 0 -> 99758 bytes eslint.config.js | 28 +++++++ index.html | 13 +++ package.json | 33 ++++++++ src/App.tsx | 28 +++++++ src/assets/react.svg | 1 + src/components/Flex.tsx | 28 +++++++ src/components/SendSysex.tsx | 105 ++++++++++++++++++++++++ src/components/SysexPreview.tsx | 51 ++++++++++++ src/components/WaveformEditor.tsx | 130 ++++++++++++++++++++++++++++++ src/hooks/use_midi.ts | 36 +++++++++ src/lib/sysex.ts | 100 +++++++++++++++++++++++ src/main.tsx | 15 ++++ src/types.ts | 10 +++ src/vite-env.d.ts | 1 + tsconfig.app.json | 24 ++++++ tsconfig.json | 7 ++ tsconfig.node.json | 22 +++++ vite.config.ts | 11 +++ 21 files changed, 679 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 bun.lockb create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 package.json create mode 100644 src/App.tsx create mode 100644 src/assets/react.svg create mode 100644 src/components/Flex.tsx create mode 100644 src/components/SendSysex.tsx create mode 100644 src/components/SysexPreview.tsx create mode 100644 src/components/WaveformEditor.tsx create mode 100644 src/hooks/use_midi.ts create mode 100644 src/lib/sysex.ts create mode 100644 src/main.tsx create mode 100644 src/types.ts create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/README.md b/README.md new file mode 100644 index 0000000..4dfd806 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# mGB Waveform Editor + +This tool allows one to replace the waveform that is selected in the WAV channel. It sends a custom +SysEx message so you will need an ArduinoBoy. + +I have not tested it in RetroPlug, but if VSTs allow SysEx, then that would be possible too. + +### Running the development server + +``` +bun run dev +``` diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..7d107230990bb4b1ca13bc7f0a818eb06ae5ed94 GIT binary patch literal 99758 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

fKJQlzQVdUD>`h`?QRx=$BO( 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 + + + + + + mGB Waveform Editor + + +

+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..33e5052 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "waveform-edit", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11.13.3", + "@emotion/styled": "^11.13.0", + "primeicons": "^7.0.0", + "primereact": "^10.8.2", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@eslint/js": "^9.9.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "globals": "^15.9.0", + "typescript": "^5.5.3", + "typescript-eslint": "^8.0.1", + "vite": "^5.4.1" + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..93d12db --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,28 @@ +import { useState } from "react"; +import { SendSysex } from "./components/SendSysex"; +import { WaveformEditor } from "./components/WaveformEditor"; +import { Waveform } from "./types"; +import { SysexPreview } from "./components/SysexPreview"; +import { Flex } from "./components/Flex"; + +const INITIAL_WAVEFORM: number[] = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + // + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, +]; + +function App() { + const [waveform, setWaveform] = useState(INITIAL_WAVEFORM); + + return ( + + + + + + + + ); +} + +export default App; diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Flex.tsx b/src/components/Flex.tsx new file mode 100644 index 0000000..fbe94ea --- /dev/null +++ b/src/components/Flex.tsx @@ -0,0 +1,28 @@ +import styled from "@emotion/styled"; + +export const Flex = styled.div<{ + row?: boolean; + rowReverse?: boolean; + col?: boolean; + colReverse?: boolean; + justify?: string; + align?: string; + grow?: string; + shrink?: string; + gap?: number; +}>` + ${({ row }) => row && `display: flex; flex-direction: row;`} + ${({ rowReverse }) => + rowReverse && `display: flex; flex-direction: row-reverse;`} + + ${({ col }) => col && `display: flex; flex-direction: column;`} + ${({ colReverse }) => + colReverse && `display: flex; flex-direction: column-reverse;`} + + ${({ justify }) => justify && `justify-content: ${justify};`} + ${({ align }) => align && `align-items: ${align};`} + ${({ gap }) => gap && `gap: ${gap}px;`} + + ${({ grow }) => grow && `flex-grow: ${grow};`} + ${({ shrink }) => shrink && `flex-shrink: ${shrink}; min-width: 0;`} +`; diff --git a/src/components/SendSysex.tsx b/src/components/SendSysex.tsx new file mode 100644 index 0000000..49d1e25 --- /dev/null +++ b/src/components/SendSysex.tsx @@ -0,0 +1,105 @@ +import { Dropdown } from "primereact/dropdown"; +import { useMidiAccess, useMidiPermission } from "../hooks/use_midi"; +import { Flex } from "./Flex"; +import { FormEventHandler, useCallback, useId, useState } from "react"; +import { Button } from "primereact/button"; +import { PrimeIcons } from "primereact/api"; +import { Knob, KnobChangeEvent } from "primereact/knob"; +import { Callback, Waveform } from "../types"; +import { Fieldset } from "primereact/fieldset"; +import { sendWaveformSysex } from "../lib/sysex"; + +export const SendSysex: React.FC<{ readonly waveform: Waveform }> = ({ + waveform, +}) => { + const perm = useMidiPermission(); + const midi = useMidiAccess(); + + const [portId, setPortId] = useState(undefined); + const [waveIndex, setWaveIndex] = useState(0); + + const sendSysex: FormEventHandler = (e) => { + e.preventDefault(); + e.stopPropagation(); + + if (!portId) throw new Error("You must select a port"); + + const port = midi?.outputs.get(portId); + + if (!port) throw new Error(`Invalid port ${portId}`); + + sendWaveformSysex(port, waveform); + }; + + const handleWaveIndexChange = useCallback>( + (e) => setWaveIndex(e.value), + [] + ); + + if (!perm) return Error: Permission unavailable; + + if (!midi?.sysexEnabled) return Error: SysEx not enabled; + + if (!midi) return Error: No MIDIAccess; + + if (!portId && midi.outputs.size) { + setPortId([...midi.outputs.values()][0].id); + } + + return ( +
+ + + {(id) => ( + {option?.name ?? placeholder}} + onChange={(e) => setPortId(e.value)} + placeholder="MIDI Port" + /> + )} + + + {(id) => ( + + )} + +
+ ); +}; + +const Field: React.FC<{ + label: string; + children: (id: string) => React.ReactNode; +}> = ({ label, children }) => { + const id = useId(); + return ( + + + {children(id)} + + ); +}; diff --git a/src/components/SysexPreview.tsx b/src/components/SysexPreview.tsx new file mode 100644 index 0000000..b26b654 --- /dev/null +++ b/src/components/SysexPreview.tsx @@ -0,0 +1,51 @@ +import { Card } from "primereact/card"; +import { Waveform } from "../types"; +import { sysexWaveformMessage, toHex } from "../lib/sysex"; +import { Button } from "primereact/button"; +import { PrimeIcons } from "primereact/api"; +import { Flex } from "./Flex"; +import { useMemo } from "react"; +import { IconUtils } from "primereact/utils"; + +export const SysexPreview: React.FC<{ waveform: Waveform }> = ({ + waveform, +}) => { + const sysex = toHex(sysexWaveformMessage(waveform)); + + const wave = sysex.slice(4, 23); + + const output = `${sysex[0]} = SYSEX header +${sysex[1]} = SYSEX type +${sysex[2]} = mGB id +${sysex[3]} = mGB channel + + +${wave.join(", ")} + + +${sysex[23]} = SYSEX EOF +`; + + // Convert Blob to URL + const blobUrl = useMemo(() => { + // Convert object to Blob + const blobConfig = new Blob( + [Uint8Array.from(sysexWaveformMessage(waveform))], + { type: "application/octet-stream" } + ); + return URL.createObjectURL(blobConfig); + }, [waveform]); + + return ( + + +
{output}
+
+ +
+ Download .syx file + + + + ); +}; diff --git a/src/components/WaveformEditor.tsx b/src/components/WaveformEditor.tsx new file mode 100644 index 0000000..026e0d8 --- /dev/null +++ b/src/components/WaveformEditor.tsx @@ -0,0 +1,130 @@ +import styled from "@emotion/styled"; +import { memo, MouseEventHandler, useCallback, useMemo } from "react"; +import { Flex } from "./Flex"; +import { BIT_DEPTH, Callback, SAMPLES_PER_WAVEFORM, Waveform } from "../types"; + +export const WaveformEditor: React.FC<{ + readonly waveform: Waveform; + readonly onChange: Callback; +}> = ({ waveform, onChange }) => { + if (waveform.length !== SAMPLES_PER_WAVEFORM) { + throw new Error( + `Waveform has incorrect sample size ${waveform.length}. Should be ${SAMPLES_PER_WAVEFORM}` + ); + } + + const handleChange = useCallback( + (sampleIndex, value) => { + const newWaveform = [...waveform]; + newWaveform[sampleIndex] = value; + + onChange(newWaveform); + }, + [onChange, waveform] + ); + + return ( + + + {waveform.slice(0, SAMPLES_PER_WAVEFORM).map((val, i) => ( + + {val.toString(16).toUpperCase()} + + ))} + + + {waveform.map((sample, i) => ( + + ))} + + + ); +}; + +const POINT_SIZE = 20; + +const Block = styled(Flex)({ + width: POINT_SIZE, + height: POINT_SIZE, + textAlign: "center", + verticalAlign: "middle", +}); + +const SAMPLES = new Array(BIT_DEPTH).fill(0); + +const SampleColumn: React.FC<{ + readonly index: number; + readonly value: number; + readonly onChange: OnSampleChange; +}> = ({ index, value, onChange }) => { + const samples = useMemo(() => [...SAMPLES], []); + + if (value >= BIT_DEPTH || value < 0) { + throw new Error( + `Invalid sample value: ${value.toString(16)}. Range is 0 - F` + ); + } + + const handleChange = useCallback( + (newValue: number) => { + onChange(index, newValue); + }, + [index, onChange] + ); + + return ( + + {samples.map((_, i) => ( + + ))} + + ); +}; + +const SamplePointWrapper = styled.div<{ isActive: boolean }>( + ({ isActive: isActive }) => ({ + width: POINT_SIZE, + height: POINT_SIZE, + + backgroundColor: isActive ? "white" : undefined, + }) +); + +type OnSampleChange = (index: number, value: number) => void; + +const LEFT_BUTTON = 1; + +const SamplePoint: React.FC<{ + readonly index: number; + readonly onChange: Callback; + readonly value: number; + readonly isActive: boolean; +}> = memo(({ value, isActive, onChange }) => { + const setPoint: MouseEventHandler = (e) => { + if (e.buttons & LEFT_BUTTON) { + // console.log(`${index} = ${value}`); + onChange(value); + } + e.preventDefault(); + e.stopPropagation(); + }; + + return ( + + ); +}); diff --git a/src/hooks/use_midi.ts b/src/hooks/use_midi.ts new file mode 100644 index 0000000..663f2e7 --- /dev/null +++ b/src/hooks/use_midi.ts @@ -0,0 +1,36 @@ +import { useState } from "react"; + +export function useMidiPermission() { + const [granted, setGranted] = useState( + undefined + ); + + (async () => { + try { + const { state } = await navigator.permissions.query({ + name: "midi", + sysex: true, + }); + setGranted(state); + } catch (e) { + console.error(`Failed to get MIDI permission`, e); + } + })(); + + return granted; +} + +export function useMidiAccess() { + const [midi, setMidi] = useState(undefined); + + (async () => { + try { + const midiAccess = await navigator.requestMIDIAccess({ sysex: true }); + setMidi(midiAccess); + } catch (e) { + console.error(`Failed to get MIDI access`, e); + } + })(); + + return midi; +} diff --git a/src/lib/sysex.ts b/src/lib/sysex.ts new file mode 100644 index 0000000..4926d82 --- /dev/null +++ b/src/lib/sysex.ts @@ -0,0 +1,100 @@ +import { BYTES_PER_WAVEFORM, SAMPLES_PER_WAVEFORM, Waveform } from "../types"; + +const MIDI_STATUS_SYSEX = 0xf0; +const SYSEX_NON_COMMERCIAL = 0x7d; +const SYSEX_EOF = 0xf7; + +const SYSEX_MGB_ID = 0x69; +const MGB_WAV_CHANNEL = 0x02; + +export function sendWaveformSysex(port: MIDIOutput, waveform: Waveform) { + port.send(sysexWaveformMessage(waveform)); +} + +export function toBytes(waveform: Waveform) { + return waveform.reduce((out: number[], sample: number, index: number) => { + const isHighNibble = index % 2; + if (isHighNibble) { + const lowNibble = out[out.length - 1]; + + const byte = lowNibble + (sample << 4); + + out[out.length - 1] = byte; + } else { + out[out.length] = sample; + } + + return out; + }, []); +} + +export function sysexMessage(message: number[]) { + return [MIDI_STATUS_SYSEX, SYSEX_NON_COMMERCIAL, ...message, SYSEX_EOF]; +} + +export function sysexWaveformMessage(waveform: number[]) { + if (waveform.length !== SAMPLES_PER_WAVEFORM) + throw new Error( + `Incorrect wav bytes ${waveform.length}. Should be ${SAMPLES_PER_WAVEFORM}` + ); + + const wavBytes = toBytes(waveform); + + if (wavBytes.length !== BYTES_PER_WAVEFORM) + throw new Error( + `Incorrect wav bytes ${wavBytes.length}. Should be ${BYTES_PER_WAVEFORM}` + ); + + return sysexMessage([ + SYSEX_MGB_ID, + MGB_WAV_CHANNEL, + ...encodeSysEx7Bit(wavBytes), + ]); +} + +/** + * Encode System Exclusive messages. + * + * SysEx messages are encoded to guarantee transmission of data bytes higher than + * 127 without breaking the MIDI protocol. Use this static method to convert the + * data you want to send. + * + * Code inspired from Ruin & Wesen's SysEx encoder/decoder - http://ruinwesen.com + */ +function encodeSysEx7Bit(inData: number[]): number[] { + const outSysEx: number[] = []; + + // const outLength = 0; // Num bytes in output array. + let count = 0; // Num 7bytes in a block. + + let msbIndex = 0; + outSysEx[msbIndex] = 0; + for (let i = 0; i < inData.length; ++i) { + const data = inData[i]; + + if (data > 0xff) + throw new Error(`Can only encode 8 bit numbers, got ${data}`); + + const msb = data >> 7; + const body = data & 0x7f; + + outSysEx[msbIndex] |= msb << (6 - count); + outSysEx[msbIndex + 1 + count] = body; + + if (count++ == 6) { + msbIndex += 8; + // outLength += 8; + outSysEx[msbIndex] = 0; + count = 0; + } + } + return outSysEx; +} + +export function toHex(message: number[]) { + return message.map((b) => b.toString(16)); +} + +export function toHexWithPrefix(message: number[]) { + return message.map((b) => "0x" + b.toString(16)); +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..f6757fe --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,15 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App.tsx"; +import { PrimeReactProvider } from "primereact/api"; + +import "primereact/resources/themes/lara-dark-teal/theme.css"; +import "primeicons/primeicons.css"; + +createRoot(document.getElementById("root")!).render( + + + + + +); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..4bec316 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,10 @@ +export type Callback = (v: T) => void; + +/** The representation of a waveform. 32 nibbles long. */ +export type Waveform = number[]; + +export const SAMPLES_PER_WAVEFORM = 32; + +export const BYTES_PER_WAVEFORM = 16; + +export const BIT_DEPTH = 16; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..f0a2350 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..0d3d714 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..f9e0483 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react-swc"; +import mkcert from "vite-plugin-mkcert"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react(), + // mkcert(), + ], +});