mirror of
https://codeberg.org/armin/sm-26.git
synced 2026-10-11 17:01:53 +02:00
343 lines
10 KiB
C++
343 lines
10 KiB
C++
|
|
#include "SynthEngine.h"
|
||
|
|
#include <cmath>
|
||
|
|
|
||
|
|
namespace sm26 {
|
||
|
|
|
||
|
|
SynthEngine::SynthEngine()
|
||
|
|
{
|
||
|
|
auto def = defaultParams();
|
||
|
|
for (int i = 0; i < kNumParams; ++i)
|
||
|
|
params[i] = def[i];
|
||
|
|
|
||
|
|
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);
|
||
|
|
allNotesOff();
|
||
|
|
reset();
|
||
|
|
}
|
||
|
|
|
||
|
|
void SynthEngine::reset()
|
||
|
|
{
|
||
|
|
allNotesOff();
|
||
|
|
delay.clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
void SynthEngine::loadPreset (const Preset& p)
|
||
|
|
{
|
||
|
|
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();
|
||
|
|
}
|
||
|
|
|
||
|
|
void SynthEngine::setPresetIndex (int i)
|
||
|
|
{
|
||
|
|
currentPresetIndex = i;
|
||
|
|
loadPreset (*PresetStore::findByIndex (i));
|
||
|
|
}
|
||
|
|
|
||
|
|
void SynthEngine::setParam (int id, float n01)
|
||
|
|
{
|
||
|
|
if (id < 0 || id >= kNumParams) return;
|
||
|
|
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);
|
||
|
|
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.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);
|
||
|
|
}
|
||
|
|
|
||
|
|
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)
|
||
|
|
{
|
||
|
|
wasPlaying = transportPlaying;
|
||
|
|
lastBpmValue = (bpm > 10.0 && bpm < 300.0) ? bpm : 120.0;
|
||
|
|
|
||
|
|
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];
|
||
|
|
|
||
|
|
const double atkSec = paramReal (PATK, atkN01);
|
||
|
|
const double decSec = paramReal (PDEC, decN01);
|
||
|
|
const double susLevel = susN01;
|
||
|
|
const double relSec = paramReal (PREL, relN01);
|
||
|
|
|
||
|
|
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 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] * 15.0f));
|
||
|
|
float delayMsParam = paramReal (PDTIME, params[PDTIME]);
|
||
|
|
float delaySec;
|
||
|
|
if (delaySync)
|
||
|
|
delaySec = (float) (stepsBeats()[stepIdx] * 60.0 / lastBpmValue);
|
||
|
|
else
|
||
|
|
delaySec = delayMsParam * 0.001f;
|
||
|
|
delaySec = std::clamp (delaySec, 0.001f, 17.0f);
|
||
|
|
lastDelaySamples = delaySec * sampleRate;
|
||
|
|
|
||
|
|
const float delayFB = params[PDFB];
|
||
|
|
const float delayMix= params[PDMIX];
|
||
|
|
const float dryMix = 1.0f - delayMix;
|
||
|
|
const float wetMix = delayMix;
|
||
|
|
|
||
|
|
int delayWrite = (int) delay.writePos[0];
|
||
|
|
int delayRead = (delayWrite - (int) lastDelaySamples + delay.maxLen * 2) % delay.maxLen;
|
||
|
|
(void) delayRead;
|
||
|
|
|
||
|
|
for (const auto& metadata : midi)
|
||
|
|
{
|
||
|
|
MidiMessage msg = metadata.getMessage();
|
||
|
|
int sm = metadata.samplePosition;
|
||
|
|
if (sm < n && msg.isNoteOn())
|
||
|
|
noteOn (msg.getNoteNumber(), msg.getFloatVelocity());
|
||
|
|
else if (sm < n && msg.isNoteOff())
|
||
|
|
noteOff (msg.getNoteNumber());
|
||
|
|
}
|
||
|
|
|
||
|
|
float* outL = buffer.getWritePointer (0);
|
||
|
|
float* outR = buffer.getNumChannels() > 1 ? buffer.getWritePointer (1) : nullptr;
|
||
|
|
|
||
|
|
for (int s = 0; s < n; ++s)
|
||
|
|
{
|
||
|
|
float sumL = 0.0f, sumR = 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 (paramWaveIndex (params[PO1WAVE]), v.ph1);
|
||
|
|
float o2 = oscWave (paramWaveIndex (params[PO2WAVE]), v.ph2);
|
||
|
|
|
||
|
|
float raw = g1p * o1 + g2p * o2 + noiseN01 * (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 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;
|
||
|
|
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 sig = softClip (filtered * (float) drive);
|
||
|
|
sig *= (float) (v.env * v.velocity * master);
|
||
|
|
sumL += sig;
|
||
|
|
sumR += sig;
|
||
|
|
}
|
||
|
|
|
||
|
|
float dry = sumL;
|
||
|
|
int w = delay.writePos[0];
|
||
|
|
float delayed = delay.buf[0][w];
|
||
|
|
float fb = dry + delayed * (float) delayFB;
|
||
|
|
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;
|
||
|
|
if (outR) outR[s] = outL[s];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace sm26
|