2026-07-13 20:57:38 +02:00
|
|
|
#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);
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
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);
|
|
|
|
|
|
2026-07-13 22:07:51 +02:00
|
|
|
modCutoff = std::clamp(modCutoff, 20.0f, static_cast<float>(getSampleRate() * 0.49));
|
2026-07-13 20:57:38 +02:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
outputBuffer.addSample(1, i, right * outputLevel);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
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; }
|
|
|
|
|
|
|
|
|
|
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 = 800.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 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;
|
|
|
|
|
};
|