chromaflock/Source/DSP/Voice.h
Armin a1cd68b6b3 Fix filter env: restore unipolar modulation so all env knobs are responsive
Reverts the sustain-relative modulation that made F.ENV SUS and the
cutoff knob feel dead while a note was held. The envelope now modulates
up from the base cutoff; at env amount 0 the cutoff equals the knob, and
lowering env amount darkens the held tone as expected.

Also extends the filter to 24/48 dB modes (extra TPT stages), adds the
matching combobox options, gives knobs a real blurred drop shadow, and
updates the AGENTS.md filterType choice count.
2026-08-13 14:15:41 +02:00

269 lines
9.7 KiB
C++

#pragma once
#include "Oscillator.h"
#include "Filter.h"
#include "Envelope.h"
#include "LFO.h"
#include <cmath>
#include <algorithm>
struct SubtractiveSound : public juce::SynthesiserSound {
bool appliesToNote(int /*midiNoteNumber*/) override { return true; }
bool appliesToChannel(int /*midiChannel*/) override { return true; }
};
class SubtractiveVoice : public juce::SynthesiserVoice {
public:
SubtractiveVoice() = default;
bool canPlaySound(juce::SynthesiserSound* sound) override {
return dynamic_cast<SubtractiveSound*>(sound) != nullptr;
}
void startNote(int midiNoteNumber, float velocity,
juce::SynthesiserSound* sound, int currentPitchWheelPosition) override {
currentMidiNote = midiNoteNumber;
noteVelocity = velocity;
double sr = getSampleRate();
if (sr <= 0.0) sr = 44100.0;
osc1.prepare(sr);
osc2.prepare(sr);
env.prepare(sr);
filterEnv.prepare(sr);
filter.prepare(sr);
filterR.prepare(sr);
lfo1.prepare(sr);
lfo2.prepare(sr);
lfo1.reset();
lfo2.reset();
baseFreq1 = 440.0f * std::pow(2.0f, (static_cast<float>(midiNoteNumber) - 69.0f) / 12.0f)
* std::pow(2.0f, semitone1 / 12.0f + fineTune1 / 1200.0f) * std::pow(2.0f, octave1)
* globalTune;
baseFreq2 = 440.0f * std::pow(2.0f, (static_cast<float>(midiNoteNumber) - 69.0f) / 12.0f)
* std::pow(2.0f, semitone2 / 12.0f + fineTune2 / 1200.0f) * std::pow(2.0f, octave2)
* globalTune;
osc1.setFrequency(baseFreq1);
osc2.setFrequency(baseFreq2);
osc1.setWaveform(waveform1);
osc2.setWaveform(waveform2);
osc1.setPan(pan);
osc2.setPan(pan);
osc1.setPhase(0.0f);
osc2.setPhase(phaseOffset2);
filter.setCoefficients(filterCutoff, filterResonance, filterType);
filterR.setCoefficients(filterCutoff, filterResonance, filterType);
env.noteOn();
filterEnv.noteOn();
quickFade = 1.0f;
quickFadeStep = 0.0f;
}
void stopNote(float velocity, bool allowTailOff) override {
env.noteOff();
filterEnv.noteOff();
if (!allowTailOff || !env.isActive())
clearCurrentNote();
}
void renderNextBlock(juce::AudioBuffer<float>& outputBuffer,
int startSample, int numSamples) override {
if (!isVoiceActive()) return;
for (int i = startSample; i < startSample + numSamples; ++i) {
float envSample = env.getNextSample();
float fEnvSample = filterEnv.getNextSample();
if (!env.isActive()) {
clearCurrentNote();
return;
}
// Per-voice quick fade (triggered on preset change)
if (quickFadeStep > 0.0f)
quickFade = std::max(0.0f, quickFade - quickFadeStep);
// LFO outputs
float lfo1Out = lfo1.getNextSample();
float lfo2Out = lfo2.getNextSample();
// Apply pitch bend + LFO to oscillator frequencies
float pitchBendMult = std::pow(2.0f, pitchBendSemitones / 12.0f);
float freq1 = baseFreq1 * pitchBendMult;
float freq2 = baseFreq2 * pitchBendMult;
auto applyPitchLfo = [&](float lfoOut, LFODest dest) {
float semitoneShift = lfoOut * 12.0f; // +/- 12 semitones range
float pitchMult = std::pow(2.0f, semitoneShift / 12.0f);
if (dest == LFODest::OSC1Freq || dest == LFODest::BothOsc)
freq1 *= pitchMult;
if (dest == LFODest::OSC2Freq || dest == LFODest::BothOsc)
freq2 *= pitchMult;
};
if (lfo1Depth > 0.001f)
applyPitchLfo(lfo1Out, lfo1Dest);
if (lfo2Depth > 0.001f)
applyPitchLfo(lfo2Out, lfo2Dest);
osc1.setFrequency(freq1);
osc2.setFrequency(freq2);
// Apply LFO to OSC2 phase (vibrato-style phase sweep)
if (lfo1Depth > 0.001f && lfo1Dest == LFODest::OSC2Phase)
osc2.setPhase(phaseOffset2 + lfo1Out * 0.5f);
if (lfo2Depth > 0.001f && lfo2Dest == LFODest::OSC2Phase)
osc2.setPhase(phaseOffset2 + lfo2Out * 0.5f);
float left = 0.0f, right = 0.0f;
osc1.process(&left, &right);
osc2.process(&left, &right);
left *= noteVelocity * envSample;
right *= noteVelocity * envSample;
// Filter cutoff with env + keytrack.
// Unipolar modulation UP from the base cutoff: the envelope (0..1)
// multiplied by env amount adds brightness on top of the cutoff
// knob, never going below it. Every env knob (attack/decay/
// sustain/release) shapes the response, and at env amount 0 the
// cutoff equals the knob.
float modCutoff = filterCutoff * std::pow(2.0f, fEnvSample * filterEnvAmount * 4.0f);
float keyTrackFactor = std::pow(2.0f, (static_cast<float>(currentMidiNote) - 60.0f) / 12.0f * keyTrack);
modCutoff *= keyTrackFactor;
// Apply LFO to filter cutoff
if (lfo1Depth > 0.001f && lfo1Dest == LFODest::FilterCutoff)
modCutoff *= std::pow(2.0f, lfo1Out * 4.0f);
if (lfo2Depth > 0.001f && lfo2Dest == LFODest::FilterCutoff)
modCutoff *= std::pow(2.0f, lfo2Out * 4.0f);
modCutoff = std::clamp(modCutoff, 20.0f, static_cast<float>(getSampleRate() * 0.49));
filter.setCoefficients(modCutoff, filterResonance, filterType);
filterR.setCoefficients(modCutoff, filterResonance, filterType);
left = filter.process(left);
right = filterR.process(right);
left = std::tanh(left * drive) / std::tanh(drive);
right = std::tanh(right * drive) / std::tanh(drive);
outputBuffer.addSample(0, i, left * outputLevel * quickFade);
outputBuffer.addSample(1, i, right * outputLevel * quickFade);
if (quickFade <= 0.0f) {
clearCurrentNote();
return;
}
}
}
void pitchWheelMoved(int newPitchWheelValue) override {}
void controllerMoved(int controllerNumber, int newControllerValue) override {}
void setParameters(Waveform wf1, Waveform wf2,
float oct1, float oct2,
float semi1, float semi2,
float fine1, float fine2,
float lvl1, float lvl2,
float phOff2,
float cut, float res, FilterType ft,
float fEnvAmt, float kTrack,
float att, float dec, float sus, float rel,
float fAtt, float fDec, float fSus, float fRel,
float p, float drv, float outLvl,
float l1Rate, float l1Depth, int l1Shape, int l1Dest,
float l2Rate, float l2Depth, int l2Shape, int l2Dest) {
waveform1 = wf1;
waveform2 = wf2;
octave1 = oct1;
octave2 = oct2;
semitone1 = semi1;
semitone2 = semi2;
fineTune1 = fine1;
fineTune2 = fine2;
osc1.setLevel(lvl1);
osc2.setLevel(lvl2);
phaseOffset2 = phOff2;
filterCutoff = cut;
filterResonance = res;
filterType = ft;
filterEnvAmount = fEnvAmt;
keyTrack = kTrack;
pan = p;
drive = std::max(drv, 1.001f);
outputLevel = outLvl * outLvl;
lfo1Depth = l1Depth;
lfo1Dest = static_cast<LFODest>(l1Dest);
lfo1.setParameters(l1Rate, l1Depth, static_cast<LFOShape>(l1Shape), lfo1Dest);
lfo2Depth = l2Depth;
lfo2Dest = static_cast<LFODest>(l2Dest);
lfo2.setParameters(l2Rate, l2Depth, static_cast<LFOShape>(l2Shape), lfo2Dest);
env.setAttack(att);
env.setDecay(dec);
env.setSustain(sus);
env.setRelease(rel);
filterEnv.setAttack(fAtt);
filterEnv.setDecay(fDec);
filterEnv.setSustain(fSus);
filterEnv.setRelease(fRel);
}
void setPitchBend(float semitones) { pitchBendSemitones = semitones; }
// Quickly fade out this voice (e.g. when a preset is switched) so any
// long release tail from the previous sound is silenced over `seconds`.
void triggerQuickRelease(float seconds) {
double sr = getSampleRate();
if (sr <= 0.0) sr = 44100.0;
quickFadeStep = 1.0f / static_cast<float>(seconds * sr);
env.noteOff();
filterEnv.noteOff();
}
private:
Oscillator osc1, osc2;
Filter filter;
Filter filterR;
ADSREnvelope env;
ADSREnvelope filterEnv;
LFO lfo1, lfo2;
Waveform waveform1 = Waveform::Saw;
Waveform waveform2 = Waveform::Square;
float octave1 = 0.0f, octave2 = -1.0f;
float semitone1 = 0.0f, semitone2 = 0.0f;
float fineTune1 = 0.0f, fineTune2 = 7.0f;
float phaseOffset2 = 0.5f;
float filterCutoff = 8000.0f;
float filterResonance = 0.7f;
FilterType filterType = FilterType::LowPass12;
float filterEnvAmount = 0.0f;
float keyTrack = 0.5f;
float pan = 0.0f;
float drive = 1.5f;
float outputLevel = 0.8f;
float baseFreq1 = 440.0f;
float baseFreq2 = 440.0f;
float globalTune = 0.5f; // synth tuned down one octave (1/2 of normal pitch)
float lfo1Depth = 0.0f;
float lfo2Depth = 0.0f;
LFODest lfo1Dest = LFODest::FilterCutoff;
LFODest lfo2Dest = LFODest::FilterCutoff;
float pitchBendSemitones = 0.0f;
int currentMidiNote = 60;
float noteVelocity = 1.0f;
float quickFade = 1.0f;
float quickFadeStep = 0.0f;
};