// waveform_view.cpp — see waveform_view.h. Pure math; no host types. #include "waveform_view.h" #include #include // std::abs (int overload) namespace reasampler::vst { namespace { std::int64_t clampFrame(std::int64_t f, std::int64_t frameCount) { if (f < 0) return 0; if (f > frameCount) return frameCount; return f; } } // namespace int frameToX(const Rect& area, std::int64_t frameCount, std::int64_t frame) { const int w = std::max(0, area.width()); if (frameCount <= 0 || w <= 0) return area.left; const std::int64_t f = clampFrame(frame, frameCount); // Linear map: x = left + round(f * w / frameCount). Rounding keeps the marker line // visually centered on its frame; the divide is exact rational (multiply first). const std::int64_t num = f * static_cast(w) + frameCount / 2; return area.left + static_cast(num / frameCount); } std::int64_t xToFrame(const Rect& area, std::int64_t frameCount, int x) { const int w = std::max(0, area.width()); if (frameCount <= 0 || w <= 0) return 0; if (x <= area.left) return 0; if (x >= area.right) return frameCount; const std::int64_t dx = static_cast(x - area.left); // Inverse of frameToX: frame = round(dx * frameCount / w). Round so click and marker draw // agree at bin granularity. const std::int64_t num = dx * frameCount + static_cast(w) / 2; return clampFrame(num / static_cast(w), frameCount); } int markerAtPoint(const Rect& area, std::int64_t frameCount, const std::int64_t* frames, int count, int x, int y) { if (count <= 0 || frames == nullptr) return -1; if (!contains(area, x, y)) return -1; for (int i = 0; i < count; ++i) { const int mx = frameToX(area, frameCount, frames[i]); if (x >= mx - kMarkerGrabWidth && x <= mx + kMarkerGrabWidth) return i; } return -1; } std::int64_t resolveDragFrame(const Rect& area, std::int64_t frameCount, std::int64_t startFrame, int dxPixels) { const std::int64_t start = clampFrame(startFrame, frameCount); if (dxPixels == 0) return start; const int w = std::max(0, area.width()); if (frameCount <= 0 || w <= 0) return start; // no room to move // Proportional shift, rounded to the nearest frame (same linear map as frameToX/xToFrame). const std::int64_t magnitude = (static_cast(std::abs(dxPixels)) * frameCount + static_cast(w) / 2) / static_cast(w); const std::int64_t shift = dxPixels > 0 ? magnitude : -magnitude; return clampFrame(start + shift, frameCount); } std::int64_t nearestZeroCrossing(const AudioSample* pcm, std::int64_t frames, std::int64_t target) { if (pcm == nullptr || frames < 2) return clampFrame(target, frames > 0 ? frames - 1 : 0); // Clamp target into a valid sample index [0, frames). std::int64_t t = target; if (t < 0) t = 0; if (t > frames - 1) t = frames - 1; // A crossing lives at frame i (1 <= i < frames) when sign(pcm[i-1]) != sign(pcm[i]) OR // pcm[i] == 0. isCrossing(i) tests exactly that. We fan out from t: at each distance d we // probe t-d before t+d, so an equidistant tie resolves to the LOWER frame (deterministic). auto isCrossing = [&](std::int64_t i) -> bool { if (i < 1 || i >= frames) return false; const AudioSample a = pcm[i - 1]; const AudioSample b = pcm[i]; if (b == 0.0f) return true; // a sample on zero is its own crossing return (a < 0.0f) != (b < 0.0f); // sign change between i-1 and i }; if (isCrossing(t)) return t; for (std::int64_t d = 1; d < frames; ++d) { const std::int64_t lo = t - d; if (lo >= 1 && isCrossing(lo)) return lo; // lower side wins the tie const std::int64_t hi = t + d; if (hi < frames && isCrossing(hi)) return hi; // Stop once both probes have run off both ends — no crossing anywhere. if (lo < 1 && hi >= frames) break; } return t; // no sign change in the whole buffer -> keep the raw (clamped) target } } // namespace reasampler::vst