DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
OfflineEnergyAnalyzer.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
22#include "../Core/OfflineProcessing.h"
23#include "../Core/detail/ScaledSumSquares.h"
24
25#if DSPARK_HAS_OFFLINE
26
27#include <algorithm>
28#include <cmath>
29#include <cstddef>
30#include <cstdint>
31#include <memory>
32#include <numeric>
33#include <span>
34#include <type_traits>
35#include <utility>
36
37namespace dspark
38{
39
42template <FloatType T> class OfflineEnergyAnalyzer final
43{
44 public:
47 struct Bin
48 {
49 double rms = 0;
50 double peak = 0;
51 std::int64_t frames = 0;
52 };
53
56 class Analysis final
57 {
58 public:
59 Analysis() = default;
60 Analysis(Analysis &&) noexcept = default;
61 Analysis &operator=(Analysis &&) noexcept = default;
63 [[nodiscard]] bool isValid() const noexcept
64 {
65 return bins_ != nullptr;
66 }
69 [[nodiscard]] std::span<const Bin> bins() const noexcept
70 {
71 return isValid() ? std::span<const Bin>(bins_.get(), count_) : std::span<const Bin>();
72 }
73 [[nodiscard]] OfflineAudioSpec getSpec() const noexcept
74 {
75 return spec_;
76 }
77 [[nodiscard]] OfflineFingerprint fingerprint() const noexcept
78 {
79 return fingerprint_;
80 }
81 [[nodiscard]] std::int64_t binFrames() const noexcept
82 {
83 return hop_;
84 }
85 [[nodiscard]] double samplePeak() const noexcept
86 {
87 return peak_;
88 }
89 [[nodiscard]] std::size_t retainedBytes() const noexcept
90 {
91 return isValid() ? count_ * sizeof(Bin) : 0;
92 }
93
94 private:
96 OfflineAudioSpec spec_;
97 OfflineFingerprint fingerprint_;
98 std::int64_t hop_ = 0;
99 std::size_t count_ = 0;
100 double peak_ = 0;
101 std::unique_ptr<Bin[]> bins_;
102 };
103
106 struct Result
107 {
110 std::size_t memoryBytes = 0;
111 [[nodiscard]] bool succeeded() const noexcept
112 {
113 return offlineSucceeded(status);
114 }
115 };
116
119 {
120 double midRms = 0, sideRms = 0;
121 double midPeak = 0, sidePeak = 0;
122 std::int64_t frames = 0;
123 };
124
133 class MidSideAnalysis final
134 {
135 public:
136 MidSideAnalysis() = default;
137 MidSideAnalysis(MidSideAnalysis &&) noexcept = default;
138 MidSideAnalysis &operator=(MidSideAnalysis &&) noexcept = default;
139 [[nodiscard]] bool isValid() const noexcept
140 {
141 return bins_ != nullptr;
142 }
144 [[nodiscard]] std::span<const MidSideBin> bins() const noexcept
145 {
146 return isValid() ? std::span<const MidSideBin>(bins_.get(), count_)
147 : std::span<const MidSideBin>();
148 }
149 [[nodiscard]] OfflineAudioSpec getSpec() const noexcept
150 {
151 return spec_;
152 }
153 [[nodiscard]] OfflineFingerprint fingerprint() const noexcept
154 {
155 return fingerprint_;
156 }
159 [[nodiscard]] OfflineFingerprint sideFingerprint() const noexcept
160 {
161 return sideFingerprint_;
162 }
163 [[nodiscard]] std::int64_t binFrames() const noexcept
164 {
165 return hop_;
166 }
167 [[nodiscard]] double samplePeak() const noexcept
168 {
169 return peak_;
170 }
171 [[nodiscard]] double midRms() const noexcept
172 {
173 return midRms_;
174 }
175 [[nodiscard]] double sideRms() const noexcept
176 {
177 return sideRms_;
178 }
179 [[nodiscard]] double sidePeak() const noexcept
180 {
181 return sidePeak_;
182 }
183 [[nodiscard]] std::size_t retainedBytes() const noexcept
184 {
185 return isValid() ? count_ * sizeof(MidSideBin) : 0;
186 }
187
188 private:
190 OfflineAudioSpec spec_;
191 OfflineFingerprint fingerprint_, sideFingerprint_;
192 std::int64_t hop_ = 0;
193 std::size_t count_ = 0;
194 double peak_ = 0, midRms_ = 0, sideRms_ = 0, sidePeak_ = 0;
195 std::unique_ptr<MidSideBin[]> bins_;
196 };
197
201 {
204 std::size_t memoryBytes = 0;
205 [[nodiscard]] bool succeeded() const noexcept
206 {
207 return offlineSucceeded(status);
208 }
209 };
210
212 [[nodiscard]] Result analyze(OfflineAudioSource<T> &source,
213 const OfflineJobOptions &options = {}) const
214 {
215 return measure<false>(source, options);
216 }
217
226 const OfflineJobOptions &options = {}) const
227 {
228 return measure<true>(source, options);
229 }
230
231 private:
232 template <bool Stereo>
233 [[nodiscard]] static auto measure(OfflineAudioSource<T> &source,
234 const OfflineJobOptions &options)
235 {
236 using Output = std::conditional_t<Stereo, MidSideResult, Result>;
237 using Map = std::conditional_t<Stereo, MidSideAnalysis, Analysis>;
238 using Cell = std::conditional_t<Stereo, MidSideBin, Bin>;
239 Output result;
240 try
241 {
242 detail::OfflineSession job(options);
243 Map candidate;
244 candidate.spec_ = source.getSpec();
245 detail::offlineValidateSpec(candidate.spec_, 2);
246 const auto spec = candidate.spec_;
247 if constexpr (Stereo)
248 if (spec.channels != 2)
250 job.checkpoint(OfflinePhase::Analyze, 0, spec.frames);
251 candidate.hop_ = std::max<std::int64_t>(
252 1, static_cast<std::int64_t>(spec.sampleRate / (Stereo ? 100 : 10)));
253 const auto count = static_cast<std::uint64_t>((spec.frames - 1) / candidate.hop_ + 1);
254 candidate.bins_ = job.allocate<Cell>(count);
255 candidate.count_ = static_cast<std::size_t>(count);
256 detail::OfflineBlock<T> scratch(job, spec, options.blockFrames);
257 // Scaled sum-of-squares keeps finite extreme double PCM measurable.
258 // A causal RMS follower or unscaled SIMD square sum cannot supply this
259 // exact-bin, overflow/underflow-safe statistic.
260 detail::ScaledSumSquares energy, sideEnergy, totalMid, totalSide;
261 std::int64_t inBin = 0;
262 std::size_t bin = 0;
263 for (std::int64_t first = 0; first < spec.frames;)
264 {
265 const int frames =
266 static_cast<int>(std::min<std::int64_t>(scratch.frames(), spec.frames - first));
267 auto block = scratch.view(frames);
268 detail::offlineRead(source, spec, first, block);
269 for (int f = 0; f < frames; ++f)
270 {
271 for (int c = 0; c < spec.channels; ++c)
272 {
273 const T value = block.getChannel(c)[f];
274 if (!std::isfinite(value))
276 detail::offlineHash(candidate.fingerprint_, value);
277 if constexpr (!Stereo)
278 energy.add(static_cast<double>(value));
279 else
280 candidate.peak_ =
281 std::max(candidate.peak_, std::abs(static_cast<double>(value)));
282 }
283 if constexpr (Stereo)
284 {
285 const double left = static_cast<double>(block.getChannel(0)[f]);
286 const double right = static_cast<double>(block.getChannel(1)[f]);
287 const double mid = std::midpoint(left, right);
288 const double side = std::midpoint(left, -right);
289 energy.add(mid);
290 sideEnergy.add(side);
291 totalMid.add(mid);
292 totalSide.add(side);
293 detail::offlineHash(candidate.sideFingerprint_, side);
294 }
295 ++inBin;
296 if (inBin == candidate.hop_ || first + f == spec.frames - 1)
297 {
298 if constexpr (Stereo)
299 {
300 candidate.bins_[bin++] = {energy.rms(static_cast<double>(inBin)),
301 sideEnergy.rms(static_cast<double>(inBin)),
302 energy.peak(), sideEnergy.peak(), inBin};
303 sideEnergy.reset();
304 }
305 else
306 {
307 const double rms =
308 energy.rms(static_cast<double>(inBin) * spec.channels);
309 candidate.bins_[bin++] = {rms, energy.peak(), inBin};
310 candidate.peak_ = std::max(candidate.peak_, energy.peak());
311 }
312 inBin = 0;
313 energy.reset();
314 }
315 }
316 first += frames;
317 job.checkpoint(OfflinePhase::Analyze, first, spec.frames);
318 }
319 if (source.getSpec() != spec)
321 if constexpr (Stereo)
322 {
323 candidate.midRms_ = totalMid.rms(static_cast<double>(spec.frames));
324 candidate.sideRms_ = totalSide.rms(static_cast<double>(spec.frames));
325 candidate.sidePeak_ = totalSide.peak();
326 }
327 result.memoryBytes = job.bytes();
328 result.analysis = std::move(candidate);
329 result.status = OfflineStatus::Success;
330 }
331 catch (...)
332 {
333 result.status = detail::offlineExceptionStatus();
334 }
335 return result;
336 }
337};
338
339} // namespace dspark
340
341#endif // DSPARK_HAS_OFFLINE
Rewindable, complete-file source with int64 positions and bounded blocks.
Read-only source-bound energy map; owns bins, never source samples.
bool isValid() const noexcept
False for a failed or moved-from result.
std::span< const Bin > bins() const noexcept
Returned spans remain valid only while this analysis is alive and unmoved.
OfflineFingerprint fingerprint() const noexcept
OfflineAudioSpec getSpec() const noexcept
Analysis(Analysis &&) noexcept=default
Immutable 10 ms stereo energy map, bound to the supplied PCM/clock.
MidSideAnalysis(MidSideAnalysis &&) noexcept=default
OfflineFingerprint sideFingerprint() const noexcept
Fingerprint of double-precision side samples, for shared gain rendering.
OfflineFingerprint fingerprint() const noexcept
std::span< const MidSideBin > bins() const noexcept
Borrowed bins; valid while this analysis is alive and unmoved.
Complete-file fixed-duration energy analysis for offline processors.
MidSideResult analyzeMidSide(OfflineAudioSource< T > &source, const OfflineJobOptions &options={}) const
Analyzes stereo PCM into separate central/lateral 10 ms energy bins.
Result analyze(OfflineAudioSource< T > &source, const OfflineJobOptions &options={}) const
Analyzes a complete mono/stereo source, with no input mutation.
void offlineFail(OfflineStatus status)
void offlineRead(OfflineAudioSource< T > &source, const OfflineAudioSpec &expected, std::int64_t first, AudioBufferView< T > block)
void offlineValidateSpec(const OfflineAudioSpec &spec, int maximumChannels=16)
void offlineHash(OfflineFingerprint &state, T sample) noexcept
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.
Immutable source format and host-provided content/timeline identity.
Linked-channel RMS and maximum absolute sample for an actual interval.
Energy of M=(L+R)/2 and S=(L-R)/2 over the same actual interval.
Separate stereo result; failed calls never publish a partial map.
Only a successful result owns an analysis; memoryBytes is requested payload.
Noncryptographic PCM fingerprint, stable across block divisions.
Resource and cooperative-cancellation controls for one worker operation.