From 3468c1f389af5a839422122699616d6881706716 Mon Sep 17 00:00:00 2001 From: michalcourson Date: Sat, 1 Nov 2025 13:33:14 -0400 Subject: [PATCH] autotune paramters, autotune in ui. UI organize --- Assets/web/package-lock.json | 19 +- Assets/web/package.json | 3 +- Assets/web/public/index.html | 4 +- Assets/web/public/manifest.json | 4 +- Assets/web/src/App.js | 484 +----------------- Assets/web/src/Components/CenterGrowSlider.js | 61 +++ Assets/web/src/Components/JuceSlider.js | 76 +++ Assets/web/src/Components/KnobBase.js | 88 ---- Assets/web/src/Components/KnobBaseThumb.js | 28 - Assets/web/src/Components/KnobPercentage.js | 33 -- Assets/web/src/Components/MidiNoteInfo.js | 69 +++ .../src/DataRecievers/MidiNoteDataReceiver.js | 54 ++ Assets/web/src/index.js | 2 +- Assets/web/src/reportWebVitals.js | 4 +- Assets/web/src/types/JuceTypes.js | 1 + Source/Shifter.cpp | 53 +- Source/Shifter.h | 68 +++ Source/WebViewPluginDemo.h | 67 ++- 18 files changed, 406 insertions(+), 712 deletions(-) create mode 100644 Assets/web/src/Components/CenterGrowSlider.js create mode 100644 Assets/web/src/Components/JuceSlider.js delete mode 100644 Assets/web/src/Components/KnobBase.js delete mode 100644 Assets/web/src/Components/KnobBaseThumb.js delete mode 100644 Assets/web/src/Components/KnobPercentage.js create mode 100644 Assets/web/src/Components/MidiNoteInfo.js create mode 100644 Assets/web/src/DataRecievers/MidiNoteDataReceiver.js create mode 100644 Assets/web/src/types/JuceTypes.js 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),