// waveform_view.h — PURE waveform/marker geometry + zero-crossing snap for the S11 // waveform surface. NO VST3, NO REAPER, NO SWELL/LICE types at the boundary. The mirror // of keyboard_strip / editor_geometry: the fiddly frame<->pixel + marker hit-test + snap // arithmetic lives here, unit-tested outside the DAW, while the editor shell // (reasampler_editor.cpp) draws the envelope + markers and marshals mouse events into it. // // The surface maps a sample's full frame span [0, frameCount] linearly across a horizontal // waveform rect. Draggable MARKERS mark frames of interest (S11: start point, loop start, // loop end). The marker set is GENERIC — N named markers with drag + snap — deliberately // not three hardcoded specials, so S15 (Trigger/Gate) can repurpose this same surface with a // different marker set (start + %-length end + fades) without reworking the machinery. // // Interaction resolves through the pure DRAG-DELTA resolver here: the shell captures a grab // on WM_LBUTTONDOWN (markerAtPoint identifies the grabbed marker), feeds each WM_MOUSEMOVE's // pixel delta back through resolveDragFrame (which clamps + optionally zero-crossing-snaps), // and commits on WM_LBUTTONUP. Live feedback is the shell re-drawing the in-flight frame. // // It reuses the same Rect + contains() as editor_geometry (one shared geometry idiom), so // this header depends on editor_geometry.h rather than redefining a rectangle type. Audio // is the peaks AudioSample float alias (the one house precedent — sampler_core / wav_trim do // the same), so the zero-crossing helper takes the same mono PCM the shell already decoded. #pragma once #include #include "editor_geometry.h" // Rect, contains — one shared geometry idiom #include "peaks.h" // AudioSample (float), the mono PCM the snap scans namespace reasampler::vst { // The width (px) of a marker's grab region either side of its x line: a grab within this many // pixels of a marker's drawn x is a grab OF that marker. Mirrors keyboard_strip's edge-grab // idiom — wide enough to grab a 1px line comfortably, narrow enough that adjacent markers stay // distinguishable. inline constexpr int kMarkerGrabWidth = 5; // The x pixel (inside `area`) of frame `frame` under the linear map: frame 0 -> area.left, // frame frameCount -> area.right. A frame is clamped to [0, frameCount] before mapping, so an // out-of-range frame pins to an edge rather than escaping the rect. frameCount <= 0 or a // zero-width area pins every frame to area.left (a degenerate, non-inverting result). Pure. int frameToX(const Rect& area, std::int64_t frameCount, std::int64_t frame); // The frame a point x (inside `area`) maps to under the inverse linear map, clamped to // [0, frameCount]. A point left of area.left yields 0; right of area.right yields frameCount. // frameCount <= 0 or a zero-width area yields 0. Pure — the inverse of frameToX (round-trips // to the same frame at bin granularity). std::int64_t xToFrame(const Rect& area, std::int64_t frameCount, int x); // Which marker (index into a caller-supplied parallel `frames` array, in draw order) a grab at // (x, y) lands on, or -1 for a point off every marker (or off the waveform area). A marker is // grabbed when x is within kMarkerGrabWidth of its drawn x AND y is inside `area`. First marker // in order wins a tie where two markers overlap within the grab band (deterministic, mirroring // keyboard_strip's first-match). `frames` is `count` frame indices; a null/empty array or // count <= 0 yields -1. Pure — a raw pointer at the boundary (no host container), like // keyboard_strip::zoneBarAtPoint. int markerAtPoint(const Rect& area, std::int64_t frameCount, const std::int64_t* frames, int count, int x, int y); // Resolve a drag to a new frame. Given the frame the grabbed marker held at grab time // (`startFrame`) and the horizontal pixel delta since grab (`dxPixels`), returns the frame the // marker should now hold: startFrame shifted by round(dxPixels * frameCount / areaWidth), // clamped to [0, frameCount]. A zero-width area or non-positive frameCount pins the result to // the clamped startFrame (no motion). This is the single arithmetic behind every marker drag; // the shell applies clamps BETWEEN markers (start <= loopEnd, loopStart <= loopEnd) after this // per-marker resolve. Pure — rounding is to the nearest frame. Returns the clamped startFrame // for dxPixels == 0. std::int64_t resolveDragFrame(const Rect& area, std::int64_t frameCount, std::int64_t startFrame, int dxPixels); // The nearest zero-crossing frame to `target` in the mono PCM, for the loop/start snap (the // S2 zero-crossing-aware requirement). A zero crossing is a frame index i (1 <= i < frames) // where the sign of pcm[i-1] and pcm[i] differ (a sample exactly 0 counts as its own crossing // — pcm[i] == 0 snaps to i). The search fans out symmetrically from the clamped target and // returns the closest crossing frame; ties (equidistant crossings on both sides) resolve to // the LOWER frame (deterministic). When the PCM has NO sign change anywhere (all one sign, or // fewer than 2 frames), returns the clamped target unchanged (nothing to snap to — the caller // keeps the raw frame). `target` is clamped to [0, frames) before searching. Pure — scans the // decoded PCM the shell already holds; no host types, no file I/O. std::int64_t nearestZeroCrossing(const AudioSample* pcm, std::int64_t frames, std::int64_t target); } // namespace reasampler::vst