monostep/Source/dsp/SynthVoice.h

229 lines
5.7 KiB
C
Raw Normal View History

#pragma once
#include <JuceHeader.h>
#include "WaveTables.h"
namespace monostep
{
class SynthVoice
{
public:
struct Params
{
Waveform waveA = Waveform::saw;
Waveform waveB = Waveform::square;
2026-08-05 23:12:17 +02:00
float osc1Coarse = 0.0f;
float osc2Coarse = 0.0f;
float osc1Fine = 0.0f;
float osc2Fine = 0.0f;
float osc1Phase = 0.0f;
float osc2Phase = 0.0f;
float cutoff = 7000.0f;
float resonance = 0.15f;
float attack = 0.005f;
float decay = 0.3f;
float sustain = 0.7f;
float release = 0.4f;
float glide = 0.12f;
float master = 0.9f;
2026-08-05 23:12:17 +02:00
float drive = 0.0f;
float ringMod = 0.0f;
float detuneRatio = 1.0f;
float mix = 0.5f;
};
2026-08-05 23:12:17 +02:00
void addPhase (float phaseOffset)
{
phaseA = frac (phaseA + phaseOffset);
phaseB = frac (phaseB + phaseOffset);
}
void prepare (double sr)
{
sampleRate = (float) sr;
2026-08-05 23:12:17 +02:00
accentSmoothCoef = 1.0f - std::exp (-1.0f / (0.003f * sampleRate));
reset();
}
void reset()
{
phaseA = phaseB = 0.0f;
currentFreq = targetFreq = 440.0f;
smoothing = 0.0f;
filterLow = filterBand = 0.0f;
env = 0.0f;
envStage = Stage::idle;
gate = false;
2026-08-05 23:12:17 +02:00
accentLevel = 1.0f;
accentSmooth = 1.0f;
}
void setParams (const Params& p) { params = p; }
bool isActive() const { return gate; }
2026-08-05 23:12:17 +02:00
void setAccentLevel (float level) { accentLevel = level; }
void noteOn (float freqHz, bool legato)
{
targetFreq = freqHz;
if (gate && legato)
{
const float time = std::max (params.glide, 0.001f);
smoothing = 1.0f - std::exp (-1.0f / (time * sampleRate));
}
else
{
currentFreq = targetFreq;
smoothing = 0.0f;
retriggerEnvelope();
}
gate = true;
}
void noteOff()
{
if (gate)
{
gate = false;
if (envStage != Stage::idle && envStage != Stage::release)
envStage = Stage::release;
}
}
void render (juce::AudioBuffer<float>& buffer, int numSamples)
{
auto* out = buffer.getWritePointer (0);
for (int i = 0; i < numSamples; ++i)
out[i] = renderSample();
}
private:
enum class Stage { idle, attack, decay, sustain, release };
void retriggerEnvelope()
{
envStage = Stage::attack;
}
void updateEnvelope()
{
const float sr = sampleRate;
switch (envStage)
{
case Stage::attack:
env += 1.0f / (params.attack * sr);
if (env >= 1.0f) { env = 1.0f; envStage = Stage::decay; }
break;
case Stage::decay:
env -= (1.0f - params.sustain) / (params.decay * sr);
if (env <= params.sustain) { env = params.sustain; envStage = Stage::sustain; }
break;
case Stage::sustain:
env = params.sustain;
break;
case Stage::release:
env -= 1.0f / (params.release * sr);
if (env <= 0.0f) { env = 0.0f; envStage = Stage::idle; }
break;
case Stage::idle:
break;
}
}
void updateGlide()
{
if (smoothing > 0.0f)
{
currentFreq += (targetFreq - currentFreq) * smoothing;
if (std::fabs (targetFreq - currentFreq) < 0.01f)
{
currentFreq = targetFreq;
smoothing = 0.0f;
}
}
}
float processFilter (float input)
{
const float f = 2.0f * std::sin (juce::MathConstants<float>::pi * params.cutoff / sampleRate);
const float q = 1.0f / (1.0f + params.resonance * 9.0f);
filterLow += f * filterBand;
const float high = input - filterLow - q * filterBand;
filterBand += f * high;
return filterLow;
}
float renderSample()
{
updateEnvelope();
updateGlide();
2026-08-05 23:12:17 +02:00
// Smooth the accent gain so it ramps in/out instead of clicking.
accentSmooth += (accentLevel - accentSmooth) * accentSmoothCoef;
const float incA = currentFreq / sampleRate;
const float incB = (currentFreq * params.detuneRatio) / sampleRate;
const float a = renderWave (params.waveA, phaseA, incA);
const float b = renderWave (params.waveB, phaseB, incB);
phaseA = frac (phaseA + incA);
phaseB = frac (phaseB + incB);
float out = (1.0f - params.mix) * a + params.mix * b;
2026-08-05 23:12:17 +02:00
if (params.ringMod > 0.0f)
out = (1.0f - params.ringMod) * out + params.ringMod * (a * b);
const float preFilter = out;
out = processFilter (out);
2026-08-05 23:12:17 +02:00
// Accented steps also get a touch of the unfiltered signal so they cut
// through the mix (presence), not just a volume bump.
if (accentSmooth > 1.0f)
out += (accentSmooth - 1.0f) * 0.2f * preFilter;
if (params.drive > 0.0f)
{
const float wet = std::tanh (out * (1.0f + params.drive * 9.0f));
out = (1.0f - params.drive) * out + params.drive * wet;
}
return out * env * params.master * accentSmooth;
}
float sampleRate = 44100.0f;
Params params;
float phaseA = 0.0f;
float phaseB = 0.0f;
float currentFreq = 440.0f;
float targetFreq = 440.0f;
float smoothing = 0.0f;
float filterLow = 0.0f;
float filterBand = 0.0f;
float env = 0.0f;
Stage envStage = Stage::idle;
bool gate = false;
2026-08-05 23:12:17 +02:00
float accentLevel = 1.0f;
float accentSmooth = 1.0f;
float accentSmoothCoef = 0.01f;
};
} // namespace monostep