S11: waveform view with draggable start/loop markers + zero-crossing snap
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.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
// waveform_view.cpp — see waveform_view.h. Pure math; no host types.
|
||||
|
||||
#include "waveform_view.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib> // 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<std::int64_t>(w) + frameCount / 2;
|
||||
return area.left + static_cast<int>(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<std::int64_t>(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<std::int64_t>(w) / 2;
|
||||
return clampFrame(num / static_cast<std::int64_t>(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::int64_t>(std::abs(dxPixels)) * frameCount +
|
||||
static_cast<std::int64_t>(w) / 2) /
|
||||
static_cast<std::int64_t>(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
|
||||
Reference in New Issue
Block a user