Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
waveform_editor.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <pulp/view/view.hpp>
10#include <vector>
11#include <functional>
12#include <algorithm>
13#include <cmath>
14
15namespace pulp::view {
16
19 int start_sample = 0;
20 int end_sample = 0;
21 std::string label;
23
24 int length() const { return end_sample - start_sample; }
25};
26
43class WaveformEditor : public View {
44public:
46
48 void set_audio_data(const float* data, int num_samples, float sample_rate) {
49 if (!data || num_samples <= 0) {
50 audio_data_.clear();
51 sample_rate_ = sample_rate;
52 total_samples_ = 0;
53 viewport_.set_total_samples(0);
54 clamp_editor_state_to_audio_bounds();
55 return;
56 }
57
58 audio_data_.assign(data, data + num_samples);
59 sample_rate_ = sample_rate;
60 total_samples_ = num_samples;
61 viewport_.set_total_samples(num_samples);
62 viewport_.set_visible_range(0, num_samples);
63 clamp_editor_state_to_audio_bounds();
64 }
65
66 int total_samples() const { return total_samples_; }
67 float sample_rate() const { return sample_rate_; }
68
69 // ── Selection ────────────────────────────────────────────────────────
70
71 void set_selection(int start, int end) {
72 set_selection_impl(start, end, true);
73 dragging_ = false;
74 }
75
77 bool has_selection() const { return selection_start_ != selection_end_; }
78 int selection_start() const { return selection_start_; }
79 int selection_end() const { return selection_end_; }
80 int selection_length() const { return selection_end() - selection_start(); }
81
82 std::function<void(int start, int end)> on_selection_changed;
83
84 // ── Zoom and scroll ──────────────────────────────────────────────────
85
87 void set_visible_range(int start, int length) {
88 viewport_.set_total_samples(total_samples_);
89 viewport_.set_visible_range(start, length);
90 }
91
92 int visible_start() const { return static_cast<int>(viewport_.visible_start); }
93 int visible_length() const { return static_cast<int>(viewport_.visible_length); }
94
96 void zoom_in(float factor = 2.0f) { viewport_.zoom_in(factor); }
97
99 void zoom_out(float factor = 2.0f) { viewport_.zoom_out(factor); }
100
102 void zoom_to_fit() { viewport_.zoom_to_fit(); }
103
108
110 void scroll(int delta_samples) { viewport_.scroll(delta_samples); }
111
112 // ── Playhead ─────────────────────────────────────────────────────────
113
114 void set_playhead(int sample) { playhead_ = std::clamp(sample, 0, total_samples_); }
115 int playhead() const { return playhead_; }
116
117 // ── Regions ──────────────────────────────────────────────────────────
118
119 void add_region(WaveformRegion region) { regions_.push_back(std::move(region)); }
120 void clear_regions() { regions_.clear(); }
121 const std::vector<WaveformRegion>& regions() const { return regions_; }
122
123 // ── Painting ─────────────────────────────────────────────────────────
124
125 void paint(canvas::Canvas& canvas) override {
126 auto b = local_bounds();
127 if (audio_data_.empty() || b.is_empty()) return;
128 const auto viewport = viewport_for_bounds(b);
129
130 // Background
131 canvas.set_fill_color(resolve_color("waveform_bg",
132 canvas::Color::hex(0x0a0a1a)));
133 canvas.fill_rect(b.x, b.y, b.width, b.height);
134
135 float center_y = b.y + b.height / 2;
136 float half_h = b.height / 2 - 2;
137
138 // Draw regions
139 for (const auto& region : regions_) {
140 float rx = sample_to_x(region.start_sample, b);
141 float rw = sample_to_x(region.end_sample, b) - rx;
142 canvas.set_fill_color(region.color);
143 canvas.fill_rect(rx, b.y, rw, b.height);
144 }
145
146 // Draw selection
147 if (has_selection()) {
148 float sx = sample_to_x(selection_start(), b);
149 float sw = sample_to_x(selection_end(), b) - sx;
150 canvas.set_fill_color(resolve_color("selection",
151 canvas::Color::rgba8(65, 105, 225, 60)));
152 canvas.fill_rect(sx, b.y, sw, b.height);
153 }
154
155 // Draw waveform (min/max per pixel)
156 canvas.set_stroke_color(resolve_color("waveform",
157 canvas::Color::hex(0x4a9eff)));
158 canvas.set_line_width(1);
159
160 float samples_per_pixel = static_cast<float>(viewport.samples_per_pixel());
161 for (int px = 0; px < static_cast<int>(b.width); ++px) {
162 int s0 = static_cast<int>(viewport.visible_start + static_cast<int64_t>(static_cast<float>(px) * samples_per_pixel));
163 int s1 = static_cast<int>(viewport.visible_start + static_cast<int64_t>(static_cast<float>(px + 1) * samples_per_pixel));
164 s0 = std::clamp(s0, 0, total_samples_ - 1);
165 s1 = std::clamp(s1, s0 + 1, total_samples_);
166
167 float min_val = 0, max_val = 0;
168 for (int s = s0; s < s1; ++s) {
169 float v = audio_data_[static_cast<size_t>(s)];
170 min_val = std::min(min_val, v);
171 max_val = std::max(max_val, v);
172 }
173
174 float x = b.x + static_cast<float>(px);
175 float y_top = center_y - max_val * half_h;
176 float y_bot = center_y - min_val * half_h;
177 canvas.stroke_line(x, y_top, x, y_bot);
178 }
179
180 // Center line
181 canvas.set_stroke_color(resolve_color("waveform_center",
182 canvas::Color::rgba8(255, 255, 255, 30)));
183 canvas.stroke_line(b.x, center_y, b.x + b.width, center_y);
184
185 // Playhead
186 const auto playhead = build_waveform_playhead_overlay(viewport, playhead_);
187 if (playhead.visible) {
188 canvas.set_stroke_color(resolve_color("playhead",
189 canvas::Color::hex(0xff4444)));
190 canvas.set_line_width(1.5f);
191 canvas.stroke_line(playhead.x, b.y, playhead.x, b.y + b.height);
192 }
193 }
194
195 // ── Events ───────────────────────────────────────────────────────────
196
197 void on_mouse_event(const MouseEvent& event) override {
198 if (event.is_cancelled) {
199 dragging_ = false;
200 return;
201 }
202
203 const bool explicit_phase = event.hasExplicitPhase();
204 const bool is_press = explicit_phase ? event.isPress() : (!dragging_ && event.is_down);
205 const bool is_drag = explicit_phase ? event.isDrag() : (dragging_ && !event.is_down);
206 const bool is_release = explicit_phase ? event.isRelease() : (dragging_ && event.is_down);
207
208 if (!is_press && !is_drag && !is_release) return;
209
210 auto b = local_bounds();
211 int sample = x_to_sample(event.position.x, b);
212
213 if (is_press) {
214 drag_anchor_ = (event.isShiftDown() && has_selection()) ? selection_start() : sample;
215 set_selection_impl(drag_anchor_, sample, true);
216 dragging_ = true;
217 } else if (is_drag && dragging_) {
218 set_selection_impl(drag_anchor_, sample, true);
219 } else if (is_release && dragging_) {
220 set_selection_impl(drag_anchor_, sample, true);
221 dragging_ = false;
222 }
223 }
224
225 bool on_key_event(const KeyEvent& event) override {
226 if (!event.is_down) return false;
227
228 if (event.isMainModifier()) {
229 if (event.key == KeyCode::a) { set_selection(0, total_samples_); return true; }
230 }
231
232 if (event.key == KeyCode::left) { scroll(-visible_length() / 10); return true; }
233 if (event.key == KeyCode::right) { scroll(visible_length() / 10); return true; }
234
235 // Main-modifier + 0 zooms to fit.
236 if (event.key == KeyCode::num0 && event.isMainModifier()) { zoom_to_fit(); return true; }
237
238 return false;
239 }
240
241 bool wants_mouse_input() const override { return true; }
242
243private:
244 std::vector<float> audio_data_;
245 float sample_rate_ = 44100.0f;
246 int total_samples_ = 0;
247 WaveformViewport viewport_;
248 int selection_start_ = 0;
249 int selection_end_ = 0;
250 int playhead_ = 0;
251 int drag_anchor_ = 0;
252 bool dragging_ = false;
253 std::vector<WaveformRegion> regions_;
254
255 void set_selection_impl(int start, int end, bool notify) {
256 start = std::clamp(start, 0, total_samples_);
257 end = std::clamp(end, 0, total_samples_);
258 if (end < start) std::swap(start, end);
259
260 const bool changed = start != selection_start_ || end != selection_end_;
261 selection_start_ = start;
262 selection_end_ = end;
263 if (notify && changed && on_selection_changed) {
264 on_selection_changed(selection_start_, selection_end_);
265 }
266 }
267
268 void clamp_editor_state_to_audio_bounds() {
269 set_selection_impl(selection_start_, selection_end_, false);
270 playhead_ = std::clamp(playhead_, 0, total_samples_);
271 drag_anchor_ = std::clamp(drag_anchor_, 0, total_samples_);
272 dragging_ = false;
273 for (auto& region : regions_) {
274 region.start_sample = std::clamp(region.start_sample, 0, total_samples_);
275 region.end_sample = std::clamp(region.end_sample, 0, total_samples_);
276 }
277 }
278
279protected:
280 // Viewport-aware sample<->pixel mapping, shared with subclasses so custom
281 // overlays (playheads, hit-testing, drag targets) stay aligned with the
282 // rendered waveform under zoom/scroll.
284 auto viewport = viewport_;
285 viewport.set_bounds(b);
286 return viewport;
287 }
288
289 float sample_to_x(int sample, const Rect& b) const {
290 return viewport_for_bounds(b).sample_to_x(sample);
291 }
292
293 int x_to_sample(float x, const Rect& b) const {
294 return static_cast<int>(viewport_for_bounds(b).x_to_sample(x));
295 }
296};
297
298} // namespace pulp::view
Definition canvas.hpp:240
Color resolve_color(const std::string &name, Color fallback={}) const
void set_focusable(bool f)
Definition view.hpp:669
Rect local_bounds() const
Definition view.hpp:88
int x_to_sample(float x, const Rect &b) const
Definition waveform_editor.hpp:293
void zoom_in(float factor=2.0f)
Zoom in by a factor (centered on the view midpoint).
Definition waveform_editor.hpp:96
bool has_selection() const
Definition waveform_editor.hpp:77
bool on_key_event(const KeyEvent &event) override
Key event with modifiers and up/down state.
Definition waveform_editor.hpp:225
int visible_start() const
Definition waveform_editor.hpp:92
std::function< void(int start, int end)> on_selection_changed
Definition waveform_editor.hpp:82
void set_playhead(int sample)
Definition waveform_editor.hpp:114
void zoom_to_fit()
Zoom to fit all audio data.
Definition waveform_editor.hpp:102
const std::vector< WaveformRegion > & regions() const
Definition waveform_editor.hpp:121
int selection_start() const
Definition waveform_editor.hpp:78
void set_audio_data(const float *data, int num_samples, float sample_rate)
Set mono audio data to display.
Definition waveform_editor.hpp:48
int total_samples() const
Definition waveform_editor.hpp:66
void zoom_out(float factor=2.0f)
Zoom out by a factor.
Definition waveform_editor.hpp:99
void set_visible_range(int start, int length)
Set the visible range (in samples).
Definition waveform_editor.hpp:87
void set_selection(int start, int end)
Definition waveform_editor.hpp:71
void scroll(int delta_samples)
Scroll by a number of samples.
Definition waveform_editor.hpp:110
void paint(canvas::Canvas &canvas) override
Definition waveform_editor.hpp:125
void clear_regions()
Definition waveform_editor.hpp:120
float sample_rate() const
Definition waveform_editor.hpp:67
int selection_length() const
Definition waveform_editor.hpp:80
float sample_to_x(int sample, const Rect &b) const
Definition waveform_editor.hpp:289
void add_region(WaveformRegion region)
Definition waveform_editor.hpp:119
WaveformEditor()
Definition waveform_editor.hpp:45
void on_mouse_event(const MouseEvent &event) override
Mouse down with full event context.
Definition waveform_editor.hpp:197
int visible_length() const
Definition waveform_editor.hpp:93
WaveformViewport viewport_for_bounds(const Rect &b) const
Definition waveform_editor.hpp:283
int selection_end() const
Definition waveform_editor.hpp:79
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 waveform_editor.hpp:241
int playhead() const
Definition waveform_editor.hpp:115
void zoom_to_selection()
Zoom to selection.
Definition waveform_editor.hpp:105
void clear_selection()
Definition waveform_editor.hpp:76
Input event types with modifier keys and pointer IDs.
Definition affine_transform.hpp:24
Hover-cursor tracking for platform window hosts.
Definition fwd.hpp:27
WaveformPlayheadOverlay build_waveform_playhead_overlay(const WaveformViewport &viewport, int64_t playhead_sample)
@ px
Absolute pixels.
Definition geometry.hpp:393
@ s
Definition input_events.hpp:41
@ a
Definition input_events.hpp:39
@ right
Definition input_events.hpp:53
@ left
Definition input_events.hpp:53
@ num0
Definition input_events.hpp:45
@ b
Definition input_events.hpp:39
@ x
Definition input_events.hpp:41
@ v
Definition input_events.hpp:41
@ end
Definition geometry.hpp:370
@ start
Definition geometry.hpp:370
@ changed
Definition gesture.hpp:17
Definition canvas.hpp:52
static Color hex(uint32_t rgb)
Construct from hex 0xRRGGBB (alpha = 1.0).
Definition canvas.hpp:66
static constexpr Color rgba8(uint8_t r, uint8_t g, uint8_t b, uint8_t a=255)
Construct from 8-bit channels [0,255] — convenience for legacy/hex code.
Definition canvas.hpp:61
Rich key event with key code, modifiers, and repeat state.
Definition input_events.hpp:193
bool isMainModifier() const
Definition input_events.hpp:203
KeyCode key
Definition input_events.hpp:194
bool is_down
True for key-down, false for key-up.
Definition input_events.hpp:196
Rich mouse/pointer event with position, modifiers, button state, pointer ID, and stylus properties.
Definition input_events.hpp:104
bool is_cancelled
True for touchesCancelled (→ pointercancel).
Definition input_events.hpp:113
Point position
Position in local view coordinates.
Definition input_events.hpp:105
float x
Definition geometry.hpp:21
Axis-aligned rectangle in continuous (float) coordinates.
Definition geometry.hpp:52
A region (selection or marker) within the waveform.
Definition waveform_editor.hpp:18
std::string label
Definition waveform_editor.hpp:21
canvas::Color color
Definition waveform_editor.hpp:22
int length() const
Definition waveform_editor.hpp:24
int end_sample
End sample index (exclusive).
Definition waveform_editor.hpp:20
int start_sample
Start sample index.
Definition waveform_editor.hpp:19
Immutable-for-paint viewport state and sample/pixel conversion rules.
Definition waveform_editor_primitives.hpp:26
float sample_to_x(int64_t sample) const
int64_t x_to_sample(float x) const
Reusable waveform editor geometry, snapping, hit-testing, and transaction helpers.