diff --git a/Source/SynthEngine.cpp b/Source/SynthEngine.cpp index 13cb914..7a67702 100644 --- a/Source/SynthEngine.cpp +++ b/Source/SynthEngine.cpp @@ -8,6 +8,7 @@ SynthEngine::SynthEngine() auto def = defaultParams(); for (int i = 0; i < kNumParams; ++i) params[i] = def[i]; + smooth = defaultParams(); knobParams[0] = PCUTOFF; knobParams[1] = PRES; @@ -30,6 +31,7 @@ void SynthEngine::prepare (double sr, int mb) sampleRate = sr; maxBlock = mb; delay.prepare (sr); + smCoef = (float) (1.0 - std::exp (-1.0 / (0.02 * std::max (sr, 1.0) + 1.0))); allNotesOff(); reset(); } @@ -141,6 +143,7 @@ void SynthEngine::resetVoice (Voice& v, int note, float vel) v.envState = 0; v.env = 0.0f; v.age = 0; + v.fadeLen = (int) (sampleRate * 0.0035) + 8; v.ic1eq = 0.0; v.ic2eq = 0.0; @@ -181,6 +184,30 @@ static float softClip (float x) return std::tanh (x * 1.5f) / std::tanh(1.5f); } +// Which parameters get one-pole smoothed per-sample to avoid zipper noise. +// Discrete/instant things (waves, filter type switch handled separately, +// envelope times, delay time) are left to snap instantly. +static constexpr bool kSmooth[kNumParams] = +{ + false, false, false, false, false, false, // waves + octaves/detune + true, true, true, true, true, false, // mix, noise, cutoff, res, fenv + false, false, false, false, // atk/dec/sus/rel + false, false, false, true, true, // dtime/dsteps/sync, dfb, dmix + true, false, true, // drive, glide, master +}; + +// Soft output limiter: linear below the knee, smooth compression above. +// Keeps chord/feedback peaks from hard-clipping at the DAW stage. +static inline float limiterSample (float x) +{ + const float T = 0.65f; + const float R = 1.0f - T; + float ax = x < 0.0f ? -x : x; + if (ax <= T) return x; + float p = T + R * std::tanh ((ax - T) / R); + return x < 0.0f ? -p : p; +} + double SynthEngine::cutoffFor (const Voice& v, double baseCutoff, double envAmt, double envLevel) { double baseHz = paramReal (PCUTOFF, (float) baseCutoff); @@ -194,6 +221,8 @@ double SynthEngine::cutoffFor (const Voice& v, double baseCutoff, double envAmt, void SynthEngine::process (AudioBuffer& buffer, MidiBuffer& midi, int n, double bpm, bool transportPlaying) { + if (n <= 0) return; + wasPlaying = transportPlaying; lastBpmValue = (bpm > 10.0 && bpm < 300.0) ? bpm : 120.0; @@ -203,49 +232,33 @@ void SynthEngine::process (AudioBuffer& buffer, MidiBuffer& midi, for (int i = 0; i < kNumParams; ++i) snap[(size_t) i] = params[i]; } - const float* const params = snap.data(); - const float cutN01 = params[PCUTOFF]; - const float resN01 = params[PRES]; - const float fenvN01 = params[PFENV]; - const float ftypeN01= params[PFTYPE]; - const float atkN01 = params[PATK]; - const float decN01 = params[PDEC]; - const float susN01 = params[PSUS]; - const float relN01 = params[PREL]; - const float driveN01= params[PDRIVE]; - const float glideN01= params[PGLIDE]; - const float masterN01= params[PMASTER]; - const float mixN01 = params[PMIX]; - const float noiseN01= params[PNOISE]; + // Collect MIDI into a list sorted by sample and dispatch each event at its + // exact position inside the block, instead of firing everything at sample 0. + struct Ev { int at; MidiMessage msg; }; + std::vector events; + events.reserve (midi.getNumEvents()); + for (const auto& metadata : midi) + events.push_back ({ juce::jlimit (0, n - 1, metadata.samplePosition), metadata.getMessage() }); - const double atkSec = paramReal (PATK, atkN01); - const double decSec = paramReal (PDEC, decN01); - const double susLevel = susN01; - const double relSec = paramReal (PREL, relN01); + const double atkSec = paramReal (PATK, snap[(size_t) PATK]); + const double decSec = paramReal (PDEC, snap[(size_t) PDEC]); + const double susLevel = snap[(size_t) PSUS]; + const double relSec = paramReal (PREL, snap[(size_t) PREL]); + const double glideN01 = snap[(size_t) PGLIDE]; const double atkCoef = 1.0 - std::exp (-1.0 / (atkSec * sampleRate + 1.0)); const double decCoef = 1.0 - std::exp (-1.0 / (decSec * sampleRate + 1.0)); const double relCoef = 1.0 - std::exp (-1.0 / (relSec * sampleRate + 1.0)); - - const double drive = 0.1 + driveN01 * 2.4; const double glideCoef = glideN01 < 0.001 ? 1.0 : 1.0 - std::exp (-1.0 / (glideN01 * sampleRate + 1.0)); - const double master = masterN01; - const double fenvScale = fenvN01 * 5.0; - (void) fenvScale; + const int filterType = paramFilterType (snap[(size_t) PFTYPE]); + const int wave1 = paramWaveIndex (snap[(size_t) PO1WAVE]); + const int wave2 = paramWaveIndex (snap[(size_t) PO2WAVE]); - const float gMix = mixN01; - const float g1 = 1.0f - gMix; - const float g2 = gMix; - const float g1p = (float) std::sqrt (g1); - const float g2p = (float) std::sqrt (g2); - - const int filterType = paramFilterType (ftypeN01); - - bool delaySync = params[PDSYNC] > 0.5f; - int stepIdx = juce::jlimit (0, 15, (int) juce::roundToInt (params[PDSTEPS])); - float delayMsParam = paramReal (PDTIME, params[PDTIME]); + bool delaySync = snap[(size_t) PDSYNC] > 0.5f; + int stepIdx = juce::jlimit (0, 15, (int) juce::roundToInt (snap[(size_t) PDSTEPS])); + float delayMsParam = paramReal (PDTIME, snap[(size_t) PDTIME]); float delaySec; if (delaySync) delaySec = (float) (stepsBeats()[stepIdx] * 60.0 / lastBpmValue); @@ -254,29 +267,52 @@ void SynthEngine::process (AudioBuffer& buffer, MidiBuffer& midi, delaySec = std::clamp (delaySec, 0.001f, 4.0f); lastDelaySamples = delaySec * sampleRate; - const float delayFB = params[PDFB]; - const float delayMix= params[PDMIX]; - const float dryMix = 1.0f - delayMix; - const float wetMix = delayMix; - - for (const auto& metadata : midi) - { - MidiMessage msg = metadata.getMessage(); - int sm = juce::jlimit (0, n - 1, metadata.samplePosition); - if (msg.isNoteOn()) - noteOn (msg.getNoteNumber(), msg.getFloatVelocity()); - else if (msg.isNoteOff()) - noteOff (msg.getNoteNumber()); - else if (msg.isAllNotesOff() || msg.isAllSoundOff()) - allNotesOff(); - } + const float* const sp = smooth.data(); float* outL = buffer.getWritePointer (0); float* outR = buffer.getNumChannels() > 1 ? buffer.getWritePointer (1) : nullptr; + int ei = 0; + const int evCount = (int) events.size(); + for (int s = 0; s < n; ++s) { - float sumL = 0.0f, sumR = 0.0f; + // 1) trigger any MIDI events landing at (or before) this sample + while (ei < evCount && events[(size_t) ei].at <= s) + { + const MidiMessage& msg = events[(size_t) ei].msg; + if (msg.isNoteOn()) + noteOn (msg.getNoteNumber(), msg.getFloatVelocity()); + else if (msg.isNoteOff()) + noteOff (msg.getNoteNumber()); + else if (msg.isAllNotesOff() || msg.isAllSoundOff()) + allNotesOff(); + ++ei; + } + + // 2) glide continuous params toward their target (anti-zipper) + for (int i = 0; i < kNumParams; ++i) + { + const float t = snap[(size_t) i]; + if (kSmooth[i]) + smooth[(size_t) i] += smCoef * (t - smooth[(size_t) i]); + else + smooth[(size_t) i] = t; + } + + // 3) crossfade filter type (LP/BP/HP) to avoid switching clicks + const float f0 = filterType == 0 ? 1.0f : 0.0f; + const float f1 = filterType == 1 ? 1.0f : 0.0f; + const float f2 = filterType == 2 ? 1.0f : 0.0f; + ftA += smCoef * (f0 - ftA); + ftB += smCoef * (f1 - ftB); + ftC += smCoef * (f2 - ftC); + + const float gMix = sp[(size_t) PMIX]; + const float g1p = std::sqrt (1.0f - gMix); + const float g2p = std::sqrt (gMix); + + float sum = 0.0f; for (auto& v : voices) { @@ -298,14 +334,14 @@ void SynthEngine::process (AudioBuffer& buffer, MidiBuffer& midi, case 3: v.env += (0.0 - v.env) * relCoef; if (v.env < 0.001) { v.active = false; v.env = 0.0; activeNotes--; } break; } - float o1 = oscWave (paramWaveIndex (params[PO1WAVE]), v.ph1); - float o2 = oscWave (paramWaveIndex (params[PO2WAVE]), v.ph2); + float o1 = oscWave (wave1, v.ph1); + float o2 = oscWave (wave2, v.ph2); - float raw = g1p * o1 + g2p * o2 + noiseN01 * (float) (((double) std::rand() / (double) RAND_MAX) * 2.0 - 1.0) * 0.5f; + float raw = g1p * o1 + g2p * o2 + sp[(size_t) PNOISE] * (float) (((double) std::rand() / (double) RAND_MAX) * 2.0 - 1.0) * 0.5f; raw *= 0.7f; - float cutoffHz = cutoffFor (v, cutN01, fenvN01, v.env); - float Q = 0.5f + resN01 * 14.0f; + float cutoffHz = cutoffFor (v, sp[(size_t) PCUTOFF], sp[(size_t) PFENV], v.env); + float Q = 0.5f + sp[(size_t) PRES] * 14.0f; float k = 1.0f / Q; double f = cutoffHz / (float) sampleRate; if (f > 0.49) f = 0.49; @@ -321,33 +357,29 @@ void SynthEngine::process (AudioBuffer& buffer, MidiBuffer& midi, v.ic1eq = 2.0 * v1 - v.ic1eq; v.ic2eq = 2.0 * v2 - v.ic2eq; - float filtered; - switch (filterType) - { - case 0: filtered = (float) v2; break; - case 1: filtered = (float) v1; break; - case 2: filtered = (float) (vin - k * v1 - v2); break; - default: filtered = (float) v2; break; - } + float filtered = ftA * (float) v2 + ftB * (float) v1 + ftC * (float) (vin - k * v1 - v2); - float sig = softClip (filtered * (float) drive); - sig *= (float) (v.env * v.velocity * master); - sumL += sig; - sumR += sig; + // short per-note fade-in kills onset/steal clicks + float vg = v.age >= v.fadeLen ? 1.0f : (float) v.age / (float) v.fadeLen; + + float sig = softClip (filtered * (0.1f + sp[(size_t) PDRIVE] * 2.4f)); + sig *= (float) (v.env * v.velocity * sp[(size_t) PMASTER] * vg); + sum += sig; } - float dry = sumL; + float dry = sum; int w = delay.writePos[0]; int r = (w - (int) lastDelaySamples + delay.maxLen * 2) % delay.maxLen; float delayed = delay.buf[0][r]; - float fb = dry + delayed * (float) delayFB; + float fb = dry + delayed * sp[(size_t) PDFB]; fb = softClip (fb * 0.7f); delay.buf[0][w] = fb; delay.buf[1][w] = fb; delay.writePos[0] = (w + 1) % delay.maxLen; delay.writePos[1] = (w + 1) % delay.maxLen; - outL[s] = dryMix * dry + wetMix * delayed; + outL[s] = (1.0f - sp[(size_t) PDMIX]) * dry + sp[(size_t) PDMIX] * delayed; + outL[s] = limiterSample (outL[s]); if (outR) outR[s] = outL[s]; } } diff --git a/Source/SynthEngine.h b/Source/SynthEngine.h index 3db1c08..cb00aaf 100644 --- a/Source/SynthEngine.h +++ b/Source/SynthEngine.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include "SynthParams.h" #include "Presets.h" @@ -54,6 +55,7 @@ private: double env = 0.0; int envState = 0; int age = 0; + int fadeLen = 32; double ic1eq = 0.0, ic2eq = 0.0; }; @@ -101,6 +103,10 @@ private: int currentPresetIndex = 0; mutable juce::SpinLock paramLock; + + std::array smooth; + float smCoef = 1.0f; + float ftA = 1.0f, ftB = 0.0f, ftC = 0.0f; }; } // namespace sm26 \ No newline at end of file