mirror of
https://codeberg.org/armin/monostep.git
synced 2026-09-01 04:10:46 +02:00
Add delay/reverb dry-wet, granular randomize toggles, rework accent DSP
- Add delay and reverb Dry/Wet parameters and knobs, wired into presets - Add Accent/Slide/Fine/Rate toggles to the Randomize panel with new per-aspect pattern randomizers - Soften accent: modest volume lift plus small cutoff and filter-attack modulation instead of the heavy 5x gain boost - Halve reverb comb/allpass delays so the wet speaks sooner - Regenerate a fresh build stamp on every build
This commit is contained in:
parent
342d413563
commit
662a1f7383
11 changed files with 310 additions and 97 deletions
|
|
@ -7,17 +7,19 @@ namespace monostep
|
|||
{
|
||||
|
||||
// Freeverb-style comb / allpass sizes, slightly offset per channel so the
|
||||
// left and right reverb tails decorrelate.
|
||||
// left and right reverb tails decorrelate. Halved from the classic freeverb
|
||||
// set so the early reflections land ~13 ms after the note instead of ~25 ms:
|
||||
// a 100%-wet reverb keeps its tail but speaks much sooner.
|
||||
static constexpr int combSizes[2][4] =
|
||||
{
|
||||
{ 1116, 1188, 1277, 1356 },
|
||||
{ 1126, 1198, 1287, 1366 }
|
||||
{ 558, 594, 639, 678 },
|
||||
{ 563, 599, 644, 683 }
|
||||
};
|
||||
|
||||
static constexpr int allpassSizes[2][2] =
|
||||
{
|
||||
{ 225, 556 },
|
||||
{ 235, 566 }
|
||||
{ 113, 278 },
|
||||
{ 118, 283 }
|
||||
};
|
||||
|
||||
float FxProcessor::delayTimeSeconds (float knob, bool sync, double bpm)
|
||||
|
|
@ -36,7 +38,7 @@ juce::String FxProcessor::delayTimeLabel (float knob, bool sync, double bpm)
|
|||
{
|
||||
if (sync)
|
||||
{
|
||||
static const char* names[] = { "1/16", "1/8", "1/8D", "1/4", "1/4D", "1/2", "1/2D", "1W" };
|
||||
static const char* names[] = { "1/16", "1/8", "3/16", "1/4", "3/8", "1/2", "3/4", "1 bar" };
|
||||
const int idx = juce::roundToInt (juce::jlimit (0.0f, 1.0f, knob) * 7.0f);
|
||||
return names[juce::jlimit (0, 7, idx)];
|
||||
}
|
||||
|
|
@ -112,13 +114,15 @@ void FxProcessor::reset()
|
|||
|
||||
void FxProcessor::setParams (const Params& p)
|
||||
{
|
||||
params.delayTime = juce::jlimit (0.0f, 1.0f, p.delayTime);
|
||||
params.delayMix = juce::jlimit (0.0f, 1.0f, p.delayMix);
|
||||
params.delaySync = p.delaySync;
|
||||
params.reverbRoom = juce::jlimit (0.0f, 1.0f, p.reverbRoom);
|
||||
params.reverbMix = juce::jlimit (0.0f, 1.0f, p.reverbMix);
|
||||
params.reverbDiff = juce::jlimit (0.0f, 1.0f, p.reverbDiff);
|
||||
params.bpm = juce::jmax (20.0, p.bpm);
|
||||
params.delayTime = juce::jlimit (0.0f, 1.0f, p.delayTime);
|
||||
params.delayFeedback = juce::jlimit (0.0f, 0.9f, p.delayFeedback);
|
||||
params.delaySync = p.delaySync;
|
||||
params.delayMix = juce::jlimit (0.0f, 1.0f, p.delayMix);
|
||||
params.reverbRoom = juce::jlimit (0.0f, 1.0f, p.reverbRoom);
|
||||
params.reverbLevel = juce::jlimit (0.0f, 1.0f, p.reverbLevel);
|
||||
params.reverbMix = juce::jlimit (0.0f, 1.0f, p.reverbMix);
|
||||
params.reverbDiff = juce::jlimit (0.0f, 1.0f, p.reverbDiff);
|
||||
params.bpm = juce::jmax (20.0, p.bpm);
|
||||
|
||||
currentDelaySeconds = delayTimeSeconds (params.delayTime, params.delaySync, params.bpm);
|
||||
}
|
||||
|
|
@ -128,8 +132,23 @@ void FxProcessor::process (juce::AudioBuffer<float>& buffer, int numSamples)
|
|||
if (numSamples <= 0 || buffer.getNumChannels() == 0)
|
||||
return;
|
||||
|
||||
// The reverb must feed off the dry (pre-delay) signal, not the delay's
|
||||
// output, so snapshot the incoming buffer before the delay runs.
|
||||
const bool reverbActive = params.reverbMix > 0.0001f && params.reverbLevel > 0.0001f;
|
||||
if (reverbActive)
|
||||
{
|
||||
if (dryScratch.getNumSamples() < numSamples)
|
||||
dryScratch.setSize (buffer.getNumChannels(), numSamples, false, false, true);
|
||||
|
||||
const int n = juce::jmin (buffer.getNumChannels(), dryScratch.getNumChannels());
|
||||
for (int c = 0; c < n; ++c)
|
||||
dryScratch.copyFrom (c, 0, buffer, c, 0, numSamples);
|
||||
}
|
||||
|
||||
processDelay (buffer, numSamples);
|
||||
processReverb (buffer, numSamples);
|
||||
|
||||
if (reverbActive)
|
||||
processReverb (dryScratch, buffer, numSamples);
|
||||
}
|
||||
|
||||
void FxProcessor::processDelay (juce::AudioBuffer<float>& buffer, int numSamples)
|
||||
|
|
@ -166,7 +185,7 @@ void FxProcessor::processDelay (juce::AudioBuffer<float>& buffer, int numSamples
|
|||
|
||||
// Feedback with a little high-frequency damping.
|
||||
d.feedbackLp += 0.35f * (delayed - d.feedbackLp);
|
||||
d.memory[d.writeIndex] = x + 0.42f * d.feedbackLp;
|
||||
d.memory[d.writeIndex] = x + params.delayFeedback * d.feedbackLp;
|
||||
|
||||
out[i] = x + delayed * mix;
|
||||
|
||||
|
|
@ -176,24 +195,38 @@ void FxProcessor::processDelay (juce::AudioBuffer<float>& buffer, int numSamples
|
|||
}
|
||||
}
|
||||
|
||||
void FxProcessor::processReverb (juce::AudioBuffer<float>& buffer, int numSamples)
|
||||
void FxProcessor::processReverb (const juce::AudioBuffer<float>& dryIn,
|
||||
juce::AudioBuffer<float>& buffer, int numSamples)
|
||||
{
|
||||
if (params.reverbMix <= 0.0001f)
|
||||
if (params.reverbMix <= 0.0001f || params.reverbLevel <= 0.0001f)
|
||||
return;
|
||||
|
||||
const int n = juce::jmin (numChannels, buffer.getNumChannels());
|
||||
const int n = juce::jmin (numChannels, juce::jmin (dryIn.getNumChannels(), buffer.getNumChannels()));
|
||||
const float combGain = 0.70f + 0.28f * params.reverbRoom; // 0.70 .. 0.98
|
||||
const float allpassGain = 0.75f * params.reverbDiff; // 0 .. 0.75
|
||||
// Room normalisation: the comb network's DC gain is ~4 / (1 - combGain),
|
||||
// so scaling by (1 - combGain) keeps the wet level bounded as the room
|
||||
// (decay length) grows instead of blowing up near oscillation. The 2.5
|
||||
// constant is calibrated (standalone replication of this network): at
|
||||
// room 0.5 it puts the note-off ring ~ +6 dB above the dry and the tail
|
||||
// ~ -6 dB one second in, so the reverb is clearly audible even at modest
|
||||
// dry/wet settings. The wet is intentionally NOT clipped here: any
|
||||
// soft-clip knee far below the signal level pins the wet at a constant
|
||||
// flat value (the early tail stops audibly decaying, which reads as a
|
||||
// "reverb slowly kicking in" plateau) and makes 100% wet sound quiet
|
||||
// next to a hot dry signal.
|
||||
const float wetScale = 2.5f * (1.0f - combGain);
|
||||
const float mix = params.reverbMix;
|
||||
|
||||
for (int ch = 0; ch < n; ++ch)
|
||||
{
|
||||
auto& r = reverbs[ch];
|
||||
const float* in = dryIn.getReadPointer (ch);
|
||||
auto* out = buffer.getWritePointer (ch);
|
||||
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
const float x = out[i];
|
||||
const float x = in[i];
|
||||
|
||||
float combSum = 0.0f;
|
||||
for (int c = 0; c < 4; ++c)
|
||||
|
|
@ -208,7 +241,7 @@ void FxProcessor::processReverb (juce::AudioBuffer<float>& buffer, int numSample
|
|||
pos = 0;
|
||||
}
|
||||
|
||||
float wet = combSum * 0.012f;
|
||||
float wet = combSum * wetScale * params.reverbLevel;
|
||||
|
||||
for (int a = 0; a < 2; ++a)
|
||||
{
|
||||
|
|
@ -223,7 +256,7 @@ void FxProcessor::processReverb (juce::AudioBuffer<float>& buffer, int numSample
|
|||
pos = 0;
|
||||
}
|
||||
|
||||
out[i] = x * (1.0f - mix) + wet * mix;
|
||||
out[i] += wet * mix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,15 @@ public:
|
|||
|
||||
struct Params
|
||||
{
|
||||
float delayTime = 0.35f; // normalized knob 0..1
|
||||
float delayMix = 0.0f; // 0..1 dry/wet
|
||||
bool delaySync = true; // tempo-synced vs. free milliseconds
|
||||
float reverbRoom = 0.5f; // 0..1
|
||||
float reverbMix = 0.0f; // 0..1 dry/wet
|
||||
float reverbDiff = 0.6f; // 0..1 allpass diffusion
|
||||
double bpm = 120.0;
|
||||
float delayTime = 0.35f; // normalized knob 0..1
|
||||
float delayFeedback = 0.45f; // 0..0.9 feedback (repeat density)
|
||||
bool delaySync = true; // tempo-synced vs. free milliseconds
|
||||
float delayMix = 0.0f; // 0..1 dry/wet
|
||||
float reverbRoom = 0.5f; // 0..1
|
||||
float reverbLevel = 1.0f; // 0..1 wet send gain
|
||||
float reverbMix = 0.0f; // 0..1 dry/wet
|
||||
float reverbDiff = 0.6f; // 0..1 allpass diffusion
|
||||
double bpm = 120.0;
|
||||
};
|
||||
|
||||
void setParams (const Params& p);
|
||||
|
|
@ -52,7 +54,7 @@ private:
|
|||
};
|
||||
|
||||
void processDelay (juce::AudioBuffer<float>& buffer, int numSamples);
|
||||
void processReverb (juce::AudioBuffer<float>& buffer, int numSamples);
|
||||
void processReverb (const juce::AudioBuffer<float>& dryIn, juce::AudioBuffer<float>& buffer, int numSamples);
|
||||
|
||||
double sampleRate = 44100.0;
|
||||
int numChannels = 2;
|
||||
|
|
@ -64,6 +66,10 @@ private:
|
|||
Params params;
|
||||
float currentDelaySeconds = 0.0f;
|
||||
|
||||
// Pre-delay snapshot: the reverb is fed the dry signal (before the delay),
|
||||
// so delay and reverb run in parallel instead of in series.
|
||||
juce::AudioBuffer<float> dryScratch;
|
||||
|
||||
std::vector<DelayChannel> delays;
|
||||
std::vector<ReverbChannel> reverbs;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -137,6 +137,10 @@ private:
|
|||
fenvStage = Stage::attack;
|
||||
}
|
||||
|
||||
// RC-style (exponential) filter envelope: the cutoff glides smoothly instead
|
||||
// of stepping linearly, so fast filter sweeps sound analog rather than
|
||||
// zippered. Each stage still reaches ~95% within the labelled time, so the
|
||||
// knob values keep their musical meaning.
|
||||
void updateFilterEnvelope()
|
||||
{
|
||||
const float sr = sampleRate;
|
||||
|
|
@ -144,23 +148,37 @@ private:
|
|||
switch (fenvStage)
|
||||
{
|
||||
case Stage::attack:
|
||||
fenv += 1.0f / (params.fAttack * sr);
|
||||
if (fenv >= 1.0f) { fenv = 1.0f; fenvStage = Stage::decay; }
|
||||
{
|
||||
// Accented notes attack the filter ever so slightly faster.
|
||||
const float acc = accentSmooth - 1.0f;
|
||||
const float t = juce::jmax (0.0005f, params.fAttack / (1.0f + acc * 0.5f));
|
||||
const float a = 1.0f - std::exp (-3.0f / (t * sr));
|
||||
fenv += (1.0f - fenv) * a;
|
||||
if (fenv >= 0.999f) { fenv = 1.0f; fenvStage = Stage::decay; }
|
||||
break;
|
||||
}
|
||||
|
||||
case Stage::decay:
|
||||
fenv -= (1.0f - params.fSustain) / (params.fDecay * sr);
|
||||
if (fenv <= params.fSustain) { fenv = params.fSustain; fenvStage = Stage::sustain; }
|
||||
{
|
||||
const float t = juce::jmax (0.0005f, params.fDecay);
|
||||
const float a = 1.0f - std::exp (-3.0f / (t * sr));
|
||||
fenv += (params.fSustain - fenv) * a;
|
||||
if (std::fabs (fenv - params.fSustain) < 0.001f) { fenv = params.fSustain; fenvStage = Stage::sustain; }
|
||||
break;
|
||||
}
|
||||
|
||||
case Stage::sustain:
|
||||
fenv = params.fSustain;
|
||||
break;
|
||||
|
||||
case Stage::release:
|
||||
fenv -= 1.0f / (params.fRelease * sr);
|
||||
if (fenv <= 0.0f) { fenv = 0.0f; fenvStage = Stage::idle; }
|
||||
{
|
||||
const float t = juce::jmax (0.0005f, params.fRelease);
|
||||
const float a = 1.0f - std::exp (-3.0f / (t * sr));
|
||||
fenv -= fenv * a;
|
||||
if (fenv <= 0.001f) { fenv = 0.0f; fenvStage = Stage::idle; }
|
||||
break;
|
||||
}
|
||||
|
||||
case Stage::idle:
|
||||
break;
|
||||
|
|
@ -278,15 +296,18 @@ private:
|
|||
|
||||
float renderSample()
|
||||
{
|
||||
updateEnvelope();
|
||||
updateFilterEnvelope();
|
||||
updateGlide();
|
||||
|
||||
// Accent gains fast attack / slow release, so the boost fades out smoothly
|
||||
// at the tail of an accented note instead of cutting off hard-edged.
|
||||
accentSmooth += (accentLevel - accentSmooth)
|
||||
* (accentLevel > accentSmooth ? accentAttackCoef : accentReleaseCoef);
|
||||
|
||||
// 0 for unaccented steps, rising with the accent knob on accented ones.
|
||||
const float accBoost = accentSmooth - 1.0f;
|
||||
|
||||
updateEnvelope();
|
||||
updateFilterEnvelope();
|
||||
updateGlide();
|
||||
|
||||
const float incA = (currentFreq * params.detuneRatioA) / sampleRate;
|
||||
const float incB = (currentFreq * params.detuneRatio) / sampleRate;
|
||||
|
||||
|
|
@ -302,14 +323,17 @@ private:
|
|||
out = (1.0f - params.ringMod) * out + params.ringMod * (a * b);
|
||||
|
||||
const float preFilter = out;
|
||||
|
||||
// Accented steps open the filter a tiny bit, so they sparkle without
|
||||
// relying on a big volume jump.
|
||||
const float envCutoff = juce::jlimit (20.0f, 20000.0f,
|
||||
params.cutoff * std::pow (2.0f, params.fAmount * fenv));
|
||||
params.cutoff * std::pow (2.0f, params.fAmount * fenv + accBoost * 1.5f));
|
||||
out = processFilter (out, envCutoff);
|
||||
|
||||
// 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 (accBoost > 0.0f)
|
||||
out += accBoost * 0.2f * preFilter;
|
||||
|
||||
if (params.drive > 0.0f)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue