DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
OfflinePunch.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
29#include "../Analysis/OfflineTransientAnalyzer.h"
30#if DSPARK_HAS_OFFLINE
31#include "../Core/detail/OfflineGain.h"
32#include "../Core/detail/OfflinePunchEnvelope.h"
33#include <algorithm>
34#include <cmath>
35#include <cstddef>
36#include <cstdint>
37#include <limits>
38#include <span>
39#include <utility>
40
41namespace dspark
42{
44template <FloatType T> class OfflinePunch final
45{
46 public:
47 struct Options
48 {
49 double boostDb = 0;
50 std::span<const OfflineRegion> exclusions{};
51 };
52 enum class Reason : std::uint8_t
53 {
54 Boosted,
60 };
61 struct Report
62 {
65 double inputSamplePeakDb = -std::numeric_limits<double>::infinity();
66 double outputSamplePeakDb = -std::numeric_limits<double>::infinity();
67 double outputTruePeakDb = -std::numeric_limits<double>::infinity();
68 std::size_t transients = 0;
69 bool peaksMeasured = false;
71 };
73 class Plan final
74 {
75 public:
76 Plan() = default;
77 Plan(Plan &&) noexcept = default;
78 Plan &operator=(Plan &&) noexcept = default;
79 [[nodiscard]] bool isValid() const noexcept
80 {
81 return curve_.isValid();
82 }
83 [[nodiscard]] OfflineAudioSpec getSpec() const noexcept
84 {
85 return spec_;
86 }
87 [[nodiscard]] Report getReport() const noexcept
88 {
89 return report_;
90 }
91 [[nodiscard]] std::size_t retainedBytes() const noexcept
92 {
93 return isValid() ? curve_.retainedBytes() + exclusions_.retainedBytes() : 0;
94 }
96 [[nodiscard]] std::span<const OfflineRegion> exclusions() const noexcept
97 {
98 return isValid() ? exclusions_.view() : std::span<const OfflineRegion>();
99 }
100 [[nodiscard]] double gainAt(std::int64_t frame) const noexcept
101 {
102 if (!isValid() || frame < 0 || frame >= spec_.frames)
103 return 1;
104 return gain(frame, curve_.weightAt(frame));
105 }
106
107 private:
108 friend class OfflinePunch;
109 [[nodiscard]] double gain(std::int64_t frame, double weight) const noexcept
110 {
111 return exclusions_.apply(frame, decibelsToGain(report_.effectiveBoostDb * weight),
112 featherFrames_);
113 }
114 OfflineAudioSpec spec_;
115 OfflineFingerprint fingerprint_;
116 Report report_;
117 double inputPeak_ = 0, featherFrames_ = 1;
119 detail::OfflineExclusions exclusions_;
120 };
122 {
125 std::size_t memoryBytes = 0;
126 [[nodiscard]] bool succeeded() const noexcept
127 {
128 return offlineSucceeded(status);
129 }
130 };
131 struct Result
132 {
135 std::size_t memoryBytes = 0;
136 [[nodiscard]] bool succeeded() const noexcept
137 {
138 return offlineSucceeded(status);
139 }
140 };
142 [[nodiscard]] PlanResult makePlan(
143 OfflineAudioSource<T> &source,
144 const typename OfflineTransientAnalyzer<T>::Analysis &analysis, const Options &options = {},
145 const OfflineJobOptions &jobOptions = {}) const
146 {
147 PlanResult result;
148 try
149 {
150 validateOptions(options);
151 if (!analysis.isValid() || (options.boostDb > 0 && !analysis.getOptions().attacks))
153 detail::OfflineSession job(jobOptions);
154 Plan plan;
155 plan.spec_ = analysis.energy().getSpec();
156 if (source.getSpec() != plan.spec_)
158 plan.fingerprint_ = analysis.energy().fingerprint();
159 plan.inputPeak_ = analysis.energy().samplePeak();
160 plan.featherFrames_ = std::max(1.0, std::round(.005 * plan.spec_.sampleRate));
161 auto &report = plan.report_;
162 report.requestedBoostDb = options.boostDb;
163 report.inputSamplePeakDb =
164 gainToDecibels(plan.inputPeak_, -std::numeric_limits<double>::infinity());
165 job.checkpoint(OfflinePhase::Plan, 0, plan.spec_.frames);
166 plan.exclusions_.assign(job, options.exclusions, plan.spec_.frames);
167 const bool allExcluded = plan.exclusions_.covers(plan.spec_.frames);
168 const auto attacks = analysis.attacks();
169 const bool active =
170 options.boostDb > 0 && plan.inputPeak_ > 0 && !attacks.empty() && !allExcluded;
171 plan.curve_.prepare(job, active ? attacks.size() : 0, plan.spec_.sampleRate);
172 if (!active)
173 report.reason = allExcluded ? Reason::AllExcluded
174 : options.boostDb == 0 ? Reason::ZeroAmount
175 : plan.inputPeak_ == 0 ? Reason::NoSignal
177 else
178 {
179 report.transients = attacks.size();
180 report.effectiveBoostDb = options.boostDb;
181 report.reason = Reason::Boosted;
182 for (std::size_t i = 0; i < attacks.size(); ++i)
183 {
184 if ((i & 1023) == 0)
185 job.checkpoint(OfflinePhase::Plan, 0, plan.spec_.frames);
186 plan.curve_.set(i, attacks[i].begin, attacks[i].end - attacks[i].begin);
187 }
188 detail::OfflineBlock<T> scratch(job, plan.spec_, jobOptions.blockFrames);
189 detail::OfflinePunchEnvelope::Cursor weights(plan.curve_, job);
190 constexpr double maximum = static_cast<double>(std::numeric_limits<T>::max());
192 source, plan.spec_, plan.fingerprint_, plan.inputPeak_, scratch, job,
193 [&](std::int64_t frame, double magnitude) {
194 const double weight = weights(frame);
195 const double gain = plan.gain(frame, weight);
196 if (magnitude > maximum / gain)
197 {
198 const double mask =
199 plan.exclusions_.apply(frame, 2, plan.featherFrames_) - 1;
200 const double cap = 20 / std::log(10.) *
201 std::log1p((maximum / magnitude - 1) / mask) /
202 weight;
203 // Reserve roundoff before evaluating exp and the feather again.
204 report.effectiveBoostDb =
205 std::max(0.0, cap - 64 * std::numeric_limits<double>::epsilon() *
206 std::max(1.0, cap));
207 report.reason = Reason::RepresentabilityLimited;
208 }
209 });
210 }
211 job.checkpoint(OfflinePhase::Plan, plan.spec_.frames, plan.spec_.frames);
212 result.status =
213 report.effectiveBoostDb > 0 ? OfflineStatus::Success : OfflineStatus::NoChange;
214 result.memoryBytes = job.bytes();
215 result.plan = std::move(plan);
216 }
217 catch (...)
218 {
219 result.status = detail::offlineExceptionStatus();
220 }
221 return result;
222 }
224 [[nodiscard]] PlanResult analyze(OfflineAudioSource<T> &source, const Options &options = {},
225 const OfflineJobOptions &job = {}) const
226 {
227 PlanResult result;
228 try
229 {
230 validateOptions(options);
231 typename OfflineTransientAnalyzer<T>::Options features;
232 features.attacks = options.boostDb > 0;
233 features.pulses = false;
234 auto analyzed = OfflineTransientAnalyzer<T>().analyze(source, features, job);
235 if (!analyzed.succeeded())
236 {
237 result.status = analyzed.status;
238 return result;
239 }
240 result = makePlan(source, analyzed.analysis, options,
241 detail::offlineRemaining(job, analyzed.analysis.retainedBytes()));
242 result.memoryBytes = std::max(analyzed.memoryBytes,
243 result.memoryBytes + analyzed.analysis.retainedBytes());
244 }
245 catch (...)
246 {
247 result.status = detail::offlineExceptionStatus();
248 }
249 return result;
250 }
252 [[nodiscard]] Result render(OfflineAudioSource<T> &source, const Plan &plan,
253 OfflineAudioSink<T> &sink, const OfflineJobOptions &job = {}) const
254 {
255 auto result = detail::offlineRenderGain<Result>(
256 source, plan.spec_, plan.fingerprint_, plan.inputPeak_, plan.isValid(),
257 plan.report_.effectiveBoostDb > 0, plan.report_, sink, job,
258 [&](detail::OfflineSession &session) {
259 return
260 [&plan, weights = detail::OfflinePunchEnvelope::Cursor(plan.curve_, session)](
261 std::int64_t frame) mutable { return plan.gain(frame, weights(frame)); };
262 }, {&plan.exclusions_, plan.featherFrames_, false});
263 if (result.succeeded() && result.report.renderInfo.representabilityLimited)
264 {
265 result.report.reason = Reason::RepresentabilityLimited;
266 result.report.effectiveBoostDb = gainToDecibels(
267 1 + result.report.renderInfo.deltaScale *
268 (decibelsToGain(result.report.effectiveBoostDb) - 1));
269 }
270 return result;
271 }
273 template <int MaxChannels>
274 [[nodiscard]] Result run(const AudioBuffer<T, MaxChannels> &input,
275 AudioBuffer<T, MaxChannels> &output, double sampleRate,
276 const Options &options = {}, const OfflineJobOptions &job = {}) const
277 {
278 return detail::offlineRun(*this, input, output, sampleRate, options, job);
279 }
280
281 private:
282 static void validateOptions(const Options &options)
283 {
284 if (!std::isfinite(options.boostDb) || (options.boostDb < 0) || (options.boostDb > 12))
285 detail::offlineFail(OfflineStatus::InvalidInput);
286 }
287};
288} // namespace dspark
289#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.
Immutable sparse weight plan bound to a complete source fingerprint.
std::size_t retainedBytes() const noexcept
std::span< const OfflineRegion > exclusions() const noexcept
Borrowed regions, valid while the plan remains alive and unmoved.
Report getReport() const noexcept
bool isValid() const noexcept
double gainAt(std::int64_t frame) const noexcept
Plan(Plan &&) noexcept=default
OfflineAudioSpec getSpec() const noexcept
Offline transient emphasis with a positive boost-dB control.
Result run(const AudioBuffer< T, MaxChannels > &input, AudioBuffer< T, MaxChannels > &output, double sampleRate, const Options &options={}, const OfflineJobOptions &job={}) const
Owning convenience render; aliases are safe and failures preserve output.
PlanResult makePlan(OfflineAudioSource< T > &source, const typename OfflineTransientAnalyzer< T >::Analysis &analysis, const Options &options={}, const OfflineJobOptions &jobOptions={}) const
Reuses attack events, then verifies PCM and bounds representable boost.
Result render(OfflineAudioSource< T > &source, const Plan &plan, OfflineAudioSink< T > &sink, const OfflineJobOptions &job={}) const
Verifies the source, then measures provisional PCM before committing the sink.
PlanResult analyze(OfflineAudioSource< T > &source, const Options &options={}, const OfflineJobOptions &job={}) const
Complete-source analysis and planning; retains only sparse automation.
Read-only analysis with source provenance; no owned audio samples. Threading: energy() is an owner-th...
const OfflineEnergyAnalyzer< T >::Analysis & energy() const noexcept
Borrows the energy map as an owner-thread reference view. Valid while this analysis remains alive and...
std::span< const Event > attacks() const noexcept
std::size_t retainedBytes() const noexcept
std::span< const OfflineRegion > view() const noexcept
double apply(std::int64_t frame, double gain, double featherFrames) const noexcept
double weightAt(std::int64_t frame) const noexcept
std::size_t retainedBytes() const noexcept
void offlineFail(OfflineStatus status)
void offlineScanMagnitude(OfflineAudioSource< T > &source, const OfflineAudioSpec &spec, const OfflineFingerprint &expectedFingerprint, double inputPeak, OfflineBlock< T > &scratch, const OfflineSession &job, Consume consume)
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
OfflineStatus
Explicit completion or no-publication outcome of an offline job.
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.
Measured-render control adjustments; no clipping or master gain trim.
Resource and cooperative-cancellation controls for one worker operation.
std::span< const OfflineRegion > exclusions
Exact PCM inside these source regions.
double boostDb
Equal transient boost, 0..12 dB; zero is exact identity.
bool succeeded() const noexcept
OfflineGainRenderInfo renderInfo
bool succeeded() const noexcept