Sound Byte Libs 0.5.1-121-g3358a44
C++ firmware library for audio applications on 32-bit ARM Cortex-M processors
Loading...
Searching...
No Matches
measure.hpp
Go to the documentation of this file.
1// sbl/dsp/analysis/measure.hpp — Measuring a rendered signal (Cross-cutting — host only)
2//
3// The measurements that made the bowed string trustworthy, in one place so a
4// new model gets them for the cost of a call rather than a copy (AP-041
5// Phase 3; RPT-035 Part 4 found the period estimator five times). Host
6// tooling and tests only: double arithmetic, std::vector, no ISR promises.
7// Never included from audio code.
8//
9// bin_energy / bin_power one frequency's share of a window (Goertzel;
10// Hann-weighted energy, or rectangular mean power)
11// autocorr_period the period near an expected lag, interpolated
12// harmonic_profile level, centroid, each harmonic in dB below the strongest
13// measure_window periodicity, slips per period, fundamental share,
14// high-band share, peak, DC, stick fraction
15// magnitude_at a processor's magnitude at one frequency, by sine drive
16// reflectance a termination's magnitude and phase delay at one frequency
17// decay_fit one partial's decay in dB/s from windowed energies
18
19#ifndef SBL_DSP_ANALYSIS_MEASURE_HPP_
20#define SBL_DSP_ANALYSIS_MEASURE_HPP_
21
22#include <cmath>
23#include <cstdint>
24#include <vector>
25
27
28inline constexpr double PI = 3.14159265358979323846;
29inline constexpr int MAX_HARMONICS = 16;
30
31// ─── One frequency's share of a window ───────────────────────────────
32
33enum class Window { Rectangular, Hann };
34
35/// |Σ w[i] x[i] e^(−jωi)|² by Goertzel's recursion, w the window. The one
36/// projection both energy measures are built on.
37template<typename T>
38inline double projection_power(const T* x, int n, double hz, double fs, Window window) {
39 const double w = 2.0 * PI * hz / fs;
40 const double coeff = 2.0 * std::cos(w);
41 double s1 = 0.0, s2 = 0.0;
42 for (int i = 0; i < n; ++i) {
43 const double weight = window == Window::Hann ? 0.5 - 0.5 * std::cos(2.0 * PI * i / n) : 1.0;
44 const double s0 = weight * x[i] + coeff * s1 - s2;
45 s2 = s1;
46 s1 = s0;
47 }
48 return s1 * s1 + s2 * s2 - coeff * s1 * s2;
49}
50
51/// Hann-weighted energy at hz, normalised by n². The window keeps harmonics
52/// apart when bins sit on a measured, not nominal, pitch.
53template<typename T>
54inline double bin_energy(const T* x, int n, double hz, double fs) {
55 return projection_power(x, n, hz, fs, Window::Hann) / (static_cast<double>(n) * n);
56}
57
58/// Rectangular power at hz as mean power: a unit-amplitude sine at hz reads 0.5,
59/// and the bins of a spectrum sum to mean_power(). The DSP suite's measure.
60template<typename T>
61inline double bin_power(const T* x, int n, double hz, double fs) {
62 return 2.0 * projection_power(x, n, hz, fs, Window::Rectangular) / (static_cast<double>(n) * n);
63}
64
65template<typename T>
66inline double mean_power(const T* x, int n) {
67 double e = 0.0;
68 for (int i = 0; i < n; ++i) e += static_cast<double>(x[i]) * x[i];
69 return n > 0 ? e / n : 0.0;
70}
71
72// ─── Period ──────────────────────────────────────────────────────────
73
74struct Period {
75 double samples; ///< the interpolated period
76 double periodicity; ///< autocorrelation at the period over the energy: 1 is periodic there
77 int lag; ///< the integer lag the peak sat on
78};
79
80/// Autocorrelation near an expected lag: the best integer lag within
81/// ±tolerance of it, refined by a parabola through its neighbours. The mean
82/// is removed on a copy; every lag is summed over the same span so the peak
83/// is not biased toward short lags.
84template<typename T>
85inline Period autocorr_period(const T* x, int n, double expected, double tolerance) {
86 std::vector<double> y(x, x + n);
87 double mean = 0.0;
88 for (double v : y) mean += v;
89 mean /= n > 0 ? n : 1;
90 for (double& v : y) v -= mean;
91
92 int lo = static_cast<int>(expected * (1.0 - tolerance));
93 int hi = static_cast<int>(expected * (1.0 + tolerance)) + 2;
94 if (lo < 1) lo = 1;
95 if (hi > n - 2) hi = n - 2;
96 const int span = n - hi - 1;
97 auto corr = [&](int lag) {
98 double acc = 0.0;
99 for (int i = 0; i < span; ++i) acc += y[i] * y[i + lag];
100 return acc;
101 };
102 Period p{expected, 0.0, lo};
103 if (span <= 0 || hi < lo) return p;
104
105 double best = corr(lo);
106 for (int lag = lo + 1; lag <= hi; ++lag) {
107 const double v = corr(lag);
108 if (v > best) { best = v; p.lag = lag; }
109 }
110 const double r0 = corr(0);
111 p.periodicity = r0 > 0.0 ? best / r0 : 0.0;
112 const double ym1 = corr(p.lag - 1), y0 = best, yp1 = corr(p.lag + 1);
113 const double den = ym1 - 2.0 * y0 + yp1;
114 p.samples = p.lag + (den != 0.0 ? 0.5 * (ym1 - yp1) / den : 0.0);
115 return p;
116}
117
118// ─── Harmonic profile ────────────────────────────────────────────────
119
120struct Profile {
121 double rms_db;
123 double db[MAX_HARMONICS]; ///< each harmonic in dB below the strongest; −120 when absent
125};
126
127/// Level, spectral centroid over the harmonics, and each harmonic's level
128/// relative to the strongest, on bins at multiples of f0 (Hann-weighted).
129template<typename T>
130inline Profile harmonic_profile(const T* x, int n, double f0, double fs, int harmonics) {
131 Profile pr{};
132 if (harmonics > MAX_HARMONICS) harmonics = MAX_HARMONICS;
133 pr.harmonics = harmonics;
134 const double energy = mean_power(x, n);
135 pr.rms_db = energy > 0.0 ? 10.0 * std::log10(energy) : -120.0;
136 double amp[MAX_HARMONICS] = {};
137 double strongest = 0.0, weighted = 0.0, total = 0.0;
138 for (int h = 1; h <= harmonics; ++h) {
139 if (h * f0 >= fs * 0.45) break;
140 const double e = bin_energy(x, n, h * f0, fs);
141 amp[h - 1] = e;
142 strongest = strongest > e ? strongest : e;
143 weighted += h * f0 * e;
144 total += e;
145 }
146 pr.centroid_hz = total > 0.0 ? weighted / total : 0.0;
147 for (int h = 0; h < MAX_HARMONICS; ++h)
148 pr.db[h] = (amp[h] > 0.0 && strongest > 0.0) ? 10.0 * std::log10(amp[h] / strongest) : -120.0;
149 return pr;
150}
151
152// ─── A bowed window ──────────────────────────────────────────────────
153
154struct Measure {
155 double periodicity; ///< normalised autocorrelation at the played period (1 = periodic there)
156 double measured_period; ///< samples, from the autocorrelation peak near the nominal
157 double slips_per_period; ///< releases per period: 1 is Helmholtz motion, 2 an octave regime
158 double h1_percent; ///< the fundamental's share of the first `harmonics` harmonics' energy
159 double hf_percent; ///< second-difference energy share: the harsh high band
160 double peak;
161 double dc;
163};
164
165/// Judge a window of a bowed model's output. The mean is removed from `x`
166/// in place, so a profile taken afterwards sees the same signal. Stick and
167/// slip counts come from the caller, who watched the exciter while it played.
168template<typename T>
169inline Measure measure_window(T* x, int n, double nominal_period, double fs, int stick, int slips,
170 int harmonics) {
171 Measure m{};
172 double mean = 0.0;
173 for (int i = 0; i < n; ++i) {
174 mean += x[i];
175 const double a = std::fabs(static_cast<double>(x[i]));
176 m.peak = a > m.peak ? a : m.peak;
177 }
178 mean /= n > 0 ? n : 1;
179 m.dc = mean;
180 for (int i = 0; i < n; ++i) x[i] -= static_cast<T>(mean);
181 m.stick_percent = n > 0 ? stick * 100 / n : 0;
182
183 const Period p = autocorr_period(x, n, nominal_period, 0.03);
184 m.periodicity = p.periodicity;
185 m.measured_period = p.samples;
186 m.slips_per_period = n > 0 ? slips * m.measured_period / n : 0.0;
187
188 const double f0 = fs / m.measured_period;
189 double total = 0.0, fundamental = 0.0;
190 for (int h = 1; h <= harmonics && h * f0 < fs * 0.45; ++h) {
191 const double e = bin_energy(x, n, h * f0, fs);
192 if (h == 1) fundamental = e;
193 total += e;
194 }
195 m.h1_percent = total > 0.0 ? 100.0 * fundamental / total : 0.0;
196
197 const double r0 = mean_power(x, n) * n;
198 double hf = 0.0;
199 for (int i = 2; i < n; ++i) {
200 const double d2 = static_cast<double>(x[i]) - 2.0 * x[i - 1] + x[i - 2];
201 hf += d2 * d2 * 0.0625;
202 }
203 m.hf_percent = r0 > 0.0 ? 100.0 * hf / r0 : 0.0;
204 return m;
205}
206
207/// One release per period, give or take a window edge.
208inline bool helmholtz(const Measure& m, double min_slips = 0.9, double max_slips = 1.1) {
209 return m.slips_per_period >= min_slips && m.slips_per_period <= max_slips;
210}
211
212// ─── Probes ──────────────────────────────────────────────────────────
213
214/// A processor's magnitude at hz: drive it with a unit sine for `settle`
215/// samples, then project its output on the drive over `n`.
216template<typename F>
217inline double magnitude_at(double hz, double fs, F&& process, int settle = 4000, int n = 20000) {
218 const double w = 2.0 * PI * hz / fs;
219 double re = 0.0, im = 0.0;
220 for (int i = 0; i < settle; ++i) process(static_cast<float>(std::sin(w * i)));
221 for (int i = settle; i < settle + n; ++i) {
222 const double y = process(static_cast<float>(std::sin(w * i)));
223 re += y * std::sin(w * i);
224 im += y * std::cos(w * i);
225 }
226 return std::sqrt(re * re + im * im) * 2.0 / n;
227}
228
230 double magnitude;
231 double phase_delay_samples; ///< beyond a wall's π: positive when a loop runs longer for it
232};
233
234/// A termination's reflection for a unit sine arriving at hz.
235template<typename F>
236inline Reflectance reflectance(double hz, double fs, F&& reflect, int settle = -1, int n = -1) {
237 if (settle < 0) settle = static_cast<int>(fs * 0.2);
238 if (n < 0) n = static_cast<int>(fs * 0.5);
239 const double w = 2.0 * PI * hz / fs;
240 for (int i = 0; i < settle; ++i) reflect(static_cast<float>(std::sin(w * i)));
241 double re = 0.0, im = 0.0;
242 for (int i = settle; i < settle + n; ++i) {
243 const double r = reflect(static_cast<float>(std::sin(w * i)));
244 re += r * std::cos(w * i);
245 im += r * std::sin(w * i);
246 }
247 const double a = 2.0 * im / n, b = 2.0 * re / n; // r ≈ a sin + b cos
248 Reflectance out{std::sqrt(a * a + b * b), 0.0};
249 double extra = std::atan2(b, a) - PI; // the phase beyond a wall's
250 while (extra > PI) extra -= 2.0 * PI;
251 while (extra < -PI) extra += 2.0 * PI;
252 out.phase_delay_samples = -extra / w;
253 return out;
254}
255
256struct Decay {
257 double db_per_s;
258 int windows_fit; ///< windows above the floor that the line was fitted through
259};
260
261/// One partial's decay: energy at hz in consecutive windows, a straight line
262/// through dB against time until 60 dB down or the floor.
263template<typename T>
264inline Decay decay_fit(const T* x, int n, double hz, int window, double fs, double floor = 1e-12) {
265 Decay d{0.0, 0};
266 const int windows = window > 0 ? n / window : 0;
267 double first_db = 0.0, sx = 0, sy = 0, sxx = 0, sxy = 0;
268 for (int w = 0; w < windows; ++w) {
269 const double e = bin_energy(x + w * window, window, hz, fs);
270 if (e < floor) break;
271 const double db = 10.0 * std::log10(e);
272 if (w == 0) first_db = db;
273 const double t = (w + 0.5) * window / fs;
274 sx += t; sy += db; sxx += t * t; sxy += t * db;
275 ++d.windows_fit;
276 if (d.windows_fit >= 2 && db < first_db - 60.0) break;
277 }
278 const int k = d.windows_fit;
279 d.db_per_s = k >= 2 ? (k * sxy - sx * sy) / (k * sxx - sx * sx) : 0.0;
280 return d;
281}
282
283} // namespace sbl::dsp::analysis
284
285#endif // SBL_DSP_ANALYSIS_MEASURE_HPP_
Host-only measurement.
Definition measure.hpp:26
constexpr int MAX_HARMONICS
Definition measure.hpp:29
double bin_energy(const T *x, int n, double hz, double fs)
Definition measure.hpp:54
double bin_power(const T *x, int n, double hz, double fs)
Definition measure.hpp:61
constexpr double PI
Definition measure.hpp:28
Measure measure_window(T *x, int n, double nominal_period, double fs, int stick, int slips, int harmonics)
Definition measure.hpp:169
Period autocorr_period(const T *x, int n, double expected, double tolerance)
Definition measure.hpp:85
double projection_power(const T *x, int n, double hz, double fs, Window window)
Definition measure.hpp:38
double mean_power(const T *x, int n)
Definition measure.hpp:66
Decay decay_fit(const T *x, int n, double hz, int window, double fs, double floor=1e-12)
Definition measure.hpp:264
Profile harmonic_profile(const T *x, int n, double f0, double fs, int harmonics)
Definition measure.hpp:130
double magnitude_at(double hz, double fs, F &&process, int settle=4000, int n=20000)
Definition measure.hpp:217
Reflectance reflectance(double hz, double fs, F &&reflect, int settle=-1, int n=-1)
A termination's reflection for a unit sine arriving at hz.
Definition measure.hpp:236
bool helmholtz(const Measure &m, double min_slips=0.9, double max_slips=1.1)
One release per period, give or take a window edge.
Definition measure.hpp:208
int windows_fit
windows above the floor that the line was fitted through
Definition measure.hpp:258
double slips_per_period
releases per period: 1 is Helmholtz motion, 2 an octave regime
Definition measure.hpp:157
double h1_percent
the fundamental's share of the first harmonics harmonics' energy
Definition measure.hpp:158
double measured_period
samples, from the autocorrelation peak near the nominal
Definition measure.hpp:156
double periodicity
normalised autocorrelation at the played period (1 = periodic there)
Definition measure.hpp:155
double hf_percent
second-difference energy share: the harsh high band
Definition measure.hpp:159
double periodicity
autocorrelation at the period over the energy: 1 is periodic there
Definition measure.hpp:76
int lag
the integer lag the peak sat on
Definition measure.hpp:77
double samples
the interpolated period
Definition measure.hpp:75
double db[MAX_HARMONICS]
each harmonic in dB below the strongest; −120 when absent
Definition measure.hpp:123
double phase_delay_samples
beyond a wall's π: positive when a loop runs longer for it
Definition measure.hpp:231