Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
convolver_non_uniform.hpp
Go to the documentation of this file.
1#pragma once
2
40
43
44#include <algorithm>
45#include <array>
46#include <cassert>
47#include <complex>
48#include <cstddef>
49#include <cstdint>
50#include <string_view>
51#include <type_traits>
52#include <vector>
53
54#if defined(__APPLE__)
55#include <TargetConditionals.h>
56#include <sys/sysctl.h>
57#endif
58
59// The tail-multiplier table was measured on Apple Silicon Macs with the
60// Accelerate backend in single precision; it applies only there.
61#if defined(PULP_SIMD_DEFAULT_BACKEND_ACCELERATE) && defined(__aarch64__) && \
62 defined(TARGET_OS_OSX) && TARGET_OS_OSX
63#define PULP_NON_UNIFORM_MEASURED_TABLE 1
64#else
65#define PULP_NON_UNIFORM_MEASURED_TABLE 0
66#endif
67
68namespace pulp::signal {
69
79template <typename SampleType = float>
81public:
83
89 static constexpr std::size_t kDefaultTailMultiplier = 4;
90
115 static constexpr std::size_t choose_tail_multiplier(std::size_t ir_length,
116 std::size_t block_size) noexcept {
117#if PULP_NON_UNIFORM_MEASURED_TABLE
118 if constexpr (!std::is_same_v<SampleType, float>) {
119 (void)ir_length;
120 (void)block_size;
122 } else {
123 // Columns: IRs nearest 4096, 16384, 48000 and 96000 taps.
124 constexpr std::array<std::uint64_t, 4> kMeasuredIr{4096, 16384, 48000, 96000};
125 std::size_t column = 0;
126 const auto n = static_cast<std::uint64_t>(ir_length);
127 while (column + 1 < kMeasuredIr.size() &&
128 n * n > kMeasuredIr[column] * kMeasuredIr[column + 1])
129 ++column;
131 case 16:
132 return std::array<std::size_t, 4>{16, 32, 64, 64}[column];
133 case 32:
134 return std::array<std::size_t, 4>{8, 16, 32, 64}[column];
135 case 64:
136 return std::array<std::size_t, 4>{8, 16, 16, 32}[column];
137 case 128:
138 return std::array<std::size_t, 4>{8, 8, 16, 32}[column];
139 case 256:
140 return std::array<std::size_t, 4>{4, 8, 16, 16}[column];
141 case 512:
142 return std::array<std::size_t, 4>{8, 8, 8, 16}[column];
143 default:
145 }
146 }
147#else
148 (void)ir_length;
149 (void)block_size;
151#endif
152 }
153
159 static constexpr bool table_measured_on(std::string_view cpu_brand) noexcept {
160 for (const std::string_view family :
161 {std::string_view{"Apple M1"}, std::string_view{"Apple M5"}}) {
162 if (cpu_brand.substr(0, family.size()) != family)
163 continue;
164 if (cpu_brand.size() == family.size() || cpu_brand[family.size()] == ' ')
165 return true;
166 }
167 return false;
168 }
169
174 static constexpr std::size_t default_tail_multiplier_for(std::string_view cpu_brand,
175 std::size_t ir_length,
176 std::size_t block_size) noexcept {
177 return table_measured_on(cpu_brand) ? choose_tail_multiplier(ir_length, block_size)
179 }
180
186 static std::size_t default_tail_multiplier(std::size_t ir_length, std::size_t block_size) {
187#if PULP_NON_UNIFORM_MEASURED_TABLE
188 static const bool measured = [] {
189 char brand[128] = {};
190 std::size_t size = sizeof(brand) - 1;
191 if (sysctlbyname("machdep.cpu.brand_string", brand, &size, nullptr, 0) != 0)
192 return false;
193 return table_measured_on(std::string_view{brand});
194 }();
195 if (measured)
196 return choose_tail_multiplier(ir_length, block_size);
197#endif
198 (void)ir_length;
199 (void)block_size;
201 }
202
206 void load_ir(const SampleType* ir, std::size_t ir_length, std::size_t block_size) {
207 load_ir(ir, ir_length, block_size, default_tail_multiplier(ir_length, block_size));
208 }
209
230 void load_ir(const SampleType* ir, std::size_t ir_length, std::size_t block_size,
231 std::size_t tail_multiplier) {
232 reset_state();
233 if (ir == nullptr || ir_length == 0 || block_size == 0) return;
234
235 // Pow-of-2 block size.
236 block_size_ = round_up_pow2(block_size);
237 if (tail_multiplier == 0) tail_multiplier = 1;
238 // Tail multiplier must be a power of 2 so the larger FFT also
239 // works on a pow-of-2 block.
240 tail_multiplier_ = round_up_pow2(tail_multiplier);
241
242 std::size_t head_samples = tail_multiplier_ * block_size_;
243 head_samples = std::min(head_samples, ir_length);
244
245 // ── head stage ──
246 head_.load_ir(ir, head_samples, block_size_);
247 head_samples_ = head_samples;
248
249 // ── tail stage (optional) ──
250 if (head_samples < ir_length) {
251 std::size_t tail_block = block_size_ * tail_multiplier_;
252 std::size_t tail_len = ir_length - head_samples;
253 // The tail IR is the slice ir[head_samples .. ir_length)
254 // with NO leading zero-padding. The K-small-block input
255 // buffering delay equals the K·B = head_samples IR-tap
256 // offset, so the natural sum lines up.
257 tail_ir_slice_.assign(ir + head_samples, ir + ir_length);
258 tail_.load_ir(tail_ir_slice_.data(), tail_len, tail_block);
259 tail_block_ = tail_block;
260 // Input/output ring buffers for the tail stage.
261 tail_input_buf_.assign(tail_block_, SampleType{0.0f});
262 tail_output_buf_.assign(tail_block_, SampleType{0.0f});
263 tail_fill_ = 0;
264 tail_stream_pos_ = tail_block_; // nothing to stream yet
265 }
266 loaded_ = true;
267 }
268
296 void process(const SampleType* input,
297 SampleType* output,
298 std::size_t num_samples) {
299 // No IR to convolve with: pass through.
300 if (!loaded_) {
301 if (input != output)
302 std::copy_n(input, num_samples, output);
303 return;
304 }
305 // Loaded, but the caller's block size does not match the partitioning.
306 if (num_samples != block_size_) {
307 std::fill_n(output, num_samples, SampleType{0.0f});
308 ++block_size_violations_;
309 history_torn_ = true;
310 return;
311 }
312 // Recover from a previous violation before folding a valid block in.
313 if (history_torn_) reset();
314
315 // ── feed this block into the tail input buffer ──
316 // Captured before the head writes `output`, so `input` may be the
317 // same buffer as `output`.
318 if (tail_block_ > 0) {
319 std::copy_n(input, block_size_, tail_input_buf_.data() + tail_fill_);
320 tail_fill_ += block_size_;
321 }
322
323 // ── head stage: live ──
324 head_.process(input, output, num_samples);
325
326 if (tail_block_ == 0) return;
327
328 // ── stream out previously-computed tail samples ──
329 if (tail_stream_pos_ < tail_block_) {
330 for (std::size_t i = 0; i < block_size_; ++i)
331 output[i] += tail_output_buf_[tail_stream_pos_ + i];
332 tail_stream_pos_ += block_size_;
333 }
334
335 if (tail_fill_ >= tail_block_) {
336 // Process one tail block worth of input. Result will be
337 // streamed out over the NEXT K small blocks (starting on
338 // the next process() call).
339 tail_.process(tail_input_buf_.data(),
340 tail_output_buf_.data(), tail_block_);
341 tail_fill_ = 0;
342 tail_stream_pos_ = 0;
343 }
344 }
345
346 void reset() {
347 head_.reset();
348 if (tail_block_ > 0) tail_.reset();
349 std::fill(tail_input_buf_.begin(), tail_input_buf_.end(), SampleType{0.0f});
350 std::fill(tail_output_buf_.begin(), tail_output_buf_.end(), SampleType{0.0f});
351 tail_fill_ = 0;
352 tail_stream_pos_ = tail_block_;
353 history_torn_ = false;
354 block_size_violations_ = 0;
355 }
356
359 std::size_t latency() const { return 0; }
360
361 std::size_t block_size() const { return block_size_; }
362
368 std::uint64_t block_size_violations() const { return block_size_violations_; }
369
370 std::size_t head_samples() const { return head_samples_; }
371 std::size_t tail_block() const { return tail_block_; }
372 std::size_t tail_multiplier() const { return tail_multiplier_; }
373
374 bool is_loaded() const { return loaded_; }
375
376private:
377 static std::size_t round_up_pow2(std::size_t v) {
378 if (v <= 1) return 1;
379 if ((v & (v - 1)) == 0) return v;
380 std::size_t p = 1;
381 while (p < v) p <<= 1;
382 return p;
383 }
384
385 void reset_state() {
386 head_ = PartitionedConvolverT<SampleType>{};
387 tail_ = PartitionedConvolverT<SampleType>{};
388 tail_ir_slice_.clear();
389 tail_input_buf_.clear();
390 tail_output_buf_.clear();
391 block_size_ = 0;
392 tail_block_ = 0;
393 tail_multiplier_ = 0;
394 head_samples_ = 0;
395 tail_fill_ = 0;
396 tail_stream_pos_ = 0;
397 loaded_ = false;
398 }
399
400 PartitionedConvolverT<SampleType> head_;
401 PartitionedConvolverT<SampleType> tail_;
402 std::vector<SampleType> tail_ir_slice_;
403 std::vector<SampleType> tail_input_buf_;
404 std::vector<SampleType> tail_output_buf_;
405 std::size_t block_size_ = 0;
406 std::size_t tail_block_ = 0;
407 std::size_t tail_multiplier_ = 0;
408 std::size_t head_samples_ = 0;
409 std::size_t tail_fill_ = 0;
410 std::size_t tail_stream_pos_ = 0; // tail_block_ ⇒ nothing to stream
411 bool loaded_ = false;
412 // Set when process() is handed a block whose size does not match the loaded
413 // IR partitioning. The next valid block resets both stages before folding
414 // itself in.
415 bool history_torn_ = false;
416 std::uint64_t block_size_violations_ = 0;
417};
418
421
422} // namespace pulp::signal
Two-stage non-uniform partitioned convolver.
Definition convolver_non_uniform.hpp:80
static constexpr std::size_t choose_tail_multiplier(std::size_t ir_length, std::size_t block_size) noexcept
The tail multiplier load_ir() uses when none is given: the head length (K · B samples) that minimises...
Definition convolver_non_uniform.hpp:115
bool is_loaded() const
Definition convolver_non_uniform.hpp:374
void process(const SampleType *input, SampleType *output, std::size_t num_samples)
Process one audio block.
Definition convolver_non_uniform.hpp:296
static std::size_t default_tail_multiplier(std::size_t ir_length, std::size_t block_size)
The tail multiplier the three-argument load_ir() uses: default_tail_multiplier_for() with this host's...
Definition convolver_non_uniform.hpp:186
void reset()
Definition convolver_non_uniform.hpp:346
std::size_t tail_block() const
Definition convolver_non_uniform.hpp:371
std::uint64_t block_size_violations() const
How many times process() was called on a loaded convolver with num_samples != block_size().
Definition convolver_non_uniform.hpp:368
static constexpr std::size_t default_tail_multiplier_for(std::string_view cpu_brand, std::size_t ir_length, std::size_t block_size) noexcept
The tail multiplier for a host whose CPU brand string is cpu_brand: choose_tail_multiplier() on a mea...
Definition convolver_non_uniform.hpp:174
void load_ir(const SampleType *ir, std::size_t ir_length, std::size_t block_size)
Load an impulse response with the tail multiplier default_tail_multiplier() picks for its length and ...
Definition convolver_non_uniform.hpp:206
std::size_t tail_multiplier() const
Definition convolver_non_uniform.hpp:372
static constexpr bool table_measured_on(std::string_view cpu_brand) noexcept
Whether the table was measured on the chip family a CPU brand string (machdep.cpu....
Definition convolver_non_uniform.hpp:159
std::size_t latency() const
Algorithmic latency in samples.
Definition convolver_non_uniform.hpp:359
void load_ir(const SampleType *ir, std::size_t ir_length, std::size_t block_size, std::size_t tail_multiplier)
Load an impulse response into both stages.
Definition convolver_non_uniform.hpp:230
std::size_t block_size() const
Definition convolver_non_uniform.hpp:361
static constexpr std::size_t kDefaultTailMultiplier
Definition convolver_non_uniform.hpp:89
std::size_t head_samples() const
Definition convolver_non_uniform.hpp:370
constexpr std::size_t convolver_block_size(std::size_t block_size) noexcept
The block size a convolver prepared for block_size accepts: the request rounded up to the next power ...
Definition convolver_messages.hpp:188
Definition additive_bank.hpp:218
NonUniformPartitionedConvolverT< float > NonUniformPartitionedConvolver
Definition convolver_non_uniform.hpp:419
@ i
Definition formant_filter_bank.hpp:20
NonUniformPartitionedConvolverT< double > NonUniformPartitionedConvolver64
Definition convolver_non_uniform.hpp:420