Conversation
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.
There was a problem hiding this comment.
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
Open (5)
Preserve float precision for stateful oscillator accumulators · New Sanitize non-finite and out-of-range waveform selectors · New Center pulse-width triangle output to prevent DC offset and clipping · New Add independent seeded pitch drift to the sub oscillator · New Add comprehensive automated coverage for the BLEP oscillator · New
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.dsppolysynth. - 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.
| 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.
|
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 |
| /* 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; |
There was a problem hiding this comment.
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.


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 forosc::simple. Itsphasearg sets where a voice's cycle starts. This is a new node rather than a change toosc::simple, so every existing patch keeps its sound.dsp/analog.dsp: a polysynth with:Unisoncents apart, the flat and sharp copies panned bySpreadIntervalsemitones away, and a sub an octave downmisc::drifton each oscillator's pitch (up toDriftcents) and on the cutoff, seeded from the note so the piece still replays exactlyfilt::svflow-passes in series (24 dB/oct) with their own envelope, velocity and key trackingTesting
ctest -R "dsp|fx|catalog|gencheck|nodes": 18/18 pass.docs/NODES.mdis regenerated.analog.dsp: no clipping or NaN, and the side channel sits 9.4 dB under the mid.