Skip to content

osc: blep, and an analog polysynth with drift and unison spread - #333

Merged
mishan merged 4 commits into
masterfrom
warmth
Oct 4, 2026
Merged

mishan merged 4 commits into
masterfrom
warmth

Conversation

@mishan

@mishan mishan commented Oct 4, 2026

Copy link
Copy Markdown
Owner

Item 5 of the instrument list: analog warmth.

  • osc::blep (new plugin): a PolyBLEP sawtooth and pulse, plus a triangle integrated from the band-limited pulse. On a sustained C7 saw at 44.1 kHz, aliased energy is -30.7 dB relative to the harmonics, against -12.7 dB for osc::simple. Its phase arg sets where a voice's cycle starts. This is a new node rather than a change to osc::simple, so every existing patch keeps its sound.
  • dsp/analog.dsp: a polysynth with:
    • three unison oscillators Unison cents apart, the flat and sharp copies panned by Spread
    • a second oscillator Interval semitones away, and a sub an octave down
    • a misc::drift on each oscillator's pitch (up to Drift cents) and on the cutoff, seeded from the note so the piece still replays exactly
    • two filt::svf low-passes in series (24 dB/oct) with their own envelope, velocity and key tracking

Testing

  • ctest -R "dsp|fx|catalog|gencheck|nodes": 18/18 pass. docs/NODES.md is regenerated.
  • The web build compiles the new plugin.
  • Rendered a phrase on analog.dsp: no clipping or NaN, and the side channel sits 9.4 dB under the mid.

mishan added 2 commits October 4, 2026 11:05
PolyBLEP corrections at each edge, the triangle a leaky integral of
the band-limited pulse. On a C7 saw at 44.1 kHz the folded energy is
-30.7 dB against the harmonics, where osc::simple's is -12.7 dB. A
phase arg sets where a voice's cycle starts, so unison copies do not
begin with their edges together.
Three osc::blep in unison spread across the stereo field, a second
oscillator and a sub, each wandering on its own misc::drift seeded
from the note, into two filt::svf in series for 24 dB an octave.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The triangle can exceed its declared range, oscillator state depends on callback size, and direct behavioral tests are missing.

Review effort: Balanced
Findings: 3 Medium severity · 2 Low severity

Open (5)
What changed in this PR

Adds a PolyBLEP oscillator and an analog-style polysynth using drift, unison, and stereo filtering.

Changes:

  • Adds saw, pulse, and triangle PolyBLEP waveforms.
  • Adds the analog.dsp polysynth.
  • Registers and documents the oscillator.
File Description
plugins/​osc/​blep.cpp Implements the oscillator.
plugins/​CMakeLists.txt Registers the plugin.
dsp/​analog.dsp Defines the analog polysynth.
docs/​NODES.md Documents oscillator arguments.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread plugins/osc/blep.cpp Outdated
Comment thread plugins/osc/blep.cpp Outdated
Comment thread plugins/osc/blep.cpp Outdated
Comment thread dsp/analog.dsp
Comment thread plugins/osc/blep.cpp
Comment on lines +131 to +132
int module_callback (thNode *node, thSynthTree *mod, unsigned int windowlen,
unsigned int samples)
The phase and the triangle's integrator step in float, as they are
kept between windows, so one sample a window and five hundred agree.
A waveform that is NaN or outside 0..2 plays the saw rather than
reaching a cast. The triangle integrates the pulse less its mean,
divided by pw (1 - pw), from where the wave is at its starting phase,
with a slow leak: centered and within full scale at any width, where
the default patch's 0.4 had put it near -1.7. The sub drifts on its
own like every other oscillator in analog.dsp. statecheck covers the
bounds, the selector, phase, sync, a C7 saw's folded 21st harmonic and
window agreement.
@mishan

mishan commented Oct 4, 2026

Copy link
Copy Markdown
Owner Author

Addressed in 31f06e1: the phase and triangle state step in float; a NaN or out-of-range waveform plays the saw; the triangle is centered and within full scale at any width; the sub has its own drift; and statecheck now covers bounds, the selector, phase, sync, aliasing of a C7 saw's folded 21st harmonic (more than 40 dB down) and window agreement.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Triangle state handling and selector validation produce incorrect output for valid input values and automation.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
Resolved since last review (4)

Comment thread plugins/osc/blep.cpp
/* A selector: whole numbers 0 to 2, and anything else -- NaN
included, which no cast to int may be handed -- the saw. */
const float w = (*in_waveform)[i];
const int wave = (thIsFinite(w) && w >= 0 && w < 3) ? (int)w : 0;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the comment rather than the code, in c9385b9. This follows osc::simple, whose selector is read the same way ("3.4 is a triangle and so is 3.9"): the whole part picks, and both declare their names with setArgValues, so the control is stepped. Anything below 0, from 3 up, or NaN still plays the saw. Rejecting fractions in this one oscillator would make the two disagree.

The whole part picks, as in osc::simple, whose "3.4 is a triangle and
so is 3.9" is the convention the stepped control is drawn from.
@mishan
mishan merged commit ac650de into master Oct 4, 2026
14 checks passed
@mishan
mishan deleted the warmth branch October 4, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants