Skip to content

Commit 610a7f0

Browse files
committed
Add CZ-style resonant sweep extended mode
A new RESONANT_SWEEP option in each operator's extended mode multiplies a sine running at k(m)x the fundamental by a fundamental-phase window, giving CZ-style formant resonance. Closed-form windows (saw, triangle, half-trapezoid, full-trapezoid) live in dsp/resonant_window.h; the table-based windows (Hann, Blackman-Harris, Tukey) reuse the existing SinTable via a second SinTable member on OpSource. Two new per-op params: a window-shape selector and a "Sweep Depth" selector (2 / 4 / 10) that scales how aggressively k(m) climbs with M. The existing M / Env->M / LFO->M controls are reused for both PHASE_REMAP and RESONANT_SWEEP modes. UI gets a multiswitch + jog button + placeholder for the future spectrum plot. Assisted-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 1ffc1a6 commit 610a7f0

6 files changed

Lines changed: 359 additions & 30 deletions

File tree

doc/12_roadmap.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ I have a lot of ideas for a '1.2' version of six sines. Not 2.0. Still compatibl
1616
- Add a cytomic SVF at 16khz / root 2 resonance
1717
- ZOH decimate the signal to 32khz at upper rate
1818
- Do a bitheight redicution (round(f * (1<<b)) / (1<<b))
19+
- Add an optional highpass at 5-15hz to kill DC
1920
- Downsample with strategy
2021
- Each of those steps except the first and last is optional.
2122
- basically all options to make it less 'clean' which all toggle at internal block
@@ -26,19 +27,17 @@ I have a lot of ideas for a '1.2' version of six sines. Not 2.0. Still compatibl
2627
- that crazy idea kisney and i chatted about
2728
- more t/k
2829

29-
## UI Ratio editor Upgrade
30-
31-
The ratio editor is a knob. That's clumsy. segmented control will be better.
32-
Want modes - like segmented float, ratio as pair of ints, etc...
33-
Some of those modes like ratio as pair of ints requires extra streaming to store and retain
34-
Probably want a legacy mode of knob
35-
3630
## Infrastructure
3731

3832
- Clap Wrapper Standalone upgrades
3933
- windows ui open isn't right
4034
- jack on linux
4135

36+
## UI Ratio editor Upgrade **DONE**
37+
38+
The ratio editor is a knob. That's clumsy. segmented control will be better.
39+
Want modes - like segmented float,
40+
4241
## Visualization **DONE**
4342

4443
- Add a simple built in spectrum and scope

src/dsp/op_source.h

Lines changed: 119 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include "synth/mono_values.h"
2828
#include "synth/voice_values.h"
2929
#include "remap_functions.h"
30+
#include "resonant_window.h"
3031

3132
namespace baconpaul::six_sines
3233
{
@@ -84,6 +85,28 @@ struct alignas(16) OpSource : public EnvelopeSupport<Patch::SourceNode>,
8485
lfoResetMod();
8586

8687
st.setSampleRate(monoValues.sr.sampleRate);
88+
stWindow.setSampleRate(monoValues.sr.sampleRate);
89+
// Pick the table for the resonant-sweep window. BLACKMAN_HARRIS and TUKEY pull
90+
// their own tables; everything else (closed-form windows + HANN) defaults to
91+
// HANN so a mid-note shape change doesn't read a stale unrelated table — the
92+
// closed-form arms in the inner loop don't read stWindow at all.
93+
{
94+
using RW = Patch::SourceNode::ResonantSweepWindow;
95+
auto rw = static_cast<RW>(
96+
static_cast<uint32_t>(std::round(sourceNode.resonantSweepWindowShape.value)));
97+
switch (rw)
98+
{
99+
case RW::BLACKMAN_HARRIS:
100+
stWindow.setWaveForm(SinTable::BLACKMAN_HARRIS_WINDOW);
101+
break;
102+
case RW::TUKEY:
103+
stWindow.setWaveForm(SinTable::TUKEY_WINDOW);
104+
break;
105+
default:
106+
stWindow.setWaveForm(SinTable::HANN_WINDOW);
107+
break;
108+
}
109+
}
87110
firstTime = true;
88111
extendedMPrior = sourceNode.extendedModeM.value;
89112
// Configure the M/N lags only if the operator is actually using an extended mode
@@ -92,12 +115,12 @@ struct alignas(16) OpSource : public EnvelopeSupport<Patch::SourceNode>,
92115
using EM = Patch::SourceNode::ExtendedMode;
93116
auto em = static_cast<EM>(
94117
static_cast<uint32_t>(std::round(sourceNode.extendedModeMode.value)));
95-
if (em == EM::PHASE_REMAP)
118+
if (em == EM::PHASE_REMAP || em == EM::RESONANT_SWEEP)
96119
{
97120
extendedLagM.setRateInMilliseconds(10, monoValues.sr.samplerate, blockSizeInv);
98121
extendedLagM.snapTo(sourceNode.extendedModeM.value);
99-
// N is unused in PHASE_REMAP — leave its lag uninitialized until a future
100-
// mode that consumes N is added.
122+
// N is unused in PHASE_REMAP / RESONANT_SWEEP — leave its lag uninitialized
123+
// until a future mode that consumes N is added.
101124
}
102125
}
103126
zeroInputs();
@@ -168,7 +191,7 @@ struct alignas(16) OpSource : public EnvelopeSupport<Patch::SourceNode>,
168191
using EM = Patch::SourceNode::ExtendedMode;
169192
auto em =
170193
static_cast<EM>(static_cast<uint32_t>(std::round(sourceNode.extendedModeMode.value)));
171-
if (em == EM::PHASE_REMAP)
194+
if (em == EM::PHASE_REMAP || em == EM::RESONANT_SWEEP)
172195
used = used || (sourceNode.lfoToExtendedModeM.value != 0);
173196

174197
return used;
@@ -363,23 +386,77 @@ struct alignas(16) OpSource : public EnvelopeSupport<Patch::SourceNode>,
363386
break;
364387
}
365388
case EM::RESONANT_SWEEP:
366-
// coming soon — falls through to the no-extension path for now
367-
innerLoopImpl<EM::NONE>(onto, fbv, rf, dRF, phs);
389+
{
390+
using RW = Patch::SourceNode::ResonantSweepWindow;
391+
using RFD = Patch::SourceNode::ResonantSweepFrequencyDepth;
392+
auto rw = static_cast<RW>(
393+
static_cast<uint32_t>(std::round(sourceNode.resonantSweepWindowShape.value)));
394+
auto rfd = static_cast<RFD>(
395+
static_cast<uint32_t>(std::round(sourceNode.resonantSweepFrequencyDepth.value)));
396+
float kScale = 4.0f;
397+
switch (rfd)
398+
{
399+
case RFD::TWO:
400+
kScale = 2.0f;
401+
break;
402+
case RFD::FOUR:
403+
kScale = 4.0f;
404+
break;
405+
case RFD::TEN:
406+
kScale = 10.0f;
407+
break;
408+
}
409+
// The PhaseMapShape template arg is unused on this branch; leave it at default.
410+
switch (rw)
411+
{
412+
case RW::SAW:
413+
innerLoopImpl<EM::RESONANT_SWEEP, Patch::SourceNode::PhaseMapShape::SAW, RW::SAW>(
414+
onto, fbv, rf, dRF, phs, kScale);
415+
break;
416+
case RW::TRIANGLE:
417+
innerLoopImpl<EM::RESONANT_SWEEP, Patch::SourceNode::PhaseMapShape::SAW,
418+
RW::TRIANGLE>(onto, fbv, rf, dRF, phs, kScale);
419+
break;
420+
case RW::TRAPEZOID:
421+
innerLoopImpl<EM::RESONANT_SWEEP, Patch::SourceNode::PhaseMapShape::SAW,
422+
RW::TRAPEZOID>(onto, fbv, rf, dRF, phs, kScale);
423+
break;
424+
case RW::FULLTRAP:
425+
innerLoopImpl<EM::RESONANT_SWEEP, Patch::SourceNode::PhaseMapShape::SAW,
426+
RW::FULLTRAP>(onto, fbv, rf, dRF, phs, kScale);
427+
break;
428+
case RW::HANN:
429+
innerLoopImpl<EM::RESONANT_SWEEP, Patch::SourceNode::PhaseMapShape::SAW, RW::HANN>(
430+
onto, fbv, rf, dRF, phs, kScale);
431+
break;
432+
case RW::BLACKMAN_HARRIS:
433+
innerLoopImpl<EM::RESONANT_SWEEP, Patch::SourceNode::PhaseMapShape::SAW,
434+
RW::BLACKMAN_HARRIS>(onto, fbv, rf, dRF, phs, kScale);
435+
break;
436+
case RW::TUKEY:
437+
innerLoopImpl<EM::RESONANT_SWEEP, Patch::SourceNode::PhaseMapShape::SAW, RW::TUKEY>(
438+
onto, fbv, rf, dRF, phs, kScale);
439+
break;
440+
}
368441
break;
442+
}
369443
case EM::REDACTED_1:
370444
// coming soon — falls through to the no-extension path for now
371445
innerLoopImpl<EM::NONE>(onto, fbv, rf, dRF, phs);
372446
break;
373447
}
374448
}
375449

376-
template <Patch::SourceNode::ExtendedMode ET,
377-
Patch::SourceNode::PhaseMapShape S = Patch::SourceNode::PhaseMapShape::SAW>
378-
void innerLoopImpl(float *onto, float *fbv, float rf, const float dRF, uint32_t &phs)
450+
template <
451+
Patch::SourceNode::ExtendedMode ET,
452+
Patch::SourceNode::PhaseMapShape S = Patch::SourceNode::PhaseMapShape::SAW,
453+
Patch::SourceNode::ResonantSweepWindow R = Patch::SourceNode::ResonantSweepWindow::SAW>
454+
void innerLoopImpl(float *onto, float *fbv, float rf, const float dRF, uint32_t &phs,
455+
float kScale = 1.0f)
379456
{
380457
using EM = Patch::SourceNode::ExtendedMode;
381458
float nextM{0.f}, dM{0.f};
382-
if constexpr (ET == EM::PHASE_REMAP)
459+
if constexpr (ET == EM::PHASE_REMAP || ET == EM::RESONANT_SWEEP)
383460
{
384461
// Raw target m for this block: patch value + external mod + env / lfo contributions.
385462
auto lfoFac = *lfoFacP;
@@ -412,6 +489,7 @@ struct alignas(16) OpSource : public EnvelopeSupport<Patch::SourceNode>,
412489

413490
auto ph = phs + phaseInput[i] + (int32_t)(feedbackLevel[i] * fb);
414491

492+
float out;
415493
if constexpr (ET == EM::PHASE_REMAP)
416494
{
417495
using PM = Patch::SourceNode::PhaseMapShape;
@@ -428,9 +506,35 @@ struct alignas(16) OpSource : public EnvelopeSupport<Patch::SourceNode>,
428506
else if constexpr (S == PM::DOUBLE_SAW)
429507
ph = remap::remapDoubleSaw(ph & phase::phaseMask, nextM);
430508
nextM += dM;
509+
out = st.at(ph);
510+
}
511+
else if constexpr (ET == EM::RESONANT_SWEEP)
512+
{
513+
using RW = Patch::SourceNode::ResonantSweepWindow;
514+
auto wph = ph & phase::phaseMask;
515+
float window;
516+
if constexpr (R == RW::TRIANGLE)
517+
window = resonant_window::windowTriangle(wph);
518+
else if constexpr (R == RW::TRAPEZOID)
519+
window = resonant_window::windowTrapezoid(wph);
520+
else if constexpr (R == RW::FULLTRAP)
521+
window = resonant_window::windowFullTrapezoid(wph);
522+
else if constexpr (R == RW::HANN || R == RW::BLACKMAN_HARRIS || R == RW::TUKEY)
523+
window = stWindow.at(wph);
524+
else // RW::SAW and any unhandled future value — keep `window` defined.
525+
window = resonant_window::windowSaw(wph);
526+
// Inner phase runs at k(m)x the fundamental and natural-wraps at the
527+
// fundamental boundary via uint32 multiplication (mod phaseMax).
528+
// kScale is the patch-selected sweep depth (2 / 4 / 10).
529+
auto kFactor = kScale * nextM + 1.0f;
530+
uint32_t kmph = static_cast<uint32_t>(static_cast<float>(wph) * kFactor);
531+
nextM += dM;
532+
out = window * st.at(kmph);
533+
}
534+
else
535+
{
536+
out = st.at(ph);
431537
}
432-
433-
auto out = st.at(ph);
434538

435539
out = out * rmLevel[i];
436540
onto[i] = out;
@@ -506,6 +610,9 @@ struct alignas(16) OpSource : public EnvelopeSupport<Patch::SourceNode>,
506610
}
507611

508612
SinTable st;
613+
// Used by RESONANT_SWEEP for table-based windows (Hann / Blackman-Harris / Tukey).
614+
// Independent of `st` so the operator's main waveform stays selectable freely.
615+
SinTable stWindow;
509616
float fbVal[2]{0.f, 0.f};
510617
};
511618
} // namespace baconpaul::six_sines

src/dsp/resonant_window.h

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Six Sines
3+
*
4+
* A synth with audio rate modulation.
5+
*
6+
* Copyright 2024-2025, Paul Walker and Various authors, as described in the github
7+
* transaction log.
8+
*
9+
* This source repo is released under the MIT license, but has
10+
* GPL3 dependencies, as such the combined work will be
11+
* released under GPL3.
12+
*
13+
* The source code and license are at https://github.com/baconpaul/six-sines
14+
*/
15+
16+
#ifndef BACONPAUL_SIX_SINES_DSP_RESONANT_WINDOW_H
17+
#define BACONPAUL_SIX_SINES_DSP_RESONANT_WINDOW_H
18+
19+
#include <cstdint>
20+
#include <cmath>
21+
22+
#include "dsp/sintable.h" // baconpaul::six_sines::phase constants
23+
24+
/*
25+
* Closed-form window functions for the CZ-style resonant sweep mode.
26+
*
27+
* Each window is multiplied by sin(2*pi * k(m) * inner_phase) where
28+
* inner_phase resets to 0 each time the fundamental phase wraps. The
29+
* window itself is a function of the fundamental phase and provides
30+
* the spectral body of the waveform.
31+
*
32+
* Float reference forms (phi in [0, 1)):
33+
* windowSaw(phi) = 1 - phi
34+
* windowTriangle(phi) = 1 - |2*phi - 1|
35+
* windowTrapezoid(phi) = phi < 1/2 ? 1 : 2*(1 - phi)
36+
* windowFullTrapezoid(phi) = phi < 1/4 ? 4*phi
37+
* : phi < 3/4 ? 1
38+
* : 4*(1 - phi)
39+
*
40+
* Integer-phase signature: phase is a uint32_t in [0, phaseMax),
41+
* matching the rest of the engine. Output is a float window amplitude
42+
* in [0, 1]. The caller multiplies this by the inner-phase sine.
43+
*
44+
* Hann / Blackman-Harris / Tukey windows are pulled from the SinTable
45+
* (`OpSource::stWindow`) rather than implemented here.
46+
*/
47+
48+
namespace baconpaul::six_sines::resonant_window
49+
{
50+
using namespace baconpaul::six_sines::phase;
51+
52+
static constexpr float invPhaseMaxF = 1.0f / phaseMaxF;
53+
54+
// Form 5 -- Resonant Saw window
55+
// Descending ramp: 1 at phase = 0, 0 at phase = phaseMax.
56+
// Hard restart at the wrap is the source of the saw-like spectral envelope.
57+
inline float windowSaw(uint32_t phase) { return 1.0f - static_cast<float>(phase) * invPhaseMaxF; }
58+
59+
// Form 6 -- Resonant Triangle window
60+
// Triangle: 0 at phase = 0, peaks at 1 at phase = halfPhase, back to 0 at phase = phaseMax.
61+
// No discontinuity at the wrap; smoother, more vowel-like resonance.
62+
inline float windowTriangle(uint32_t phase)
63+
{
64+
const float p = static_cast<float>(phase) * invPhaseMaxF;
65+
return 1.0f - std::fabs(2.0f * p - 1.0f);
66+
}
67+
68+
// Form 7 -- Resonant Trapezoid window
69+
// Flat at 1 for phase in [0, halfPhase), then linear decay to 0 at phase = phaseMax.
70+
// Pulse-like body with a strong formant from the hard restart at phase = 0.
71+
inline float windowTrapezoid(uint32_t phase)
72+
{
73+
if (phase < halfPhase)
74+
return 1.0f;
75+
return 2.0f * (1.0f - static_cast<float>(phase) * invPhaseMaxF);
76+
}
77+
78+
// Symmetric full trapezoid: 0 -> 1 over [0, 1/4), held at 1 over [1/4, 3/4),
79+
// 1 -> 0 over [3/4, 1). No discontinuity at the wrap; flat-topped pulse body.
80+
inline float windowFullTrapezoid(uint32_t phase)
81+
{
82+
constexpr uint32_t quarterPhase = phaseMax >> 2;
83+
constexpr uint32_t threeQuarterPhase = quarterPhase * 3;
84+
if (phase < quarterPhase)
85+
return 4.0f * static_cast<float>(phase) * invPhaseMaxF;
86+
if (phase < threeQuarterPhase)
87+
return 1.0f;
88+
return 4.0f * (1.0f - static_cast<float>(phase) * invPhaseMaxF);
89+
}
90+
91+
} // namespace baconpaul::six_sines::resonant_window
92+
#endif

0 commit comments

Comments
 (0)