149 lines
5.9 KiB
C++
149 lines
5.9 KiB
C++
// keyboard_strip.cpp — see keyboard_strip.h. Pure math; no host types.
|
|
|
|
#include "core/instrument/ui/keyboard_strip.h"
|
|
|
|
#include <algorithm>
|
|
|
|
namespace reasampler::instrument::ui {
|
|
|
|
namespace {
|
|
|
|
int clampNote(int n) {
|
|
if (n < 0) return 0;
|
|
if (n > kStripKeyCount - 1) return kStripKeyCount - 1;
|
|
return n;
|
|
}
|
|
|
|
// Map a key BOUNDARY in [0, kStripKeyCount] to an x pixel inside a band of the given
|
|
// left/width. keyEdge is a boundary (0..128): 0 -> band left, 128 -> band right. Integer
|
|
// math, floored — key N's left is keyEdgeToX(N) and its right is keyEdgeToX(N+1), tiling
|
|
// adjacent keys/zones without a seam (mirror of embed_strip::keyEdgeToX).
|
|
int keyEdgeToX(int bandLeft, int bandWidth, int keyEdge) {
|
|
if (keyEdge <= 0) return bandLeft;
|
|
if (keyEdge >= kStripKeyCount) return bandLeft + bandWidth;
|
|
return bandLeft + (keyEdge * bandWidth) / kStripKeyCount;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
StripLayout layoutStrip(int w, int h) {
|
|
const int cw = std::max(0, w);
|
|
const int ch = std::max(0, h);
|
|
StripLayout out;
|
|
out.keys = Rect::ltrb(0, 0, cw, ch);
|
|
return out;
|
|
}
|
|
|
|
int keyLeftX(const StripLayout& layout, int note) {
|
|
const Rect& band = layout.keys;
|
|
const int bandWidth = std::max(0, band.width);
|
|
// note is a KEY here (0..127); its left edge is boundary `note`. Callers pass note+1 to
|
|
// get a key's right edge, and 128 maps to the band right.
|
|
const int edge = note < 0 ? 0 : (note > kStripKeyCount ? kStripKeyCount : note);
|
|
return keyEdgeToX(band.x, bandWidth, edge);
|
|
}
|
|
|
|
Rect keyRect(const StripLayout& layout, int note) {
|
|
const int n = clampNote(note);
|
|
const int leftX = keyLeftX(layout, n);
|
|
const int rightX = keyLeftX(layout, n + 1);
|
|
return Rect::ltrb(leftX, layout.keys.y, std::max(leftX, rightX), layout.keys.bottom());
|
|
}
|
|
|
|
Rect rootMarkerRect(const StripLayout& layout, int rootNote) {
|
|
return keyRect(layout, rootNote);
|
|
}
|
|
|
|
int keyAtPoint(const StripLayout& layout, int x, int y) {
|
|
const Rect& band = layout.keys;
|
|
if (!contains(band, x, y)) return -1;
|
|
const int bandWidth = std::max(0, band.width);
|
|
if (bandWidth <= 0) return -1;
|
|
// Invert keyEdgeToX: the key whose half-open [leftX, rightX) contains x. Floor-divide
|
|
// the pixel offset back to a key; clamp defensively (a point on band.right()-1 maps to 127).
|
|
const int offset = x - band.x;
|
|
int note = (offset * kStripKeyCount) / bandWidth;
|
|
return clampNote(note);
|
|
}
|
|
|
|
Rect zoneBarRect(const StripLayout& layout, int lowNote, int highNote) {
|
|
int lo = clampNote(lowNote);
|
|
int hi = clampNote(highNote);
|
|
if (lo > hi) lo = hi; // defensive: a malformed zone collapses rather than inverts
|
|
const int leftX = keyLeftX(layout, lo);
|
|
const int rightX = keyLeftX(layout, hi + 1);
|
|
return Rect::ltrb(leftX, layout.keys.y, std::max(leftX, rightX), layout.keys.bottom());
|
|
}
|
|
|
|
ZoneGrab zoneGrabAt(const StripLayout& layout, int lowNote, int highNote, int x, int y) {
|
|
const Rect bar = zoneBarRect(layout, lowNote, highNote);
|
|
if (!contains(bar, x, y)) return ZoneGrab::kNone;
|
|
|
|
const int barW = bar.width;
|
|
// A narrow bar (< 2*edge) has no body: split at the midpoint, LOW edge wins the tie so
|
|
// a click exactly on the midpoint resizes low (deterministic).
|
|
if (barW < 2 * kStripEdgeGrabWidth) {
|
|
const int mid = bar.x + barW / 2;
|
|
return x <= mid ? ZoneGrab::kLowEdge : ZoneGrab::kHighEdge;
|
|
}
|
|
if (x < bar.x + kStripEdgeGrabWidth) return ZoneGrab::kLowEdge;
|
|
if (x >= bar.right() - kStripEdgeGrabWidth) return ZoneGrab::kHighEdge;
|
|
return ZoneGrab::kBody;
|
|
}
|
|
|
|
ZoneBarHit zoneBarAtPoint(const StripLayout& layout, const int* lows, const int* highs,
|
|
int count, int x, int y) {
|
|
if (count <= 0 || lows == nullptr || highs == nullptr) return ZoneBarHit{};
|
|
if (!contains(layout.keys, x, y)) return ZoneBarHit{};
|
|
for (int i = 0; i < count; ++i) {
|
|
const ZoneGrab g = zoneGrabAt(layout, lows[i], highs[i], x, y);
|
|
if (g != ZoneGrab::kNone) return ZoneBarHit{i, g};
|
|
}
|
|
return ZoneBarHit{}; // on the band but on no bar
|
|
}
|
|
|
|
bool isNaturalKey(int note) {
|
|
// Clamp to the valid MIDI range before indexing.
|
|
const int n = note < 0 ? 0 : (note > kStripKeyCount - 1 ? kStripKeyCount - 1 : note);
|
|
// The 12-semitone pattern of natural (white) keys within an octave, starting at C:
|
|
// positions 0(C) 2(D) 4(E) 5(F) 7(G) 9(A) 11(B) are natural;
|
|
// positions 1(C#) 3(D#) 6(F#) 8(G#) 10(A#) are accidental.
|
|
static constexpr bool kNatural[12] = {
|
|
true, // 0 C
|
|
false, // 1 C#
|
|
true, // 2 D
|
|
false, // 3 D#
|
|
true, // 4 E
|
|
true, // 5 F
|
|
false, // 6 F#
|
|
true, // 7 G
|
|
false, // 8 G#
|
|
true, // 9 A
|
|
false, // 10 A#
|
|
true, // 11 B
|
|
};
|
|
return kNatural[n % 12];
|
|
}
|
|
|
|
int resolveDragNote(const StripLayout& layout, int startNote, int dxPixels) {
|
|
if (dxPixels == 0) return clampNote(startNote);
|
|
const int bandWidth = std::max(0, layout.keys.width);
|
|
if (bandWidth <= 0) return clampNote(startNote); // zero-width -> no motion
|
|
// Proportional shift: same linear mapping as keyAtPoint/keyEdgeToX so click and drag
|
|
// agree across the full strip, even on non-divisible-by-128 widths. The proportional
|
|
// key width is (bandWidth / kStripKeyCount) in exact rational arithmetic; rounding to
|
|
// the nearest key (half-key drag flips at the key centre) is achieved by adding
|
|
// bandWidth/2 to the absolute pixel delta before dividing — identical to the old
|
|
// formula except keyWidth is now derived from the same linear map (exact rational)
|
|
// rather than the truncated-integer bandWidth/128 that caused drift at the far end.
|
|
const int half = bandWidth / 2;
|
|
int shift;
|
|
if (dxPixels > 0) {
|
|
shift = (dxPixels * kStripKeyCount + half) / bandWidth;
|
|
} else {
|
|
shift = -(((-dxPixels) * kStripKeyCount + half) / bandWidth);
|
|
}
|
|
return clampNote(startNote + shift);
|
|
}
|
|
|
|
} // namespace reasampler::instrument::ui
|