Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
wt_lofi.hpp
Go to the documentation of this file.
1#pragma once
2
77
79
80#include <algorithm>
81#include <cmath>
82#include <cstddef>
83#include <span>
84#include <utility>
85#include <vector>
86
87namespace pulp::signal::osc {
88
91public:
95 static constexpr int kDefaultBitDepth = 8;
98 static constexpr int kFullPrecision = 0;
99
103 static constexpr int kDefaultOversampleFactor = 16;
105 static constexpr double kReconstructionStopbandDb = 96.0;
106
117 bool set_tables(std::vector<std::vector<double>> tables,
119 std::size_t length = tables.empty() ? 0 : tables.front().size();
120 for (const auto& t : tables) {
121 if (t.size() != length) return false; // Mismatched: reject atomically.
122 }
123
124 bit_depth_ = bit_depth;
125 tables_.clear();
126 table_length_ = length;
127 if (table_length_ > 0) {
128 tables_.reserve(tables.size());
129 for (auto& src : tables) {
130 std::vector<double> table(table_length_, 0.0);
131 for (std::size_t i = 0; i < table_length_; ++i)
132 table[i] = quantize(src[i], bit_depth_);
133 tables_.push_back(std::move(table));
134 }
135 }
136 current_table_ = std::min<int>(current_table_,
137 tables_.empty() ? 0
138 : static_cast<int>(tables_.size()) - 1);
139 return true;
140 }
141
143 void prepare(double sample_rate) {
144 if (sample_rate > 0.0) sample_rate_ = sample_rate;
145 }
146
151 void set_scan(double pos) {
152 if (tables_.size() < 2) { current_table_ = 0; return; }
153 const double clamped = std::clamp(pos, 0.0, 1.0);
154 const double scaled = clamped * static_cast<double>(tables_.size() - 1);
155 current_table_ = static_cast<int>(std::lround(scaled));
156 current_table_ = std::clamp(current_table_, 0,
157 static_cast<int>(tables_.size()) - 1);
158 }
159
164 void set_reconstruction(bool enabled) {
165 reconstruction_ = enabled;
166 rebuild_filter();
167 }
168
172 void set_oversample_factor(int factor) {
173 oversample_ = factor > 1 ? factor : 1;
174 rebuild_filter();
175 }
176
179 void reset() {
180 table_phase_ = 0.0;
181 std::fill(ring_.begin(), ring_.end(), 0.0);
182 ring_pos_ = 0;
183 }
184
190 double next(double increment) {
191 if (tables_.empty() || table_length_ == 0) return 0.0;
192 const double step = increment * static_cast<double>(table_length_);
193
194 if (!reconstruction_active()) {
195 const double s = sample_zoh();
196 advance_phase(step);
197 return s;
198 }
199
200 // Oversample the staircase, push each fine sample through the
201 // reconstruction FIR, and keep one decimated sample. Only the retained
202 // output's convolution is computed (filter-then-decimate), so the cost is
203 // one dot product per host sample regardless of the oversample factor.
204 const double sub_step = step / static_cast<double>(oversample_);
205 double out = 0.0;
206 for (int m = 0; m < oversample_; ++m) {
207 push_ring(sample_zoh());
208 advance_phase(sub_step);
209 if (m == oversample_ - 1) out = convolve_ring();
210 }
211 return out;
212 }
213
214 std::size_t table_length() const { return table_length_; }
215 std::size_t table_count() const { return tables_.size(); }
216 int current_table() const { return current_table_; }
217 int bit_depth() const { return bit_depth_; }
223 std::span<const double> stored_table(std::size_t i) const {
224 return i < tables_.size() ? std::span<const double>(tables_[i])
225 : std::span<const double>();
226 }
227
231 double sample_rate() const { return sample_rate_; }
232 bool reconstruction_enabled() const { return reconstruction_; }
233 int oversample_factor() const { return oversample_; }
234
235private:
240 static double quantize(double x, int bits) {
241 if (bits <= 0 || bits >= 53) return x;
242 const double levels = std::ldexp(1.0, bits - 1) - 1.0;
243 return std::round(x * levels) / levels;
244 }
245
246 bool reconstruction_active() const {
247 return reconstruction_ && oversample_ > 1 && !coeffs_.empty();
248 }
249
253 double sample_zoh() const {
254 const auto& table = tables_[static_cast<std::size_t>(current_table_)];
255 std::size_t idx = static_cast<std::size_t>(table_phase_);
256 if (idx >= table_length_) idx = table_length_ - 1; // guard fp edge only.
257 return table[idx];
258 }
259
260 void advance_phase(double step) {
261 table_phase_ += step;
262 const double L = static_cast<double>(table_length_);
263 if (table_phase_ >= L || table_phase_ < 0.0)
264 table_phase_ -= L * std::floor(table_phase_ / L);
265 if (table_phase_ >= L) table_phase_ = 0.0; // fp snap at the boundary.
266 }
267
268 void push_ring(double x) {
269 ring_[ring_pos_] = x;
270 ring_pos_ = ring_pos_ + 1 == ring_.size() ? 0 : ring_pos_ + 1;
271 }
272
273 double convolve_ring() const {
274 const std::size_t n = coeffs_.size();
275 double acc = 0.0;
276 std::size_t idx = ring_pos_ == 0 ? n - 1 : ring_pos_ - 1; // newest sample.
277 for (std::size_t j = 0; j < n; ++j) {
278 acc += coeffs_[j] * ring_[idx];
279 idx = idx == 0 ? n - 1 : idx - 1;
280 }
281 return acc;
282 }
283
293 void rebuild_filter() {
294 coeffs_.clear();
295 ring_.clear();
296 ring_pos_ = 0;
297 if (!reconstruction_ || oversample_ <= 1) return;
298
299 const double host_nyquist_norm = 0.5 / static_cast<double>(oversample_);
300 const double cutoff = host_nyquist_norm;
301 const double transition = 0.20 * host_nyquist_norm;
303 const std::size_t taps =
305 coeffs_ = design_windowed_sinc(taps, cutoff, beta);
306 ring_.assign(coeffs_.size(), 0.0);
307 }
308
309 std::vector<std::vector<double>> tables_;
310 std::size_t table_length_ = 0;
311 int bit_depth_ = kDefaultBitDepth;
312 int current_table_ = 0;
313
314 double sample_rate_ = 48000.0;
315 double table_phase_ = 0.0;
316
317 bool reconstruction_ = true;
318 int oversample_ = kDefaultOversampleFactor;
319 std::vector<double> coeffs_;
320 std::vector<double> ring_;
321 std::size_t ring_pos_ = 0;
322};
323
324} // namespace pulp::signal::osc
Dedicated variable-clock ZOH wavetable oscillator (the lo-fi tier).
Definition wt_lofi.hpp:90
void set_oversample_factor(int factor)
Set the reconstruction oversample factor (>= 1; 1 disables the stage even when reconstruction is enab...
Definition wt_lofi.hpp:172
static constexpr int kDefaultOversampleFactor
Default oversample factor for the reconstruction stage.
Definition wt_lofi.hpp:103
static constexpr double kReconstructionStopbandDb
Reconstruction-filter stopband depth (dB) handed to the Kaiser design.
Definition wt_lofi.hpp:105
int current_table() const
Definition wt_lofi.hpp:216
void set_reconstruction(bool enabled)
Enable (default) or disable the oversample โ†’ reconstruction โ†’ decimate stage.
Definition wt_lofi.hpp:164
double sample_rate() const
The configured sample rate.
Definition wt_lofi.hpp:231
double next(double increment)
Generate one sample and advance by increment cycles (frequency รท sample rate).
Definition wt_lofi.hpp:190
void reset()
Reset the playback phase to the start of the cycle and clear the reconstruction filter state.
Definition wt_lofi.hpp:179
void prepare(double sample_rate)
Set the sample rate. Safe to call again on a rate change.
Definition wt_lofi.hpp:143
int bit_depth() const
Definition wt_lofi.hpp:217
std::span< const double > stored_table(std::size_t i) const
Read-only view of a stored table โ€” the actual (post-quantization) sample data the reader plays.
Definition wt_lofi.hpp:223
int oversample_factor() const
Definition wt_lofi.hpp:233
std::size_t table_count() const
Definition wt_lofi.hpp:215
bool reconstruction_enabled() const
Definition wt_lofi.hpp:232
static constexpr int kDefaultBitDepth
Default stored-table resolution.
Definition wt_lofi.hpp:95
std::size_t table_length() const
Definition wt_lofi.hpp:214
void set_scan(double pos)
Select the table nearest scan position pos (0 = first, 1 = last), clamped.
Definition wt_lofi.hpp:151
bool set_tables(std::vector< std::vector< double > > tables, int bit_depth=kDefaultBitDepth)
Install the raw single-cycle table set (off the audio thread), returning whether the set was accepted...
Definition wt_lofi.hpp:117
static constexpr int kFullPrecision
Sentinel bit depth: store the table at full double precision (no quantization).
Definition wt_lofi.hpp:98
Definition blep.hpp:72
@ levels
Definition multi_channel_meter.hpp:48
@ i
Definition formant_filter_bank.hpp:20
std::size_t kaiser_length_for_transition(double stopband_db, double normalized_width)
Definition windowed_sinc_design.hpp:36
std::vector< double > design_windowed_sinc(std::size_t taps, double cutoff, double beta)
Definition windowed_sinc_design.hpp:58
double kaiser_beta_for_stopband(double stopband_db)
Definition windowed_sinc_design.hpp:27
@ j
Definition input_events.hpp:40
@ n
Definition input_events.hpp:40
@ x
Definition input_events.hpp:41
Shared Kaiser-windowed-sinc FIR design helpers.