Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
spectrum_trace.hpp
Go to the documentation of this file.
1#pragma once
2
21
22#include <algorithm>
23#include <array>
24#include <cmath>
25#include <cstddef>
26#include <cstdint>
27#include <limits>
28#include <span>
29#include <type_traits>
30
31namespace pulp::signal {
32
33enum class SpectrumBandScale : std::uint8_t { linear, logarithmic };
34enum class SpectrumBandAggregation : std::uint8_t { mean_power, maximum };
35enum class SpectrumFrequencyWeighting : std::uint8_t { none, a, c };
36
53
54template <typename SampleType, std::size_t MaxBands> struct SpectrumTraceFrameT {
55 static_assert(std::is_floating_point_v<SampleType>);
56
57 std::array<SampleType, MaxBands> magnitude_db{};
58 std::array<SampleType, MaxBands> peak_db{};
59 std::array<SampleType, MaxBands> center_hz{};
60 std::size_t band_count = 0;
61 std::uint64_t sequence = 0;
62};
63
64template <typename SampleType = float, std::size_t MaxInputBins = 4097, std::size_t MaxBands = 256>
66 static_assert(std::is_floating_point_v<SampleType>);
67 static_assert(MaxInputBins >= 2);
68 static_assert(MaxInputBins <= std::numeric_limits<std::uint32_t>::max() / 2u + 1u);
69 static_assert(MaxBands > 0);
70
71 public:
73
74 SpectrumTraceT() noexcept {
76 config.fft_size =
77 static_cast<std::uint32_t>(std::min<std::size_t>(1024, 2 * (MaxInputBins - 1)));
78 config.band_count = std::min<std::size_t>(128, MaxBands);
79 (void)configure(config);
80 }
81
84 [[nodiscard]] bool configure(const SpectrumTraceConfig& config) noexcept {
85 std::array<std::size_t, MaxBands> first_bin{};
86 std::array<std::size_t, MaxBands> end_bin{};
87 std::array<SampleType, MaxBands> center_hz{};
88 std::array<SampleType, MaxInputBins> weighting_db{};
89
90 const auto scale_valid = config.band_scale == SpectrumBandScale::linear ||
92 const auto aggregation_valid = config.aggregation == SpectrumBandAggregation::mean_power ||
94 const auto weighting_valid = config.weighting == SpectrumFrequencyWeighting::none ||
97 if (!std::isfinite(config.sample_rate) || config.sample_rate < 1.0 ||
98 config.sample_rate > 384000.0 || config.fft_size < 2 || (config.fft_size & 1u) != 0 ||
99 config.fft_size / 2u + 1u > MaxInputBins || config.band_count == 0 ||
100 config.band_count > MaxBands || !std::isfinite(config.minimum_hz) ||
101 !std::isfinite(config.maximum_hz) || config.minimum_hz < 0.0 ||
102 config.maximum_hz <= config.minimum_hz ||
103 config.maximum_hz > config.sample_rate * 0.5 ||
104 (config.band_scale == SpectrumBandScale::logarithmic && config.minimum_hz <= 0.0) ||
105 !std::isfinite(config.floor_db) || !std::isfinite(config.ceiling_db) ||
106 config.ceiling_db <= config.floor_db || !std::isfinite(config.attack) ||
107 !std::isfinite(config.release) || config.attack < 0.0 || config.attack > 1.0 ||
108 config.release < 0.0 || config.release > 1.0 ||
109 !std::isfinite(config.peak_decay_db_per_frame) ||
110 config.peak_decay_db_per_frame < 0.0 || !scale_valid || !aggregation_valid ||
111 !weighting_valid || !representable_(config.floor_db) ||
112 !representable_(config.ceiling_db) || !representable_(config.peak_decay_db_per_frame)) {
113 return false;
114 }
115
116 const auto input_bins = static_cast<std::size_t>(config.fft_size / 2u + 1u);
117 const double bin_hz = config.sample_rate / static_cast<double>(config.fft_size);
118 for (std::size_t band = 0; band < config.band_count; ++band) {
119 const double t0 = static_cast<double>(band) / static_cast<double>(config.band_count);
120 const double t1 =
121 static_cast<double>(band + 1) / static_cast<double>(config.band_count);
122 const double low = band_edge_(config, t0);
123 const double high = band_edge_(config, t1);
124 const auto first = static_cast<std::size_t>(std::ceil(low / bin_hz));
125 const auto end = band + 1 == config.band_count
126 ? static_cast<std::size_t>(std::floor(high / bin_hz)) + 1
127 : static_cast<std::size_t>(std::ceil(high / bin_hz));
128 first_bin[band] = std::min(first, input_bins);
129 end_bin[band] = std::min(end, input_bins);
130 const double center = config.band_scale == SpectrumBandScale::logarithmic
131 ? std::sqrt(low * high)
132 : (low + high) * 0.5;
133 center_hz[band] = static_cast<SampleType>(center);
134 }
135 for (std::size_t bin = 0; bin < input_bins; ++bin) {
136 weighting_db[bin] = static_cast<SampleType>(
137 calculate_weighting_db_(static_cast<double>(bin) * bin_hz, config.weighting));
138 }
139
140 config_ = config;
141 input_bin_count_ = input_bins;
142 first_bin_ = first_bin;
143 end_bin_ = end_bin;
144 weighting_db_ = weighting_db;
145 centers_hz_ = center_hz;
146 reset();
147 return true;
148 }
149
151 [[nodiscard]] bool process_frame(std::span<const SampleType> magnitude_db) noexcept {
152 if (magnitude_db.size() != input_bin_count_)
153 return false;
154
155 for (std::size_t band = 0; band < config_.band_count; ++band) {
156 SampleType target = static_cast<SampleType>(config_.floor_db);
157 const bool band_has_bins = first_bin_[band] < end_bin_[band];
158 if (band_has_bins && config_.aggregation == SpectrumBandAggregation::maximum) {
159 for (std::size_t bin = first_bin_[band]; bin < end_bin_[band]; ++bin)
160 target = std::max(target, sanitize_bin_(magnitude_db[bin], bin));
161 } else if (band_has_bins) {
162 long double power = 0.0L;
163 for (std::size_t bin = first_bin_[band]; bin < end_bin_[band]; ++bin) {
164 const auto db = sanitize_bin_(magnitude_db[bin], bin);
165 power += std::pow(10.0L, static_cast<long double>(db) / 10.0L);
166 }
167 power /= static_cast<long double>(end_bin_[band] - first_bin_[band]);
168 const auto aggregated_db = 10.0L * std::log10(power);
169 target = static_cast<SampleType>(
170 std::clamp(aggregated_db, static_cast<long double>(config_.floor_db),
171 static_cast<long double>(config_.ceiling_db)));
172 }
173 target = std::clamp(target, static_cast<SampleType>(config_.floor_db),
174 static_cast<SampleType>(config_.ceiling_db));
175
176 if (!initialized_) {
177 frame_.magnitude_db[band] = target;
178 frame_.peak_db[band] = target;
179 peak_hold_remaining_[band] = config_.peak_hold_frames;
180 continue;
181 }
182
183 auto& smoothed = frame_.magnitude_db[band];
184 const auto coefficient =
185 static_cast<SampleType>(target > smoothed ? config_.attack : config_.release);
186 smoothed = std::lerp(smoothed, target, coefficient);
187
188 auto& peak = frame_.peak_db[band];
189 if (smoothed >= peak) {
190 peak = smoothed;
191 peak_hold_remaining_[band] = config_.peak_hold_frames;
192 } else if (peak_hold_remaining_[band] > 0) {
193 --peak_hold_remaining_[band];
194 } else {
195 const auto decay = static_cast<SampleType>(config_.peak_decay_db_per_frame);
196 const auto lowest = std::numeric_limits<SampleType>::lowest();
197 const auto decayed =
199 peak = std::max(smoothed, decayed);
200 }
201 }
202
203 initialized_ = true;
204 frame_.band_count = config_.band_count;
205 frame_.sequence = ++sequence_;
206 return true;
207 }
208
209 void reset() noexcept {
210 initialized_ = false;
211 sequence_ = 0;
212 frame_ = {};
213 frame_.band_count = config_.band_count;
214 for (std::size_t band = 0; band < config_.band_count; ++band) {
215 frame_.magnitude_db[band] = static_cast<SampleType>(config_.floor_db);
216 frame_.peak_db[band] = static_cast<SampleType>(config_.floor_db);
217 frame_.center_hz[band] = centers_hz_[band];
218 }
219 peak_hold_remaining_.fill(0);
220 }
221
223 [[nodiscard]] const Frame& current() const noexcept {
224 return frame_;
225 }
226 [[nodiscard]] const SpectrumTraceConfig& config() const noexcept {
227 return config_;
228 }
229 [[nodiscard]] std::size_t input_bin_count() const noexcept {
230 return input_bin_count_;
231 }
232 [[nodiscard]] static constexpr std::uint32_t algorithmic_latency_samples() noexcept {
233 return 0;
234 }
235
236 private:
237 static bool representable_(double value) noexcept {
238 return value >= static_cast<double>(std::numeric_limits<SampleType>::lowest()) &&
239 value <= static_cast<double>(std::numeric_limits<SampleType>::max());
240 }
241
242 static double band_edge_(const SpectrumTraceConfig& config, double t) noexcept {
244 return std::lerp(config.minimum_hz, config.maximum_hz, t);
245 return std::exp(std::lerp(std::log(config.minimum_hz), std::log(config.maximum_hz), t));
246 }
247
248 static double calculate_weighting_db_(double frequency_hz,
249 SpectrumFrequencyWeighting weighting) noexcept {
250 if (weighting == SpectrumFrequencyWeighting::none)
251 return 0.0;
252 if (!(frequency_hz > 0.0) || !std::isfinite(frequency_hz))
253 return -200.0;
254 const double f2 = frequency_hz * frequency_hz;
255 constexpr double f1 = 20.6;
256 constexpr double f2a = 107.7;
257 constexpr double f3 = 737.9;
258 constexpr double f4 = 12200.0;
259 if (weighting == SpectrumFrequencyWeighting::a) {
260 const double numerator = f4 * f4 * f2 * f2;
261 const double denominator =
262 (f2 + f1 * f1) * std::sqrt((f2 + f2a * f2a) * (f2 + f3 * f3)) * (f2 + f4 * f4);
263 return 20.0 * std::log10(numerator / denominator) + 2.0;
264 }
265 const double numerator = f4 * f4 * f2;
266 const double denominator = (f2 + f1 * f1) * (f2 + f4 * f4);
267 return 20.0 * std::log10(numerator / denominator) + 0.06;
268 }
269
270 SampleType sanitize_bin_(SampleType value, std::size_t bin) const noexcept {
271 const auto floor = static_cast<SampleType>(config_.floor_db);
272 const auto ceiling = static_cast<SampleType>(config_.ceiling_db);
273 if (!std::isfinite(value))
274 return floor;
275 if (bin == 0 && config_.weighting != SpectrumFrequencyWeighting::none)
276 return floor;
277 const auto correction = weighting_db_[bin];
278 const auto maximum = std::numeric_limits<SampleType>::max();
279 const auto lowest = std::numeric_limits<SampleType>::lowest();
280 if (correction > SampleType{0} && value > SampleType{0} && correction > maximum - value)
281 return ceiling;
282 if (correction < SampleType{0} && value < SampleType{0} && correction < lowest - value)
283 return floor;
284 const auto weighted = static_cast<SampleType>(value + correction);
285 return std::clamp(weighted, floor, ceiling);
286 }
287
288 SpectrumTraceConfig config_{};
289 std::size_t input_bin_count_ = 0;
290 std::array<std::size_t, MaxBands> first_bin_{};
291 std::array<std::size_t, MaxBands> end_bin_{};
292 std::array<SampleType, MaxInputBins> weighting_db_{};
293 std::array<SampleType, MaxBands> centers_hz_{};
294 std::array<std::uint32_t, MaxBands> peak_hold_remaining_{};
295 Frame frame_{};
296 std::uint64_t sequence_ = 0;
297 bool initialized_ = false;
298};
299
302
303} // namespace pulp::signal
Definition spectrum_trace.hpp:65
const Frame & current() const noexcept
Same-thread view. This reference is not a concurrent publication API.
Definition spectrum_trace.hpp:223
static constexpr std::uint32_t algorithmic_latency_samples() noexcept
Definition spectrum_trace.hpp:232
bool configure(const SpectrumTraceConfig &config) noexcept
Control-thread operation.
Definition spectrum_trace.hpp:84
bool process_frame(std::span< const SampleType > magnitude_db) noexcept
Rejected frames leave the current trace and sequence unchanged.
Definition spectrum_trace.hpp:151
std::size_t input_bin_count() const noexcept
Definition spectrum_trace.hpp:229
const SpectrumTraceConfig & config() const noexcept
Definition spectrum_trace.hpp:226
SpectrumTraceFrameT< SampleType, MaxBands > Frame
Definition spectrum_trace.hpp:72
SpectrumTraceT() noexcept
Definition spectrum_trace.hpp:74
void reset() noexcept
Definition spectrum_trace.hpp:209
Definition additive_bank.hpp:218
@ first
Definition dither.hpp:23
@ center
Definition multi_channel_meter.hpp:68
@ peak
Sample peak. Literal, fastest-responding.
Definition feedforward_compressor.hpp:119
float magnitude_db(const BiquadT< SampleType > &filter, double freq_hz, double sample_rate)
Ask a live filter what it looks like: the magnitude, in dB, that this BiquadT is applying at freq_hz ...
Definition frequency_response.hpp:130
SpectrumFrequencyWeighting
Definition spectrum_trace.hpp:35
@ a
Definition spectrum_trace.hpp:35
@ none
Definition spectrum_trace.hpp:35
@ c
Definition spectrum_trace.hpp:35
SpectrumTraceT< float > SpectrumTrace
Definition spectrum_trace.hpp:300
@ decay
Definition envelope.hpp:60
SpectrumTraceT< double > SpectrumTrace64
Definition spectrum_trace.hpp:301
SpectrumBandScale
Definition spectrum_trace.hpp:33
@ logarithmic
Definition spectrum_trace.hpp:33
@ linear
Definition spectrum_trace.hpp:33
@ logarithmic
Definition modulation_curve.hpp:29
SpectrumBandAggregation
Definition spectrum_trace.hpp:34
@ maximum
Definition spectrum_trace.hpp:34
@ mean_power
Definition spectrum_trace.hpp:34
@ a
Definition formant_filter_bank.hpp:20
@ none
Definition dither.hpp:17
@ linear
2-point. Cheapest; audible imaging when pitching up.
Definition granular.hpp:133
@ value
the widget's primary value (Knob, Fader, RangeSlider, Toggle, ProgressBar)
Definition binding_diagnostics.hpp:19
@ f2
Definition input_events.hpp:61
@ f4
Definition input_events.hpp:61
@ f1
Definition input_events.hpp:61
Definition spectrum_trace.hpp:37
double maximum_hz
Definition spectrum_trace.hpp:42
double release
Definition spectrum_trace.hpp:49
SpectrumBandAggregation aggregation
Definition spectrum_trace.hpp:44
double attack
Definition spectrum_trace.hpp:48
SpectrumBandScale band_scale
Definition spectrum_trace.hpp:43
double sample_rate
Definition spectrum_trace.hpp:38
SpectrumFrequencyWeighting weighting
Definition spectrum_trace.hpp:45
double floor_db
Definition spectrum_trace.hpp:46
double minimum_hz
Definition spectrum_trace.hpp:41
double ceiling_db
Definition spectrum_trace.hpp:47
std::uint32_t fft_size
Definition spectrum_trace.hpp:39
std::size_t band_count
Definition spectrum_trace.hpp:40
double peak_decay_db_per_frame
Definition spectrum_trace.hpp:51
std::uint32_t peak_hold_frames
Definition spectrum_trace.hpp:50
Definition spectrum_trace.hpp:54
std::size_t band_count
Definition spectrum_trace.hpp:60
std::array< SampleType, MaxBands > peak_db
Definition spectrum_trace.hpp:58
std::uint64_t sequence
Definition spectrum_trace.hpp:61
std::array< SampleType, MaxBands > center_hz
Definition spectrum_trace.hpp:59
std::array< SampleType, MaxBands > magnitude_db
Definition spectrum_trace.hpp:57