Skip to content

Commit 88127c2

Browse files
authored
Track unison spread and pan continuously while a voice is playing (#370)
Previously the per-voice unison ratio multiplier and pan shift were frozen at note-on, so moving the Unison Spread or Unison Pan knobs (or automating them) only affected newly-triggered notes. Both quantities are now derived per engine block from the smoothed mono parameter values, so held notes follow knob moves and host automation. Assisted-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 2a64aff commit 88127c2

4 files changed

Lines changed: 34 additions & 26 deletions

File tree

src/synth/mono_values.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ struct MonoValues
8181

8282
SRProvider sr;
8383

84+
// Per-engine-block hoists of mono unison params, refreshed by Synth::processInternal
85+
// so each Voice::renderBlock can derive uniRatioMul / uniPanShift without per-voice
86+
// table lookups. unisonSpreadFactorMinus1 = 2^unisonSpread.value - 1.
87+
float unisonSpreadFactorMinus1{0.f};
88+
float unisonPanScalar{0.f};
89+
8490
float audioInBlock alignas(16)[blockSize]{}; // engine-rate audio in, mono mix
8591
};
8692
}; // namespace baconpaul::six_sines

src/synth/synth.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,12 @@ template <bool multiOut> void Synth::processInternal(const clap_output_events_t
289289
loops++;
290290
lagHandler.process();
291291

292+
// Hoist mono unison params so per-voice renderBlock derives uniRatioMul / uniPanShift
293+
// from the smoothed scalars without each voice repeating the twoToTheX lookup.
294+
monoValues.unisonSpreadFactorMinus1 =
295+
monoValues.twoToTheX.twoToThe(patch.output.unisonSpread.value) - 1.f;
296+
monoValues.unisonPanScalar = patch.output.unisonPan.value;
297+
292298
auto op1IsAudioIn =
293299
((int)std::round(patch.sourceNodes[0].waveForm.value) == SinTable::AUDIO_IN);
294300
if (audioInResampler && op1IsAudioIn)

src/synth/synth.h

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -158,33 +158,18 @@ struct Synth
158158
int made{0};
159159

160160
int lastStart{0};
161-
auto usp = synth.patch.output.unisonSpread.value;
162-
usp = synth.monoValues.twoToTheX.twoToThe(usp);
163-
float uniVal[5]{1.0};
164-
float uniPan[5]{0.0};
165-
float uniScale[5]{0.0};
166161
assert(ct <= 5);
167-
if (ct > 1)
168-
{
169-
auto dUni = 2 * (usp - 1) / (ct - 1);
170-
for (int i = 0; i < ct; ++i)
171-
{
172-
auto val = (1.0 - (usp - 1)) + dUni * i;
173-
if (val < 1)
174-
{
175-
val = 1.0 / ((1.0 + (usp - 1)) - dUni * i);
176-
}
177-
uniVal[i] = val;
178-
uniScale[i] = 2 * (i - 1.0 * (ct - 1) / 2) / (ct - 1);
179-
uniPan[i] =
180-
std::clamp(synth.patch.output.unisonPan.value * uniScale[i], -1.f, 1.f);
181-
}
182-
}
162+
const bool hasCenter = (ct > 1 && (ct % 2 == 1));
183163

184164
auto upr = synth.patch.output.uniPhaseRand.value > 0.5;
185165
auto prt = synth.patch.output.rephaseOnRetrigger > 0.5;
186166
for (int vc = 0; vc < ct; ++vc)
187167
{
168+
// Bipolar position −1..1 across the unison field; 0 when ct==1.
169+
// uniRatioMul and uniPanShift are derived from this in Voice::renderBlock
170+
// each block, so they track host smoothing on unisonSpread / unisonPan.
171+
const float uniScale = (ct > 1) ? (2.f * (vc - 0.5f * (ct - 1)) / (ct - 1)) : 0.f;
172+
188173
if (ibuf[vc].instruction !=
189174
sst::voicemanager::VoiceInitInstructionsEntry<
190175
baconpaul::six_sines::Synth::VMConfig>::Instruction::SKIP)
@@ -202,12 +187,12 @@ struct Synth
202187
synth.voices[i].voiceValues.releaseVelocity = 0;
203188
synth.voices[i].voiceValues.uniCount = ct;
204189
synth.voices[i].voiceValues.uniIndex = vc;
205-
synth.voices[i].voiceValues.hasCenterVoice = (ct > 1 && (ct % 2 == 1));
190+
synth.voices[i].voiceValues.hasCenterVoice = hasCenter;
206191
synth.voices[i].voiceValues.isCenterVoice =
207-
(ct > 1 && (ct % 2 == 1)) && (std::fabs(uniScale[vc]) < 1e-4);
208-
synth.voices[i].voiceValues.uniRatioMul = uniVal[vc];
209-
synth.voices[i].voiceValues.uniPanShift = uniPan[vc];
210-
synth.voices[i].voiceValues.uniPMScale = uniScale[vc];
192+
hasCenter && (std::fabs(uniScale) < 1e-4f);
193+
synth.voices[i].voiceValues.uniRatioMul = 1.f;
194+
synth.voices[i].voiceValues.uniPanShift = 0.f;
195+
synth.voices[i].voiceValues.uniPMScale = uniScale;
211196
synth.voices[i].voiceValues.phaseRandom = (vc > 0 && upr);
212197
synth.voices[i].voiceValues.rephaseOnRetrigger = (!upr && prt);
213198
synth.voices[i].voiceValues.noteExpressionTuningInSemis = 0;

src/synth/voice.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,17 @@ void Voice::attack()
7272

7373
void Voice::renderBlock()
7474
{
75+
// Refresh unison-derived per-voice scalars from the (smoothed) mono hoists so
76+
// unisonSpread / unisonPan track host automation and UI knob moves mid-note.
77+
if (voiceValues.uniCount > 1)
78+
{
79+
const float prod = monoValues.unisonSpreadFactorMinus1 * voiceValues.uniPMScale;
80+
voiceValues.uniRatioMul =
81+
(voiceValues.uniPMScale >= 0.f) ? (1.f + prod) : (1.f / (1.f - prod));
82+
voiceValues.uniPanShift =
83+
std::clamp(monoValues.unisonPanScalar * voiceValues.uniPMScale, -1.f, 1.f);
84+
}
85+
7586
float retuneKey = voiceValues.key;
7687
if (monoValues.mtsClient && MTS_HasMaster(monoValues.mtsClient))
7788
{

0 commit comments

Comments
 (0)