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
calibration.hpp
Go to the documentation of this file.
1// sbl/control/calibration.hpp — Source calibration: fit, standard, routine (AP-033)
2//
3// A source reads a little off: a CV jack's divider is 2% high, its op-amp
4// offset puts 0 V at 0.003 su. Calibration measures the source against known
5// references and stores the correction as a line — scale and offset — that
6// the app applies to every later reading.
7//
8// LinearFit the correction: apply(measured) = measured * scale + offset
9// fit_points() least squares over 2..N captured points
10// VOctStandard what a reference means in su (1 V per octave)
11// PointCalibration the routine: begin, capture, capture, done
12//
13// One shape drives every calibration an app runs — a button calls advance(),
14// another cancel(), and the UI slot polls state() for the LED. The library
15// holds no storage: fit() is POD, and the app writes it wherever it keeps
16// its calibration (CAL.BIN today).
17//
18// Usage — two-point V/Oct on a 1 V and 3 V reference:
19// constexpr float kRefs[] = {1.0f, 3.0f}; // volts
20// sbl::control::PointCalibration<2> cal{kRefs};
21//
22// cal.begin(); // button: start
23// cal.capture(jack.value()); // button: patch 1 V, capture
24// cal.capture(jack.value()); // button: patch 3 V, capture → Done
25// float pitch = cal.apply(jack.value()); // corrected, from now on
26//
27// Loading a stored fit skips the routine entirely: cal.set_fit(saved).
28
29#ifndef SBL_CONTROL_CALIBRATION_HPP_
30#define SBL_CONTROL_CALIBRATION_HPP_
31
32#include <cstdint>
33
34namespace sbl::control {
35
36// ─── The correction ──────────────────────────────────────────────────
37
38/// measured su → corrected su. Identity until a fit is computed or loaded.
39struct LinearFit {
40 float scale = 1.0f;
41 float offset = 0.0f;
42
43 constexpr float apply(float measured_su) const { return measured_su * scale + offset; }
44};
45
46/**
47 * @brief Least-squares line through captured points
48 *
49 * @param measured what the source read at each reference
50 * @param expected what it should have read (the standard's answer)
51 * @param n 2 or more
52 * @param out the fit, untouched when the points are degenerate
53 * @return false if n < 2 or every measurement is the same value
54 */
55inline bool fit_points(const float* measured, const float* expected, uint8_t n,
56 LinearFit& out) {
57 if (n < 2) return false;
58
59 float sum_x = 0.0f, sum_y = 0.0f;
60 for (uint8_t i = 0; i < n; ++i) {
61 sum_x += measured[i];
62 sum_y += expected[i];
63 }
64 const float inv_n = 1.0f / static_cast<float>(n);
65 const float mean_x = sum_x * inv_n;
66 const float mean_y = sum_y * inv_n;
67
68 float cov = 0.0f, var = 0.0f;
69 for (uint8_t i = 0; i < n; ++i) {
70 const float dx = measured[i] - mean_x;
71 cov += dx * (expected[i] - mean_y);
72 var += dx * dx;
73 }
74 if (var <= 1e-12f) return false; // every point read the same: no line
75
76 out.scale = cov / var;
77 out.offset = mean_y - out.scale * mean_x;
78 return true;
79}
80
81// ─── What a reference means ──────────────────────────────────────────
82
83/**
84 * @brief 1 V per octave in SBL pitch su (1.0 su spans 8 octaves)
85 *
86 * References are volts. A standard with a different law — 1.2 V per octave,
87 * or a Hz-per-volt input — supplies its own `expected_su`.
88 */
90 static constexpr float kSuPerVolt = 0.125f;
91 static constexpr float expected_su(float volts) { return volts * kSuPerVolt; }
92};
93
94// ─── The routine ─────────────────────────────────────────────────────
95
96enum class CalState : uint8_t {
97 Idle, ///< not calibrating; fit() is whatever was loaded or measured
98 Capturing, ///< waiting for the next reference point
99 Done, ///< a fit was computed; accept() or cancel()
100 Failed, ///< the points were degenerate; cancel() to restore
101};
102
103/**
104 * @brief Capture N reference points, then fit
105 *
106 * The app decides when a reading has settled and calls capture() with the
107 * source's current su. The last capture computes the fit. cancel() restores
108 * the fit that was in force when begin() was called, so an abandoned
109 * calibration costs nothing.
110 */
111template<uint8_t N, typename Standard = VOctStandard>
113 static_assert(N >= 2, "a line needs two points");
114
115public:
116 /// @param references the reference values, in the standard's units
117 explicit PointCalibration(const float (&references)[N]) {
118 for (uint8_t i = 0; i < N; ++i) references_[i] = references[i];
119 }
120
121 /// Start capturing at point 0, remembering the current fit.
122 void begin() {
123 backup_ = fit_;
124 point_ = 0;
125 state_ = CalState::Capturing;
126 }
127
128 /**
129 * @brief Record the source's reading for the current reference
130 * @return true when this capture completed the calibration
131 */
132 bool capture(float measured_su) {
133 if (state_ != CalState::Capturing) return false;
134 measured_[point_] = measured_su;
135 ++point_;
136 if (point_ < N) return false;
137
138 float expected[N];
139 for (uint8_t i = 0; i < N; ++i) expected[i] = Standard::expected_su(references_[i]);
140
141 LinearFit fitted;
142 if (fit_points(measured_, expected, N, fitted)) {
143 fit_ = fitted;
144 state_ = CalState::Done;
145 } else {
146 state_ = CalState::Failed;
147 }
148 return true;
149 }
150
151 /// Leave the routine, keeping the new fit (the app persists it).
152 void accept() {
153 if (state_ == CalState::Done) state_ = CalState::Idle;
154 }
155
156 /// Abandon: restore the fit from before begin().
157 void cancel() {
158 fit_ = backup_;
159 state_ = CalState::Idle;
160 point_ = 0;
161 }
162
163 /// Correct a reading. Identity until a fit is computed or loaded.
164 float apply(float measured_su) const { return fit_.apply(measured_su); }
165
166 CalState state() const { return state_; }
167 bool active() const { return state_ == CalState::Capturing; }
168
169 /// Points captured so far — the reference to patch next, for the UI.
170 uint8_t point() const { return point_; }
171 static constexpr uint8_t points() { return N; }
172
173 /// The reference the app should patch for the current point.
174 float reference() const { return references_[point_ < N ? point_ : N - 1]; }
175
176 const LinearFit& fit() const { return fit_; }
177
178 /// Load a stored fit (boot) or install one by hand.
179 void set_fit(const LinearFit& fit) { fit_ = fit; }
180
181private:
182 float references_[N]{};
183 float measured_[N]{};
184 LinearFit fit_{};
185 LinearFit backup_{};
186 uint8_t point_ = 0;
187 CalState state_ = CalState::Idle;
188};
189
190} // namespace sbl::control
191
192#endif // SBL_CONTROL_CALIBRATION_HPP_
Capture N reference points, then fit.
void accept()
Leave the routine, keeping the new fit (the app persists it).
bool capture(float measured_su)
Record the source's reading for the current reference.
PointCalibration(const float(&references)[N])
void begin()
Start capturing at point 0, remembering the current fit.
void cancel()
Abandon: restore the fit from before begin().
void set_fit(const LinearFit &fit)
Load a stored fit (boot) or install one by hand.
const LinearFit & fit() const
float reference() const
The reference the app should patch for the current point.
uint8_t point() const
Points captured so far — the reference to patch next, for the UI.
static constexpr uint8_t points()
float apply(float measured_su) const
Correct a reading. Identity until a fit is computed or loaded.
Control Stack v2: scheduler, event queue, merge, CC mapping, calibration, presets.
@ Capturing
waiting for the next reference point
@ Failed
the points were degenerate; cancel() to restore
@ Idle
not calibrating; fit() is whatever was loaded or measured
@ Done
a fit was computed; accept() or cancel()
bool fit_points(const float *measured, const float *expected, uint8_t n, LinearFit &out)
Least-squares line through captured points.
measured su → corrected su. Identity until a fit is computed or loaded.
constexpr float apply(float measured_su) const
1 V per octave in SBL pitch su (1.0 su spans 8 octaves)
static constexpr float kSuPerVolt
static constexpr float expected_su(float volts)