mirror of
https://codeberg.org/armin/chromaflock.git
synced 2026-09-01 04:10:47 +02:00
629 lines
28 KiB
C++
629 lines
28 KiB
C++
#include "PluginProcessor.h"
|
|
#include "PluginEditor.h"
|
|
|
|
// Beat-sync multipliers and their display labels (period = multiplier * beat).
|
|
static const float beatValues[] = {0.25f, 0.3333333f, 0.5f, 0.75f, 1.0f, 1.5f, 2.0f};
|
|
static const char* beatLabels[] = {"1/4", "1/3", "1/2", "3/4", "1.0", "1.5", "2.0"};
|
|
static const int numBeats = 7;
|
|
|
|
// Halftone transpose menu: "0" plus -12..-1 and +1..+12.
|
|
const juce::StringArray& getSemitoneChoices() {
|
|
static const juce::StringArray s = [] {
|
|
juce::StringArray a;
|
|
a.add("0");
|
|
for (int i = 12; i >= 1; --i) a.add("-" + juce::String(i));
|
|
for (int i = 1; i <= 12; ++i) a.add("+" + juce::String(i));
|
|
return a;
|
|
}();
|
|
return s;
|
|
}
|
|
static int semitoneFromIndex(int idx) {
|
|
if (idx <= 0) return 0;
|
|
if (idx <= 12) return -(13 - idx); // idx 1..12 -> -12..-1
|
|
return idx - 12; // idx 13..24 -> +1..+12
|
|
}
|
|
|
|
static juce::String beatValueToText(float v, int) {
|
|
int i = juce::jlimit(0, numBeats - 1, juce::roundToInt(v));
|
|
return juce::String(beatLabels[i]);
|
|
}
|
|
|
|
ChromaFlockProcessor::ChromaFlockProcessor()
|
|
: AudioProcessor(BusesProperties()
|
|
.withOutput("Output", juce::AudioChannelSet::stereo(), true)),
|
|
apvts(*this, nullptr, "Parameters", createParameterLayout()) {
|
|
synth.addSound(new SubtractiveSound());
|
|
for (int i = 0; i < maxVoices; ++i)
|
|
synth.addVoice(new SubtractiveVoice());
|
|
}
|
|
|
|
ChromaFlockProcessor::~ChromaFlockProcessor() {}
|
|
|
|
juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::createParameterLayout() {
|
|
juce::AudioProcessorValueTreeState::ParameterLayout layout;
|
|
|
|
auto zeroOne = juce::NormalisableRange<float>(0.0f, 1.0f, 0.01f);
|
|
auto minusOneOne = juce::NormalisableRange<float>(-1.0f, 1.0f, 0.01f);
|
|
|
|
// LFO1 depth: fine 0.001 step, with a skew of 2.0 (value = p^2) so the
|
|
// lower knob range resolves much finer and the 0.001 step only applies
|
|
// in the upper range.
|
|
juce::NormalisableRange<float> lfoDepthRange(0.0f, 1.0f, 0.001f, 2.0f);
|
|
|
|
// OSC 1
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"osc1Wave", 1}, "OSC1 Wave",
|
|
juce::StringArray{"Sine", "Saw", "Square", "Triangle", "Noise",
|
|
"Pulse 25%", "Pulse 12%", "Rev Saw", "Full Rect", "Half Rect",
|
|
"Stair 4", "Stair 8", "Soft Sine", "Sinc", "Exp Pulse", "Double Sine", "SuperSaw"}, 1));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"osc1Oct", 1}, "OSC1 Octave",
|
|
juce::NormalisableRange<float>(-3.0f, 3.0f, 1.0f), 0.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"osc1Semi", 1}, "OSC1 Semi",
|
|
juce::NormalisableRange<float>(-12.0f, 12.0f, 1.0f), 0.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"osc1Fine", 1}, "OSC1 Fine",
|
|
juce::NormalisableRange<float>(-100.0f, 100.0f, 1.0f), 0.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"osc1Level", 1}, "OSC1 Level", zeroOne, 0.5f));
|
|
|
|
// OSC 2
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"osc2Wave", 1}, "OSC2 Wave",
|
|
juce::StringArray{"Sine", "Saw", "Square", "Triangle", "Noise",
|
|
"Pulse 25%", "Pulse 12%", "Rev Saw", "Full Rect", "Half Rect",
|
|
"Stair 4", "Stair 8", "Soft Sine", "Sinc", "Exp Pulse", "Double Sine", "SuperSaw"}, 3));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"osc2Oct", 1}, "OSC2 Octave",
|
|
juce::NormalisableRange<float>(-3.0f, 3.0f, 1.0f), -1.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"osc2Semi", 1}, "OSC2 Semi",
|
|
juce::NormalisableRange<float>(-12.0f, 12.0f, 1.0f), 0.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"osc2Fine", 1}, "OSC2 Fine",
|
|
juce::NormalisableRange<float>(-100.0f, 100.0f, 1.0f), 7.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"osc2Level", 1}, "OSC2 Level", zeroOne, 0.5f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"phaseOffset", 1}, "Phase Offset", zeroOne, 0.5f));
|
|
|
|
// FILTER
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"filterType", 1}, "Filter Type",
|
|
juce::StringArray{"LP 12dB", "LP 24dB", "Band Pass", "High Pass", "Notch"}, 0));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"filterCutoff", 1}, "Filter Cutoff",
|
|
juce::NormalisableRange<float>(
|
|
20.0f, 20000.0f,
|
|
[](float start, float end, float n) { return start * std::pow(end / start, n); },
|
|
[](float start, float end, float v) { return std::log(v / start) / std::log(end / start); },
|
|
[](float, float, float v) { return v; }),
|
|
8000.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"filterRes", 1}, "Filter Resonance", zeroOne, 0.2f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"filterEnvAmt", 1}, "Filter Env Amount", zeroOne, 0.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"keyTrack", 1}, "Key Tracking", zeroOne, 0.5f));
|
|
|
|
// FILTER ENVELOPE
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"fEnvAttack", 1}, "Filter Attack",
|
|
juce::NormalisableRange<float>(0.001f, 5.0f, 0.001f, 0.3f), 0.01f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"fEnvDecay", 1}, "Filter Decay",
|
|
juce::NormalisableRange<float>(0.001f, 5.0f, 0.001f, 0.3f), 0.3f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"fEnvSustain", 1}, "Filter Sustain", zeroOne, 0.5f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"fEnvRelease", 1}, "Filter Release",
|
|
juce::NormalisableRange<float>(0.001f, 10.0f, 0.001f, 0.3f), 0.5f));
|
|
|
|
// AMP ENVELOPE
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"envAttack", 1}, "Attack",
|
|
juce::NormalisableRange<float>(0.001f, 5.0f, 0.001f, 0.3f), 0.03f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"envDecay", 1}, "Decay",
|
|
juce::NormalisableRange<float>(0.001f, 5.0f, 0.001f, 0.3f), 0.3f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"envSustain", 1}, "Sustain", zeroOne, 0.7f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"envRelease", 1}, "Release",
|
|
juce::NormalisableRange<float>(0.001f, 10.0f, 0.001f, 0.3f), 0.5f));
|
|
|
|
// GLOBAL
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"pan", 1}, "Pan",
|
|
juce::NormalisableRange<float>(-1.0f, 1.0f, 0.01f), 0.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"drive", 1}, "Drive",
|
|
juce::NormalisableRange<float>(1.0f, 5.0f, 0.01f, 0.5f), 1.2f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"masterLevel", 1}, "Master Level", zeroOne, 0.7f));
|
|
|
|
// PITCH BEND
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"pitchBendRange", 1}, "Pitch Bend Range",
|
|
juce::NormalisableRange<float>(1.0f, 12.0f, 1.0f), 2.0f));
|
|
|
|
// TRANSPOSE
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"octaveTranspose", 1}, "Octave Transpose",
|
|
juce::StringArray{"-3", "-2", "-1", "0", "+1", "+2", "+3"}, 3));
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"semitoneTranspose", 1}, "Halftone Transpose",
|
|
getSemitoneChoices(), 0));
|
|
|
|
// DISTORTION
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"distType", 1}, "Dist Type",
|
|
juce::StringArray{"Soft Clip", "Hard Clip", "Foldback", "Overdrive"}, 0));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"distAmount", 1}, "Dist Amount", zeroOne, 0.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"distSymmetry", 1}, "Dist Symmetry", minusOneOne, 0.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"distTone", 1}, "Dist Tone", zeroOne, 1.0f));
|
|
|
|
// COMPRESSOR
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"compThreshold", 1}, "Comp Threshold",
|
|
juce::NormalisableRange<float>(-60.0f, 0.0f, 0.1f), -20.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"compRatio", 1}, "Comp Ratio",
|
|
juce::NormalisableRange<float>(1.0f, 20.0f, 0.1f), 4.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"compAttack", 1}, "Comp Attack",
|
|
juce::NormalisableRange<float>(0.1f, 100.0f, 0.1f), 10.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"compRelease", 1}, "Comp Release",
|
|
juce::NormalisableRange<float>(10.0f, 1000.0f, 1.0f), 100.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"compMakeup", 1}, "Comp Makeup",
|
|
juce::NormalisableRange<float>(0.0f, 24.0f, 0.1f), 0.0f));
|
|
|
|
// AUTO-PAN
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"autoPanRate", 1}, "Pan Rate",
|
|
juce::NormalisableRange<float>(0.05f, 20.0f, 0.01f, 0.4f), 2.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"autoPanDepth", 1}, "Pan Depth", zeroOne, 0.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"autoPanPhase", 1}, "Pan Phase", zeroOne, 0.0f));
|
|
|
|
// LIMITER
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"limiterThreshold", 1}, "Limiter Threshold",
|
|
juce::NormalisableRange<float>(0.0f, 1.0f, 0.001f, 0.3f), 1.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"limiterCeiling", 1}, "Limiter Ceiling",
|
|
juce::NormalisableRange<float>(0.0f, 1.0f, 0.001f, 0.3f), 0.9f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"limiterRelease", 1}, "Limiter Release",
|
|
juce::NormalisableRange<float>(1.0f, 500.0f, 0.1f), 50.0f));
|
|
|
|
// LFO 1
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"lfo1Rate", 1}, "LFO1 Rate",
|
|
juce::NormalisableRange<float>(0.05f, 20.0f, 0.01f, 0.4f), 2.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"lfo1Depth", 1}, "LFO1 Depth", lfoDepthRange, 0.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"lfo1Shape", 1}, "LFO1 Shape",
|
|
juce::StringArray{"Sine", "Triangle", "Saw", "Square"}, 0));
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"lfo1Dest", 1}, "LFO1 Dest",
|
|
juce::StringArray{"Filter", "OSC1", "OSC2", "Both OSC", "OSC2 Phase"}, 0));
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"lfo1Sync", 1}, "LFO1 Sync",
|
|
juce::StringArray{"Sync Off", "Sync On"}, 0));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"lfo1Beat", 1}, "LFO1 Beat",
|
|
juce::NormalisableRange<float>(0.0f, static_cast<float>(numBeats - 1), 1.0f), 4.0f,
|
|
juce::AudioParameterFloatAttributes()
|
|
.withStringFromValueFunction(beatValueToText)));
|
|
|
|
// LFO 2
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"lfo2Rate", 1}, "LFO2 Rate",
|
|
juce::NormalisableRange<float>(0.05f, 20.0f, 0.01f, 0.4f), 2.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"lfo2Depth", 1}, "LFO2 Depth", lfoDepthRange, 0.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"lfo2Shape", 1}, "LFO2 Shape",
|
|
juce::StringArray{"Sine", "Triangle", "Saw", "Square"}, 0));
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"lfo2Dest", 1}, "LFO2 Dest",
|
|
juce::StringArray{"Filter", "OSC1", "OSC2", "Both OSC", "OSC2 Phase"}, 0));
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"lfo2Sync", 1}, "LFO2 Sync",
|
|
juce::StringArray{"Sync Off", "Sync On"}, 0));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"lfo2Beat", 1}, "LFO2 Beat",
|
|
juce::NormalisableRange<float>(0.0f, static_cast<float>(numBeats - 1), 1.0f), 4.0f,
|
|
juce::AudioParameterFloatAttributes()
|
|
.withStringFromValueFunction(beatValueToText)));
|
|
|
|
// DELAY
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"delayTime", 1}, "Delay Time",
|
|
juce::NormalisableRange<float>(10.0f, 1000.0f, 1.0f, 0.3f), 200.0f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"delayFeedback", 1}, "Delay Feedback", zeroOne, 0.3f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"delayMix", 1}, "Delay Mix", zeroOne, 0.25f));
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"delaySync", 1}, "Delay Sync",
|
|
juce::StringArray{"Sync Off", "Sync On"}, 0));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"delayBeat", 1}, "Delay Beat",
|
|
juce::NormalisableRange<float>(0.0f, static_cast<float>(numBeats - 1), 1.0f), 4.0f,
|
|
juce::AudioParameterFloatAttributes()
|
|
.withStringFromValueFunction(beatValueToText)));
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"delayPingPong", 1}, "Delay Ping-Pong",
|
|
juce::StringArray{"Off", "On"}, 0));
|
|
|
|
// REVERB
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"reverbSize", 1}, "Reverb Size", zeroOne, 0.5f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"reverbDamping", 1}, "Reverb Damping", zeroOne, 0.5f));
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
|
juce::ParameterID{"reverbMix", 1}, "Reverb Mix", zeroOne, 0.2f));
|
|
|
|
// ARPEGGIATOR
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"arpEnabled", 1}, "Arp On",
|
|
juce::StringArray{"Off", "On"}, 0));
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"arpPattern", 1}, "Arp Pattern",
|
|
juce::StringArray{"Up / Minor", "Down / Minor", "Up & Down / Major",
|
|
"Down & Up / Major", "Random", "As Played", "Chord", "Up & Down X",
|
|
"Down & Up X", "Random Once", "Octave Up", "Octave Down", "Pinky Up",
|
|
"Pinky Down", "Up / Major", "Down / Major", "Up & Down / Minor",
|
|
"C3/C4 1", "C3/C4 2", "C3/C4 3", "C3/C4 4", "C3/C4 5"}, 0));
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"arpOctaves", 1}, "Arp Octaves",
|
|
juce::StringArray{"1", "2", "3"}, 1));
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"arpDirection", 1}, "Arp Direction",
|
|
juce::StringArray{"Up", "Down"}, 0));
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
|
juce::ParameterID{"arpRate", 1}, "Arp Rate",
|
|
juce::StringArray{"1/32", "1/16", "1/8", "1/4", "1/2", "1", "2"}, 1));
|
|
|
|
return layout;
|
|
}
|
|
|
|
void ChromaFlockProcessor::prepareToPlay(double sampleRate, int samplesPerBlock) {
|
|
currentSampleRate = sampleRate;
|
|
synth.setCurrentPlaybackSampleRate(sampleRate);
|
|
distortion.prepare(sampleRate);
|
|
compression.prepare(sampleRate);
|
|
limiter.prepare(sampleRate);
|
|
delay.prepare(sampleRate);
|
|
reverbFX.prepare(sampleRate);
|
|
autoPanPhase = 0.0f;
|
|
arpSampleCount = 0.0;
|
|
std::vector<int> arpStop;
|
|
arp.reset(arpStop);
|
|
updateVoiceParameters();
|
|
}
|
|
|
|
void ChromaFlockProcessor::releaseResources() {}
|
|
|
|
void ChromaFlockProcessor::fadeOutActiveVoices(float seconds) {
|
|
for (int i = 0; i < synth.getNumVoices(); ++i) {
|
|
if (auto* v = dynamic_cast<SubtractiveVoice*>(synth.getVoice(i))) {
|
|
if (v->isVoiceActive())
|
|
v->triggerQuickRelease(seconds);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ChromaFlockProcessor::updateVoiceParameters() {
|
|
auto getParam = [&](const juce::String& id) -> float {
|
|
auto* p = apvts.getRawParameterValue(id);
|
|
return p != nullptr ? p->load() : 0.0f;
|
|
};
|
|
|
|
for (int i = 0; i < synth.getNumVoices(); ++i) {
|
|
if (auto* voice = dynamic_cast<SubtractiveVoice*>(synth.getVoice(i))) {
|
|
float pbRange = getParam("pitchBendRange");
|
|
voice->setPitchBend(getPitchBend() * pbRange);
|
|
|
|
float l1Rate = getParam("lfo1Rate");
|
|
if (getParam("lfo1Sync") > 0.5f) {
|
|
int bi = juce::jlimit(0, numBeats - 1, juce::roundToInt(getParam("lfo1Beat")));
|
|
l1Rate = static_cast<float>(currentBpm) / (60.0f * beatValues[bi]);
|
|
}
|
|
float l2Rate = getParam("lfo2Rate");
|
|
if (getParam("lfo2Sync") > 0.5f) {
|
|
int bi = juce::jlimit(0, numBeats - 1, juce::roundToInt(getParam("lfo2Beat")));
|
|
l2Rate = static_cast<float>(currentBpm) / (60.0f * beatValues[bi]);
|
|
}
|
|
|
|
voice->setParameters(
|
|
static_cast<Waveform>(static_cast<int>(getParam("osc1Wave"))),
|
|
static_cast<Waveform>(static_cast<int>(getParam("osc2Wave"))),
|
|
getParam("osc1Oct"),
|
|
getParam("osc2Oct"),
|
|
getParam("osc1Semi"),
|
|
getParam("osc2Semi"),
|
|
getParam("osc1Fine"),
|
|
getParam("osc2Fine"),
|
|
getParam("osc1Level"),
|
|
getParam("osc2Level"),
|
|
getParam("phaseOffset"),
|
|
getParam("filterCutoff"),
|
|
getParam("filterRes"),
|
|
static_cast<FilterType>(static_cast<int>(getParam("filterType"))),
|
|
getParam("filterEnvAmt"),
|
|
getParam("keyTrack"),
|
|
getParam("envAttack"),
|
|
getParam("envDecay"),
|
|
getParam("envSustain"),
|
|
getParam("envRelease"),
|
|
getParam("fEnvAttack"),
|
|
getParam("fEnvDecay"),
|
|
getParam("fEnvSustain"),
|
|
getParam("fEnvRelease"),
|
|
getParam("pan"),
|
|
getParam("drive"),
|
|
1.0f,
|
|
l1Rate,
|
|
getParam("lfo1Depth"),
|
|
static_cast<int>(getParam("lfo1Shape")),
|
|
static_cast<int>(getParam("lfo1Dest")),
|
|
l2Rate,
|
|
getParam("lfo2Depth"),
|
|
static_cast<int>(getParam("lfo2Shape")),
|
|
static_cast<int>(getParam("lfo2Dest"))
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
int ChromaFlockProcessor::getTransposeSemitones() const {
|
|
auto* op = apvts.getRawParameterValue("octaveTranspose");
|
|
auto* sp = apvts.getRawParameterValue("semitoneTranspose");
|
|
int octVal = (op != nullptr ? juce::roundToInt(op->load()) : 3) - 3; // index 0..6 -> -3..+3
|
|
int semiVal = (sp != nullptr ? semitoneFromIndex(juce::roundToInt(sp->load())) : 0);
|
|
return octVal * 12 + semiVal;
|
|
}
|
|
|
|
void ChromaFlockProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) {
|
|
juce::ScopedNoDenormals noDenormals;
|
|
|
|
if (auto* playHead = getPlayHead()) {
|
|
if (auto position = playHead->getPosition())
|
|
if (auto bpm = position->getBpm())
|
|
currentBpm = *bpm;
|
|
}
|
|
|
|
buffer.clear();
|
|
updateVoiceParameters();
|
|
|
|
auto getRaw = [&](const juce::String& id) -> float {
|
|
auto* p = apvts.getRawParameterValue(id);
|
|
return p != nullptr ? p->load() : 0.0f;
|
|
};
|
|
|
|
bool arpOn = getRaw("arpEnabled") > 0.5f;
|
|
if (arpOn) {
|
|
static const double arpRateBeats[] = {0.125, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0};
|
|
int rateIdx = juce::jlimit(0, 6, juce::roundToInt(getRaw("arpRate")));
|
|
arp.setParameters(true,
|
|
juce::roundToInt(getRaw("arpPattern")),
|
|
juce::roundToInt(getRaw("arpOctaves")) + 1,
|
|
juce::roundToInt(getRaw("arpDirection")),
|
|
arpRateBeats[rateIdx],
|
|
currentBpm);
|
|
} else {
|
|
arp.setParameters(false, 0, 1, 0, 0.5, currentBpm);
|
|
}
|
|
|
|
int transpose = getTransposeSemitones();
|
|
juce::MidiBuffer transposed;
|
|
for (const auto metadata : midiMessages) {
|
|
auto msg = metadata.getMessage();
|
|
if (msg.isNoteOn()) {
|
|
int note = msg.getNoteNumber();
|
|
activeNotes[note].store(true, std::memory_order_relaxed);
|
|
if (arpOn)
|
|
arp.noteOn(note, msg.getFloatVelocity());
|
|
else {
|
|
msg.setNoteNumber(juce::jlimit(0, 127, note + transpose));
|
|
transposed.addEvent(msg, metadata.samplePosition);
|
|
}
|
|
} else if (msg.isNoteOff()) {
|
|
int note = msg.getNoteNumber();
|
|
activeNotes[note].store(false, std::memory_order_relaxed);
|
|
if (arpOn)
|
|
arp.noteOff(note);
|
|
else {
|
|
msg.setNoteNumber(juce::jlimit(0, 127, note + transpose));
|
|
transposed.addEvent(msg, metadata.samplePosition);
|
|
}
|
|
} else {
|
|
transposed.addEvent(msg, metadata.samplePosition);
|
|
}
|
|
}
|
|
|
|
if (arpOn) {
|
|
// A freshly pressed key triggers the first note immediately; the
|
|
// clock then starts from zero so the subsequent ticks stay synced.
|
|
if (arp.shouldTriggerNow()) {
|
|
std::vector<int> stopNotes;
|
|
std::vector<Arpeggiator::NotePitch> playNotes;
|
|
arp.step(stopNotes, playNotes);
|
|
for (int n : stopNotes)
|
|
synth.noteOff(1, juce::jlimit(0, 127, n + transpose), 0.0f, true);
|
|
for (const auto& np : playNotes)
|
|
synth.noteOn(1, juce::jlimit(0, 127, np.note + transpose), np.velocity);
|
|
arpSampleCount = 0.0;
|
|
}
|
|
|
|
double tickSamples = arp.getTickSeconds() * currentSampleRate;
|
|
if (tickSamples < 1.0) tickSamples = 1.0;
|
|
arpSampleCount += buffer.getNumSamples();
|
|
while (arpSampleCount >= tickSamples) {
|
|
arpSampleCount -= tickSamples;
|
|
std::vector<int> stopNotes;
|
|
std::vector<Arpeggiator::NotePitch> playNotes;
|
|
arp.step(stopNotes, playNotes);
|
|
for (int n : stopNotes)
|
|
synth.noteOff(1, juce::jlimit(0, 127, n + transpose), 0.0f, true);
|
|
for (const auto& np : playNotes)
|
|
synth.noteOn(1, juce::jlimit(0, 127, np.note + transpose), np.velocity);
|
|
}
|
|
} else {
|
|
arpSampleCount = 0.0;
|
|
std::vector<int> stopNotes;
|
|
arp.reset(stopNotes);
|
|
for (int n : stopNotes)
|
|
synth.noteOff(1, juce::jlimit(0, 127, n + transpose), 0.0f, true);
|
|
}
|
|
|
|
synth.renderNextBlock(buffer, transposed, 0, buffer.getNumSamples());
|
|
|
|
// Read FX params
|
|
auto getParam = [&](const juce::String& id) -> float {
|
|
auto* p = apvts.getRawParameterValue(id);
|
|
return p != nullptr ? p->load() : 0.0f;
|
|
};
|
|
|
|
distortion.setParameters(
|
|
static_cast<DistortionType>(static_cast<int>(getParam("distType"))),
|
|
getParam("distAmount"),
|
|
getParam("distSymmetry"),
|
|
getParam("distTone"));
|
|
|
|
compression.setParameters(
|
|
getParam("compThreshold"),
|
|
getParam("compRatio"),
|
|
getParam("compAttack"),
|
|
getParam("compRelease"),
|
|
getParam("compMakeup"));
|
|
|
|
float panRate = getParam("autoPanRate");
|
|
float panDepth = getParam("autoPanDepth");
|
|
autoPanPhaseOffset = getParam("autoPanPhase");
|
|
|
|
float delayTimeMs = getParam("delayTime");
|
|
if (getParam("delaySync") > 0.5f) {
|
|
int bi = juce::jlimit(0, numBeats - 1, juce::roundToInt(getParam("delayBeat")));
|
|
delayTimeMs = beatValues[bi] * 60000.0f / static_cast<float>(currentBpm);
|
|
}
|
|
delay.setParameters(delayTimeMs, getParam("delayFeedback"), getParam("delayMix"),
|
|
getParam("delayPingPong") > 0.5f);
|
|
reverbFX.setParameters(getParam("reverbSize"), getParam("reverbDamping"), 0.8f, getParam("reverbMix"));
|
|
limiter.setParameters(getParam("limiterThreshold"), getParam("limiterCeiling"),
|
|
getParam("limiterRelease"));
|
|
|
|
// Master fader applies a natural (perceptual) taper and is the final gain
|
|
// stage, so it always controls output level regardless of the limiter.
|
|
float masterValue = getParam("masterLevel");
|
|
float masterGain = masterValue * masterValue;
|
|
|
|
// FX processing: distortion → compression → auto-pan
|
|
int numSamples = buffer.getNumSamples();
|
|
float* leftData = buffer.getWritePointer(0);
|
|
float* rightData = buffer.getNumChannels() > 1 ? buffer.getWritePointer(1) : leftData;
|
|
|
|
float twoPi = 6.2831853f;
|
|
for (int i = 0; i < numSamples; ++i) {
|
|
float left = leftData[i];
|
|
float right = rightData[i];
|
|
|
|
// Distortion (stereo)
|
|
left = distortion.process(left);
|
|
right = distortion.process(right);
|
|
|
|
// Compression (stereo)
|
|
left = compression.process(left);
|
|
right = compression.process(right);
|
|
|
|
// Brickwall limiter (stereo)
|
|
left = limiter.process(left);
|
|
right = limiter.process(right);
|
|
|
|
// Auto-pan
|
|
if (panDepth > 0.001f) {
|
|
float panLfo = std::sin((autoPanPhase + autoPanPhaseOffset) * twoPi);
|
|
float leftGain = 1.0f - panDepth * 0.5f * (1.0f + panLfo);
|
|
float rightGain = 1.0f - panDepth * 0.5f * (1.0f - panLfo);
|
|
left *= leftGain;
|
|
right *= rightGain;
|
|
autoPanPhase += panRate / static_cast<float>(currentSampleRate);
|
|
if (autoPanPhase >= 1.0f) autoPanPhase -= 1.0f;
|
|
}
|
|
|
|
// Delay
|
|
delay.process(left, right);
|
|
|
|
leftData[i] = left;
|
|
rightData[i] = right;
|
|
}
|
|
|
|
// Reverb (block-based)
|
|
reverbFX.process(leftData, rightData, numSamples);
|
|
|
|
// Master volume — final stage, after limiter/reverb.
|
|
for (int i = 0; i < numSamples; ++i) {
|
|
leftData[i] *= masterGain;
|
|
rightData[i] *= masterGain;
|
|
}
|
|
|
|
// Metering
|
|
float sumSquares = 0.0f;
|
|
float peak = 0.0f;
|
|
for (int i = 0; i < numSamples; ++i) {
|
|
float mix = (leftData[i] + rightData[i]) * 0.5f;
|
|
|
|
int writePos = scopeWritePos.load(std::memory_order_relaxed);
|
|
scopeBuffer[writePos] = mix;
|
|
scopeWritePos.store((writePos + 1) % scopeBufferSize, std::memory_order_release);
|
|
|
|
int fftPos = fftWritePos.load(std::memory_order_relaxed);
|
|
fftInput[fftPos] = mix;
|
|
fftWritePos.store((fftPos + 1) % fftSize, std::memory_order_release);
|
|
|
|
float absMix = std::abs(mix);
|
|
if (absMix > peak) peak = absMix;
|
|
sumSquares += mix * mix;
|
|
}
|
|
|
|
float rms = std::sqrt(sumSquares / static_cast<float>(numSamples));
|
|
float level = juce::jmax(rms, peak);
|
|
level = std::min(level * 1.5f, 1.0f);
|
|
|
|
float prev = rmsLevel.load(std::memory_order_relaxed);
|
|
if (level > prev)
|
|
rmsLevel.store(level, std::memory_order_relaxed);
|
|
else
|
|
rmsLevel.store(prev * 0.95f, std::memory_order_relaxed);
|
|
}
|
|
|
|
juce::AudioProcessorEditor* ChromaFlockProcessor::createEditor() {
|
|
return new ChromaFlockEditor(*this);
|
|
}
|
|
|
|
void ChromaFlockProcessor::getStateInformation(juce::MemoryBlock& destData) {
|
|
auto state = apvts.copyState();
|
|
std::unique_ptr<juce::XmlElement> xml(state.createXml());
|
|
copyXmlToBinary(*xml, destData);
|
|
}
|
|
|
|
void ChromaFlockProcessor::setStateInformation(const void* data, int sizeInBytes) {
|
|
std::unique_ptr<juce::XmlElement> xml(getXmlFromBinary(data, sizeInBytes));
|
|
if (xml && xml->hasTagName(apvts.state.getType()))
|
|
apvts.replaceState(juce::ValueTree::fromXml(*xml));
|
|
}
|
|
|
|
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() {
|
|
return new ChromaFlockProcessor();
|
|
}
|