DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
OfflineClip.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
10#include "../OfflineProcessing.h"
11#if DSPARK_HAS_OFFLINE
12#include "../../Analysis/OfflineEnergyAnalyzer.h"
13#include "../TruePeakDetector.h"
14#include "OfflineClipProjection.h"
15#include "OfflineGainSource.h"
16#include "OfflineWorker.h"
17#include <algorithm>
18#include <array>
19#include <cmath>
20#include <limits>
21#include <memory>
22#include <optional>
23#include <span>
24#include <type_traits>
25#include <utility>
26
27namespace dspark
28{
31{
32 Sine,
33 Tanh,
35};
36
37namespace detail
38{
40{
41 double reductionDb = 0;
43 std::span<const OfflineRegion> exclusions;
44};
49
51{
52 double peak = 0, truePeak = 0;
53 bool representable = true;
54};
56{
57 double ceiling = 1, peak = 0, errorDb = std::numeric_limits<double>::infinity();
58 int attempts = 0;
59 bool converged = false, bracketed = false;
60};
61
62inline double offlineClipPeakErrorDb(double peak, double target) noexcept
63{
64 return peak > 0 ? (20 / std::log(10.)) * std::log1p((peak - target) / target)
65 : -std::numeric_limits<double>::infinity();
66}
67
68// A fixed absolute dB tolerance can accept unity or even gain for a small
69// reduction request. Bound the error by half the representable reduction too.
70inline bool offlineClipTargetMet(double peak, double target, double errorDb) noexcept
71{
72 if (target == 1)
73 return peak == 1;
74 const double toleranceDb = std::min(.005, -(10 / std::log(10.)) * std::log1p(target - 1));
75 return peak < 1 && std::abs(errorDb) <= toleranceDb;
76}
77
78// Search the actual rounded output peak. The peak need not be monotone in the
79// ceiling: maintain a measured sign bracket and reset all DSP for every trial.
80// Neither a final limiter nor an output gain adjustment participates.
81template <class Render>
82OfflineClipCalibration offlineCalibrateClip(double initialCeiling, double target, Render render)
83{
84 constexpr int maximumPasses = 24;
86 const auto trial = [&](double ceiling)
87 {
88 const auto measured = render(ceiling);
89 ++best.attempts;
90 if (!(measured.peak >= 0) || !std::isfinite(measured.peak))
92 const double error = offlineClipPeakErrorDb(measured.peak, target);
93 if (measured.representable && std::abs(error) < std::abs(best.errorDb))
94 {
95 best.ceiling = ceiling;
96 best.peak = measured.peak;
97 best.errorDb = error;
98 }
99 best.converged = offlineClipTargetMet(best.peak, target, best.errorDb);
100 return measured.peak;
101 };
102 double lo = initialCeiling, hi = initialCeiling;
103 double fl = trial(initialCeiling), fh = fl;
104 if (best.converged)
105 return best;
106 if (fl < target)
107 {
108 while (best.attempts < maximumPasses && fh < target && hi < 1e12)
109 {
110 lo = hi;
111 fl = fh;
112 hi *= 2;
113 fh = trial(hi);
114 if (best.converged)
115 return best;
116 }
117 }
118 else
119 {
120 // The caller implements the exact zero-curve endpoint, including its
121 // exclusion mask. An arbitrary tiny positive ceiling is both more
122 // expensive and less reliable as the lower calibration bracket.
123 lo = 0;
124 fl = trial(lo);
125 if (best.converged)
126 return best;
127 }
128 best.bracketed = fl < target && fh > target;
129 if (!best.bracketed)
130 return best;
131 double lowWeight = 1, highWeight = 1;
132 int previousSide = 0;
133 bool flatEndpoint = false;
134 while (best.attempts < maximumPasses)
135 {
136 // Illinois weighting prevents a nearly flat unity endpoint from
137 // pinning false position. Keep the actual measured sign bracket.
138 const double lowResidual = lowWeight * (target - fl);
139 const double highResidual = highWeight * (fh - target);
140 const double fraction = flatEndpoint
141 ? .5
142 : std::clamp(lowResidual / (lowResidual + highResidual), .1, .9);
143 const double ceiling = lo + fraction * (hi - lo);
144 if (!(ceiling > lo && ceiling < hi))
145 break;
146 const double peak = trial(ceiling);
147 if (best.converged)
148 return best;
149 if (peak > target)
150 {
151 // Rounded PCM can have an exactly flat region. Bisect it rather
152 // than spending repeated complete renders on the same peak.
153 flatEndpoint = peak == fh;
154 hi = ceiling;
155 fh = peak;
156 lowWeight = previousSide == 1 ? lowWeight * .5 : 1;
157 highWeight = 1;
158 previousSide = 1;
159 }
160 else
161 {
162 flatEndpoint = peak == fl;
163 lo = ceiling;
164 fl = peak;
165 highWeight = previousSide == -1 ? highWeight * .5 : 1;
166 lowWeight = 1;
167 previousSide = -1;
168 }
169 }
170 return best;
171}
172
173template <FloatType T, bool Soft> class OfflineClipProcessor
174{
175 public:
177 using Options = std::conditional_t<Soft, OfflineSoftClipOptions, OfflineClipOptions>;
189 struct Report
190 {
195 double targetErrorDb = 0;
196 double inputSamplePeakDb = -std::numeric_limits<double>::infinity();
197 double outputSamplePeakDb = -std::numeric_limits<double>::infinity();
198 double outputTruePeakDb = -std::numeric_limits<double>::infinity();
199 double normalizedCeiling = 1;
203 bool targetMet = false;
205 };
207 class Plan final
208 {
209 public:
210 Plan() = default;
211 Plan(Plan &&) noexcept = default;
212 Plan &operator=(Plan &&) noexcept = default;
213 [[nodiscard]] bool isValid() const noexcept
214 {
215 return state_ != nullptr;
216 }
217 [[nodiscard]] OfflineAudioSpec getSpec() const noexcept
218 {
219 return state_ ? state_->spec : OfflineAudioSpec{};
220 }
221 [[nodiscard]] Report getReport() const noexcept
222 {
223 return state_ ? state_->report : Report{};
224 }
225 [[nodiscard]] std::size_t retainedBytes() const noexcept
226 {
227 return state_ ? sizeof(State) + state_->exclusions.retainedBytes() : 0;
228 }
230 [[nodiscard]] std::span<const OfflineRegion> exclusions() const noexcept
231 {
232 return state_ ? state_->exclusions.view() : std::span<const OfflineRegion>{};
233 }
234
235 private:
237 struct State
238 {
239 OfflineAudioSpec spec;
240 OfflineFingerprint fingerprint;
241 OfflineExclusions exclusions;
242 Report report;
243 Curve curve = Curve::Sine;
244 double inputPeak = 0, targetGain = 1, featherFrames = 1;
245 bool active = false;
246 };
247 std::unique_ptr<State> state_;
248 };
250 {
253 std::size_t memoryBytes = 0;
254 [[nodiscard]] bool succeeded() const noexcept
255 {
256 return offlineSucceeded(status);
257 }
258 };
259 struct Result
260 {
263 std::size_t memoryBytes = 0;
264 [[nodiscard]] bool succeeded() const noexcept
265 {
266 return offlineSucceeded(status);
267 }
268 };
269
272 const typename OfflineEnergyAnalyzer<T>::Analysis &analysis,
273 const Options &options = {},
274 const OfflineJobOptions &jobOptions = {}) const
275 {
276 PlanResult result;
277 try
278 {
279 validateOptions(options);
280 if (!analysis.isValid())
282 OfflineSession job(jobOptions);
283 if (source.getSpec() != analysis.getSpec())
285 job.checkpoint(OfflinePhase::Plan, 0, analysis.getSpec().frames);
286 job.charge(sizeof(typename Plan::State));
287 Plan plan;
288 plan.state_ = std::make_unique<typename Plan::State>();
289 auto &state = *plan.state_;
290 state.spec = analysis.getSpec();
291 state.fingerprint = analysis.fingerprint();
292 state.inputPeak = analysis.samplePeak();
293 state.featherFrames = std::max(1., std::round(.005 * state.spec.sampleRate));
294 if constexpr (Soft)
295 state.curve = options.curve;
296 state.exclusions.assign(job, options.exclusions, state.spec.frames);
297 const bool allExcluded = state.exclusions.covers(state.spec.frames);
298 auto &report = state.report;
299 report.requestedReductionDb = options.reductionDb;
300 report.oversamplingFactor = options.oversamplingFactor;
301 report.inputSamplePeakDb =
302 gainToDecibels(state.inputPeak, -std::numeric_limits<double>::infinity());
303 if (state.inputPeak > 0)
304 {
305 state.targetGain = offlineRepresentablePeakGain<T>(
306 state.inputPeak, decibelsToGain(-options.reductionDb));
307 report.effectiveReductionDb = -gainToDecibels(state.targetGain);
308 report.representabilityLimited =
309 (state.targetGain == 1 && options.reductionDb > 0) ||
310 options.reductionDb - report.effectiveReductionDb > 1e-6;
311 }
312 state.active = options.reductionDb > 0 && state.inputPeak > 0 && state.targetGain < 1 &&
313 !allExcluded;
314 report.reason = options.reductionDb == 0 ? Reason::ZeroAmount
315 : state.inputPeak == 0 ? Reason::NoSignal
316 : allExcluded ? Reason::AllExcluded
317 : state.targetGain == 1 ? Reason::RepresentabilityLimited
319 job.checkpoint(OfflinePhase::Plan, state.spec.frames, state.spec.frames);
320 if (source.getSpec() != state.spec)
322 result.status = state.active ? OfflineStatus::Success : OfflineStatus::NoChange;
323 result.memoryBytes = job.bytes();
324 result.plan = std::move(plan);
325 }
326 catch (...)
327 {
328 result.status = offlineExceptionStatus();
329 }
330 return result;
331 }
333 [[nodiscard]] PlanResult analyze(OfflineAudioSource<T> &source, const Options &options = {},
334 const OfflineJobOptions &job = {}) const
335 {
336 PlanResult result;
337 try
338 {
339 validateOptions(options);
340 auto analyzed = OfflineEnergyAnalyzer<T>().analyze(source, job);
341 if (!analyzed.succeeded())
342 {
343 result.status = analyzed.status;
344 return result;
345 }
346 result = makePlan(source, analyzed.analysis, options,
347 offlineRemaining(job, analyzed.analysis.retainedBytes()));
348 result.memoryBytes = std::max(analyzed.memoryBytes,
349 result.memoryBytes + analyzed.analysis.retainedBytes());
350 }
351 catch (...)
352 {
353 result.status = offlineExceptionStatus();
354 }
355 return result;
356 }
358 [[nodiscard]] Result render(OfflineAudioSource<T> &source, const Plan &plan,
359 OfflineAudioSink<T> &sink, const OfflineJobOptions &job = {}) const
360 {
361 if constexpr (!Soft)
362 return renderCurve<ClipperCurve::Hard>(source, plan, sink, job);
363 else
364 {
365 const auto curve = plan.isValid() ? plan.state_->curve : Curve::Sine;
366 switch (curve)
367 {
368 case Curve::Sine:
369 return renderCurve<ClipperCurve::Sine>(source, plan, sink, job);
370 case Curve::Tanh:
371 return renderCurve<ClipperCurve::Tanh>(source, plan, sink, job);
372 case Curve::GoldenRatio:
373 return renderCurve<ClipperCurve::GoldenRatio>(source, plan, sink, job);
374 }
375 Result result;
376 result.status = OfflineStatus::InvalidInput;
377 return result;
378 }
379 }
381 template <int MaxChannels>
382 [[nodiscard]] Result run(const AudioBuffer<T, MaxChannels> &input,
383 AudioBuffer<T, MaxChannels> &output, double sampleRate,
384 const Options &options = {}, const OfflineJobOptions &job = {}) const
385 {
386 return offlineRun(*this, input, output, sampleRate, options, job);
387 }
388
389 private:
390 static void validateOptions(const Options &options)
391 {
392 const int factor = options.oversamplingFactor;
393 if (!std::isfinite(options.reductionDb) || (options.reductionDb < 0) ||
394 (options.reductionDb > 12) ||
395 (factor != 1 && factor != 2 && factor != 4 && factor != 8 && factor != 16))
397 if constexpr (Soft)
398 if (options.curve != Curve::Sine && options.curve != Curve::Tanh &&
399 options.curve != Curve::GoldenRatio)
401 }
402 template <ClipperCurve C> static double initialCeiling(double target)
403 {
404 double low = 0, high = 1;
405 while (clipperShape<C>(1., high) < target && high < 1e12)
406 high *= 2;
407 for (int i = 0; i < 64; ++i)
408 {
409 const double middle = (low + high) / 2;
410 if (clipperShape<C>(1., middle) > target)
411 high = middle;
412 else
413 low = middle;
414 }
415 return (low + high) / 2;
416 }
417
418 template <ClipperCurve C>
419 static Result renderCurve(OfflineAudioSource<T> &source, const Plan &plan,
420 OfflineAudioSink<T> &sink, const OfflineJobOptions &options)
421 {
422 Result result;
423 OfflineSinkTransaction<T> transaction;
424 try
425 {
426 if (!plan.isValid())
428 const auto &state = *plan.state_;
429 const auto spec = state.spec;
430 if (source.getSpec() != spec)
432 result.report = state.report;
433 auto &report = result.report;
434 OfflineSession job(options);
435 job.checkpoint(OfflinePhase::Verify, 0, spec.frames);
436 const int blockLimit = std::min(4096, options.blockFrames);
437 OfflineBlock<T> input(job, spec, blockLimit), output(job, spec, blockLimit);
438 const double normalizer = state.inputPeak > 0 ? state.inputPeak : 1;
439 const double maximum = static_cast<double>(std::numeric_limits<T>::max());
440 // Reuse the existing verified PCM cache. Unity control performs no
441 // gain processing; only its source validation and bounded reads are used.
442 struct UnityControl
443 {
444 double operator()(std::int64_t) const noexcept
445 {
446 return 1;
447 }
448 };
449 using Cache = OfflineGainSource<T, UnityControl>;
450 struct Reader
451 {
452 Cache *cache;
453 int channel;
454 void operator()(std::int64_t first, int count, double *values)
455 {
456 cache->readAudio(channel, first, count, values);
457 }
458 };
459 using Projection = offline_clip::BoundedClip<C, Reader>;
460 std::optional<Cache> cached;
461 std::optional<offline_clip::TailPolicy<C>> policy;
462 std::array<offline_clip::TailCertificate, 2> sourceStats;
463 bool sourceStatsReady = false;
464 if (state.active)
465 {
466 cached.emplace(source, spec, state.fingerprint, state.inputPeak, job, options,
467 UnityControl{});
468 const int order = report.oversamplingFactor <= 2 ? 18 : 10;
469 // All temporary inverseBoxPower vector requests, before construction.
470 job.charge(
471 static_cast<std::size_t>(6 * (order + 1) + (order + 3) * (2 * order + 1)) *
472 sizeof(double));
473 policy.emplace(report.oversamplingFactor);
474 }
475 // Bilateral finite-source projection is evaluated at original indices;
476 // no causal source-frame delay is inserted and later removed.
477 report.compensatedLatencyFrames = 0;
478 result.memoryBytes = job.bytes();
479 auto passOptions = offlineRemaining(options, job.bytes());
480 struct ProgressContext
481 {
482 const OfflineJobOptions *options;
483 std::int64_t frames;
484 } progress{&options, spec.frames};
485 if (options.progress)
486 {
487 passOptions.progressContext = &progress;
488 passOptions.progress = [](void *context, OfflineProgress)
489 {
490 const auto &p = *static_cast<ProgressContext *>(context);
491 // Internal high-rate/padded clocks are not source positions.
492 return p.options->progress(p.options->progressContext,
493 {OfflinePhase::Verify, 0, p.frames});
494 };
495 }
496 // Keep allocation accounting and reusable storage alive across
497 // calibration. Geometry changes return their scratch to the same
498 // pool; only genuinely new upstream payload consumes the budget.
499 OfflineSession passJob(passOptions);
500 OfflineMemoryPool scratch(passJob, false);
501 passJob.useScratchResource(scratch);
502 std::array<std::optional<Projection>, 2> projections;
503 const auto pass = [&](double ceiling, bool publish)
504 {
505 const auto phase = publish ? OfflinePhase::Render : OfflinePhase::Verify;
506 job.checkpoint(OfflinePhase::Verify, 0, spec.frames);
507 if (cached && ceiling > 0)
508 {
509 cached->reset();
510 std::array<offline_clip::TailCertificate, 2> shapeStats;
511 for (std::size_t leaf = 0; leaf < cached->leaves(); ++leaf)
512 {
513 const auto first = static_cast<std::int64_t>(leaf) * cached->block();
514 const int count = static_cast<int>(
515 std::min<std::int64_t>(cached->block(), spec.frames - first));
516 for (int c = 0; c < spec.channels; ++c)
517 {
518 const auto *raw = cached->audio(leaf, c);
519 for (int i = 0; i < count; ++i)
520 {
521 const double x = static_cast<double>(raw[i]) / normalizer;
522 if (!sourceStatsReady)
523 sourceStats[c].append(x);
524 shapeStats[c].append(clipperShape<C>(x, ceiling));
525 }
526 }
527 job.checkpoint(OfflinePhase::Verify, first + count, spec.frames);
528 }
529 sourceStatsReady = true;
530 std::array<offline_clip::TailPlan, 2> tails;
531 std::int64_t padding = 64;
532 for (int c = 0; c < spec.channels; ++c)
533 {
534 tails[c] = policy->choose(ceiling, sourceStats[c], shapeStats[c]);
535 padding = std::max(padding, tails[c].padding);
536 }
537 // Balance duration-sized moments against fixed FFT/cached PCM
538 // scratch. The actual allocations still enforce the job budget.
539 int grid = 128;
540 long double best = std::numeric_limits<long double>::infinity();
541 const long double extent =
542 static_cast<long double>(spec.frames) + 2.L * padding;
543 for (int candidate = 128;
544 candidate <= 16384 && candidate * report.oversamplingFactor <= 65536;
545 candidate *= 2)
546 {
547 const long double cost =
548 (1000.L * report.oversamplingFactor - 32) * candidate +
549 3456.L * extent / candidate;
550 if (cost < best)
551 {
552 best = cost;
553 grid = candidate;
554 }
555 }
556 for (int c = 0; c < spec.channels; ++c)
557 {
558 cached->reset();
559 if (projections[c] && projections[c]->hasGeometry(padding, grid))
560 projections[c]->reset(ceiling, tails[c].exterior);
561 else
562 projections[c].emplace(passJob, Reader{&*cached, c}, spec.frames,
563 padding, report.oversamplingFactor, ceiling,
564 tails[c].exterior, grid);
565 projections[c]->prepareProjection();
566 }
567 }
568 result.memoryBytes = std::max(result.memoryBytes, job.bytes() + passJob.bytes());
569 // Prepare every owned DSP allocation before opening the destination.
570 if (publish)
571 {
572 transaction.sink = &sink;
573 offlineCallSink([&] { return sink.begin(spec); });
574 }
575 job.checkpoint(phase, 0, spec.frames);
576 if (cached)
577 cached->reset();
578 OfflineClipMeasurement measured;
579 OfflineFingerprint fingerprint;
580 TruePeakDetector<double, 2> detector;
581 std::int64_t written = 0;
582 const auto consume = [&](std::int64_t first, int count,
583 const std::array<const double *, 2> &filtered)
584 {
585 if (first != written)
587 for (int offset = 0; offset < count;)
588 {
589 const auto at = first + offset;
590 int length = std::min(input.frames(), count - offset);
591 std::array<const T *, 2> raw{};
592 if (cached)
593 {
594 const auto leaf = static_cast<std::size_t>(at / cached->block());
595 const int local = static_cast<int>(at % cached->block());
596 length = std::min(length, cached->block() - local);
597 for (int c = 0; c < spec.channels; ++c)
598 raw[c] = cached->audio(leaf, c) + local;
599 }
600 else
601 {
602 auto block = input.view(length);
603 offlineRead(source, spec, at, block);
604 for (int c = 0; c < spec.channels; ++c)
605 raw[c] = block.getChannel(c);
606 }
607 auto out = output.view(length);
608 for (int f = 0; f < length; ++f)
609 {
610 const double mask =
611 state.active
612 ? 1 - state.exclusions.apply(at + f, 0, state.featherFrames)
613 : 0;
614 for (int c = 0; c < spec.channels; ++c)
615 {
616 const T rawValue = raw[c][f];
617 if (!std::isfinite(rawValue))
619 if (std::abs(static_cast<double>(rawValue)) > state.inputPeak)
621 offlineHash(fingerprint, rawValue);
622 double normalized = static_cast<double>(rawValue) / normalizer;
623 T value = rawValue;
624 if (mask != 0)
625 {
626 const double shaped =
627 ceiling == 0 ? 0 : filtered[c][offset + f];
628 normalized = std::lerp(normalized, shaped, mask);
629 if (!std::isfinite(normalized))
631 if (std::abs(normalized) > maximum / normalizer)
632 {
633 measured.representable = false;
634 if (publish)
636 }
637 else
638 {
639 value = static_cast<T>(normalized * normalizer);
640 if (!std::isfinite(value))
642 normalized = static_cast<double>(value) / normalizer;
643 }
644 }
645 measured.peak = std::max(measured.peak, std::abs(normalized));
646 if (publish)
647 {
648 out.getChannel(c)[f] = value;
649 measured.truePeak = std::max(
650 measured.truePeak, detector.processSample(normalized, c));
651 }
652 }
653 }
654 if (publish)
656 [&]
657 {
658 std::array<const T *, 2> pointers{};
659 for (int c = 0; c < spec.channels; ++c)
660 pointers[c] = out.getChannel(c);
661 return sink.write(at, {pointers.data(), spec.channels, length});
662 });
663 offset += length;
664 written += length;
665 job.checkpoint(phase, written, spec.frames);
666 }
667 };
668 if (cached && ceiling > 0)
669 {
670 for (auto leaf = projections[0]->firstLeaf(); leaf < projections[0]->endLeaf();
671 ++leaf)
672 {
673 std::array<const double *, 2> filtered{};
674 int count = 0;
675 for (int c = 0; c < spec.channels; ++c)
676 {
677 const auto data = projections[c]->get(leaf);
678 filtered[c] = data.data();
679 if (c && count != static_cast<int>(data.size()))
681 count = static_cast<int>(data.size());
682 }
683 consume(projections[0]->leafOrigin(leaf), count, filtered);
684 }
685 }
686 else
687 while (written < spec.frames)
688 consume(written,
689 static_cast<int>(
690 std::min<std::int64_t>(input.frames(), spec.frames - written)),
691 {});
692 if (written != spec.frames || fingerprint != state.fingerprint ||
693 source.getSpec() != spec)
695 if (publish)
696 for (int c = 0; c < spec.channels; ++c)
697 measured.truePeak = std::max(measured.truePeak, detector.getTailPeak(c));
698 return measured;
699 };
700 OfflineClipCalibration calibrated;
701 if (state.active)
702 {
703 calibrated =
704 offlineCalibrateClip(initialCeiling<C>(state.targetGain), state.targetGain,
705 [&](double ceiling) { return pass(ceiling, false); });
706 report.calibrationPasses = calibrated.attempts;
707 report.normalizedCeiling = calibrated.ceiling;
708 report.targetErrorDb = calibrated.errorDb;
709 if (!calibrated.converged)
710 {
711 const bool constrained =
712 !calibrated.bracketed && !state.exclusions.view().empty();
713 report.reason =
717 }
718 }
719 const auto final = pass(calibrated.ceiling, true);
720 if (state.active && (!final.representable || final.peak != calibrated.peak))
722 constexpr double silence = -std::numeric_limits<double>::infinity();
723 report.outputSamplePeakDb = final.peak > 0 ? gainToDecibels(final.peak, silence) +
724 gainToDecibels(normalizer, silence)
725 : silence;
726 report.outputTruePeakDb = final.truePeak > 0 ? gainToDecibels(final.truePeak, silence) +
727 gainToDecibels(normalizer, silence)
728 : silence;
729 report.achievedReductionDb =
730 state.inputPeak > 0 && final.peak > 0 ? -gainToDecibels(final.peak) : 0;
731 report.targetErrorDb = offlineClipPeakErrorDb(final.peak, state.targetGain);
732 report.targetMet = state.inputPeak > 0 &&
733 offlineClipTargetMet(final.peak, state.targetGain,
734 report.targetErrorDb);
735 if (state.active && !report.targetMet)
737 if (state.active)
738 report.reason = report.representabilityLimited ? Reason::RepresentabilityLimited
740 job.checkpoint(OfflinePhase::Render, spec.frames, spec.frames);
741 if (source.getSpec() != spec)
743 offlineCallSink([&] { return sink.commit(); });
744 transaction.sink = nullptr;
745 result.status = state.active ? OfflineStatus::Success : OfflineStatus::NoChange;
746 }
747 catch (...)
748 {
749 result.status = offlineExceptionStatus();
750 }
751 return result;
752 }
753};
754} // namespace detail
755} // namespace dspark
756#endif // DSPARK_HAS_OFFLINE
Owning audio buffer with contiguous, 32-byte aligned storage.
Transactional worker sink for arbitrarily long offline output.
Rewindable, complete-file source with int64 positions and bounded blocks.
virtual OfflineAudioSpec getSpec() const noexcept=0
Returns format and provenance by value.
Read-only source-bound energy map; owns bins, never source samples.
bool isValid() const noexcept
False for a failed or moved-from result.
OfflineFingerprint fingerprint() const noexcept
OfflineAudioSpec getSpec() const noexcept
Complete-file fixed-duration energy analysis for offline processors.
Result analyze(OfflineAudioSource< T > &source, const OfflineJobOptions &options={}) const
Analyzes a complete mono/stereo source, with no input mutation.
Immutable format/fingerprint/control plan, with owned exclusion regions.
std::size_t retainedBytes() const noexcept
OfflineAudioSpec getSpec() const noexcept
std::span< const OfflineRegion > exclusions() const noexcept
Borrowed regions, valid while the plan remains alive and unmoved.
Result run(const AudioBuffer< T, MaxChannels > &input, AudioBuffer< T, MaxChannels > &output, double sampleRate, const Options &options={}, const OfflineJobOptions &job={}) const
Owning render; aliasing is safe and failure leaves output unchanged.
PlanResult analyze(OfflineAudioSource< T > &source, const Options &options={}, const OfflineJobOptions &job={}) const
Analyzes the complete source and makes a reusable worker plan.
PlanResult makePlan(OfflineAudioSource< T > &source, const typename OfflineEnergyAnalyzer< T >::Analysis &analysis, const Options &options={}, const OfflineJobOptions &jobOptions={}) const
Reuses complete-source energy analysis; no PCM or filtered audio is retained.
Result render(OfflineAudioSource< T > &source, const Plan &plan, OfflineAudioSink< T > &sink, const OfflineJobOptions &job={}) const
Calibrates actual PCM, then verifies a provisional render before committing it.
std::conditional_t< Soft, OfflineSoftClipOptions, OfflineClipOptions > Options
void offlineFail(OfflineStatus status)
Processor::Result offlineRun(const Processor &processor, const AudioBuffer< T, MaxChannels > &input, AudioBuffer< T, MaxChannels > &output, double sampleRate, const Options &options, const OfflineJobOptions &job)
void offlineCallSink(Function &&function)
void offlineRead(OfflineAudioSource< T > &source, const OfflineAudioSpec &expected, std::int64_t first, AudioBufferView< T > block)
double offlineClipPeakErrorDb(double peak, double target) noexcept
Definition OfflineClip.h:62
void offlineHash(OfflineFingerprint &state, T sample) noexcept
bool offlineClipTargetMet(double peak, double target, double errorDb) noexcept
Definition OfflineClip.h:70
OfflineClipCalibration offlineCalibrateClip(double initialCeiling, double target, Render render)
Definition OfflineClip.h:82
OfflineJobOptions offlineRemaining(const OfflineJobOptions &job, std::size_t used)
OfflineStatus offlineExceptionStatus() noexcept
Main namespace for the DSPark framework.
bool offlineSucceeded(OfflineStatus status) noexcept
True for successful processing, including an explicit identity result.
T decibelsToGain(T dB, T minusInfinityDb=T(-100)) noexcept
Converts a value in decibels to linear gain.
Definition DspMath.h:74
OfflineSoftClipCurve
Soft curves shared with Clipper, including its sine approximation.
Definition OfflineClip.h:31
OfflineStatus
Explicit completion or no-publication outcome of an offline job.
@ TargetUnreachable
Exact protected PCM prevents the requested processing target.
T gainToDecibels(T gain, T minusInfinityDb=T(-100)) noexcept
Converts a linear gain value to decibels.
Definition DspMath.h:89
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.
std::span< const OfflineRegion > exclusions
Copied source-frame regions; PCM stays exact.
Definition OfflineClip.h:43
double reductionDb
Positive sample-peak reduction, 0..12 dB; zero is exact identity.
Definition OfflineClip.h:41
int oversamplingFactor
Explicit local factor: 1, 2, 4, 8 or 16.
Definition OfflineClip.h:42
double effectiveReductionDb
After rounding the target toward the input PCM peak.
double normalizedCeiling
Curve ceiling divided by the complete-source peak.
bool targetMet
Set only after final rounded PCM verification.
double targetErrorDb
Final output peak relative to the effective target.
int compensatedLatencyFrames
Internal delay removed from the result.