Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
explicit_q_resonator_bank.hpp
Go to the documentation of this file.
1#pragma once
2
11
14#include <pulp/signal/svf.hpp>
15
16#include <algorithm>
17#include <array>
18#include <cmath>
19#include <cstddef>
20#include <cstdint>
21#include <limits>
22#include <memory>
23#include <type_traits>
24
25namespace pulp::signal {
26
27template <typename SampleType = float> class ExplicitQResonatorBankT {
28 public:
29 static_assert(std::is_floating_point_v<SampleType>);
30 static constexpr int kMaximumBands = 64;
31
32 struct BandRecipe {
33 double frequency_hz = 1000.0;
34 double q = 4.0;
35 double gain_db = 0.0;
36 double attack_ms = 5.0;
37 double release_ms = 50.0;
38 };
39
40 [[nodiscard]] bool prepare(double sample_rate, int capacity) {
41 if (!std::isfinite(sample_rate) || sample_rate < 20.0 / 0.45 || capacity < 1 ||
43 return false;
44
45 auto candidate = std::make_unique<PreparedState>();
46 candidate->sample_rate = sample_rate;
47 candidate->capacity = capacity;
48 const double initial_frequency = std::min(1000.0, 0.45 * sample_rate);
49 for (int i = 0; i < capacity; ++i) {
50 auto& band = candidate->bands[static_cast<std::size_t>(i)];
51 band.current.frequency_hz = initial_frequency;
52 band.target.frequency_hz = initial_frequency;
53 band.configured_frequency_hz = initial_frequency;
54 band.filter.configure(Svf64::Mode::bandpass, sample_rate, initial_frequency, 4.0);
55 band.envelope.prepare(sample_rate);
56 band.envelope.set_attack_ms(5.0);
57 band.envelope.set_release_ms(50.0);
58 }
59 state_ = std::move(candidate);
60 staged_ = {};
61 next_generation_ = 1;
62 return true;
63 }
64
65 [[nodiscard]] bool stage_band(int index, const BandRecipe& recipe) noexcept {
66 if (!state_ || index < 0 || index >= state_->capacity)
67 return false;
68 staged_[static_cast<std::size_t>(index)] = recipe;
69 return true;
70 }
71
72 [[nodiscard]] bool set_coefficient_transition_ms(double transition_ms) noexcept {
73 if (!valid_transition(transition_ms))
74 return false;
75 transition_ms_ = transition_ms;
76 return true;
77 }
78
79 [[nodiscard]] bool publish(int active_count) noexcept {
80 return publish(active_count, transition_ms_);
81 }
82
83 [[nodiscard]] bool publish(int active_count, double transition_ms) noexcept {
84 if (!state_ || active_count < 0 || active_count > state_->capacity ||
85 !valid_transition(transition_ms))
86 return false;
87 for (int i = 0; i < active_count; ++i)
88 if (!valid_recipe(staged_[static_cast<std::size_t>(i)], state_->sample_rate))
89 return false;
90 const double transition_samples_exact = transition_ms * state_->sample_rate / 1000.0;
91 if (!std::isfinite(transition_samples_exact) ||
92 transition_samples_exact >=
93 static_cast<double>(std::numeric_limits<std::uint64_t>::max()))
94 return false;
95
96 PublishedRecipe payload;
97 payload.bands = staged_;
98 payload.active_count = active_count;
99 payload.transition_samples =
100 static_cast<std::uint64_t>(std::ceil(transition_samples_exact));
101 payload.generation = next_generation_++;
102 if (next_generation_ == 0)
103 ++next_generation_;
104 state_->publication.write(payload);
105 return true;
106 }
107
108 SampleType process(SampleType input) noexcept {
109 if (!state_)
110 return SampleType{};
111 adopt_publication();
112 if (!std::isfinite(static_cast<double>(input))) {
113 reset_active_audio_state();
114 return SampleType{};
115 }
116
117 double sum = 0.0;
118 for (int i = 0; i < state_->active_count; ++i) {
119 auto& band = state_->bands[static_cast<std::size_t>(i)];
120 advance(band);
121 if (band.configured_attack_ms != band.current.attack_ms) {
122 band.envelope.set_attack_ms(band.current.attack_ms);
123 band.configured_attack_ms = band.current.attack_ms;
124 }
125 if (band.configured_release_ms != band.current.release_ms) {
126 band.envelope.set_release_ms(band.current.release_ms);
127 band.configured_release_ms = band.current.release_ms;
128 }
129 if (band.configured_frequency_hz != band.current.frequency_hz ||
130 band.configured_q != band.current.q) {
131 band.filter.configure(Svf64::Mode::bandpass, state_->sample_rate,
132 band.current.frequency_hz, band.current.q);
133 band.configured_frequency_hz = band.current.frequency_hz;
134 band.configured_q = band.current.q;
135 }
136 const double filtered = band.filter.process(static_cast<double>(input));
137 const double level = band.envelope.process(std::abs(filtered));
138 const double output = filtered * db_to_gain(band.current.gain_db);
139 band.last_output = std::isfinite(filtered) ? filtered : 0.0;
140 band.last_envelope = std::isfinite(level) ? level : 0.0;
141 if (!std::isfinite(output) || !std::isfinite(level)) {
142 band.filter.reset();
143 band.envelope.reset();
144 }
145 sum += std::isfinite(output) ? output : 0.0;
146 }
147 if (!std::isfinite(sum))
148 return SampleType{};
149 return to_sample(sum);
150 }
151
152 void process(const SampleType* input, SampleType* output, int num_samples) noexcept {
153 if (!output || num_samples <= 0)
154 return;
155 for (int i = 0; i < num_samples; ++i)
156 output[i] = process(input ? input[i] : SampleType{});
157 }
158
159 void reset() noexcept {
160 if (!state_)
161 return;
162 adopt_publication();
163 reset_active_audio_state();
164 for (int i = 0; i < state_->active_count; ++i) {
165 auto& band = state_->bands[static_cast<std::size_t>(i)];
166 band.current = band.target;
167 band.remaining = 0;
168 }
169 }
170
171 [[nodiscard]] int capacity() const noexcept {
172 return state_ ? state_->capacity : 0;
173 }
174 [[nodiscard]] int active_band_count() const noexcept {
175 return state_ ? state_->active_count : 0;
176 }
177 [[nodiscard]] int latency_samples() const noexcept {
178 return 0;
179 }
180 [[nodiscard]] std::size_t retained_bytes() const noexcept {
181 return state_ ? sizeof(PreparedState) : 0;
182 }
183
184 [[nodiscard]] SampleType band_output(int index) const noexcept {
185 return valid_active_index(index)
186 ? to_sample(state_->bands[static_cast<std::size_t>(index)].last_output)
187 : SampleType{};
188 }
189 [[nodiscard]] SampleType envelope_at(int index) const noexcept {
190 return valid_active_index(index)
191 ? to_sample(state_->bands[static_cast<std::size_t>(index)].last_envelope)
192 : SampleType{};
193 }
194
195 [[nodiscard]] BandRecipe applied_band_recipe(int index) const noexcept {
196 return state_ && index >= 0 && index < state_->capacity
197 ? state_->bands[static_cast<std::size_t>(index)].current
198 : BandRecipe{};
199 }
200
201 [[nodiscard]] static double estimated_t60_seconds(const BandRecipe& recipe) noexcept {
202 return valid_tail_recipe(recipe)
203 ? 6.907755278982137 * recipe.q / (3.14159265358979323846 * recipe.frequency_hz)
204 : 0.0;
205 }
206
207 private:
208 struct PublishedRecipe {
209 std::array<BandRecipe, kMaximumBands> bands{};
210 int active_count = 0;
211 std::uint64_t transition_samples = 0;
212 std::uint64_t generation = 0;
213 };
214 struct BandState {
215 Svf64 filter;
216 BallisticsFilterT<double> envelope;
217 BandRecipe current{};
218 BandRecipe target{};
219 BandRecipe step{};
220 std::uint64_t remaining = 0;
221 double last_output = 0.0;
222 double last_envelope = 0.0;
223 double configured_attack_ms = 5.0;
224 double configured_release_ms = 50.0;
225 double configured_frequency_hz = 1000.0;
226 double configured_q = 4.0;
227 };
228 struct PreparedState {
229 double sample_rate = 0.0;
230 int capacity = 0;
231 int active_count = 0;
232 std::uint64_t consumed_generation = 0;
233 runtime::TripleBuffer<PublishedRecipe> publication;
234 std::array<BandState, kMaximumBands> bands{};
235 };
236
237 static bool valid_recipe(const BandRecipe& r, double sample_rate) noexcept {
238 return std::isfinite(r.frequency_hz) && r.frequency_hz >= 20.0 &&
239 r.frequency_hz <= 0.45 * sample_rate && std::isfinite(r.q) && r.q >= 0.5 &&
240 r.q <= 100.0 && std::isfinite(r.gain_db) && r.gain_db >= -60.0 &&
241 r.gain_db <= 24.0 && std::isfinite(r.attack_ms) && r.attack_ms >= 0.01 &&
242 r.attack_ms <= 60000.0 && std::isfinite(r.release_ms) && r.release_ms >= 0.01 &&
243 r.release_ms <= 60000.0;
244 }
245 static bool valid_transition(double ms) noexcept {
246 return std::isfinite(ms) && ms >= 0.0 && ms <= 500.0;
247 }
248 static bool valid_tail_recipe(const BandRecipe& recipe) noexcept {
249 return std::isfinite(recipe.frequency_hz) && recipe.frequency_hz > 0.0 &&
250 std::isfinite(recipe.q) && recipe.q > 0.0;
251 }
252 static double db_to_gain(double db) noexcept {
253 return std::pow(10.0, db / 20.0);
254 }
255 static SampleType to_sample(double value) noexcept {
256 if (!std::isfinite(value))
257 return SampleType{};
258 const double limit = static_cast<double>(std::numeric_limits<SampleType>::max());
259 return static_cast<SampleType>(std::clamp(value, -limit, limit));
260 }
261 static BandRecipe delta(const BandRecipe& a, const BandRecipe& b, double n) noexcept {
262 return {(b.frequency_hz - a.frequency_hz) / n, (b.q - a.q) / n, (b.gain_db - a.gain_db) / n,
263 (b.attack_ms - a.attack_ms) / n, (b.release_ms - a.release_ms) / n};
264 }
265 static void add(BandRecipe& a, const BandRecipe& d) noexcept {
266 a.frequency_hz += d.frequency_hz;
267 a.q += d.q;
268 a.gain_db += d.gain_db;
269 a.attack_ms += d.attack_ms;
270 a.release_ms += d.release_ms;
271 }
272 void adopt_publication() noexcept {
273 const auto& p = state_->publication.read();
274 if (p.generation == 0 || p.generation == state_->consumed_generation)
275 return;
276 const int touched_bands = std::max(state_->active_count, p.active_count);
277 for (int i = 0; i < touched_bands; ++i) {
278 auto& band = state_->bands[static_cast<std::size_t>(i)];
279 if (i >= p.active_count) {
280 band.filter.reset();
281 band.envelope.reset();
282 band.last_output = 0.0;
283 band.last_envelope = 0.0;
284 band.remaining = 0;
285 continue;
286 }
287 band.target = p.bands[static_cast<std::size_t>(i)];
288 if (p.transition_samples == 0) {
289 band.current = band.target;
290 band.remaining = 0;
291 } else {
292 band.step =
293 delta(band.current, band.target, static_cast<double>(p.transition_samples));
294 band.remaining = p.transition_samples;
295 }
296 }
297 state_->active_count = p.active_count;
298 state_->consumed_generation = p.generation;
299 }
300 static void advance(BandState& band) noexcept {
301 if (band.remaining == 0)
302 return;
303 add(band.current, band.step);
304 if (--band.remaining == 0)
305 band.current = band.target;
306 }
307 void reset_active_audio_state() noexcept {
308 for (int i = 0; i < state_->active_count; ++i) {
309 auto& band = state_->bands[static_cast<std::size_t>(i)];
310 band.filter.reset();
311 band.envelope.reset();
312 band.last_output = 0.0;
313 band.last_envelope = 0.0;
314 }
315 }
316 bool valid_active_index(int index) const noexcept {
317 return state_ && index >= 0 && index < state_->active_count;
318 }
319
320 std::unique_ptr<PreparedState> state_;
321 std::array<BandRecipe, kMaximumBands> staged_{};
322 double transition_ms_ = 20.0;
323 std::uint64_t next_generation_ = 1;
324};
325
328
329} // namespace pulp::signal
Peak/RMS envelope follower with independent attack and release curves.
Definition explicit_q_resonator_bank.hpp:27
static constexpr int kMaximumBands
Definition explicit_q_resonator_bank.hpp:30
std::size_t retained_bytes() const noexcept
Definition explicit_q_resonator_bank.hpp:180
BandRecipe applied_band_recipe(int index) const noexcept
Current RT-side recipe, including any in-flight transition.
Definition explicit_q_resonator_bank.hpp:195
void process(const SampleType *input, SampleType *output, int num_samples) noexcept
Definition explicit_q_resonator_bank.hpp:152
SampleType process(SampleType input) noexcept
Definition explicit_q_resonator_bank.hpp:108
bool stage_band(int index, const BandRecipe &recipe) noexcept
Definition explicit_q_resonator_bank.hpp:65
int active_band_count() const noexcept
Definition explicit_q_resonator_bank.hpp:174
int capacity() const noexcept
Definition explicit_q_resonator_bank.hpp:171
bool set_coefficient_transition_ms(double transition_ms) noexcept
Definition explicit_q_resonator_bank.hpp:72
bool prepare(double sample_rate, int capacity)
Definition explicit_q_resonator_bank.hpp:40
bool publish(int active_count, double transition_ms) noexcept
Definition explicit_q_resonator_bank.hpp:83
int latency_samples() const noexcept
Definition explicit_q_resonator_bank.hpp:177
SampleType envelope_at(int index) const noexcept
Definition explicit_q_resonator_bank.hpp:189
static double estimated_t60_seconds(const BandRecipe &recipe) noexcept
Ideal second-order -60 dB amplitude-tail estimate used for render sizing.
Definition explicit_q_resonator_bank.hpp:201
void reset() noexcept
Definition explicit_q_resonator_bank.hpp:159
bool publish(int active_count) noexcept
Definition explicit_q_resonator_bank.hpp:79
SampleType band_output(int index) const noexcept
Last filtered sample before gain_db is applied to the summed output.
Definition explicit_q_resonator_bank.hpp:184
@ d
Definition pitch.hpp:22
@ b
Definition pitch.hpp:31
@ index
Definition gpu_pass.hpp:140
Definition additive_bank.hpp:218
ExplicitQResonatorBankT< double > ExplicitQResonatorBank64
Definition explicit_q_resonator_bank.hpp:327
ExplicitQResonatorBankT< float > ExplicitQResonatorBank
Definition explicit_q_resonator_bank.hpp:326
@ a
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
SvfT< double > Svf64
Definition svf.hpp:149
@ r
Definition input_events.hpp:41
@ n
Definition input_events.hpp:40
@ p
Definition input_events.hpp:40
Definition explicit_q_resonator_bank.hpp:32
double q
Definition explicit_q_resonator_bank.hpp:34
double gain_db
Definition explicit_q_resonator_bank.hpp:35
double release_ms
Definition explicit_q_resonator_bank.hpp:37
double frequency_hz
Definition explicit_q_resonator_bank.hpp:33
double attack_ms
Definition explicit_q_resonator_bank.hpp:36
Lock-free latest-value publication between two threads.