midi notes avaiable on ui
This commit is contained in:
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
203
Assets/web/src/Components/PianoKeyboard.js
Normal file
203
Assets/web/src/Components/PianoKeyboard.js
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user