S5 Tier-1: zoned keymap editor + performance-map playback/persistence
Zone editor in the IPlugView LICE surface, zoned resolution built off-thread into the LoadedInstrument keymap with Tier-0 fallback, performance map in VST3 component state with v1 back-compat. Pure resolve/build/serialize + geometry with CTest coverage.
This commit is contained in:
+164
-22
@@ -38,6 +38,9 @@ const LICE_pixel kColBtnHitBg = LICE_RGBA(58, 96, 84, 255);
|
||||
const LICE_pixel kColBtnBorder = LICE_RGBA(120, 200, 160, 255);
|
||||
const COLORREF kRgbText = RGB(210, 230, 220);
|
||||
|
||||
const LICE_pixel kColZoneSelBg = LICE_RGBA(48, 72, 64, 255);
|
||||
const LICE_pixel kColCtrlBg = LICE_RGBA(60, 60, 66, 255);
|
||||
|
||||
void drawText(LICE_IBitmap* bmp, const Rect& r, const char* s, COLORREF col) {
|
||||
// LICE has no built-in font handle here; use GDI text into the bitmap DC, matching
|
||||
// bank_panel's drawCenteredText approach (SetTextColor + DrawText on getDC()).
|
||||
@@ -47,6 +50,26 @@ void drawText(LICE_IBitmap* bmp, const Rect& r, const char* s, COLORREF col) {
|
||||
RECT gr{r.left, r.top, r.right, r.bottom};
|
||||
DrawTextA(dc, s, -1, &gr, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
|
||||
}
|
||||
|
||||
void drawTextCentered(LICE_IBitmap* bmp, const Rect& r, const char* s, COLORREF col) {
|
||||
HDC dc = bmp->getDC();
|
||||
SetBkMode(dc, TRANSPARENT);
|
||||
SetTextColor(dc, col);
|
||||
RECT gr{r.left, r.top, r.right, r.bottom};
|
||||
DrawTextA(dc, s, -1, &gr, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
|
||||
}
|
||||
|
||||
// Clamp a MIDI note to [0, 127].
|
||||
int clampNote(int n) { return n < 0 ? 0 : (n > 127 ? 127 : n); }
|
||||
|
||||
// A short display name for a bank sample id, from the snapshotted list (id if unnamed,
|
||||
// "?" if the id no longer resolves — e.g. a stale zone).
|
||||
std::string sampleLabel(const std::vector<SampleChoice>& samples, const std::string& id) {
|
||||
for (const SampleChoice& c : samples) {
|
||||
if (c.id == id) return c.displayName.empty() ? c.id : c.displayName;
|
||||
}
|
||||
return "?"; // stale: id not in the live bank
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
@@ -62,11 +85,26 @@ void ReaSamplerEditor::refreshSampleList() {
|
||||
if (!processor_) {
|
||||
samples_.clear();
|
||||
selectedId_.clear();
|
||||
map_.zones.clear();
|
||||
selectedZone_ = -1;
|
||||
return;
|
||||
}
|
||||
auto banks = processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey);
|
||||
samples_ = banks ? listSamples(*banks) : std::vector<SampleChoice>{};
|
||||
selectedId_ = processor_->selectedSampleId();
|
||||
map_ = processor_->performanceMap();
|
||||
if (selectedZone_ >= static_cast<int>(map_.zones.size())) selectedZone_ = -1;
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::commitMapAndReload() {
|
||||
// UI thread only. Publish the edited map to the processor, then rebuild the instrument
|
||||
// off the audio thread (reloadFromBank bakes the zones into the live Keymap).
|
||||
if (!processor_) return;
|
||||
processor_->setPerformanceMap(map_);
|
||||
processor_->reloadFromBank();
|
||||
#ifdef _WIN32
|
||||
if (childHwnd_) InvalidateRect(childHwnd_, nullptr, FALSE);
|
||||
#endif
|
||||
}
|
||||
|
||||
ReaSamplerEditor::~ReaSamplerEditor() {
|
||||
@@ -160,28 +198,33 @@ void ReaSamplerEditor::paint(HDC hdc) {
|
||||
LICE_SysBitmap bmp(w, h);
|
||||
LICE_Clear(&bmp, kColBackground);
|
||||
|
||||
const EditorLayout layout = layoutEditor(w, h);
|
||||
const KeymapEditorLayout layout = layoutKeymapEditor(w, h);
|
||||
const EditorLayout& base = layout.base;
|
||||
|
||||
// Title band: the plugin name + whether a live bank is linked.
|
||||
LICE_FillRect(&bmp, layout.titleBar.left, layout.titleBar.top,
|
||||
layout.titleBar.width(), layout.titleBar.height(), kColTitleBg, 1.0f,
|
||||
0);
|
||||
// Title band: the plugin name + a live-state / mode readout.
|
||||
LICE_FillRect(&bmp, base.titleBar.left, base.titleBar.top, base.titleBar.width(),
|
||||
base.titleBar.height(), kColTitleBg, 1.0f, 0);
|
||||
std::string title = "ReaSampler Instrument";
|
||||
if (processor_ && processor_->bridge().isConnected()) {
|
||||
title += samples_.empty() ? " [bank: empty]" : " [pick a sample]";
|
||||
if (samples_.empty()) {
|
||||
title += " [bank: empty]";
|
||||
} else if (map_.zones.empty()) {
|
||||
title += " [Tier 0: pick a sample - Add Zone for a keymap]";
|
||||
} else {
|
||||
title += " [keymap: " + std::to_string(map_.zones.size()) + " zone(s)]";
|
||||
}
|
||||
} else {
|
||||
title += " [host: no bridge]";
|
||||
}
|
||||
Rect titleText{layout.titleBar.left + 8, layout.titleBar.top,
|
||||
layout.titleBar.right - 8, layout.titleBar.bottom};
|
||||
Rect titleText{base.titleBar.left + 8, base.titleBar.top, base.titleBar.right - 8,
|
||||
base.titleBar.bottom};
|
||||
drawText(&bmp, titleText, title.c_str(), kRgbText);
|
||||
|
||||
// Sample list: one row per bank sample, the selected one highlighted. Clip drawing
|
||||
// to rows that fall within the canvas (sampleRowRect computes all; we skip off-screen
|
||||
// ones so a huge bank doesn't waste paint).
|
||||
// LEFT column: the bank-sample list. The selected id (fallback + "sample to add" for
|
||||
// a new zone) is highlighted. Clip rows past the visible list.
|
||||
for (int i = 0; i < static_cast<int>(samples_.size()); ++i) {
|
||||
const Rect row = sampleRowRect(layout, i);
|
||||
if (row.top >= layout.canvas.bottom) break; // past the visible area
|
||||
const Rect row = keymapSampleRowRect(layout, i);
|
||||
if (row.top >= layout.sampleList.bottom) break;
|
||||
const bool sel = !selectedId_.empty() && samples_[i].id == selectedId_;
|
||||
LICE_FillRect(&bmp, row.left, row.top, row.width(), row.height(),
|
||||
sel ? kColBtnHitBg : kColBtnBg, 1.0f, 0);
|
||||
@@ -195,6 +238,44 @@ void ReaSamplerEditor::paint(HDC hdc) {
|
||||
kRgbText);
|
||||
}
|
||||
|
||||
// RIGHT column: the zone panel. "Add Zone" button on top, then one row per zone.
|
||||
LICE_FillRect(&bmp, layout.addZoneButton.left, layout.addZoneButton.top,
|
||||
layout.addZoneButton.width(), layout.addZoneButton.height(), kColBtnBg,
|
||||
1.0f, 0);
|
||||
LICE_DrawRect(&bmp, layout.addZoneButton.left, layout.addZoneButton.top,
|
||||
layout.addZoneButton.width() - 1, layout.addZoneButton.height() - 1,
|
||||
kColBtnBorder, 1.0f, 0);
|
||||
drawTextCentered(&bmp, layout.addZoneButton, "+ Add Zone", kRgbText);
|
||||
|
||||
// The seven per-row controls, laid out left-to-right pinned to the right edge.
|
||||
const char* kCtrlGlyphs[7] = {"-", "+", "-", "+", "-", "+", "x"};
|
||||
for (int i = 0; i < static_cast<int>(map_.zones.size()); ++i) {
|
||||
const Rect row = zoneRowRect(layout, i);
|
||||
if (row.top >= layout.zoneRowArea.bottom) break; // past the visible panel
|
||||
const bool sel = (i == selectedZone_);
|
||||
LICE_FillRect(&bmp, row.left, row.top, row.width(), row.height(),
|
||||
sel ? kColZoneSelBg : kColBtnBg, 1.0f, 0);
|
||||
|
||||
const PerformanceZone& z = map_.zones[i];
|
||||
std::string label = sampleLabel(samples_, z.sampleId);
|
||||
label += " " + std::to_string(z.lowNote) + "-" + std::to_string(z.highNote);
|
||||
label += " r" + (z.rootOverride ? std::to_string(*z.rootOverride) + "*"
|
||||
: std::string("(bank)"));
|
||||
// Label area stops where the control block begins (7 * ctrl width from the right).
|
||||
const int ctrlBlockLeft = row.right - 7 * kZoneCtrlWidth;
|
||||
Rect textR{row.left + 6, row.top, ctrlBlockLeft - 4, row.bottom};
|
||||
drawText(&bmp, textR, label.c_str(), kRgbText);
|
||||
|
||||
// Draw the 7 mini-buttons.
|
||||
for (int s = 0; s < 7; ++s) {
|
||||
const int bx = ctrlBlockLeft + s * kZoneCtrlWidth;
|
||||
Rect cell{bx, row.top + 2, bx + kZoneCtrlWidth - 1, row.bottom - 2};
|
||||
LICE_FillRect(&bmp, cell.left, cell.top, cell.width(), cell.height(),
|
||||
kColCtrlBg, 1.0f, 0);
|
||||
drawTextCentered(&bmp, cell, kCtrlGlyphs[s], kRgbText);
|
||||
}
|
||||
}
|
||||
|
||||
BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY);
|
||||
}
|
||||
|
||||
@@ -202,16 +283,77 @@ void ReaSamplerEditor::onClick(int x, int y) {
|
||||
if (!processor_) return;
|
||||
RECT cr{};
|
||||
GetClientRect(childHwnd_, &cr);
|
||||
const EditorLayout layout = layoutEditor(cr.right - cr.left, cr.bottom - cr.top);
|
||||
const int row = sampleRowHitTest(layout, static_cast<int>(samples_.size()), x, y);
|
||||
if (row < 0) return;
|
||||
const KeymapEditorLayout layout =
|
||||
layoutKeymapEditor(cr.right - cr.left, cr.bottom - cr.top);
|
||||
|
||||
// Select this sample and rebuild the instrument OFF the audio thread (this WM_
|
||||
// handler runs on the UI thread). reloadFromBank reads the id we just set.
|
||||
processor_->setSelectedSampleId(samples_[row].id);
|
||||
processor_->reloadFromBank();
|
||||
selectedId_ = samples_[row].id;
|
||||
InvalidateRect(childHwnd_, nullptr, FALSE);
|
||||
// 1. Left list: pick the Tier-0 fallback / the sample a new zone will use.
|
||||
const int row =
|
||||
keymapSampleRowHitTest(layout, static_cast<int>(samples_.size()), x, y);
|
||||
if (row >= 0) {
|
||||
processor_->setSelectedSampleId(samples_[row].id);
|
||||
selectedId_ = samples_[row].id;
|
||||
// A fallback change only affects playback when the map is empty; reload so the
|
||||
// Tier-0 case updates immediately.
|
||||
processor_->reloadFromBank();
|
||||
InvalidateRect(childHwnd_, nullptr, FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. "Add Zone": append a full-keyboard zone for the currently-selected sample (root
|
||||
// from the bank intrinsic — no override until the user nudges it).
|
||||
if (addZoneHitTest(layout, x, y)) {
|
||||
if (selectedId_.empty()) return; // nothing selected to add
|
||||
PerformanceZone z;
|
||||
z.sampleId = selectedId_;
|
||||
z.lowNote = 0;
|
||||
z.highNote = 127;
|
||||
map_.zones.push_back(z);
|
||||
selectedZone_ = static_cast<int>(map_.zones.size()) - 1;
|
||||
commitMapAndReload();
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Zone rows: select a zone, nudge its range/root, or delete it.
|
||||
const ZoneHit hit =
|
||||
zoneHitTest(layout, static_cast<int>(map_.zones.size()), x, y);
|
||||
if (hit.zoneIndex < 0) return;
|
||||
selectedZone_ = hit.zoneIndex;
|
||||
|
||||
if (hit.field == ZoneField::kZoneNone) {
|
||||
InvalidateRect(childHwnd_, nullptr, FALSE); // select only, no reload
|
||||
return;
|
||||
}
|
||||
if (hit.field == ZoneField::kDelete) {
|
||||
map_.zones.erase(map_.zones.begin() + hit.zoneIndex);
|
||||
selectedZone_ = -1;
|
||||
commitMapAndReload();
|
||||
return;
|
||||
}
|
||||
|
||||
PerformanceZone& z = map_.zones[hit.zoneIndex];
|
||||
switch (hit.field) {
|
||||
case ZoneField::kLowDown: z.lowNote = clampNote(z.lowNote - 1); break;
|
||||
case ZoneField::kLowUp: z.lowNote = clampNote(z.lowNote + 1); break;
|
||||
case ZoneField::kHighDown: z.highNote = clampNote(z.highNote - 1); break;
|
||||
case ZoneField::kHighUp: z.highNote = clampNote(z.highNote + 1); break;
|
||||
case ZoneField::kRootDown: {
|
||||
const int base = z.rootOverride ? *z.rootOverride : 60;
|
||||
z.rootOverride = clampNote(base - 1); // first nudge establishes the override
|
||||
break;
|
||||
}
|
||||
case ZoneField::kRootUp: {
|
||||
const int base = z.rootOverride ? *z.rootOverride : 60;
|
||||
z.rootOverride = clampNote(base + 1);
|
||||
break;
|
||||
}
|
||||
default: break; // kZoneNone/kDelete handled above
|
||||
}
|
||||
// Keep low <= high after a range nudge (clamp the moved edge against its partner).
|
||||
if (z.lowNote > z.highNote) {
|
||||
if (hit.field == ZoneField::kLowUp) z.lowNote = z.highNote;
|
||||
else if (hit.field == ZoneField::kHighDown) z.highNote = z.lowNote;
|
||||
}
|
||||
commitMapAndReload();
|
||||
}
|
||||
|
||||
LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam,
|
||||
|
||||
Reference in New Issue
Block a user