Files
reasampler/src/core/instrument/ui/keyboard_strip.cpp
T

128 lines
4.3 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 {
// 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