#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(0.0f, 1.0f, 0.01f); auto minusOneOne = juce::NormalisableRange(-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 lfoDepthRange(0.0f, 1.0f, 0.001f, 2.0f); // OSC 1 layout.add(std::make_unique( 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::ParameterID{"osc1Oct", 1}, "OSC1 Octave", juce::NormalisableRange(-3.0f, 3.0f, 1.0f), 0.0f)); layout.add(std::make_unique( juce::ParameterID{"osc1Semi", 1}, "OSC1 Semi", juce::NormalisableRange(-12.0f, 12.0f, 1.0f), 0.0f)); layout.add(std::make_unique( juce::ParameterID{"osc1Fine", 1}, "OSC1 Fine", juce::NormalisableRange(-100.0f, 100.0f, 1.0f), 0.0f)); layout.add(std::make_unique( juce::ParameterID{"osc1Level", 1}, "OSC1 Level", zeroOne, 0.5f)); // OSC 2 layout.add(std::make_unique( 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::ParameterID{"osc2Oct", 1}, "OSC2 Octave", juce::NormalisableRange(-3.0f, 3.0f, 1.0f), -1.0f)); layout.add(std::make_unique( juce::ParameterID{"osc2Semi", 1}, "OSC2 Semi", juce::NormalisableRange(-12.0f, 12.0f, 1.0f), 0.0f)); layout.add(std::make_unique( juce::ParameterID{"osc2Fine", 1}, "OSC2 Fine", juce::NormalisableRange(-100.0f, 100.0f, 1.0f), 7.0f)); layout.add(std::make_unique( juce::ParameterID{"osc2Level", 1}, "OSC2 Level", zeroOne, 0.5f)); layout.add(std::make_unique( juce::ParameterID{"phaseOffset", 1}, "Phase Offset", zeroOne, 0.5f)); // FILTER layout.add(std::make_unique( juce::ParameterID{"filterType", 1}, "Filter Type", juce::StringArray{"LP 12dB", "LP 24dB", "Band Pass", "High Pass", "Notch"}, 0)); layout.add(std::make_unique( juce::ParameterID{"filterCutoff", 1}, "Filter Cutoff", juce::NormalisableRange( 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::ParameterID{"filterRes", 1}, "Filter Resonance", zeroOne, 0.2f)); layout.add(std::make_unique( juce::ParameterID{"filterEnvAmt", 1}, "Filter Env Amount", zeroOne, 0.0f)); layout.add(std::make_unique( juce::ParameterID{"keyTrack", 1}, "Key Tracking", zeroOne, 0.5f)); // FILTER ENVELOPE layout.add(std::make_unique( juce::ParameterID{"fEnvAttack", 1}, "Filter Attack", juce::NormalisableRange(0.001f, 5.0f, 0.001f, 0.3f), 0.01f)); layout.add(std::make_unique( juce::ParameterID{"fEnvDecay", 1}, "Filter Decay", juce::NormalisableRange(0.001f, 5.0f, 0.001f, 0.3f), 0.3f)); layout.add(std::make_unique( juce::ParameterID{"fEnvSustain", 1}, "Filter Sustain", zeroOne, 0.5f)); layout.add(std::make_unique( juce::ParameterID{"fEnvRelease", 1}, "Filter Release", juce::NormalisableRange(0.001f, 10.0f, 0.001f, 0.3f), 0.5f)); // AMP ENVELOPE layout.add(std::make_unique( juce::ParameterID{"envAttack", 1}, "Attack", juce::NormalisableRange(0.001f, 5.0f, 0.001f, 0.3f), 0.03f)); layout.add(std::make_unique( juce::ParameterID{"envDecay", 1}, "Decay", juce::NormalisableRange(0.001f, 5.0f, 0.001f, 0.3f), 0.3f)); layout.add(std::make_unique( juce::ParameterID{"envSustain", 1}, "Sustain", zeroOne, 0.7f)); layout.add(std::make_unique( juce::ParameterID{"envRelease", 1}, "Release", juce::NormalisableRange(0.001f, 10.0f, 0.001f, 0.3f), 0.5f)); // GLOBAL layout.add(std::make_unique( juce::ParameterID{"pan", 1}, "Pan", juce::NormalisableRange(-1.0f, 1.0f, 0.01f), 0.0f)); layout.add(std::make_unique( juce::ParameterID{"drive", 1}, "Drive", juce::NormalisableRange(1.0f, 5.0f, 0.01f, 0.5f), 1.2f)); layout.add(std::make_unique( juce::ParameterID{"masterLevel", 1}, "Master Level", zeroOne, 0.7f)); // PITCH BEND layout.add(std::make_unique( juce::ParameterID{"pitchBendRange", 1}, "Pitch Bend Range", juce::NormalisableRange(1.0f, 12.0f, 1.0f), 2.0f)); // TRANSPOSE layout.add(std::make_unique( juce::ParameterID{"octaveTranspose", 1}, "Octave Transpose", juce::StringArray{"-3", "-2", "-1", "0", "+1", "+2", "+3"}, 3)); layout.add(std::make_unique( juce::ParameterID{"semitoneTranspose", 1}, "Halftone Transpose", getSemitoneChoices(), 0)); // DISTORTION layout.add(std::make_unique( juce::ParameterID{"distType", 1}, "Dist Type", juce::StringArray{"Soft Clip", "Hard Clip", "Foldback", "Overdrive"}, 0)); layout.add(std::make_unique( juce::ParameterID{"distAmount", 1}, "Dist Amount", zeroOne, 0.0f)); layout.add(std::make_unique( juce::ParameterID{"distSymmetry", 1}, "Dist Symmetry", minusOneOne, 0.0f)); layout.add(std::make_unique( juce::ParameterID{"distTone", 1}, "Dist Tone", zeroOne, 1.0f)); // COMPRESSOR layout.add(std::make_unique( juce::ParameterID{"compThreshold", 1}, "Comp Threshold", juce::NormalisableRange(-60.0f, 0.0f, 0.1f), -20.0f)); layout.add(std::make_unique( juce::ParameterID{"compRatio", 1}, "Comp Ratio", juce::NormalisableRange(1.0f, 20.0f, 0.1f), 4.0f)); layout.add(std::make_unique( juce::ParameterID{"compAttack", 1}, "Comp Attack", juce::NormalisableRange(0.1f, 100.0f, 0.1f), 10.0f)); layout.add(std::make_unique( juce::ParameterID{"compRelease", 1}, "Comp Release", juce::NormalisableRange(10.0f, 1000.0f, 1.0f), 100.0f)); layout.add(std::make_unique( juce::ParameterID{"compMakeup", 1}, "Comp Makeup", juce::NormalisableRange(0.0f, 24.0f, 0.1f), 0.0f)); // AUTO-PAN layout.add(std::make_unique( juce::ParameterID{"autoPanRate", 1}, "Pan Rate", juce::NormalisableRange(0.05f, 20.0f, 0.01f, 0.4f), 2.0f)); layout.add(std::make_unique( juce::ParameterID{"autoPanDepth", 1}, "Pan Depth", zeroOne, 0.0f)); layout.add(std::make_unique( juce::ParameterID{"autoPanPhase", 1}, "Pan Phase", zeroOne, 0.0f)); // LIMITER layout.add(std::make_unique( juce::ParameterID{"limiterThreshold", 1}, "Limiter Threshold", juce::NormalisableRange(0.0f, 1.0f, 0.001f, 0.3f), 1.0f)); layout.add(std::make_unique( juce::ParameterID{"limiterCeiling", 1}, "Limiter Ceiling", juce::NormalisableRange(0.0f, 1.0f, 0.001f, 0.3f), 0.9f)); layout.add(std::make_unique( juce::ParameterID{"limiterRelease", 1}, "Limiter Release", juce::NormalisableRange(1.0f, 500.0f, 0.1f), 50.0f)); // LFO 1 layout.add(std::make_unique( juce::ParameterID{"lfo1Rate", 1}, "LFO1 Rate", juce::NormalisableRange(0.05f, 20.0f, 0.01f, 0.4f), 2.0f)); layout.add(std::make_unique( juce::ParameterID{"lfo1Depth", 1}, "LFO1 Depth", lfoDepthRange, 0.0f)); layout.add(std::make_unique( juce::ParameterID{"lfo1Shape", 1}, "LFO1 Shape", juce::StringArray{"Sine", "Triangle", "Saw", "Square"}, 0)); layout.add(std::make_unique( juce::ParameterID{"lfo1Dest", 1}, "LFO1 Dest", juce::StringArray{"Filter", "OSC1", "OSC2", "Both OSC", "OSC2 Phase"}, 0)); layout.add(std::make_unique( juce::ParameterID{"lfo1Sync", 1}, "LFO1 Sync", juce::StringArray{"Sync Off", "Sync On"}, 0)); layout.add(std::make_unique( juce::ParameterID{"lfo1Beat", 1}, "LFO1 Beat", juce::NormalisableRange(0.0f, static_cast(numBeats - 1), 1.0f), 4.0f, juce::AudioParameterFloatAttributes() .withStringFromValueFunction(beatValueToText))); // LFO 2 layout.add(std::make_unique( juce::ParameterID{"lfo2Rate", 1}, "LFO2 Rate", juce::NormalisableRange(0.05f, 20.0f, 0.01f, 0.4f), 2.0f)); layout.add(std::make_unique( juce::ParameterID{"lfo2Depth", 1}, "LFO2 Depth", lfoDepthRange, 0.0f)); layout.add(std::make_unique( juce::ParameterID{"lfo2Shape", 1}, "LFO2 Shape", juce::StringArray{"Sine", "Triangle", "Saw", "Square"}, 0)); layout.add(std::make_unique( juce::ParameterID{"lfo2Dest", 1}, "LFO2 Dest", juce::StringArray{"Filter", "OSC1", "OSC2", "Both OSC", "OSC2 Phase"}, 0)); layout.add(std::make_unique( juce::ParameterID{"lfo2Sync", 1}, "LFO2 Sync", juce::StringArray{"Sync Off", "Sync On"}, 0)); layout.add(std::make_unique( juce::ParameterID{"lfo2Beat", 1}, "LFO2 Beat", juce::NormalisableRange(0.0f, static_cast(numBeats - 1), 1.0f), 4.0f, juce::AudioParameterFloatAttributes() .withStringFromValueFunction(beatValueToText))); // DELAY layout.add(std::make_unique( juce::ParameterID{"delayTime", 1}, "Delay Time", juce::NormalisableRange(10.0f, 1000.0f, 1.0f, 0.3f), 200.0f)); layout.add(std::make_unique( juce::ParameterID{"delayFeedback", 1}, "Delay Feedback", zeroOne, 0.3f)); layout.add(std::make_unique( juce::ParameterID{"delayMix", 1}, "Delay Mix", zeroOne, 0.25f)); layout.add(std::make_unique( juce::ParameterID{"delaySync", 1}, "Delay Sync", juce::StringArray{"Sync Off", "Sync On"}, 0)); layout.add(std::make_unique( juce::ParameterID{"delayBeat", 1}, "Delay Beat", juce::NormalisableRange(0.0f, static_cast(numBeats - 1), 1.0f), 4.0f, juce::AudioParameterFloatAttributes() .withStringFromValueFunction(beatValueToText))); layout.add(std::make_unique( juce::ParameterID{"delayPingPong", 1}, "Delay Ping-Pong", juce::StringArray{"Off", "On"}, 0)); // REVERB layout.add(std::make_unique( juce::ParameterID{"reverbSize", 1}, "Reverb Size", zeroOne, 0.5f)); layout.add(std::make_unique( juce::ParameterID{"reverbDamping", 1}, "Reverb Damping", zeroOne, 0.5f)); layout.add(std::make_unique( juce::ParameterID{"reverbMix", 1}, "Reverb Mix", zeroOne, 0.2f)); 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; updateVoiceParameters(); } void ChromaFlockProcessor::releaseResources() {} void ChromaFlockProcessor::fadeOutActiveVoices(float seconds) { for (int i = 0; i < synth.getNumVoices(); ++i) { if (auto* v = dynamic_cast(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(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(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(currentBpm) / (60.0f * beatValues[bi]); } voice->setParameters( static_cast(static_cast(getParam("osc1Wave"))), static_cast(static_cast(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(static_cast(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(getParam("lfo1Shape")), static_cast(getParam("lfo1Dest")), l2Rate, getParam("lfo2Depth"), static_cast(getParam("lfo2Shape")), static_cast(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& 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(); for (const auto metadata : midiMessages) if (metadata.getMessage().isNoteOn()) activeNotes[metadata.getMessage().getNoteNumber()].store(true, std::memory_order_relaxed); else if (metadata.getMessage().isNoteOff()) activeNotes[metadata.getMessage().getNoteNumber()].store(false, std::memory_order_relaxed); int transpose = getTransposeSemitones(); juce::MidiBuffer transposed; for (const auto metadata : midiMessages) { auto msg = metadata.getMessage(); if (msg.isNoteOn() || msg.isNoteOff()) msg.setNoteNumber(juce::jlimit(0, 127, msg.getNoteNumber() + transpose)); transposed.addEvent(msg, metadata.samplePosition); } 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(static_cast(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(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(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(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 xml(state.createXml()); copyXmlToBinary(*xml, destData); } void ChromaFlockProcessor::setStateInformation(const void* data, int sizeInBytes) { std::unique_ptr 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(); }