Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
sample_sinc_kernel.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <array>
5#include <cmath>
6#include <cstddef>
7#include <cstdint>
8#include <limits>
9#include <span>
10#include <type_traits>
11#include <vector>
12
13namespace pulp::audio {
14
15inline constexpr std::size_t kMaximumSampleSincCutoffTables = 32;
16inline constexpr std::size_t kMaximumSampleSincOctaveTables = 8;
17inline constexpr double kMaximumDenseSampleSincConsumption = 128.0;
18inline constexpr std::uint32_t kDefaultSampleSincHalfWidth = 16;
19inline constexpr std::uint32_t kHighQualitySampleSincHalfWidth = 24;
20
23 std::uint32_t phases = 512;
24 double cutoff = 1.0;
25 double kaiser_beta = 9.0;
26};
27
29
31public:
33 // Borrowed immutable table view. The issuing kernel owner must remain
34 // built and must not be released or rebuilt until every prepared reader
35 // and block plan using this view is quiescent.
36 std::uint32_t tap_count() const noexcept { return half_width_ * 2; }
37 std::int32_t first_offset() const noexcept {
38 return 1 - static_cast<std::int32_t>(half_width_);
39 }
40 std::uint32_t half_width() const noexcept { return half_width_; }
41 std::uint32_t phases() const noexcept { return phases_; }
42 double cutoff() const noexcept { return cutoff_; }
43 const void* storage_identity() const noexcept { return table_; }
44 bool valid() const noexcept {
45 const auto count = static_cast<std::size_t>(tap_count());
46 const auto rows = static_cast<std::size_t>(phases_) + 1;
47 return table_ != nullptr && half_width_ >= 2 && half_width_ <= 64 &&
48 phases_ >= 2 && phases_ <= 4096 &&
49 cutoff_ > 0.0 && cutoff_ <= 1.0 && std::isfinite(cutoff_) &&
50 rows <= std::numeric_limits<std::size_t>::max() / count &&
51 table_sample_count_ >= rows * count;
52 }
53
54 float apply(std::span<const float> taps, double fraction) const noexcept {
55 if (!valid() || taps.size() != tap_count() ||
56 !std::isfinite(fraction)) {
57 return 0.0f;
58 }
59 const auto phase = std::clamp(fraction, 0.0, 1.0) *
60 static_cast<double>(phases_);
61 const auto first_phase = phase >= static_cast<double>(phases_)
62 ? phases_ - 1
63 : static_cast<std::uint32_t>(phase);
64 const auto blend = static_cast<float>(
65 std::clamp(phase - static_cast<double>(first_phase), 0.0, 1.0));
66 const auto count = static_cast<std::size_t>(tap_count());
67 const auto* first = table_ + static_cast<std::size_t>(first_phase) * count;
68 const auto* second = first + count;
69 float result = 0.0f;
70 for (std::size_t tap = 0; tap < count; ++tap) {
71 const auto coefficient =
72 first[tap] + blend * (second[tap] - first[tap]);
73 result += coefficient * taps[tap];
74 }
75 return result;
76 }
77
78private:
79 friend class SampleSincKernel;
80
81 SampleSincKernelView(const float* table,
82 std::size_t table_sample_count,
83 std::uint32_t half_width,
84 std::uint32_t phases,
85 double cutoff) noexcept
86 : table_(table),
87 table_sample_count_(table_sample_count),
88 half_width_(half_width),
89 phases_(phases),
90 cutoff_(cutoff) {}
91
92 const float* table_ = nullptr;
93 std::size_t table_sample_count_ = 0;
94 std::uint32_t half_width_ = 0;
95 std::uint32_t phases_ = 0;
96 double cutoff_ = 0.0;
97};
98
99static_assert(std::is_trivially_copyable_v<SampleSincKernelView>);
100
102public:
103 SampleSincKernel() = default;
108
109 bool build(const SampleSincKernelConfig& config) {
110 release();
111 if (!valid_config(config)) return false;
112 const auto taps = static_cast<std::size_t>(config.half_width) * 2;
113 const auto rows = static_cast<std::size_t>(config.phases) + 1;
114 if (rows > std::numeric_limits<std::size_t>::max() / taps)
115 return false;
116
117 try {
118 table_.assign(rows * taps, 0.0f);
119 } catch (...) {
120 return false;
121 }
122
123 const auto window_denominator = bessel_i0(config.kaiser_beta);
124 for (std::uint32_t phase = 0; phase <= config.phases; ++phase) {
125 const auto fraction = static_cast<double>(phase) /
126 static_cast<double>(config.phases);
127 double sum = 0.0;
128 auto* row = table_.data() + static_cast<std::size_t>(phase) * taps;
129 for (std::size_t tap = 0; tap < taps; ++tap) {
130 const auto offset = static_cast<double>(
131 static_cast<std::int64_t>(tap) + 1 -
132 static_cast<std::int64_t>(config.half_width)) - fraction;
133 const auto normalized = offset /
134 static_cast<double>(config.half_width);
135 const auto window = std::abs(normalized) < 1.0
136 ? bessel_i0(config.kaiser_beta *
137 std::sqrt(1.0 - normalized * normalized)) /
138 window_denominator
139 : 0.0;
140 const auto coefficient = config.cutoff *
141 sinc(config.cutoff * offset) * window;
142 row[tap] = static_cast<float>(coefficient);
143 sum += coefficient;
144 }
145 if (!std::isfinite(sum) || std::abs(sum) < 1.0e-12) {
146 release();
147 return false;
148 }
149 const auto inverse_sum = 1.0 / sum;
150 for (std::size_t tap = 0; tap < taps; ++tap)
151 row[tap] = static_cast<float>(
152 static_cast<double>(row[tap]) * inverse_sum);
153 }
154
155 view_ = SampleSincKernelView(table_.data(), table_.size(),
156 config.half_width, config.phases,
157 config.cutoff);
158 return true;
159 }
160
161 void release() noexcept {
162 view_ = {};
163 std::vector<float>().swap(table_);
164 }
165
166 SampleSincKernelView view() const noexcept { return view_; }
167
168private:
169 static bool valid_config(const SampleSincKernelConfig& config) noexcept {
170 return config.half_width >= 2 && config.half_width <= 64 &&
171 config.phases >= 2 && config.phases <= 4096 &&
172 config.cutoff > 0.0 && config.cutoff <= 1.0 &&
173 std::isfinite(config.cutoff) &&
174 config.kaiser_beta >= 0.0 &&
175 std::isfinite(config.kaiser_beta);
176 }
177
178 static double sinc(double value) noexcept {
179 if (std::abs(value) < 1.0e-12) return 1.0;
180 constexpr double pi = 3.1415926535897932384626433832795;
181 const auto argument = pi * value;
182 return std::sin(argument) / argument;
183 }
184
185 static double bessel_i0(double value) noexcept {
186 double sum = 1.0;
187 double term = 1.0;
188 const auto half = value * 0.5;
189 for (int order = 1; order < 64; ++order) {
190 const auto ratio = half / static_cast<double>(order);
191 term *= ratio * ratio;
192 sum += term;
193 if (term <= sum * 1.0e-15) break;
194 }
195 return sum;
196 }
197
198 std::vector<float> table_;
199 SampleSincKernelView view_{};
200};
201
205 float narrower_gain = 0.0f;
206
207 bool valid() const noexcept {
208 return wider.valid() && narrower.valid() &&
209 wider.tap_count() == narrower.tap_count() &&
210 narrower_gain >= 0.0f && narrower_gain <= 1.0f &&
211 std::isfinite(narrower_gain);
212 }
213
214 float apply(std::span<const float> taps, double fraction) const noexcept {
215 if (!valid()) return 0.0f;
216 if (narrower_gain <= 0.0f ||
217 wider.storage_identity() == narrower.storage_identity())
218 return wider.apply(taps, fraction);
219 if (narrower_gain >= 1.0f)
220 return narrower.apply(taps, fraction);
221 const auto wide = wider.apply(taps, fraction);
222 const auto narrow = narrower.apply(taps, fraction);
223 return wide + narrower_gain * (narrow - wide);
224 }
225};
226
227static_assert(std::is_trivially_copyable_v<SampleSincKernelSelection>);
228
230 std::array<SampleSincKernelView, kMaximumSampleSincCutoffTables> kernels{};
231 std::uint32_t kernel_count = 0;
232
233 bool valid() const noexcept {
234 if (kernel_count == 0 || kernel_count > kernels.size()) return false;
235 const auto taps = kernels[0].tap_count();
236 for (std::uint32_t index = 0; index < kernel_count; ++index) {
237 if (!kernels[index].valid() || kernels[index].tap_count() != taps)
238 return false;
239 if (index > 0 &&
240 kernels[index].cutoff() >= kernels[index - 1].cutoff())
241 return false;
242 }
243 return true;
244 }
245
247 double source_frames_per_output) const noexcept {
248 if (!valid() || !(source_frames_per_output > 0.0) ||
249 !std::isfinite(source_frames_per_output)) {
250 return {};
251 }
252 const auto target_cutoff = std::min(1.0, 1.0 / source_frames_per_output);
253 if (target_cutoff >= kernels[0].cutoff())
254 return {kernels[0], kernels[0], 0.0f};
255 for (std::uint32_t index = 1; index < kernel_count; ++index) {
256 if (target_cutoff == kernels[index].cutoff())
257 return {kernels[index], kernels[index], 0.0f};
258 if (target_cutoff < kernels[index].cutoff()) continue;
259 const auto wider = kernels[index - 1];
260 const auto narrower = kernels[index];
261 const auto gain = static_cast<float>(
262 (wider.cutoff() - target_cutoff) /
263 (wider.cutoff() - narrower.cutoff()));
264 return {wider, narrower, std::clamp(gain, 0.0f, 1.0f)};
265 }
266 return {};
267 }
268};
269
270static_assert(std::is_trivially_copyable_v<SampleSincKernelBankView>);
271
273public:
279
281 double maximum_source_frames_per_output,
282 std::uint32_t half_width = kDefaultSampleSincHalfWidth,
283 std::uint32_t phases = 512,
284 double kaiser_beta = 9.0) {
285 release();
286 if (!(maximum_source_frames_per_output > 0.0) ||
287 !std::isfinite(maximum_source_frames_per_output)) {
288 return false;
289 }
290 const auto octave = maximum_source_frames_per_output <= 1.0
291 ? 0.0
292 : std::ceil(std::log2(maximum_source_frames_per_output));
293 if (octave >= static_cast<double>(kMaximumSampleSincOctaveTables))
294 return false;
295 return build(static_cast<std::uint32_t>(octave) + 1,
296 half_width, phases, kaiser_beta);
297 }
298
299 // Production-quality bank: quarter-octave cutoff spacing bounds the
300 // above-target contribution while preserving continuous table blending.
301 // The 48-tap default supplies enough transition rejection for the first
302 // fractional-downsample interval, where octave-only 32-tap tables leak a
303 // near-Nyquist source tone into the output by only 7-11 dB.
305 double maximum_source_frames_per_output,
306 std::uint32_t tables_per_octave = 4,
307 std::uint32_t half_width = kHighQualitySampleSincHalfWidth,
308 std::uint32_t phases = 512,
309 double kaiser_beta = 9.0) {
310 release();
311 if (!(maximum_source_frames_per_output > 0.0) ||
312 maximum_source_frames_per_output >
314 !std::isfinite(maximum_source_frames_per_output) ||
315 tables_per_octave == 0 || tables_per_octave > 8) {
316 return false;
317 }
318 const auto intervals = maximum_source_frames_per_output <= 1.0
319 ? 0.0
320 : std::ceil(std::log2(maximum_source_frames_per_output) *
321 static_cast<double>(tables_per_octave));
322 if (intervals >= static_cast<double>(kMaximumSampleSincCutoffTables))
323 return false;
324 const auto table_count = static_cast<std::uint32_t>(intervals) + 1;
325 for (std::uint32_t index = 0; index < table_count; ++index) {
326 const auto cutoff = std::exp2(
327 -static_cast<double>(index) /
328 static_cast<double>(tables_per_octave));
329 if (!kernels_[index].build(
330 {half_width, phases, cutoff, kaiser_beta})) {
331 release();
332 return false;
333 }
334 view_.kernels[index] = kernels_[index].view();
335 }
336 view_.kernel_count = table_count;
337 return true;
338 }
339
340 bool build(std::uint32_t cutoff_table_count,
341 std::uint32_t half_width = kDefaultSampleSincHalfWidth,
342 std::uint32_t phases = 512,
343 double kaiser_beta = 9.0) {
344 release();
345 if (cutoff_table_count == 0 ||
346 cutoff_table_count > kMaximumSampleSincCutoffTables) {
347 return false;
348 }
349 for (std::uint32_t index = 0; index < cutoff_table_count; ++index) {
350 const auto cutoff = std::ldexp(1.0, -static_cast<int>(index));
351 if (!kernels_[index].build({half_width, phases, cutoff, kaiser_beta})) {
352 release();
353 return false;
354 }
355 view_.kernels[index] = kernels_[index].view();
356 }
357 view_.kernel_count = cutoff_table_count;
358 return true;
359 }
360
361 void release() noexcept {
362 view_ = {};
363 for (auto& kernel : kernels_) kernel.release();
364 }
365
366 SampleSincKernelBankView view() const noexcept { return view_; }
367
368private:
369 std::array<SampleSincKernel, kMaximumSampleSincCutoffTables> kernels_{};
370 SampleSincKernelBankView view_{};
371};
372
373} // namespace pulp::audio
SampleSincKernelBankView view() const noexcept
Definition sample_sinc_kernel.hpp:366
SampleSincKernelBank(const SampleSincKernelBank &)=delete
void release() noexcept
Definition sample_sinc_kernel.hpp:361
bool build_for_maximum_consumption(double maximum_source_frames_per_output, std::uint32_t half_width=kDefaultSampleSincHalfWidth, std::uint32_t phases=512, double kaiser_beta=9.0)
Definition sample_sinc_kernel.hpp:280
bool build_dense_for_maximum_consumption(double maximum_source_frames_per_output, std::uint32_t tables_per_octave=4, std::uint32_t half_width=kHighQualitySampleSincHalfWidth, std::uint32_t phases=512, double kaiser_beta=9.0)
Definition sample_sinc_kernel.hpp:304
SampleSincKernelBank(SampleSincKernelBank &&)=delete
bool build(std::uint32_t cutoff_table_count, std::uint32_t half_width=kDefaultSampleSincHalfWidth, std::uint32_t phases=512, double kaiser_beta=9.0)
Definition sample_sinc_kernel.hpp:340
SampleSincKernelBank & operator=(SampleSincKernelBank &&)=delete
SampleSincKernelBank & operator=(const SampleSincKernelBank &)=delete
Definition sample_sinc_kernel.hpp:30
const void * storage_identity() const noexcept
Definition sample_sinc_kernel.hpp:43
bool valid() const noexcept
Definition sample_sinc_kernel.hpp:44
double cutoff() const noexcept
Definition sample_sinc_kernel.hpp:42
float apply(std::span< const float > taps, double fraction) const noexcept
Definition sample_sinc_kernel.hpp:54
std::uint32_t phases() const noexcept
Definition sample_sinc_kernel.hpp:41
friend class SampleSincKernel
Definition sample_sinc_kernel.hpp:79
std::uint32_t tap_count() const noexcept
Definition sample_sinc_kernel.hpp:36
std::uint32_t half_width() const noexcept
Definition sample_sinc_kernel.hpp:40
std::int32_t first_offset() const noexcept
Definition sample_sinc_kernel.hpp:37
Definition sample_sinc_kernel.hpp:101
SampleSincKernel & operator=(const SampleSincKernel &)=delete
SampleSincKernel(SampleSincKernel &&)=delete
void release() noexcept
Definition sample_sinc_kernel.hpp:161
SampleSincKernel & operator=(SampleSincKernel &&)=delete
SampleSincKernelView view() const noexcept
Definition sample_sinc_kernel.hpp:166
SampleSincKernel(const SampleSincKernel &)=delete
bool build(const SampleSincKernelConfig &config)
Definition sample_sinc_kernel.hpp:109
Definition analyzer_provider.hpp:12
constexpr std::size_t kMaximumSampleSincCutoffTables
Definition sample_sinc_kernel.hpp:15
constexpr std::uint32_t kHighQualitySampleSincHalfWidth
Definition sample_sinc_kernel.hpp:19
constexpr double kMaximumDenseSampleSincConsumption
Definition sample_sinc_kernel.hpp:17
constexpr std::size_t kMaximumSampleSincOctaveTables
Definition sample_sinc_kernel.hpp:16
constexpr std::uint32_t kDefaultSampleSincHalfWidth
Definition sample_sinc_kernel.hpp:18
float sum(const float *data, std::size_t count) noexcept
Definition sample_sinc_kernel.hpp:229
SampleSincKernelSelection select(double source_frames_per_output) const noexcept
Definition sample_sinc_kernel.hpp:246
bool valid() const noexcept
Definition sample_sinc_kernel.hpp:233
std::array< SampleSincKernelView, kMaximumSampleSincCutoffTables > kernels
Definition sample_sinc_kernel.hpp:230
std::uint32_t kernel_count
Definition sample_sinc_kernel.hpp:231
Definition sample_sinc_kernel.hpp:21
std::uint32_t phases
Definition sample_sinc_kernel.hpp:23
double cutoff
Definition sample_sinc_kernel.hpp:24
std::uint32_t half_width
Definition sample_sinc_kernel.hpp:22
double kaiser_beta
Definition sample_sinc_kernel.hpp:25
Definition sample_sinc_kernel.hpp:202
SampleSincKernelView narrower
Definition sample_sinc_kernel.hpp:204
SampleSincKernelView wider
Definition sample_sinc_kernel.hpp:203
bool valid() const noexcept
Definition sample_sinc_kernel.hpp:207
float apply(std::span< const float > taps, double fraction) const noexcept
Definition sample_sinc_kernel.hpp:214
float narrower_gain
Definition sample_sinc_kernel.hpp:205