DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
OfflineStereo.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 "../../Effects/StereoGenerator.h"
13#include "OfflineWorker.h"
14#include <algorithm>
15#include <array>
16#include <cmath>
17#include <cstdint>
18#include <optional>
19#include <span>
20
21namespace dspark::detail
22{
23// Supplies original PCM and the canonical unit-width delta at the same source
24// frame. No whole-file PCM is retained. A mono source is duplicated explicitly
25// by the caller's validated plan; the original ring itself keeps one channel.
26template <FloatType T> class OfflineStereoStream final
27{
28 OfflineAudioSource<T> &source_;
29 OfflineAudioSpec spec_;
30 OfflineFingerprint expected_;
31 double inputPeak_;
32 const OfflineSession &job_;
33 OfflineBlock<T> input_, aligned_;
34 std::optional<StereoGenerator<double>> generator_;
35 std::unique_ptr<T[]> original_;
36 int latency_ = 0, ringSize_ = 0;
37
38 public:
40 OfflineFingerprint fingerprint, double inputPeak,
41 StereoGenerator<double>::Options options, bool generate,
42 OfflineSession &job, int blockFrames)
43 : source_(source), spec_(spec), expected_(fingerprint), inputPeak_(inputPeak), job_(job),
44 input_(job, spec, std::min(256, blockFrames)),
45 aligned_(job, spec, std::min(256, blockFrames))
46 {
47 if (generate)
48 {
49 const AudioSpec environment{spec.sampleRate, 256, 2};
50 const auto memory =
52 if (memory == 0)
54 job.charge(memory);
55 generator_.emplace();
56 generator_->setWidth(1);
57 if (!generator_->prepare(environment, options))
59 latency_ = generator_->getLatency();
60 }
61 ringSize_ = latency_ + input_.frames();
62 original_ = job.template allocate<T>(static_cast<std::uint64_t>(ringSize_) * spec.channels);
63 }
64 [[nodiscard]] int latency() const noexcept
65 {
66 return latency_;
67 }
68
69 template <class Consume> void run(Consume consume, OfflinePhase phase = OfflinePhase::Render)
70 {
71 job_.checkpoint(phase, 0, spec_.frames);
72 if (generator_)
73 generator_->resetAtFrame(static_cast<std::uint64_t>(spec_.timelineOrigin));
74 OfflineFingerprint fingerprint;
75 int write = 0, read = 0, skip = latency_;
76 std::int64_t written = 0;
77 const auto block = [&](std::int64_t first, int count, bool flush) {
78 auto input = input_.view(count);
79 if (!flush)
80 offlineRead(source_, spec_, first, input);
81 std::array<std::array<double, 256>, 2> work{};
82 std::array<double, 256> delta{};
83 for (int i = 0; i < count; ++i)
84 {
85 for (int c = 0; c < spec_.channels; ++c)
86 {
87 const T sample = flush ? T(0) : input.getChannel(c)[i];
88 if (!std::isfinite(sample))
90 if (!flush)
91 {
92 offlineHash(fingerprint, sample);
93 if (std::abs(static_cast<double>(sample)) > inputPeak_)
95 }
96 original_[static_cast<std::size_t>(c) * ringSize_ + write] = sample;
97 work[c][i] = static_cast<double>(sample);
98 }
99 if (spec_.channels == 1)
100 work[1][i] = work[0][i];
101 write = (write + 1) % ringSize_;
102 }
103 if (generator_)
104 {
105 double *channels[]{work[0].data(), work[1].data()};
106 if (!generator_->processBlock({channels, 2, count},
107 {delta.data(), static_cast<std::size_t>(count)}))
109 }
110 const int start = std::min(skip, count);
111 skip -= start;
112 const int produced = count - start;
113 auto original = aligned_.view(produced);
114 for (int i = 0; i < produced; ++i)
115 {
116 for (int c = 0; c < spec_.channels; ++c)
117 original.getChannel(c)[i] =
118 original_[static_cast<std::size_t>(c) * ringSize_ + read];
119 read = (read + 1) % ringSize_;
120 }
121 if (produced)
122 consume(written, original,
123 std::span<const double>(delta.data() + start,
124 static_cast<std::size_t>(produced)));
125 written += produced;
126 };
127 for (std::int64_t first = 0; first < spec_.frames;)
128 {
129 const int count =
130 static_cast<int>(std::min<std::int64_t>(input_.frames(), spec_.frames - first));
131 block(first, count, false);
132 first += count;
133 job_.checkpoint(phase, first, spec_.frames);
134 }
135 // Do not form frames + latency in the signed source clock.
136 for (int remaining = latency_; remaining > 0;)
137 {
138 const int count = std::min(input_.frames(), remaining);
139 block(spec_.frames, count, true);
140 remaining -= count;
141 job_.checkpoint(phase, spec_.frames, spec_.frames);
142 }
143 if (written != spec_.frames || fingerprint != expected_ || source_.getSpec() != spec_)
145 }
146};
147} // namespace dspark::detail
148#endif // DSPARK_HAS_OFFLINE
Rewindable, complete-file source with int64 positions and bounded blocks.
static std::size_t getPrepareMemoryBound(const AudioSpec &spec, Options options) noexcept
Conservative cumulative setup allocation bound, or zero for invalid settings. Includes state,...
AudioBufferView< T > view(int count) noexcept
void charge(std::size_t bytes)
void checkpoint(OfflinePhase phase, std::int64_t completed, std::int64_t total) const
void run(Consume consume, OfflinePhase phase=OfflinePhase::Render)
OfflineStereoStream(OfflineAudioSource< T > &source, OfflineAudioSpec spec, OfflineFingerprint fingerprint, double inputPeak, StereoGenerator< double >::Options options, bool generate, OfflineSession &job, int blockFrames)
void offlineFail(OfflineStatus status)
void offlineRead(OfflineAudioSource< T > &source, const OfflineAudioSpec &expected, std::int64_t first, AudioBufferView< T > block)
void offlineHash(OfflineFingerprint &state, T sample) noexcept
OfflinePhase
Current kind of work; completed/total restart for each pass, and a phase may recur.
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:37
Immutable source format and host-provided content/timeline identity.
std::int64_t timelineOrigin
Metadata; read offsets are relative to frame zero.
Noncryptographic PCM fingerprint, stable across block divisions.
Configuration applied by prepare(), never by the audio callback.