DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
AudioIntervalAnalyzer.h
1// DSPark -- Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi -- MIT License
3
4#pragma once
5
15#include "LoudnessMeter.h"
16#include "../Core/detail/ScaledSumSquares.h"
17
18#include <algorithm>
19#include <cmath>
20#include <cstdint>
21#include <limits>
22
23namespace dspark {
24
31template <FloatType T>
33{
34public:
36 struct Interval
37 {
38 std::int64_t begin = 0, end = 0;
39 bool operator==(const Interval&) const noexcept = default;
40 };
46
53 {
54 double value = std::numeric_limits<double>::quiet_NaN();
57 [[nodiscard]] bool hasValue() const noexcept
59 };
60
76
89
91
98 [[nodiscard]] bool prepare(const AudioSpec& spec, Interval requested,
100 std::int64_t streamOrigin = 0) noexcept
101 {
102 result_ = {};
103 result_.requested = requested;
104 result_.measured = { requested.begin, requested.begin };
105 result_.context = context;
106 result_.sampleRate = spec.sampleRate;
107 result_.channels = spec.numChannels;
108 energy_.reset();
109 contextSilent_ = true;
110 sealed_ = false;
111 nextFrame_ = origin_ = streamOrigin;
112 maxBlockSize_ = spec.maxBlockSize;
113 if (!spec.isValid() || spec.numChannels > 2 || streamOrigin < 0 ||
114 requested.begin < streamOrigin || requested.end < requested.begin ||
115 (context != Context::Independent && context != Context::IncludeContinuous))
116 {
118 return false;
119 }
120 regionMeter_.prepare(spec);
121 if (context == Context::IncludeContinuous) continuousMeter_.prepare(spec);
122 if (requested.begin == requested.end) result_.status = Status::Complete;
123 return true;
124 }
125
131 [[nodiscard]] bool processBlock(AudioBufferView<const T> block,
132 std::int64_t firstFrame) noexcept
133 {
134 if (result_.status == Status::Complete) return true;
135 if (sealed_ || result_.status != Status::Pending) return false;
136 const int count = block.getNumSamples();
137 if (count < 0 || count > maxBlockSize_ || block.getNumChannels() != result_.channels ||
138 firstFrame < 0 || firstFrame > std::numeric_limits<std::int64_t>::max() - count)
139 return fail(Status::InvalidArgument);
140 if (firstFrame != nextFrame_) return fail(Status::DiscontinuousInput);
141 if (count == 0) return true;
142 for (int ch = 0; ch < result_.channels; ++ch)
143 if (block.getChannel(ch) == nullptr) return fail(Status::InvalidArgument);
144 const auto end = std::min(firstFrame + count, result_.requested.end);
145 const auto begin = std::max(firstFrame, result_.requested.begin);
146 const bool observe = result_.context == Context::IncludeContinuous;
147 const auto usedBegin = observe ? firstFrame : begin;
148 // Validate the relevant portion before mutating a meter on this block.
149 for (auto frame = usedBegin; frame < end; ++frame)
150 for (int ch = 0; ch < result_.channels; ++ch)
151 if (!std::isfinite(block.getChannel(ch)[frame - firstFrame]))
152 return fail(Status::NonFiniteInput);
153 if (observe)
154 {
155 feed(continuousMeter_, block, 0, static_cast<int>(end - firstFrame));
156 for (auto frame = firstFrame; frame < end; ++frame)
157 for (int ch = 0; ch < result_.channels; ++ch)
158 contextSilent_ &= block.getChannel(ch)[frame - firstFrame] == T(0);
159 }
160 if (begin < end)
161 {
162 feed(regionMeter_, block, static_cast<int>(begin - firstFrame),
163 static_cast<int>(end - begin));
164 for (auto frame = begin; frame < end; ++frame)
165 for (int ch = 0; ch < result_.channels; ++ch)
166 energy_.add(static_cast<double>(block.getChannel(ch)[frame - firstFrame]));
167 result_.measured.end = end;
168 }
169 nextFrame_ = firstFrame + count;
170 if (end == result_.requested.end) result_.status = Status::Complete;
171 return true;
172 }
173
179 [[nodiscard]] Result finish() noexcept
180 {
181 if (sealed_) return result_;
182 sealed_ = true;
183 if (result_.status == Status::Pending) result_.status = Status::Truncated;
184 if (!result_.complete())
185 {
186 const auto validity = result_.status == Status::Truncated
188 result_.rms.validity = result_.samplePeak.validity = result_.truePeakDbtp.validity = validity;
189 markLoudness(result_.region, validity);
190 if (result_.context == Context::IncludeContinuous) markLoudness(result_.continuous, validity);
191 return result_;
192 }
193 const auto frames = result_.measured.end - result_.measured.begin;
194 const auto validity = frames == 0 ? Validity::Empty :
195 energy_.peak() == 0 ? Validity::Silence : Validity::Valid;
196 result_.rms = { frames ? energy_.rms(static_cast<double>(frames) * result_.channels) :
197 std::numeric_limits<double>::quiet_NaN(), validity, result_.measured };
198 result_.samplePeak = { frames ? energy_.peak() :
199 std::numeric_limits<double>::quiet_NaN(), validity, result_.measured };
200 regionMeter_.finalizeTruePeak();
201 result_.truePeakDbtp = { frames ? static_cast<double>(regionMeter_.getTruePeakDb()) :
202 std::numeric_limits<double>::quiet_NaN(), validity, result_.measured };
203 if (!regionMeter_.isMeasurementValid())
204 result_.truePeakDbtp = { std::numeric_limits<double>::quiet_NaN(),
206 result_.region = loudness(regionMeter_, result_.measured, energy_.peak() == 0);
207 if (result_.context == Context::IncludeContinuous)
208 result_.continuous = loudness(continuousMeter_,
209 { origin_, frames ? result_.measured.end : origin_ }, contextSilent_);
210 return result_;
211 }
212
213private:
214 bool fail(Status status) noexcept { result_.status = status; return false; }
215 void feed(LoudnessMeter<T>& meter, AudioBufferView<const T> block, int offset, int count) noexcept
216 {
217 if (result_.channels == 1) meter.process(block.getChannel(0) + offset, count);
218 else meter.process(block.getChannel(0) + offset, block.getChannel(1) + offset, count);
219 }
220 static void markLoudness(Loudness& reading, Validity validity) noexcept
221 {
222 reading.momentary.validity = reading.shortTerm.validity =
223 reading.integrated.validity = reading.range.validity = validity;
224 }
225 static Loudness loudness(const LoudnessMeter<T>& meter, Interval input, bool silent) noexcept
226 {
227 Loudness out;
228 out.input = input;
229 const auto info = meter.getMeasurementInfo();
230 out.committedEnd = input.begin + info.committedFrames;
231 out.pendingFrames = info.pendingFrames;
232 out.hopFrames = info.hopFrames;
233 out.integratedBlocks = info.integratedBlocks;
234 out.rangeBlocks = info.rangeBlocks;
235 if (input.begin == input.end) { markLoudness(out, Validity::Empty); return out; }
236 if (!meter.isMeasurementValid()) { markLoudness(out, Validity::NumericalError); return out; }
237 const auto metric = [&](double value, int hops, bool gated, std::uint64_t blocks,
238 bool cumulative) -> Measurement
239 {
240 const auto length = static_cast<std::int64_t>(hops) * info.hopFrames;
241 if (info.committedFrames < length)
242 return { std::numeric_limits<double>::quiet_NaN(), Validity::TooShort, input };
243 const Interval support { cumulative ? input.begin : out.committedEnd - length,
244 out.committedEnd };
245 if (gated && blocks == 0 && !silent)
246 return { std::numeric_limits<double>::quiet_NaN(), Validity::BelowGate, support };
247 return { value, silent ? Validity::Silence : Validity::Valid, support };
248 };
249 out.momentary = metric(meter.getMomentaryLUFS(), 4, false, 0, false);
250 out.shortTerm = metric(meter.getShortTermLUFS(), 30, false, 0, false);
251 out.integrated = metric(meter.getIntegratedLUFS(), 4, true, info.integratedBlocks, true);
252 out.range = metric(meter.getLoudnessRange(), 30, true, info.rangeBlocks, true);
253 return out;
254 }
255
256 LoudnessMeter<T> regionMeter_, continuousMeter_;
257 detail::ScaledSumSquares energy_;
258 Result result_;
259 std::int64_t origin_ = 0, nextFrame_ = 0;
260 int maxBlockSize_ = 0;
261 bool contextSilent_ = true, sealed_ = false;
262};
263} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
One requested mono/stereo interval per instance; feed several instances the same blocks to measure ov...
bool prepare(const AudioSpec &spec, Interval requested, Context context=Context::Independent, std::int64_t streamOrigin=0) noexcept
Starts a fresh measurement, resetting all filters, windows and gates. streamOrigin is the first frame...
Result finish() noexcept
Seals the result, including the finite region's true-peak tail. Call at EOF or after completing the i...
bool processBlock(AudioBufferView< const T > block, std::int64_t firstFrame) noexcept
Consumes a contiguous block at firstFrame; clips both region edges. No samples at or after requested....
double rms(double count) const noexcept
void add(double value) noexcept
Main namespace for the DSPark framework.
Absolute, nonnegative frame coordinates [begin,end), per channel.
bool operator==(const Interval &) const noexcept=default
Readouts at the last complete 100 ms hop within input. committedEnd is a source-frame position,...
A numeric value with its validity and actual window/gate support. Invalid/unavailable values are NaN....
Immutable-by-convention result, safe to copy to another thread.
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:37
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:71
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:58
int maxBlockSize
Maximum number of samples per processing block.
Definition AudioSpec.h:53
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:45