From fe6ee5e51e08af693da1158ee9973744d01228e3 Mon Sep 17 00:00:00 2001 From: michalcourson Date: Tue, 4 Nov 2025 20:31:55 -0500 Subject: [PATCH] closes #2 Add dynamic toggle options. Add toggle for turning on and off the autotune --- Assets/web/src/App.js | 2 + Assets/web/src/Components/JuceCheckbox.js | 54 +++ Assets/web/src/Components/JuceComboBox.js | 64 ++++ Source/PluginEditor.cpp | 10 + Source/PluginEditor.h | 10 + Source/PluginProcessor.cpp | 1 + Source/PluginProcessor.h | 8 + Source/Shifter.cpp | 400 +++++++++++----------- Source/Shifter.h | 2 + 9 files changed, 356 insertions(+), 195 deletions(-) create mode 100644 Assets/web/src/Components/JuceCheckbox.js create mode 100644 Assets/web/src/Components/JuceComboBox.js diff --git a/Assets/web/src/App.js b/Assets/web/src/App.js index fc5b5d4..d0ae111 100644 --- a/Assets/web/src/App.js +++ b/Assets/web/src/App.js @@ -29,6 +29,7 @@ import "@fontsource/roboto/700.css"; import Container from "@mui/material/Container"; import * as Juce from "juce-framework-frontend"; import JuceSlider from "./Components/JuceSlider.js"; +import JuceCheckbox from "./Components/JuceCheckbox.js"; import MidiNoteInfo from "./Components/MidiNoteInfo.js"; import { controlParameterIndexAnnotation } from "./types/JuceTypes.js"; @@ -50,6 +51,7 @@ function App() { + diff --git a/Assets/web/src/Components/JuceCheckbox.js b/Assets/web/src/Components/JuceCheckbox.js new file mode 100644 index 0000000..772fe0d --- /dev/null +++ b/Assets/web/src/Components/JuceCheckbox.js @@ -0,0 +1,54 @@ +import React, { useState, useEffect } from "react"; +import PropTypes from "prop-types"; +import Box from "@mui/material/Box"; +import * as Juce from "juce-framework-frontend"; +import Checkbox from "@mui/material/Checkbox"; +import FormGroup from "@mui/material/FormGroup"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import { controlParameterIndexAnnotation } from "../types/JuceTypes.js"; + +export default 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 ( + + + + + + ); +} diff --git a/Assets/web/src/Components/JuceComboBox.js b/Assets/web/src/Components/JuceComboBox.js new file mode 100644 index 0000000..d1ad3d1 --- /dev/null +++ b/Assets/web/src/Components/JuceComboBox.js @@ -0,0 +1,64 @@ +import React, { useState, useEffect } from "react"; +import PropTypes from "prop-types"; +import Box from "@mui/material/Box"; +import * as Juce from "juce-framework-frontend"; +import Select from "@mui/material/Select"; +import InputLabel from "@mui/material/InputLabel"; +import MenuItem from "@mui/material/MenuItem"; +import FormControl from "@mui/material/FormControl"; +import { controlParameterIndexAnnotation } from "../types/JuceTypes.js"; +export default 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} + + + + ); +} diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index d4bd0d3..41758a9 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -161,6 +161,16 @@ WebViewPluginAudioProcessorEditor::WebViewPluginAudioProcessorEditor(WebViewPlug options = options.withOptionsFrom(*slider_relays.back()); } + for (auto& toggleId : p.parameters.toggleIds) { + toggle_relays.push_back(new WebToggleButtonRelay{ toggleId }); + toggle_attatchments.push_back(new + WebToggleButtonParameterAttachment( + *processorRef.state.getParameter(toggleId), + *toggle_relays.back(), + processorRef.state.undoManager)); + options = options.withOptionsFrom(*toggle_relays.back()); + } + webComponent = new SinglePageBrowser(options); addAndMakeVisible(*webComponent); diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 01eaf0d..e0c35df 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -38,6 +38,13 @@ public: for (auto& relays : slider_relays) { delete relays; } + + for (auto& attatchments : toggle_attatchments) { + delete attatchments; + } + for (auto& relays : toggle_relays) { + delete relays; + } } std::optional getResource(const String& url); @@ -83,6 +90,9 @@ private: std::vector slider_relays; std::vector< WebSliderParameterAttachment*> slider_attatchments; + std::vector toggle_relays; + std::vector< WebToggleButtonParameterAttachment*> toggle_attatchments; + WebControlParameterIndexReceiver controlParameterIndexReceiver; SinglePageBrowser* webComponent = nullptr; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index f8b278e..4f3de5e 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -67,6 +67,7 @@ void WebViewPluginAudioProcessor::processBlock(juce::AudioBuffer& buffer, shifter.SetAutoTuneDepth(state.getParameterAsValue("autoTuneDepth").getValue()); shifter.SetPortamentoTime(state.getParameterAsValue("portTime").getValue()); shifter.SetHarmonyMix(state.getParameterAsValue("harmonyMix").getValue()); + shifter.SetAutoTuneEnable(state.getParameterAsValue("autoTuneEnabled").getValue()); juce::AudioBuffer const_buff; const_buff.makeCopyOf(buffer); shifter.Process(const_buff.getArrayOfReadPointers(), (float**)buffer.getArrayOfWritePointers(), buffer.getNumSamples()); diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 825b81c..6031e52 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -87,13 +87,21 @@ public: NormalisableRange {0.001f, 0.2f, .001f}, .01f); + + toggleIds.push_back("autoTuneEnabled"); + addToLayout(layout, + ParameterID("autoTuneEnabled"), + "AutoTune Enabled", + false); } + /*AudioParameterFloat& formantPreserve; AudioParameterFloat& autoTuneSpeed; AudioParameterFloat& autoTuneDepth; AudioParameterFloat& portTime;*/ std::vector sliderIds; + std::vector toggleIds; /*AudioParameterBool& mute; AudioParameterChoice& filterType;*/ diff --git a/Source/Shifter.cpp b/Source/Shifter.cpp index 8b9d287..d59c6c0 100644 --- a/Source/Shifter.cpp +++ b/Source/Shifter.cpp @@ -12,16 +12,16 @@ void Shifter::SetAutoTuneSpeed(float val) { } void Shifter::SetAutoTuneDepth(float val) { - out_midi_smoother.SetDepth(val); + out_midi_smoother.SetDepth(val); } static inline float mtof(float m) { - return powf(2, (m - 69.0f) / 12.0f) * 440.0f; + return powf(2, (m - 69.0f) / 12.0f) * 440.0f; } static inline bool float_equal(float one, float two) { - return abs(one - two) < 1e-5f; + return abs(one - two) < 1e-5f; } void Shifter::Init(float samplerate, int samplesPerBlock) @@ -29,127 +29,127 @@ 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(samplerate); - } - for (int i = 0; i < BUFFER_SIZE; ++i) - { - in_buffer[i] = 0; - out_buffer[0][i] = 0; - out_buffer[1][i] = 0; - } - for (int i = 0; i < 8192; ++i) { - cos_lookup[i] = cos(2 * PI_F * i / 8192.0); - } + volume = 1; + helm.setframesize(1024); + helm.setoverlap(1); + for (int i = 0; i < MAX_VOICES; ++i) + { + voices[i].Init(samplerate); + } + for (int i = 0; i < BUFFER_SIZE; ++i) + { + in_buffer[i] = 0; + out_buffer[0][i] = 0; + out_buffer[1][i] = 0; + } + for (int i = 0; i < 8192; ++i) { + cos_lookup[i] = cos(2 * PI_F * i / 8192.0); + } } void Shifter::Process(const float* const* in, - float** out, - size_t size) + float** out, + size_t size) { - DetectPitch(in, out, size); - SetRates(); - // for (size_t i = 0; i < size; ++i) { - // out[0][i] = 0; - // } - GetSamples(out, in[0], size); - //for (size_t i = 0; i < size; ++i) - //{ - // // out[0][i] = osc.Process(); - // // out[0][i] = in[0][i]; - // //out[0][i] = out[0][i] + in[0][i]; - // out[1][i] = out[0][i]; - //} - // } + DetectPitch(in, out, size); + SetRates(); + // for (size_t i = 0; i < size; ++i) { + // out[0][i] = 0; + // } + GetSamples(out, in[0], size); + //for (size_t i = 0; i < size; ++i) + //{ + // // out[0][i] = osc.Process(); + // // out[0][i] = in[0][i]; + // //out[0][i] = out[0][i] + in[0][i]; + // out[1][i] = out[0][i]; + //} + // } } float findMedian(float a, float b, float c) { - if ((a >= b && a <= c) || (a <= b && a >= c)) - return a; - else if ((b >= a && b <= c) || (b <= a && b >= c)) - return b; - else - return c; + if ((a >= b && a <= c) || (a <= b && a >= c)) + return a; + else if ((b >= a && b <= c) || (b <= a && b >= c)) + return b; + else + return c; } void Shifter::DetectPitch(const float* const* in, float** out, size_t size) { - // detect current pitch - // pitch_detect.update(in[0], size); - // if(pitch_detect.available()) - // { - // float read = pitch_detect.read(); - // if(read >= 35 && read <= 2000) - // { - // for(int i = 2; i > 0; --i){ - // last_freqs[i] = last_freqs[i-1]; - // } - // last_freqs[0] = read; - // } - // } + // detect current pitch + // pitch_detect.update(in[0], size); + // if(pitch_detect.available()) + // { + // float read = pitch_detect.read(); + // if(read >= 35 && read <= 2000) + // { + // for(int i = 2; i > 0; --i){ + // last_freqs[i] = last_freqs[i-1]; + // } + // last_freqs[0] = read; + // } + // } - // current_pitch = findMedian(last_freqs[0], last_freqs[1], last_freqs[2]); - // in_period = 1.0 / current_pitch * 48000; + // current_pitch = findMedian(last_freqs[0], last_freqs[1], last_freqs[2]); + // in_period = 1.0 / current_pitch * 48000; - helm.iosamples(in[0], out[0], size); - float period = helm.getperiod(); - float fidel = helm.getfidelity(); - //DBG("frequency: " << 48000 / period << " fidel: " << fidel); + helm.iosamples(in[0], out[0], size); + float period = helm.getperiod(); + float fidel = helm.getfidelity(); + //DBG("frequency: " << 48000 / period << " fidel: " << fidel); - // Adjustable filter amount (0.0f = no filtering, 1.0f = max filtering) - static float in_period_filter_amount = 0.5f; // You can expose this as a parameter + // Adjustable filter amount (0.0f = no filtering, 1.0f = max filtering) + 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 = sample_rate_ / in_period; - - float midi = (12 * log2f(in_freq / 440) + 69.0f); + 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 = sample_rate_ / in_period; - //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)); + 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 = sample_rate_ / mtof(out_midi); } void Shifter::SetRates() {} float Shifter::GetOutputEnvelopePeriod(int out_voice) { - if (out_voice >= MAX_VOICES) { - return in_period * formant_preserve + out_period *(1.0 - formant_preserve); - } - //TODO add something so that low pitch ratios end up reducing formant_preservation - return in_period * formant_preserve + voices[out_voice].CurrentPeriod() * (1.0 - formant_preserve); + if (out_voice >= MAX_VOICES) { + return in_period * formant_preserve + out_period * (1.0 - formant_preserve); + } + //TODO add something so that low pitch ratios end up reducing formant_preservation + return in_period * formant_preserve + voices[out_voice].CurrentPeriod() * (1.0 - formant_preserve); } int Shifter::GetPeakIndex() { - int index = in_playhead - in_period * 2; - if (index < 0) - index += BUFFER_SIZE; + int index = in_playhead - in_period * 2; + if (index < 0) + index += BUFFER_SIZE; - //search for max absolute value - int max_index = -1; - float max_value = -2; - for (int j = 0; j < in_period; ++j) - { - //float val = fabs(in_buffer[index]); - float val = in_buffer[index]; - if (val > max_value) - { - max_index = index; - max_value = val; - } - if (++index >= BUFFER_SIZE) - { - index -= BUFFER_SIZE; - } - } - return max_index; + //search for max absolute value + int max_index = -1; + float max_value = -2; + for (int j = 0; j < in_period; ++j) + { + //float val = fabs(in_buffer[index]); + float val = in_buffer[index]; + if (val > max_value) + { + max_index = index; + max_value = val; + } + if (++index >= BUFFER_SIZE) + { + index -= BUFFER_SIZE; + } + } + return max_index; } //void Shifter::AddInterpolatedFrame(int voice, int max_index, float resampling_period) { @@ -187,135 +187,145 @@ int Shifter::GetPeakIndex() { //} void Shifter::AddInterpolatedFrame(int voice, int max_index, float resampling_period) { - float period_ratio = in_period / resampling_period; - float f_index; - f_index = max_index - in_period; - if (f_index < 0) - { - f_index += BUFFER_SIZE; - } - float mult = 0; - int out_index = out_playhead; - for (int j = 0; j < resampling_period * 2; ++j) - { - // mult = .5 - // * (1 - cosf(2 * PI_F * j / (period_to_use * 2 - 1))); - float interp = f_index - (int)f_index; - mult = .5 * (1 - cos_lookup[(int)((float)j / (resampling_period * 2.0) * 8191.0)]); - float value = ((1 - interp) * in_buffer[(int)f_index] + (interp)*in_buffer[(int)(f_index + 1) % 8192]) * mult; - if(voice >= MAX_VOICES) { - //value *= volume; - out_buffer[0][out_index] += value * melody_mix; - out_buffer[1][out_index] += value * melody_mix; - } else { - value *= voices[voice].CurrentAmplitude() * volume; - out_buffer[0][out_index] += value * voices[voice].GetPanning(0) * harmony_mix; - out_buffer[1][out_index] += value * voices[voice].GetPanning(1) * harmony_mix; + float period_ratio = in_period / resampling_period; + float f_index; + f_index = max_index - in_period; + if (f_index < 0) + { + f_index += BUFFER_SIZE; + } + float mult = 0; + int out_index = out_playhead; + for (int j = 0; j < resampling_period * 2; ++j) + { + // mult = .5 + // * (1 - cosf(2 * PI_F * j / (period_to_use * 2 - 1))); + float interp = f_index - (int)f_index; + mult = .5 * (1 - cos_lookup[(int)((float)j / (resampling_period * 2.0) * 8191.0)]); + float value = ((1 - interp) * in_buffer[(int)f_index] + (interp)*in_buffer[(int)(f_index + 1) % 8192]) * mult; + if (voice >= MAX_VOICES) { + //value *= volume; + out_buffer[0][out_index] += value * melody_mix; + out_buffer[1][out_index] += value * melody_mix; + } + else { + value *= voices[voice].CurrentAmplitude() * volume; + out_buffer[0][out_index] += value * voices[voice].GetPanning(0) * harmony_mix; + out_buffer[1][out_index] += value * voices[voice].GetPanning(1) * harmony_mix; } - - f_index += period_ratio; - if (f_index >= BUFFER_SIZE) - { - f_index -= BUFFER_SIZE; - } - if (++out_index >= BUFFER_SIZE) - { - out_index -= BUFFER_SIZE; - } - } + + f_index += period_ratio; + if (f_index >= BUFFER_SIZE) + { + f_index -= BUFFER_SIZE; + } + if (++out_index >= BUFFER_SIZE) + { + out_index -= BUFFER_SIZE; + } + } } void Shifter::GetSamples(float** output, const float* input, size_t size) { - for (int i = 0; i < size; ++i) - { + for (int i = 0; i < size; ++i) + { - //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; - if (voices[out_p].PeriodOverflow()) - { - float resampling_period = GetOutputEnvelopePeriod(out_p); - + //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; + if (voices[out_p].PeriodOverflow()) + { + float resampling_period = GetOutputEnvelopePeriod(out_p); - //find the start index + + //find the start index int max_index = GetPeakIndex(); - //add samples centered on that max + //add samples centered on that max AddInterpolatedFrame(out_p, max_index, resampling_period); - } - } - if (out_period_counter > out_period) - { - out_period_counter -= out_period; - float resampling_period = GetOutputEnvelopePeriod(MAX_VOICES); - + } + } + + if (out_period_counter > out_period) + { + out_period_counter -= out_period; + if (enable_autotune) { + float resampling_period = GetOutputEnvelopePeriod(MAX_VOICES); - //find the start index - int max_index = GetPeakIndex(); - //add samples centered on that max - AddInterpolatedFrame(MAX_VOICES, max_index, resampling_period); - } - //add input samples - in_buffer[in_playhead] = input[i]; + //find the start index + int max_index = GetPeakIndex(); - //output samples, set to 0 - for (int ch = 0; ch < 2; ++ch) { - output[ch][i] = out_buffer[ch][out_playhead]; - out_buffer[ch][out_playhead] = 0; - } - + //add samples centered on that max + AddInterpolatedFrame(MAX_VOICES, max_index, resampling_period); + } + } - //increment playheads - if (++in_playhead >= BUFFER_SIZE) - { - in_playhead -= BUFFER_SIZE; - } - if (++out_playhead >= BUFFER_SIZE) - { - out_playhead -= BUFFER_SIZE; - } - out_period_counter++; - } + + + //add input samples + in_buffer[in_playhead] = input[i]; + + //output samples, set to 0 + for (int ch = 0; ch < 2; ++ch) { + output[ch][i] = out_buffer[ch][out_playhead]; + if (!enable_autotune) { + output[ch][i] += input[i] * melody_mix; + } + out_buffer[ch][out_playhead] = 0; + } + + + //increment playheads + if (++in_playhead >= BUFFER_SIZE) + { + in_playhead -= BUFFER_SIZE; + } + if (++out_playhead >= BUFFER_SIZE) + { + out_playhead -= BUFFER_SIZE; + } + out_period_counter++; + } } void Shifter::AddMidiNote(int note) { - for (int i = 0; i < MAX_VOICES; ++i) { - if (voices[i].onoff_ && voices[i].GetMidiNote() == note) { - voices[i].Release(); - } - } - for (int i = 0; i < MAX_VOICES; ++i) { - if (!voices[i].onoff_) { + for (int i = 0; i < MAX_VOICES; ++i) { + if (voices[i].onoff_ && voices[i].GetMidiNote() == note) { + voices[i].Release(); + } + } + for (int i = 0; i < MAX_VOICES; ++i) { + if (!voices[i].onoff_) { voices[i].Trigger(note); - return; - } - } + return; + } + } } void Shifter::RemoveMidiNote(int note) { - for (int i = 0; i < MAX_VOICES; ++i) { - if (voices[i].GetMidiNote() == note) { - voices[i].Release(); - } - } + for (int i = 0; i < MAX_VOICES; ++i) { + if (voices[i].GetMidiNote() == note) { + voices[i].Release(); + } + } } void Shifter::SetHarmonyMix(float mix) { - if (mix < .5) { + if (mix < .5) { melody_mix = 1.0f; harmony_mix = mix * 2.0f; - } - else { + } + else { harmony_mix = 1.0f; melody_mix = (1.0f - mix) * 2.0f; - } + } } \ No newline at end of file diff --git a/Source/Shifter.h b/Source/Shifter.h index 243f8fc..54fdee1 100644 --- a/Source/Shifter.h +++ b/Source/Shifter.h @@ -157,6 +157,7 @@ public: float getInputPitch() const { return sample_rate_ / in_period; } float getOutputPitch() const { return sample_rate_ / out_period; } void SetHarmonyMix(float mix); + void SetAutoTuneEnable(bool enable) { enable_autotune = enable; } float out_midi = 40; ShifterVoice voices[MAX_VOICES]; @@ -219,6 +220,7 @@ private: float cos_lookup[8192]; float sample_rate_; int blocksize; + bool enable_autotune = false; MidiPitchSmoother out_midi_smoother; }; #endif \ No newline at end of file