Phase S Wave 2: three-view ReaSampler 9000 editor (Sample home / Browse modal / Zone)

Retire the flat toggle; Sample-home face wires the Wave-1 envelope overlay + draggable nodes, preview button + velocity knob, Mono/Stereo, key-track. Browse reduced to a confirm/cancel modal; Zone retained with piano-pattern strip. RT-safe off-thread preview-note mailbox added.
This commit is contained in:
2026-07-27 14:17:58 -04:00
parent 203005e6cf
commit c12a374b88
5 changed files with 1094 additions and 542 deletions
+66 -4
View File
@@ -197,9 +197,12 @@ tresult PLUGIN_API ReaSamplerProcessor::setState(IBStream* state) {
channelMode_ = cs.channelMode;
}
applyOutputArrangement(cs.channelMode);
// S-VIEW-4: restore the per-instance preview velocity. No mutex — setState is a load-time
// call serialized by the host; there is no concurrent writer before Wave 2.
previewVelocity_ = cs.previewVelocity;
// S-VIEW-4: restore the per-instance preview velocity. Guarded by previewMutex_ — since Wave 2
// the editor's velocity knob is a concurrent UI-thread writer.
{
std::lock_guard<std::mutex> lock(previewMutex_);
previewVelocity_ = cs.previewVelocity;
}
// Rebuild from the restored state (off-thread — setState is a load-time call).
reloadFromBank();
return kResultOk;
@@ -220,7 +223,7 @@ tresult PLUGIN_API ReaSamplerProcessor::getState(IBStream* state) {
std::lock_guard<std::mutex> lock(assignMarkerMutex_);
state_out.lastConsumedAssignGeneration = lastConsumedAssignGeneration_; // S8 reader marker
}
state_out.previewVelocity = previewVelocity_; // S-VIEW-4: persist the preview strike velocity
state_out.previewVelocity = previewVelocity(); // S-VIEW-4: persist the preview strike velocity
const std::vector<std::uint8_t> bytes = serializeComponentState(state_out);
if (!bytes.empty()) {
const tresult wr = state->write(const_cast<std::uint8_t*>(bytes.data()),
@@ -255,6 +258,43 @@ ChannelMode ReaSamplerProcessor::channelMode() {
return channelMode_;
}
std::uint8_t ReaSamplerProcessor::previewVelocity() {
std::lock_guard<std::mutex> lock(previewMutex_);
return previewVelocity_;
}
void ReaSamplerProcessor::setPreviewVelocity(std::uint8_t velocity) {
// Clamp to the MIDI-note range [1,127] (0 would be a note-off by convention — a preview
// strike must sound). The editor's knob maps its 0..1 domain into this range before calling.
if (velocity < 1) velocity = 1;
if (velocity > 127) velocity = 127;
std::lock_guard<std::mutex> lock(previewMutex_);
previewVelocity_ = velocity;
}
void ReaSamplerProcessor::previewNoteOn(int note) {
if (note < 0) note = 0;
if (note > 127) note = 127;
const std::uint8_t vel = previewVelocity(); // latch the current knob value into the request
// Advance the sequence (wrapping; process compares for inequality, so a wrap is harmless as
// long as we never land back on the exact value the audio thread last consumed in one step —
// 16 bits gives 65535 posts between collisions, unreachable at UI-click rates).
const std::uint16_t seq = ++previewOnSeq_ == 0 ? ++previewOnSeq_ : previewOnSeq_;
const std::uint32_t packed = (static_cast<std::uint32_t>(seq) << 16) |
(static_cast<std::uint32_t>(vel) << 8) |
static_cast<std::uint32_t>(note & 0xFF);
previewOnRequest_.store(packed, std::memory_order_release);
}
void ReaSamplerProcessor::previewNoteOff(int note) {
if (note < 0) note = 0;
if (note > 127) note = 127;
const std::uint16_t seq = ++previewOffSeq_ == 0 ? ++previewOffSeq_ : previewOffSeq_;
const std::uint32_t packed = (static_cast<std::uint32_t>(seq) << 16) |
static_cast<std::uint32_t>(note & 0xFF);
previewOffRequest_.store(packed, std::memory_order_release);
}
void ReaSamplerProcessor::applyOutputArrangement(ChannelMode mode) {
// Set the single output bus's SpeakerArrangement to the mode's arrangement so getBusInfo /
// getBusArrangement report the right channel count. The default getBusArrangement (from the
@@ -525,6 +565,28 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
}
}
// S-VIEW-4 preview mailbox: drain the off-thread preview-trigger requests (a single relaxed
// atomic load each — RT-safe). A request is NEW when its packed sequence differs from the last
// one we consumed; fire it once, then latch the sequence so the same request never re-fires.
// Preview note-on/off drive the SAME voice engine as host MIDI (a preview is just a note with
// no MIDI wire) — off-thread posted, audio-thread consumed, no lock, no allocation.
if (inst) {
const std::uint32_t on = previewOnRequest_.load(std::memory_order_acquire);
const std::uint16_t onSeq = static_cast<std::uint16_t>(on >> 16);
if (onSeq != 0 && onSeq != previewOnConsumed_) {
previewOnConsumed_ = onSeq;
const int vel = static_cast<int>((on >> 8) & 0xFF);
const int note = static_cast<int>(on & 0xFF);
if (vel > 0) inst->engine.noteOn(note, vel);
}
const std::uint32_t off = previewOffRequest_.load(std::memory_order_acquire);
const std::uint16_t offSeq = static_cast<std::uint16_t>(off >> 16);
if (offSeq != 0 && offSeq != previewOffConsumed_) {
previewOffConsumed_ = offSeq;
inst->engine.noteOff(static_cast<int>(off & 0xFF));
}
}
if (data.numOutputs <= 0 || !data.outputs || data.numSamples <= 0) {
embedPeak_.store(0.f, std::memory_order_relaxed);
return kResultOk;