Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
eq_curve_view.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <functional>
8#include <pulp/view/view.hpp>
9#include <span>
10#include <vector>
11
12namespace pulp::view {
13
14// ── EqCurveView ─────────────────────────────────────────────────────────────
15// Parametric EQ curve visualization with draggable band handles.
16// Each band has frequency, gain, Q, and filter type.
17//
18// The drawn curve is the true magnitude response of the bands: each band is
19// designed into biquad coefficients (FilterDesign) and evaluated via
20// signal::response_curve_db. It is the same math the audio path runs, so the
21// picture matches what a listener hears — a low-pass rolls off, a notch cuts a
22// null, a shelf plateaus.
23
24class EqCurveView : public View {
25 public:
27
28 struct Band {
29 float frequency = 1000.0f; // Hz
30 float gain_db = 0.0f; // dB (-24 to +24)
31 float q = 1.0f; // Q factor (0.1 to 30)
33 bool enabled = true;
34 // Per-band color. Left fully transparent by default as the "unset"
35 // sentinel — the view then assigns one from band_palette_color(). A
36 // plain Color{} is opaque BLACK (alpha defaults to 1), which would read
37 // as an explicitly-chosen black, so the sentinel must zero alpha.
38 Color color{0.0f, 0.0f, 0.0f, 0.0f};
39 };
40
42
43 // Band management
44 void set_bands(std::vector<Band> bands);
45 const std::vector<Band>& bands() const {
46 return bands_;
47 }
48 void set_band(size_t index, const Band& band);
49 void add_band(Band band);
50 void remove_band(size_t index);
51 size_t band_count() const {
52 return bands_.size();
53 }
54
55 // Display range
56 void set_frequency_range(float min_hz, float max_hz);
57 void set_gain_range(float min_db, float max_db);
58 float min_frequency() const {
59 return min_freq_;
60 }
61 float max_frequency() const {
62 return max_freq_;
63 }
64 float min_gain() const {
65 return min_db_;
66 }
67 float max_gain() const {
68 return max_db_;
69 }
70
71 // Sample rate the bands are designed against. A digital filter's response
72 // depends on it — the same 8 kHz shelf is a gentle tilt at 96 kHz and a
73 // near-cliff at 44.1 kHz — so a curve drawn at the wrong rate is the wrong
74 // curve. Set it from Processor::prepare().
76 float sample_rate() const {
77 return sample_rate_;
78 }
79
80 // Magnitude of the combined band cascade at a frequency, in dB. The value
81 // the curve is drawn from; exposed so callers can label a readout or probe
82 // the response without re-deriving it.
83 float magnitude_db_at(float freq_hz) const;
84
85 // Axes this view plots against — shared with any widget that wants to
86 // overlay on it (a spectrum, a crosshair readout) so both agree on where a
87 // given frequency and dB value land.
90
91 // Grid lines
92 void set_show_grid(bool show) {
93 show_grid_ = show;
94 }
95 bool show_grid() const {
96 return show_grid_;
97 }
98
99 // Per-band curves + translucent fills, each in the band's color, drawn under
100 // the composite so every handle sits on its own curve. On by default.
101 void set_show_band_curves(bool show) {
102 show_band_curves_ = show;
103 }
104 bool show_band_curves() const {
105 return show_band_curves_;
106 }
107
108 // Draw handles for DISABLED (bypassed) bands as dimmed dots instead of
109 // hiding them. Off by default so existing callers are unaffected; a host
110 // that models bypass (the composite already excludes a disabled band) turns
111 // this on so a bypassed band still shows a faded, re-grabbable handle.
113 show_disabled_handles_ = show;
114 }
116 return show_disabled_handles_;
117 }
118
119 // dB (vertical) and frequency (horizontal) axis labels. On by default.
120 void set_show_labels(bool show) {
121 show_labels_ = show;
122 }
123 bool show_labels() const {
124 return show_labels_;
125 }
126
127 // Live readout (frequency / gain / Q) of the band under the pointer or being
128 // dragged. On by default.
129 void set_show_readout(bool show) {
130 show_readout_ = show;
131 }
132 bool show_readout() const {
133 return show_readout_;
134 }
135
136 // Reserve empty space at the TOP of the plotting area (pixels). The grid,
137 // curve, handles, and the top dB axis label are all pushed down by this
138 // amount so a host with no chrome above the graph (a plugin editor with no
139 // header) has room to paint its own readout / controls in the gap without
140 // clipping the "+12" label against the window edge. Off (0) by default so
141 // existing callers are unaffected. The bottom axis (frequency labels) does
142 // not move.
144 content_top_pad_ = px > 0.0f ? px : 0.0f;
145 }
146 float content_top_padding() const {
147 return content_top_pad_;
148 }
149
150 // Index of the band handle at a local point, or -1. Exposes the handle
151 // hit-test so a host that layers its own gestures on the dots (e.g.
152 // ⌥-click to bypass) can reuse the exact handle geometry instead of
153 // duplicating it. Additive; existing callers are unaffected.
154 int hit_test_handle(Point pos) const {
155 return hit_test_band(pos);
156 }
157
158 // Ease each handle's radius toward its target (resting vs. hovered/active)
159 // instead of snapping, so a hover reads as a small settle rather than a pop.
160 // Off by default (existing callers snap as before). A host that turns this
161 // on should keep the render loop alive while hover_animating() is true — the
162 // shared needs_continuous_frames() predicate already consults it.
164 hover_animation_ = on;
165 }
166 bool hover_animation() const {
167 return hover_animation_;
168 }
169
170 // True while any handle radius is mid-transition (drives continuous frames).
171 bool hover_animating() const {
172 return hover_animating_;
173 }
174
175 // Default color assigned to band i when its Band::color is unset, from a
176 // built-in palette. Exposed so callers can match handles or legends to it.
177 static Color band_palette_color(size_t index);
178
179 // Spectrum analyzer overlay (optional)
180 void set_spectrum(const float* magnitudes_db, size_t bin_count);
182
183 // Analyzer-capable by DEFAULT (on), so the widget shows a bound spectrum out
184 // of the box. When on, a bound spectrum is drawn and the widget joins the
185 // shared continuous-frame set WHILE data is present; a widget with no spectrum
186 // simply draws nothing (and does not spin frames). When off, no overlay is
187 // drawn even if stale smoothed data remains. A plugin can turn it off.
189 bool analyzer_enabled() const {
190 return analyzer_enabled_;
191 }
192
193 // True only while the analyzer should drive per-vsync repaints: enabled AND
194 // spectrum data present. `needs_continuous_frames()` consults this, so a
195 // default-on but silent/no-signal editor with NO bound spectrum stays idle
196 // instead of compositing a static frame every tick. Mirrors hover_animating().
197 bool analyzer_animating() const {
198 return analyzer_enabled_ && !spectrum_smoothed_.empty();
199 }
200
201 // Drag the LEFT dB-scale gutter to zoom the analyzer dBFS range, Logic-style
202 // (drag DOWN → lower/expand the floor toward −100 dBFS = zoom OUT; drag UP →
203 // raise the floor toward −40 dBFS = zoom IN). Scoped to a narrow left gutter
204 // over the dB labels so it never conflicts with band-dot dragging (a dot hit
205 // always wins). On by default (active whenever the analyzer is used); a plugin
206 // can disable it. The range is clamped to top ≤ +20, bottom ≥ −100, span ≥ 20.
208 analyzer_scale_draggable_ = on;
209 }
211 return analyzer_scale_draggable_;
212 }
213
214 // ── Analyzer dBFS scale ─────────────────────────────────────────────────
215 // The spectrum overlay is drawn on its OWN dBFS scale spanning the FULL
216 // plotting height, decoupled from the ±gain axis the EQ curve/dots use.
217 // Default: analyzer_top_db (0 dBFS) at the top of the plot, analyzer_bottom_db
218 // (−60 dBFS) at the bottom — so a smoothed envelope fills the whole graph
219 // (Logic / Pro-Q style) instead of being crushed into the gain axis. The
220 // setters exist for a future drag-to-zoom; the defaults are hardcoded.
221 void set_analyzer_range(float top_db, float bottom_db) {
222 analyzer_top_db_ = top_db;
223 analyzer_bottom_db_ = bottom_db;
224 }
225 float analyzer_top_db() const {
226 return analyzer_top_db_;
227 }
228 float analyzer_bottom_db() const {
229 return analyzer_bottom_db_;
230 }
231 // Map a dBFS value to a plot-space y via the analyzer scale (top_db → top of
232 // plot, bottom_db → bottom). Clamped to the plot. Independent of the gain
233 // axis; the spectrum overlay and any analyzer tick labels use this.
234 float analyzer_db_to_y(float dbfs) const;
235
236 // ── StateStore binding (parameter automation: record + playback) ────────
237 // Parameter IDs for one band's frequency, gain, and Q. One entry per band,
238 // in the same order as the bands passed to set_bands(). A zero id means the
239 // band has no parameter for that field (e.g. a gain-less filter).
245
246 // Bind this view to a StateStore so parameter automation works end-to-end:
247 //
248 // • Record (Direction A): the built-in drag / scroll / pinch / reset
249 // handlers bracket their edits in begin_gesture → set_value → end_gesture
250 // so the host records automation and groups the drag into one undo step.
251 // • Playback (Direction B): a Main-thread listener maps a host-originated
252 // parameter change back onto the corresponding band and repaints, so the
253 // curve follows automation. The listener is a PURE widget update — it
254 // never re-opens a gesture nor writes back to the store, so it cannot
255 // feed back into the record path.
256 //
257 // Additive/opt-in: on_band_changed still fires. RT-safety: begin/end_gesture
258 // and set_value run on the host main thread (the drag handlers already do);
259 // the Main listener fires only from the UI-thread listener pump.
260 void bind_bands(pulp::state::StateStore& store, std::span<const BandParamIds> ids);
261
262 // Whether the built-in handlers record their edits to the bound store
263 // (Direction A). On by default. A host that runs its OWN edit logic
264 // (relative / group moves, numeric scrubbing) and drives the store itself
265 // turns this OFF so the view does not double-write, while STILL receiving
266 // host-playback updates through the bind_bands listener (Direction B).
268 record_gestures_ = on;
269 }
270 bool record_gestures() const {
271 return record_gestures_;
272 }
273
274 // Interaction callbacks
275 std::function<void(size_t band_index, Band band)> on_band_changed;
276 std::function<void(size_t band_index)> on_band_selected;
277
278 // Selected band
279 int selected_band() const {
280 return selected_band_;
281 }
282 void set_selected_band(int index) {
283 selected_band_ = index;
284 }
285
286 // Band currently under the pointer (-1 if none). Drives the hover highlight
287 // and readout; exposed so hosts/tests can observe hover state.
288 int hovered_band() const {
289 return hovered_band_;
290 }
291
292 void paint(canvas::Canvas& canvas) override;
293 void on_mouse_event(const MouseEvent& event) override;
294 // Trackpad pinch adjusts the Q of the hovered (else selected) band: pinch-in
295 // narrows (higher Q), pinch-out widens (lower Q). Targets the hovered/selected
296 // band rather than the gesture position — the platform reports the gesture
297 // center in window coords, which a nested/offset view cannot hit-test.
298 void on_gesture_event(const GestureEvent& event) override;
299 void on_mouse_down(Point pos) override;
300 void on_mouse_up(Point pos) override;
301 void on_mouse_drag(Point pos) override;
302 // Passive hover (no button) arrives here on real hosts, NOT through
303 // on_mouse_event — so the hover highlight + readout must track it here.
304 void on_hover_move(Point local_pos) override;
305 void on_mouse_leave() override;
306 bool wants_mouse_input() const override {
307 return true;
308 }
309
310 private:
311 std::vector<Band> bands_;
312 float min_freq_ = 20.0f;
313 float max_freq_ = 20000.0f;
314 float min_db_ = -24.0f;
315 float max_db_ = 24.0f;
316 float sample_rate_ = 48000.0f;
317 bool show_grid_ = true;
318 bool show_band_curves_ = true;
319 bool show_disabled_handles_ = false;
320 bool show_labels_ = true;
321 bool show_readout_ = true;
322 float content_top_pad_ = 0.0f; // empty space reserved at the top (px)
323 bool hover_animation_ = false; // ease handle radius toward target (opt-in)
324 bool hover_animating_ = false; // any handle mid-transition this frame
325 std::vector<float> handle_radius_; // per-band eased radius (-1 = uninit)
326 std::vector<float> spectrum_;
327 // Persistent temporally-smoothed spectrum (fast attack / slow release),
328 // updated per frame in set_spectrum() and drawn from in paint() so the
329 // analyzer flows instead of flickering.
330 std::vector<float> spectrum_smoothed_;
331 int selected_band_ = -1;
332 int dragging_band_ = -1;
333 int hovered_band_ = -1;
334 // A double-click both resets a band AND is followed by a drag-start press
335 // from the platform; this suppresses that one spurious on_mouse_down.
336 bool suppress_next_down_ = false;
337 Point drag_start_{};
338
339 // Coefficients of the enabled bands, rebuilt only when a band or the
340 // sample rate changes — not per frame, and never per curve point.
341 std::vector<signal::BiquadCoefficients> coefficients_;
342 // Scratch buffers for the drawn curves. Members so paint() reuses the
343 // allocation across frames instead of allocating on every repaint.
344 mutable std::vector<float> curve_db_;
345 mutable std::vector<float> spectrum_db_;
346 mutable std::vector<float> band_db_;
347 mutable std::vector<canvas::Canvas::Point2D> fill_poly_;
348
349 // Analyzer dBFS scale (full plot height), independent of the ±gain axis.
350 float analyzer_top_db_ = 0.0f;
351 float analyzer_bottom_db_ = -60.0f;
352
353 // Analyzer state. Enabled (analyzer-capable) by default; the drag-to-zoom
354 // gutter is active whenever the analyzer is used.
355 bool analyzer_enabled_ = true;
356 bool analyzer_scale_draggable_ = true;
357 // Left dB-gutter drag-to-zoom transient state.
358 bool gutter_hover_ = false; // pointer over the gutter (affordance)
359 bool analyzer_drag_active_ = false;
360 float analyzer_drag_start_y_ = 0.0f;
361 float analyzer_drag_start_top_ = 0.0f;
362 float analyzer_drag_start_bottom_ = 0.0f;
363
364 // ── StateStore binding state ────────────────────────────────────────────
365 pulp::state::StateStore* store_ = nullptr;
366 std::vector<BandParamIds> band_params_;
367 bool record_gestures_ = true;
368 // True while a Direction-A write is in flight, so the bind_bands listener
369 // ignores our own echo (the host adapter suppresses its own separately).
370 bool writing_to_store_ = false;
371 // Param ids whose gesture is open for the active dot drag (0 = none).
372 pulp::state::ParamID drag_freq_gesture_ = 0;
373 pulp::state::ParamID drag_gain_gesture_ = 0;
374
375 void rebuild_coefficients();
376 float freq_to_x(float freq) const;
377 float x_to_freq(float x) const;
378 float db_to_y(float db) const;
379 float y_to_db(float y) const;
380 int hit_test_band(Point pos) const;
381 // True when `pos` falls in the narrow left dB-scale gutter (the drag-to-zoom
382 // zone over the dB labels), within the plotting height.
383 bool point_in_analyzer_gutter(Point pos) const;
384
385 // StateStore helpers. store_set brackets the write with the echo guard;
386 // store_begin/store_end forward gesture boundaries (no value write).
387 void store_begin(pulp::state::ParamID id);
388 void store_set(pulp::state::ParamID id, float value);
389 void store_end(pulp::state::ParamID id);
390 // Direction B: apply a host-originated parameter change to the matching band.
391 void apply_param_from_host(pulp::state::ParamID id, float value);
392
393 // Listener subscriptions for Direction B. Declared LAST so they are torn
394 // down FIRST — the listener callback reads bands_ / band_params_, so it must
395 // be removed before those members are destroyed.
396 std::vector<pulp::state::ListenerToken> param_listener_tokens_;
397};
398
399} // namespace pulp::view
Definition canvas.hpp:240
Centralized parameter storage with lock-free value access.
Definition store.hpp:164
void add_band(Band band)
bool hover_animation() const
Definition eq_curve_view.hpp:166
static Color band_palette_color(size_t index)
bool show_readout() const
Definition eq_curve_view.hpp:132
void on_gesture_event(const GestureEvent &event) override
Gesture event (pinch/rotate from multi-touch or trackpad).
void on_mouse_up(Point pos) override
void set_gain_range(float min_db, float max_db)
bool analyzer_animating() const
Definition eq_curve_view.hpp:197
float min_gain() const
Definition eq_curve_view.hpp:64
void on_hover_move(Point local_pos) override
Pointer moved over this view with no button held, reported in this view's LOCAL coordinates.
void set_show_disabled_handles(bool show)
Definition eq_curve_view.hpp:112
DecibelScale gain_scale() const
void set_show_readout(bool show)
Definition eq_curve_view.hpp:129
LogFrequencyScale frequency_scale() const
void bind_bands(pulp::state::StateStore &store, std::span< const BandParamIds > ids)
void on_mouse_drag(Point pos) override
void set_show_labels(bool show)
Definition eq_curve_view.hpp:120
std::function< void(size_t band_index)> on_band_selected
Definition eq_curve_view.hpp:276
FilterType
Definition eq_curve_view.hpp:26
@ peak
Definition eq_curve_view.hpp:26
@ band_pass
Definition eq_curve_view.hpp:26
@ high_pass
Definition eq_curve_view.hpp:26
@ high_shelf
Definition eq_curve_view.hpp:26
@ notch
Definition eq_curve_view.hpp:26
@ low_shelf
Definition eq_curve_view.hpp:26
@ low_pass
Definition eq_curve_view.hpp:26
float sample_rate() const
Definition eq_curve_view.hpp:76
void set_record_gestures(bool on)
Definition eq_curve_view.hpp:267
void set_analyzer_scale_draggable(bool on)
Definition eq_curve_view.hpp:207
void on_mouse_down(Point pos) override
void set_frequency_range(float min_hz, float max_hz)
void on_mouse_event(const MouseEvent &event) override
Mouse down with full event context.
void set_bands(std::vector< Band > bands)
std::function< void(size_t band_index, Band band)> on_band_changed
Definition eq_curve_view.hpp:275
void set_band(size_t index, const Band &band)
float analyzer_top_db() const
Definition eq_curve_view.hpp:225
void on_mouse_leave() override
float min_frequency() const
Definition eq_curve_view.hpp:58
void set_sample_rate(float sample_rate)
void set_show_grid(bool show)
Definition eq_curve_view.hpp:92
int hit_test_handle(Point pos) const
Definition eq_curve_view.hpp:154
float content_top_padding() const
Definition eq_curve_view.hpp:146
const std::vector< Band > & bands() const
Definition eq_curve_view.hpp:45
void paint(canvas::Canvas &canvas) override
void remove_band(size_t index)
void set_analyzer_enabled(bool on)
bool analyzer_scale_draggable() const
Definition eq_curve_view.hpp:210
bool record_gestures() const
Definition eq_curve_view.hpp:270
bool analyzer_enabled() const
Definition eq_curve_view.hpp:189
void set_spectrum(const float *magnitudes_db, size_t bin_count)
float max_gain() const
Definition eq_curve_view.hpp:67
bool show_labels() const
Definition eq_curve_view.hpp:123
size_t band_count() const
Definition eq_curve_view.hpp:51
void set_analyzer_range(float top_db, float bottom_db)
Definition eq_curve_view.hpp:221
void set_selected_band(int index)
Definition eq_curve_view.hpp:282
float analyzer_bottom_db() const
Definition eq_curve_view.hpp:228
bool hover_animating() const
Definition eq_curve_view.hpp:171
void set_content_top_padding(float px)
Definition eq_curve_view.hpp:143
float max_frequency() const
Definition eq_curve_view.hpp:61
int hovered_band() const
Definition eq_curve_view.hpp:288
void set_hover_animation(bool on)
Definition eq_curve_view.hpp:163
bool show_grid() const
Definition eq_curve_view.hpp:95
bool wants_mouse_input() const override
True if this widget handles mouse down/drag through virtual methods even when it does not expose JS p...
Definition eq_curve_view.hpp:306
float magnitude_db_at(float freq_hz) const
void set_show_band_curves(bool show)
Definition eq_curve_view.hpp:101
float analyzer_db_to_y(float dbfs) const
bool show_disabled_handles() const
Definition eq_curve_view.hpp:115
bool show_band_curves() const
Definition eq_curve_view.hpp:104
int selected_band() const
Definition eq_curve_view.hpp:279
Shared axis mapping for frequency-domain plots.
Definition affine_transform.hpp:24
uint32_t ParamID
Unique parameter identifier, stable across plugin versions.
Definition parameter.hpp:55
Hover-cursor tracking for platform window hosts.
Definition fwd.hpp:27
@ px
Absolute pixels.
Definition geometry.hpp:393
canvas::Color Color
Definition theme.hpp:12
@ value
the widget's primary value (Knob, Fader, RangeSlider, Toggle, ProgressBar)
Definition binding_diagnostics.hpp:19
@ y
Definition input_events.hpp:42
@ x
Definition input_events.hpp:41
@ on
Definition accessibility.hpp:132
Maps decibels to a vertical pixel position on a linear axis (dB is already logarithmic in amplitude,...
Definition graph_scale.hpp:55
Definition eq_curve_view.hpp:240
pulp::state::ParamID gain
Definition eq_curve_view.hpp:242
pulp::state::ParamID freq
Definition eq_curve_view.hpp:241
pulp::state::ParamID q
Definition eq_curve_view.hpp:243
Definition eq_curve_view.hpp:28
bool enabled
Definition eq_curve_view.hpp:33
float q
Definition eq_curve_view.hpp:31
float frequency
Definition eq_curve_view.hpp:29
float gain_db
Definition eq_curve_view.hpp:30
Color color
Definition eq_curve_view.hpp:38
FilterType type
Definition eq_curve_view.hpp:32
High-level gesture event for pinch (scale) and rotate.
Definition input_events.hpp:181
Maps frequency (Hz) to a horizontal pixel position on a logarithmic axis.
Definition graph_scale.hpp:23
Rich mouse/pointer event with position, modifiers, button state, pointer ID, and stylus properties.
Definition input_events.hpp:104
Definition geometry.hpp:20