sm-26/Source/SynthEngine.cpp

397 lines
13 KiB
C++
Raw Permalink Normal View History

#include "SynthEngine.h"
#include <cmath>
namespace sm26 {
SynthEngine::SynthEngine()
{
auto def = defaultParams();
for (int i = 0; i < kNumParams; ++i)
params[i] = def[i];
smooth = defaultParams();
knobParams[0] = PCUTOFF;
knobParams[1] = PRES;
knobParams[2] = PFENV;
knobParams[3] = PDMIX;
knobNames[0] = "CUTOFF";
knobNames[1] = "RES";
knobNames[2] = "F-ENV";
knobNames[3] = "DELAY";
for (int i = 0; i < kNumKnobs; ++i)
{ knobLows[i] = 0.0f; knobHighs[i] = 1.0f; }
std::srand (12345);
}
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();
}
void SynthEngine::reset()
{
allNotesOff();
delay.clear();
}
void SynthEngine::loadPreset (const Preset& p)
{
juce::SpinLock::ScopedLockType lock (paramLock);
for (int i = 0; i < kNumParams; ++i)
params[i] = p.base[i];
for (int i = 0; i < kNumKnobs; ++i)
{
knobParams[i] = p.ctl[i].param;
knobNames[i] = p.ctl[i].name;
knobLows[i] = p.ctl[i].lo;
knobHighs[i] = p.ctl[i].hi;
}
allNotesOff();
delay.clear();
}
void SynthEngine::setPresetIndex (int i)
{
currentPresetIndex = i;
loadPreset (*PresetStore::findByIndex (i));
}
double SynthEngine::delayTimeMs() const
{
if (getParam (PDSYNC) > 0.5f)
{
const int stepIdx = juce::jlimit (0, 15, (int) juce::roundToInt (getParam (PDSTEPS) * 15.0f));
return stepsBeats()[stepIdx] * 60000.0 / lastBpmValue;
}
return paramReal (PDTIME, getParam (PDTIME));
}
void SynthEngine::setParam (int id, float n01)
{
if (id < 0 || id >= kNumParams) return;
juce::SpinLock::ScopedLockType lock (paramLock);
params[id] = juce::jlimit (0.0f, 1.0f, n01);
}
float SynthEngine::getParam (int id) const
{
if (id < 0 || id >= kNumParams) return 0.0f;
return params[id];
}
SynthEngine::KnobView SynthEngine::knobView (int knob) const
{
if (knob < 0 || knob >= kNumKnobs) return {};
KnobView k;
k.name = knobNames[knob];
k.param = knobParams[knob];
k.value01 = getParam (k.param);
k.lo = knobLows[knob];
k.hi = knobHighs[knob];
return k;
}
float SynthEngine::knobValue (int knob) const
{
if (knob < 0 || knob >= kNumKnobs) return 0.0f;
return getParam (knobParams[knob]);
}
const char* SynthEngine::waveNameFor (int index) const
{
return sm26::waveName (juce::jlimit (0, 4, index));
}
void SynthEngine::noteOn (int note, float vel)
{
Voice* v = findFreeOrSteal();
resetVoice (*v, note, vel);
}
void SynthEngine::noteOff (int note)
{
for (auto& v : voices)
if (v.active && v.note == note)
v.envState = 3;
}
void SynthEngine::allNotesOff()
{
for (auto& v : voices) { v.active = false; v.env = 0.0; v.envState = 0; }
activeNotes = 0;
}
SynthEngine::Voice* SynthEngine::findFreeOrSteal()
{
int freeIdx = -1;
int oldest = -1;
int oldestAge = -1;
for (int i = 0; i < kNumVoices; ++i)
{
if (! voices[i].active) { freeIdx = i; break; }
if (voices[i].age > oldestAge) { oldest = i; oldestAge = voices[i].age; }
}
if (freeIdx >= 0) { activeNotes++; return &voices[freeIdx]; }
voices[oldest].envState = 3;
voices[oldest].env = 0.0f;
return &voices[oldest];
}
void SynthEngine::resetVoice (Voice& v, int note, float vel)
{
v.active = true;
v.note = note;
v.velocity = 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;
double freq = 440.0 * std::pow (2.0, (note - 69) / 12.0);
double inc = freq / sampleRate;
double oct1 = paramReal (PO1OCT, params[PO1OCT]);
double oct2 = paramReal (PO2OCT, params[PO2OCT]);
double dt1 = paramReal (PO1CT, params[PO1CT]) / 1200.0;
double dt2 = paramReal (PO2CT, params[PO2CT]) / 1200.0;
double f1 = freq * std::pow (2.0, oct1 + dt1);
double f2 = freq * std::pow (2.0, oct2 + dt2);
v.inc1 = f1 / sampleRate;
v.inc2 = f2 / sampleRate;
v.glidedInc1 = v.inc1;
v.ph1 = 0.0;
v.ph2 = 0.0;
}
float SynthEngine::oscWave (int wave, double phase)
{
double ph = phase - std::floor(phase);
switch (wave)
{
case 0: return (float) std::sin (ph * 6.283185307179586);
case 1: return (float) (1.0 - 4.0 * std::fabs (ph - 0.5));
case 2: return (float) (2.0 * ph - 1.0);
case 3: return (float) (ph < 0.5 ? 1.0 : -1.0);
case 4: return (float) (((double) std::rand() / (double) RAND_MAX) * 2.0 - 1.0);
default: return 0.0f;
}
}
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);
baseHz = std::clamp (baseHz, 20.0, 20000.0);
double mod = envAmt * envLevel * 5.0;
double hz = baseHz * std::pow (2.0, mod);
hz = std::clamp (hz, 20.0, std::min (sampleRate * 0.45, 20000.0));
return (float) hz;
}
void SynthEngine::process (AudioBuffer<float>& 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;
std::array<float, kNumParams> snap;
{
juce::SpinLock::ScopedLockType lock (paramLock);
for (int i = 0; i < kNumParams; ++i)
snap[(size_t) i] = params[i];
}
// 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<Ev> 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, 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 glideCoef = glideN01 < 0.001 ? 1.0 : 1.0 - std::exp (-1.0 / (glideN01 * sampleRate + 1.0));
const int filterType = paramFilterType (snap[(size_t) PFTYPE]);
const int wave1 = paramWaveIndex (snap[(size_t) PO1WAVE]);
const int wave2 = paramWaveIndex (snap[(size_t) PO2WAVE]);
bool delaySync = snap[(size_t) PDSYNC] > 0.5f;
int stepIdx = juce::jlimit (0, 15, (int) juce::roundToInt (snap[(size_t) PDSTEPS] * 15.0f));
float delayMsParam = paramReal (PDTIME, snap[(size_t) PDTIME]);
float delaySec;
if (delaySync)
delaySec = (float) (stepsBeats()[stepIdx] * 60.0 / lastBpmValue);
else
delaySec = delayMsParam * 0.001f;
delaySec = std::clamp (delaySec, 0.001f, 4.0f);
lastDelaySamples = delaySec * sampleRate;
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)
{
// 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)
{
if (! v.active) continue;
v.age++;
v.ph1 += v.inc1;
v.ph2 += v.inc2;
if (v.ph1 > 1.0) v.ph1 -= 1.0;
if (v.ph2 > 1.0) v.ph2 -= 1.0;
v.glidedInc1 = (float) ((1.0 - glideCoef) * v.glidedInc1 + glideCoef * v.inc1);
switch (v.envState)
{
case 0: v.env += (1.0 - v.env) * atkCoef; if (v.env > 0.999) v.envState = 1; break;
case 1: v.env += (susLevel - v.env) * decCoef; break;
case 2: break;
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 (wave1, v.ph1);
float o2 = oscWave (wave2, v.ph2);
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, 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;
double g = std::tan (juce::MathConstants<double>::pi * f);
double a1 = 1.0 / (1.0 + g * (g + k));
double a2 = g * a1;
double a3 = g * a2;
double vin = raw;
double v3 = vin - v.ic2eq;
double v1 = a1 * v.ic1eq + a2 * v3;
double v2 = v.ic2eq + a2 * v.ic1eq + a3 * v3;
v.ic1eq = 2.0 * v1 - v.ic1eq;
v.ic2eq = 2.0 * v2 - v.ic2eq;
float filtered = ftA * (float) v2 + ftB * (float) v1 + ftC * (float) (vin - k * v1 - v2);
// 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 = 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 * 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] = (1.0f - sp[(size_t) PDMIX]) * dry + sp[(size_t) PDMIX] * delayed;
outL[s] = limiterSample (outL[s]);
if (outR) outR[s] = outL[s];
}
}
} // namespace sm26