Preview rides the real note path: mailbox drains into VoiceEngine noteOn/noteOff at the root note; PreviewCard retired; first-poll reopen heal reloads a silent-but-should-be-loaded instrument
This commit is contained in:
@@ -579,7 +579,7 @@ void ReaSamplerProcessor::publishBuiltLocked(std::unique_ptr<LoadedInstrument> b
|
||||
|
||||
void ReaSamplerProcessor::rebuildVoiceEngine() {
|
||||
// OFF THE AUDIO THREAD (the editor's voice-deck click handlers). See the header contract:
|
||||
// a voice-param change touches NO audio data, so this rebuilds the engine + preview card
|
||||
// a voice-param change touches NO audio data, so this rebuilds the engine
|
||||
// around a COPY of the live instrument's already-decoded keymap — no bridge, no disk —
|
||||
// and publishes through the same drain-slot swap, so ringing tails survive.
|
||||
std::lock_guard<std::mutex> lock(reloadMutex_);
|
||||
@@ -718,9 +718,24 @@ ReaSamplerProcessor::pollBankSync(bool isFocusedTarget) {
|
||||
!firstPoll && bankGenerationChanged(lastSeenBankGeneration_, currentGen);
|
||||
lastSeenBankGeneration_ = currentGen;
|
||||
|
||||
if (genChanged || result.applied) {
|
||||
// REOPEN HEAL: the baseline-without-reload assumption above fails when the setState-time
|
||||
// reload ran BEFORE the extension's PROJEXTSTATE was parseable during project load — the
|
||||
// bridge read came back empty, so live_ installed SILENCE while the restored state (a
|
||||
// selection or zones) says something SHOULD be loaded. Without this, the swallowed
|
||||
// baseline left the instrument (preview AND host MIDI) dead until some param change
|
||||
// forced a reload. Detect the mismatch on the first poll and reload; a deliberately
|
||||
// empty instance (no selection, no zones) never churns, and a load that legitimately
|
||||
// failed (missing WAV) costs one redundant, harmless reload on editor open.
|
||||
bool healReload = false;
|
||||
if (firstPoll && live_.load(std::memory_order_acquire) == nullptr) {
|
||||
healReload = !selectedSampleId().empty() || !performanceMap().empty();
|
||||
}
|
||||
|
||||
if (genChanged || result.applied || healReload) {
|
||||
reloadFromBank(); // atomic pointer-swap handoff — glitch-free mid-play (S4 graveyard)
|
||||
result.reloaded = genChanged; // report S9 vs S8 distinctly for the editor's reaction
|
||||
// Report the reload (S9 change or reopen heal) distinctly from an S8 apply so the
|
||||
// editor re-snapshots its bank view.
|
||||
result.reloaded = genChanged || healReload;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -758,7 +773,7 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
||||
processGeneration_.store(heldGen, std::memory_order_release);
|
||||
|
||||
// Phase S drain retirement: publish whether the drain snapshot is FULLY idle (every engine
|
||||
// voice AND its preview card silent) by naming its OWN installedAt (0 = no drain / still
|
||||
// voice silent) by naming its OWN installedAt (0 = no drain / still
|
||||
// sounding). Evaluated at block START — idleness is monotone for a drain (it receives no
|
||||
// note-ons), so a snapshot observed idle here stays idle; a tail that dies mid-block simply
|
||||
// publishes one block later. Bounded scan (<= maxVoices), relaxed store — RT-safe.
|
||||
@@ -797,28 +812,17 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
||||
// CC 120 (All Sounds Off): hard-stop semantics — immediate silence regardless
|
||||
// of play mode, including Trigger one-shots that ignore CC 123. This is the
|
||||
// true "panic" for a ringing one-shot (e.g. a full-length capture).
|
||||
// Both clear the mono held stack. Both apply to live AND drain, engine AND preview.
|
||||
// allNotesOff / allSoundsOff / releaseAll / hardStop are RT-safe (no allocation,
|
||||
// bounded scans).
|
||||
// Both clear the mono held stack. Both apply to live AND drain. A ringing
|
||||
// preview note is a real engine voice since the PreviewCard retirement, so
|
||||
// the panics cover it with no separate routing. allNotesOff / allSoundsOff
|
||||
// are RT-safe (no allocation, bounded scans).
|
||||
const auto cc = static_cast<int>(e.midiCCOut.controlNumber);
|
||||
if (cc == kCtrlAllSoundsOff) {
|
||||
if (inst) {
|
||||
inst->engine.allSoundsOff();
|
||||
inst->preview.hardStop();
|
||||
}
|
||||
if (drain) {
|
||||
drain->engine.allSoundsOff();
|
||||
drain->preview.hardStop();
|
||||
}
|
||||
if (inst) inst->engine.allSoundsOff();
|
||||
if (drain) drain->engine.allSoundsOff();
|
||||
} else if (cc == kCtrlAllNotesOff) {
|
||||
if (inst) {
|
||||
inst->engine.allNotesOff();
|
||||
inst->preview.releaseAll();
|
||||
}
|
||||
if (drain) {
|
||||
drain->engine.allNotesOff();
|
||||
drain->preview.releaseAll();
|
||||
}
|
||||
if (inst) inst->engine.allNotesOff();
|
||||
if (drain) drain->engine.allNotesOff();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -827,10 +831,11 @@ 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.
|
||||
// Phase S: preview note-on/off drive the dedicated PREVIEW CARD — a single voice structurally
|
||||
// OUTSIDE the MIDI pool, so a full pool can never drop a preview and a preview can never
|
||||
// steal a playing MIDI voice (the FA1-review isolation fix). Host MIDI routes ONLY to the
|
||||
// engine (above); the card is summed alongside it in the render below.
|
||||
// Preview redesign: the drained requests drive the MAIN VoiceEngine — the exact
|
||||
// noteOn/noteOff calls the host MIDI marshal above makes — so a preview note is a real
|
||||
// voice: it counts against the voice count, can steal / be stolen, and respects
|
||||
// Poly/Mono + Retrigger/Legato (deliberate reversal of the retired PreviewCard's
|
||||
// isolation). The editor posts the root note, so it plays at unity.
|
||||
// Consume (advance the sequence) even when inst is null so a note-on posted while no instrument
|
||||
// is loaded does not re-fire stale on the next instrument load.
|
||||
{
|
||||
@@ -841,7 +846,7 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
||||
if (inst) {
|
||||
const int vel = static_cast<int>((on >> 8) & 0xFF);
|
||||
const int note = static_cast<int>(on & 0xFF);
|
||||
if (vel > 0) inst->preview.noteOn(note, vel);
|
||||
if (vel > 0) inst->engine.noteOn(note, vel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -850,11 +855,11 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
||||
const std::uint16_t offSeq = static_cast<std::uint16_t>(off >> 16);
|
||||
if (offSeq != 0 && offSeq != previewOffConsumed_) {
|
||||
previewOffConsumed_ = offSeq;
|
||||
// Route the preview note-off to BOTH cards (mirror of the host note-off): a
|
||||
// Route the preview note-off to BOTH engines (mirror of the host note-off): a
|
||||
// preview held across a reload — e.g. a curve edit committed mid-press — must
|
||||
// release the old-snapshot card now draining, not just the (fresh) live one.
|
||||
if (inst) inst->preview.noteOff(static_cast<int>(off & 0xFF));
|
||||
if (drain) drain->preview.noteOff(static_cast<int>(off & 0xFF));
|
||||
// release the old-snapshot voice now draining, not just the (fresh) live one.
|
||||
if (inst) inst->engine.noteOff(static_cast<int>(off & 0xFF));
|
||||
if (drain) drain->engine.noteOff(static_cast<int>(off & 0xFF));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -892,14 +897,8 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
||||
// path (both channels equal), so a mono capture in stereo mode is centered, not silent.
|
||||
// The DRAIN engine's ringing tails ADD on top (render mixes into the cleared buffer).
|
||||
for (int32 i = 0; i < frames; ++i) { ch0[i] = 0.f; ch1[i] = 0.f; }
|
||||
if (inst) {
|
||||
inst->engine.render(ch0, ch1, static_cast<std::size_t>(frames));
|
||||
inst->preview.render(ch0, ch1, static_cast<std::size_t>(frames));
|
||||
}
|
||||
if (drain) {
|
||||
drain->engine.render(ch0, ch1, static_cast<std::size_t>(frames));
|
||||
drain->preview.render(ch0, ch1, static_cast<std::size_t>(frames));
|
||||
}
|
||||
if (inst) inst->engine.render(ch0, ch1, static_cast<std::size_t>(frames));
|
||||
if (drain) drain->engine.render(ch0, ch1, static_cast<std::size_t>(frames));
|
||||
// FB1 post-mixer master gain: ramp gainCurrent_ toward the atomic target per-sample so
|
||||
// continuous knob drags produce no zipper noise and the true-zero bottom causes no click.
|
||||
// Applied AFTER the voice sum and BEFORE the extra-channel mirror + peak so both see the
|
||||
@@ -943,14 +942,8 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
||||
// Mono: render into channel 0, replicate to any extra channels (mono bus is 1 channel;
|
||||
// the replicate is defensive for a host that still hands >1 channel on a mono bus).
|
||||
for (int32 i = 0; i < frames; ++i) ch0[i] = 0.f;
|
||||
if (inst) {
|
||||
inst->engine.render(ch0, static_cast<std::size_t>(frames));
|
||||
inst->preview.render(ch0, static_cast<std::size_t>(frames));
|
||||
}
|
||||
if (drain) {
|
||||
drain->engine.render(ch0, static_cast<std::size_t>(frames));
|
||||
drain->preview.render(ch0, static_cast<std::size_t>(frames));
|
||||
}
|
||||
if (inst) inst->engine.render(ch0, static_cast<std::size_t>(frames));
|
||||
if (drain) drain->engine.render(ch0, static_cast<std::size_t>(frames));
|
||||
// FB1 post-mixer master gain (mono path) — same ramp contract as the stereo branch:
|
||||
// post-sum, pre-peak/replicate, per-sample gainCurrent_ ramp toward target, RT-safe.
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user