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

View File

@ -67,14 +67,17 @@ static inline bool float_equal(float one, float two) {
return abs(one - two) < 1e-5f; return abs(one - two) < 1e-5f;
} }
void Shifter::Init() void Shifter::Init(float samplerate, int samplesPerBlock)
{ {
sample_rate_ = samplerate;
blocksize = samplesPerBlock;
out_midi_smoother.SetFrameTime((float)samplesPerBlock / samplerate);
volume = 1; volume = 1;
helm.setframesize(1024); helm.setframesize(1024);
helm.setoverlap(1); helm.setoverlap(1);
for (int i = 0; i < MAX_VOICES; ++i) for (int i = 0; i < MAX_VOICES; ++i)
{ {
voices[i].Init(48000); voices[i].Init(samplerate);
} }
for (int i = 0; i < BUFFER_SIZE; ++i) for (int i = 0; i < BUFFER_SIZE; ++i)
{ {
@ -148,13 +151,13 @@ void Shifter::DetectPitch(const float* const* in, float** out, size_t size)
// Smoothly filter in_period changes // Smoothly filter in_period changes
in_period = in_period * in_period_filter_amount + period * (1.0f - in_period_filter_amount); in_period = in_period * in_period_filter_amount + period * (1.0f - in_period_filter_amount);
} }
float in_freq = 48000 / in_period; float in_freq = sample_rate_ / in_period;
float midi = (12 * log2f(in_freq / 440) + 69.0f); float midi = (12 * log2f(in_freq / 440) + 69.0f);
//target_out_period = in_period * out_period_filter_amount + target_out_period * (1 - out_period_filter_amount); //target_out_period = in_period * out_period_filter_amount + target_out_period * (1 - out_period_filter_amount);
out_midi = out_midi_smoother.update(midi, (int)(midi+.5)); out_midi = out_midi_smoother.update(midi, (int)(midi+.5));
out_period = 48000.0f / mtof(out_midi); out_period = sample_rate_ / mtof(out_midi);
} }
void Shifter::SetRates() {} void Shifter::SetRates() {}
@ -275,8 +278,8 @@ void Shifter::GetSamples(float** output, const float* input, size_t size)
//add new samples if necessary //add new samples if necessary
for (int out_p = 0; out_p < MAX_VOICES; ++out_p) for (int out_p = 0; out_p < MAX_VOICES; ++out_p)
{ {
voices[out_p].Process();
if (!voices[out_p].IsActive()) continue; if (!voices[out_p].IsActive()) continue;
voices[out_p].Process();
if (voices[out_p].PeriodOverflow()) if (voices[out_p].PeriodOverflow())
{ {
float resampling_period = GetOutputEnvelopePeriod(out_p); float resampling_period = GetOutputEnvelopePeriod(out_p);
@ -328,12 +331,12 @@ void Shifter::GetSamples(float** output, const float* input, size_t size)
void Shifter::AddMidiNote(int note) { void Shifter::AddMidiNote(int note) {
for (int i = 0; i < MAX_VOICES; ++i) { for (int i = 0; i < MAX_VOICES; ++i) {
if (voices[i].IsActive() && voices[i].GetMidiNote() == note) { if (voices[i].onoff_ && voices[i].GetMidiNote() == note) {
return; voices[i].Release();
} }
} }
for (int i = 0; i < MAX_VOICES; ++i) { for (int i = 0; i < MAX_VOICES; ++i) {
if (!voices[i].IsActive()) { if (!voices[i].onoff_) {
voices[i].Trigger(note); voices[i].Trigger(note);
return; return;
} }
@ -343,9 +346,8 @@ void Shifter::AddMidiNote(int note) {
void Shifter::RemoveMidiNote(int note) { void Shifter::RemoveMidiNote(int note) {
for (int i = 0; i < MAX_VOICES; ++i) { for (int i = 0; i < MAX_VOICES; ++i) {
if (voices[i].IsActive() && voices[i].GetMidiNote() == note) { if (voices[i].GetMidiNote() == note) {
voices[i].Release(); voices[i].Release();
return;
} }
} }
} }

View File

@ -75,7 +75,7 @@ public:
class Shifter { class Shifter {
public: public:
void Init(); void Init(float samplerate, int samplesPerBlock);
void Process(const float* const* in, void Process(const float* const* in,
float** out, float** out,
size_t size); size_t size);
@ -91,6 +91,7 @@ public:
} }
float out_midi = 40; float out_midi = 40;
ShifterVoice voices[MAX_VOICES];
private: private:
void DetectPitch(const float* const* in, float** out, size_t size); void DetectPitch(const float* const* in, float** out, size_t size);
@ -140,11 +141,13 @@ private:
float out_period_filter_amount = 0.7f; // You can expose this as a parameter float out_period_filter_amount = 0.7f; // You can expose this as a parameter
ShifterVoice voices[MAX_VOICES];
float out_period = 0; //C3 float out_period = 0; //C3
float in_period = 366.936; float in_period = 366.936;
float out_period_counter = 0; float out_period_counter = 0;
float cos_lookup[8192]; float cos_lookup[8192];
float sample_rate_;
int blocksize;
}; };
#endif #endif

View File

@ -145,26 +145,26 @@ private:
int64 writeIx = 0; int64 writeIx = 0;
}; };
class SpectralBars //class SpectralBars
{ //{
public: //public:
//template <typename T> // //template <typename T>
void push(int data) // void push(int data)
{ // {
testQueue.push(data); // testQueue.push(data);
} // }
//
void compute(Span<int> output) { // void compute(Span<int> output) {
int index = 0; // int index = 0;
for (auto it = output.begin(); it != output.end(); ++it) { // for (auto it = output.begin(); it != output.end(); ++it) {
*it = testQueue.get(index++); // *it = testQueue.get(index++);
} // }
} // }
//
//
private: //private:
circ_queue<int, 256> testQueue; // circ_queue<int, 256> testQueue;
}; //};
//============================================================================== //==============================================================================
class WebViewPluginAudioProcessor : public AudioProcessor class WebViewPluginAudioProcessor : public AudioProcessor
@ -200,6 +200,7 @@ public:
//============================================================================== //==============================================================================
void getStateInformation(MemoryBlock& destData) override; void getStateInformation(MemoryBlock& destData) override;
void setStateInformation(const void* data, int sizeInBytes) override; void setStateInformation(const void* data, int sizeInBytes) override;
bool new_midi = false;
struct Parameters struct Parameters
{ {
@ -261,18 +262,20 @@ public:
Parameters parameters; Parameters parameters;
AudioProcessorValueTreeState state; AudioProcessorValueTreeState state;
SpinLock midiLock;
std::vector<int> spectrumData = [] { return std::vector<int>(256, 0.0f); }(); /*std::vector<int> spectrumData = [] { return std::vector<int>(256, 0.0f); }();
SpinLock spectrumDataLock; SpinLock spectrumDataLock;
SpectralBars spectralBars; SpectralBars spectralBars;*/
dsp::LadderFilter<float> filter; dsp::LadderFilter<float> filter;
Shifter shifter;
private: private:
//============================================================================== //==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WebViewPluginAudioProcessor) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WebViewPluginAudioProcessor)
Shifter shifter;
}; };
//============================================================================== //==============================================================================
@ -288,14 +291,14 @@ WebViewPluginAudioProcessor::WebViewPluginAudioProcessor(AudioProcessorValueTree
parameters(layout), parameters(layout),
state(*this, nullptr, "STATE", std::move(layout)) state(*this, nullptr, "STATE", std::move(layout))
{ {
shifter.Init(); shifter.Init(48000.0f, 48);
} }
//============================================================================== //==============================================================================
void WebViewPluginAudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock) void WebViewPluginAudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
{ {
const auto channels = std::max(getTotalNumInputChannels(), getTotalNumOutputChannels()); const auto channels = std::max(getTotalNumInputChannels(), getTotalNumOutputChannels());
shifter.Init(); shifter.Init((float)sampleRate, samplesPerBlock);
if (channels == 0) if (channels == 0)
return; return;
@ -335,8 +338,17 @@ void WebViewPluginAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer,
for (const auto metadata : midi) for (const auto metadata : midi)
{ {
const auto msg = metadata.getMessage(); const auto msg = metadata.getMessage();
if (msg.isNoteOn()) shifter.AddMidiNote(msg.getNoteNumber()); if (msg.isNoteOn()) {
else if (msg.isNoteOff()) shifter.RemoveMidiNote(msg.getNoteNumber()); shifter.AddMidiNote(msg.getNoteNumber());
new_midi = true;
//editor.webComponent.emitEventIfBrowserIsVisible("midNoteData", var{});
}
else if (msg.isNoteOff()) {
shifter.RemoveMidiNote(msg.getNoteNumber());
new_midi = true;
//editor.webComponent.emitEventIfBrowserIsVisible("midNoteData", var{});
}
} }
@ -344,12 +356,11 @@ void WebViewPluginAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer,
//DBG(shifter.out_midi[MAX_VOICES]); //DBG(shifter.out_midi[MAX_VOICES]);
//push midi note //push midi note
//spectralBars.push(shifter.out_midi[MAX_VOICES]); //spectralBars.push(shifter.out_midi[MAX_VOICES]);
const SpinLock::ScopedTryLockType lock(spectrumDataLock); const SpinLock::ScopedTryLockType lock(midiLock);
if (!lock.isLocked()) if (!lock.isLocked())
return; return;
spectralBars.compute({ spectrumData.data(), spectrumData.size() });
} }
@ -446,9 +457,9 @@ public:
{ {
static constexpr size_t numFramesBuffered = 5; static constexpr size_t numFramesBuffered = 5;
SpinLock::ScopedLockType lock{ processorRef.spectrumDataLock }; SpinLock::ScopedLockType lock{ processorRef.midiLock };
Array<var> frame; /*Array<var> frame;
for (size_t i = 1; i < processorRef.spectrumData.size(); ++i) for (size_t i = 1; i < processorRef.spectrumData.size(); ++i)
frame.add(processorRef.spectrumData[i]); frame.add(processorRef.spectrumData[i]);
@ -458,14 +469,18 @@ public:
spectrumDataFrames.push_back(std::move(frame)); spectrumDataFrames.push_back(std::move(frame));
while (spectrumDataFrames.size() > numFramesBuffered) while (spectrumDataFrames.size() > numFramesBuffered)
spectrumDataFrames.pop_front(); spectrumDataFrames.pop_front();*/
static int64 callbackCounter = 0; static int64 callbackCounter = 0;
/*if ( spectrumDataFrames.size() == numFramesBuffered /*if ( spectrumDataFrames.size() == numFramesBuffered
&& callbackCounter++ % (int64) numFramesBuffered) && callbackCounter++ % (int64) numFramesBuffered)
{*/ {*/
webComponent.emitEventIfBrowserIsVisible("spectrumData", var{}); if (processorRef.new_midi) {
processorRef.new_midi = false;
webComponent.emitEventIfBrowserIsVisible("midNoteData", var{});
}
//} //}
} }
@ -595,16 +610,22 @@ std::optional<WebBrowserComponent::Resource> WebViewPluginAudioProcessorEditor::
return WebBrowserComponent::Resource{ streamToVector(stream), String { "text/html" } }; return WebBrowserComponent::Resource{ streamToVector(stream), String { "text/html" } };
} }
if (urlToRetrive == "spectrumData.json") if (urlToRetrive == "midNoteData.json")
{ {
Array<var> frames; juce::Array<var> notes;
int voice_num = 0;
for (const auto& frame : spectrumDataFrames) for (auto& voice : processorRef.shifter.voices) {
frames.add(frame); if (voice.onoff_) {
auto obj = new DynamicObject();
obj->setProperty("voice", voice_num);
obj->setProperty("midi", voice.GetMidiNote());
notes.add(var(obj));
}
voice_num++;
}
DynamicObject::Ptr d(new DynamicObject()); DynamicObject::Ptr d(new DynamicObject());
d->setProperty("timeResolutionMs", getTimerInterval()); d->setProperty("notes", notes);
d->setProperty("frames", std::move(frames));
const auto s = JSON::toString(d.get()); const auto s = JSON::toString(d.get());
MemoryInputStream stream{ s.getCharPointer(), s.getNumBytesAsUTF8(), false }; MemoryInputStream stream{ s.getCharPointer(), s.getNumBytesAsUTF8(), false };
@ -652,7 +673,7 @@ WebViewPluginAudioProcessorEditor::WebViewPluginAudioProcessorEditor(WebViewPlug
setSize(500, 500); setSize(500, 500);
startTimerHz(20); startTimerHz(60);
} }
//============================================================================== //==============================================================================
@ -675,5 +696,7 @@ public:
} }
bool hasEditor() const override { return true; } bool hasEditor() const override { return true; }
AudioProcessorEditor* createEditor() override { return new WebViewPluginAudioProcessorEditor(*this); } AudioProcessorEditor* createEditor() override {
return new WebViewPluginAudioProcessorEditor(*this);
}
}; };

View File

@ -18,18 +18,23 @@ static inline float mtof(float m)
void ShifterVoice::Init(float sample_rate) { void ShifterVoice::Init(float sample_rate) {
portamento_.Init(sample_rate, 0.05f); //default portamento time sample_rate_ = sample_rate;
amplitude_envelope_.Init(sample_rate); portamento_.Init(sample_rate_, 0.05f); //default portamento time
amplitude_envelope_.Init(sample_rate_);
amplitude_envelope_.SetAttackTime(0.2f); amplitude_envelope_.SetAttackTime(0.2f);
amplitude_envelope_.SetDecayTime(0.1f); amplitude_envelope_.SetDecayTime(0.1f);
amplitude_envelope_.SetReleaseTime(.1f); amplitude_envelope_.SetReleaseTime(.05f);
onoff_ = false; onoff_ = false;
overflow_ = false; overflow_ = false;
current_midi = 60; current_midi = 60;
current_period_ = 48000.0f / mtof((float)current_midi); current_period_ = sample_rate_ / mtof((float)current_midi);
current_amplitude = 0.0f; current_amplitude = 0.0f;
period_counter = 0.0f; period_counter = 0.0f;
panning = 0.5f; panning = 0.5f;
//on reset, make sure envelope is not running
while (amplitude_envelope_.IsRunning()) {
amplitude_envelope_.Process(false);
}
} }
@ -49,7 +54,7 @@ void ShifterVoice::Release() {
void ShifterVoice::Process() { void ShifterVoice::Process() {
current_amplitude = amplitude_envelope_.Process(onoff_); current_amplitude = amplitude_envelope_.Process(onoff_);
current_period_ = 48000.0f / mtof(portamento_.Process((float)current_midi)); current_period_ = sample_rate_ / mtof(portamento_.Process((float)current_midi));
period_counter++; period_counter++;
overflow_ = period_counter >= current_period_; overflow_ = period_counter >= current_period_;
if (overflow_) { if (overflow_) {

View File

@ -32,12 +32,14 @@ public:
void SetAdsrTimes(float attack, float decay, float release); void SetAdsrTimes(float attack, float decay, float release);
float GetPanning(int channel) const; float GetPanning(int channel) const;
int GetMidiNote() const { return current_midi; } int GetMidiNote() const { return current_midi; }
bool onoff_;
private: private:
Port portamento_; Port portamento_;
Adsr amplitude_envelope_; Adsr amplitude_envelope_;
bool onoff_;
bool overflow_; bool overflow_;
float sample_rate_;
int current_midi; int current_midi;
float current_period_; float current_period_;
float current_amplitude; float current_amplitude;