Retire the zone system: one capture = one parameter set, and re-seam the engine and Sample face into bands

This commit is contained in:
2026-07-30 07:15:54 -04:00
parent a689fb75eb
commit 8d4ccbf841
61 changed files with 5416 additions and 8008 deletions
+5 -1
View File
@@ -2,7 +2,11 @@
#include "core/instrument/ui/browser_scroll.h"
#include "core/instrument/ui/editor_geometry.h" // kPad / kTitleHeight / kNavButtonWidth
// The Browse modal is a full-window sheet drawn over the Sample face, so it reuses that
// face's chrome metrics rather than minting its own — a divergent title height or pad would
// make the sheet visibly not line up with what it covers.
#include "core/instrument/ui/sample_bands.h" // kPad / kTitleHeight
#include "core/instrument/ui/sample_chrome.h" // kNavButtonWidth (the Back button's slot)
#include <algorithm>
#include <cctype>
-273
View File
@@ -1,273 +0,0 @@
// editor_geometry.cpp — see editor_geometry.h. Pure math; no host types.
#include "core/instrument/ui/editor_geometry.h"
#include <algorithm>
namespace reasampler::instrument::ui {
namespace {
constexpr int kTitleBarHeight = 28;
constexpr int kButtonMargin = 10;
constexpr int kButtonWidth = 120;
constexpr int kButtonHeight = 24;
} // namespace
EditorLayout layoutEditor(int w, int h) {
// Clamp to non-negative extents so a degenerate view can't produce inverted rects.
const int cw = std::max(0, w);
const int ch = std::max(0, h);
EditorLayout out;
const int titleH = std::min(kTitleBarHeight, ch);
out.titleBar = Rect::ltrb(0, 0, cw, titleH);
out.canvas = Rect::ltrb(0, titleH, cw, ch);
// Button inset from the canvas top-left, clamped so it never overhangs a small view.
const int bx = out.canvas.x + kButtonMargin;
const int by = out.canvas.y + kButtonMargin;
const int bRight = std::min(bx + kButtonWidth, out.canvas.right());
const int bBottom = std::min(by + kButtonHeight, out.canvas.bottom());
out.button = Rect::ltrb(bx, by, std::max(bx, bRight), std::max(by, bBottom));
return out;
}
HitTarget hitTest(const EditorLayout& layout, int x, int y) {
if (contains(layout.button, x, y)) return HitTarget::kButton;
return HitTarget::kNone;
}
Rect sampleRowRect(const EditorLayout& layout, int index) {
if (index < 0) return Rect{};
const int top = layout.canvas.y + index * kSampleRowHeight;
return Rect::ltrb(layout.canvas.x, top, layout.canvas.right(), top + kSampleRowHeight);
}
int sampleRowHitTest(const EditorLayout& layout, int rowCount, int x, int y) {
if (rowCount <= 0) return -1;
if (x < layout.canvas.x || x >= layout.canvas.right()) return -1;
if (y < layout.canvas.y) return -1;
if (y >= layout.canvas.bottom()) return -1;
const int index = (y - layout.canvas.y) / kSampleRowHeight;
if (index < 0 || index >= rowCount) return -1;
const Rect r = sampleRowRect(layout, index);
if (y >= r.bottom()) return -1;
return index;
}
// --- Keymap editor -----------------------------------------------------------
KeymapEditorLayout layoutKeymapEditor(int w, int h) {
KeymapEditorLayout out;
out.base = layoutEditor(w, h);
const Rect& canvas = out.base.canvas;
const int canvasW = std::max(0, canvas.width);
const int splitW = canvasW / kZonePanelFraction; // width of the zone panel
const int splitX = std::max(canvas.x, canvas.right() - splitW);
out.sampleList = Rect::ltrb(canvas.x, canvas.y, splitX, canvas.bottom());
out.zonePanel = Rect::ltrb(splitX, canvas.y, canvas.right(), canvas.bottom());
const int addH = std::min(kAddZoneHeight, std::max(0, out.zonePanel.height));
out.addZoneButton =
Rect::ltrb(out.zonePanel.x, out.zonePanel.y, out.zonePanel.right(),
out.zonePanel.y + addH);
out.zoneRowArea = Rect::ltrb(out.zonePanel.x, out.addZoneButton.bottom(),
out.zonePanel.right(), out.zonePanel.bottom());
return out;
}
Rect keymapSampleRowRect(const KeymapEditorLayout& layout, int index) {
if (index < 0) return Rect{};
const int top = layout.sampleList.y + index * kSampleRowHeight;
return Rect::ltrb(layout.sampleList.x, top, layout.sampleList.right(),
top + kSampleRowHeight);
}
int keymapSampleRowHitTest(const KeymapEditorLayout& layout, int rowCount, int x, int y) {
if (rowCount <= 0) return -1;
const Rect& list = layout.sampleList;
if (x < list.x || x >= list.right()) return -1;
if (y < list.y || y >= list.bottom()) return -1;
const int index = (y - list.y) / kSampleRowHeight;
if (index < 0 || index >= rowCount) return -1;
const Rect r = keymapSampleRowRect(layout, index);
if (y >= r.bottom()) return -1;
return index;
}
Rect zoneRowRect(const KeymapEditorLayout& layout, int index) {
if (index < 0) return Rect{};
const int top = layout.zoneRowArea.y + index * kZoneRowHeight;
return Rect::ltrb(layout.zoneRowArea.x, top, layout.zoneRowArea.right(),
top + kZoneRowHeight);
}
ZoneHit zoneHitTest(const KeymapEditorLayout& layout, int zoneCount, int x, int y) {
if (zoneCount <= 0) return ZoneHit{};
const Rect& area = layout.zoneRowArea;
if (x < area.x || x >= area.right()) return ZoneHit{};
if (y < area.y || y >= area.bottom()) return ZoneHit{};
const int index = (y - area.y) / kZoneRowHeight;
if (index < 0 || index >= zoneCount) return ZoneHit{};
const Rect row = zoneRowRect(layout, index);
if (y >= row.bottom()) return ZoneHit{};
// Seven mini-buttons pinned to the right edge, each kZoneCtrlWidth wide, in slot
// order 0..6; a click left of the leftmost is the label ("select").
const ZoneField fields[7] = {
ZoneField::kLowDown, ZoneField::kLowUp, ZoneField::kHighDown,
ZoneField::kHighUp, ZoneField::kRootDown, ZoneField::kRootUp,
ZoneField::kDelete,
};
const int slots = 7;
const int ctrlBlockLeft = row.right() - slots * kZoneCtrlWidth;
if (x < ctrlBlockLeft) return ZoneHit{index, ZoneField::kZoneNone}; // label -> select
const int slot = (x - ctrlBlockLeft) / kZoneCtrlWidth;
if (slot < 0 || slot >= slots) return ZoneHit{index, ZoneField::kZoneNone};
return ZoneHit{index, fields[slot]};
}
bool addZoneHitTest(const KeymapEditorLayout& layout, int x, int y) {
return contains(layout.addZoneButton, x, y);
}
namespace {
constexpr int kHeroMinHeight = 150; // elastic hero's floor
constexpr int kClusterHeight = 52; // root strip + preview + vel knob + curve btn + channel toggle
constexpr int kStripBandHeight = 40; // keyboard-strip band height (root strip + zone strip)
// Cluster's fixed right-anchored run: Preview button, vel knob cell, curve button, Mono|Stereo.
constexpr int kPreviewBtnW = 64;
constexpr int kVelCellW = 48;
constexpr int kCurveBtnSize = 28;
constexpr int kChanSegW = 52;
constexpr int kChanSegH = 18;
} // namespace
// Band order: title (fixed) -> hero (elastic, absorbs remaining height, floor
// kHeroMinHeight) -> cluster (fixed) -> deck (fixed height `deckH`, bottom-anchored). A
// window too short for the floor keeps the hero at its floor and clips lower bands.
SampleBands computeSampleBands(int w, int h, int deckH) {
SampleBands b;
const int titleH = (std::min)(kTitleHeight, h);
b.title = Rect::ltrb(0, 0, w, titleH);
// Two nav buttons right-anchored in the title band (Browse then Zone).
const int navTop = 2;
const int navBot = (std::max)(navTop, titleH - 2);
const Rect zone = Rect::ltrb(w - kPad - kNavButtonWidth, navTop, w - kPad, navBot);
const Rect browse = Rect::ltrb(zone.x - 4 - kNavButtonWidth, navTop, zone.x - 4, navBot);
b.navBrowse = browse;
b.navZone = zone;
int deckTop = h - kPad - deckH;
int clusterTop = deckTop - kClusterHeight - 4;
int heroBottom = clusterTop - 4;
if (heroBottom - titleH < kHeroMinHeight) {
heroBottom = titleH + kHeroMinHeight; // hero floor wins; lower bands clip below
clusterTop = heroBottom + 4;
deckTop = clusterTop + kClusterHeight + 4;
}
b.hero = Rect::ltrb(kPad, titleH, w - kPad, heroBottom);
b.cluster = Rect::ltrb(0, clusterTop, w, clusterTop + kClusterHeight);
b.deck = Rect::ltrb(kPad, deckTop, w - kPad, deckTop + deckH);
return b;
}
ClusterRects clusterRects(const Rect& cluster, const Rect& chanMono, int knobSize) {
ClusterRects r;
const int stripTop = cluster.y + (cluster.height - kStripBandHeight) / 2;
const int stripBot = stripTop + kStripBandHeight;
const int curveTop = cluster.y + (cluster.height - kCurveBtnSize) / 2;
r.curveBtn = Rect::ltrb(chanMono.x - kPad - kCurveBtnSize, curveTop,
chanMono.x - kPad, curveTop + kCurveBtnSize);
r.velCell = Rect::ltrb(r.curveBtn.x - kPad - kVelCellW, stripTop,
r.curveBtn.x - kPad, stripBot);
const int knobLeft = r.velCell.x + (kVelCellW - knobSize) / 2;
r.velKnob = Rect::ltrb(knobLeft, r.velCell.y, knobLeft + knobSize,
r.velCell.y + knobSize);
r.velLabel = Rect::ltrb(r.velCell.x, r.velKnob.bottom(), r.velCell.right(), r.velCell.bottom());
r.preview = Rect::ltrb(r.velCell.x - kPad - kPreviewBtnW, stripTop,
r.velCell.x - kPad, stripBot);
r.rootStrip = Rect::ltrb(cluster.x + kPad, stripTop, r.preview.x - kPad, stripBot);
return r;
}
ChannelToggleRects channelToggleRects(const Rect& area) {
const int top = area.y + (area.height - kChanSegH) / 2;
const int right = area.right() - kPad;
const Rect stereo = Rect::ltrb(right - kChanSegW, top, right, top + kChanSegH);
const Rect mono = Rect::ltrb(stereo.x - kChanSegW, top, stereo.x, top + kChanSegH);
return {mono, stereo};
}
Rect zoneContentArea(int w, int h) {
const int titleH = (std::min)(kTitleHeight, h);
return Rect::ltrb(0, titleH, w, h);
}
Rect zoneBackRect(int w, int h) {
return Rect::ltrb(w - kPad - kNavButtonWidth, 2, w - kPad,
(std::max)(2, (std::min)(kTitleHeight, h) - 2));
}
Rect zoneAddRect(const Rect& content) {
return Rect::ltrb(content.x + kPad, content.y + 4, content.x + kPad + 96,
content.y + 4 + 20);
}
Rect zoneDeleteRect(const Rect& addR) {
return Rect::ltrb(addR.right() + 8, addR.y, addR.right() + 8 + 64, addR.bottom());
}
// Sits below the "+ Add Zone" affordance (top+4, height 20) with a 12px gap.
Rect zonesStripArea(const Rect& content) {
const int stripTop = content.y + 4 + 20 + 12; // addR.bottom() + 12
return Rect::ltrb(content.x + kPad, stripTop, content.right() - kPad,
stripTop + kStripBandHeight);
}
// Anchored off zonesStripArea.bottom() so the legend top tracks the strip bottom.
Rect noteEntryFieldsArea(const Rect& content) {
const int stripBottom = zonesStripArea(content).bottom();
const int top = stripBottom + 8; // legendTop (== zonesStripArea.bottom() + 8)
return Rect::ltrb(content.x + 8 + 128, top, content.right() - 8, top + 18);
}
Rect noteEntryFieldRect(const Rect& fields, int f) {
if (f < 0 || f > 2 || fields.width <= 0) return Rect{};
const int segW = fields.width / 3;
const int left = fields.x + f * segW + (f > 0 ? 4 : 0); // small inter-field gap
const int right = (f == 2) ? fields.right() : fields.x + (f + 1) * segW;
return Rect::ltrb(left, fields.y, right, fields.bottom());
}
Rect zonesControlPanel(const Rect& content) {
const Rect strip = zonesStripArea(content);
const int panelTop = strip.bottom() + 8 + 18 + 8; // strip + the 18px legend row + gap
return Rect::ltrb(content.x + kPad, panelTop, content.right() - kPad,
content.bottom() - 4);
}
// Top-anchored; reserves a column at the panel's right for the curve-preview button so
// no deck row starts inside it.
Rect zonesDeckArea(const Rect& content) {
const Rect panel = zonesControlPanel(content);
return Rect::ltrb(panel.x, panel.y, panel.right() - kCurveBtnSize - kPad, panel.bottom());
}
Rect zonesCurveButton(const Rect& content) {
const Rect panel = zonesControlPanel(content);
return Rect::ltrb(panel.right() - kCurveBtnSize, panel.y, panel.right(), panel.y + kCurveBtnSize);
}
} // namespace reasampler::instrument::ui
+5 -174
View File
@@ -1,8 +1,9 @@
// editor_geometry.h — view geometry + hit-test for the VST3 IPlugView LICE editor. The
// IPlugView shell owns window/bitmap/SWELL plumbing; the rectangle math and hit-testing
// live here so they can be unit-tested outside the DAW.
#pragma once
// editor_geometry.h — the shared geometry vocabulary for the VST3 editor's pure modules:
// the one concrete `Rect` (aliased from core/ui) and its half-open `contains()`. Every
// instrument UI module speaks these types, so they live in one place rather than each
// module reaching into core/ui separately. The Sample face's own layout lives in
// sample_bands (the band-stack allocator) and the per-band modules.
#include "core/ui/rect.h"
@@ -11,174 +12,4 @@ namespace reasampler::instrument::ui {
using Rect = ::reasampler::ui::Rect;
using ::reasampler::ui::contains;
// Title band + one button + remaining canvas, clamped so a degenerate (too-small) view
// never yields a region spilling outside the surface.
struct EditorLayout {
Rect titleBar;
Rect button;
Rect canvas;
};
// Divide a (w x h) client area into the editor's top-level regions. Pure.
EditorLayout layoutEditor(int w, int h);
enum class HitTarget {
kNone,
kButton,
};
// Classify a click at (x, y) against a layout.
HitTarget hitTest(const EditorLayout& layout, int x, int y);
// --- Sample-selection list ---------------------------------------------------
//
// A vertical stack of fixed-height rows below the title bar; clicking a row selects that
// sample. Pure geometry only — the shell draws names and routes the click.
inline constexpr int kSampleRowHeight = 22;
// Rect for row `index` (0-based), laid out top-down inside the layout's canvas. Rows
// beyond what the canvas can show are still computed (the shell clips at paint time); a
// negative index yields an empty rect.
Rect sampleRowRect(const EditorLayout& layout, int index);
// Row index a click at (x, y) lands on given `rowCount` rows, or -1 for a click outside
// the list.
int sampleRowHitTest(const EditorLayout& layout, int rowCount, int x, int y);
// --- Keymap editor ------------------------------------------------------------
//
// Splits the canvas into a LEFT bank-sample list (the sample-selection rows above, reused
// as the "sample to add / fallback pick") and a RIGHT zone panel listing the performance
// map's zones. An "Add Zone" button sits at the top of the zone panel; each zone row
// carries nudge/delete mini-buttons (LICE has no native numeric entry field).
inline constexpr int kZoneRowHeight = 24;
inline constexpr int kZonePanelFraction = 2; // zone panel gets the RIGHT 1/2 of the canvas
inline constexpr int kZoneCtrlWidth = 20; // width of one nudge/delete mini-button
inline constexpr int kAddZoneHeight = 22; // "Add Zone" button band height
// Clamps every rect to the canvas so a degenerate view still yields in-bounds rects.
struct KeymapEditorLayout {
EditorLayout base;
Rect sampleList; // LEFT column
Rect zonePanel; // RIGHT column
Rect addZoneButton; // top of the zone panel
Rect zoneRowArea; // below addZoneButton
};
KeymapEditorLayout layoutKeymapEditor(int w, int h);
// Rect for bank-sample row `index` inside the LEFT column. Negative index -> empty.
Rect keymapSampleRowRect(const KeymapEditorLayout& layout, int index);
// Bank-sample row a click lands on inside the left list, or -1 outside it.
int keymapSampleRowHitTest(const KeymapEditorLayout& layout, int rowCount, int x, int y);
// Rect for zone row `index` inside zoneRowArea. Negative index -> empty.
Rect zoneRowRect(const KeymapEditorLayout& layout, int index);
// A zone row's interactive fields: a label on the left, then seven fixed-width
// mini-buttons on the right (low-, low+, high-, high+, root-, root+, delete). kZoneNone
// means the click missed a control (e.g. the label) — the shell may still treat that as
// "select this zone".
enum class ZoneField {
kZoneNone,
kLowDown,
kLowUp,
kHighDown,
kHighUp,
kRootDown,
kRootUp,
kDelete,
};
// Which zone row (or -1) and which field within it a click landed on. A click on
// "Add Zone" is reported separately by addZoneHitTest.
struct ZoneHit {
int zoneIndex = -1;
ZoneField field = ZoneField::kZoneNone;
};
// Classify a click at (x, y) against `zoneCount` zone rows. {-1, kZoneNone} for a miss.
// Within a row, the seven mini-buttons occupy fixed-width slots on the right edge; a
// click left of those slots is {index, kZoneNone} (the label area — "select").
ZoneHit zoneHitTest(const KeymapEditorLayout& layout, int zoneCount, int x, int y);
bool addZoneHitTest(const KeymapEditorLayout& layout, int x, int y);
// --- Sample / Zone face layout ------------------------------------------------
//
// The capture-first editor's band/cluster/zone-surface layout math. Draw and hit-test
// both derive every rect from these formulas so they can never drift; the shell only
// draws + routes. The Browse-modal layout lives in browser_scroll (its search box
// height feeds it).
inline constexpr int kPad = 8;
inline constexpr int kTitleHeight = 26;
inline constexpr int kNavButtonWidth = 62; // Browse / Zone / Back title-band buttons
// Sample-face bands (top->bottom): TITLE (name + Browse/Zone nav), a full-width elastic
// HERO (absorbs all height left after the fixed bands, floored), the root+preview
// CLUSTER, and the bottom-anchored knob DECK (height `deckH` from knob_deck's wrap). A
// window shorter than the hero floor clips the lower bands past the window bottom.
struct SampleBands {
Rect title;
Rect navBrowse;
Rect navZone;
Rect hero; // waveform + envelope overlay
Rect cluster; // root strip + preview + vel knob + curve button + channel toggle
Rect deck;
};
SampleBands computeSampleBands(int w, int h, int deckH);
// Cluster sub-rects: the root strip keeps the left side at remainder width; the right
// side is the fixed-width right-anchored run (Preview · vel knob cell · curve button ·
// Mono|Stereo). `knobSize` is the deck knob square, passed in so this module does not
// depend on knob_deck.
struct ClusterRects {
Rect rootStrip;
Rect preview;
Rect velCell; // preview-velocity knob cell (knob + label band)
Rect velKnob;
Rect velLabel;
Rect curveBtn; // opens the curve-preview popup
};
ClusterRects clusterRects(const Rect& cluster, const Rect& chanMono, int knobSize);
// Mono/stereo toggle: a two-segment control right-anchored in `area`, vertically centered.
struct ChannelToggleRects {
Rect mono;
Rect stereo;
};
ChannelToggleRects channelToggleRects(const Rect& area);
// Zone-view content area: the whole window below the title band.
Rect zoneContentArea(int w, int h);
// Zone/Browse "Back" button — the same slot the Sample face's Zone nav button occupies.
Rect zoneBackRect(int w, int h);
// "+ Add Zone" affordance and the "Delete" button beside it (Delete only draws/hits
// when a zone is selected).
Rect zoneAddRect(const Rect& content);
Rect zoneDeleteRect(const Rect& addR);
// Zone-view keyboard strip rect: below "+ Add Zone" with a 12px gap, padded kPad
// horizontally.
Rect zonesStripArea(const Rect& content);
// Numeric-entry field row area inside the Zones legend, and the rect of field `f`
// (0=low, 1=high, 2=root) within it — three equal segments left-to-right. Out-of-range
// index yields an empty rect.
Rect noteEntryFieldsArea(const Rect& content);
Rect noteEntryFieldRect(const Rect& fields, int f);
// Per-zone parameter panel below the strip + legend, running to the content bottom; the
// knob-deck area within it (a right column reserved for the curve-preview button); and
// that button's rect (right-anchored at the panel top).
Rect zonesControlPanel(const Rect& content);
Rect zonesDeckArea(const Rect& content);
Rect zonesCurveButton(const Rect& content);
} // namespace reasampler::instrument::ui
+3 -15
View File
@@ -15,7 +15,7 @@ int clampNote(int n) {
}
// Maps a key boundary (0..128) to an x pixel; keyEdge==128 maps to the band's right. A
// zone's left uses floor(low) and its right uses floor(high+1), tiling adjacent zones
// span's left uses floor(low) and its right uses floor(high+1), tiling adjacent spans
// without a seam.
int keyEdgeToX(int bandLeft, int bandWidth, int keyEdge) {
if (keyEdge <= 0) return bandLeft;
@@ -44,31 +44,19 @@ EmbedLayout layoutEmbed(int w, int h) {
return out;
}
Rect zoneSegmentRect(const EmbedLayout& layout, int lowNote, int highNote) {
Rect keySpanRect(const EmbedLayout& layout, int lowNote, int highNote) {
const Rect& band = layout.keymap;
const int bandWidth = std::max(0, band.width);
int lo = clampNote(lowNote);
int hi = clampNote(highNote);
if (lo > hi) lo = hi; // defensive: a malformed zone collapses rather than inverts
if (lo > hi) lo = hi; // defensive: a malformed span collapses rather than inverts
const int leftX = keyEdgeToX(band.x, bandWidth, lo);
const int rightX = keyEdgeToX(band.x, bandWidth, hi + 1);
return Rect::ltrb(leftX, band.y, std::max(leftX, rightX), band.bottom());
}
int zoneAtPoint(const EmbedLayout& layout, const EmbedZone* zones, int zoneCount, int x,
int y) {
if (zoneCount <= 0 || zones == nullptr) return -1;
if (!contains(layout.keymap, x, y)) return -1;
// First covering zone in draw order wins (first-match, mirroring the core's resolve).
for (int i = 0; i < zoneCount; ++i) {
const Rect r = zoneSegmentRect(layout, zones[i].lowNote, zones[i].highNote);
if (contains(r, x, y)) return i;
}
return -1; // on the band but on an uncovered key
}
Rect levelFillRect(const EmbedLayout& layout, double level) {
const Rect& band = layout.levelBand;
if (band.width <= 0 || band.height <= 0) return Rect{};
+9 -22
View File
@@ -3,9 +3,9 @@
// bitmap + mouse coords) into these functions.
//
// A single compact band REAPER draws inline in the track/mixer control panel via the
// Cockos embedded-UI surface: each performance zone as a horizontal segment across the
// keyboard span (MIDI 0..127 mapped to the strip width), plus a thin activity level band
// at the bottom. Interaction is zone selection only — no editing.
// Cockos embedded-UI surface: the loaded capture across the keyboard span (MIDI 0..127
// mapped to the strip width) with its root marked, plus a thin activity level band at the
// bottom. Read-only — the strip displays, it never edits.
#pragma once
@@ -19,18 +19,10 @@ inline constexpr int kEmbedKeyCount = 128;
inline constexpr int kEmbedLevelBandHeight = 4;
inline constexpr int kEmbedKeymapMinHeight = 6;
// One zone rendered on the strip: its inclusive MIDI key range — the minimal projection
// of a PerformanceZone the strip needs (no sample ids or PCM). Expected in [0,127] with
// low <= high; layout clamps defensively regardless.
struct EmbedZone {
int lowNote = 0;
int highNote = 127;
};
// Clamped to the area so a degenerate (tiny) size never yields a region spilling outside
// the surface.
struct EmbedLayout {
Rect keymap; // top: zone-segment band
Rect keymap; // top: keyboard-span band
Rect levelBand; // bottom: level/activity indicator
};
@@ -39,16 +31,11 @@ struct EmbedLayout {
// kEmbedKeymapMinHeight); the keymap takes the rest.
EmbedLayout layoutEmbed(int w, int h);
// Horizontal sub-rect of the keymap band for a zone spanning [lowNote, highNote]
// (inclusive). Spans the half-open pixel range so adjacent zones tile without a gap or
// overlap. Notes clamp to [0,127] and low clamps to <= high.
Rect zoneSegmentRect(const EmbedLayout& layout, int lowNote, int highNote);
// Zone a click at (x, y) lands on, given zones in draw order, or -1 for a miss. When
// zones overlap on a key, the first covering zone in order wins — mirroring the sampler
// core's first-match Keymap::resolve, so selection agrees with playback.
int zoneAtPoint(const EmbedLayout& layout, const EmbedZone* zones, int zoneCount, int x,
int y);
// Horizontal sub-rect of the keymap band for the inclusive key span [lowNote, highNote].
// Spans the half-open pixel range so adjacent spans tile without a gap or overlap. Notes
// clamp to [0,127] and low clamps to <= high. The loaded capture uses the full span; a
// single-key span (low == high) is the root marker.
Rect keySpanRect(const EmbedLayout& layout, int lowNote, int highNote);
// Filled portion of the level band for a 0..1 level (clamped); left sub-rect of levelBand
// whose width is level * band width.
+1 -36
View File
@@ -15,7 +15,7 @@ int clampNote(int 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/zones without a seam. Mirrors embed_strip::keyEdgeToX.
// 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;
@@ -62,41 +62,6 @@ int keyAtPoint(const StripLayout& layout, int x, int y) {
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; // 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 has no body: split at the midpoint, low edge wins the tie.
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) {
const int n = note < 0 ? 0 : (note > kStripKeyCount - 1 ? kStripKeyCount - 1 : note);
static constexpr bool kNatural[12] = {
+8 -42
View File
@@ -1,11 +1,10 @@
// keyboard_strip.h — layout + hit-test + drag math for the capture-first editor's
// keyboard strip. Mirror of editor_geometry/embed_strip/mode_switch; the shell draws
// and marshals mouse events into these functions.
// keyboard_strip.h — layout + hit-test + drag math for the editor's keyboard strip.
// Mirror of embed_strip/mode_switch; the shell draws and marshals mouse events into these
// functions.
//
// The strip maps the full 128-key MIDI span across a horizontal band (the same idiom
// embed_strip uses) and serves two faces: the single-capture fast path (a root marker,
// click-a-key or drag it to set root) and the opt-in zones panel (each zone drawn as a
// bar with edge-grab resize handles + a body move-handle).
// embed_strip uses). The loaded capture responds across that whole span, so the strip's
// job is the root marker: click a key, or drag the marker, to set the root.
#pragma once
@@ -17,10 +16,6 @@ namespace reasampler::instrument::ui {
// stay independent.
inline constexpr int kStripKeyCount = 128;
// Pixel width of a zone bar's edge-grab region. A zone narrower than 2x this has no
// body move-handle (both edges win their halves).
inline constexpr int kStripEdgeGrabWidth = 6;
// The keys band takes the whole strip area today; clamped so a degenerate size never
// yields an inverted rect.
struct StripLayout {
@@ -37,44 +32,15 @@ int keyLeftX(const StripLayout& layout, int note);
// Half-open rect of a single key `note`, clamped to [0,127].
Rect keyRect(const StripLayout& layout, int note);
// Root-marker rect for the single-capture fast path; equivalent to
// keyRect(layout, rootNote) but named so the intent reads at the call site.
// Root-marker rect; equivalent to keyRect(layout, rootNote) but named so the intent reads
// at the call site.
Rect rootMarkerRect(const StripLayout& layout, int rootNote);
// MIDI note a point (x, y) lands on, or -1 outside the keys band.
int keyAtPoint(const StripLayout& layout, int x, int y);
// Horizontal sub-rect for a zone spanning [lowNote, highNote] inclusive. Notes clamp to
// [0,127] and low clamps to <= high, so a malformed zone never yields an inverted rect.
Rect zoneBarRect(const StripLayout& layout, int lowNote, int highNote);
// Which part of a zone bar a grab landed on: an edge resizes that boundary, the body
// moves the whole span, kNone means the grab missed the bar.
enum class ZoneGrab {
kNone,
kLowEdge,
kHighEdge,
kBody,
};
// Classify a grab at (x, y) against one zone's bar. A narrow bar (< 2*kStripEdgeGrabWidth)
// resolves the near half to each edge (no body); the low edge wins a tie at the exact
// midpoint.
ZoneGrab zoneGrabAt(const StripLayout& layout, int lowNote, int highNote, int x, int y);
// Zone (index into the parallel `lows`/`highs` arrays, draw order) whose bar a grab
// lands on, plus which part, or {-1, kNone} for a miss. First covering zone in draw
// order wins.
struct ZoneBarHit {
int zoneIndex = -1;
ZoneGrab grab = ZoneGrab::kNone;
};
ZoneBarHit zoneBarAtPoint(const StripLayout& layout, const int* lows, const int* highs,
int count, int x, int y);
// Resolves a drag to a new MIDI note: `startNote` shifted by round(dxPixels / keyWidth),
// clamped to [0,127]. The one arithmetic behind edge-resize, body-move (apply to both
// edges with the same delta to preserve span), and root-marker drag.
// clamped to [0,127]. The one arithmetic behind the root-marker drag.
int resolveDragNote(const StripLayout& layout, int startNote, int dxPixels);
// True when `note` (clamped to [0,127]) is a natural (white) key in 12-tone equal
+54
View File
@@ -0,0 +1,54 @@
// sample_bands.cpp — see sample_bands.h. Pure math; no host types.
#include "core/instrument/ui/sample_bands.h"
#include <algorithm>
namespace reasampler::instrument::ui {
SampleBands computeSampleBands(int w, int h, int deckHeight) {
const int cw = std::max(0, w);
const int ch = std::max(0, h);
const int deckH = std::max(0, deckHeight);
SampleBands b;
const int chromeH = std::min(kTitleHeight + kChromeRowHeight, ch);
b.chrome = Rect::ltrb(0, 0, cw, chromeH);
// Decks are bottom-anchored so the deck row sits on the window edge at any height; the
// waveform absorbs whatever is left. When that leaves less than the two-lane floor the
// FLOOR WINS and the deck band is pushed past the window bottom (clipped) rather than
// squeezing the waveform into an unreadable sliver.
int deckTop = ch - kPad - deckH;
int waveTop = chromeH + kBandGap;
int waveBottom = deckTop - kBandGap;
if (waveBottom - waveTop < kWaveformMinHeight) {
waveBottom = waveTop + kWaveformMinHeight;
deckTop = waveBottom + kBandGap;
}
b.waveform = Rect::ltrb(kPad, waveTop, std::max(kPad, cw - kPad), waveBottom);
b.decks = Rect::ltrb(kPad, deckTop, std::max(kPad, cw - kPad), deckTop + deckH);
return b;
}
WaveformLanes waveformLanes(const Rect& waveform, bool stereo) {
WaveformLanes lanes;
if (waveform.empty()) return lanes;
if (!stereo) {
lanes.upper = waveform; // one lane; `lower` stays empty
return lanes;
}
// Split the usable height evenly, giving the seam to the gap. An odd remainder goes to
// the upper (left) lane so the two lanes never disagree about the seam row.
const int usable = std::max(0, waveform.height - kLaneGap);
const int lowerH = usable / 2;
const int upperH = usable - lowerH;
const int upperBottom = waveform.y + upperH;
lanes.upper = Rect::ltrb(waveform.x, waveform.y, waveform.right(), upperBottom);
lanes.lower = Rect::ltrb(waveform.x, upperBottom + kLaneGap, waveform.right(),
waveform.bottom());
return lanes;
}
} // namespace reasampler::instrument::ui
+53
View File
@@ -0,0 +1,53 @@
#pragma once
// sample_bands.h — THE band-stack allocator for the Sample face: the one module that owns
// the editor's vertical inventory. Three bands, top to bottom — CHROME (toolbar + control
// row), WAVEFORM (elastic, sized to hold two stacked channel lanes), DECKS (the knob-deck
// row). Everything else in the editor fills a band it is handed; nothing else allocates
// vertical space, so a band's owner can re-lay its interior without moving its neighbours.
#include "core/instrument/ui/editor_geometry.h" // Rect
namespace reasampler::instrument::ui {
// Shared outer inset every band honours horizontally.
inline constexpr int kPad = 8;
// Chrome band: the toolbar row (title + nav) stacked over the control row (piano strip,
// preview, velocity knob, curve button, channel toggle). sample_chrome partitions it.
inline constexpr int kTitleHeight = 26;
inline constexpr int kChromeRowHeight = 52;
// Waveform band floor: two stacked lanes plus the seam between them. The band never shrinks
// below this — a window too short for it clips the bands beneath instead, so the waveform
// stays a usable two-lane surface at every size.
inline constexpr int kLaneMinHeight = 74;
inline constexpr int kLaneGap = 2;
inline constexpr int kWaveformMinHeight = 2 * kLaneMinHeight + kLaneGap;
// Vertical seam between adjacent bands.
inline constexpr int kBandGap = 4;
// The vertical inventory. Bands never overlap and are returned top-to-bottom; a band may be
// empty() on a degenerate window, in which case its owner draws and hit-tests nothing.
struct SampleBands {
Rect chrome; // full width: toolbar row + control row
Rect waveform; // kPad-inset, elastic, >= kWaveformMinHeight
Rect decks; // kPad-inset, bottom-anchored, height `deckHeight`
};
// Divide a (w x h) client area into the three bands. `deckHeight` is the knob deck's own
// wrapped height (from knob_deck) — the only interior measurement the allocator needs, so
// the deck band is exactly as tall as its content. Pure.
SampleBands computeSampleBands(int w, int h, int deckHeight);
// The waveform band's two channel lanes: L above R, separated by kLaneGap. In mono only
// `upper` is populated (it takes the whole band) and `lower` is empty — a mono capture has
// no second lane to draw, and overlays that ride the waveform draw ONCE across the whole
// band in either mode, never per lane.
struct WaveformLanes {
Rect upper;
Rect lower; // empty() in mono
};
WaveformLanes waveformLanes(const Rect& waveform, bool stereo);
} // namespace reasampler::instrument::ui
+71
View File
@@ -0,0 +1,71 @@
// sample_chrome.cpp — see sample_chrome.h. Pure math; no host types.
#include "core/instrument/ui/sample_chrome.h"
#include <algorithm>
#include "core/instrument/ui/sample_bands.h" // kPad / kTitleHeight / kChromeRowHeight
namespace reasampler::instrument::ui {
namespace {
constexpr int kStripBandHeight = 40; // the root/piano strip's own height inside the row
// The control row's fixed right-anchored run, right to left.
constexpr int kChanSegW = 52;
constexpr int kChanSegH = 18;
constexpr int kCurveBtnSize = 28;
constexpr int kVelCellW = 48;
constexpr int kPreviewBtnW = 64;
} // namespace
ChromeRects chromeRects(const Rect& chrome, int knobSize) {
ChromeRects r;
if (chrome.empty()) return r;
const int titleH = std::min(kTitleHeight, chrome.height);
r.toolbar = Rect::ltrb(chrome.x, chrome.y, chrome.right(), chrome.y + titleH);
r.controls = Rect::ltrb(chrome.x, r.toolbar.bottom(), chrome.right(), chrome.bottom());
const int navTop = r.toolbar.y + 2;
const int navBot = std::max(navTop, r.toolbar.bottom() - 2);
r.navBrowse = Rect::ltrb(std::max(chrome.x, chrome.right() - kPad - kNavButtonWidth),
navTop, std::max(chrome.x, chrome.right() - kPad), navBot);
if (r.controls.empty()) return r;
const Rect& row = r.controls;
// Vertically centre the two heights the row uses: the tall strip band (which the preview
// button and velocity cell align to) and the smaller square/segment controls.
const int stripTop = row.y + (row.height - kStripBandHeight) / 2;
const int stripBot = stripTop + kStripBandHeight;
const int chanTop = row.y + (row.height - kChanSegH) / 2;
const int chanRight = row.right() - kPad;
r.chanStereo = Rect::ltrb(chanRight - kChanSegW, chanTop, chanRight, chanTop + kChanSegH);
r.chanMono = Rect::ltrb(r.chanStereo.x - kChanSegW, chanTop, r.chanStereo.x,
chanTop + kChanSegH);
const int curveTop = row.y + (row.height - kCurveBtnSize) / 2;
r.curveBtn = Rect::ltrb(r.chanMono.x - kPad - kCurveBtnSize, curveTop,
r.chanMono.x - kPad, curveTop + kCurveBtnSize);
r.velCell = Rect::ltrb(r.curveBtn.x - kPad - kVelCellW, stripTop,
r.curveBtn.x - kPad, stripBot);
const int knobLeft = r.velCell.x + (kVelCellW - knobSize) / 2;
r.velKnob = Rect::ltrb(knobLeft, r.velCell.y, knobLeft + knobSize,
r.velCell.y + knobSize);
r.velLabel = Rect::ltrb(r.velCell.x, r.velKnob.bottom(), r.velCell.right(),
r.velCell.bottom());
r.preview = Rect::ltrb(r.velCell.x - kPad - kPreviewBtnW, stripTop,
r.velCell.x - kPad, stripBot);
// Remainder width; clamped so a narrow window collapses the strip rather than inverting it.
r.rootStrip = Rect::ltrb(row.x + kPad, stripTop,
std::max(row.x + kPad, r.preview.x - kPad), stripBot);
return r;
}
} // namespace reasampler::instrument::ui
+34
View File
@@ -0,0 +1,34 @@
#pragma once
// sample_chrome.h — interior geometry of the Sample face's CHROME band: the toolbar row
// (title + Browse) over the control row (root/piano strip, preview trigger, preview-velocity
// knob cell, curve-preview button, Mono|Stereo toggle). Reads the band rect the allocator
// hands it (sample_bands) and never allocates vertical space of its own.
#include "core/instrument/ui/editor_geometry.h" // Rect
namespace reasampler::instrument::ui {
inline constexpr int kNavButtonWidth = 62; // the Browse toolbar button
// Every interactive rect inside the chrome band, in one pass so draw and hit-test cannot
// derive them differently. The control row's right-anchored run is fixed-width (preview,
// velocity cell, curve button, channel toggle) and the root strip takes the remainder, so
// the strip grows with the window.
struct ChromeRects {
Rect toolbar; // full-width top row
Rect navBrowse; // right-anchored in the toolbar
Rect controls; // full-width second row
Rect rootStrip; // remainder-width, left
Rect preview;
Rect velCell; // preview-velocity knob cell (knob + label band)
Rect velKnob;
Rect velLabel;
Rect curveBtn; // opens the velocity-curve popup
Rect chanMono;
Rect chanStereo;
};
// `knobSize` is the deck knob square, passed in so this module does not depend on knob_deck.
ChromeRects chromeRects(const Rect& chrome, int knobSize);
} // namespace reasampler::instrument::ui