Sound Byte Libs 29c5ff3
C++ firmware library for audio applications on 32-bit ARM Cortex-M processors
Loading...
Searching...
No Matches
polyblep_osc.hpp
Go to the documentation of this file.
1// sbl/widgets/source/polyblep_osc.hpp — PolyBLEP oscillator (Audio Stack — Widgets)
2//
3// Band-limited saw, square, triangle, and pulse waveforms using polynomial
4// band-limited step (PolyBLEP) correction at discontinuities. Zero flash
5// cost — no lookup tables needed.
6//
7// Saw/Square/Pulse use 2nd-order PolyBLEP at step discontinuities.
8// Triangle uses 4th-order Integrated PolyBLEP at slope discontinuities,
9// directly band-limiting the naive triangle without a post-filter.
10//
11// Adapted from Mutable Instruments Plaits oscillator.h and Warps
12// oscillator.cc (MIT license, Copyright 2014-2016 Emilie Gillet).
13// PolyBLEP math from stmlib/dsp/polyblep.h.
14//
15// Usage:
16// sbl::widgets::source::PolyBlepOsc osc;
17// osc.set_waveform(Waveform::Saw);
18// osc.set_frequency(440.0f);
19// osc.process(buf, frames);
20
21#pragma once
22
23#include <cstdint>
24
25#include <sbl/dsp/polyblep.hpp>
26#include <sbl/dsp/pitch.hpp>
27
28namespace sbl::widgets::source {
29
30enum class Waveform : uint8_t {
31 Saw,
32 Square,
34 Pulse,
35};
36
38public:
39 /// @note All public methods are ISR-safe — bounded computation, no I/O.
40
41 /** @brief Set active waveform */
42 void set_waveform(Waveform w) { waveform_ = w; }
43
44 /**
45 * @brief Set frequency in Hz
46 * @param freq_hz Frequency in Hz (e.g., 440.0f)
47 * @param sr Sample rate (default 48000)
48 */
49 void set_frequency(float freq_hz, float sr = 48000.0f) {
50 frequency_ = freq_hz / sr;
51 }
52
53 /**
54 * @brief Set frequency from MIDI note number (requires FPU)
55 * @param midi_note MIDI note (69 = A4 = 440 Hz)
56 * @param sample_rate Sample rate
57 */
58 void set_note(float midi_note, float sample_rate = 48000.0f) {
59 frequency_ = dsp::note_to_frequency(midi_note) / sample_rate;
60 }
61
62 /**
63 * @brief Set pulse width (only affects Pulse waveform)
64 * @param pw Pulse width, clamped to [0.05, 0.95]. Default 0.5.
65 */
66 void set_pulse_width(float pw) {
67 pw_ = (pw < 0.05f) ? 0.05f : (pw > 0.95f) ? 0.95f : pw;
68 }
69
70 /** @brief Set output amplitude (0.0 = silence, 1.0 = full scale) */
71 void set_amplitude(float amp) { amplitude_ = amp; }
72
73 /**
74 * @brief Generate audio samples
75 * @param out Output buffer (float, [-1.0, 1.0])
76 * @param frames Number of samples
77 */
78 void process(float* out, uint16_t frames) {
79 uint16_t pos = 0;
80 while (pos < frames) {
81 uint16_t n = frames - pos;
82 if (n > MAX_BLOCK) n = MAX_BLOCK;
83 render_float(out + pos, n);
84 if (amplitude_ != 1.0f) {
85 for (uint16_t i = 0; i < n; ++i) {
86 out[pos + i] *= amplitude_;
87 }
88 }
89 pos += n;
90 }
91 }
92
93 /** @brief Hard sync — reset phase to zero */
94 void sync() {
95 phase_ = 0.0f;
96 next_sample_ = 0.0f;
97 high_ = true;
98 }
99
100 /** @brief Reset phase to a specific value (0.0–1.0) */
101 void sync(float phase) {
102 phase_ = phase;
103 next_sample_ = 0.0f;
104 }
105
106 /** @brief Current phase as uint32_t (for API compatibility with WavetableOsc) */
107 uint32_t phase() const {
108 return static_cast<uint32_t>(phase_ * 4294967296.0f);
109 }
110
111 /** @brief Current normalized frequency (freq_hz / sample_rate) */
112 float frequency() const { return frequency_; }
113
114private:
115 static constexpr uint16_t MAX_BLOCK = 48;
116
117 float phase_ = 0.0f;
118 float frequency_ = 0.0f;
119 float pw_ = 0.5f;
120 float next_sample_ = 0.0f;
121 bool high_ = true;
122 Waveform waveform_ = Waveform::Saw;
123 float amplitude_ = 1.0f;
124
125 // -------------------------------------------------------------------------
126 // Dispatch to waveform-specific float render
127 // -------------------------------------------------------------------------
128 void render_float(float* out, uint16_t frames) {
129 switch (waveform_) {
130 case Waveform::Saw: render_saw_f(out, frames); break;
131 case Waveform::Square: render_square_f(out, frames, 0.5f); break;
132 case Waveform::Triangle: render_triangle_f(out, frames); break;
133 case Waveform::Pulse: render_square_f(out, frames, pw_); break;
134 }
135 }
136
137 // -------------------------------------------------------------------------
138 // Saw — single discontinuity at phase wrap (Warps lines 141-155)
139 // -------------------------------------------------------------------------
140 void render_saw_f(float* out, uint16_t frames) {
141 using namespace dsp::polyblep;
142 float phase = phase_;
143 float next = next_sample_;
144 float freq = frequency_;
145
146 for (uint16_t i = 0; i < frames; ++i) {
147 float this_sample = next;
148 next = 0.0f;
149
150 phase += freq;
151 if (phase >= 1.0f) {
152 phase -= 1.0f;
153 float t = phase / freq;
154 this_sample -= this_blep(t);
155 next -= next_blep(t);
156 }
157 next += phase;
158 this_sample = 2.0f * this_sample - 1.0f;
159
160 out[i] = clamp(this_sample);
161 }
162 phase_ = phase;
163 next_sample_ = next;
164 }
165
166 // -------------------------------------------------------------------------
167 // Square / Pulse — two edges per cycle
168 // -------------------------------------------------------------------------
169 void render_square_f(float* out, uint16_t frames, float pw) {
170 using namespace dsp::polyblep;
171 float phase = phase_;
172 float next = next_sample_;
173 float freq = frequency_;
174 bool high = high_;
175
176 float fall_at = pw;
177
178 for (uint16_t i = 0; i < frames; ++i) {
179 float this_sample = next;
180 next = 0.0f;
181
182 phase += freq;
183
184 // Falling edge at duty cycle boundary
185 if (high && phase >= fall_at) {
186 float t = (phase - fall_at) / freq;
187 this_sample -= this_blep(t);
188 next -= next_blep(t);
189 high = false;
190 }
191 // Rising edge at phase wrap
192 if (phase >= 1.0f) {
193 phase -= 1.0f;
194 float t = phase / freq;
195 this_sample += this_blep(t);
196 next += next_blep(t);
197 high = true;
198 }
199
200 next += high ? 1.0f : 0.0f;
201 this_sample = 2.0f * this_sample - 1.0f;
202
203 out[i] = clamp(this_sample);
204 }
205 phase_ = phase;
206 next_sample_ = next;
207 high_ = high;
208 }
209
210 // -------------------------------------------------------------------------
211 // Triangle — Integrated PolyBLEP on slope discontinuities
212 // -------------------------------------------------------------------------
213 void render_triangle_f(float* out, uint16_t frames) {
214 using namespace dsp::polyblep;
215 float phase = phase_;
216 float next = next_sample_;
217 float freq = frequency_;
218 bool high = high_;
219
220 constexpr float slope_up = 2.0f;
221 constexpr float slope_down = 2.0f;
222 const float discontinuity = (slope_up + slope_down) * freq;
223
224 for (uint16_t i = 0; i < frames; ++i) {
225 float this_sample = next;
226 next = 0.0f;
227
228 phase += freq;
229
230 // Slope change at midpoint (rising → falling)
231 if (high ^ (phase < 0.5f)) {
232 float t = (phase - 0.5f) / freq;
233 this_sample -= this_integrated_blep(t) * discontinuity;
234 next -= next_integrated_blep(t) * discontinuity;
235 high = phase < 0.5f;
236 }
237 // Slope change at wrap (falling → rising)
238 if (phase >= 1.0f) {
239 phase -= 1.0f;
240 float t = phase / freq;
241 this_sample += this_integrated_blep(t) * discontinuity;
242 next += next_integrated_blep(t) * discontinuity;
243 high = true;
244 }
245
246 // Naive triangle: rises [0, 0.5) then falls [0.5, 1.0)
247 next += high
248 ? phase * slope_up
249 : 1.0f - (phase - 0.5f) * slope_down;
250
251 out[i] = clamp(2.0f * this_sample - 1.0f);
252 }
253 phase_ = phase;
254 next_sample_ = next;
255 high_ = high;
256 }
257
258 // -------------------------------------------------------------------------
259 // Float clamp to [-1, 1]
260 // -------------------------------------------------------------------------
261 static float clamp(float s) {
262 return (s > 1.0f) ? 1.0f : (s < -1.0f) ? -1.0f : s;
263 }
264};
265
266} // namespace sbl::widgets::source
uint32_t phase() const
Current phase as uint32_t (for API compatibility with WavetableOsc)
void set_amplitude(float amp)
Set output amplitude (0.0 = silence, 1.0 = full scale)
void set_frequency(float freq_hz, float sr=48000.0f)
Set frequency in Hz.
void set_pulse_width(float pw)
Set pulse width (only affects Pulse waveform)
void set_note(float midi_note, float sample_rate=48000.0f)
Set frequency from MIDI note number (requires FPU)
void sync()
Hard sync — reset phase to zero.
void process(float *out, uint16_t frames)
Generate audio samples.
float frequency() const
Current normalized frequency (freq_hz / sample_rate)
void set_waveform(Waveform w)
Set active waveform.
void sync(float phase)
Reset phase to a specific value (0.0–1.0)
float next_integrated_blep(float t)
Definition polyblep.hpp:46
float this_integrated_blep(float t)
Correction for the current sample at a slope discontinuity.
Definition polyblep.hpp:54
float this_blep(float t)
Definition polyblep.hpp:31
float next_blep(float t)
Correction for the next sample at a step discontinuity.
Definition polyblep.hpp:36
float note_to_frequency(float midi_note)
MIDI note to frequency in Hz. A4 = 440 Hz.
Definition pitch.hpp:52
Sound source widgets.
Definition noise.hpp:15