Skip to content

Commit

Permalink
Allow choosing from all the mGB built-in waves
Browse files Browse the repository at this point in the history
  • Loading branch information
tstirrat committed Sep 1, 2024
1 parent dc291b7 commit b40bdc2
Show file tree
Hide file tree
Showing 10 changed files with 325 additions and 121 deletions.
31 changes: 20 additions & 11 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import { useState } from "react";
import { SendSysex } from "./components/SendSysex";
import { WaveformEditor } from "./components/WaveformEditor";
import { Waveform } from "./types";
import { SysexPreview } from "./components/SysexPreview";
import { Callback, Waveform } from "./types";
import { MGB_WAVEFORMS } from "./lib/globals";
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,
];
import { fromBytes } from "./lib/sysex";
import { WaveformSelector } from "./components/WaveformSelector";

function App() {
const [waveform, setWaveform] = useState<Waveform>(INITIAL_WAVEFORM);
// deep clone the mGB default set (bytes) into samples
const [waveforms, setWaveforms] = useState<Waveform[]>(
MGB_WAVEFORMS.map((bytes) => [...fromBytes(bytes)])
);
const [waveIndex, setWaveIndex] = useState(0);

const waveform = waveforms[waveIndex];

const setCurrentWaveform: Callback<Waveform> = (waveform) =>
setWaveforms((prev) => {
const newWaveforms = [...prev];
newWaveforms[waveIndex] = waveform;
return newWaveforms;
});

return (
<Flex row justify="center" align="center">
<Flex col align="stretch" gap={8} style={{ maxWidth: 800 }}>
<WaveformEditor waveform={waveform} onChange={setWaveform} />
<WaveformEditor waveform={waveform} onChange={setCurrentWaveform} />
<WaveformSelector value={waveIndex} onChange={setWaveIndex} />
<SendSysex waveform={waveform} />
<SysexPreview waveform={waveform} />
</Flex>
</Flex>
);
Expand Down
15 changes: 15 additions & 0 deletions src/components/Field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useId } from "react";
import { Flex } from "./Flex";

export 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>
);
};
86 changes: 42 additions & 44 deletions src/components/SendSysex.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Dropdown } from "primereact/dropdown";
import { useMidiAccess, useMidiPermission } from "../hooks/use_midi";
import { Flex } from "./Flex";
import { FormEventHandler, useId, useState } from "react";
import { FormEventHandler, useState } from "react";
import { Button } from "primereact/button";
import { PrimeIcons } from "primereact/api";
import { Waveform } from "../types";
import { Fieldset } from "primereact/fieldset";
import { sendWaveformSysex } from "../lib/sysex";
import { Field } from "./Field";
import { Card } from "primereact/card";
import { SysexPreview } from "./SysexPreview";
import { SysexDownloadButton } from "./SysexDownloadButton";

export const SendSysex: React.FC<{ readonly waveform: Waveform }> = ({
waveform,
Expand Down Expand Up @@ -40,48 +43,43 @@ export const SendSysex: React.FC<{ readonly waveform: Waveform }> = ({
}

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>
<Button label="Send" icon={PrimeIcons.PLAY} />
</Flex>
</Fieldset>
);
};
<Card title="Send to mGB">
<Flex col align="start" gap={8}>
<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>

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>
<Button label="Send" icon={PrimeIcons.PLAY} />
</Flex>

<Flex row align="baseline">
<SysexDownloadButton waveform={waveform} />
<SysexPreview waveform={waveform} />
</Flex>
</Flex>
</Card>
);
};
41 changes: 41 additions & 0 deletions src/components/SysexDownloadButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Waveform } from "../types";
import { sysexWaveformMessage } from "../lib/sysex";
import { useMemo } from "react";
import { Button } from "primereact/button";
import { PrimeIcons } from "primereact/api";

export const SysexDownloadButton: React.FC<{ waveform: Waveform }> = ({
waveform,
}) => {
const blobUrl = useMemo(() => {
// Convert object to Blob
const blobConfig = new Blob(
[Uint8Array.from(sysexWaveformMessage(waveform))],
{ type: "application/octet-stream" }
);
return URL.createObjectURL(blobConfig);
}, [waveform]);

const handleDownload = () => {
const fileName = "mGB-patch.syx";
downloadFile(blobUrl, fileName);
};

return (
<Button
text
icon={PrimeIcons.DOWNLOAD}
onClick={handleDownload}
label="Download .syx file"
/>
);
};

function downloadFile(blobUrl: string, fileName: string) {
const link = document.createElement("a");
link.href = blobUrl;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
55 changes: 11 additions & 44 deletions src/components/SysexPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { Card } from "primereact/card";
import { Waveform } from "../types";
import { sysexWaveformMessage, toHex } from "../lib/sysex";
import { useMemo } from "react";
import { Button } from "primereact/button";
import { PrimeIcons } from "primereact/api";

export const SysexPreview: React.FC<{ waveform: Waveform }> = ({
waveform,
Expand All @@ -18,51 +14,22 @@ ${sysex[2]} = mGB id
${sysex[3]} = mGB channel
<wave data>
${wave.join(", ")}
${wave.slice(0, 8).join(", ")}
${wave.slice(8, 16).join(", ")}
${wave.slice(16).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]);

const handleDownload = () => {
const fileName = "mGB-patch.syx";
downloadFile(blobUrl, fileName);
};

return (
<Card>
<Button
icon={PrimeIcons.DOWNLOAD}
onClick={handleDownload}
label="Download .syx file"
/>
<details>
<summary>
<strong>Sysex data</strong>
</summary>
<code>
<pre>{output}</pre>
</code>
</details>
</Card>
<details>
<summary>
<strong>Show SysEx</strong>
</summary>
<code>
<pre>{output}</pre>
</code>
</details>
);
};

function downloadFile(blobUrl: string, fileName: string) {
const link = document.createElement("a");
link.href = blobUrl;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
36 changes: 26 additions & 10 deletions src/components/WaveformEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
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";
import { Callback, Waveform } from "../types";
import { BIT_DEPTH, SAMPLES_PER_WAVEFORM } from "../lib/globals";

export const WaveformEditor: React.FC<{
readonly waveform: Waveform;
Expand All @@ -25,13 +26,7 @@ export const WaveformEditor: React.FC<{

return (
<Flex col align="center">
<Flex row grow="1" align="center" style={{ height: POINT_SIZE }}>
{waveform.slice(0, SAMPLES_PER_WAVEFORM).map((val, i) => (
<Block key={i} align="center" justify="center">
{val.toString(16).toUpperCase()}
</Block>
))}
</Flex>
<WaveformHex waveform={waveform} />
<Flex row grow="1" align="center" style={{ border: `2px solid white` }}>
{waveform.map((sample, i) => (
<SampleColumn
Expand All @@ -48,12 +43,13 @@ export const WaveformEditor: React.FC<{

const POINT_SIZE = 20;

const Block = styled(Flex)({
const Block = styled(Flex)<{ primary?: boolean }>(({ primary }) => ({
width: POINT_SIZE,
height: POINT_SIZE,
textAlign: "center",
verticalAlign: "middle",
});
color: primary ? `var(--primary-color)` : undefined,
}));

const SAMPLES = new Array(BIT_DEPTH).fill(0);

Expand Down Expand Up @@ -128,3 +124,23 @@ const SamplePoint: React.FC<{
/>
);
});

/** The indexes to show in primary color so that hex grouping is more clear */
const HEX_INDEX = [2, 3, 6, 7, 10, 11, 14, 15, 18, 19, 22, 23, 26, 27, 30, 31];

const WaveformHex: React.FC<{ waveform: Waveform }> = ({ waveform }) => {
return (
<Flex row grow="1" align="center" style={{ height: POINT_SIZE }}>
{waveform.slice(0, SAMPLES_PER_WAVEFORM).map((val, i) => (
<Block
key={i}
align="center"
justify="center"
primary={HEX_INDEX.includes(i)}
>
{val.toString(16).toUpperCase()}
</Block>
))}
</Flex>
);
};
44 changes: 44 additions & 0 deletions src/components/WaveformSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { InputText } from "primereact/inputtext";
import { Slider } from "primereact/slider";
import { memo } from "react";
import { WAVEFORM_SLOTS } from "../lib/globals";
import { Callback } from "../types";
import { Field } from "./Field";
import { Flex } from "./Flex";

export const WaveformSelector: React.FC<{
value: number;
onChange: Callback<number>;
}> = ({ value, onChange }) => {
if (value >= WAVEFORM_SLOTS)
throw new Error(
`Invalid waveform index ${value}. Only 0-${WAVEFORM_SLOTS - 1} are valid`
);

return (
<Field label="Waveform index">
{(id) => <SliderWithDisplay id={id} value={value} onChange={onChange} />}
</Field>
);
};

const SliderWithDisplay: React.FC<{
id: string;
value: number;
onChange: Callback<number>;
}> = memo(({ id, value, onChange }) => {
return (
<Flex row gap={16} align="center" grow="1" style={{ padding: "0 8px" }}>
<Slider
id={id}
style={{ flex: 1 }}
max={WAVEFORM_SLOTS - 1}
value={value}
onChange={(e) => {
if (typeof e.value === "number") onChange(e.value);
}}
/>
<InputText value={value.toString()} readOnly style={{ width: "48px" }} />
</Flex>
);
});
Loading

0 comments on commit b40bdc2

Please sign in to comment.