Compare commits

...

4 Commits

Author SHA1 Message Date
3468c1f389 autotune paramters, autotune in ui. UI organize 2025-11-01 13:33:14 -04:00
55e80b4c74 midi notes avaiable on ui 2025-10-30 21:39:06 -04:00
3645e38dd5 adjustable portamento, fixed adsr 2025-10-30 20:00:55 -04:00
098bd49cb5 better autotune speed 2025-10-30 19:49:58 -04:00
21 changed files with 724 additions and 666 deletions

View File

@ -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",

View File

@ -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"
}
}

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
@ -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`.
-->
<title>Ladder Filter</title>
<title>Harmonizer</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -1,6 +1,6 @@
{
"short_name": "Ladder Filter",
"name": "Ladder Filter",
"short_name": "Harmonizer",
"name": "Harmonizer",
"icons": [
{
"src": "favicon.ico",

View File

@ -26,357 +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 { 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 (
<Box
{...{
[controlParameterIndexAnnotation]:
sliderState.properties.parameterIndex,
}}
>
<Typography sx={{ mt: 1.5 }}>
{properties.name}: {sliderState.getScaledValue()} {properties.label}
</Typography>
<Slider
aria-label={title}
value={value}
scale={calculateValue}
onChange={handleChange}
min={0}
max={1}
step={1 / (properties.numSteps - 1)}
onChangeCommitted={changeCommitted}
onMouseDown={mouseDown}
/>
</Box>
);
}
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 = <Checkbox checked={value} onChange={handleChange} />;
return (
<Box
{...{
[controlParameterIndexAnnotation]:
checkboxState.properties.parameterIndex,
}}
>
<FormGroup>
<FormControlLabel control={cb} label={properties.name} />
</FormGroup>
</Box>
);
}
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 (
<Box
{...{
[controlParameterIndexAnnotation]:
comboBoxState.properties.parameterIndex,
}}
>
<FormControl fullWidth>
<InputLabel id={identifier}>{properties.name}</InputLabel>
<Select
labelId={identifier}
value={value}
label={properties.name}
onChange={handleChange}
>
{properties.choices.map((choice, i) => (
<MenuItem value={i} key={i}>
{choice}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
);
}
const sayHello = Juce.getNativeFunction("sayHello");
const 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
);
}
}
function FreqBandInfo() {
const canvasRef = useRef(null);
let dataReceiver = null;
let isActive = true;
// eslint-disable-next-line no-unused-vars
const render = (timeStampMs) => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
var grd = ctx.createLinearGradient(0, 0, 0, canvas.height);
grd.addColorStop(0, "#1976d2");
grd.addColorStop(1, "#dae9f8");
ctx.fillStyle = grd;
if (dataReceiver != null) {
const levels = dataReceiver.getLevels(timeStampMs);
if (levels != null) {
const numBars = levels.length;
const barWidth = canvas.width / numBars;
const barHeight = canvas.height;
for (const [i, l] of levels.entries()) {
ctx.fillRect(
i * barWidth,
barHeight - l * barHeight,
barWidth,
l * barHeight
);
}
}
}
if (isActive) window.requestAnimationFrame(render);
};
useEffect(() => {
dataReceiver = new SpectrumDataReceiver(10);
isActive = true;
window.requestAnimationFrame(render);
return function cleanup() {
isActive = false;
dataReceiver.unregister();
};
});
const canvasStyle = {
marginLeft: "0",
marginRight: "0",
marginTop: "1em",
display: "block",
width: "94%",
bottom: "0",
position: "absolute",
};
return (
<Box>
<canvas height={90} style={canvasStyle} ref={canvasRef}></canvas>
</Box>
);
}
function App() {
const controlParameterIndexUpdater = new Juce.ControlParameterIndexUpdater(
@ -387,81 +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 = (
<>
<IconButton
size="small"
aria-label="close"
color="inherit"
onClick={handleClose}
>
<CloseIcon fontSize="small" />
</IconButton>
</>
);
return (
<div>
<Container>
<JuceSlider identifier="formantSlider" title="Formant" />
<JuceSlider identifier="autoTuneSpeedSlider" title="Auto Tune Speed" />
<JuceSlider identifier="autoTuneDepthSlider" title="Auto Tune Depth" />
<JuceSlider identifier="portTimeSlider" title="Portamento Speed" />
</Container>
<CardActions style={{ justifyContent: "center" }}>
<Button
variant="contained"
sx={{ marginTop: 2 }}
onClick={() => {
sayHello("JUCE").then((result) => {
setMessage(result);
openSnackbar();
});
}}
>
Call backend function
</Button>
</CardActions>
<CardActions style={{ justifyContent: "center" }}>
<Button
variant="contained"
sx={{ marginTop: 2 }}
onClick={() => {
fetch(Juce.getBackendResourceAddress("data.txt"))
.then((response) => response.text())
.then((text) => {
setMessage("Data fetched: " + text);
openSnackbar();
});
}}
>
Fetch data from backend
</Button>
</CardActions>
<JuceCheckbox identifier="muteToggle" />
<br></br>
<JuceComboBox identifier="filterTypeCombo" />
<FreqBandInfo></FreqBandInfo>
<Snackbar
open={open}
autoHideDuration={6000}
onClose={handleClose}
message={snackbarMessage}
action={action}
/>
<MidiNoteInfo />
</div>
);
}

View File

@ -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 (
<div
style={{
position: "relative",
width: "100%",
height: `${height}px`,
backgroundColor,
overflow: "hidden",
}}
>
{/* Negative (left) bar */}
<div
style={{
...baseStyle,
right: "50%", // anchored to the center
width: `${negativeWidth}%`,
backgroundColor: negativeColor,
}}
/>
{/* Positive (right) bar */}
<div
style={{
...baseStyle,
left: "50%", // anchored to the center
width: `${positiveWidth}%`,
backgroundColor: positiveColor,
}}
/>
</div>
);
}

View File

@ -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 (
<Box
{...{
[controlParameterIndexAnnotation]:
sliderState.properties.parameterIndex,
}}
>
<Typography sx={{ mt: 1.5 }}>
{properties.name}: {sliderState.getScaledValue()} {properties.label}
</Typography>
<Slider
aria-label={title}
value={value}
scale={calculateValue}
onChange={handleChange}
min={0}
max={1}
step={1 / (properties.numSteps - 1)}
onChangeCommitted={changeCommitted}
onMouseDown={mouseDown}
/>
</Box>
);
}

View File

@ -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 (
<div
className={clsx(
"w-16 flex flex-col gap-0.5 justify-center items-center text-xs select-none",
"outline-none focus-within:outline-1 focus-within:outline-offset-4 focus-within:outline-stone-300"
)}
>
<KnobHeadlessLabel id={labelId}>{label}</KnobHeadlessLabel>
<KnobHeadless
id={knobId}
aria-labelledby={labelId}
className="relative w-16 h-16 outline-none"
valueMin={valueMin}
valueMax={valueMax}
valueRaw={valueRaw}
valueRawRoundFn={valueRawRoundFn}
valueRawDisplayFn={valueRawDisplayFn}
dragSensitivity={dragSensitivity}
axis={axis}
mapTo01={mapTo01}
mapFrom01={mapFrom01}
onValueRawChange={setValueRaw}
{...keyboardControlHandlers}
>
<KnobBaseThumb theme={theme} value01={value01} />
</KnobHeadless>
<KnobHeadlessOutput htmlFor={knobId}>
{valueRawDisplayFn(valueRaw)}
</KnobHeadlessOutput>
</div>
);
}

View File

@ -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 (
<div
className={clsx(
"absolute h-full w-full rounded-full",
theme === "stone" && "bg-stone-300",
theme === "pink" && "bg-pink-300",
theme === "green" && "bg-green-300",
theme === "sky" && "bg-sky-300"
)}
>
<div className="absolute h-full w-full" style={{ rotate: `${angle}deg` }}>
<div className="absolute left-1/2 top-0 h-1/2 w-[2px] -translate-x-1/2 rounded-sm bg-stone-950" />
</div>
</div>
);
}

View File

@ -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 (
<KnobBase
valueDefault={valueDefault}
valueMin={valueMin}
valueMax={valueMax}
stepFn={stepFn}
stepLargerFn={stepLargerFn}
valueRawRoundFn={valueRawRoundFn}
valueRawDisplayFn={valueRawDisplayFn}
theme={theme}
label={label}
axis={axis}
/>
);
}
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)}%`;

View File

@ -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 (
<div style={{ padding: "1rem" }}>
<PianoKeyboard heldNotes={notes} />
<h1>Autotune Note: {getCharfromNoteIndex(autotuneNote)}</h1>
<label>Input cents</label>
<CenterGrowSlider value={inputCents} />
<label>Output cents</label>
<CenterGrowSlider value={outputCents} />
</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

@ -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);
}
}

View File

@ -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(

View File

@ -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);

View File

@ -0,0 +1 @@
export const controlParameterIndexAnnotation = "controlparameterindex";

View File

@ -6,6 +6,15 @@
#define PI_F 3.1415927410125732421875f
#endif
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;
@ -15,14 +24,17 @@ static inline bool float_equal(float one, float two) {
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;
helm.setframesize(1024);
helm.setoverlap(1);
for (int i = 0; i < MAX_VOICES; ++i)
{
voices[i].Init(48000);
voices[i].Init(samplerate);
}
for (int i = 0; i < BUFFER_SIZE; ++i)
{
@ -90,28 +102,19 @@ 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.7f; // 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
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;
int midi = (int)(12 * log2f(in_freq / 440) + 69.5f);
float target_out_period = 48000.0f / mtof(midi);
if (midi != last_autotune_midi) {
last_autotune_midi = midi;
out_period = in_period;
}
float error = target_out_period - out_period;
float adjustment = error * out_period_filter_amount;
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);
out_midi = midi;
out_period += adjustment;
out_midi = out_midi_smoother.update(midi, (int)(midi+.5));
out_period = sample_rate_ / mtof(out_midi);
}
void Shifter::SetRates() {}
@ -232,8 +235,8 @@ void Shifter::GetSamples(float** output, const float* input, size_t size)
//add new samples if necessary
for (int out_p = 0; out_p < MAX_VOICES; ++out_p)
{
if (!voices[out_p].IsActive()) continue;
voices[out_p].Process();
if (!voices[out_p].IsActive()) continue;
if (voices[out_p].PeriodOverflow())
{
float resampling_period = GetOutputEnvelopePeriod(out_p);
@ -285,12 +288,12 @@ void Shifter::GetSamples(float** output, const float* input, size_t size)
void Shifter::AddMidiNote(int note) {
for (int i = 0; i < MAX_VOICES; ++i) {
if (voices[i].IsActive() && voices[i].GetMidiNote() == note) {
return;
if (voices[i].onoff_ && voices[i].GetMidiNote() == note) {
voices[i].Release();
}
}
for (int i = 0; i < MAX_VOICES; ++i) {
if (!voices[i].IsActive()) {
if (!voices[i].onoff_) {
voices[i].Trigger(note);
return;
}
@ -300,9 +303,8 @@ void Shifter::AddMidiNote(int note) {
void Shifter::RemoveMidiNote(int note) {
for (int i = 0; i < MAX_VOICES; ++i) {
if (voices[i].IsActive() && voices[i].GetMidiNote() == note) {
if (voices[i].GetMidiNote() == note) {
voices[i].Release();
return;
}
}
}

View File

@ -73,9 +73,73 @@ 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();
void Init(float samplerate, int samplesPerBlock);
void Process(const float* const* in,
float** out,
size_t size);
@ -83,9 +147,18 @@ public:
void AddMidiNote(int note);
void RemoveMidiNote(int note);
void SetFormantPreserve(float val) { formant_preserve = val; }
void SetAutoTuneSpeed(float val) { out_period_filter_amount = 1 - 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; }
int out_midi = -1;
float out_midi = 40;
ShifterVoice voices[MAX_VOICES];
private:
void DetectPitch(const float* const* in, float** out, size_t size);
@ -135,11 +208,14 @@ private:
float out_period_filter_amount = 0.7f; // You can expose this as a parameter
ShifterVoice voices[MAX_VOICES];
float out_period = 0; //C3
float in_period = 366.936;
float out_period_counter = 0;
float cos_lookup[8192];
float sample_rate_;
int blocksize;
MidiPitchSmoother out_midi_smoother;
};
#endif

View File

@ -64,6 +64,8 @@ namespace ID
PARAMETER_ID(formantPreserve)
PARAMETER_ID(autoTuneSpeed)
PARAMETER_ID(autoTuneDepth)
PARAMETER_ID(portTime)
PARAMETER_ID(mute)
PARAMETER_ID(filterType)
@ -144,26 +146,26 @@ private:
int64 writeIx = 0;
};
class SpectralBars
{
public:
//template <typename T>
void push(int data)
{
testQueue.push(data);
}
void compute(Span<int> output) {
int index = 0;
for (auto it = output.begin(); it != output.end(); ++it) {
*it = testQueue.get(index++);
}
}
private:
circ_queue<int, 256> testQueue;
};
//class SpectralBars
//{
//public:
// //template <typename T>
// void push(int data)
// {
// testQueue.push(data);
// }
//
// void compute(Span<int> output) {
// int index = 0;
// for (auto it = output.begin(); it != output.end(); ++it) {
// *it = testQueue.get(index++);
// }
// }
//
//
//private:
// circ_queue<int, 256> testQueue;
//};
//==============================================================================
class WebViewPluginAudioProcessor : public AudioProcessor
@ -199,6 +201,7 @@ public:
//==============================================================================
void getStateInformation(MemoryBlock& destData) override;
void setStateInformation(const void* data, int sizeInBytes) override;
bool new_midi = false;
struct Parameters
{
@ -213,8 +216,18 @@ public:
autoTuneSpeed(addToLayout<AudioParameterFloat>(layout,
ID::autoTuneSpeed,
"AutoTune Speed",
NormalisableRange<float> {0.0f, 1.0f, .01f},
NormalisableRange<float> {0.001f, 0.1f, .001f},
.5f)),
autoTuneDepth(addToLayout<AudioParameterFloat>(layout,
ID::autoTuneDepth,
"AutoTune Depth",
NormalisableRange<float> {0.0f, 1.1f, .01f},
.5f)),
portTime(addToLayout<AudioParameterFloat>(layout,
ID::portTime,
"Portamento Speed",
NormalisableRange<float> {0.001f, 0.2f, .001f},
.01f)),
mute(addToLayout<AudioParameterBool>(layout, ID::mute, "Mute", false)),
filterType(addToLayout<AudioParameterChoice>(layout,
ID::filterType,
@ -226,6 +239,8 @@ public:
AudioParameterFloat& formantPreserve;
AudioParameterFloat& autoTuneSpeed;
AudioParameterFloat& autoTuneDepth;
AudioParameterFloat& portTime;
AudioParameterBool& mute;
AudioParameterChoice& filterType;
@ -254,18 +269,20 @@ public:
Parameters parameters;
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;
SpectralBars spectralBars;
SpectralBars spectralBars;*/
dsp::LadderFilter<float> filter;
Shifter shifter;
private:
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WebViewPluginAudioProcessor)
Shifter shifter;
};
//==============================================================================
@ -281,14 +298,14 @@ WebViewPluginAudioProcessor::WebViewPluginAudioProcessor(AudioProcessorValueTree
parameters(layout),
state(*this, nullptr, "STATE", std::move(layout))
{
shifter.Init();
shifter.Init(48000.0f, 48);
}
//==============================================================================
void WebViewPluginAudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
{
const auto channels = std::max(getTotalNumInputChannels(), getTotalNumOutputChannels());
shifter.Init();
shifter.Init((float)sampleRate, samplesPerBlock);
if (channels == 0)
return;
@ -320,6 +337,8 @@ void WebViewPluginAudioProcessor::processBlock(juce::AudioBuffer<float>& 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<float> const_buff;
const_buff.makeCopyOf(buffer);
shifter.Process(const_buff.getArrayOfReadPointers(), (float**)buffer.getArrayOfWritePointers(), buffer.getNumSamples());
@ -327,8 +346,17 @@ void WebViewPluginAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer,
for (const auto metadata : midi)
{
const auto msg = metadata.getMessage();
if (msg.isNoteOn()) shifter.AddMidiNote(msg.getNoteNumber());
else if (msg.isNoteOff()) shifter.RemoveMidiNote(msg.getNoteNumber());
if (msg.isNoteOn()) {
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{});
}
}
@ -336,12 +364,11 @@ void WebViewPluginAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer,
//DBG(shifter.out_midi[MAX_VOICES]);
//push midi note
//spectralBars.push(shifter.out_midi[MAX_VOICES]);
const SpinLock::ScopedTryLockType lock(spectrumDataLock);
const SpinLock::ScopedTryLockType lock(midiLock);
if (!lock.isLocked())
return;
spectralBars.compute({ spectrumData.data(), spectrumData.size() });
}
@ -438,27 +465,27 @@ public:
{
static constexpr size_t numFramesBuffered = 5;
SpinLock::ScopedLockType lock{ processorRef.spectrumDataLock };
Array<var> 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();
SpinLock::ScopedLockType lock{ processorRef.midiLock };
static int64 callbackCounter = 0;
processorRef.new_midi = false;
juce::Array<var> 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++;
}
/*if ( spectrumDataFrames.size() == numFramesBuffered
&& callbackCounter++ % (int64) numFramesBuffered)
{*/
webComponent.emitEventIfBrowserIsVisible("spectrumData", var{});
//}
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:
@ -466,6 +493,8 @@ private:
WebSliderRelay formantSliderRelay{ "formantSlider" };
WebSliderRelay autoTuneSpeedSliderRelay{ "autoTuneSpeedSlider" };
WebSliderRelay autoTuneDepthSliderRelay{ "autoTuneDepthSlider" };
WebSliderRelay portTimeSliderRelay{ "portTimeSlider" };
WebToggleButtonRelay muteToggleRelay{ "muteToggle" };
WebComboBoxRelay filterTypeComboRelay{ "filterTypeCombo" };
@ -478,6 +507,8 @@ private:
.withNativeIntegrationEnabled()
.withOptionsFrom(formantSliderRelay)
.withOptionsFrom(autoTuneSpeedSliderRelay)
.withOptionsFrom(autoTuneDepthSliderRelay)
.withOptionsFrom(portTimeSliderRelay)
.withOptionsFrom(muteToggleRelay)
.withOptionsFrom(filterTypeComboRelay)
.withOptionsFrom(controlParameterIndexReceiver)
@ -493,6 +524,8 @@ private:
WebSliderParameterAttachment formantAttachment;
WebSliderParameterAttachment autoTuneSpeedAttachment;
WebSliderParameterAttachment autoTuneDepthAttachment;
WebSliderParameterAttachment portTimeAttachment;
WebToggleButtonParameterAttachment muteAttachment;
WebComboBoxParameterAttachment filterTypeAttachment;
@ -584,16 +617,23 @@ std::optional<WebBrowserComponent::Resource> WebViewPluginAudioProcessorEditor::
return WebBrowserComponent::Resource{ streamToVector(stream), String { "text/html" } };
}
if (urlToRetrive == "spectrumData.json")
if (urlToRetrive == "midNoteData.json")
{
Array<var> frames;
for (const auto& frame : spectrumDataFrames)
frames.add(frame);
juce::Array<var> 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("timeResolutionMs", getTimerInterval());
d->setProperty("frames", std::move(frames));
d->setProperty("notes", notes);
const auto s = JSON::toString(d.get());
MemoryInputStream stream{ s.getCharPointer(), s.getNumBytesAsUTF8(), false };
@ -624,6 +664,12 @@ 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),
muteAttachment(*processorRef.state.getParameter(ID::mute.getParamID()),
muteToggleRelay,
processorRef.state.undoManager),
@ -633,12 +679,12 @@ WebViewPluginAudioProcessorEditor::WebViewPluginAudioProcessorEditor(WebViewPlug
{
addAndMakeVisible(webComponent);
//webComponent.goToURL(localDevServerAddress);
webComponent.goToURL (WebBrowserComponent::getResourceProviderRoot());
webComponent.goToURL(localDevServerAddress);
//webComponent.goToURL (WebBrowserComponent::getResourceProviderRoot());
setSize(500, 500);
startTimerHz(20);
startTimerHz(60);
}
//==============================================================================
@ -661,5 +707,7 @@ public:
}
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) {
portamento_.Init(sample_rate, 0.05f); //default portamento time
amplitude_envelope_.Init(sample_rate);
sample_rate_ = sample_rate;
portamento_.Init(sample_rate_, 0.05f); //default portamento time
amplitude_envelope_.Init(sample_rate_);
amplitude_envelope_.SetAttackTime(0.2f);
amplitude_envelope_.SetDecayTime(0.2f);
amplitude_envelope_.SetReleaseTime(1.0f);
amplitude_envelope_.SetDecayTime(0.1f);
amplitude_envelope_.SetReleaseTime(.05f);
onoff_ = false;
overflow_ = false;
current_midi = 60;
current_period_ = 48000.0f / mtof((float)current_midi);
current_period_ = sample_rate_ / mtof((float)current_midi);
current_amplitude = 0.0f;
period_counter = 0.0f;
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() {
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++;
overflow_ = period_counter >= current_period_;
if (overflow_) {

View File

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