diff --git a/Assets/web/package-lock.json b/Assets/web/package-lock.json
index 80c80a3..652fed5 100644
--- a/Assets/web/package-lock.json
+++ b/Assets/web/package-lock.json
@@ -26,7 +26,8 @@
"devDependencies": {
"eslint": "^8.43.0",
"eslint-plugin-react": "^7.32.2",
- "npm-build-zip": "^1.0.4"
+ "npm-build-zip": "^1.0.4",
+ "prettier": "^3.6.2"
}
},
"../../../../../Downloads/JUCE/modules/juce_gui_extra/native/javascript": {
@@ -15391,6 +15392,22 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prettier": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
+ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
diff --git a/Assets/web/package.json b/Assets/web/package.json
index e72ba9d..23dd3e7 100644
--- a/Assets/web/package.json
+++ b/Assets/web/package.json
@@ -46,6 +46,7 @@
"devDependencies": {
"eslint": "^8.43.0",
"eslint-plugin-react": "^7.32.2",
- "npm-build-zip": "^1.0.4"
+ "npm-build-zip": "^1.0.4",
+ "prettier": "^3.6.2"
}
}
diff --git a/Assets/web/public/index.html b/Assets/web/public/index.html
index 8747593..04d9cf9 100644
--- a/Assets/web/public/index.html
+++ b/Assets/web/public/index.html
@@ -1,4 +1,4 @@
-
+
@@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
- Ladder Filter
+ Harmonizer
diff --git a/Assets/web/public/manifest.json b/Assets/web/public/manifest.json
index 38a49bc..0fe5c21 100644
--- a/Assets/web/public/manifest.json
+++ b/Assets/web/public/manifest.json
@@ -1,6 +1,6 @@
{
- "short_name": "Ladder Filter",
- "name": "Ladder Filter",
+ "short_name": "Harmonizer",
+ "name": "Harmonizer",
"icons": [
{
"src": "favicon.ico",
diff --git a/Assets/web/src/App.js b/Assets/web/src/App.js
index 8de0d42..61da4db 100644
--- a/Assets/web/src/App.js
+++ b/Assets/web/src/App.js
@@ -26,419 +26,15 @@ import "@fontsource/roboto/400.css";
import "@fontsource/roboto/500.css";
import "@fontsource/roboto/700.css";
-import Box from "@mui/material/Container";
-// 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 PianoKeyboard from "./Components/PianoKeyboard";
-
-import { React, useState, useEffect, useRef } from "react";
-import PropTypes from "prop-types";
-
import * as Juce from "juce-framework-frontend";
+import JuceSlider from "./Components/JuceSlider.js";
+import MidiNoteInfo from "./Components/MidiNoteInfo.js";
+import { controlParameterIndexAnnotation } from "./types/JuceTypes.js";
+
+import { React } from "react";
import "./App.css";
-// import { KnobPercentage } from "./Components/KnobPercentage";
-
-// Custom attributes in React must be in all lower case
-const controlParameterIndexAnnotation = "controlparameterindex";
-
-function JuceSlider({ identifier, title }) {
- JuceSlider.propTypes = {
- identifier: PropTypes.string,
- title: PropTypes.string,
- };
-
- const sliderState = Juce.getSliderState(identifier);
-
- const [value, setValue] = useState(sliderState.getNormalisedValue());
- const [properties, setProperties] = useState(sliderState.properties);
-
- const handleChange = (event, newValue) => {
- sliderState.setNormalisedValue(newValue);
- setValue(newValue);
- };
-
- const mouseDown = () => {
- sliderState.sliderDragStarted();
- };
-
- const changeCommitted = (event, newValue) => {
- sliderState.setNormalisedValue(newValue);
- sliderState.sliderDragEnded();
- };
-
- useEffect(() => {
- const valueListenerId = sliderState.valueChangedEvent.addListener(() => {
- setValue(sliderState.getNormalisedValue());
- });
- const propertiesListenerId = sliderState.propertiesChangedEvent.addListener(
- () => setProperties(sliderState.properties)
- );
-
- return function cleanup() {
- sliderState.valueChangedEvent.removeListener(valueListenerId);
- sliderState.propertiesChangedEvent.removeListener(propertiesListenerId);
- };
- });
-
- function calculateValue() {
- return sliderState.getScaledValue();
- }
-
- return (
-
-
- {properties.name}: {sliderState.getScaledValue()} {properties.label}
-
-
-
- );
-}
-
-// function JuceCheckbox({ identifier }) {
-// JuceCheckbox.propTypes = {
-// identifier: PropTypes.string,
-// };
-
-// const checkboxState = Juce.getToggleState(identifier);
-
-// const [value, setValue] = useState(checkboxState.getValue());
-// const [properties, setProperties] = useState(checkboxState.properties);
-
-// 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)
-// );
-
-// return function cleanup() {
-// checkboxState.valueChangedEvent.removeListener(valueListenerId);
-// checkboxState.propertiesChangedEvent.removeListener(propertiesListenerId);
-// };
-// });
-
-// const cb = ;
-
-// return (
-//
-//
-//
-//
-//
-// );
-// }
-
-// function JuceComboBox({ identifier }) {
-// JuceComboBox.propTypes = {
-// identifier: PropTypes.string,
-// };
-
-// const comboBoxState = Juce.getComboBoxState(identifier);
-
-// const [value, setValue] = useState(comboBoxState.getChoiceIndex());
-// const [properties, setProperties] = useState(comboBoxState.properties);
-
-// 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);
-// });
-
-// return function cleanup() {
-// comboBoxState.valueChangedEvent.removeListener(valueListenerId);
-// comboBoxState.propertiesChangedEvent.removeListener(propertiesListenerId);
-// };
-// });
-
-// return (
-//
-//
-// {properties.name}
-//
-//
-//
-// );
-// }
-
-// const sayHello = Juce.getNativeFunction("sayHello");
-
-// const SpectrumDataReceiver_eventId = "spectrumData";
-
-// 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 b.entries()) result[i] += s * val;
-
-// return result;
-// }
-
-// 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;
-
-// 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.midNoteDataRegistrationId = window.__JUCE__.backend.addEventListener(
- MidNoteDataReceiver_eventId,
- () => {
- fetch(Juce.getBackendResourceAddress("midNoteData.json"))
- .then((response) => response.text())
- .then((text) => {
- const data = JSON.parse(text);
- self.notes = data.notes;
- });
- }
- );
- }
-
- getNotes() {
- return this.notes;
- }
-
- unregister() {
- window.__JUCE__.backend.removeEventListener(this.midNoteDataRegistrationId);
- }
-}
-
-function MidiNoteInfo() {
- const [notes, setNotes] = useState([]);
- const dataReceiverRef = useRef(null);
- const isActiveRef = useRef(true);
-
- useEffect(() => {
- dataReceiverRef.current = new MidNoteDataReceiver();
- isActiveRef.current = true;
-
- function render() {
- if (dataReceiverRef.current) {
- setNotes(dataReceiverRef.current.getNotes());
- }
- if (isActiveRef.current) {
- window.requestAnimationFrame(render);
- }
- }
-
- window.requestAnimationFrame(render);
-
- return function cleanup() {
- isActiveRef.current = false;
- if (dataReceiverRef.current) {
- dataReceiverRef.current.unregister();
- }
- };
- }, []);
-
- return (
-
- );
-}
-
-// 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 (
-//
-//
-//
-// );
-// }
function App() {
const controlParameterIndexUpdater = new Juce.ControlParameterIndexUpdater(
@@ -449,83 +45,15 @@ function App() {
controlParameterIndexUpdater.handleMouseMove(event);
});
- // const [open, setOpen] = useState(false);
- // const [snackbarMessage, setMessage] = useState("No message received yet");
-
- // const openSnackbar = () => {
- // setOpen(true);
- // };
-
- // const handleClose = (event, reason) => {
- // if (reason === "clickaway") {
- // return;
- // }
-
- // setOpen(false);
- // };
-
- // const action = (
- // <>
- //
- //
- //
- // >
- // );
-
return (
+
- {/*
-
-
-
-
-
-
-
- */}
-
- {/* */}
);
}
diff --git a/Assets/web/src/Components/CenterGrowSlider.js b/Assets/web/src/Components/CenterGrowSlider.js
new file mode 100644
index 0000000..85bab76
--- /dev/null
+++ b/Assets/web/src/Components/CenterGrowSlider.js
@@ -0,0 +1,61 @@
+/* eslint-disable react/prop-types */
+import React from "react";
+
+export default function CenterGrowSlider({
+ value,
+ min = -50,
+ max = 50,
+ positiveColor = "#4caf50",
+ negativeColor = "#f44336",
+ backgroundColor = "rgba(150,150,150,0.3)",
+ height = 8,
+}) {
+ // Clamp the value
+ const clamped = Math.max(min, Math.min(max, value));
+ const range = max - min;
+ const halfRange = range / 2;
+ const magnitude = Math.abs(clamped) / halfRange; // 0..1
+
+ // Calculate widths (each bar maxes out at 50% width)
+ const positiveWidth = clamped > 0 ? magnitude * 50 : 0;
+ const negativeWidth = clamped < 0 ? magnitude * 50 : 0;
+
+ const baseStyle = {
+ position: "absolute",
+ top: "50%",
+ height: `${height}px`,
+ transform: "translateY(-50%)",
+ };
+
+ return (
+
+ {/* Negative (left) bar */}
+
+
+ {/* Positive (right) bar */}
+
+
+ );
+}
diff --git a/Assets/web/src/Components/JuceSlider.js b/Assets/web/src/Components/JuceSlider.js
new file mode 100644
index 0000000..9c0ef72
--- /dev/null
+++ b/Assets/web/src/Components/JuceSlider.js
@@ -0,0 +1,76 @@
+import PropTypes from "prop-types";
+import Box from "@mui/material/Container";
+import Typography from "@mui/material/Typography";
+
+import Slider from "@mui/material/Slider";
+import * as Juce from "juce-framework-frontend";
+import { React, useState, useEffect } from "react";
+import { controlParameterIndexAnnotation } from "../types/JuceTypes.js";
+
+export default function JuceSlider({ identifier, title }) {
+ JuceSlider.propTypes = {
+ identifier: PropTypes.string,
+ title: PropTypes.string,
+ };
+
+ const sliderState = Juce.getSliderState(identifier);
+
+ const [value, setValue] = useState(sliderState.getNormalisedValue());
+ const [properties, setProperties] = useState(sliderState.properties);
+
+ const handleChange = (event, newValue) => {
+ sliderState.setNormalisedValue(newValue);
+ setValue(newValue);
+ };
+
+ const mouseDown = () => {
+ sliderState.sliderDragStarted();
+ };
+
+ const changeCommitted = (event, newValue) => {
+ sliderState.setNormalisedValue(newValue);
+ sliderState.sliderDragEnded();
+ };
+
+ useEffect(() => {
+ const valueListenerId = sliderState.valueChangedEvent.addListener(() => {
+ setValue(sliderState.getNormalisedValue());
+ });
+ const propertiesListenerId = sliderState.propertiesChangedEvent.addListener(
+ () => setProperties(sliderState.properties)
+ );
+
+ return function cleanup() {
+ sliderState.valueChangedEvent.removeListener(valueListenerId);
+ sliderState.propertiesChangedEvent.removeListener(propertiesListenerId);
+ };
+ });
+
+ function calculateValue() {
+ return sliderState.getScaledValue();
+ }
+
+ return (
+
+
+ {properties.name}: {sliderState.getScaledValue()} {properties.label}
+
+
+
+ );
+}
diff --git a/Assets/web/src/Components/KnobBase.js b/Assets/web/src/Components/KnobBase.js
deleted file mode 100644
index 333579b..0000000
--- a/Assets/web/src/Components/KnobBase.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import clsx from "clsx";
-import { useId, useState, React } from "react";
-import {
- KnobHeadless,
- KnobHeadlessLabel,
- KnobHeadlessOutput,
- useKnobKeyboardControls,
-} from "react-knob-headless";
-import { mapFrom01Linear, mapTo01Linear } from "@dsp-ts/math";
-import { KnobBaseThumb } from "./KnobBaseThumb";
-
-export function KnobBase({
- theme,
- label,
- valueDefault,
- valueMin,
- valueMax,
- valueRawRoundFn,
- valueRawDisplayFn,
- axis,
- stepFn,
- stepLargerFn,
- mapTo01 = mapTo01Linear,
- mapFrom01 = mapFrom01Linear,
-}) {
- KnobBase.propTypes = {
- theme: KnobBase.string,
- label: KnobBase.string,
- valueDefault: KnobBase.number,
- valueMin: KnobBase.number,
- valueMax: KnobBase.number,
- valueRawRoundFn: KnobBase.func,
- valueRawDisplayFn: KnobBase.func,
- axis: KnobBase.string,
- stepFn: KnobBase.func,
- stepLargerFn: KnobBase.func,
- mapTo01: KnobBase.func,
- mapFrom01: KnobBase.func,
- };
- const knobId = useId();
- const labelId = useId();
- const [valueRaw, setValueRaw] = useState(valueDefault);
- const value01 = mapTo01(valueRaw, valueMin, valueMax);
- const step = stepFn(valueRaw);
- const stepLarger = stepLargerFn(valueRaw);
- const dragSensitivity = 0.006;
-
- const keyboardControlHandlers = useKnobKeyboardControls({
- valueRaw,
- valueMin,
- valueMax,
- step,
- stepLarger,
- onValueRawChange: setValueRaw,
- });
-
- return (
-
- {label}
-
-
-
-
- {valueRawDisplayFn(valueRaw)}
-
-
- );
-}
diff --git a/Assets/web/src/Components/KnobBaseThumb.js b/Assets/web/src/Components/KnobBaseThumb.js
deleted file mode 100644
index cbb622c..0000000
--- a/Assets/web/src/Components/KnobBaseThumb.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import clsx from "clsx";
-import { mapFrom01Linear } from "@dsp-ts/math";
-import { React } from "react";
-
-export function KnobBaseThumb({ theme, value01 }) {
- KnobBaseThumb.propTypes = {
- theme: KnobBaseThumb.string,
- value01: KnobBaseThumb.number,
- };
- const angleMin = -145;
- const angleMax = 145;
- const angle = mapFrom01Linear(value01, angleMin, angleMax);
- return (
-
- );
-}
diff --git a/Assets/web/src/Components/KnobPercentage.js b/Assets/web/src/Components/KnobPercentage.js
deleted file mode 100644
index 1e7ab66..0000000
--- a/Assets/web/src/Components/KnobPercentage.js
+++ /dev/null
@@ -1,33 +0,0 @@
-"use client";
-import { KnobBase } from "./KnobBase";
-import { React } from "react";
-
-export function KnobPercentage({ theme, label, axis }) {
- KnobPercentage.propTypes = {
- theme: KnobPercentage.string,
- label: KnobPercentage.string,
- axis: KnobPercentage.string,
- };
- return (
-
- );
-}
-
-const valueMin = 0;
-const valueMax = 100;
-const valueDefault = 50;
-const stepFn = (valueRaw) => 1;
-const stepLargerFn = (valueRaw) => 10;
-const valueRawRoundFn = Math.round;
-const valueRawDisplayFn = (valueRaw) => `${valueRawRoundFn(valueRaw)}%`;
diff --git a/Assets/web/src/Components/MidiNoteInfo.js b/Assets/web/src/Components/MidiNoteInfo.js
new file mode 100644
index 0000000..1bd68af
--- /dev/null
+++ b/Assets/web/src/Components/MidiNoteInfo.js
@@ -0,0 +1,69 @@
+import React, { useState, useEffect, useRef } from "react";
+import PianoKeyboard from "./PianoKeyboard";
+import MidNoteDataReceiver from "../DataRecievers/MidiNoteDataReceiver.js";
+import CenterGrowSlider from "./CenterGrowSlider.js";
+// import { Slider } from "@mui/material";
+// import { styled } from "@mui/material/styles";
+
+// eslint-disable-next-line react/prop-types
+
+export default function MidiNoteInfo() {
+ const [notes, setNotes] = useState([]);
+ const [inputCents, setInputCents] = useState(0);
+ const [outputCents, setOutputCents] = useState(0);
+ const [autotuneNote, setAutotuneNote] = useState(0);
+ const dataReceiverRef = useRef(null);
+ const isActiveRef = useRef(true);
+
+ function getCharfromNoteIndex(index) {
+ const NOTE_NAMES = [
+ "C",
+ "C♯",
+ "D",
+ "D♯",
+ "E",
+ "F",
+ "F♯",
+ "G",
+ "G♯",
+ "A",
+ "A♯",
+ "B",
+ ];
+ if (typeof index !== "number" || isNaN(index)) return "";
+ return NOTE_NAMES[index % NOTE_NAMES.length];
+ }
+
+ useEffect(() => {
+ dataReceiverRef.current = new MidNoteDataReceiver();
+ isActiveRef.current = true;
+
+ function render() {
+ if (!isActiveRef.current) return;
+ if (dataReceiverRef.current) {
+ setNotes(dataReceiverRef.current.getNotes());
+ setInputCents(dataReceiverRef.current.getInputCents());
+ setOutputCents(dataReceiverRef.current.getOutputCents());
+ setAutotuneNote(dataReceiverRef.current.getAutotuneNote());
+ }
+ window.requestAnimationFrame(render);
+ }
+
+ window.requestAnimationFrame(render);
+ return function cleanup() {
+ isActiveRef.current = false;
+ if (dataReceiverRef.current) dataReceiverRef.current.unregister();
+ };
+ }, []);
+
+ return (
+
+
+
Autotune Note: {getCharfromNoteIndex(autotuneNote)}
+
+
+
+
+
+ );
+}
diff --git a/Assets/web/src/DataRecievers/MidiNoteDataReceiver.js b/Assets/web/src/DataRecievers/MidiNoteDataReceiver.js
new file mode 100644
index 0000000..0a1b741
--- /dev/null
+++ b/Assets/web/src/DataRecievers/MidiNoteDataReceiver.js
@@ -0,0 +1,54 @@
+// import * as Juce from "juce-framework-frontend";
+// import reportWebVitals from "../reportWebVitals";
+
+const MidNoteDataReceiver_eventId = "midNoteData";
+export default class MidNoteDataReceiver {
+ constructor() {
+ this.notes = [];
+ this.input_pitch = 0;
+ this.output_pitch = 0;
+ let self = this;
+ this.midNoteDataRegistrationId = window.__JUCE__.backend.addEventListener(
+ MidNoteDataReceiver_eventId,
+ (event) => {
+ self.notes = event.notes;
+ self.input_pitch = event.input_pitch;
+ self.output_pitch = event.output_pitch;
+ console.log("in: " + self.input_pitch);
+ console.log("out: " + self.output_pitch);
+ // reportWebVitals(console.log(event));
+ // fetch(Juce.getBackendResourceAddress("midNoteData.json"))
+ // .then((response) => response.text())
+ // .then((text) => {
+ // const data = JSON.parse(text);
+ // self.notes = data.notes;
+ // });
+ }
+ );
+ }
+
+ getNotes() {
+ return this.notes;
+ }
+
+ frequencytoMidi(frequency) {
+ return 69 + 12 * Math.log2(frequency / 440);
+ }
+
+ getAutotuneNote() {
+ const midi = this.frequencytoMidi(this.output_pitch);
+ return Math.round(midi % 12);
+ }
+ getInputCents() {
+ const midi = this.frequencytoMidi(this.input_pitch);
+ return Math.round((midi - Math.round(midi)) * 100);
+ }
+ getOutputCents() {
+ const midi = this.frequencytoMidi(this.output_pitch);
+ return Math.round((midi - Math.round(midi)) * 100);
+ }
+
+ unregister() {
+ window.__JUCE__.backend.removeEventListener(this.midNoteDataRegistrationId);
+ }
+}
diff --git a/Assets/web/src/index.js b/Assets/web/src/index.js
index b927142..9959f71 100644
--- a/Assets/web/src/index.js
+++ b/Assets/web/src/index.js
@@ -25,7 +25,7 @@ import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
-import reportWebVitals from "./reportWebVitals";
+import reportWebVitals from "./reportWebVitals.js";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
diff --git a/Assets/web/src/reportWebVitals.js b/Assets/web/src/reportWebVitals.js
index 9228a5a..ef6a7a6 100644
--- a/Assets/web/src/reportWebVitals.js
+++ b/Assets/web/src/reportWebVitals.js
@@ -21,9 +21,9 @@
==============================================================================
*/
-const reportWebVitals = onPerfEntry => {
+const reportWebVitals = (onPerfEntry) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
- import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
+ import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
diff --git a/Assets/web/src/types/JuceTypes.js b/Assets/web/src/types/JuceTypes.js
new file mode 100644
index 0000000..623c54e
--- /dev/null
+++ b/Assets/web/src/types/JuceTypes.js
@@ -0,0 +1 @@
+export const controlParameterIndexAnnotation = "controlparameterindex";
diff --git a/Source/Shifter.cpp b/Source/Shifter.cpp
index 562bd0d..f8139c9 100644
--- a/Source/Shifter.cpp
+++ b/Source/Shifter.cpp
@@ -7,57 +7,14 @@
#endif
-class MidiPitchSmoother {
-public:
-
- MidiPitchSmoother() {
- tau = .1;
- dt = 2048.0f / 48000.0f;
- PcorrPrev = 60.0f;
- PtargPrev = 60;
- }
- void SetFrameTime(float frame_time) {
- dt = frame_time;
- }
-
- void SetTimeConstant(float t) {
- tau = t;
- }
- float update(float Pdet, int Ptarget) {
- // Detect large jump (new note)
- float diff = std::fabs(Pdet - PcorrPrev);
- if (Ptarget != PtargPrev) {
- // Immediately reset to new note
- PcorrPrev = Pdet;
- PtargPrev = Ptarget;
- return PcorrPrev;
- }
-
- // Compute smoothing coefficient
- float alpha = 1.0 - std::exp(-dt / tau);
-
- // Smooth within same note
- float PcorrNew = PcorrPrev + alpha * (Ptarget - PcorrPrev);
-
- PcorrPrev = PcorrNew;
- return PcorrNew;
- }
-
-
-private:
-
- float tau; // Time constant (s)
- float dt; // Frame time (s)
- float PcorrPrev; // Previous corrected pitch (MIDI)
- int PtargPrev; // Previous corrected pitch (MIDI)
-};
-
-MidiPitchSmoother out_midi_smoother;
-
void Shifter::SetAutoTuneSpeed(float val) {
out_midi_smoother.SetTimeConstant(val);
}
+void Shifter::SetAutoTuneDepth(float val) {
+ out_midi_smoother.SetDepth(val);
+}
+
static inline float mtof(float m)
{
return powf(2, (m - 69.0f) / 12.0f) * 440.0f;
@@ -145,7 +102,7 @@ void Shifter::DetectPitch(const float* const* in, float** out, size_t size)
//DBG("frequency: " << 48000 / period << " fidel: " << fidel);
// Adjustable filter amount (0.0f = no filtering, 1.0f = max filtering)
- static float in_period_filter_amount = 0.0f; // You can expose this as a parameter
+ static float in_period_filter_amount = 0.5f; // You can expose this as a parameter
if (fidel > 0.95) {
// Smoothly filter in_period changes
diff --git a/Source/Shifter.h b/Source/Shifter.h
index 9af3288..a2eb829 100644
--- a/Source/Shifter.h
+++ b/Source/Shifter.h
@@ -73,6 +73,70 @@ public:
T* tail;
};
+
+
+class MidiPitchSmoother {
+public:
+
+ MidiPitchSmoother() {
+ tau = .1;
+ dt = 2048.0f / 48000.0f;
+ PcorrPrev = 60.0f;
+ PtargPrev = 60;
+ depth = 1.0f;
+ }
+ void SetFrameTime(float frame_time) {
+ dt = frame_time;
+ }
+ void SetDepth(float d) {
+ // clamp to [0,1]
+ if (d < 0.0f) d = 0.0f;
+ if (d > 1.0f) d = 1.0f;
+ depth = d;
+ }
+
+ void SetTimeConstant(float t) {
+ tau = t;
+ }
+ float update(float Pdet, int Ptarget) {
+ // Detect large jump (new note)
+ float diff = Pdet - (int)Pdet;
+
+ if (Ptarget != PtargPrev) {
+ // Immediately reset to new note
+ PcorrPrev = diff;
+ PtargPrev = Ptarget;
+ inputPrev = Pdet;
+ return Ptarget+ PcorrPrev;
+ }
+ PtargPrev = Ptarget;
+ diff = Pdet - inputPrev;
+
+ // Compute smoothing coefficient
+ float alpha = 1.0 - std::exp(-dt / tau);
+
+ // Compute smoothed pitch toward target
+ float PcorrFull = PcorrPrev + alpha * (0 - PcorrPrev) * depth + (1 - depth) * diff;
+
+ // Apply depth: scale the correction amount
+ inputPrev = Pdet;
+ PcorrPrev = PcorrFull;
+ return Ptarget + PcorrFull;
+ }
+
+
+private:
+
+ float tau; // Time constant (s)
+ float dt; // Frame time (s)
+ float PcorrPrev; // Previous corrected pitch (MIDI)
+ int PtargPrev; // Previous corrected pitch (MIDI)
+ float depth; // Amount of correction applied [0..1]
+ float inputPrev;
+};
+
+
+
class Shifter {
public:
void Init(float samplerate, int samplesPerBlock);
@@ -84,11 +148,14 @@ public:
void RemoveMidiNote(int note);
void SetFormantPreserve(float val) { formant_preserve = val; }
void SetAutoTuneSpeed(float val);
+ void SetAutoTuneDepth(float val);
void SetPortamentoTime(float time) {
for (int i = 0; i < MAX_VOICES; ++i) {
voices[i].SetPortamentoTime(time);
}
}
+ float getInputPitch() const { return sample_rate_ / in_period; }
+ float getOutputPitch() const { return sample_rate_ / out_period; }
float out_midi = 40;
ShifterVoice voices[MAX_VOICES];
@@ -149,5 +216,6 @@ private:
float cos_lookup[8192];
float sample_rate_;
int blocksize;
+ MidiPitchSmoother out_midi_smoother;
};
#endif
\ No newline at end of file
diff --git a/Source/WebViewPluginDemo.h b/Source/WebViewPluginDemo.h
index c941a22..a1bf101 100644
--- a/Source/WebViewPluginDemo.h
+++ b/Source/WebViewPluginDemo.h
@@ -63,8 +63,9 @@ namespace ID
#define PARAMETER_ID(str) static const ParameterID str { #str, 1 };
PARAMETER_ID(formantPreserve)
- PARAMETER_ID(autoTuneSpeed)
- PARAMETER_ID(portTime)
+ PARAMETER_ID(autoTuneSpeed)
+ PARAMETER_ID(autoTuneDepth)
+ PARAMETER_ID(portTime)
PARAMETER_ID(mute)
PARAMETER_ID(filterType)
@@ -215,13 +216,18 @@ public:
autoTuneSpeed(addToLayout(layout,
ID::autoTuneSpeed,
"AutoTune Speed",
- NormalisableRange {0.001f, 0.4f, .001f},
+ NormalisableRange {0.001f, 0.1f, .001f},
+ .5f)),
+ autoTuneDepth(addToLayout(layout,
+ ID::autoTuneDepth,
+ "AutoTune Depth",
+ NormalisableRange {0.0f, 1.1f, .01f},
.5f)),
portTime(addToLayout(layout,
ID::portTime,
"Portamento Speed",
NormalisableRange {0.001f, 0.2f, .001f},
- .001f)),
+ .01f)),
mute(addToLayout(layout, ID::mute, "Mute", false)),
filterType(addToLayout(layout,
ID::filterType,
@@ -233,6 +239,7 @@ public:
AudioParameterFloat& formantPreserve;
AudioParameterFloat& autoTuneSpeed;
+ AudioParameterFloat& autoTuneDepth;
AudioParameterFloat& portTime;
AudioParameterBool& mute;
AudioParameterChoice& filterType;
@@ -275,7 +282,7 @@ public:
private:
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WebViewPluginAudioProcessor)
-
+
};
//==============================================================================
@@ -330,6 +337,7 @@ void WebViewPluginAudioProcessor::processBlock(juce::AudioBuffer& buffer,
buffer.clear(i, 0, buffer.getNumSamples());
shifter.SetFormantPreserve(parameters.formantPreserve.get());
shifter.SetAutoTuneSpeed(parameters.autoTuneSpeed.get());
+ shifter.SetAutoTuneDepth(parameters.autoTuneDepth.get());
shifter.SetPortamentoTime(parameters.portTime.get());
juce::AudioBuffer const_buff;
const_buff.makeCopyOf(buffer);
@@ -338,13 +346,13 @@ void WebViewPluginAudioProcessor::processBlock(juce::AudioBuffer& buffer,
for (const auto metadata : midi)
{
const auto msg = metadata.getMessage();
- if (msg.isNoteOn()) {
+ if (msg.isNoteOn()) {
shifter.AddMidiNote(msg.getNoteNumber());
new_midi = true;
//editor.webComponent.emitEventIfBrowserIsVisible("midNoteData", var{});
}
- else if (msg.isNoteOff()) {
+ else if (msg.isNoteOff()) {
shifter.RemoveMidiNote(msg.getNoteNumber());
new_midi = true;
//editor.webComponent.emitEventIfBrowserIsVisible("midNoteData", var{});
@@ -459,29 +467,25 @@ public:
SpinLock::ScopedLockType lock{ processorRef.midiLock };
- /*Array frame;
-
- for (size_t i = 1; i < processorRef.spectrumData.size(); ++i)
- frame.add(processorRef.spectrumData[i]);
-
- spectrumDataFrames.clear();
-
- spectrumDataFrames.push_back(std::move(frame));
-
- while (spectrumDataFrames.size() > numFramesBuffered)
- spectrumDataFrames.pop_front();*/
-
static int64 callbackCounter = 0;
-
- /*if ( spectrumDataFrames.size() == numFramesBuffered
- && callbackCounter++ % (int64) numFramesBuffered)
- {*/
- if (processorRef.new_midi) {
- processorRef.new_midi = false;
- webComponent.emitEventIfBrowserIsVisible("midNoteData", var{});
+ processorRef.new_midi = false;
+ juce::Array notes;
+ int voice_num = 0;
+ for (auto& voice : processorRef.shifter.voices) {
+ 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());
+ d->setProperty("notes", notes);
+ d->setProperty("input_pitch", processorRef.shifter.getInputPitch());
+ d->setProperty("output_pitch", processorRef.shifter.getOutputPitch());
+ webComponent.emitEventIfBrowserIsVisible("midNoteData", d.get());
}
private:
@@ -489,6 +493,7 @@ private:
WebSliderRelay formantSliderRelay{ "formantSlider" };
WebSliderRelay autoTuneSpeedSliderRelay{ "autoTuneSpeedSlider" };
+ WebSliderRelay autoTuneDepthSliderRelay{ "autoTuneDepthSlider" };
WebSliderRelay portTimeSliderRelay{ "portTimeSlider" };
WebToggleButtonRelay muteToggleRelay{ "muteToggle" };
WebComboBoxRelay filterTypeComboRelay{ "filterTypeCombo" };
@@ -502,6 +507,7 @@ private:
.withNativeIntegrationEnabled()
.withOptionsFrom(formantSliderRelay)
.withOptionsFrom(autoTuneSpeedSliderRelay)
+ .withOptionsFrom(autoTuneDepthSliderRelay)
.withOptionsFrom(portTimeSliderRelay)
.withOptionsFrom(muteToggleRelay)
.withOptionsFrom(filterTypeComboRelay)
@@ -518,6 +524,7 @@ private:
WebSliderParameterAttachment formantAttachment;
WebSliderParameterAttachment autoTuneSpeedAttachment;
+ WebSliderParameterAttachment autoTuneDepthAttachment;
WebSliderParameterAttachment portTimeAttachment;
WebToggleButtonParameterAttachment muteAttachment;
WebComboBoxParameterAttachment filterTypeAttachment;
@@ -612,6 +619,7 @@ std::optional WebViewPluginAudioProcessorEditor::
if (urlToRetrive == "midNoteData.json")
{
+
juce::Array notes;
int voice_num = 0;
for (auto& voice : processorRef.shifter.voices) {
@@ -656,6 +664,9 @@ WebViewPluginAudioProcessorEditor::WebViewPluginAudioProcessorEditor(WebViewPlug
autoTuneSpeedAttachment(*processorRef.state.getParameter(ID::autoTuneSpeed.getParamID()),
autoTuneSpeedSliderRelay,
processorRef.state.undoManager),
+ autoTuneDepthAttachment(*processorRef.state.getParameter(ID::autoTuneDepth.getParamID()),
+ autoTuneDepthSliderRelay,
+ processorRef.state.undoManager),
portTimeAttachment(*processorRef.state.getParameter(ID::portTime.getParamID()),
portTimeSliderRelay,
processorRef.state.undoManager),