refactor(osc): support held-note parameter updates without phase reset
Add Oscillator::setParams() which updates detune, pan, and gain for already-initialised sub-voices without resetting their phases. This preserves phase continuity when unison or spread is LFO-modulated at control rate. noteOn() now just seeds phases and delegates to setParams(). Precompute panL/panR in SubVoice instead of calling cos/sin per sample. Add paramsValid and cachedParams to skip redundant work. Replace SynthVoice's lastUnisonA/B tracking with a single oscillatorsNeedNoteOn flag: noteOn sets it, render clears it after the first block. Add SubLevel and NoiseLevel to the per-voice modulation targets so they can be modulated like other parameters.
This commit is contained in:
+21
-8
@@ -35,7 +35,8 @@ void SynthVoice::reset()
|
||||
velocity = 0.0f;
|
||||
baseFreq = 0.0;
|
||||
active = released = false;
|
||||
lastUnisonA = lastUnisonB = 1;
|
||||
noteId = 0;
|
||||
oscillatorsNeedNoteOn = false;
|
||||
noteRandom = 0.5f;
|
||||
scratch.clear();
|
||||
}
|
||||
@@ -48,7 +49,8 @@ void SynthVoice::noteOn (int noteNumber, float velocity01, double freqHz, juce::
|
||||
active = true;
|
||||
released = false;
|
||||
seed = noteSeed;
|
||||
lastUnisonA = lastUnisonB = 0; // force unison reconfigure on first render
|
||||
noteId = noteSeed;
|
||||
oscillatorsNeedNoteOn = true; // initialise fresh oscillator phases on first render
|
||||
|
||||
juce::Random rng (noteSeed);
|
||||
noteRandom = rng.nextFloat();
|
||||
@@ -151,10 +153,19 @@ void SynthVoice::render (float* outL, float* outR, int numSamples, const RenderC
|
||||
const double freqA = bent * std::pow (2.0, (double) a.coarse / 12.0 + (double) a.fine / 1200.0);
|
||||
const double freqB = bent * std::pow (2.0, (double) b.coarse / 12.0 + (double) b.fine / 1200.0);
|
||||
|
||||
// Reconfigure unison only when the integer count changes (avoids phase reset
|
||||
// on every block when unison is LFO-modulated at control rate).
|
||||
if (a.unison != lastUnisonA) { oscA.noteOn (freqA, a, seed); lastUnisonA = a.unison; }
|
||||
if (b.unison != lastUnisonB) { oscB.noteOn (freqB, b, seed + 1); lastUnisonB = b.unison; }
|
||||
// Initialise phases only for a fresh note; update held-note unison parameters
|
||||
// without resetting existing phases when modulated at control rate.
|
||||
if (oscillatorsNeedNoteOn)
|
||||
{
|
||||
oscA.noteOn (freqA, a, seed);
|
||||
oscB.noteOn (freqB, b, seed + 1);
|
||||
oscillatorsNeedNoteOn = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
oscA.setParams (a);
|
||||
oscB.setParams (b);
|
||||
}
|
||||
|
||||
// 7. Modulated filter parameters.
|
||||
FilterBankParams fb = ctx.filters;
|
||||
@@ -177,6 +188,8 @@ void SynthVoice::render (float* outL, float* outR, int numSamples, const RenderC
|
||||
const Wavetable& wtA = ctx.wavetables->getTable (a.wave);
|
||||
const Wavetable& wtB = ctx.wavetables->getTable (b.wave);
|
||||
const double subMult = (ctx.subOct == -2) ? 0.25 : (ctx.subOct == -1) ? 0.5 : 1.0;
|
||||
const float subLevel = clampF (ctx.subLevel + mod[(int) ModTarget::SubLevel], 0.0f, 1.0f);
|
||||
const float noiseLevel = clampF (ctx.noiseLevel + mod[(int) ModTarget::NoiseLevel], 0.0f, 1.0f);
|
||||
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
@@ -186,9 +199,9 @@ void SynthVoice::render (float* outL, float* outR, int numSamples, const RenderC
|
||||
|
||||
float mono = 0.0f;
|
||||
if (ctx.subOn)
|
||||
sub.processAdd (bent * subMult, ctx.subShape, ctx.subLevel, mono);
|
||||
sub.processAdd (bent * subMult, ctx.subShape, subLevel, mono);
|
||||
if (ctx.noiseOn)
|
||||
noise.processAdd (ctx.noiseType, ctx.noiseLevel, mono);
|
||||
noise.processAdd (ctx.noiseType, noiseLevel, mono);
|
||||
l += mono;
|
||||
r += mono;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user