-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Basic waveform editor (very crude) - Send patch to mGB directly via MIDI - Download .syx file to store and use from some other software
- Loading branch information
0 parents
commit 9f87414
Showing
21 changed files
with
679 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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? |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
``` |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import js from '@eslint/js' | ||
import globals from 'globals' | ||
import reactHooks from 'eslint-plugin-react-hooks' | ||
import reactRefresh from 'eslint-plugin-react-refresh' | ||
import tseslint from 'typescript-eslint' | ||
|
||
export default tseslint.config( | ||
{ ignores: ['dist'] }, | ||
{ | ||
extends: [js.configs.recommended, ...tseslint.configs.recommended], | ||
files: ['**/*.{ts,tsx}'], | ||
languageOptions: { | ||
ecmaVersion: 2020, | ||
globals: globals.browser, | ||
}, | ||
plugins: { | ||
'react-hooks': reactHooks, | ||
'react-refresh': reactRefresh, | ||
}, | ||
rules: { | ||
...reactHooks.configs.recommended.rules, | ||
'react-refresh/only-export-components': [ | ||
'warn', | ||
{ allowConstantExport: true }, | ||
], | ||
}, | ||
}, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>mGB Waveform Editor</title> | ||
</head> | ||
<body> | ||
<div id="root"></div> | ||
<script type="module" src="/src/main.tsx"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Waveform>(INITIAL_WAVEFORM); | ||
|
||
return ( | ||
<Flex row justify="center" align="center"> | ||
<Flex col align="stretch" gap={8} style={{ maxWidth: 800 }}> | ||
<WaveformEditor waveform={waveform} onChange={setWaveform} /> | ||
<SendSysex waveform={waveform} /> | ||
<SysexPreview waveform={waveform} /> | ||
</Flex> | ||
</Flex> | ||
); | ||
} | ||
|
||
export default App; |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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;`} | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string | undefined>(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<Callback<KnobChangeEvent>>( | ||
(e) => setWaveIndex(e.value), | ||
[] | ||
); | ||
|
||
if (!perm) return <strong>Error: Permission unavailable</strong>; | ||
|
||
if (!midi?.sysexEnabled) return <strong>Error: SysEx not enabled</strong>; | ||
|
||
if (!midi) return <strong>Error: No MIDIAccess</strong>; | ||
|
||
if (!portId && midi.outputs.size) { | ||
setPortId([...midi.outputs.values()][0].id); | ||
} | ||
|
||
return ( | ||
<Fieldset> | ||
<Flex | ||
row | ||
align="center" | ||
justify="start" | ||
gap={8} | ||
as="form" | ||
onSubmit={sendSysex} | ||
> | ||
<Field label="Port"> | ||
{(id) => ( | ||
<Dropdown | ||
inputId={id} | ||
name="midiPort" | ||
options={[...midi.outputs.values()]} | ||
optionLabel="name" | ||
optionValue="id" | ||
value={portId} | ||
valueTemplate={( | ||
option: MIDIOutput | undefined, | ||
{ placeholder } | ||
) => <span>{option?.name ?? placeholder}</span>} | ||
onChange={(e) => setPortId(e.value)} | ||
placeholder="MIDI Port" | ||
/> | ||
)} | ||
</Field> | ||
<Field label="Waveform index"> | ||
{(id) => ( | ||
<Knob | ||
name="waveIndex" | ||
id={id} | ||
onChange={handleWaveIndexChange} | ||
value={waveIndex} | ||
max={16} | ||
/> | ||
)} | ||
</Field> | ||
<Button label="Send" icon={PrimeIcons.PLAY} /> | ||
</Flex> | ||
</Fieldset> | ||
); | ||
}; | ||
|
||
const Field: React.FC<{ | ||
label: string; | ||
children: (id: string) => React.ReactNode; | ||
}> = ({ label, children }) => { | ||
const id = useId(); | ||
return ( | ||
<Flex row align="center" gap={8} style={{ alignContent: "center" }}> | ||
<label htmlFor={id}>{label}</label> | ||
{children(id)} | ||
</Flex> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 data> | ||
${wave.join(", ")} | ||
</wave data> | ||
${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 ( | ||
<Card title="Sysex data"> | ||
<code> | ||
<pre>{output}</pre> | ||
</code> | ||
<Flex row align="end" grow="1"> | ||
<a href={blobUrl} download="mGB-patch.syx"> | ||
Download .syx file | ||
</a> | ||
</Flex> | ||
</Card> | ||
); | ||
}; |
Oops, something went wrong.