Sound Byte Libs 29c5ff3
C++ firmware library for audio applications on 32-bit ARM Cortex-M processors
Loading...
Searching...
No Matches
supersaw_osc.hpp
Go to the documentation of this file.
1// sbl/widgets/source/supersaw_osc.hpp — SuperSaw oscillator (Audio Stack — Widgets)
2//
3// Three PolyBLEP saws — center pitch plus two detuned copies — with
4// configurable stereo spread. The classic JP-8000 "super saw" sound,
5// scaled to 3 oscillators for embedded CPU budgets.
6//
7// Detuned copies are offset symmetrically in cents from the center
8// pitch. Stereo spread pans them into the stereo field: 0% = mono,
9// 100% = hard L/R. The center always stays centered.
10//
11// Float-domain rendering pipeline: PolyBLEP renders to float,
12// pan_mix() accumulates in float, output is float.
13//
14// Usage:
15// sbl::widgets::source::SuperSawOsc osc;
16// osc.set_note(60.0f); // Middle C
17// osc.set_detune(15.0f); // 15 cents spread
18// osc.set_stereo_spread(0.5f); // 50% stereo width
19//
20// // In audio callback:
21// float left[48] = {}, right[48] = {};
22// osc.process_stereo(left, right, frames); // accumulates into L/R
23
24#pragma once
25
26#include <cstdint>
27
28#include <sbl/dsp/stereo.hpp>
30
31namespace sbl::widgets::source {
32
34public:
35 /// @note All public methods are ISR-safe — bounded computation, no I/O.
36
38 set_amplitude(amplitude_);
39 }
40
41 /**
42 * @brief Set pitch from MIDI note number
43 * @param midi_note MIDI note (69 = A4 = 440 Hz)
44 * @param sample_rate Sample rate (default 48000)
45 */
46 void set_note(float midi_note, float sample_rate = 48000.0f) {
47 note_ = midi_note;
48 sample_rate_ = sample_rate;
49 recompute_frequencies();
50 }
51
52 /**
53 * @brief Set frequency in Hz
54 * @param freq_hz Frequency in Hz (e.g., 440.0f)
55 * @param sr Sample rate (default 48000)
56 */
57 void set_frequency(float freq_hz, float sr = 48000.0f) {
58 center_.set_frequency(freq_hz, sr);
59 note_ = 69.0f + 12.0f * log2f(freq_hz / 440.0f);
60 sample_rate_ = sr;
61 recompute_detuned();
62 }
63
64 /**
65 * @brief Set detune spread in cents
66 * @param cents Detune spread in cents (0–50 typical)
67 */
68 void set_detune(float cents) {
69 detune_cents_ = cents;
70 recompute_detuned();
71 }
72
73 /**
74 * @brief Set stereo spread
75 * @param spread 0.0 = mono (all center), 1.0 = hard L/R
76 */
77 void set_stereo_spread(float spread) {
78 if (spread == stereo_spread_) return;
79 stereo_spread_ = spread;
80 recompute_pan();
81 }
82
83 /**
84 * @brief Set output amplitude
85 *
86 * Amplitude is distributed equally across the 3 oscillators
87 * so their sum doesn't exceed the specified level.
88 *
89 * @param amp Float amplitude (default 1.0)
90 */
91 void set_amplitude(float amp) {
92 amplitude_ = amp;
93 float per_osc = amp / 3.0f;
94 center_.set_amplitude(per_osc);
95 left_.set_amplitude(per_osc);
96 right_.set_amplitude(per_osc);
97 }
98
99 /**
100 * @brief Render into stereo float buffers (accumulating)
101 *
102 * ACCUMULATES into left[] and right[] — caller must zero the
103 * buffers before the first voice when mixing multiple SuperSaws.
104 *
105 * @param left Left output buffer (float, += semantics)
106 * @param right Right output buffer (float, += semantics)
107 * @param frames Number of samples
108 */
109 void process_stereo(float* left, float* right, uint16_t frames) {
110 uint16_t n = (frames > MAX_BLOCK) ? MAX_BLOCK : frames;
111
112 float fbuf[MAX_BLOCK];
113 float fleft[MAX_BLOCK] = {};
114 float fright[MAX_BLOCK] = {};
115
116 // Center → center pan
117 center_.process(fbuf, n);
118 dsp::stereo::pan_mix(fbuf, fleft, fright, panf_center_, n);
119
120 // Left-detuned → left pan
121 left_.process(fbuf, n);
122 dsp::stereo::pan_mix(fbuf, fleft, fright, panf_left_, n);
123
124 // Right-detuned → right pan
125 right_.process(fbuf, n);
126 dsp::stereo::pan_mix(fbuf, fleft, fright, panf_right_, n);
127
128 // Accumulate into output
129 for (uint16_t i = 0; i < n; ++i) {
130 left[i] += fleft[i];
131 right[i] += fright[i];
132 }
133 }
134
135 /**
136 * @brief Render mono (sum of all 3 saws)
137 *
138 * Does NOT accumulate — overwrites output.
139 *
140 * @param out Output buffer (float, overwritten)
141 * @param frames Number of samples
142 */
143 void process(float* out, uint16_t frames) {
144 uint16_t n = (frames > MAX_BLOCK) ? MAX_BLOCK : frames;
145
146 center_.process(out, n);
147
148 float buf[MAX_BLOCK];
149 left_.process(buf, n);
150 for (uint16_t i = 0; i < n; ++i) out[i] += buf[i];
151
152 right_.process(buf, n);
153 for (uint16_t i = 0; i < n; ++i) out[i] += buf[i];
154 }
155
156 /** @brief Hard sync — reset all oscillator phases */
157 void sync() {
158 center_.sync();
159 left_.sync();
160 right_.sync();
161 }
162
163 /** @brief Current center frequency (normalized, for diagnostics) */
164 float frequency() const { return center_.frequency(); }
165
166 /** @brief Current detune in cents */
167 float detune() const { return detune_cents_; }
168
169 /** @brief Current stereo spread */
170 float stereo_spread() const { return stereo_spread_; }
171
172private:
173 static constexpr uint16_t MAX_BLOCK = 48;
174
175 PolyBlepOsc center_;
176 PolyBlepOsc left_;
177 PolyBlepOsc right_;
178
179 float note_ = 60.0f;
180 float sample_rate_ = 48000.0f;
181 float detune_cents_ = 0.0f;
182 float stereo_spread_ = 0.0f;
183 float amplitude_ = 1.0f;
184
185 // Pan gains for stereo pipeline
189
190 void recompute_frequencies() {
191 center_.set_note(note_, sample_rate_);
192 recompute_detuned();
193 }
194
195 void recompute_detuned() {
196 float offset = detune_cents_ / 100.0f; // cents → semitones
197 left_.set_note(note_ - offset, sample_rate_);
198 right_.set_note(note_ + offset, sample_rate_);
199 }
200
201 void recompute_pan() {
202 float half = stereo_spread_ * 0.5f;
203 panf_center_ = dsp::stereo::constant_power(0.5f);
204 panf_left_ = dsp::stereo::constant_power(0.5f - half);
205 panf_right_ = dsp::stereo::constant_power(0.5f + half);
206 }
207};
208
209} // namespace sbl::widgets::source
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_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 process(float *out, uint16_t frames)
Render mono (sum of all 3 saws)
float stereo_spread() const
Current stereo spread.
void sync()
Hard sync — reset all oscillator phases.
void set_note(float midi_note, float sample_rate=48000.0f)
Set pitch from MIDI note number.
void set_amplitude(float amp)
Set output amplitude.
float detune() const
Current detune in cents.
void set_detune(float cents)
Set detune spread in cents.
void set_frequency(float freq_hz, float sr=48000.0f)
Set frequency in Hz.
void set_stereo_spread(float spread)
Set stereo spread.
void process_stereo(float *left, float *right, uint16_t frames)
Render into stereo float buffers (accumulating)
float frequency() const
Current center frequency (normalized, for diagnostics)
PanGain constant_power(float position)
Compute constant-power pan gains (-3 dB at center)
Definition stereo.hpp:53
void pan_mix(const float *mono, float *left, float *right, PanGain gain, uint16_t frames)
Apply pan gains to a mono buffer and accumulate into stereo buffers.
Definition stereo.hpp:78
Sound source widgets.
Definition noise.hpp:15
Stereo pan gains (float)
Definition stereo.hpp:33