DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
OfflineGainSource.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
10#include "OfflineProduct.h"
11#if DSPARK_HAS_OFFLINE
12#include <algorithm>
13#include <array>
14#include <cmath>
15#include <cstddef>
16#include <cstdint>
17#include <limits>
18#include <memory>
19#include <optional>
20#include <utility>
21
22namespace dspark::detail
23{
24
25// Closed half-line sums for independent constant gain continuation at each end.
26// All special-function arguments are positive half-integers. Recurrences are
27// anchored every 128 frames, so long sources need neither a full weight array
28// nor a transcendental evaluation for every source sample.
30{
31 public:
32 OfflineEndpointWeights(OfflineSession &job, std::int64_t frames, int block)
33 : frames_(frames), block_(block),
34 values_(job.allocate<double>(12 * static_cast<std::uint64_t>(block)))
35 {
36 cached_.fill(-1);
37 center_ = psi(static_cast<double>(frames) * .5 + .5)[0];
38 }
39
40 [[nodiscard]] const double *get(std::size_t leaf, int side, bool diagonal = false)
41 {
42 const auto slot = leaf % 3;
43 if (cached_[slot] != static_cast<std::int64_t>(leaf))
44 {
45 auto *data = values_.get() + slot * 4 * block_;
46 std::fill_n(data, 4 * block_, 0.0);
47 const auto first = static_cast<std::int64_t>(leaf) * block_;
48 const int count = static_cast<int>(std::min<std::int64_t>(block_, frames_ - first));
49 std::array<double, 2> left{}, right{};
50 std::int64_t previousLeft = 0, previousRight = 0;
51 for (int i = 0; i < count; ++i)
52 {
53 const auto frame = first + i;
54 const auto dl = frame + (frame % 2 ? 2 : 1);
55 auto dr = frames_ - frame;
56 dr += 1 - dr % 2;
57 if (i % 128 == 0)
58 {
59 left = psi(static_cast<double>(dl) * .5);
60 right = psi(static_cast<double>(dr) * .5);
61 }
62 else
63 {
64 if (dl != previousLeft)
65 {
66 const double reciprocal = 2 / static_cast<double>(previousLeft);
67 left[0] += reciprocal;
68 left[1] -= reciprocal * reciprocal;
69 }
70 if (dr != previousRight)
71 {
72 const double reciprocal = 2 / static_cast<double>(dr);
73 right[0] -= reciprocal;
74 right[1] += reciprocal * reciprocal;
75 }
76 }
77 previousLeft = dl;
78 previousRight = dr;
79 data[i] = .5 * (left[0] - center_);
80 data[block_ + i] = -.5 * (right[0] - center_);
81 constexpr double divisor = 4 * std::numbers::pi * std::numbers::pi;
82 data[2 * block_ + i] = left[1] / divisor;
83 data[3 * block_ + i] = right[1] / divisor;
84 }
85 cached_[slot] = static_cast<std::int64_t>(leaf);
86 }
87 return values_.get() + (slot * 4 + side + (diagonal ? 2 : 0)) * block_;
88 }
89
90 private:
91 [[nodiscard]] static std::array<double, 2> psi(double z) noexcept
92 {
93 double p = 0, q = 0;
94 while (z < 32)
95 {
96 const double u = 1 / z;
97 p -= u;
98 q += u * u;
99 z += 1;
100 }
101 const double u = 1 / z, v = u * u;
102 // Digamma and trigamma recurrence plus Bernoulli asymptotic expansion.
103 // The first omitted term at z>=32 is below double rounding accuracy.
104 p += std::log(z) - u / 2 - v * (1. / 12 - v * (1. / 120 - v *
105 (1. / 252 - v * (1. / 240 - v / 132))));
106 q += u + v / 2 + u * v * (1. / 6 - v * (1. / 30 - v *
107 (1. / 42 - v * (1. / 30 - v * 5. / 66))));
108 return {p, q};
109 }
110 std::int64_t frames_;
111 int block_;
112 double center_ = 0;
113 std::array<std::int64_t, 3> cached_{};
114 std::unique_ptr<double[]> values_;
115};
116
117template <FloatType T, class Cursor> class OfflineGainSource final
118{
119 // The nested product stencil and endpoint transform together need five
120 // neighbouring source leaves. Retain all five to avoid repeated PCM reads,
121 // hashing and control reconstruction when visiting the same output block.
122 static constexpr std::size_t cacheLeaves = 5;
123 public:
125 const OfflineFingerprint &fingerprint, double peak, OfflineSession &job,
126 const OfflineJobOptions &options, Cursor cursor)
127 : source_(source), spec_(spec), expected_(fingerprint), peak_(peak), job_(job),
128 options_(options), cursor_(std::move(cursor))
129 {
130 offlineValidateSpec(spec, 2);
131 while (block_ < spec.frames && block_ < 4096)
132 block_ *= 2;
133 leaves_ = static_cast<std::uint64_t>(spec.frames / block_ + (spec.frames % block_ != 0));
134 fingerprints_ = job.allocate<OfflineFingerprint>(leaves_);
135 audio_ = job.allocate<T>(cacheLeaves * static_cast<std::uint64_t>(block_) * spec.channels);
136 gain_ = job.allocate<double>(cacheLeaves * static_cast<std::uint64_t>(block_));
137 reset();
138 preflight();
139 }
140
141 [[nodiscard]] int block() const noexcept
142 {
143 return block_;
144 }
145 [[nodiscard]] std::int64_t frames() const noexcept
146 {
147 return spec_.frames;
148 }
149 [[nodiscard]] int channels() const noexcept
150 {
151 return spec_.channels;
152 }
153 [[nodiscard]] std::size_t leaves() const noexcept
154 {
155 return static_cast<std::size_t>(leaves_);
156 }
157 [[nodiscard]] double normalizer() const noexcept
158 {
159 return peak_ > 0 ? peak_ : 1;
160 }
161 [[nodiscard]] double baseline() const noexcept
162 {
163 return baseline_;
164 }
165 [[nodiscard]] double endpoint(int side) const noexcept
166 {
167 return endpoints_[side];
168 }
169 [[nodiscard]] double minimumGain() const noexcept
170 {
171 return minimum_;
172 }
173 [[nodiscard]] double maximumGain() const noexcept
174 {
175 return maximum_;
176 }
177 [[nodiscard]] double scalarPeak() const noexcept
178 {
179 return scalarPeak_;
180 }
181 [[nodiscard]] bool constant() const noexcept
182 {
183 return minimum_ == maximum_;
184 }
185
186 void reset() noexcept
187 {
188 cached_.fill(-1);
189 }
190
191 [[nodiscard]] const T *audio(std::size_t leaf, int channel)
192 {
193 load(leaf);
194 return audio_.get() + (leaf % cacheLeaves * spec_.channels + channel) * block_;
195 }
196 [[nodiscard]] const double *gain(std::size_t leaf)
197 {
198 load(leaf);
199 return gain_.get() + leaf % cacheLeaves * block_;
200 }
201
202 void readAudio(int channel, std::int64_t first, int count, double *output)
203 {
204 read(first, count, output, [&](std::size_t leaf, int offset, int length, double *out) {
205 const auto *input = audio(leaf, channel) + offset;
206 for (int i = 0; i < length; ++i)
207 out[i] = static_cast<double>(input[i]) / normalizer();
208 });
209 }
210 void readControl(std::int64_t first, int count, double *output)
211 {
212 read(first, count, output, [&](std::size_t leaf, int offset, int length, double *out) {
213 const auto *input = gain(leaf) + offset;
214 for (int i = 0; i < length; ++i)
215 out[i] = input[i] - baseline_;
216 });
217 }
218 void verifySpec() const
219 {
220 if (source_.getSpec() != spec_)
222 }
223
224 private:
225 template <class Sample> void read(std::int64_t first, int count, double *output, Sample sample)
226 {
227 if (first < 0 || count < 0 || count > spec_.frames || first > spec_.frames - count)
229 while (count > 0)
230 {
231 const int offset = static_cast<int>(first % block_);
232 const int length = std::min(block_ - offset, count);
233 sample(static_cast<std::size_t>(first / block_), offset, length, output);
234 first += length;
235 output += length;
236 count -= length;
237 }
238 }
239 void load(std::size_t leaf)
240 {
241 if (leaf >= leaves_)
243 const auto slot = leaf % cacheLeaves;
244 if (cached_[slot] == static_cast<std::int64_t>(leaf))
245 return;
246 const auto first = static_cast<std::int64_t>(leaf) * block_;
247 const int count = static_cast<int>(std::min<std::int64_t>(block_, spec_.frames - first));
248 auto *data = audio_.get() + slot * spec_.channels * block_;
249 auto *control = gain_.get() + slot * block_;
250 std::fill_n(data, spec_.channels * block_, T(0));
251 for (int offset = 0; offset < count;)
252 {
253 const int length = std::min(options_.blockFrames, count - offset);
254 std::array<T *, 2> channels{};
255 for (int c = 0; c < spec_.channels; ++c)
256 channels[c] = data + c * block_ + offset;
257 offlineRead(source_, spec_, first + offset,
258 AudioBufferView<T>(channels.data(), spec_.channels, length));
259 offset += length;
260 }
261 OfflineFingerprint fingerprint;
262 for (int i = 0; i < count; ++i)
263 {
264 const double gain = cursor_(first + i);
265 if (!(gain > 0) || !std::isfinite(gain))
267 control[i] = gain;
268 for (int c = 0; c < spec_.channels; ++c)
269 {
270 const T x = data[c * block_ + i];
271 if (!std::isfinite(x))
273 if (std::abs(static_cast<double>(x)) > peak_)
275 offlineHash(fingerprint, x);
276 }
277 }
278 if (verified_ && fingerprint != fingerprints_[leaf])
280 if (!verified_)
281 fingerprints_[leaf] = fingerprint;
282 cached_[slot] = static_cast<std::int64_t>(leaf);
283 }
284 void preflight()
285 {
286 job_.checkpoint(OfflinePhase::Verify, 0, spec_.frames);
287 OfflineFingerprint fingerprint;
288 double firstGain = 1, lastGain = 1;
289 for (std::size_t leaf = 0; leaf < leaves_; ++leaf)
290 {
291 const auto first = static_cast<std::int64_t>(leaf) * block_;
292 const int count =
293 static_cast<int>(std::min<std::int64_t>(block_, spec_.frames - first));
294 const auto *control = gain(leaf);
295 const auto *data = audio(leaf, 0);
296 if (leaf == 0)
297 firstGain = control[0];
298 lastGain = control[count - 1];
299 for (int i = 0; i < count; ++i)
300 {
301 minimum_ = std::min(minimum_, control[i]);
302 maximum_ = std::max(maximum_, control[i]);
303 for (int c = 0; c < spec_.channels; ++c)
304 {
305 const T x = data[c * block_ + i];
306 offlineHash(fingerprint, x);
307 scalarPeak_ =
308 std::max(scalarPeak_,
309 std::abs(static_cast<double>(x) / normalizer() * control[i]));
310 }
311 }
312 job_.checkpoint(OfflinePhase::Verify, first + count, spec_.frames);
313 }
314 verifySpec();
315 if (fingerprint != expected_)
317 // The mean is only an affine reference. The gain product independently
318 // continues firstGain to the left and lastGain to the right.
319 endpoints_ = {firstGain, lastGain};
320 baseline_ = (firstGain + lastGain) * .5;
321 verified_ = true;
322 reset();
323 }
324
325 OfflineAudioSource<T> &source_;
326 OfflineAudioSpec spec_;
327 OfflineFingerprint expected_;
328 double peak_;
329 OfflineSession &job_;
330 const OfflineJobOptions &options_;
331 Cursor cursor_;
332 int block_ = 128;
333 std::uint64_t leaves_ = 0;
334 bool verified_ = false;
335 double minimum_ = std::numeric_limits<double>::infinity(), maximum_ = 0;
336 double baseline_ = 1, scalarPeak_ = 0;
337 std::array<double, 2> endpoints_{1, 1};
338 std::array<std::int64_t, cacheLeaves> cached_{};
339 std::unique_ptr<OfflineFingerprint[]> fingerprints_;
340 std::unique_ptr<T[]> audio_;
341 std::unique_ptr<double[]> gain_;
342};
343
344template <FloatType T, class Cursor> class OfflineGainProduct final
345{
347 struct AudioReader
348 {
349 Source *source = nullptr;
350 int channel = 0;
351 void operator()(std::int64_t first, int count, double *output)
352 {
353 source->readAudio(channel, first, count, output);
354 }
355 };
356 struct ControlReader
357 {
358 Source *source = nullptr;
359 void operator()(std::int64_t first, int count, double *output)
360 {
361 source->readControl(first, count, output);
362 }
363 };
364 struct WeightedReader
365 {
366 AudioReader *audio = nullptr;
367 OfflineEndpointWeights *weights = nullptr;
368 int block = 0, side = 0;
369 std::array<double, 2> coefficients{};
370 void operator()(std::int64_t first, int count, double *output)
371 {
372 (*audio)(first, count, output);
373 for (int offset = 0; offset < count;)
374 {
375 const auto frame = first + offset;
376 const int begin = static_cast<int>(frame % block);
377 const int length = std::min(block - begin, count - offset);
378 const auto leaf = static_cast<std::size_t>(frame / block);
379 const auto *weight = weights->get(leaf, side == 2 ? 0 : side);
380 const auto *right = side == 2 ? weights->get(leaf, 1) : nullptr;
381 for (int i = 0; i < length; ++i)
382 output[offset + i] *= right
383 ? coefficients[0] * weight[begin + i] + coefficients[1] * right[begin + i]
384 : weight[begin + i];
385 offset += length;
386 }
387 }
388 };
390
391 public:
392 OfflineGainProduct(Source &source, OfflineSession &job, bool calibrateBoundary = false)
393 : source_(source), work_(job, source.frames(), source.block()), control_{&source}
394 {
395 endpoints_ = {source.endpoint(0), source.endpoint(1)};
396 gainMap_.emplace(work_, control_);
397 for (int c = 0; c < source.channels(); ++c)
398 {
399 source.reset();
400 readers_[c] = {&source, c};
401 audioMaps_[c].emplace(work_, readers_[c]);
402 combinations_[c].emplace(work_, readers_[c], control_, *audioMaps_[c], *gainMap_);
403 source.reset();
404 combinedMaps_[c].emplace(work_, *combinations_[c]);
405 }
406 if (calibrateBoundary || source.endpoint(0) != source.endpoint(1))
407 {
408 boundaryCount_ = calibrateBoundary ? 2 : 1;
409 work_.prepareCauchy();
410 weights_.emplace(job, source.frames(), source.block());
411 boundaryWork_ = job.allocate<double>(2 * static_cast<std::uint64_t>(source.block()));
412 if (calibrateBoundary)
413 boundaryResponse_ = job.allocate<double>(
414 2 * static_cast<std::uint64_t>(source.block()) * source.channels());
415 for (int c = 0; c < source.channels(); ++c)
416 for (int side = 0; side < boundaryCount_; ++side)
417 {
418 // Fixed endpoints are one linear combination. Only calibration
419 // needs the two independent basis maps and response buffers.
420 weighted_[c][side] = {&readers_[c], &*weights_, source.block(),
421 calibrateBoundary ? side : 2,
422 {endpoints_[0] - source.baseline(), endpoints_[1] - source.baseline()}};
423 source.reset();
424 boundaryMaps_[c][side].emplace(work_, weighted_[c][side]);
425 }
426 }
427 output_ =
428 job.allocate<double>(static_cast<std::uint64_t>(source.block()) * source.channels());
429 }
432 void setEndpoints(double left, double right) noexcept
433 {
434 endpoints_ = {left, right};
435 }
436 [[nodiscard]] const double *boundaryResponse(int side) const noexcept
437 {
438 return boundaryResponse_.get() + side * source_.channels() * source_.block();
439 }
440 void reset() noexcept
441 {
442 source_.reset();
443 for (int c = 0; c < source_.channels(); ++c)
444 combinations_[c]->reset();
445 }
446 // Returned planar deltas last until the next evaluate(). The maps are reused
447 // for target calibration, verification and provisional output, with no new
448 // allocations or whole-source temporary between these passes.
449 [[nodiscard]] const double *evaluate(std::size_t leaf)
450 {
451 const int block = source_.block();
452 for (int c = 0; c < source_.channels(); ++c)
453 {
454 auto *output = output_.get() + c * block;
455 combinedMaps_[c]->evaluate(work_, leaf, *combinations_[c], output, true);
456 const auto *data = combinations_[c]->get(leaf);
457 for (int i = 0; i < block; ++i)
458 output[i] = (source_.baseline() - 1) * data[i] + .75 * data[i] * data[block + i] +
459 data[2 * block + i] * data[3 * block + i] - output[i];
460 if (weights_)
461 {
462 auto *a = boundaryWork_.get(), *weighted = a + block;
463 audioMaps_[c]->evaluateCauchy(work_, leaf, readers_[c], a, true);
464 for (int side = 0; side < boundaryCount_; ++side)
465 {
466 boundaryMaps_[c][side]->evaluateCauchy(work_, leaf, weighted_[c][side],
467 weighted, true);
468 const auto *f = weights_->get(leaf, side);
469 const auto *d = weights_->get(leaf, side, true);
470 const auto *rightF = boundaryCount_ == 1 ? weights_->get(leaf, 1) : nullptr;
471 const auto *rightD = boundaryCount_ == 1 ? weights_->get(leaf, 1, true) : nullptr;
472 auto *response = boundaryResponse_
473 ? boundaryResponse_.get() + (side * source_.channels() + c) * block : weighted;
474 const double left = endpoints_[0] - source_.baseline();
475 const double right = endpoints_[1] - source_.baseline();
476 const double coefficient = boundaryCount_ == 1 ? 1 : side == 0 ? left : right;
477 for (int i = 0; i < block; ++i)
478 {
479 const double sign = i % 2 ? -1. : 1.;
480 const double fValue = rightF ? left * f[i] + right * rightF[i] : f[i];
481 const double dValue = rightD ? left * d[i] + right * rightD[i] : d[i];
482 response[i] = dValue * data[i] +
483 sign / std::numbers::pi * (fValue * a[i] - weighted[i]);
484 output[i] += coefficient * response[i];
485 }
486 }
487 }
488 }
489 return output_.get();
490 }
491
492 private:
493 Source &source_;
495 std::array<AudioReader, 2> readers_{};
496 ControlReader control_;
497 std::optional<OfflineHilbertMap> gainMap_;
498 std::array<std::optional<OfflineHilbertMap>, 2> audioMaps_, combinedMaps_;
499 std::array<std::optional<Combination>, 2> combinations_;
500 std::optional<OfflineEndpointWeights> weights_;
501 std::array<std::array<WeightedReader, 2>, 2> weighted_{};
502 std::array<std::array<std::optional<OfflineHilbertMap>, 2>, 2> boundaryMaps_;
503 std::unique_ptr<double[]> boundaryWork_;
504 std::unique_ptr<double[]> boundaryResponse_;
505 std::array<double, 2> endpoints_{1, 1};
506 int boundaryCount_ = 0;
507 std::unique_ptr<double[]> output_;
508};
509} // namespace dspark::detail
510#endif // DSPARK_HAS_OFFLINE
Rewindable, complete-file source with int64 positions and bounded blocks.
OfflineEndpointWeights(OfflineSession &job, std::int64_t frames, int block)
const double * get(std::size_t leaf, int side, bool diagonal=false)
void setEndpoints(double left, double right) noexcept
const double * boundaryResponse(int side) const noexcept
const double * evaluate(std::size_t leaf)
OfflineGainProduct & operator=(const OfflineGainProduct &)=delete
OfflineGainProduct(Source &source, OfflineSession &job, bool calibrateBoundary=false)
OfflineGainProduct(const OfflineGainProduct &)=delete
double maximumGain() const noexcept
void readAudio(int channel, std::int64_t first, int count, double *output)
const T * audio(std::size_t leaf, int channel)
const double * gain(std::size_t leaf)
double minimumGain() const noexcept
double endpoint(int side) const noexcept
std::int64_t frames() const noexcept
std::size_t leaves() const noexcept
OfflineGainSource(OfflineAudioSource< T > &source, const OfflineAudioSpec &spec, const OfflineFingerprint &fingerprint, double peak, OfflineSession &job, const OfflineJobOptions &options, Cursor cursor)
void readControl(std::int64_t first, int count, double *output)
std::unique_ptr< U[]> allocate(std::uint64_t count)
void checkpoint(OfflinePhase phase, std::int64_t completed, std::int64_t total) const
void offlineFail(OfflineStatus status)
void offlineRead(OfflineAudioSource< T > &source, const OfflineAudioSpec &expected, std::int64_t first, AudioBufferView< T > block)
void offlineValidateSpec(const OfflineAudioSpec &spec, int maximumChannels=16)
void offlineHash(OfflineFingerprint &state, T sample) noexcept
Immutable source format and host-provided content/timeline identity.
Noncryptographic PCM fingerprint, stable across block divisions.
Resource and cooperative-cancellation controls for one worker operation.