9743547dff
Pure waveform_view module (frame<->pixel markers, zero-crossing snap); per-zone loop/start overrides on PerformanceZone with self-versioning zone payload; core start-point read offset; editor waveform surface with drag machine.
84 lines
5.5 KiB
C++
84 lines
5.5 KiB
C++
// 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 <cstdint>
|
|
|
|
#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
|