From 55e80b4c741518986955497829c3e44b23a38880 Mon Sep 17 00:00:00 2001 From: michalcourson Date: Thu, 30 Oct 2025 21:39:06 -0400 Subject: [PATCH] midi notes avaiable on ui --- Assets/web/src/App.js | 549 ++++++++++++--------- Assets/web/src/Components/PianoKeyboard.js | 203 ++++++++ Source/Shifter.cpp | 22 +- Source/Shifter.h | 7 +- Source/WebViewPluginDemo.h | 107 ++-- Source/shifter_voice.cpp | 15 +- Source/shifter_voice.h | 4 +- 7 files changed, 604 insertions(+), 303 deletions(-) create mode 100644 Assets/web/src/Components/PianoKeyboard.js diff --git a/Assets/web/src/App.js b/Assets/web/src/App.js index fea956d..8de0d42 100644 --- a/Assets/web/src/App.js +++ b/Assets/web/src/App.js @@ -27,21 +27,22 @@ import "@fontsource/roboto/500.css"; import "@fontsource/roboto/700.css"; import Box from "@mui/material/Container"; -import Checkbox from "@mui/material/Checkbox"; +// import Checkbox from "@mui/material/Checkbox"; import Typography from "@mui/material/Typography"; import Container from "@mui/material/Container"; import Slider from "@mui/material/Slider"; -import Button from "@mui/material/Button"; -import CardActions from "@mui/material/CardActions"; -import Snackbar from "@mui/material/Snackbar"; -import IconButton from "@mui/material/IconButton"; -import CloseIcon from "@mui/icons-material/Close"; -import InputLabel from "@mui/material/InputLabel"; -import MenuItem from "@mui/material/MenuItem"; -import FormControl from "@mui/material/FormControl"; -import Select from "@mui/material/Select"; -import FormGroup from "@mui/material/FormGroup"; -import FormControlLabel from "@mui/material/FormControlLabel"; +// import Button from "@mui/material/Button"; +// import CardActions from "@mui/material/CardActions"; +// import Snackbar from "@mui/material/Snackbar"; +// import IconButton from "@mui/material/IconButton"; +// import CloseIcon from "@mui/icons-material/Close"; +// import InputLabel from "@mui/material/InputLabel"; +// import MenuItem from "@mui/material/MenuItem"; +// import FormControl from "@mui/material/FormControl"; +// import Select from "@mui/material/Select"; +// import FormGroup from "@mui/material/FormGroup"; +// import FormControlLabel from "@mui/material/FormControlLabel"; +import PianoKeyboard from "./Components/PianoKeyboard"; import { React, useState, useEffect, useRef } from "react"; import PropTypes from "prop-types"; @@ -122,262 +123,323 @@ function JuceSlider({ identifier, title }) { ); } -function JuceCheckbox({ identifier }) { - JuceCheckbox.propTypes = { - identifier: PropTypes.string, - }; +// function JuceCheckbox({ identifier }) { +// JuceCheckbox.propTypes = { +// identifier: PropTypes.string, +// }; - const checkboxState = Juce.getToggleState(identifier); +// const checkboxState = Juce.getToggleState(identifier); - const [value, setValue] = useState(checkboxState.getValue()); - const [properties, setProperties] = useState(checkboxState.properties); +// const [value, setValue] = useState(checkboxState.getValue()); +// const [properties, setProperties] = useState(checkboxState.properties); - const handleChange = (event) => { - checkboxState.setValue(event.target.checked); - setValue(event.target.checked); - }; +// const handleChange = (event) => { +// checkboxState.setValue(event.target.checked); +// setValue(event.target.checked); +// }; - useEffect(() => { - const valueListenerId = checkboxState.valueChangedEvent.addListener(() => { - setValue(checkboxState.getValue()); - }); - const propertiesListenerId = - checkboxState.propertiesChangedEvent.addListener(() => - setProperties(checkboxState.properties) - ); +// useEffect(() => { +// const valueListenerId = checkboxState.valueChangedEvent.addListener(() => { +// setValue(checkboxState.getValue()); +// }); +// const propertiesListenerId = +// checkboxState.propertiesChangedEvent.addListener(() => +// setProperties(checkboxState.properties) +// ); - return function cleanup() { - checkboxState.valueChangedEvent.removeListener(valueListenerId); - checkboxState.propertiesChangedEvent.removeListener(propertiesListenerId); - }; - }); +// return function cleanup() { +// checkboxState.valueChangedEvent.removeListener(valueListenerId); +// checkboxState.propertiesChangedEvent.removeListener(propertiesListenerId); +// }; +// }); - const cb = ; +// const cb = ; - return ( - - - - - - ); -} +// return ( +// +// +// +// +// +// ); +// } -function JuceComboBox({ identifier }) { - JuceComboBox.propTypes = { - identifier: PropTypes.string, - }; +// function JuceComboBox({ identifier }) { +// JuceComboBox.propTypes = { +// identifier: PropTypes.string, +// }; - const comboBoxState = Juce.getComboBoxState(identifier); +// const comboBoxState = Juce.getComboBoxState(identifier); - const [value, setValue] = useState(comboBoxState.getChoiceIndex()); - const [properties, setProperties] = useState(comboBoxState.properties); +// const [value, setValue] = useState(comboBoxState.getChoiceIndex()); +// const [properties, setProperties] = useState(comboBoxState.properties); - const handleChange = (event) => { - comboBoxState.setChoiceIndex(event.target.value); - setValue(event.target.value); - }; +// const handleChange = (event) => { +// comboBoxState.setChoiceIndex(event.target.value); +// setValue(event.target.value); +// }; - useEffect(() => { - const valueListenerId = comboBoxState.valueChangedEvent.addListener(() => { - setValue(comboBoxState.getChoiceIndex()); - }); - const propertiesListenerId = - comboBoxState.propertiesChangedEvent.addListener(() => { - setProperties(comboBoxState.properties); - }); +// useEffect(() => { +// const valueListenerId = comboBoxState.valueChangedEvent.addListener(() => { +// setValue(comboBoxState.getChoiceIndex()); +// }); +// const propertiesListenerId = +// comboBoxState.propertiesChangedEvent.addListener(() => { +// setProperties(comboBoxState.properties); +// }); - return function cleanup() { - comboBoxState.valueChangedEvent.removeListener(valueListenerId); - comboBoxState.propertiesChangedEvent.removeListener(propertiesListenerId); - }; - }); +// return function cleanup() { +// comboBoxState.valueChangedEvent.removeListener(valueListenerId); +// comboBoxState.propertiesChangedEvent.removeListener(propertiesListenerId); +// }; +// }); - return ( - - - {properties.name} - - - - ); -} +// return ( +// +// +// {properties.name} +// +// +// +// ); +// } -const sayHello = Juce.getNativeFunction("sayHello"); +// const sayHello = Juce.getNativeFunction("sayHello"); -const SpectrumDataReceiver_eventId = "spectrumData"; +// const SpectrumDataReceiver_eventId = "spectrumData"; -function interpolate(a, b, s) { - let result = new Array(a.length).fill(0); +// function interpolate(a, b, s) { +// let result = new Array(a.length).fill(0); - for (const [i, val] of a.entries()) result[i] += (1 - s) * val; +// for (const [i, val] of a.entries()) result[i] += (1 - s) * val; - for (const [i, val] of b.entries()) result[i] += s * val; +// for (const [i, val] of b.entries()) result[i] += s * val; - return result; -} +// return result; +// } -function mod(dividend, divisor) { - const quotient = Math.floor(dividend / divisor); - return dividend - divisor * quotient; -} +// function mod(dividend, divisor) { +// const quotient = Math.floor(dividend / divisor); +// return dividend - divisor * quotient; +// } -class SpectrumDataReceiver { - constructor(bufferLength) { - this.bufferLength = bufferLength; - this.buffer = new Array(this.bufferLength); - this.readIndex = 0; - this.writeIndex = 0; - this.lastTimeStampMs = 0; - this.timeResolutionMs = 0; +// class SpectrumDataReceiver { +// constructor(bufferLength) { +// this.bufferLength = bufferLength; +// this.buffer = new Array(this.bufferLength); +// this.readIndex = 0; +// this.writeIndex = 0; +// this.lastTimeStampMs = 0; +// this.timeResolutionMs = 0; +// let self = this; +// this.spectrumDataRegistrationId = window.__JUCE__.backend.addEventListener( +// SpectrumDataReceiver_eventId, +// () => { +// fetch(Juce.getBackendResourceAddress("spectrumData.json")) +// .then((response) => response.text()) +// .then((text) => { +// const data = JSON.parse(text); + +// if (self.timeResolutionMs == 0) { +// self.timeResolutionMs = data.timeResolutionMs; + +// // We want to stay behind the write index by a full batch plus one +// // so that we can keep reading buffered frames until we receive the +// // new batch +// self.readIndex = -data.frames.length - 1; + +// self.buffer.fill(new Array(data.frames[0].length).fill(0)); +// } + +// for (const f of data.frames) +// self.buffer[mod(self.writeIndex++, self.bufferLength)] = f; +// }); +// } +// ); +// } + +// getBufferItem(index) { +// return this.buffer[mod(index, this.buffer.length)]; +// } + +// getLevels(timeStampMs) { +// if (this.timeResolutionMs == 0) return null; + +// const previousTimeStampMs = this.lastTimeStampMs; +// this.lastTimeStampMs = timeStampMs; + +// if (previousTimeStampMs == 0) return this.buffer[0]; + +// const timeAdvance = +// (timeStampMs - previousTimeStampMs) / this.timeResolutionMs; +// this.readIndex += timeAdvance; + +// const integralPart = Math.floor(this.readIndex); +// const fractionalPart = this.readIndex - integralPart; + +// return interpolate( +// this.getBufferItem(integralPart), +// this.getBufferItem(integralPart + 1), +// fractionalPart +// ); +// } + +// unregister() { +// window.__JUCE__.backend.removeEventListener( +// this.spectrumDataRegistrationId +// ); +// } +// } +const MidNoteDataReceiver_eventId = "midNoteData"; +class MidNoteDataReceiver { + constructor() { + this.notes = []; let self = this; - this.spectrumDataRegistrationId = window.__JUCE__.backend.addEventListener( - SpectrumDataReceiver_eventId, + this.midNoteDataRegistrationId = window.__JUCE__.backend.addEventListener( + MidNoteDataReceiver_eventId, () => { - fetch(Juce.getBackendResourceAddress("spectrumData.json")) + fetch(Juce.getBackendResourceAddress("midNoteData.json")) .then((response) => response.text()) .then((text) => { const data = JSON.parse(text); - - if (self.timeResolutionMs == 0) { - self.timeResolutionMs = data.timeResolutionMs; - - // We want to stay behind the write index by a full batch plus one - // so that we can keep reading buffered frames until we receive the - // new batch - self.readIndex = -data.frames.length - 1; - - self.buffer.fill(new Array(data.frames[0].length).fill(0)); - } - - for (const f of data.frames) - self.buffer[mod(self.writeIndex++, self.bufferLength)] = f; + self.notes = data.notes; }); } ); } - getBufferItem(index) { - return this.buffer[mod(index, this.buffer.length)]; - } - - getLevels(timeStampMs) { - if (this.timeResolutionMs == 0) return null; - - const previousTimeStampMs = this.lastTimeStampMs; - this.lastTimeStampMs = timeStampMs; - - if (previousTimeStampMs == 0) return this.buffer[0]; - - const timeAdvance = - (timeStampMs - previousTimeStampMs) / this.timeResolutionMs; - this.readIndex += timeAdvance; - - const integralPart = Math.floor(this.readIndex); - const fractionalPart = this.readIndex - integralPart; - - return interpolate( - this.getBufferItem(integralPart), - this.getBufferItem(integralPart + 1), - fractionalPart - ); + getNotes() { + return this.notes; } unregister() { - window.__JUCE__.backend.removeEventListener( - this.spectrumDataRegistrationId - ); + window.__JUCE__.backend.removeEventListener(this.midNoteDataRegistrationId); } } -function FreqBandInfo() { - const canvasRef = useRef(null); - let dataReceiver = null; - let isActive = true; +function MidiNoteInfo() { + const [notes, setNotes] = useState([]); + const dataReceiverRef = useRef(null); + const isActiveRef = useRef(true); - // eslint-disable-next-line no-unused-vars - const render = (timeStampMs) => { - const canvas = canvasRef.current; - const ctx = canvas.getContext("2d"); - ctx.clearRect(0, 0, canvas.width, canvas.height); + useEffect(() => { + dataReceiverRef.current = new MidNoteDataReceiver(); + isActiveRef.current = true; - var grd = ctx.createLinearGradient(0, 0, 0, canvas.height); - grd.addColorStop(0, "#1976d2"); - grd.addColorStop(1, "#dae9f8"); - ctx.fillStyle = grd; - - if (dataReceiver != null) { - const levels = dataReceiver.getLevels(timeStampMs); - - if (levels != null) { - const numBars = levels.length; - const barWidth = canvas.width / numBars; - const barHeight = canvas.height; - - for (const [i, l] of levels.entries()) { - ctx.fillRect( - i * barWidth, - barHeight - l * barHeight, - barWidth, - l * barHeight - ); - } + function render() { + if (dataReceiverRef.current) { + setNotes(dataReceiverRef.current.getNotes()); + } + if (isActiveRef.current) { + window.requestAnimationFrame(render); } } - if (isActive) window.requestAnimationFrame(render); - }; - - useEffect(() => { - dataReceiver = new SpectrumDataReceiver(10); - isActive = true; window.requestAnimationFrame(render); return function cleanup() { - isActive = false; - dataReceiver.unregister(); + isActiveRef.current = false; + if (dataReceiverRef.current) { + dataReceiverRef.current.unregister(); + } }; - }); - - const canvasStyle = { - marginLeft: "0", - marginRight: "0", - marginTop: "1em", - display: "block", - width: "94%", - bottom: "0", - position: "absolute", - }; + }, []); return ( - - - +
+ +
); } +// 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( controlParameterIndexAnnotation @@ -387,33 +449,33 @@ function App() { controlParameterIndexUpdater.handleMouseMove(event); }); - const [open, setOpen] = useState(false); - const [snackbarMessage, setMessage] = useState("No message received yet"); + // const [open, setOpen] = useState(false); + // const [snackbarMessage, setMessage] = useState("No message received yet"); - const openSnackbar = () => { - setOpen(true); - }; + // const openSnackbar = () => { + // setOpen(true); + // }; - const handleClose = (event, reason) => { - if (reason === "clickaway") { - return; - } + // const handleClose = (event, reason) => { + // if (reason === "clickaway") { + // return; + // } - setOpen(false); - }; + // setOpen(false); + // }; - const action = ( - <> - - - - - ); + // const action = ( + // <> + // + // + // + // + // ); return (
@@ -422,7 +484,8 @@ function App() { - + + {/*
); } diff --git a/Assets/web/src/Components/PianoKeyboard.js b/Assets/web/src/Components/PianoKeyboard.js new file mode 100644 index 0000000..81a67bf --- /dev/null +++ b/Assets/web/src/Components/PianoKeyboard.js @@ -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 ( +
+ {/* White keys */} +
+ {whiteKeys.map((k) => ( +
+
+ {k.noteName} + {k.held && ( + + {k.voice} + + )} +
+
+ ))} +
+ {/* Black keys */} +
+ {blackKeys.map((k) => ( +
+
+ {k.held && ( + + {k.voice} + + )} +
+
+ ))} +
+
+ ); +} diff --git a/Source/Shifter.cpp b/Source/Shifter.cpp index 36a37ad..562bd0d 100644 --- a/Source/Shifter.cpp +++ b/Source/Shifter.cpp @@ -67,14 +67,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) { @@ -148,13 +151,13 @@ void Shifter::DetectPitch(const float* const* in, float** out, size_t size) // 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; 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 = out_midi_smoother.update(midi, (int)(midi+.5)); - out_period = 48000.0f / mtof(out_midi); + out_period = sample_rate_ / mtof(out_midi); } void Shifter::SetRates() {} @@ -275,8 +278,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) { + voices[out_p].Process(); if (!voices[out_p].IsActive()) continue; - voices[out_p].Process(); if (voices[out_p].PeriodOverflow()) { float resampling_period = GetOutputEnvelopePeriod(out_p); @@ -328,12 +331,12 @@ void Shifter::GetSamples(float** output, const float* input, size_t size) void Shifter::AddMidiNote(int note) { 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; } @@ -343,9 +346,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; } } } \ No newline at end of file diff --git a/Source/Shifter.h b/Source/Shifter.h index 4494ac5..9af3288 100644 --- a/Source/Shifter.h +++ b/Source/Shifter.h @@ -75,7 +75,7 @@ public: class Shifter { public: - void Init(); + void Init(float samplerate, int samplesPerBlock); void Process(const float* const* in, float** out, size_t size); @@ -91,6 +91,7 @@ public: } float out_midi = 40; + ShifterVoice voices[MAX_VOICES]; private: void DetectPitch(const float* const* in, float** out, size_t size); @@ -140,11 +141,13 @@ private: float out_period_filter_amount = 0.7f; // You can expose this as a parameter - 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; }; #endif \ No newline at end of file diff --git a/Source/WebViewPluginDemo.h b/Source/WebViewPluginDemo.h index 99ea5bd..c941a22 100644 --- a/Source/WebViewPluginDemo.h +++ b/Source/WebViewPluginDemo.h @@ -145,26 +145,26 @@ private: int64 writeIx = 0; }; -class SpectralBars -{ -public: - //template - void push(int data) - { - testQueue.push(data); - } - - void compute(Span output) { - int index = 0; - for (auto it = output.begin(); it != output.end(); ++it) { - *it = testQueue.get(index++); - } - } - - -private: - circ_queue testQueue; -}; +//class SpectralBars +//{ +//public: +// //template +// void push(int data) +// { +// testQueue.push(data); +// } +// +// void compute(Span output) { +// int index = 0; +// for (auto it = output.begin(); it != output.end(); ++it) { +// *it = testQueue.get(index++); +// } +// } +// +// +//private: +// circ_queue testQueue; +//}; //============================================================================== class WebViewPluginAudioProcessor : public AudioProcessor @@ -200,6 +200,7 @@ public: //============================================================================== void getStateInformation(MemoryBlock& destData) override; void setStateInformation(const void* data, int sizeInBytes) override; + bool new_midi = false; struct Parameters { @@ -261,18 +262,20 @@ public: Parameters parameters; AudioProcessorValueTreeState state; + SpinLock midiLock; - std::vector spectrumData = [] { return std::vector(256, 0.0f); }(); + /*std::vector spectrumData = [] { return std::vector(256, 0.0f); }(); SpinLock spectrumDataLock; - SpectralBars spectralBars; + SpectralBars spectralBars;*/ dsp::LadderFilter filter; + Shifter shifter; private: //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WebViewPluginAudioProcessor) - Shifter shifter; + }; //============================================================================== @@ -288,14 +291,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; @@ -335,8 +338,17 @@ void WebViewPluginAudioProcessor::processBlock(juce::AudioBuffer& 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{}); + } } @@ -344,12 +356,11 @@ void WebViewPluginAudioProcessor::processBlock(juce::AudioBuffer& 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() }); } @@ -446,9 +457,9 @@ public: { static constexpr size_t numFramesBuffered = 5; - SpinLock::ScopedLockType lock{ processorRef.spectrumDataLock }; + SpinLock::ScopedLockType lock{ processorRef.midiLock }; - Array frame; + /*Array frame; for (size_t i = 1; i < processorRef.spectrumData.size(); ++i) frame.add(processorRef.spectrumData[i]); @@ -458,14 +469,18 @@ public: spectrumDataFrames.push_back(std::move(frame)); while (spectrumDataFrames.size() > numFramesBuffered) - spectrumDataFrames.pop_front(); + spectrumDataFrames.pop_front();*/ static int64 callbackCounter = 0; /*if ( spectrumDataFrames.size() == numFramesBuffered && callbackCounter++ % (int64) numFramesBuffered) {*/ - webComponent.emitEventIfBrowserIsVisible("spectrumData", var{}); + if (processorRef.new_midi) { + processorRef.new_midi = false; + webComponent.emitEventIfBrowserIsVisible("midNoteData", var{}); + } + //} } @@ -595,16 +610,22 @@ std::optional WebViewPluginAudioProcessorEditor:: return WebBrowserComponent::Resource{ streamToVector(stream), String { "text/html" } }; } - if (urlToRetrive == "spectrumData.json") + if (urlToRetrive == "midNoteData.json") { - Array frames; - - for (const auto& frame : spectrumDataFrames) - frames.add(frame); + 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("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 }; @@ -652,7 +673,7 @@ WebViewPluginAudioProcessorEditor::WebViewPluginAudioProcessorEditor(WebViewPlug setSize(500, 500); - startTimerHz(20); + startTimerHz(60); } //============================================================================== @@ -675,5 +696,7 @@ public: } bool hasEditor() const override { return true; } - AudioProcessorEditor* createEditor() override { return new WebViewPluginAudioProcessorEditor(*this); } + AudioProcessorEditor* createEditor() override { + return new WebViewPluginAudioProcessorEditor(*this); + } }; diff --git a/Source/shifter_voice.cpp b/Source/shifter_voice.cpp index 4250c56..dd50a3a 100644 --- a/Source/shifter_voice.cpp +++ b/Source/shifter_voice.cpp @@ -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.1f); - amplitude_envelope_.SetReleaseTime(.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_) { diff --git a/Source/shifter_voice.h b/Source/shifter_voice.h index 029b945..373fa46 100644 --- a/Source/shifter_voice.h +++ b/Source/shifter_voice.h @@ -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;