midi notes avaiable on ui

This commit is contained in:
michalcourson
2025-10-30 21:39:06 -04:00
parent 3645e38dd5
commit 55e80b4c74
7 changed files with 604 additions and 303 deletions

View File

@ -27,21 +27,22 @@ import "@fontsource/roboto/500.css";
import "@fontsource/roboto/700.css";
import Box from "@mui/material/Container";
import Checkbox from "@mui/material/Checkbox";
// import Checkbox from "@mui/material/Checkbox";
import Typography from "@mui/material/Typography";
import Container from "@mui/material/Container";
import Slider from "@mui/material/Slider";
import Button from "@mui/material/Button";
import CardActions from "@mui/material/CardActions";
import Snackbar from "@mui/material/Snackbar";
import IconButton from "@mui/material/IconButton";
import CloseIcon from "@mui/icons-material/Close";
import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select";
import FormGroup from "@mui/material/FormGroup";
import FormControlLabel from "@mui/material/FormControlLabel";
// import Button from "@mui/material/Button";
// import CardActions from "@mui/material/CardActions";
// import Snackbar from "@mui/material/Snackbar";
// import IconButton from "@mui/material/IconButton";
// import CloseIcon from "@mui/icons-material/Close";
// import InputLabel from "@mui/material/InputLabel";
// import MenuItem from "@mui/material/MenuItem";
// import FormControl from "@mui/material/FormControl";
// import Select from "@mui/material/Select";
// import FormGroup from "@mui/material/FormGroup";
// import FormControlLabel from "@mui/material/FormControlLabel";
import PianoKeyboard from "./Components/PianoKeyboard";
import { React, useState, useEffect, useRef } from "react";
import PropTypes from "prop-types";
@ -122,262 +123,323 @@ function JuceSlider({ identifier, title }) {
);
}
function JuceCheckbox({ identifier }) {
JuceCheckbox.propTypes = {
identifier: PropTypes.string,
};
// function JuceCheckbox({ identifier }) {
// JuceCheckbox.propTypes = {
// identifier: PropTypes.string,
// };
const checkboxState = Juce.getToggleState(identifier);
// const checkboxState = Juce.getToggleState(identifier);
const [value, setValue] = useState(checkboxState.getValue());
const [properties, setProperties] = useState(checkboxState.properties);
// const [value, setValue] = useState(checkboxState.getValue());
// const [properties, setProperties] = useState(checkboxState.properties);
const handleChange = (event) => {
checkboxState.setValue(event.target.checked);
setValue(event.target.checked);
};
// const handleChange = (event) => {
// checkboxState.setValue(event.target.checked);
// setValue(event.target.checked);
// };
useEffect(() => {
const valueListenerId = checkboxState.valueChangedEvent.addListener(() => {
setValue(checkboxState.getValue());
});
const propertiesListenerId =
checkboxState.propertiesChangedEvent.addListener(() =>
setProperties(checkboxState.properties)
);
// useEffect(() => {
// const valueListenerId = checkboxState.valueChangedEvent.addListener(() => {
// setValue(checkboxState.getValue());
// });
// const propertiesListenerId =
// checkboxState.propertiesChangedEvent.addListener(() =>
// setProperties(checkboxState.properties)
// );
return function cleanup() {
checkboxState.valueChangedEvent.removeListener(valueListenerId);
checkboxState.propertiesChangedEvent.removeListener(propertiesListenerId);
};
});
// return function cleanup() {
// checkboxState.valueChangedEvent.removeListener(valueListenerId);
// checkboxState.propertiesChangedEvent.removeListener(propertiesListenerId);
// };
// });
const cb = <Checkbox checked={value} onChange={handleChange} />;
// const cb = <Checkbox checked={value} onChange={handleChange} />;
return (
<Box
{...{
[controlParameterIndexAnnotation]:
checkboxState.properties.parameterIndex,
}}
>
<FormGroup>
<FormControlLabel control={cb} label={properties.name} />
</FormGroup>
</Box>
);
}
// return (
// <Box
// {...{
// [controlParameterIndexAnnotation]:
// checkboxState.properties.parameterIndex,
// }}
// >
// <FormGroup>
// <FormControlLabel control={cb} label={properties.name} />
// </FormGroup>
// </Box>
// );
// }
function JuceComboBox({ identifier }) {
JuceComboBox.propTypes = {
identifier: PropTypes.string,
};
// function JuceComboBox({ identifier }) {
// JuceComboBox.propTypes = {
// identifier: PropTypes.string,
// };
const comboBoxState = Juce.getComboBoxState(identifier);
// const comboBoxState = Juce.getComboBoxState(identifier);
const [value, setValue] = useState(comboBoxState.getChoiceIndex());
const [properties, setProperties] = useState(comboBoxState.properties);
// const [value, setValue] = useState(comboBoxState.getChoiceIndex());
// const [properties, setProperties] = useState(comboBoxState.properties);
const handleChange = (event) => {
comboBoxState.setChoiceIndex(event.target.value);
setValue(event.target.value);
};
// const handleChange = (event) => {
// comboBoxState.setChoiceIndex(event.target.value);
// setValue(event.target.value);
// };
useEffect(() => {
const valueListenerId = comboBoxState.valueChangedEvent.addListener(() => {
setValue(comboBoxState.getChoiceIndex());
});
const propertiesListenerId =
comboBoxState.propertiesChangedEvent.addListener(() => {
setProperties(comboBoxState.properties);
});
// useEffect(() => {
// const valueListenerId = comboBoxState.valueChangedEvent.addListener(() => {
// setValue(comboBoxState.getChoiceIndex());
// });
// const propertiesListenerId =
// comboBoxState.propertiesChangedEvent.addListener(() => {
// setProperties(comboBoxState.properties);
// });
return function cleanup() {
comboBoxState.valueChangedEvent.removeListener(valueListenerId);
comboBoxState.propertiesChangedEvent.removeListener(propertiesListenerId);
};
});
// return function cleanup() {
// comboBoxState.valueChangedEvent.removeListener(valueListenerId);
// comboBoxState.propertiesChangedEvent.removeListener(propertiesListenerId);
// };
// });
return (
<Box
{...{
[controlParameterIndexAnnotation]:
comboBoxState.properties.parameterIndex,
}}
>
<FormControl fullWidth>
<InputLabel id={identifier}>{properties.name}</InputLabel>
<Select
labelId={identifier}
value={value}
label={properties.name}
onChange={handleChange}
>
{properties.choices.map((choice, i) => (
<MenuItem value={i} key={i}>
{choice}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
);
}
// return (
// <Box
// {...{
// [controlParameterIndexAnnotation]:
// comboBoxState.properties.parameterIndex,
// }}
// >
// <FormControl fullWidth>
// <InputLabel id={identifier}>{properties.name}</InputLabel>
// <Select
// labelId={identifier}
// value={value}
// label={properties.name}
// onChange={handleChange}
// >
// {properties.choices.map((choice, i) => (
// <MenuItem value={i} key={i}>
// {choice}
// </MenuItem>
// ))}
// </Select>
// </FormControl>
// </Box>
// );
// }
const sayHello = Juce.getNativeFunction("sayHello");
// const sayHello = Juce.getNativeFunction("sayHello");
const SpectrumDataReceiver_eventId = "spectrumData";
// const SpectrumDataReceiver_eventId = "spectrumData";
function interpolate(a, b, s) {
let result = new Array(a.length).fill(0);
// function interpolate(a, b, s) {
// let result = new Array(a.length).fill(0);
for (const [i, val] of a.entries()) result[i] += (1 - s) * val;
// for (const [i, val] of a.entries()) result[i] += (1 - s) * val;
for (const [i, val] of b.entries()) result[i] += s * val;
// for (const [i, val] of b.entries()) result[i] += s * val;
return result;
}
// return result;
// }
function mod(dividend, divisor) {
const quotient = Math.floor(dividend / divisor);
return dividend - divisor * quotient;
}
// function mod(dividend, divisor) {
// const quotient = Math.floor(dividend / divisor);
// return dividend - divisor * quotient;
// }
class SpectrumDataReceiver {
constructor(bufferLength) {
this.bufferLength = bufferLength;
this.buffer = new Array(this.bufferLength);
this.readIndex = 0;
this.writeIndex = 0;
this.lastTimeStampMs = 0;
this.timeResolutionMs = 0;
// class SpectrumDataReceiver {
// constructor(bufferLength) {
// this.bufferLength = bufferLength;
// this.buffer = new Array(this.bufferLength);
// this.readIndex = 0;
// this.writeIndex = 0;
// this.lastTimeStampMs = 0;
// this.timeResolutionMs = 0;
// let self = this;
// this.spectrumDataRegistrationId = window.__JUCE__.backend.addEventListener(
// SpectrumDataReceiver_eventId,
// () => {
// fetch(Juce.getBackendResourceAddress("spectrumData.json"))
// .then((response) => response.text())
// .then((text) => {
// const data = JSON.parse(text);
// if (self.timeResolutionMs == 0) {
// self.timeResolutionMs = data.timeResolutionMs;
// // We want to stay behind the write index by a full batch plus one
// // so that we can keep reading buffered frames until we receive the
// // new batch
// self.readIndex = -data.frames.length - 1;
// self.buffer.fill(new Array(data.frames[0].length).fill(0));
// }
// for (const f of data.frames)
// self.buffer[mod(self.writeIndex++, self.bufferLength)] = f;
// });
// }
// );
// }
// getBufferItem(index) {
// return this.buffer[mod(index, this.buffer.length)];
// }
// getLevels(timeStampMs) {
// if (this.timeResolutionMs == 0) return null;
// const previousTimeStampMs = this.lastTimeStampMs;
// this.lastTimeStampMs = timeStampMs;
// if (previousTimeStampMs == 0) return this.buffer[0];
// const timeAdvance =
// (timeStampMs - previousTimeStampMs) / this.timeResolutionMs;
// this.readIndex += timeAdvance;
// const integralPart = Math.floor(this.readIndex);
// const fractionalPart = this.readIndex - integralPart;
// return interpolate(
// this.getBufferItem(integralPart),
// this.getBufferItem(integralPart + 1),
// fractionalPart
// );
// }
// unregister() {
// window.__JUCE__.backend.removeEventListener(
// this.spectrumDataRegistrationId
// );
// }
// }
const MidNoteDataReceiver_eventId = "midNoteData";
class MidNoteDataReceiver {
constructor() {
this.notes = [];
let self = this;
this.spectrumDataRegistrationId = window.__JUCE__.backend.addEventListener(
SpectrumDataReceiver_eventId,
this.midNoteDataRegistrationId = window.__JUCE__.backend.addEventListener(
MidNoteDataReceiver_eventId,
() => {
fetch(Juce.getBackendResourceAddress("spectrumData.json"))
fetch(Juce.getBackendResourceAddress("midNoteData.json"))
.then((response) => response.text())
.then((text) => {
const data = JSON.parse(text);
if (self.timeResolutionMs == 0) {
self.timeResolutionMs = data.timeResolutionMs;
// We want to stay behind the write index by a full batch plus one
// so that we can keep reading buffered frames until we receive the
// new batch
self.readIndex = -data.frames.length - 1;
self.buffer.fill(new Array(data.frames[0].length).fill(0));
}
for (const f of data.frames)
self.buffer[mod(self.writeIndex++, self.bufferLength)] = f;
self.notes = data.notes;
});
}
);
}
getBufferItem(index) {
return this.buffer[mod(index, this.buffer.length)];
}
getLevels(timeStampMs) {
if (this.timeResolutionMs == 0) return null;
const previousTimeStampMs = this.lastTimeStampMs;
this.lastTimeStampMs = timeStampMs;
if (previousTimeStampMs == 0) return this.buffer[0];
const timeAdvance =
(timeStampMs - previousTimeStampMs) / this.timeResolutionMs;
this.readIndex += timeAdvance;
const integralPart = Math.floor(this.readIndex);
const fractionalPart = this.readIndex - integralPart;
return interpolate(
this.getBufferItem(integralPart),
this.getBufferItem(integralPart + 1),
fractionalPart
);
getNotes() {
return this.notes;
}
unregister() {
window.__JUCE__.backend.removeEventListener(
this.spectrumDataRegistrationId
);
window.__JUCE__.backend.removeEventListener(this.midNoteDataRegistrationId);
}
}
function FreqBandInfo() {
const canvasRef = useRef(null);
let dataReceiver = null;
let isActive = true;
function MidiNoteInfo() {
const [notes, setNotes] = useState([]);
const dataReceiverRef = useRef(null);
const isActiveRef = useRef(true);
// eslint-disable-next-line no-unused-vars
const render = (timeStampMs) => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
useEffect(() => {
dataReceiverRef.current = new MidNoteDataReceiver();
isActiveRef.current = true;
var grd = ctx.createLinearGradient(0, 0, 0, canvas.height);
grd.addColorStop(0, "#1976d2");
grd.addColorStop(1, "#dae9f8");
ctx.fillStyle = grd;
if (dataReceiver != null) {
const levels = dataReceiver.getLevels(timeStampMs);
if (levels != null) {
const numBars = levels.length;
const barWidth = canvas.width / numBars;
const barHeight = canvas.height;
for (const [i, l] of levels.entries()) {
ctx.fillRect(
i * barWidth,
barHeight - l * barHeight,
barWidth,
l * barHeight
);
}
function render() {
if (dataReceiverRef.current) {
setNotes(dataReceiverRef.current.getNotes());
}
if (isActiveRef.current) {
window.requestAnimationFrame(render);
}
}
if (isActive) window.requestAnimationFrame(render);
};
useEffect(() => {
dataReceiver = new SpectrumDataReceiver(10);
isActive = true;
window.requestAnimationFrame(render);
return function cleanup() {
isActive = false;
dataReceiver.unregister();
isActiveRef.current = false;
if (dataReceiverRef.current) {
dataReceiverRef.current.unregister();
}
};
});
const canvasStyle = {
marginLeft: "0",
marginRight: "0",
marginTop: "1em",
display: "block",
width: "94%",
bottom: "0",
position: "absolute",
};
}, []);
return (
<Box>
<canvas height={90} style={canvasStyle} ref={canvasRef}></canvas>
</Box>
<div>
<PianoKeyboard heldNotes={notes} />
</div>
);
}
// function FreqBandInfo() {
// const canvasRef = useRef(null);
// let dataReceiver = null;
// let isActive = true;
// // eslint-disable-next-line no-unused-vars
// const render = (timeStampMs) => {
// const canvas = canvasRef.current;
// const ctx = canvas.getContext("2d");
// ctx.clearRect(0, 0, canvas.width, canvas.height);
// var grd = ctx.createLinearGradient(0, 0, 0, canvas.height);
// grd.addColorStop(0, "#1976d2");
// grd.addColorStop(1, "#dae9f8");
// ctx.fillStyle = grd;
// if (dataReceiver != null) {
// const levels = dataReceiver.getLevels(timeStampMs);
// if (levels != null) {
// const numBars = levels.length;
// const barWidth = canvas.width / numBars;
// const barHeight = canvas.height;
// for (const [i, l] of levels.entries()) {
// ctx.fillRect(
// i * barWidth,
// barHeight - l * barHeight,
// barWidth,
// l * barHeight
// );
// }
// }
// }
// if (isActive) window.requestAnimationFrame(render);
// };
// useEffect(() => {
// dataReceiver = new SpectrumDataReceiver(10);
// isActive = true;
// window.requestAnimationFrame(render);
// return function cleanup() {
// isActive = false;
// dataReceiver.unregister();
// };
// });
// const canvasStyle = {
// marginLeft: "0",
// marginRight: "0",
// marginTop: "1em",
// display: "block",
// width: "94%",
// bottom: "0",
// position: "absolute",
// };
// return (
// <Box>
// <canvas height={90} style={canvasStyle} ref={canvasRef}></canvas>
// </Box>
// );
// }
function App() {
const controlParameterIndexUpdater = new Juce.ControlParameterIndexUpdater(
controlParameterIndexAnnotation
@ -387,33 +449,33 @@ function App() {
controlParameterIndexUpdater.handleMouseMove(event);
});
const [open, setOpen] = useState(false);
const [snackbarMessage, setMessage] = useState("No message received yet");
// const [open, setOpen] = useState(false);
// const [snackbarMessage, setMessage] = useState("No message received yet");
const openSnackbar = () => {
setOpen(true);
};
// const openSnackbar = () => {
// setOpen(true);
// };
const handleClose = (event, reason) => {
if (reason === "clickaway") {
return;
}
// const handleClose = (event, reason) => {
// if (reason === "clickaway") {
// return;
// }
setOpen(false);
};
// setOpen(false);
// };
const action = (
<>
<IconButton
size="small"
aria-label="close"
color="inherit"
onClick={handleClose}
>
<CloseIcon fontSize="small" />
</IconButton>
</>
);
// const action = (
// <>
// <IconButton
// size="small"
// aria-label="close"
// color="inherit"
// onClick={handleClose}
// >
// <CloseIcon fontSize="small" />
// </IconButton>
// </>
// );
return (
<div>
@ -422,7 +484,8 @@ function App() {
<JuceSlider identifier="autoTuneSpeedSlider" title="Auto Tune Speed" />
<JuceSlider identifier="portTimeSlider" title="Portamento Speed" />
</Container>
<CardActions style={{ justifyContent: "center" }}>
<MidiNoteInfo />
{/* <CardActions style={{ justifyContent: "center" }}>
<Button
variant="contained"
sx={{ marginTop: 2 }}
@ -454,15 +517,15 @@ function App() {
</CardActions>
<JuceCheckbox identifier="muteToggle" />
<br></br>
<JuceComboBox identifier="filterTypeCombo" />
<FreqBandInfo></FreqBandInfo>
<Snackbar
<JuceComboBox identifier="filterTypeCombo" /> */}
{/* <Snackbar
open={open}
autoHideDuration={6000}
onClose={handleClose}
message={snackbarMessage}
action={action}
/>
/> */}
</div>
);
}

View File

@ -0,0 +1,203 @@
/* eslint-disable react/prop-types */
import React /*, { useRef, useEffect, useState }*/ from "react";
const NOTE_NAMES = [
"C",
"C♯",
"D",
"D♯",
"E",
"F",
"F♯",
"G",
"G♯",
"A",
"A♯",
"B",
];
const WHITE_KEYS = [0, 2, 4, 5, 7, 9, 11];
// C2 = 36, C5 = 72
const LOWEST_MIDI = 36;
const HIGHEST_MIDI = 72;
function getNoteName(midi) {
const octave = Math.floor(midi / 12) - 1;
const note = NOTE_NAMES[midi % 12];
return `${note}${octave}`;
}
function isWhiteKey(midi) {
return WHITE_KEYS.includes(midi % 12);
}
export default function PianoKeyboard({ heldNotes }) {
const heldMap = {};
heldNotes.forEach((n) => (heldMap[n.midi] = n.voice));
const keys = [];
for (let midi = LOWEST_MIDI; midi <= HIGHEST_MIDI; midi++) {
const white = isWhiteKey(midi);
const held = heldMap[midi] !== undefined;
keys.push({
midi,
white,
held,
voice: heldMap[midi],
noteName: getNoteName(midi),
});
}
const whiteKeys = keys.filter((k) => k.white);
const blackKeys = keys.filter((k) => !k.white);
// For responsive black key positioning
const numWhite = whiteKeys.length;
// Map midi to white key index for black key positioning
const midiToWhiteIndex = {};
let whiteIdx = 0;
for (let midi = LOWEST_MIDI; midi <= HIGHEST_MIDI; midi++) {
if (isWhiteKey(midi)) {
midiToWhiteIndex[midi] = whiteIdx++;
}
}
// For each black key, find its position between white keys
function getBlackKeyPercent(midi) {
// Black keys are always after a white key except for the first key
// For example, C# is between C and D
// So, find the previous white key index, then add ~0.65 of a white key width
const prevWhite = midi - 1;
const idx = midiToWhiteIndex[prevWhite];
if (idx === undefined) return 0;
// Offset: (idx + 0.65) / numWhite * 100%
return ((idx + 0.65) / numWhite) * 100;
}
return (
<div
style={{
position: "relative",
height: 80,
userSelect: "none",
width: "100%",
minWidth: 200,
maxWidth: 900,
margin: "0 auto",
}}
>
{/* White keys */}
<div
style={{
display: "flex",
position: "relative",
zIndex: 1,
height: "100%",
}}
>
{whiteKeys.map((k) => (
<div
key={k.midi}
style={{
flex: "1 1 0",
height: "100%",
border: "1px solid #888",
background: k.held ? "#2a82caff" : "#fff",
position: "relative",
boxSizing: "border-box",
marginRight: -1,
display: "flex",
flexDirection: "column-reverse",
alignItems: "center",
justifyContent: "flex-start",
fontSize: 10,
fontFamily: "monospace",
overflow: "hidden",
}}
>
<div
style={{
display: "flex",
flexDirection: "column-reverse",
alignItems: "center",
}}
>
<span style={{ opacity: 0.5 }}>{k.noteName}</span>
{k.held && (
<span
style={{
color: "#fff",
fontWeight: "bold",
fontSize: 14,
lineHeight: "14px",
marginBottom: 2,
}}
>
{k.voice}
</span>
)}
</div>
</div>
))}
</div>
{/* Black keys */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
height: "62%",
width: "100%",
zIndex: 2,
pointerEvents: "none",
}}
>
{blackKeys.map((k) => (
<div
key={k.midi}
style={{
position: "absolute",
left: `${getBlackKeyPercent(k.midi)}%`,
width: `${(100 / numWhite) * 0.65}%`,
height: "100%",
background: k.held ? "#2a82caff" : "#222",
border: "1px solid #444",
borderRadius: 3,
display: "flex",
flexDirection: "column-reverse",
alignItems: "center",
justifyContent: "flex-start",
fontSize: 10,
fontFamily: "monospace",
boxSizing: "border-box",
transform: "translateX(-50%)",
}}
>
<div
style={{
display: "flex",
flexDirection: "column-reverse",
alignItems: "center",
}}
>
{k.held && (
<span
style={{
color: "#fff",
fontWeight: "bold",
fontSize: 14,
lineHeight: "14px",
marginBottom: 2,
}}
>
{k.voice}
</span>
)}
</div>
</div>
))}
</div>
</div>
);
}