Rebuild the chrome band: full-width piano strip with uniform key widths, note tooltips, one toolbar font

This commit is contained in:
2026-07-30 09:14:07 -04:00
parent ea52b14f2a
commit ae23ee0882
14 changed files with 656 additions and 386 deletions
+78 -51
View File
@@ -8,18 +8,29 @@ 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;
}
// Maps a key boundary (0..128) to an x pixel. Key N's left is keyEdgeToX(N), right is
// keyEdgeToX(N+1) — tiles adjacent keys without a seam. Mirrors 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;
// 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
@@ -28,42 +39,22 @@ 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);
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;
}
int keyLeftX(const StripLayout& layout, int note) {
const Rect& band = layout.keys;
const int bandWidth = std::max(0, band.width);
// note is a key (0..127); callers pass note+1 to get its right edge, 128 -> 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;
// Inverts keyEdgeToX: the key whose half-open [leftX, rightX) contains x.
const int offset = x - band.x;
int note = (offset * kStripKeyCount) / bandWidth;
return clampNote(note);
}
bool isNaturalKey(int note) {
const int n = note < 0 ? 0 : (note > kStripKeyCount - 1 ? kStripKeyCount - 1 : note);
const int n = clampNote(note);
static constexpr bool kNatural[12] = {
true, // 0 C
false, // 1 C#
@@ -81,20 +72,56 @@ bool isNaturalKey(int note) {
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
// Same linear mapping as keyAtPoint/keyEdgeToX (exact rational), not a truncated-integer
// bandWidth/128 key width — that drifted at the far end of the strip.
const int half = bandWidth / 2;
int shift;
if (dxPixels > 0) {
shift = (dxPixels * kStripKeyCount + half) / bandWidth;
} else {
shift = -(((-dxPixels) * kStripKeyCount + half) / bandWidth);
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());
}
return clampNote(startNote + shift);
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