#pragma once #include #include "WaveTables.h" namespace monostep { class SynthVoice { public: struct Params { Waveform waveA = Waveform::saw; Waveform waveB = Waveform::square; 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; float drive = 0.0f; float ringMod = 0.0f; float detuneRatio = 1.0f; float mix = 0.5f; }; void addPhase (float phaseOffset) { phaseA = frac (phaseA + phaseOffset); phaseB = frac (phaseB + phaseOffset); } void prepare (double sr) { sampleRate = (float) sr; 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; accentLevel = 1.0f; accentSmooth = 1.0f; } void setParams (const Params& p) { params = p; } bool isActive() const { return gate; } 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& 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::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(); // 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; if (params.ringMod > 0.0f) out = (1.0f - params.ringMod) * out + params.ringMod * (a * b); const float preFilter = out; out = processFilter (out); // 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; float accentLevel = 1.0f; float accentSmooth = 1.0f; float accentSmoothCoef = 0.01f; }; } // namespace monostep