DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
OfflineProcessing.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
25#if !defined(DSPARK_NO_OFFLINE) && (defined(__cpp_exceptions) || defined(_CPPUNWIND))
26#define DSPARK_HAS_OFFLINE 1
27#else
28#define DSPARK_HAS_OFFLINE 0
29#endif
30
31#if DSPARK_HAS_OFFLINE
32
33#include "AudioBuffer.h"
34#include "DspMath.h"
35
36#include <algorithm>
37#include <array>
38#include <atomic>
39#include <bit>
40#include <cmath>
41#include <cstddef>
42#include <cstdint>
43#include <cstring>
44#include <limits>
45#include <memory>
46#include <memory_resource>
47#include <new>
48#include <type_traits>
49
50namespace dspark
51{
52
72
74[[nodiscard]] inline bool offlineSucceeded(OfflineStatus status) noexcept
75{
76 return status == OfflineStatus::Success || status == OfflineStatus::NoChange;
77}
78
81{
82 double sampleRate = 48000;
83 int channels = 0;
84 std::int64_t frames = 0;
85 std::uint64_t sourceId = 0;
86 std::uint64_t revision = 0;
87 std::int64_t timelineOrigin = 0;
88
90 [[nodiscard]] bool isValid() const noexcept
91 {
92 return std::isfinite(sampleRate) && sampleRate >= 1 &&
93 sampleRate <= static_cast<double>(std::numeric_limits<int>::max()) && channels > 0 &&
94 channels <= 16 && frames >= 0 && timelineOrigin >= 0 &&
95 frames <= std::numeric_limits<std::int64_t>::max() - timelineOrigin;
96 }
97 bool operator==(const OfflineAudioSpec &) const noexcept = default;
98};
99
102{
103 std::int64_t begin = 0;
104 std::int64_t end = 0;
105 bool operator==(const OfflineRegion &) const noexcept = default;
106};
107
109enum class OfflinePhase : std::uint8_t
110{
111 Analyze,
112 Plan,
113 Verify,
114 Render
115};
116
119{
121 std::int64_t completed = 0;
122 std::int64_t total = 0;
123};
124
135{
136 int blockFrames = 4096;
137 std::size_t memoryBudgetBytes = 256u * 1024u * 1024u;
138 const std::atomic<bool> *cancel = nullptr;
139 void *progressContext = nullptr;
140 bool (*progress)(void *, OfflineProgress) = nullptr;
141};
142
145{
146 bool bandlimited = false;
147 bool targetFeasible = true;
149 bool boundaryCalibrated = false;
150 double deltaScale = 1;
151 double boundaryGain = 1;
152 double leftBoundaryGain = 1;
153 double rightBoundaryGain = 1;
156};
157
166template <FloatType T> class OfflineAudioSource
167{
168 public:
169 virtual ~OfflineAudioSource() = default;
171 [[nodiscard]] virtual OfflineAudioSpec getSpec() const noexcept = 0;
173 virtual bool read(std::int64_t first, AudioBufferView<T> destination) = 0;
174};
175
183template <FloatType T, int MaxChannels = 16>
185{
186 public:
189 std::uint64_t sourceId = 0, std::uint64_t revision = 0,
190 std::int64_t timelineOrigin = 0) noexcept
191 : audio_(audio), spec_{sampleRate, audio.getNumChannels(), audio.getNumSamples(), sourceId,
192 revision, timelineOrigin}
193 {
194 }
195
196 [[nodiscard]] OfflineAudioSpec getSpec() const noexcept override
197 {
198 return spec_;
199 }
200 bool read(std::int64_t first, AudioBufferView<T> destination) override
201 {
202 const int count = destination.getNumSamples();
203 if (first < 0 || count < 0 || count > spec_.frames || first > spec_.frames - count ||
204 destination.getNumChannels() != spec_.channels)
205 return false;
206 for (int ch = 0; ch < spec_.channels; ++ch)
207 {
208 if (count == 0)
209 continue;
210 if (!audio_.getChannel(ch) || !destination.getChannel(ch))
211 return false;
212 std::memmove(destination.getChannel(ch), audio_.getChannel(ch) + first,
213 static_cast<std::size_t>(count) * sizeof(T));
214 }
215 return true;
216 }
217
218 private:
220 OfflineAudioSpec spec_;
221};
222
231template <FloatType T> class OfflineAudioSink
232{
233 public:
234 virtual ~OfflineAudioSink() = default;
235 virtual bool begin(const OfflineAudioSpec &spec) = 0;
236 virtual bool write(std::int64_t first, AudioBufferView<const T> block) = 0;
237 virtual bool commit() = 0;
238 virtual void abort() noexcept = 0;
239};
240
248{
249 std::uint64_t first = 14695981039346656037ull;
250 std::uint64_t second = 0x9e3779b97f4a7c15ull;
251 bool operator==(const OfflineFingerprint &) const noexcept = default;
252};
253
254namespace detail
255{
256
261
262[[noreturn]] inline void offlineFail(OfflineStatus status)
263{
264 throw OfflineFailure{status};
265}
266
267// Called only inside a catch block. Transport and callback failures are translated
268// at their boundary; an otherwise unexpected internal error is numerical failure.
269[[nodiscard]] inline OfflineStatus offlineExceptionStatus() noexcept
270{
271 try
272 {
273 throw;
274 }
275 catch (const OfflineFailure &failure)
276 {
277 return failure.status;
278 }
279 catch (const std::bad_alloc &)
280 {
281 return OfflineStatus::AllocationFailure;
282 }
283 catch (...)
284 {
285 return OfflineStatus::NumericalFailure;
286 }
287}
288
289[[nodiscard]] inline std::size_t offlineBytes(std::uint64_t count, std::size_t size)
290{
291 if (count > std::numeric_limits<std::size_t>::max() / size)
292 offlineFail(OfflineStatus::MemoryLimit);
293 return static_cast<std::size_t>(count) * size;
294}
295
296template <typename T> struct OfflineScratchDeleter
297{
298 std::pmr::memory_resource *resource = nullptr;
299 std::size_t count = 0;
300 void operator()(T *pointer) const noexcept
301 {
302 if (resource)
303 {
304 std::destroy_n(pointer, count);
305 resource->deallocate(pointer, count * sizeof(T), alignof(T));
306 }
307 else
308 delete[] pointer;
309 }
310};
311template <typename T>
312using OfflineScratchArray = std::unique_ptr<T[], OfflineScratchDeleter<T>>;
313
314class OfflineSession final
315{
316 public:
317 explicit OfflineSession(const OfflineJobOptions &options) : options_(options)
318 {
319 if (options.blockFrames < 1)
320 offlineFail(OfflineStatus::InvalidInput);
321 }
322 void charge(std::size_t bytes)
323 {
324 if (bytes > options_.memoryBudgetBytes - bytes_)
325 offlineFail(OfflineStatus::MemoryLimit);
326 bytes_ += bytes;
327 }
328 template <typename U> [[nodiscard]] std::unique_ptr<U[]> allocate(std::uint64_t count)
329 {
330 charge(offlineBytes(count, sizeof(U)));
331 return std::make_unique<U[]>(static_cast<std::size_t>(count));
332 }
333 // A resource accounts for every upstream request itself and must outlive
334 // its scratch arrays. Without one, allocation preserves the original path.
335 void useScratchResource(std::pmr::memory_resource &resource) noexcept
336 {
337 scratch_ = &resource;
338 }
339 template <typename U> [[nodiscard]] OfflineScratchArray<U> allocateScratch(std::uint64_t count)
340 {
341 if (!scratch_)
342 return {allocate<U>(count).release(), {}};
343 const auto bytes = offlineBytes(count, sizeof(U));
344 auto *data = static_cast<U *>(scratch_->allocate(bytes, alignof(U)));
345 try
346 {
347 std::uninitialized_value_construct_n(data, static_cast<std::size_t>(count));
348 }
349 catch (...)
350 {
351 scratch_->deallocate(data, bytes, alignof(U));
352 throw;
353 }
354 return {data, {scratch_, static_cast<std::size_t>(count)}};
355 }
356 [[nodiscard]] std::size_t bytes() const noexcept
357 {
358 return bytes_;
359 }
360 void checkpoint(OfflinePhase phase, std::int64_t completed, std::int64_t total) const
361 {
362 if (options_.cancel && options_.cancel->load(std::memory_order_relaxed))
363 offlineFail(OfflineStatus::Cancelled);
364 if (options_.progress)
365 {
366 bool proceed = false;
367 try
368 {
369 proceed = options_.progress(options_.progressContext, {phase, completed, total});
370 }
371 catch (...)
372 {
373 offlineFail(OfflineStatus::CallbackFailed);
374 }
375 if (!proceed)
376 offlineFail(OfflineStatus::Cancelled);
377 }
378 }
379
380 private:
381 const OfflineJobOptions &options_;
382 std::size_t bytes_ = 0;
383 std::pmr::memory_resource *scratch_ = nullptr;
384};
385
386// Reuses released scratch blocks without refunding the cumulative job budget.
387// Both backing storage and pool metadata are charged before each new request.
388// Power-of-two capacity preserves the tempo worker's growth policy; exact
389// capacity avoids rounding large projection maps when only geometry changes.
390class OfflineMemoryPool final : public std::pmr::memory_resource
391{
392 public:
393 explicit OfflineMemoryPool(OfflineSession &job, bool roundCapacity = true)
394 : job_(job), roundCapacity_(roundCapacity) {}
398 {
399 while (blocks_)
400 {
401 auto *block = blocks_;
402 blocks_ = block->next;
403 if (block->alignment > alignof(std::max_align_t))
404 ::operator delete(block, std::align_val_t{block->alignment});
405 else
406 ::operator delete(block);
407 }
408 }
409 private:
410 struct alignas(std::max_align_t) Block
411 {
412 Block *next;
413 void *data;
414 std::size_t capacity, alignment;
415 bool available;
416 };
417 void *do_allocate(std::size_t bytes, std::size_t alignment) override
418 {
419 Block *best = nullptr;
420 for (auto *b = blocks_; b; b = b->next)
421 if (b->available && b->capacity >= bytes && b->alignment >= alignment &&
422 (!best || b->capacity < best->capacity))
423 best = b;
424 if (best)
425 {
426 best->available = false;
427 return best->data;
428 }
429 std::size_t capacity = roundCapacity_ ? 1 : std::max(std::size_t(1), bytes);
430 while (capacity < bytes)
431 {
432 if (capacity > std::numeric_limits<std::size_t>::max() / 2)
433 offlineFail(OfflineStatus::MemoryLimit);
434 capacity *= 2;
435 }
436 alignment = std::max(alignment, alignof(Block));
437 if (alignment > std::numeric_limits<std::size_t>::max() - sizeof(Block))
438 offlineFail(OfflineStatus::MemoryLimit);
439 const auto offset = (sizeof(Block) + alignment - 1) / alignment * alignment;
440 if (capacity > std::numeric_limits<std::size_t>::max() - offset)
441 offlineFail(OfflineStatus::MemoryLimit);
442 const auto total = offset + capacity;
443 job_.charge(total);
444 void *raw = alignment > alignof(std::max_align_t)
445 ? ::operator new(total, std::align_val_t{alignment}) : ::operator new(total);
446 auto *block = new (raw) Block{blocks_, static_cast<std::byte *>(raw) + offset,
447 capacity, alignment, false};
448 blocks_ = block;
449 return block->data;
450 }
451 void do_deallocate(void *p, std::size_t, std::size_t) override
452 {
453 for (auto *b = blocks_; b; b = b->next)
454 if (b->data == p)
455 {
456 b->available = true;
457 return;
458 }
459 }
460 bool do_is_equal(const std::pmr::memory_resource &other) const noexcept override
461 {
462 return this == &other;
463 }
464 OfflineSession &job_;
465 bool roundCapacity_;
466 Block *blocks_ = nullptr;
467};
468
470 std::size_t used)
471{
472 if (used > job.memoryBudgetBytes)
473 offlineFail(OfflineStatus::MemoryLimit);
474 auto out = job;
475 out.memoryBudgetBytes -= used;
476 return out;
477}
478
479inline void offlineValidateSpec(const OfflineAudioSpec &spec, int maximumChannels = 16)
480{
481 if (!spec.isValid())
482 offlineFail(OfflineStatus::InvalidInput);
483 if (spec.channels > maximumChannels)
484 offlineFail(OfflineStatus::UnsupportedLayout);
485 if (spec.frames == 0)
486 offlineFail(OfflineStatus::EmptyInput);
487}
488
489template <FloatType T> class OfflineBlock final
490{
491 public:
492 OfflineBlock(OfflineSession &session, const OfflineAudioSpec &spec, int blockFrames)
493 : frames_(static_cast<int>(std::min<std::int64_t>(spec.frames, blockFrames))),
494 channels_(spec.channels),
495 data_(session.allocate<T>(static_cast<std::uint64_t>(frames_) * channels_))
496 {
497 }
498 [[nodiscard]] AudioBufferView<T> view(int count) noexcept
499 {
500 std::array<T *, 16> pointers{};
501 for (int c = 0; c < channels_; ++c)
502 pointers[static_cast<std::size_t>(c)] =
503 data_.get() + static_cast<std::size_t>(c) * frames_;
504 return {pointers.data(), channels_, count};
505 }
506 [[nodiscard]] int frames() const noexcept
507 {
508 return frames_;
509 }
510
511 private:
512 int frames_, channels_;
513 std::unique_ptr<T[]> data_;
514};
515
516template <FloatType T>
517inline void offlineRead(OfflineAudioSource<T> &source, const OfflineAudioSpec &expected,
518 std::int64_t first, AudioBufferView<T> block)
519{
520 if (source.getSpec() != expected)
521 offlineFail(OfflineStatus::SourceMismatch);
522 bool ok = false;
523 try
524 {
525 ok = source.read(first, block);
526 }
527 catch (...)
528 {
529 offlineFail(OfflineStatus::SourceReadFailed);
530 }
531 if (!ok)
532 offlineFail(OfflineStatus::SourceReadFailed);
533 if (source.getSpec() != expected)
534 offlineFail(OfflineStatus::SourceMismatch);
535}
536
537template <FloatType T> inline void offlineHash(OfflineFingerprint &state, T sample) noexcept
538{
539 static_assert(std::is_same_v<T, float> || std::is_same_v<T, double>,
540 "Offline PCM supports float and double");
541 const std::uint64_t word = [&] {
542 if constexpr (sizeof(T) == 4)
543 return static_cast<std::uint64_t>(std::bit_cast<std::uint32_t>(sample));
544 else
545 return std::bit_cast<std::uint64_t>(sample);
546 }();
547 state.first = (state.first ^ word) * 1099511628211ull;
548 state.second =
549 std::rotl(state.second ^ word, 27) * 0x3c79ac492ba7b653ull + 0x1c69b3f74ac4ae35ull;
550}
551
552} // namespace detail
553} // namespace dspark
554
555#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.
Transactional worker sink for arbitrarily long offline output.
virtual bool begin(const OfflineAudioSpec &spec)=0
virtual void abort() noexcept=0
virtual bool commit()=0
virtual ~OfflineAudioSink()=default
virtual bool write(std::int64_t first, AudioBufferView< const T > block)=0
Rewindable, complete-file source with int64 positions and bounded blocks.
virtual OfflineAudioSpec getSpec() const noexcept=0
Returns format and provenance by value.
virtual bool read(std::int64_t first, AudioBufferView< T > destination)=0
Copies a bounded source-relative range into the supplied block.
virtual ~OfflineAudioSource()=default
Borrowed planar-buffer adapter; does not allocate, copy or own input.
OfflineBufferSource(AudioBufferView< const T, MaxChannels > audio, double sampleRate, std::uint64_t sourceId=0, std::uint64_t revision=0, std::int64_t timelineOrigin=0) noexcept
Binds an existing view and optional host content/revision keys.
OfflineAudioSpec getSpec() const noexcept override
Returns format and provenance by value.
bool read(std::int64_t first, AudioBufferView< T > destination) override
Copies a bounded source-relative range into the supplied block.
AudioBufferView< T > view(int count) noexcept
OfflineBlock(OfflineSession &session, const OfflineAudioSpec &spec, int blockFrames)
OfflineMemoryPool(const OfflineMemoryPool &)=delete
OfflineMemoryPool & operator=(const OfflineMemoryPool &)=delete
OfflineMemoryPool(OfflineSession &job, bool roundCapacity=true)
OfflineSession(const OfflineJobOptions &options)
void useScratchResource(std::pmr::memory_resource &resource) noexcept
void charge(std::size_t bytes)
std::unique_ptr< U[]> allocate(std::uint64_t count)
std::size_t bytes() const noexcept
OfflineScratchArray< U > allocateScratch(std::uint64_t count)
void checkpoint(OfflinePhase phase, std::int64_t completed, std::int64_t total) const
Constrains a type to IEEE floating-point (float or double).
Definition DspMath.h:30
void offlineFail(OfflineStatus status)
void offlineRead(OfflineAudioSource< T > &source, const OfflineAudioSpec &expected, std::int64_t first, AudioBufferView< T > block)
std::unique_ptr< T[], OfflineScratchDeleter< T > > OfflineScratchArray
void offlineValidateSpec(const OfflineAudioSpec &spec, int maximumChannels=16)
void offlineHash(OfflineFingerprint &state, T sample) noexcept
std::size_t offlineBytes(std::uint64_t count, std::size_t size)
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.
OfflineStatus
Explicit completion or no-publication outcome of an offline job.
@ TargetUnreachable
Exact protected PCM prevents the requested processing target.
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.
bool operator==(const OfflineAudioSpec &) const noexcept=default
bool isValid() const noexcept
Checks transport bounds, not the contents or identity of the audio.
std::int64_t timelineOrigin
Metadata; read offsets are relative to frame zero.
Noncryptographic PCM fingerprint, stable across block divisions.
bool operator==(const OfflineFingerprint &) const noexcept=default
Measured-render control adjustments; no clipping or master gain trim.
double rightBoundaryGain
Independent constant continuation after the final frame.
double boundaryGain
Affine reference, before deltaScale; mean of endpoint controls.
double maximumControlGain
Includes any representability cap on the processing delta.
bool targetFeasible
False if no admissible processing delta meets the peak target.
double deltaScale
Applied to the processing delta, leaving protected PCM unchanged.
double leftBoundaryGain
Independent constant continuation before frame zero.
double minimumControlGain
Calibrated control range, not the output/input sample ratio.
bool boundaryCalibrated
Exterior controls adjusted for boundary-event peaks.
bool bandlimited
Product evaluated before optional exact-PCM constraints.
bool representabilityLimited
Extra gain reduced to avoid floating-point overflow.
Resource and cooperative-cancellation controls for one worker operation.
bool(* progress)(void *, OfflineProgress)
Returning false cancels.
const std::atomic< bool > * cancel
Worker progress in source frames, or control points in the Plan pass.
Source-frame interval [begin,end), relative to the start of the source.
bool operator==(const OfflineRegion &) const noexcept=default
void operator()(T *pointer) const noexcept