DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
OfflineWorker.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 <algorithm>
13#include <cmath>
14#include <cstring>
15#include <limits>
16#include <memory>
17#include <span>
18#include <utility>
19
20namespace dspark::detail
21{
22// Round a peak target toward the input so PCM quantization cannot demand more
23// global reduction than requested. Division before comparison remains meaningful
24// when the input/target themselves are subnormal, including denorm_min.
25template <FloatType T>
26[[nodiscard]] inline double offlineRepresentablePeakGain(double inputPeak, double gain) noexcept
27{
28 T target = static_cast<T>(inputPeak * gain);
29 if (target == T(0))
30 target = std::numeric_limits<T>::denorm_min();
31 else if (static_cast<double>(target) / inputPeak < gain)
32 target = std::nextafter(target, std::numeric_limits<T>::infinity());
33 return std::min(1.0, static_cast<double>(target) / inputPeak);
34}
35
41
42// Full-source validation and summary without a retained feature map. Magnitude
43// is channel-linked; consumers choose which additional statistics they need.
44template <FloatType T, class Consume>
46 const OfflineAudioSpec &spec,
47 OfflineBlock<T> &scratch, const OfflineSession &job,
48 OfflinePhase phase, Consume consume)
49{
50 job.checkpoint(phase, 0, spec.frames);
52 for (std::int64_t first = 0; first < spec.frames;)
53 {
54 const int frames =
55 static_cast<int>(std::min<std::int64_t>(scratch.frames(), spec.frames - first));
56 auto block = scratch.view(frames);
57 detail::offlineRead(source, spec, first, block);
58 for (int f = 0; f < frames; ++f)
59 {
60 double magnitude = 0;
61 for (int c = 0; c < spec.channels; ++c)
62 {
63 const T x = block.getChannel(c)[f];
64 if (!std::isfinite(x))
67 magnitude = std::max(magnitude, std::abs(static_cast<double>(x)));
68 }
69 summary.peak = std::max(summary.peak, magnitude);
70 consume(first + f, magnitude);
71 }
72 first += frames;
73 job.checkpoint(phase, first, spec.frames);
74 }
75 if (source.getSpec() != spec)
77 return summary;
78}
79
80// Verify an existing full-source analysis while visiting sample magnitudes.
81template <FloatType T, class Consume>
83 const OfflineFingerprint &expectedFingerprint, double inputPeak,
84 OfflineBlock<T> &scratch, const OfflineSession &job,
85 Consume consume)
86{
87 const auto summary = offlineScanSource(
88 source, spec, scratch, job, OfflinePhase::Plan, [&](std::int64_t frame, double magnitude) {
89 if (magnitude > inputPeak)
91 consume(frame, magnitude);
92 });
93 if (summary.fingerprint != expectedFingerprint)
95}
96
98{
99 public:
100 void assign(OfflineSession &job, std::span<const OfflineRegion> regions, std::int64_t frames)
101 {
102 capacity_ = regions.size();
103 if (capacity_)
104 {
105 regions_ = job.allocate<OfflineRegion>(capacity_);
106 std::copy(regions.begin(), regions.end(), regions_.get());
107 for (const auto r : regions)
108 if ((r.begin < 0) || (r.end <= r.begin) || (r.end > frames))
110 std::sort(regions_.get(), regions_.get() + capacity_,
111 [](OfflineRegion a, OfflineRegion b) { return a.begin < b.begin; });
112 for (std::size_t i = 0; i < capacity_; ++i)
113 {
114 const auto r = regions_[i];
115 if (count_ && r.begin <= regions_[count_ - 1].end)
116 regions_[count_ - 1].end = std::max(r.end, regions_[count_ - 1].end);
117 else
118 regions_[count_++] = r;
119 }
120 }
121 }
122 [[nodiscard]] std::span<const OfflineRegion> view() const noexcept
123 {
124 return {regions_.get(), count_};
125 }
126 [[nodiscard]] std::size_t retainedBytes() const noexcept
127 {
128 return capacity_ * sizeof(OfflineRegion);
129 }
130 [[nodiscard]] bool covers(std::int64_t frames) const noexcept
131 {
132 return count_ == 1 && regions_[0].begin == 0 && regions_[0].end == frames;
133 }
134 [[nodiscard]] double apply(std::int64_t frame, double gain, double featherFrames) const noexcept
135 {
136 if (count_ == 0 || gain == 1)
137 return gain;
138 std::size_t lo = 0, hi = count_;
139 while (lo < hi)
140 {
141 const auto mid = lo + (hi - lo) / 2;
142 if (regions_[mid].end <= frame)
143 lo = mid + 1;
144 else
145 hi = mid;
146 }
147 double right = 1, left = 1;
148 if (lo < count_)
149 {
150 if (frame >= regions_[lo].begin)
151 return 1;
152 right = std::clamp(static_cast<double>(regions_[lo].begin - frame) / featherFrames, 0.0,
153 1.0);
154 }
155 if (lo > 0)
156 left = std::clamp(static_cast<double>(frame - regions_[lo - 1].end) / featherFrames,
157 0.0, 1.0);
158 if (left == 1 && right == 1)
159 return gain;
160 // A nearest-boundary minimum has a cusp where two feathers meet.
161 // Multiplying masks removes that corner. Three zero edge derivatives
162 // suppress modulation sidebands without touching protected PCM.
163 const auto smooth = [](double t) {
164 const double u = std::min(t, 1 - t);
165 const double value = u * u * u * u * (35 - u * (84 - u * (70 - 20 * u)));
166 return t <= .5 ? value : 1 - value;
167 };
168 return 1 + (gain - 1) * smooth(left) * smooth(right);
169 }
170
171 private:
172 std::size_t count_ = 0, capacity_ = 0;
173 std::unique_ptr<OfflineRegion[]> regions_;
174};
175
176template <FloatType T> struct OfflineSinkTransaction
177{
180 {
181 if (sink)
182 sink->abort();
183 }
184};
185
186template <class Function> inline void offlineCallSink(Function &&function)
187{
188 bool ok = false;
189 try
190 {
191 ok = function();
192 }
193 catch (const std::bad_alloc &)
194 {
196 }
197 catch (...)
198 {
200 }
201 if (!ok)
203}
204
205template <class Processor, FloatType T, int MaxChannels, class Options>
206[[nodiscard]] typename Processor::Result offlineRun(const Processor &processor,
207 const AudioBuffer<T, MaxChannels> &input,
209 double sampleRate, const Options &options,
210 const OfflineJobOptions &job)
211{
212 typename Processor::Result result;
213 try
214 {
215 OfflineBufferSource<T, MaxChannels> source(input.toView(), sampleRate);
216 auto prepared = processor.analyze(source, options, job);
217 if (!prepared.succeeded())
218 {
219 result.status = prepared.status;
220 return result;
221 }
222 auto outputSpec = source.getSpec();
223 if constexpr (requires { prepared.plan.getOutputSpec(); })
224 outputSpec = prepared.plan.getOutputSpec();
225 detail::offlineValidateSpec(outputSpec, MaxChannels);
226 if (outputSpec.frames > std::numeric_limits<int>::max())
228 const auto sampleBytes =
229 detail::offlineBytes(static_cast<std::uint64_t>(outputSpec.frames), sizeof(T));
230 if (sampleBytes > std::numeric_limits<std::size_t>::max() - 31)
232 const auto outputBytes = detail::offlineBytes(
233 (sampleBytes + 31) / 32, 32 * static_cast<std::size_t>(outputSpec.channels));
234 auto renderJob = detail::offlineRemaining(job, prepared.plan.retainedBytes());
235 renderJob = detail::offlineRemaining(renderJob, outputBytes);
236 class BufferSink final : public OfflineAudioSink<T>
237 {
238 public:
239 explicit BufferSink(AudioBuffer<T, MaxChannels> &destination)
240 : destination_(destination)
241 {
242 }
243 bool begin(const OfflineAudioSpec &spec) override
244 {
245 candidate_.resize(spec.channels, static_cast<int>(spec.frames));
246 return true;
247 }
248 bool write(std::int64_t first, AudioBufferView<const T> block) override
249 {
250 for (int c = 0; c < block.getNumChannels(); ++c)
251 std::memcpy(candidate_.getChannel(c) + first, block.getChannel(c),
252 static_cast<std::size_t>(block.getNumSamples()) * sizeof(T));
253 return true;
254 }
255 bool commit() override
256 {
257 destination_ = std::move(candidate_);
258 return true;
259 }
260 void abort() noexcept override
261 {
262 candidate_ = AudioBuffer<T, MaxChannels>();
263 }
264
265 private:
266 AudioBuffer<T, MaxChannels> &destination_;
268 } sink(output);
269 result = processor.render(source, prepared.plan, sink, renderJob);
270 result.memoryBytes = std::max(
271 prepared.memoryBytes, result.memoryBytes + prepared.plan.retainedBytes() + outputBytes);
272 }
273 catch (...)
274 {
275 result.status = detail::offlineExceptionStatus();
276 }
277 return result;
278}
279} // namespace dspark::detail
280#endif // DSPARK_HAS_OFFLINE
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
int getNumSamples() const noexcept
Returns the number of samples per channel.
int getNumChannels() const noexcept
Returns the number of channels in this view.
T * getChannel(int ch) const noexcept
Returns a pointer to the sample data for the given channel.
Owning audio buffer with contiguous, 32-byte aligned storage.
AudioBufferView< T, MaxChannels > toView() noexcept
Returns a non-owning mutable view of this buffer. The view's channel capacity is propagated from MaxC...
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.
Borrowed planar-buffer adapter; does not allocate, copy or own input.
OfflineAudioSpec getSpec() const noexcept override
Returns format and provenance by value.
AudioBufferView< T > view(int count) noexcept
void assign(OfflineSession &job, std::span< const OfflineRegion > regions, std::int64_t frames)
std::size_t retainedBytes() const noexcept
bool covers(std::int64_t frames) const noexcept
std::span< const OfflineRegion > view() const noexcept
double apply(std::int64_t frame, double gain, double featherFrames) const noexcept
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)
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 offlineScanMagnitude(OfflineAudioSource< T > &source, const OfflineAudioSpec &spec, const OfflineFingerprint &expectedFingerprint, double inputPeak, OfflineBlock< T > &scratch, const OfflineSession &job, Consume consume)
void offlineRead(OfflineAudioSource< T > &source, const OfflineAudioSpec &expected, std::int64_t first, AudioBufferView< T > block)
double offlineRepresentablePeakGain(double inputPeak, double gain) noexcept
void offlineValidateSpec(const OfflineAudioSpec &spec, int maximumChannels=16)
void offlineHash(OfflineFingerprint &state, T sample) noexcept
OfflineSourceSummary offlineScanSource(OfflineAudioSource< T > &source, const OfflineAudioSpec &spec, OfflineBlock< T > &scratch, const OfflineSession &job, OfflinePhase phase, Consume consume)
std::size_t offlineBytes(std::uint64_t count, std::size_t size)
OfflineJobOptions offlineRemaining(const OfflineJobOptions &job, std::size_t used)
OfflineStatus offlineExceptionStatus() noexcept
OfflinePhase
Current kind of work; completed/total restart for each pass, and a phase may recur.
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.
Source-frame interval [begin,end), relative to the start of the source.