// keyboard_strip.cpp — see keyboard_strip.h. Pure math; no host types. #include "core/instrument/ui/keyboard_strip.h" #include 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