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
+39 -106
View File
@@ -1,8 +1,8 @@
// editor_session.cpp — the ReaSamplerEditor's session/bridge state: construction, the
// live-bank snapshot (refreshFromBank / rebuildVisible), the sync tick, the
// commit-and-reload seam, selection loading, the picked-capture marker resolution/upsert
// helpers, and the decoded-PCM + peak thumbnail caches. UI thread only; every edit commits
// off the audio thread via the processor's reloadInstrument.
// commit-and-reload seam, selection loading, the loaded capture's marker resolution, and
// the decoded-PCM + peak thumbnail caches. UI thread only; every edit commits off the audio
// thread via the processor's reloadInstrument.
#include "shell/instrument/reasampler_editor.h"
@@ -38,8 +38,8 @@ using util::readFileBytes;
ReaSamplerEditor::ReaSamplerEditor(ReaSamplerProcessor* processor)
: CPluginView(nullptr), processor_(processor) {
// Default view size, tuned to the Sample-face band heights: title + hero waveform +
// cluster + control strip. 840x620 clears the full face without scroll on 1080p.
// Default view size, tuned to the three band heights: chrome + two-lane waveform +
// deck row. 840x620 clears the full face without scroll on 1080p.
ViewRect r(0, 0, 840, 620);
setRect(r);
}
@@ -53,30 +53,21 @@ void ReaSamplerEditor::refreshFromBank() {
banks_.clear();
visible_.clear();
selectedId_.clear();
map_.zones.clear();
selectedZone_ = -1;
params_ = InstrumentParams{};
return;
}
auto banksJson = processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey);
samples_ = banksJson ? listSamples(*banksJson) : std::vector<SampleChoice>{};
banks_ = banksJson ? listBanks(*banksJson) : std::vector<BankChoice>{};
selectedId_ = processor_->selectedSampleId();
const auto prevZoneCount = static_cast<int>(map_.zones.size());
map_ = processor_->performanceMap();
params_ = processor_->instrumentParams();
channelMode_ = processor_->channelMode();
voiceCount_ = processor_->voiceCount();
voiceMode_ = processor_->voiceMode();
monoTrigger_ = processor_->monoTrigger();
if (selectedZone_ >= static_cast<int>(map_.zones.size())) selectedZone_ = -1;
// A refresh that emptied the selection closes the curve popup — an open-but-invisible
// modal would otherwise swallow clicks on the empty state.
if (selectedId_.empty() && map_.zones.empty()) curvePopupOpen_ = false;
// On the Zone surface, close the popup if the zone count changed at all — a mid-list
// deletion can leave selectedZone_ in range but silently naming a different zone.
if (view_ == View::kZone && curvePopupOpen_) {
const auto newZoneCount = static_cast<int>(map_.zones.size());
if (selectedZone_ < 0 || newZoneCount != prevZoneCount) curvePopupOpen_ = false;
}
if (selectedId_.empty()) curvePopupOpen_ = false;
// Drop a filter that names a bank no longer present.
if (!activeFilterBankId_.empty()) {
bool found = false;
@@ -127,13 +118,13 @@ void ReaSamplerEditor::onSyncTimer() {
#endif // _WIN32
void ReaSamplerEditor::commitAndReload() {
// UI thread only. Publishes the edited selection + zones, then rebuilds off the audio
// thread. The reload also copies the picked capture's file ref + intrinsics into the
// instance-owned refs table — a browser load is the moment the instance becomes
// UI thread only. Publishes the edited selection + parameter set, then rebuilds off the
// audio thread. The reload also copies the loaded capture's file ref + intrinsics into
// the instance-owned refs table — a browser load is the moment the instance becomes
// self-contained for that sample.
if (!processor_) return;
processor_->setSelectedSampleId(selectedId_);
processor_->setPerformanceMap(map_);
processor_->setInstrumentParams(params_);
processor_->reloadInstrument();
// The reload may have auto-defaulted the channel mode (implicit only) — re-read so the
// toggle draws what the engine actually decoded with.
@@ -144,23 +135,25 @@ void ReaSamplerEditor::commitAndReload() {
}
void ReaSamplerEditor::loadSelection(const std::string& id) {
// A Sample-face load REPLACES the loaded sound: the previous sample's materialized
// full-range zone must not linger, or first-match resolve would keep playing it.
// Authored Zone-view maps (narrow key ranges) are left untouched.
// A load REPLACES the loaded sound. The shaping parameters (play mode, envelopes, pitch
// engine, key-track, velocity curve) are NOT reset — the one set governs whatever is
// loaded, so a load swaps the sound and keeps the settings. The three CAPTURE-ANCHORED
// overrides are: a root, a loop span and a start frame all name positions in the
// OUTGOING capture and mean nothing in the new one, so they clear and the new capture
// plays from its own bank intrinsics.
selectedId_ = id;
if (reconcileSingleCaptureZones(map_, selectedId_)) {
selectedZone_ = map_.zones.empty() ? -1 : 0;
}
params_.rootOverride.reset();
params_.loopOverride.reset();
params_.startPoint.reset();
commitAndReload();
}
ReaSamplerEditor::SetupMarkers ReaSamplerEditor::pickedMarkers(std::int64_t frames) const {
SetupMarkers m;
// Seed from the bank's intrinsic loop (fact about the file), then let a per-zone override
// for the picked id win (the instrument's performance choice). Read the loop intrinsic from
// the live bank blob (the same path selectSample uses); when that is not readable (extension
// absent / not yet parsed) the instance-owned ref carries the same intrinsics. The override
// lives in map_.
// Seed from the bank's intrinsic loop (fact about the file), then let the parameter set's
// override win (the instrument's performance choice). Read the loop intrinsic from the
// live bank blob (the same path selectSample uses); when that is not readable (extension
// absent / not yet parsed) the instance-owned ref carries the same intrinsics.
if (processor_) {
std::optional<SelectedSample> sel;
auto banksJson =
@@ -176,17 +169,13 @@ ReaSamplerEditor::SetupMarkers ReaSamplerEditor::pickedMarkers(std::int64_t fram
m.loopEnd = sel->loop.end;
}
}
// The override (loop + start) on a zone for the picked id supersedes the intrinsic.
for (const PerformanceZone& z : map_.zones) {
if (z.sampleId != selectedId_) continue;
if (z.loopOverride) {
m.hasLoop = z.loopOverride->hasLoop;
m.loopStart = z.loopOverride->start;
m.loopEnd = z.loopOverride->end;
}
if (z.startPoint) m.start = *z.startPoint;
break;
// The parameter set's override (loop + start) supersedes the intrinsic.
if (params_.loopOverride) {
m.hasLoop = params_.loopOverride->hasLoop;
m.loopStart = params_.loopOverride->start;
m.loopEnd = params_.loopOverride->end;
}
if (params_.startPoint) m.start = *params_.startPoint;
// Default an unset loop's end to the sample length so the loop markers have somewhere sane
// to sit before the user drags (loopStart stays 0). The "no loop" state is m.hasLoop==false;
// the markers are still drawn (drag one to CREATE a loop).
@@ -194,79 +183,23 @@ ReaSamplerEditor::SetupMarkers ReaSamplerEditor::pickedMarkers(std::int64_t fram
return m;
}
int ReaSamplerEditor::upsertPickedOverride(const SetupMarkers& m) {
// Find-or-append the zone for selectedId_ and write the loop/start override fields. The
// bank intrinsic is never written (read-only bank consumer). selectedId_ must be
// non-empty; callers are responsible for that guard. Returns the zone index (0-based) so
// callers can update selectedZone_.
void ReaSamplerEditor::applyMarkers(const SetupMarkers& m) {
// Write the edited markers into the parameter set as the loop/start override. The bank
// intrinsic is never written (read-only bank consumer).
SampleLoop loop;
loop.hasLoop = m.hasLoop;
loop.start = m.loopStart;
loop.end = m.loopEnd;
for (int i = 0; i < static_cast<int>(map_.zones.size()); ++i) {
PerformanceZone& z = map_.zones[static_cast<std::size_t>(i)];
if (z.sampleId == selectedId_) {
z.loopOverride = loop;
z.startPoint = m.start;
return i;
}
}
PerformanceZone z;
z.sampleId = selectedId_;
z.lowNote = 0;
z.highNote = 127;
z.loopOverride = loop;
z.startPoint = m.start;
map_.zones.push_back(z);
return static_cast<int>(map_.zones.size()) - 1;
}
PerformanceZone ReaSamplerEditor::effectiveSampleZone() const {
// The picked id's one-zone override, if the map already carries one; else a product-default
// zone bound to the picked id (not appended — a read-only resolve; a control edit
// materializes it via ensureSampleZone).
for (const PerformanceZone& z : map_.zones) {
if (z.sampleId == selectedId_) return z;
}
PerformanceZone z;
z.sampleId = selectedId_;
z.lowNote = 0;
z.highNote = 127;
return z;
params_.loopOverride = loop;
params_.startPoint = m.start;
}
int ReaSamplerEditor::effectiveRoot() const {
int root = 60;
if (params_.rootOverride) return *params_.rootOverride;
for (const SampleChoice& s : samples_) {
if (s.id == selectedId_ && s.rootNote) root = *s.rootNote;
if (s.id == selectedId_ && s.rootNote) return *s.rootNote;
}
for (const PerformanceZone& z : map_.zones) {
if (z.sampleId == selectedId_ && z.rootOverride) root = *z.rootOverride;
}
return root;
}
int ReaSamplerEditor::ensureSampleZone() {
if (selectedId_.empty()) return -1;
for (int i = 0; i < static_cast<int>(map_.zones.size()); ++i) {
if (map_.zones[static_cast<std::size_t>(i)].sampleId == selectedId_) return i;
}
PerformanceZone z;
z.sampleId = selectedId_;
z.lowNote = 0;
z.highNote = 127;
map_.zones.push_back(z);
return static_cast<int>(map_.zones.size()) - 1;
}
void ReaSamplerEditor::commitPickedMarkers(const SetupMarkers& m) {
// Materialize the edited markers as a per-zone loop/start override on the picked id (upsert):
// a full-keyboard zone carrying the override. This plays identically to the un-zoned single
// capture (one chromatic zone) and round-trips through the component state; the zone becomes
// visible if the user opens the Zones panel. The bank intrinsic is never written.
if (selectedId_.empty()) return;
upsertPickedOverride(m);
commitAndReload();
return 60;
}
const std::vector<AudioSample>& ReaSamplerEditor::monoPcmFor(const std::string& sampleId) {