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:
+58
-34
@@ -6,48 +6,77 @@ namespace serum
|
|||||||
void Oscillator::reset()
|
void Oscillator::reset()
|
||||||
{
|
{
|
||||||
for (auto& v : voices)
|
for (auto& v : voices)
|
||||||
{
|
v = SubVoice {};
|
||||||
v.phase = 0.0;
|
|
||||||
v.detuneRatio = 1.0;
|
|
||||||
v.pan = 0.0f;
|
|
||||||
v.level = 1.0f;
|
|
||||||
}
|
|
||||||
activeUnison = 1;
|
activeUnison = 1;
|
||||||
|
initializedUnison = 0;
|
||||||
|
paramsValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Oscillator::noteOn (double freqHz, const OscParams& p, juce::uint32 seed)
|
void Oscillator::noteOn (double freqHz, const OscParams& p, juce::uint32 seed)
|
||||||
{
|
{
|
||||||
jassert (freqHz > 0.0);
|
jassert (freqHz > 0.0);
|
||||||
rng.setSeed (seed);
|
rng.setSeed (seed);
|
||||||
|
initializedUnison = 0;
|
||||||
|
paramsValid = false;
|
||||||
|
setParams (p);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Oscillator::setParams (const OscParams& p) noexcept
|
||||||
|
{
|
||||||
const int uni = juce::jlimit (1, kMaxUnison, p.unison);
|
const int uni = juce::jlimit (1, kMaxUnison, p.unison);
|
||||||
activeUnison = uni;
|
const bool countChanged = ! paramsValid || uni != activeUnison;
|
||||||
|
const bool detuneChanged = countChanged || p.detune != cachedParams.detune;
|
||||||
|
const bool panChanged = countChanged || p.pan != cachedParams.pan || p.spread != cachedParams.spread;
|
||||||
|
if (! detuneChanged && ! panChanged)
|
||||||
|
return;
|
||||||
|
|
||||||
const double basePhase = (double) p.phase * kTwoPi;
|
const double basePhase = (double) p.phase * kTwoPi;
|
||||||
|
for (int v = initializedUnison; v < uni; ++v)
|
||||||
for (int v = 0; v < uni; ++v)
|
|
||||||
{
|
{
|
||||||
// Even phase spacing prevents cancellation across unison voices.
|
// Even phase spacing distributes the initial unison phases.
|
||||||
double offset = (uni > 1) ? ((double) v / (double) uni) * kTwoPi : 0.0;
|
double offset = (uni > 1) ? ((double) v / (double) uni) * kTwoPi : 0.0;
|
||||||
double random = rng.nextFloat() * (double) p.randPhase * kTwoPi;
|
double random = rng.nextFloat() * (double) p.randPhase * kTwoPi;
|
||||||
voices[(size_t) v].phase = basePhase + offset + random;
|
voices[(size_t) v].phase = basePhase + offset + random;
|
||||||
|
|
||||||
// Detune: linear spread in cents, 0..50 cents at full depth.
|
|
||||||
double detuneCents = 0.0;
|
|
||||||
if (uni > 1)
|
|
||||||
detuneCents = (double) p.detune * 50.0 * ((double) (v - (uni - 1) / 2.0) / (double) ((uni - 1) / 2.0));
|
|
||||||
voices[(size_t) v].detuneRatio = std::pow (2.0, detuneCents / 1200.0);
|
|
||||||
|
|
||||||
// Stereo spread.
|
|
||||||
float panPos = (uni > 1) ? ((float) v / (float) (uni - 1) - 0.5f) * 2.0f * p.spread : 0.0f;
|
|
||||||
voices[(size_t) v].pan = panPos;
|
|
||||||
|
|
||||||
// Gain scaling with a centre emphasis for odd unison counts.
|
|
||||||
float lvl = 1.0f / std::sqrt ((float) uni);
|
|
||||||
if ((uni & 1) && v == uni / 2)
|
|
||||||
lvl *= 1.3f;
|
|
||||||
voices[(size_t) v].level = lvl;
|
|
||||||
}
|
}
|
||||||
|
initializedUnison = juce::jmax (initializedUnison, uni);
|
||||||
|
|
||||||
|
const float unisonLevel = 1.0f / std::sqrt ((float) uni);
|
||||||
|
for (int v = 0; v < uni; ++v)
|
||||||
|
{
|
||||||
|
auto& sv = voices[(size_t) v];
|
||||||
|
if (detuneChanged)
|
||||||
|
{
|
||||||
|
// Detune: linear spread in cents, 0..50 cents at full depth.
|
||||||
|
double detuneCents = 0.0;
|
||||||
|
if (uni > 1)
|
||||||
|
detuneCents = (double) p.detune * 50.0 * ((double) (v - (uni - 1) / 2.0) / (double) ((uni - 1) / 2.0));
|
||||||
|
sv.detuneRatio = std::pow (2.0, detuneCents / 1200.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (panChanged)
|
||||||
|
{
|
||||||
|
// Stereo spread.
|
||||||
|
float panPos = (uni > 1) ? ((float) v / (float) (uni - 1) - 0.5f) * 2.0f * p.spread : 0.0f;
|
||||||
|
panPos = juce::jlimit (-1.0f, 1.0f, panPos + p.pan);
|
||||||
|
|
||||||
|
// Constant-power pan.
|
||||||
|
const float panAngle = (panPos + 1.0f) * 0.5f * juce::MathConstants<float>::halfPi;
|
||||||
|
sv.panL = std::cos (panAngle);
|
||||||
|
sv.panR = std::sin (panAngle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (countChanged)
|
||||||
|
{
|
||||||
|
// Gain scaling with a centre emphasis for odd unison counts.
|
||||||
|
float lvl = unisonLevel;
|
||||||
|
if ((uni & 1) && v == uni / 2)
|
||||||
|
lvl *= 1.3f;
|
||||||
|
sv.level = lvl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activeUnison = uni;
|
||||||
|
cachedParams = p;
|
||||||
|
paramsValid = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
float Oscillator::warpPhase (float phase, const OscParams& p) const noexcept
|
float Oscillator::warpPhase (float phase, const OscParams& p) const noexcept
|
||||||
@@ -96,7 +125,7 @@ void Oscillator::processAdd (const Wavetable& wt, const OscParams& p, double fre
|
|||||||
if (! p.enabled || p.level <= 0.0f || freqHz <= 0.0)
|
if (! p.enabled || p.level <= 0.0f || freqHz <= 0.0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const int uni = juce::jlimit (1, kMaxUnison, p.unison);
|
const int uni = activeUnison;
|
||||||
const float framePos = p.wtPos * 255.0f;
|
const float framePos = p.wtPos * 255.0f;
|
||||||
const double phaseInc = kTwoPi * freqHz / sr;
|
const double phaseInc = kTwoPi * freqHz / sr;
|
||||||
|
|
||||||
@@ -116,14 +145,9 @@ void Oscillator::processAdd (const Wavetable& wt, const OscParams& p, double fre
|
|||||||
float sample = wt.readSafe (framePos, warpPhase (phase01, p));
|
float sample = wt.readSafe (framePos, warpPhase (phase01, p));
|
||||||
sample = warpSample (sample, p);
|
sample = warpSample (sample, p);
|
||||||
|
|
||||||
// Constant-power pan.
|
|
||||||
const float panAngle = (sv.pan + 1.0f) * 0.5f * 1.5707963267948966f;
|
|
||||||
const float panL = std::cos (panAngle);
|
|
||||||
const float panR = std::sin (panAngle);
|
|
||||||
|
|
||||||
const float gain = sv.level * p.level;
|
const float gain = sv.level * p.level;
|
||||||
accL += sample * gain * panL;
|
accL += sample * gain * sv.panL;
|
||||||
accR += sample * gain * panR;
|
accR += sample * gain * sv.panR;
|
||||||
}
|
}
|
||||||
|
|
||||||
outL += accL;
|
outL += accL;
|
||||||
|
|||||||
+8
-3
@@ -41,8 +41,9 @@ public:
|
|||||||
void prepare (double sampleRate) { sr = sampleRate; reset(); }
|
void prepare (double sampleRate) { sr = sampleRate; reset(); }
|
||||||
void reset();
|
void reset();
|
||||||
|
|
||||||
// (Re)configure unison sub-voices: phase offsets, detune, pan and gain.
|
// Initialise a fresh note's unison sub-voices: phase offsets, detune, pan and gain.
|
||||||
void noteOn (double freqHz, const OscParams& p, juce::uint32 seed);
|
void noteOn (double freqHz, const OscParams& p, juce::uint32 seed);
|
||||||
|
void setParams (const OscParams& p) noexcept;
|
||||||
|
|
||||||
// Accumulate this oscillator's contribution into outL/outR.
|
// Accumulate this oscillator's contribution into outL/outR.
|
||||||
void processAdd (const Wavetable& wt, const OscParams& p, double freqHz,
|
void processAdd (const Wavetable& wt, const OscParams& p, double freqHz,
|
||||||
@@ -55,16 +56,20 @@ private:
|
|||||||
{
|
{
|
||||||
double phase = 0.0;
|
double phase = 0.0;
|
||||||
double detuneRatio = 1.0;
|
double detuneRatio = 1.0;
|
||||||
float pan = 0.0f;
|
float panL = 0.70710678f;
|
||||||
|
float panR = 0.70710678f;
|
||||||
float level = 1.0f;
|
float level = 1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::array<SubVoice, kMaxUnison> voices;
|
std::array<SubVoice, kMaxUnison> voices;
|
||||||
int activeUnison = 1;
|
int activeUnison = 1;
|
||||||
|
int initializedUnison = 0;
|
||||||
|
bool paramsValid = false;
|
||||||
|
OscParams cachedParams;
|
||||||
double sr = 44100.0;
|
double sr = 44100.0;
|
||||||
juce::Random rng;
|
juce::Random rng;
|
||||||
|
|
||||||
static constexpr double kTwoPi = 6.28318530717958647692;
|
static constexpr double kTwoPi = juce::MathConstants<double>::twoPi;
|
||||||
|
|
||||||
float warpPhase (float phase, const OscParams& p) const noexcept;
|
float warpPhase (float phase, const OscParams& p) const noexcept;
|
||||||
float warpSample (float sample, const OscParams& p) const noexcept;
|
float warpSample (float sample, const OscParams& p) const noexcept;
|
||||||
|
|||||||
+21
-8
@@ -35,7 +35,8 @@ void SynthVoice::reset()
|
|||||||
velocity = 0.0f;
|
velocity = 0.0f;
|
||||||
baseFreq = 0.0;
|
baseFreq = 0.0;
|
||||||
active = released = false;
|
active = released = false;
|
||||||
lastUnisonA = lastUnisonB = 1;
|
noteId = 0;
|
||||||
|
oscillatorsNeedNoteOn = false;
|
||||||
noteRandom = 0.5f;
|
noteRandom = 0.5f;
|
||||||
scratch.clear();
|
scratch.clear();
|
||||||
}
|
}
|
||||||
@@ -48,7 +49,8 @@ void SynthVoice::noteOn (int noteNumber, float velocity01, double freqHz, juce::
|
|||||||
active = true;
|
active = true;
|
||||||
released = false;
|
released = false;
|
||||||
seed = noteSeed;
|
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);
|
juce::Random rng (noteSeed);
|
||||||
noteRandom = rng.nextFloat();
|
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 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);
|
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
|
// Initialise phases only for a fresh note; update held-note unison parameters
|
||||||
// on every block when unison is LFO-modulated at control rate).
|
// without resetting existing phases when modulated at control rate.
|
||||||
if (a.unison != lastUnisonA) { oscA.noteOn (freqA, a, seed); lastUnisonA = a.unison; }
|
if (oscillatorsNeedNoteOn)
|
||||||
if (b.unison != lastUnisonB) { oscB.noteOn (freqB, b, seed + 1); lastUnisonB = b.unison; }
|
{
|
||||||
|
oscA.noteOn (freqA, a, seed);
|
||||||
|
oscB.noteOn (freqB, b, seed + 1);
|
||||||
|
oscillatorsNeedNoteOn = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
oscA.setParams (a);
|
||||||
|
oscB.setParams (b);
|
||||||
|
}
|
||||||
|
|
||||||
// 7. Modulated filter parameters.
|
// 7. Modulated filter parameters.
|
||||||
FilterBankParams fb = ctx.filters;
|
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& wtA = ctx.wavetables->getTable (a.wave);
|
||||||
const Wavetable& wtB = ctx.wavetables->getTable (b.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 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)
|
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;
|
float mono = 0.0f;
|
||||||
if (ctx.subOn)
|
if (ctx.subOn)
|
||||||
sub.processAdd (bent * subMult, ctx.subShape, ctx.subLevel, mono);
|
sub.processAdd (bent * subMult, ctx.subShape, subLevel, mono);
|
||||||
if (ctx.noiseOn)
|
if (ctx.noiseOn)
|
||||||
noise.processAdd (ctx.noiseType, ctx.noiseLevel, mono);
|
noise.processAdd (ctx.noiseType, noiseLevel, mono);
|
||||||
l += mono;
|
l += mono;
|
||||||
r += mono;
|
r += mono;
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -87,7 +87,7 @@ private:
|
|||||||
bool released = false;
|
bool released = false;
|
||||||
juce::uint64 noteId = 0;
|
juce::uint64 noteId = 0;
|
||||||
|
|
||||||
int lastUnisonA = 1, lastUnisonB = 1;
|
bool oscillatorsNeedNoteOn = false;
|
||||||
|
|
||||||
juce::uint32 seed = 0;
|
juce::uint32 seed = 0;
|
||||||
float noteRandom = 0.5f;
|
float noteRandom = 0.5f;
|
||||||
|
|||||||
Reference in New Issue
Block a user