// 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 { // Pitch class of each natural, and the count of naturals strictly below each pitch class. constexpr int kNaturalPitchClass[7] = {0, 2, 4, 5, 7, 9, 11}; constexpr int kNaturalsBelowPc[12] = {0, 1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 6}; int clampNote(int n) { if (n < 0) return 0; if (n > kStripKeyCount - 1) return kStripKeyCount - 1; return n; } // The MIDI note of white key `index` (0..74). int whiteNoteAt(int index) { const int i = index < 0 ? 0 : (index > kStripWhiteKeyCount - 1 ? kStripWhiteKeyCount - 1 : index); return clampNote((i / 7) * 12 + kNaturalPitchClass[i % 7]); } // The black key straddling white-key boundary `b`, or -1 where the scale has none (E-F and // B-C are adjacent naturals). int blackNoteAtBoundary(int b) { if (b <= 0 || b >= kStripWhiteKeyCount) return -1; const int below = whiteNoteAt(b) - 1; return (below >= 0 && !isNaturalKey(below)) ? below : -1; } } // namespace StripLayout layoutStrip(int w, int h) { const int cw = std::max(0, w); const int ch = std::max(0, h); StripLayout out; out.band = Rect::ltrb(0, 0, cw, ch); if (ch <= 0) return out; out.whiteWidth = cw / kStripWhiteKeyCount; if (out.whiteWidth <= 0) return out; // narrower than one pixel per white key const int keysW = out.whiteWidth * kStripWhiteKeyCount; const int left = (cw - keysW) / 2; // residue split evenly into the two end margins out.keys = Rect::ltrb(left, 0, left + keysW, ch); out.blackWidth = std::max(1, (out.whiteWidth * 3) / 5); out.blackHeight = std::max(1, (ch * 3) / 5); return out; } bool isNaturalKey(int note) { const int n = clampNote(note); 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 whiteIndexOf(int note) { const int n = clampNote(note); return (n / 12) * 7 + kNaturalsBelowPc[n % 12]; } Rect keyRect(const StripLayout& layout, int note) { if (layout.keys.empty()) return Rect{}; const int n = clampNote(note); const int wi = whiteIndexOf(n); if (isNaturalKey(n)) { const int left = layout.keys.x + wi * layout.whiteWidth; return Rect::ltrb(left, layout.keys.y, left + layout.whiteWidth, layout.keys.bottom()); } const int centre = layout.keys.x + wi * layout.whiteWidth; const int left = centre - layout.blackWidth / 2; return Rect::ltrb(left, layout.keys.y, left + layout.blackWidth, layout.keys.y + layout.blackHeight); } Rect rootMarkerRect(const StripLayout& layout, int rootNote) { return keyRect(layout, rootNote); } int keyAtPoint(const StripLayout& layout, int x, int y) { if (!contains(layout.keys, x, y)) return -1; const int offset = x - layout.keys.x; const int wi = std::min(offset / layout.whiteWidth, kStripWhiteKeyCount - 1); if (y < layout.keys.y + layout.blackHeight) { // Only the two boundaries flanking this white key can carry an overlapping black. const int candidates[2] = {wi, wi + 1}; for (const int b : candidates) { const int note = blackNoteAtBoundary(b); if (note >= 0 && contains(keyRect(layout, note), x, y)) return note; } } return whiteNoteAt(wi); } int resolveDragNote(const StripLayout& layout, int x, int y) { if (layout.keys.empty()) return -1; const int cx = std::clamp(x, layout.keys.x, layout.keys.right() - 1); const int cy = std::clamp(y, layout.keys.y, layout.keys.bottom() - 1); return keyAtPoint(layout, cx, cy); } std::string noteName(int note) { static constexpr const char* kNames[12] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}; const int n = clampNote(note); return std::string(kNames[n % 12]) + std::to_string(n / 12 - 1); } } // namespace reasampler::instrument::ui