Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
sample_heritage_bus_dsp.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <algorithm>
7#include <array>
8#include <cmath>
9#include <complex>
10#include <cstddef>
11#include <cstdint>
12#include <limits>
13#include <span>
14#include <type_traits>
15#include <variant>
16
17namespace pulp::audio {
18
25
31public:
33 const SampleHeritagePreparedProfile& profile,
34 double sample_rate,
35 std::size_t channel_count) noexcept {
36 release();
37 if (profile.schema_version != kSampleHeritageProfileSchemaVersion ||
38 profile.bus_count > kSampleHeritageMaximumBusBlocks ||
39 !(sample_rate >= 8000.0) || sample_rate > 384000.0 ||
40 !std::isfinite(sample_rate) || channel_count == 0 ||
41 channel_count > kSampleHeritageMaximumChannels)
43
44 profile_ = profile;
45 sample_rate_ = sample_rate;
46 channel_count_ = channel_count;
47 std::array<bool, kSampleHeritageMaximumBusBlocks> seen{};
48 std::size_t previous_type = 0;
49 bool have_previous_type = false;
50 for (std::size_t index = 0; index < profile_.bus_count; ++index) {
51 const auto& spec = profile_.bus[index];
52 if (spec.domain != SampleHeritageBlockDomain::Bus) {
53 release();
55 }
56 const auto type = spec.parameters.index();
57 if (type >= seen.size() || seen[type] ||
58 (have_previous_type && type < previous_type)) {
59 release();
61 }
62 seen[type] = true;
63 previous_type = type;
64 have_previous_type = true;
65 const auto valid = std::visit(
66 [&](const auto& block) noexcept {
67 using Block = std::decay_t<decltype(block)>;
68 if constexpr (std::is_same_v<
70 return prepare_noise(index, block);
71 else
72 return std::isfinite(block.drive) && block.drive >= 0.0f &&
73 block.drive <= 16.0f &&
74 std::isfinite(block.ceiling) &&
75 block.ceiling >= 0.001f && block.ceiling <= 4.0f;
76 },
77 spec.parameters);
78 if (!valid) {
79 release();
81 }
82 if (spec.bypass) noise_[index] = {};
83 }
84 prepared_ = true;
86 }
87
88 void release() noexcept {
89 profile_ = {};
90 noise_ = {};
91 sample_rate_ = 0.0;
92 channel_count_ = 0;
93 prepared_ = false;
94 }
95
96 void reset() noexcept {
97 if (!prepared_) return;
98 for (std::size_t index = 0; index < profile_.bus_count; ++index) {
99 auto& runtime = noise_[index];
100 runtime.random_state = runtime.seed;
101 runtime.filter_state = {};
102 }
103 }
104
106 bool any_voice_active) noexcept {
107 return process_impl(buffer, {}, any_voice_active);
108 }
109
111 BufferView<float> buffer,
112 std::span<const std::uint8_t> voice_activity) noexcept {
113 if (voice_activity.size() != buffer.num_samples())
115 return process_impl(buffer, voice_activity, false);
116 }
117
118private:
119 SampleHeritageBusDspStatus process_impl(
120 BufferView<float> buffer,
121 std::span<const std::uint8_t> voice_activity,
122 bool constant_voice_activity) noexcept {
123 if (!prepared_) return SampleHeritageBusDspStatus::NotPrepared;
124 if (buffer.num_channels() != channel_count_)
126
127 for (std::size_t index = 0; index < profile_.bus_count; ++index) {
128 const auto& spec = profile_.bus[index];
129 if (spec.bypass) continue;
130 std::visit(
131 [&](const auto& block) noexcept {
132 using Block = std::decay_t<decltype(block)>;
133 if constexpr (std::is_same_v<
135 process_noise(index, block, voice_activity,
136 constant_voice_activity, buffer);
137 else
138 process_output(block, buffer);
139 },
140 spec.parameters);
141 }
143 }
144
145public:
147 SampleHeritageRuntimeEngineState& state) const noexcept {
148 state = {};
150 for (std::size_t index = 0; index < profile_.bus_count; ++index) {
151 const auto& spec = profile_.bus[index];
152 if (spec.bypass) continue;
153 const auto* block = std::get_if<SampleHeritageBusNoiseIdleBlock>(
154 &spec.parameters);
155 if (block == nullptr ||
156 block->seed_policy !=
158 continue;
159 auto& saved = state.rng_states[state.rng_state_count++];
160 saved.stage_index = static_cast<std::uint8_t>(index);
162 saved.random_state = noise_[index].random_state;
163 }
165 }
166
168 const SampleHeritageRuntimeEngineState& state) noexcept {
170 if (state.rng_state_count > state.rng_states.size())
172 std::size_t expected = 0;
173 for (std::size_t index = 0; index < profile_.bus_count; ++index) {
174 const auto& spec = profile_.bus[index];
175 if (spec.bypass) continue;
176 const auto* block = std::get_if<SampleHeritageBusNoiseIdleBlock>(
177 &spec.parameters);
178 if (block == nullptr ||
179 block->seed_policy !=
181 continue;
182 if (expected >= state.rng_state_count)
184 const auto& saved = state.rng_states[expected++];
185 if (saved.stage_index != index ||
188 if (saved.random_state == 0)
190 }
191 if (expected != state.rng_state_count)
193 reset();
194 for (std::size_t index = 0; index < state.rng_state_count; ++index)
195 noise_[state.rng_states[index].stage_index].random_state =
196 state.rng_states[index].random_state;
198 }
199
200 double tilt_response_db(std::size_t bus_index,
201 double frequency) const noexcept {
202 if (!prepared_ || bus_index >= profile_.bus_count ||
203 !(frequency > 0.0) || frequency > sample_rate_ * 0.5 ||
204 !std::isfinite(frequency))
205 return std::numeric_limits<double>::quiet_NaN();
206 const auto& runtime = noise_[bus_index];
207 double magnitude = runtime.reference_normalization;
208 for (std::size_t shelf = 0; shelf < runtime.shelf_count; ++shelf)
209 magnitude *= response_magnitude(runtime.shelves[shelf], sample_rate_,
210 frequency);
211 return 20.0 * std::log10(magnitude);
212 }
213
214private:
215 static constexpr std::size_t kMaximumTiltShelves = 20;
216
217 struct Biquad {
218 double b0 = 1.0;
219 double b1 = 0.0;
220 double b2 = 0.0;
221 double a1 = 0.0;
222 double a2 = 0.0;
223 };
224
225 struct BiquadState {
226 double z1 = 0.0;
227 double z2 = 0.0;
228 };
229
230 struct NoiseRuntime {
231 std::uint64_t seed = 0;
232 std::uint64_t random_state = 0;
233 std::array<Biquad, kMaximumTiltShelves> shelves{};
234 std::array<std::array<BiquadState, kMaximumTiltShelves>,
235 kSampleHeritageMaximumChannels> filter_state{};
236 std::size_t shelf_count = 0;
237 double reference_normalization = 1.0;
238 };
239
240 static std::uint64_t next_random(std::uint64_t& state) noexcept {
241 state ^= state >> 12;
242 state ^= state << 25;
243 state ^= state >> 27;
244 return state * UINT64_C(2685821657736338717);
245 }
246
247 static float bipolar_random(std::uint64_t& state) noexcept {
248 const auto bits = static_cast<std::uint32_t>(next_random(state) >> 40);
249 return static_cast<float>(bits) * (2.0f / 16777215.0f) - 1.0f;
250 }
251
252 static Biquad high_shelf(double sample_rate,
253 double frequency,
254 double gain_db) noexcept {
255 constexpr double pi = 3.14159265358979323846;
256 const auto a = std::pow(10.0, gain_db / 40.0);
257 const auto omega = 2.0 * pi * frequency / sample_rate;
258 const auto cosine = std::cos(omega);
259 const auto sine = std::sin(omega);
260 const auto alpha = sine * std::sqrt(2.0) * 0.5;
261 const auto beta = 2.0 * std::sqrt(a) * alpha;
262 const auto a0 = (a + 1.0) - (a - 1.0) * cosine + beta;
263 return {
264 a * ((a + 1.0) + (a - 1.0) * cosine + beta) / a0,
265 -2.0 * a * ((a - 1.0) + (a + 1.0) * cosine) / a0,
266 a * ((a + 1.0) + (a - 1.0) * cosine - beta) / a0,
267 2.0 * ((a - 1.0) - (a + 1.0) * cosine) / a0,
268 ((a + 1.0) - (a - 1.0) * cosine - beta) / a0,
269 };
270 }
271
272 static double response_magnitude(const Biquad& filter,
273 double sample_rate,
274 double frequency) noexcept {
275 constexpr double pi = 3.14159265358979323846;
276 const auto z = std::polar(1.0, -2.0 * pi * frequency / sample_rate);
277 const auto numerator = filter.b0 + filter.b1 * z + filter.b2 * z * z;
278 const auto denominator = 1.0 + filter.a1 * z + filter.a2 * z * z;
279 return std::abs(numerator / denominator);
280 }
281
282 bool prepare_noise(std::size_t index,
283 const SampleHeritageBusNoiseIdleBlock& block) noexcept {
284 if (!std::isfinite(block.noise_amplitude) ||
285 !std::isfinite(block.idle_amplitude) ||
286 !std::isfinite(block.tilt_db_per_octave) ||
287 !std::isfinite(block.tilt_floor_hz) ||
288 !std::isfinite(block.tilt_reference_hz) ||
289 block.noise_amplitude < 0.0f || block.noise_amplitude > 1.0f ||
290 block.idle_amplitude < 0.0f || block.idle_amplitude > 1.0f ||
291 block.tilt_db_per_octave < -24.0f ||
292 block.tilt_db_per_octave > 24.0f || block.tilt_floor_hz < 1.0 ||
293 block.tilt_reference_hz < block.tilt_floor_hz ||
294 block.tilt_reference_hz >= sample_rate_ * 0.5 ||
297 (block.seed_policy !=
299 block.seed_policy !=
301 ((std::max(block.noise_amplitude, block.idle_amplitude) != 0.0f ||
302 block.seed_policy ==
304 block.seed == 0))
305 return false;
306
307 auto& runtime = noise_[index];
308 runtime.seed = block.seed;
309 runtime.random_state = block.seed;
310 if (block.tilt_db_per_octave == 0.0f) return true;
311
312 const auto upper = sample_rate_ * 0.5;
313 auto lower = block.tilt_floor_hz;
314 while (lower < upper && runtime.shelf_count < runtime.shelves.size()) {
315 const auto next = std::min(lower * 2.0, upper);
316 const auto octaves = std::log2(next / lower);
317 const auto center = std::sqrt(lower * next);
318 runtime.shelves[runtime.shelf_count++] = high_shelf(
319 sample_rate_, center, block.tilt_db_per_octave * octaves);
320 lower = next;
321 }
322 double reference_gain = 1.0;
323 for (std::size_t shelf = 0; shelf < runtime.shelf_count; ++shelf)
324 reference_gain *= response_magnitude(
325 runtime.shelves[shelf], sample_rate_, block.tilt_reference_hz);
326 runtime.reference_normalization = reference_gain > 0.0
327 ? 1.0 / reference_gain
328 : 1.0;
329 return true;
330 }
331
332 static float process_biquad(const Biquad& filter,
333 BiquadState& state,
334 float input) noexcept {
335 const auto output = filter.b0 * input + state.z1;
336 state.z1 = filter.b1 * input - filter.a1 * output + state.z2;
337 state.z2 = filter.b2 * input - filter.a2 * output;
338 return static_cast<float>(output);
339 }
340
341 void process_noise(std::size_t index,
342 const SampleHeritageBusNoiseIdleBlock& block,
343 std::span<const std::uint8_t> voice_activity,
344 bool constant_voice_activity,
345 BufferView<float> buffer) noexcept {
346 auto& runtime = noise_[index];
347 for (std::size_t frame = 0; frame < buffer.num_samples(); ++frame) {
348 const auto voice_active = voice_activity.empty()
349 ? constant_voice_activity
350 : voice_activity[frame] != 0;
351 const auto noise_open =
352 block.gate == SampleHeritageNoiseGate::AlwaysOn || voice_active;
353 for (std::size_t channel = 0; channel < channel_count_; ++channel) {
354 float generated = 0.0f;
355 if (block.idle_amplitude != 0.0f)
356 generated += bipolar_random(runtime.random_state) *
357 block.idle_amplitude;
358 if (noise_open && block.noise_amplitude != 0.0f)
359 generated += bipolar_random(runtime.random_state) *
360 block.noise_amplitude;
361 for (std::size_t shelf = 0; shelf < runtime.shelf_count; ++shelf)
362 generated = process_biquad(
363 runtime.shelves[shelf],
364 runtime.filter_state[channel][shelf], generated);
365 buffer.channel(channel)[frame] += static_cast<float>(
366 generated * runtime.reference_normalization);
367 }
368 }
369 }
370
371 static void process_output(const SampleHeritageBusOutputDriveBlock& block,
372 BufferView<float> buffer) noexcept {
373 for (std::size_t channel = 0; channel < buffer.num_channels(); ++channel)
374 for (auto& sample : buffer.channel(channel)) {
375 const auto driven = sample * block.drive;
376 sample = std::isinf(driven)
377 ? std::copysign(block.ceiling, driven)
378 : block.ceiling * driven /
379 (block.ceiling + std::abs(driven));
380 }
381 }
382
383 SampleHeritagePreparedProfile profile_{};
384 std::array<NoiseRuntime, kSampleHeritageMaximumBusBlocks> noise_{};
385 double sample_rate_ = 0.0;
386 std::size_t channel_count_ = 0;
387 bool prepared_ = false;
388};
389
390} // namespace pulp::audio
Non-owning view over multi-channel audio sample data.
Definition buffer.hpp:30
Stateful post-mix processor for typed heritage bus blocks.
Definition sample_heritage_bus_dsp.hpp:30
SampleHeritageBusDspStatus process(BufferView< float > buffer, std::span< const std::uint8_t > voice_activity) noexcept
Definition sample_heritage_bus_dsp.hpp:110
SampleHeritageRuntimeStateStatus capture_runtime_state(SampleHeritageRuntimeEngineState &state) const noexcept
Definition sample_heritage_bus_dsp.hpp:146
SampleHeritageBusDspStatus process(BufferView< float > buffer, bool any_voice_active) noexcept
Definition sample_heritage_bus_dsp.hpp:105
SampleHeritageBusDspStatus prepare(const SampleHeritagePreparedProfile &profile, double sample_rate, std::size_t channel_count) noexcept
Definition sample_heritage_bus_dsp.hpp:32
double tilt_response_db(std::size_t bus_index, double frequency) const noexcept
Definition sample_heritage_bus_dsp.hpp:200
void reset() noexcept
Definition sample_heritage_bus_dsp.hpp:96
void release() noexcept
Definition sample_heritage_bus_dsp.hpp:88
SampleHeritageRuntimeStateStatus restore_runtime_state(const SampleHeritageRuntimeEngineState &state) noexcept
Definition sample_heritage_bus_dsp.hpp:167
Definition analyzer_provider.hpp:12
constexpr std::size_t kSampleHeritageMaximumBusBlocks
Definition sample_heritage_schema.hpp:25
@ Noise
Definition sample_heritage_runtime_state.hpp:20
SampleHeritageRuntimeStateStatus
Definition sample_heritage_runtime_state.hpp:95
@ NotPrepared
Definition sample_heritage_runtime_state.hpp:97
@ InvalidRandomState
Definition sample_heritage_runtime_state.hpp:101
@ Ok
Definition sample_heritage_runtime_state.hpp:96
@ InvalidStageLayout
Definition sample_heritage_runtime_state.hpp:100
SampleHeritageBusDspStatus
Definition sample_heritage_bus_dsp.hpp:19
@ InvalidDimensions
Definition sample_heritage_bus_dsp.hpp:23
@ NotPrepared
Definition sample_heritage_bus_dsp.hpp:21
@ Ok
Definition sample_heritage_bus_dsp.hpp:20
@ InvalidProfile
Definition sample_heritage_bus_dsp.hpp:22
@ Ok
Definition loop_types.hpp:34
@ RestartFromProfileSeed
Definition sample_heritage_schema.hpp:31
@ ContinueSerializedState
Definition sample_heritage_schema.hpp:32
constexpr std::uint32_t kSampleHeritageProfileSchemaVersion
Definition sample_heritage_schema.hpp:18
@ Bus
Definition sample_heritage_schema.hpp:35
@ AlwaysOn
Definition sample_heritage_schema.hpp:48
@ VoiceActive
Definition sample_heritage_schema.hpp:48
constexpr std::size_t kSampleHeritageMaximumChannels
Definition sample_heritage_schema.hpp:27
@ center
Definition canvas.hpp:221
@ sine
Definition test_signal.hpp:14
@ a
Definition pitch.hpp:29
@ alpha
Straight alpha: src * a + dst * (1 - a).
Definition gpu_pass.hpp:268
@ index
Definition gpu_pass.hpp:140
Definition generated_event_source.hpp:33
constexpr double pi
Definition fir_design.hpp:164
@ buffer
A borrowed static buffer with a traversable playhead.
Definition granular.hpp:110
void abs(const float *a, float *dst, std::size_t count) noexcept
SequencerStateChannel is the audio-safe transport for a plugin's structured NON-parameter state: the ...
Definition audio_device_manager.hpp:48
@ block
Filled glyph cell; the covered glyph is drawn in the bg color.
Definition caret.hpp:66
@ z
Definition input_events.hpp:42
Definition sample_heritage_schema.hpp:133
Definition sample_heritage_schema.hpp:296
std::size_t bus_count
Definition sample_heritage_schema.hpp:304
std::array< SampleHeritageBusBlockSpec, kSampleHeritageMaximumBusBlocks > bus
Definition sample_heritage_schema.hpp:303
Definition sample_heritage_runtime_state.hpp:54