mirror of
https://codeberg.org/armin/sm-26.git
synced 2026-10-11 17:01:53 +02:00
Smooth parameter changes, crossfade filter type, add note fade-in and soft limiter
This commit is contained in:
parent
ab9ea542a7
commit
1a36b5baf0
2 changed files with 111 additions and 73 deletions
|
|
@ -8,6 +8,7 @@ SynthEngine::SynthEngine()
|
||||||
auto def = defaultParams();
|
auto def = defaultParams();
|
||||||
for (int i = 0; i < kNumParams; ++i)
|
for (int i = 0; i < kNumParams; ++i)
|
||||||
params[i] = def[i];
|
params[i] = def[i];
|
||||||
|
smooth = defaultParams();
|
||||||
|
|
||||||
knobParams[0] = PCUTOFF;
|
knobParams[0] = PCUTOFF;
|
||||||
knobParams[1] = PRES;
|
knobParams[1] = PRES;
|
||||||
|
|
@ -30,6 +31,7 @@ void SynthEngine::prepare (double sr, int mb)
|
||||||
sampleRate = sr;
|
sampleRate = sr;
|
||||||
maxBlock = mb;
|
maxBlock = mb;
|
||||||
delay.prepare (sr);
|
delay.prepare (sr);
|
||||||
|
smCoef = (float) (1.0 - std::exp (-1.0 / (0.02 * std::max (sr, 1.0) + 1.0)));
|
||||||
allNotesOff();
|
allNotesOff();
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
|
|
@ -141,6 +143,7 @@ void SynthEngine::resetVoice (Voice& v, int note, float vel)
|
||||||
v.envState = 0;
|
v.envState = 0;
|
||||||
v.env = 0.0f;
|
v.env = 0.0f;
|
||||||
v.age = 0;
|
v.age = 0;
|
||||||
|
v.fadeLen = (int) (sampleRate * 0.0035) + 8;
|
||||||
v.ic1eq = 0.0;
|
v.ic1eq = 0.0;
|
||||||
v.ic2eq = 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);
|
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 SynthEngine::cutoffFor (const Voice& v, double baseCutoff, double envAmt, double envLevel)
|
||||||
{
|
{
|
||||||
double baseHz = paramReal (PCUTOFF, (float) baseCutoff);
|
double baseHz = paramReal (PCUTOFF, (float) baseCutoff);
|
||||||
|
|
@ -194,6 +221,8 @@ double SynthEngine::cutoffFor (const Voice& v, double baseCutoff, double envAmt,
|
||||||
void SynthEngine::process (AudioBuffer<float>& buffer, MidiBuffer& midi,
|
void SynthEngine::process (AudioBuffer<float>& buffer, MidiBuffer& midi,
|
||||||
int n, double bpm, bool transportPlaying)
|
int n, double bpm, bool transportPlaying)
|
||||||
{
|
{
|
||||||
|
if (n <= 0) return;
|
||||||
|
|
||||||
wasPlaying = transportPlaying;
|
wasPlaying = transportPlaying;
|
||||||
lastBpmValue = (bpm > 10.0 && bpm < 300.0) ? bpm : 120.0;
|
lastBpmValue = (bpm > 10.0 && bpm < 300.0) ? bpm : 120.0;
|
||||||
|
|
||||||
|
|
@ -203,49 +232,33 @@ void SynthEngine::process (AudioBuffer<float>& buffer, MidiBuffer& midi,
|
||||||
for (int i = 0; i < kNumParams; ++i)
|
for (int i = 0; i < kNumParams; ++i)
|
||||||
snap[(size_t) i] = params[i];
|
snap[(size_t) i] = params[i];
|
||||||
}
|
}
|
||||||
const float* const params = snap.data();
|
|
||||||
|
|
||||||
const float cutN01 = params[PCUTOFF];
|
// Collect MIDI into a list sorted by sample and dispatch each event at its
|
||||||
const float resN01 = params[PRES];
|
// exact position inside the block, instead of firing everything at sample 0.
|
||||||
const float fenvN01 = params[PFENV];
|
struct Ev { int at; MidiMessage msg; };
|
||||||
const float ftypeN01= params[PFTYPE];
|
std::vector<Ev> events;
|
||||||
const float atkN01 = params[PATK];
|
events.reserve (midi.getNumEvents());
|
||||||
const float decN01 = params[PDEC];
|
for (const auto& metadata : midi)
|
||||||
const float susN01 = params[PSUS];
|
events.push_back ({ juce::jlimit (0, n - 1, metadata.samplePosition), metadata.getMessage() });
|
||||||
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 atkSec = paramReal (PATK, snap[(size_t) PATK]);
|
||||||
const double decSec = paramReal (PDEC, decN01);
|
const double decSec = paramReal (PDEC, snap[(size_t) PDEC]);
|
||||||
const double susLevel = susN01;
|
const double susLevel = snap[(size_t) PSUS];
|
||||||
const double relSec = paramReal (PREL, relN01);
|
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 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 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 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 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;
|
const int filterType = paramFilterType (snap[(size_t) PFTYPE]);
|
||||||
(void) fenvScale;
|
const int wave1 = paramWaveIndex (snap[(size_t) PO1WAVE]);
|
||||||
|
const int wave2 = paramWaveIndex (snap[(size_t) PO2WAVE]);
|
||||||
|
|
||||||
const float gMix = mixN01;
|
bool delaySync = snap[(size_t) PDSYNC] > 0.5f;
|
||||||
const float g1 = 1.0f - gMix;
|
int stepIdx = juce::jlimit (0, 15, (int) juce::roundToInt (snap[(size_t) PDSTEPS]));
|
||||||
const float g2 = gMix;
|
float delayMsParam = paramReal (PDTIME, snap[(size_t) PDTIME]);
|
||||||
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]);
|
|
||||||
float delaySec;
|
float delaySec;
|
||||||
if (delaySync)
|
if (delaySync)
|
||||||
delaySec = (float) (stepsBeats()[stepIdx] * 60.0 / lastBpmValue);
|
delaySec = (float) (stepsBeats()[stepIdx] * 60.0 / lastBpmValue);
|
||||||
|
|
@ -254,29 +267,52 @@ void SynthEngine::process (AudioBuffer<float>& buffer, MidiBuffer& midi,
|
||||||
delaySec = std::clamp (delaySec, 0.001f, 4.0f);
|
delaySec = std::clamp (delaySec, 0.001f, 4.0f);
|
||||||
lastDelaySamples = delaySec * sampleRate;
|
lastDelaySamples = delaySec * sampleRate;
|
||||||
|
|
||||||
const float delayFB = params[PDFB];
|
const float* const sp = smooth.data();
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
float* outL = buffer.getWritePointer (0);
|
float* outL = buffer.getWritePointer (0);
|
||||||
float* outR = buffer.getNumChannels() > 1 ? buffer.getWritePointer (1) : nullptr;
|
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)
|
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)
|
for (auto& v : voices)
|
||||||
{
|
{
|
||||||
|
|
@ -298,14 +334,14 @@ void SynthEngine::process (AudioBuffer<float>& 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;
|
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 o1 = oscWave (wave1, v.ph1);
|
||||||
float o2 = oscWave (paramWaveIndex (params[PO2WAVE]), v.ph2);
|
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;
|
raw *= 0.7f;
|
||||||
|
|
||||||
float cutoffHz = cutoffFor (v, cutN01, fenvN01, v.env);
|
float cutoffHz = cutoffFor (v, sp[(size_t) PCUTOFF], sp[(size_t) PFENV], v.env);
|
||||||
float Q = 0.5f + resN01 * 14.0f;
|
float Q = 0.5f + sp[(size_t) PRES] * 14.0f;
|
||||||
float k = 1.0f / Q;
|
float k = 1.0f / Q;
|
||||||
double f = cutoffHz / (float) sampleRate;
|
double f = cutoffHz / (float) sampleRate;
|
||||||
if (f > 0.49) f = 0.49;
|
if (f > 0.49) f = 0.49;
|
||||||
|
|
@ -321,33 +357,29 @@ void SynthEngine::process (AudioBuffer<float>& buffer, MidiBuffer& midi,
|
||||||
v.ic1eq = 2.0 * v1 - v.ic1eq;
|
v.ic1eq = 2.0 * v1 - v.ic1eq;
|
||||||
v.ic2eq = 2.0 * v2 - v.ic2eq;
|
v.ic2eq = 2.0 * v2 - v.ic2eq;
|
||||||
|
|
||||||
float filtered;
|
float filtered = ftA * (float) v2 + ftB * (float) v1 + ftC * (float) (vin - k * v1 - v2);
|
||||||
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);
|
// short per-note fade-in kills onset/steal clicks
|
||||||
sig *= (float) (v.env * v.velocity * master);
|
float vg = v.age >= v.fadeLen ? 1.0f : (float) v.age / (float) v.fadeLen;
|
||||||
sumL += sig;
|
|
||||||
sumR += sig;
|
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 w = delay.writePos[0];
|
||||||
int r = (w - (int) lastDelaySamples + delay.maxLen * 2) % delay.maxLen;
|
int r = (w - (int) lastDelaySamples + delay.maxLen * 2) % delay.maxLen;
|
||||||
float delayed = delay.buf[0][r];
|
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);
|
fb = softClip (fb * 0.7f);
|
||||||
delay.buf[0][w] = fb;
|
delay.buf[0][w] = fb;
|
||||||
delay.buf[1][w] = fb;
|
delay.buf[1][w] = fb;
|
||||||
delay.writePos[0] = (w + 1) % delay.maxLen;
|
delay.writePos[0] = (w + 1) % delay.maxLen;
|
||||||
delay.writePos[1] = (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];
|
if (outR) outR[s] = outL[s];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <JuceHeader.h>
|
#include <JuceHeader.h>
|
||||||
|
#include <array>
|
||||||
#include "SynthParams.h"
|
#include "SynthParams.h"
|
||||||
#include "Presets.h"
|
#include "Presets.h"
|
||||||
|
|
||||||
|
|
@ -54,6 +55,7 @@ private:
|
||||||
double env = 0.0;
|
double env = 0.0;
|
||||||
int envState = 0;
|
int envState = 0;
|
||||||
int age = 0;
|
int age = 0;
|
||||||
|
int fadeLen = 32;
|
||||||
double ic1eq = 0.0, ic2eq = 0.0;
|
double ic1eq = 0.0, ic2eq = 0.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -101,6 +103,10 @@ private:
|
||||||
|
|
||||||
int currentPresetIndex = 0;
|
int currentPresetIndex = 0;
|
||||||
mutable juce::SpinLock paramLock;
|
mutable juce::SpinLock paramLock;
|
||||||
|
|
||||||
|
std::array<float, kNumParams> smooth;
|
||||||
|
float smCoef = 1.0f;
|
||||||
|
float ftA = 1.0f, ftB = 0.0f, ftC = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sm26
|
} // namespace sm26
|
||||||
Loading…
Add table
Add a link
Reference in a new issue