Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
sample_rate_conversion.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <algorithm>
7#include <array>
8#include <cmath>
9#include <cstdint>
10#include <limits>
11#include <memory>
12#include <new>
13#include <optional>
14#include <span>
15#include <vector>
16
17namespace pulp::audio {
18
20
21// Converter byte budgets include conservative allocator bookkeeping for every
22// retained allocation plus the shared-ownership control block. Payloads use
23// actual container capacity once construction completes.
24inline constexpr std::uint64_t kConverterAllocationOverhead = 4u * sizeof(void*);
25inline constexpr std::uint64_t kConverterSharedOwnershipBytes = 8u * sizeof(void*);
26
32 public:
33 // Keep at least four sinc zero crossings on each side of the read point.
34 // Narrower fixed kernels cannot provide a meaningful decimation stopband.
35 static constexpr double kMinimumSupportedCutoff = 0.125;
36
37 explicit PreparedSampleRateConversion(double cutoff) {
38 if (!std::isfinite(cutoff))
39 cutoff = 1.0;
40 if (cutoff >= kMinimumSupportedCutoff)
41 kernel_.build(32, 512, 10.0, cutoff);
42 }
43
44 bool ready() const noexcept {
45 return kernel_.ready();
46 }
47
48 static std::uint64_t estimated_prepared_bytes() noexcept;
49
50 std::uint64_t prepared_bytes() const noexcept {
52 kernel_.prepared_bytes() +
53 (kernel_.prepared_bytes() != 0 ? kConverterAllocationOverhead : 0u);
54 }
55
56 float read(std::span<const float> source, double position) const noexcept {
57 if (source.empty() ||
58 source.size() > static_cast<std::size_t>(std::numeric_limits<int>::max()) ||
59 !std::isfinite(position))
60 return 0.0f;
61 position = std::clamp(position, 0.0, static_cast<double>(source.size() - 1));
62 return kernel_.read(source.data(), static_cast<int>(source.size()), position);
63 }
64
65 private:
67 struct IncrementalBuildTag {};
68
69 PreparedSampleRateConversion(IncrementalBuildTag, double cutoff) {
70 if (std::isfinite(cutoff) && cutoff >= kMinimumSupportedCutoff)
71 kernel_.begin_build(32, 512, 10.0, cutoff);
72 }
73
74 bool build_step() {
75 return kernel_.build_step();
76 }
77
79};
80
82 public:
83 explicit SampleRateConversionBuilder(double cutoff) {
84 if (!std::isfinite(cutoff) ||
86 return;
87 result_.reset(new PreparedSampleRateConversion(
88 PreparedSampleRateConversion::IncrementalBuildTag{}, cutoff));
89 }
90
91 bool valid() const noexcept {
92 return result_ != nullptr;
93 }
94
95 bool step() {
96 return !result_ || result_->build_step();
97 }
98
99 std::shared_ptr<const PreparedSampleRateConversion> take() {
100 return result_ && result_->ready() ? std::move(result_) : nullptr;
101 }
102
103 private:
104 std::shared_ptr<PreparedSampleRateConversion> result_;
105};
106
114class PreparedVariableRateConversion;
115
117 public:
120 VariableRateConversionBuilder(std::shared_ptr<const AudioFileData> source,
121 std::uint64_t source_start, std::uint64_t source_frames);
122
123 bool step();
124 bool valid() const noexcept {
125 return result_ != nullptr;
126 }
127 std::shared_ptr<const PreparedVariableRateConversion> take();
128
129 private:
130 std::shared_ptr<PreparedVariableRateConversion> result_;
131 signal::SincResampler decimator_;
132 std::size_t kernel_level_ = 0;
133 bool decimator_started_ = false;
134 std::size_t level_ = 0;
135 std::size_t channel_ = 0;
136 std::size_t frame_ = 0;
137};
138
139class PreparedVariableRateConversion {
140 public:
141 static constexpr std::size_t kLinearCutoffLevels = 32;
142 static constexpr std::size_t kCutoffLevels = kLinearCutoffLevels + 1;
143 static constexpr std::size_t kPyramidLevels = 19;
144 static constexpr std::size_t kPyramidBlockSamples = 1'024;
145 static constexpr double kMaximumSourceFramesPerOutputFrame = 1'048'576.0;
146
147 static std::shared_ptr<const PreparedVariableRateConversion>
148 build(std::shared_ptr<const AudioFileData> source, std::uint64_t source_start,
149 std::uint64_t source_frames) {
150 VariableRateConversionBuilder builder(std::move(source), source_start, source_frames);
151 while (!builder.step()) {
152 }
153 return builder.take();
154 }
155
156 static std::optional<std::uint64_t> prepared_bytes(std::uint64_t source_frames,
157 std::size_t channels) noexcept;
158
159 std::uint64_t prepared_bytes() const noexcept {
160 return prepared_bytes_;
161 }
162
163 bool matches_source_slice(const std::shared_ptr<const AudioFileData>& source,
164 std::uint64_t source_start,
165 std::uint64_t source_frames) const noexcept {
166 return source_ == source && source_start_ == source_start &&
167 source_frames_ == source_frames;
168 }
169
170 float read(std::size_t channel, double position,
171 double source_frames_per_output_frame) const noexcept {
172 if (!source_ || channel >= source_->channels.size() || !std::isfinite(position) ||
173 !std::isfinite(source_frames_per_output_frame) ||
174 source_frames_per_output_frame <= 0.0 ||
175 source_frames_per_output_frame > kMaximumSourceFramesPerOutputFrame)
176 return 0.0f;
177
178 const auto pyramid_level =
179 source_frames_per_output_frame <= 2.0
180 ? std::size_t{0}
181 : std::min(kPyramidLevels, static_cast<std::size_t>(std::ceil(
182 std::log2(source_frames_per_output_frame / 2.0))));
183 const auto scale = std::exp2(static_cast<double>(pyramid_level));
184 const auto residual_step = source_frames_per_output_frame / scale;
185 const auto required_cutoff = std::min(1.0, 1.0 / residual_step);
186 const auto kernel_level = static_cast<std::size_t>(
187 std::ceil((1.0 - required_cutoff) * 2.0 * static_cast<double>(kLinearCutoffLevels)));
188 if (kernel_level >= kernels_.size())
189 return 0.0f;
190
191 position /= scale;
192 if (pyramid_level == 0) {
193 const auto source = std::span<const float>(source_->channels[channel])
194 .subspan(static_cast<std::size_t>(source_start_),
195 static_cast<std::size_t>(source_frames_));
196 if (source.empty() ||
197 source.size() > static_cast<std::size_t>(std::numeric_limits<int>::max()))
198 return 0.0f;
199 return kernels_[kernel_level].read(source.data(), static_cast<int>(source.size()),
200 position);
201 }
202 return read_chunked(kernels_[kernel_level], pyramid_[pyramid_level - 1u][channel],
203 position);
204 }
205
206 private:
208
209 struct ChunkedSamples {
210 static constexpr std::size_t kFanout = 64;
211 using SampleBlock = std::array<float, kPyramidBlockSamples>;
212
213 struct LeafPage {
215 for (auto* block : blocks)
216 delete block;
217 }
218 std::array<SampleBlock*, kFanout> blocks{};
219 };
220
221 struct MiddlePage {
223 for (auto* leaf : leaves)
224 delete leaf;
225 }
226 std::array<LeafPage*, kFanout> leaves{};
227 };
228
229 struct UpperPage {
231 for (auto* middle : middles)
232 delete middle;
233 }
234 std::array<MiddlePage*, kFanout> middles{};
235 };
236
237 ChunkedSamples() = default;
238 ChunkedSamples(const ChunkedSamples&) = delete;
239 ChunkedSamples& operator=(const ChunkedSamples&) = delete;
240
241 ~ChunkedSamples() {
242 for (auto* upper : uppers)
243 delete upper;
244 }
245
246 bool push_back(float sample) noexcept {
247 auto block_index = size / kPyramidBlockSamples;
248 const auto block_slot = block_index % kFanout;
249 block_index /= kFanout;
250 const auto leaf_slot = block_index % kFanout;
251 block_index /= kFanout;
252 const auto middle_slot = block_index % kFanout;
253 const auto upper_slot = block_index / kFanout;
254 if (upper_slot >= kFanout)
255 return false;
256 auto*& upper = uppers[upper_slot];
257 if (!upper) {
258 upper = new (std::nothrow) UpperPage;
259 if (!upper)
260 return false;
261 ++upper_count;
262 }
263 auto*& middle = upper->middles[middle_slot];
264 if (!middle) {
265 middle = new (std::nothrow) MiddlePage;
266 if (!middle)
267 return false;
268 ++middle_count;
269 }
270 auto*& leaf = middle->leaves[leaf_slot];
271 if (!leaf) {
272 leaf = new (std::nothrow) LeafPage;
273 if (!leaf)
274 return false;
275 ++leaf_count;
276 }
277 auto*& block = leaf->blocks[block_slot];
278 if (!block) {
279 block = new (std::nothrow) SampleBlock;
280 if (!block)
281 return false;
282 ++block_count;
283 }
284 (*block)[size % kPyramidBlockSamples] = sample;
285 ++size;
286 return true;
287 }
288
289 float operator[](std::size_t index) const noexcept {
290 auto block_index = index / kPyramidBlockSamples;
291 const auto block_slot = block_index % kFanout;
292 block_index /= kFanout;
293 const auto leaf_slot = block_index % kFanout;
294 block_index /= kFanout;
295 const auto middle_slot = block_index % kFanout;
296 const auto upper_slot = block_index / kFanout;
297 return (*uppers[upper_slot]
298 ->middles[middle_slot]
299 ->leaves[leaf_slot]
300 ->blocks[block_slot])[index % kPyramidBlockSamples];
301 }
302
303 std::uint64_t allocated_bytes() const noexcept {
304 const auto allocations = upper_count + middle_count + leaf_count + block_count;
305 return upper_count * sizeof(UpperPage) + middle_count * sizeof(MiddlePage) +
306 leaf_count * sizeof(LeafPage) + block_count * sizeof(SampleBlock) +
307 allocations * kConverterAllocationOverhead;
308 }
309
310 std::array<UpperPage*, kFanout> uppers{};
311 std::size_t size = 0;
312 std::uint64_t upper_count = 0;
313 std::uint64_t middle_count = 0;
314 std::uint64_t leaf_count = 0;
315 std::uint64_t block_count = 0;
316 };
317
318 static float read_chunked(const signal::SincResampler& kernel, const ChunkedSamples& source,
319 double position) noexcept {
320 if (source.size == 0 || !std::isfinite(position))
321 return 0.0f;
322 position = std::clamp(position, 0.0, static_cast<double>(source.size - 1u));
323 const auto center = static_cast<std::int64_t>(std::floor(position));
324 const auto fraction = position - static_cast<double>(center);
325 std::array<float, 64> neighborhood{};
326 const auto taps = static_cast<std::size_t>(kernel.taps());
327 for (std::size_t tap = 0; tap < taps; ++tap) {
328 const auto index =
329 std::clamp(center + static_cast<std::int64_t>(tap) - kernel.half_width() + 1,
330 std::int64_t{0}, static_cast<std::int64_t>(source.size - 1u));
331 neighborhood[tap] = source[static_cast<std::size_t>(index)];
332 }
333 return kernel.apply(neighborhood.data(), fraction);
334 }
335
336 PreparedVariableRateConversion(std::shared_ptr<const AudioFileData> source,
337 std::uint64_t source_start, std::uint64_t source_frames)
338 : source_(std::move(source)), source_start_(source_start), source_frames_(source_frames) {
339 const auto bytes = prepared_bytes(source_frames_, source_ ? source_->channels.size() : 0);
340 prepared_bytes_ = bytes.value_or(0);
341 }
342
343 void refresh_prepared_bytes() noexcept {
344 std::uint64_t bytes =
346 for (const auto& kernel : kernels_)
347 bytes += kernel.prepared_bytes() +
348 (kernel.prepared_bytes() != 0 ? kConverterAllocationOverhead : 0u);
349 const auto channels = source_->channels.size();
350 for (const auto& level : pyramid_) {
351 if (!level)
352 continue;
353 bytes += channels * sizeof(ChunkedSamples) + kConverterAllocationOverhead;
354 for (std::size_t channel = 0; channel < channels; ++channel)
355 bytes += level[channel].allocated_bytes();
356 }
357 prepared_bytes_ = bytes;
358 }
359
360 std::shared_ptr<const AudioFileData> source_;
361 std::uint64_t source_start_ = 0;
362 std::uint64_t source_frames_ = 0;
363 std::uint64_t prepared_bytes_ = 0;
364 std::array<std::unique_ptr<ChunkedSamples[]>, kPyramidLevels> pyramid_;
365 std::array<signal::SincResampler, kCutoffLevels> kernels_;
366};
367
369 constexpr auto kernel_table_bytes = (512u + 1u) * (2u * 32u) * sizeof(float);
372}
373
374inline std::optional<std::uint64_t>
376 std::size_t channels) noexcept {
377 if (source_frames == 0 ||
378 source_frames > static_cast<std::uint64_t>(std::numeric_limits<int>::max()) ||
379 channels == 0)
380 return std::nullopt;
381 constexpr std::uint64_t kernel_table_bytes =
382 kCutoffLevels * (128u + 1u) * (2u * 16u) * sizeof(float);
383 std::uint64_t bytes = sizeof(PreparedVariableRateConversion) + kConverterAllocationOverhead +
384 kConverterSharedOwnershipBytes + kernel_table_bytes +
386 auto level_frames = source_frames;
387 for (std::size_t level = 0; level < kPyramidLevels; ++level) {
388 level_frames = level_frames / 2u + level_frames % 2u;
389 const auto ceil_count = [](std::uint64_t value, std::uint64_t divisor) noexcept {
390 return value / divisor + (value % divisor != 0 ? 1u : 0u);
391 };
392 const auto block_count = ceil_count(level_frames, kPyramidBlockSamples);
393 const auto leaf_count = ceil_count(block_count, ChunkedSamples::kFanout);
394 const auto middle_count = ceil_count(leaf_count, ChunkedSamples::kFanout);
395 const auto upper_count = ceil_count(middle_count, ChunkedSamples::kFanout);
396 const auto allocation_count = block_count + leaf_count + middle_count + upper_count;
397 const auto channel_bytes = sizeof(ChunkedSamples) +
398 block_count * sizeof(ChunkedSamples::SampleBlock) +
399 leaf_count * sizeof(ChunkedSamples::LeafPage) +
400 middle_count * sizeof(ChunkedSamples::MiddlePage) +
401 upper_count * sizeof(ChunkedSamples::UpperPage) +
402 allocation_count * kConverterAllocationOverhead;
403 if (bytes > std::numeric_limits<std::uint64_t>::max() - kConverterAllocationOverhead)
404 return std::nullopt;
405 const auto available =
406 std::numeric_limits<std::uint64_t>::max() - bytes - kConverterAllocationOverhead;
407 if (channels > available / channel_bytes)
408 return std::nullopt;
409 bytes += channel_bytes * channels + kConverterAllocationOverhead;
410 }
411 return bytes;
412}
413
415 std::shared_ptr<const AudioFileData> source, std::uint64_t source_start,
416 std::uint64_t source_frames) {
417 if (!source || source_frames == 0 || source_frames > std::numeric_limits<int>::max() ||
418 source->channels.empty())
419 return;
420 const auto total_frames = source->num_frames();
421 if (total_frames == 0 || source_start > total_frames ||
422 source_frames > total_frames - source_start ||
423 !std::all_of(
424 source->channels.begin(), source->channels.end(),
425 [total_frames](const auto& channel) { return channel.size() == total_frames; }))
426 return;
427 result_.reset(
428 new PreparedVariableRateConversion(std::move(source), source_start, source_frames));
429 result_->kernels_[0].begin_build(16, 128, 10.0, 1.0);
430}
431
433 if (!result_ || level_ == PreparedVariableRateConversion::kPyramidLevels)
434 return true;
436 if (!result_->kernels_[kernel_level_].build_step())
437 return false;
438 ++kernel_level_;
440 const auto cutoff =
441 1.0 - static_cast<double>(kernel_level_) /
442 (2.0 * static_cast<double>(
444 result_->kernels_[kernel_level_].begin_build(16, 128, 10.0, cutoff);
445 }
446 return false;
447 }
448 if (!decimator_started_) {
449 decimator_.begin_build(32, 128, 10.0, 0.5);
450 decimator_started_ = true;
451 return false;
452 }
453 if (!decimator_.build_step())
454 return false;
455 if (!result_->pyramid_[level_]) {
456 auto* channels = new (std::nothrow)
457 PreparedVariableRateConversion::ChunkedSamples[result_->source_->channels.size()];
458 if (!channels) {
459 result_.reset();
460 return true;
461 }
462 result_->pyramid_[level_].reset(channels);
463 return false;
464 }
465 auto& output = result_->pyramid_[level_][channel_];
466 const auto source_frames =
467 level_ == 0 ? result_->source_frames_ : result_->pyramid_[level_ - 1u][channel_].size;
468 const auto output_frames = (source_frames + 1u) / 2u;
469 if (frame_ < output_frames) {
470 const auto position = static_cast<double>(frame_ * 2u);
471 const auto sample =
472 level_ == 0 ? decimator_.read(result_->source_->channels[channel_].data() +
473 static_cast<std::size_t>(result_->source_start_),
474 static_cast<int>(result_->source_frames_), position)
475 : PreparedVariableRateConversion::read_chunked(
476 decimator_, result_->pyramid_[level_ - 1u][channel_], position);
477 if (!output.push_back(sample)) {
478 result_.reset();
479 return true;
480 }
481 ++frame_;
482 return false;
483 }
484 frame_ = 0;
485 ++channel_;
486 if (channel_ == result_->source_->channels.size()) {
487 channel_ = 0;
488 ++level_;
489 }
491}
492
493inline std::shared_ptr<const PreparedVariableRateConversion> VariableRateConversionBuilder::take() {
495 return nullptr;
496 result_->refresh_prepared_bytes();
497 return std::move(result_);
498}
499
500} // namespace pulp::audio
Immutable, allocation-free read kernel after control-thread construction.
Definition sample_rate_conversion.hpp:31
PreparedSampleRateConversion(double cutoff)
Definition sample_rate_conversion.hpp:37
float read(std::span< const float > source, double position) const noexcept
Definition sample_rate_conversion.hpp:56
static std::uint64_t estimated_prepared_bytes() noexcept
Definition sample_rate_conversion.hpp:368
static constexpr double kMinimumSupportedCutoff
Definition sample_rate_conversion.hpp:35
std::uint64_t prepared_bytes() const noexcept
Definition sample_rate_conversion.hpp:50
friend class SampleRateConversionBuilder
Definition sample_rate_conversion.hpp:66
bool ready() const noexcept
Definition sample_rate_conversion.hpp:44
Definition sample_rate_conversion.hpp:139
static constexpr std::size_t kPyramidLevels
Definition sample_rate_conversion.hpp:143
static std::shared_ptr< const PreparedVariableRateConversion > build(std::shared_ptr< const AudioFileData > source, std::uint64_t source_start, std::uint64_t source_frames)
Definition sample_rate_conversion.hpp:148
static constexpr std::size_t kPyramidBlockSamples
Definition sample_rate_conversion.hpp:144
float read(std::size_t channel, double position, double source_frames_per_output_frame) const noexcept
Definition sample_rate_conversion.hpp:170
friend class VariableRateConversionBuilder
Definition sample_rate_conversion.hpp:207
static constexpr std::size_t kCutoffLevels
Definition sample_rate_conversion.hpp:142
static constexpr double kMaximumSourceFramesPerOutputFrame
Definition sample_rate_conversion.hpp:145
bool matches_source_slice(const std::shared_ptr< const AudioFileData > &source, std::uint64_t source_start, std::uint64_t source_frames) const noexcept
Definition sample_rate_conversion.hpp:163
static constexpr std::size_t kLinearCutoffLevels
Definition sample_rate_conversion.hpp:141
std::uint64_t prepared_bytes() const noexcept
Definition sample_rate_conversion.hpp:159
Definition sample_rate_conversion.hpp:81
SampleRateConversionBuilder(double cutoff)
Definition sample_rate_conversion.hpp:83
std::shared_ptr< const PreparedSampleRateConversion > take()
Definition sample_rate_conversion.hpp:99
bool step()
Definition sample_rate_conversion.hpp:95
bool valid() const noexcept
Definition sample_rate_conversion.hpp:91
std::shared_ptr< const PreparedVariableRateConversion > take()
Definition sample_rate_conversion.hpp:493
VariableRateConversionBuilder(std::shared_ptr< const AudioFileData > source, std::uint64_t source_start, std::uint64_t source_frames)
Invalid/null sources and out-of-range slices produce an invalid builder; step() then completes immedi...
Definition sample_rate_conversion.hpp:414
bool valid() const noexcept
Definition sample_rate_conversion.hpp:124
bool step()
Definition sample_rate_conversion.hpp:432
bool build_step()
Definition sinc_resampler.hpp:81
void begin_build(int half_width=16, int phases=512, double beta=9.0, double cutoff=1.0)
Starts a resumable kernel-table build.
Definition sinc_resampler.hpp:67
Definition analyzer_provider.hpp:12
constexpr std::uint64_t kConverterSharedOwnershipBytes
Definition sample_rate_conversion.hpp:25
constexpr std::uint64_t kConverterAllocationOverhead
Definition sample_rate_conversion.hpp:24
@ center
Definition canvas.hpp:221
@ index
Definition gpu_pass.hpp:140
SincResamplerT< float > SincResampler
Definition sinc_resampler.hpp:199
@ move
Definition web_event_translate.hpp:110
@ block
Filled glyph cell; the covered glyph is drawn in the bg color.
Definition caret.hpp:66
STL namespace.
Fractional-delay resampling by a Kaiser-windowed sinc kernel.
std::array< SampleBlock *, kFanout > blocks
Definition sample_rate_conversion.hpp:218
std::array< LeafPage *, kFanout > leaves
Definition sample_rate_conversion.hpp:226
std::array< MiddlePage *, kFanout > middles
Definition sample_rate_conversion.hpp:234