Merge pS-w2-t1-shell: three-view ReaSampler 9000 editor (S-VIEW-1/2/5/8 + Wave-1 module wiring)
This commit is contained in:
+18
-1
@@ -762,6 +762,13 @@ add_library(param_slider STATIC src/vst/param_slider.cpp)
|
||||
target_include_directories(param_slider PUBLIC src/vst)
|
||||
target_link_libraries(param_slider PUBLIC editor_geometry)
|
||||
|
||||
# trigger_seam (Phase S-VIEW-3) — PURE Trigger-mode frames<->fraction converter for the TRIGGER
|
||||
# SEAM documented in envelope_overlay.h: triggerPlayLength / framesToFadeFraction /
|
||||
# fadeFractionToFrames. Owns the one formula so pack (draw) and unpack (commit) are provably
|
||||
# consistent. No shell/LICE/REAPER types — only <cstdint>. NEITHER SDK.
|
||||
add_library(trigger_seam STATIC src/vst/trigger_seam.cpp)
|
||||
target_include_directories(trigger_seam PUBLIC src/vst)
|
||||
|
||||
# envelope_overlay (Phase S-VIEW-3) — PURE amp-envelope -> polyline geometry for the Sample-view
|
||||
# envelope overlay: AHDSR (Gate) / fade+%-length (Trigger) params + the sample's wall-clock
|
||||
# duration -> a breakpoint polyline in the waveform rect, at the same time base waveform_view maps.
|
||||
@@ -835,6 +842,12 @@ add_executable(param_slider_tests tests/test_param_slider.cpp)
|
||||
target_link_libraries(param_slider_tests PRIVATE param_slider)
|
||||
add_test(NAME param_slider_tests COMMAND param_slider_tests)
|
||||
|
||||
# trigger_seam (S-VIEW-3): the pure Trigger-mode frames<->fraction converter.
|
||||
# Links ONLY trigger_seam — no editor_geometry dep, the plainest data-boundary proof possible.
|
||||
add_executable(trigger_seam_tests tests/test_trigger_seam.cpp)
|
||||
target_link_libraries(trigger_seam_tests PRIVATE trigger_seam)
|
||||
add_test(NAME trigger_seam_tests COMMAND trigger_seam_tests)
|
||||
|
||||
# envelope_overlay (S-VIEW-3): the pure amp-envelope -> polyline geometry (Gate AHDSR + Trigger
|
||||
# fade/%-length) at the waveform time base. Links ONLY envelope_overlay (+ its editor_geometry
|
||||
# dep) — NEITHER SDK — the plain-data-boundary proof.
|
||||
@@ -1050,10 +1063,14 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
|
||||
# module above): theme supplies role->KitColor + spectralColor, component_geometry the
|
||||
# KitBox/button/slider geometry, bank_grid the shared dB display-compression the waveform
|
||||
# primitive uses. All REAPER/SWELL-free.
|
||||
# envelope_overlay + envelope_edit (S-VIEW-3): the pure amp-envelope -> polyline forward map
|
||||
# and the node hit-test + pixel-delta -> clamped-param inverse map the Sample-view envelope
|
||||
# overlay draws + drags against; envelope_edit links envelope_overlay transitively (shared
|
||||
# node vocabulary + timeToX/levelToY). Both engine-free, DAW-verified in the shell.
|
||||
target_link_libraries(reasampler_vst PRIVATE vst3_sdk editor_geometry bridge_marshal
|
||||
sample_map capture_paths embed_strip app_version capture_browser keyboard_strip
|
||||
waveform_view bank_sync browser_scroll note_entry param_slider
|
||||
theme component_geometry bank_grid)
|
||||
theme component_geometry bank_grid trigger_seam envelope_overlay envelope_edit)
|
||||
# SDK_INC gives reaper_vst3_interfaces.h + reaper_plugin_functions.h for the bridge;
|
||||
# WDL_INC gives LICE for the editor. The VST3 SDK headers come from vst3_sdk PUBLIC.
|
||||
target_include_directories(reasampler_vst PRIVATE ${SDK_INC} ${WDL_INC})
|
||||
|
||||
+868
-520
File diff suppressed because it is too large
Load Diff
+108
-14
@@ -31,6 +31,8 @@
|
||||
#include "public.sdk/source/common/pluginview.h"
|
||||
|
||||
#include "editor_geometry.h" // Rect (the shell's sub-rect type, shared with the pure modules)
|
||||
#include "envelope_edit.h" // EnvClampBounds / NodeHit (S-VIEW-3 envelope node hit-test/edit)
|
||||
#include "envelope_overlay.h" // AmpEnvelope / EnvNode (S-VIEW-3 envelope overlay draw seam)
|
||||
#include "param_slider.h" // ControlRow (the S12/S15/S16 control-surface geometry)
|
||||
#include "peaks.h" // Envelope (the cached peak thumbnail)
|
||||
#include "sample_map.h" // SampleChoice, BankChoice, PerformanceMap (the shell's snapshot)
|
||||
@@ -64,16 +66,21 @@ protected:
|
||||
Steinberg::tresult PLUGIN_API onSize(Steinberg::ViewRect* newSize) override;
|
||||
|
||||
private:
|
||||
// Which face the editor shows. The browser is the default; the Zones panel is the
|
||||
// demoted opt-in view reached by the toggle. Both draw over the same snapshotted bank.
|
||||
enum class View { kBrowser, kZones };
|
||||
// Which face the editor shows (S-VIEW-1, three-view model). Sample is the HOME/default
|
||||
// face (the loaded capture). Browse is a full-window MODAL picker overlaid on Sample
|
||||
// (select + confirm/cancel changes the loaded capture, then dismisses). Zone is the
|
||||
// dedicated multi-zone keymap surface, button-summoned. All three draw over the same
|
||||
// snapshotted bank; Browse + Zone return to Sample when dismissed.
|
||||
enum class View { kSample, kBrowse, kZone };
|
||||
|
||||
// What a mouse drag is currently editing (the drag-state machine). kNone = no drag in
|
||||
// flight. The zone-edit grabs mirror keyboard_strip::ZoneGrab; kRootMarker is the
|
||||
// single-capture root drag on the setup strip; kWaveMarker is a draggable start/loop
|
||||
// marker on the S11 waveform surface (which marker is in waveMarker_).
|
||||
// marker on the S11 waveform surface (which marker is in waveMarker_); kEnvNode is a
|
||||
// draggable envelope breakpoint on the Sample-view hero overlay (S-VIEW-3, which node in
|
||||
// envNode_).
|
||||
enum class DragKind { kNone, kRootMarker, kZoneLow, kZoneHigh, kZoneBody, kWaveMarker,
|
||||
kScrollThumb, kParamSlider };
|
||||
kScrollThumb, kParamSlider, kEnvNode };
|
||||
|
||||
// The parameter controls on the setup surface (S12 AHDSR + the S15/S16 control surfaces).
|
||||
// The int value is the ControlDesc id the pure param_slider hit-test returns; the shell
|
||||
@@ -93,6 +100,7 @@ private:
|
||||
kPitchEnvAttack, // AD pitch attack (S16)
|
||||
kPitchEnvDecay, // AD pitch decay (S16)
|
||||
kPitchEnvDepth, // AD pitch depth in +/- semitones (S16)
|
||||
kKeyTrack, // S-VIEW-6 key-tracking 0..200% (lives on PerformanceZone, not ZonePlaySeconds)
|
||||
kCount
|
||||
};
|
||||
|
||||
@@ -111,13 +119,17 @@ private:
|
||||
// bank_panel's L2 hover model.
|
||||
enum class HoverKind {
|
||||
kNone,
|
||||
kToggleBrowser, // the Browser toggle segment
|
||||
kToggleZones, // the Zones toggle segment
|
||||
kNavBrowse, // the Sample-view "Browse" title-band button (opens the Browse modal)
|
||||
kNavZone, // the Sample-view "Zone" title-band button (opens the Zone surface)
|
||||
kBack, // the Browse/Zone "back" affordance (returns to Sample)
|
||||
kSearchBox, // the browser search box
|
||||
kFilterTab, // a bank-filter tab (index = tab ordinal, 0 = All)
|
||||
kCard, // a capture card (index = visible_ index)
|
||||
kBrowseConfirm, // the Browse modal "Load" confirm button
|
||||
kBrowseCancel, // the Browse modal "Cancel" button
|
||||
kChanMono, // the mono channel-mode segment
|
||||
kChanStereo, // the stereo channel-mode segment
|
||||
kPreview, // the Sample-view preview-trigger button
|
||||
kAddZone, // the "+ Add Zone" button
|
||||
kDeleteZone, // the "Delete" zone button
|
||||
kControl, // a param-panel control row (index = ControlDesc id)
|
||||
@@ -131,16 +143,30 @@ private:
|
||||
|
||||
#ifdef _WIN32
|
||||
void paint(HDC hdc);
|
||||
void paintBrowser(LICE_IBitmap* bmp, int w, int h);
|
||||
void paintSetup(LICE_IBitmap* bmp, const Rect& area);
|
||||
void paintZones(LICE_IBitmap* bmp, int w, int h);
|
||||
void paintSample(LICE_IBitmap* bmp, int w, int h); // S-VIEW-2 home face
|
||||
void paintBrowse(LICE_IBitmap* bmp, int w, int h); // S-VIEW-5 modal picker overlay
|
||||
void paintZone(LICE_IBitmap* bmp, int w, int h); // S-VIEW-8 zone surface
|
||||
void paintEmptyState(LICE_IBitmap* bmp, const Rect& area);
|
||||
void paintControls(LICE_IBitmap* bmp, const Rect& panel); // S12/S15/S16 param surface
|
||||
void paintControls(LICE_IBitmap* bmp, const Rect& panel, const PerformanceZone& zone); // S12/S15/S16 + keyTrack
|
||||
// Trace the S-VIEW-3 amp-envelope overlay + its draggable node handles over `waveArea` for
|
||||
// `zone`'s play params, at the sample's wall-clock duration. Shared by the Sample hero band.
|
||||
void paintEnvelopeOverlay(LICE_IBitmap* bmp, const Rect& waveArea, const PerformanceZone& zone,
|
||||
std::int64_t frames);
|
||||
|
||||
void onMouseDown(int x, int y);
|
||||
void onMouseMove(int x, int y);
|
||||
void onMouseUp(int x, int y);
|
||||
|
||||
// Route a click at (x,y) into the param control panel `panel` editing map_.zones[zoneIndex]:
|
||||
// a toggle segment commits immediately, a slider grab starts a live param-drag (kParamSlider),
|
||||
// the keyTrack slider likewise (against the zone's keyTrack scalar). Returns true when the
|
||||
// click landed on a control (handled). `zoneIndex` must be a valid index into map_.zones.
|
||||
bool handleControlClick(int zoneIndex, const Rect& panel, int x, int y);
|
||||
// Apply a slider/toggle interaction to map_.zones[zoneIndex] for control `id`: routes ordinary
|
||||
// controls through applyControl against the zone's play struct, and kKeyTrack against the
|
||||
// zone's keyTrack scalar (0..200% over the slider's 0..1). Used by both the click + drag paths.
|
||||
void applyZoneControl(int zoneIndex, int id, double value, int segment);
|
||||
|
||||
// Resolve the interactive element under (x, y) into hover_ (Phase L, L3). Called from
|
||||
// WM_MOUSEMOVE (also while a drag is in flight — the resolved element just isn't used
|
||||
// for a hover repaint mid-drag). Repaints only when the hovered element changed, so an
|
||||
@@ -244,6 +270,53 @@ private:
|
||||
// into the control's stored domain) or a toggle's `segment` (0/1). Mutates `play` in place.
|
||||
void applyControl(int id, ZonePlaySeconds& play, double value, int segment) const;
|
||||
|
||||
// --- S-VIEW-3 envelope overlay seam (frames <-> fraction converter) ----------
|
||||
//
|
||||
// envelope_overlay's AmpEnvelope is a DERIVED VIEW, not a TriggerParams copy: it stores the
|
||||
// Trigger fades as FRACTIONS of the played span, while the zone stores them as SOURCE FRAMES.
|
||||
// These two members own the non-trivial conversion on BOTH paths (documented in
|
||||
// envelope_overlay.h's TRIGGER SEAM note). `frames` is the sample's total source frame count;
|
||||
// `rate` is the live sample rate (the wall-clock AHDSR seconds are rate-free and copy 1-to-1,
|
||||
// but the Trigger played-span math needs the frame count).
|
||||
|
||||
// PACK (draw): zone play params -> AmpEnvelope. Copies AHDSR seconds directly; derives the
|
||||
// Trigger fade fractions from the source-frame fades over the played span.
|
||||
// `startFrame` is the zone's effective start point (zone.startPoint.value_or(0)).
|
||||
AmpEnvelope packEnvelope(const ZonePlaySeconds& play, std::int64_t frames,
|
||||
std::int64_t startFrame) const;
|
||||
|
||||
// UNPACK (commit): an edited AmpEnvelope -> the zone's play params. Copies AHDSR seconds
|
||||
// directly; converts the Trigger fade fractions back to source frames over the played span.
|
||||
// `startFrame` is the zone's effective start point (zone.startPoint.value_or(0)).
|
||||
// Mutates `play` in place; only the mode-relevant fields are written.
|
||||
void unpackEnvelope(const AmpEnvelope& env, std::int64_t frames, std::int64_t startFrame,
|
||||
ZonePlaySeconds& play) const;
|
||||
|
||||
// The clamp bounds envelope_edit uses, matching the control-panel sliders' own domains (so a
|
||||
// node drag can never produce a param a slider couldn't — the S-VIEW-F2 invariant).
|
||||
EnvClampBounds envClampBounds() const;
|
||||
|
||||
// --- Sample-view resolution helpers (the ONE storage site, S15-F2) -----------
|
||||
//
|
||||
// The single-capture Sample face reads/writes the same one-zone map site as the Zone surface.
|
||||
// These resolve the effective values for the picked id: effectiveSampleZone returns the picked
|
||||
// id's one-zone override (found in map_) or a product-default PerformanceZone bound to the
|
||||
// picked id (not yet materialized — a control edit materializes it, mirroring the Zone path).
|
||||
PerformanceZone effectiveSampleZone() const;
|
||||
// The effective root: the picked id's rootOverride, else its bank intrinsic, else middle C.
|
||||
int effectiveRoot() const;
|
||||
// The live sample rate from the bridge (for the envelope overlay's seconds<->frames time base),
|
||||
// or 0 when unavailable (the caller guards). Matches the voice engine's resolution rate.
|
||||
double liveSampleRate() const;
|
||||
// The persisted preview velocity as a 0..1 slider value (MIDI 1..127 mapped onto [0,1]).
|
||||
double previewVelocity01() const;
|
||||
|
||||
// Find-or-materialize the one-zone override for the picked id and return a mutable index into
|
||||
// map_.zones (appending a product-default zone if none exists). selectedId_ must be non-empty.
|
||||
// The mirror of upsertPickedOverride for a control edit — used when a Sample-face control edit
|
||||
// needs a concrete zone to write. Returns -1 if selectedId_ is empty.
|
||||
int ensureSampleZone();
|
||||
|
||||
ReaSamplerProcessor* processor_ = nullptr;
|
||||
|
||||
// --- Snapshot of the live bank (drawn each paint; refreshed off the audio thread) ---
|
||||
@@ -255,9 +328,22 @@ private:
|
||||
ChannelMode channelMode_ = ChannelMode::Mono; // S7 mono/stereo toggle snapshot
|
||||
|
||||
// --- Transient UI state (not persisted; component state carries selection + zones) ---
|
||||
View view_ = View::kBrowser; // default face is the browser
|
||||
View view_ = View::kSample; // default face is the loaded-sample home (S-VIEW-1)
|
||||
std::string activeFilterBankId_; // "" = All; else a bank id from banks_
|
||||
int selectedZone_ = -1; // highlighted zone in the Zones panel; -1 = none
|
||||
int selectedZone_ = -1; // highlighted zone in the Zone surface; -1 = none
|
||||
|
||||
// --- S-VIEW-5 Browse modal picker (a selection PENDING confirm) ---------------
|
||||
// The Browse overlay is a select-then-confirm picker: a click marks a pending pick without
|
||||
// loading it; Confirm (or double-click) commits it to selectedId_ + reloads and returns to
|
||||
// Sample; Cancel discards it and returns to Sample unchanged. "" = nothing picked yet.
|
||||
std::string browsePendingId_;
|
||||
int lastBrowseClickCard_ = -1; // for double-click-to-load detection (visible_ index)
|
||||
|
||||
// --- S-VIEW-4 preview-trigger note (transient) -------------------------------
|
||||
// The MIDI note the preview button is currently sounding (a held Gate voice), or -1 when the
|
||||
// button is up. Set on preview-button press (note-on posted to the processor), cleared on
|
||||
// release (note-off posted). One note at a time — a fresh press releases the prior.
|
||||
int previewingNote_ = -1;
|
||||
|
||||
// --- S13 drop-to-load affordance (relay DEGRADED — transient, never persisted) ----
|
||||
// S13's cross-artifact ingest relay (editor drop -> extension ingest) is NOT shipped: the
|
||||
@@ -303,14 +389,22 @@ private:
|
||||
WaveMarker waveMarker_ = WaveMarker::kStart;
|
||||
SetupMarkers dragStartMarkers_;
|
||||
std::int64_t dragSampleFrames_ = 0; // decoded length of the sample under the drag
|
||||
std::int64_t dragStartFrame_ = 0; // zone startPoint at grab time (0 if absent); for env-node drag
|
||||
|
||||
// S12 scrollbar-thumb drag: the offset held at grab time (the pixel-delta resolver shifts
|
||||
// from it). S12/S15/S16 param-slider drag: which control id + the panel it lives in (the
|
||||
// shell re-lays the panel each move to map x->value against the live control rect).
|
||||
int dragStartScrollOffset_ = 0;
|
||||
int dragParamId_ = -1;
|
||||
int dragParamId_ = -1; // ControlDesc id under a kParamSlider drag; -2 = preview-vel knob
|
||||
int dragParamZone_ = -1; // the zone index a kParamSlider drag edits (Sample or Zone face)
|
||||
Rect dragParamPanel_{};
|
||||
|
||||
// S-VIEW-3 envelope-node drag: which node is grabbed + the AmpEnvelope snapshotted at grab
|
||||
// (so the pixel delta is absolute, per envelope_edit's grabEnv contract). The overlay rect +
|
||||
// sample frame count are re-derived at move time from the live Sample-view layout.
|
||||
EnvNode envNode_ = EnvNode::Origin;
|
||||
AmpEnvelope dragStartEnv_{};
|
||||
|
||||
// --- Peak-thumbnail cache (mirror of bank_panel; id -> envelope at a bin width) ------
|
||||
// Keyed by "id|binCount" so a resize recomputes at the new width. Cleared on refresh so
|
||||
// a bank edit (a re-captured or deleted sample) does not show a stale thumbnail.
|
||||
|
||||
@@ -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,34 @@ 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.
|
||||
// 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.
|
||||
{
|
||||
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;
|
||||
if (inst) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inst) {
|
||||
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;
|
||||
|
||||
@@ -149,6 +149,13 @@ public:
|
||||
// The bridge, for the editor's live-state readout + sample list. Owned here; the
|
||||
// editor borrows it (outlives the editor).
|
||||
ReaperBridge& bridge() { return bridge_; }
|
||||
|
||||
// The live host sample rate latched from setupProcessing (the SAME rate reloadFromBank
|
||||
// resolves seconds->frames against). The editor's S-VIEW-3 envelope overlay reads it to place
|
||||
// its wall-clock seconds on the same time base the voice engine plays them over. 0.0 before
|
||||
// setupProcessing runs (the editor guards). Read on the UI thread; a plain load — sampleRate_
|
||||
// is set once by setupProcessing before any audio and does not change under the editor.
|
||||
double sampleRate() const { return sampleRate_; }
|
||||
// The current single-capture selection id (main/UI thread reads for the editor). Guarded
|
||||
// by selectionMutex_ — never touched on the audio thread. Since S10 this is the ONE picked
|
||||
// capture the default face plays chromatically when the performance map is empty; an EMPTY
|
||||
@@ -173,6 +180,22 @@ public:
|
||||
// (same mode) does neither. UI thread only.
|
||||
void setChannelMode(ChannelMode mode);
|
||||
|
||||
// The per-instance preview-trigger velocity (S-VIEW-4, MIDI 1..127). Read/written on the
|
||||
// UI thread (the Sample-view velocity knob) and by getState/setState (host load-save thread);
|
||||
// guarded by previewMutex_. Persisted in component state (v6). NOT read on the audio thread.
|
||||
std::uint8_t previewVelocity();
|
||||
void setPreviewVelocity(std::uint8_t velocity);
|
||||
|
||||
// Fire a one-shot PREVIEW note-on / note-off through the live voice engine (S-VIEW-4), OFF
|
||||
// the audio thread (the editor's preview-trigger button drives these on the UI thread). The
|
||||
// request is handed to process() via a lock-free single-slot mailbox drained at block start —
|
||||
// no allocation, no lock on the audio thread. previewNoteOn plays `note` at the current
|
||||
// previewVelocity(); previewNoteOff releases it (Gate) — Trigger zones ignore note-off and
|
||||
// play through. A momentary button (down = on, up = off) reads as a natural key press. This
|
||||
// is PLAYBACK ONLY: it never captures, never inserts a timeline item.
|
||||
void previewNoteOn(int note);
|
||||
void previewNoteOff(int note);
|
||||
|
||||
private:
|
||||
// Apply `mode` to the output audio bus's SpeakerArrangement (kMono / kStereo). Called from
|
||||
// initialize (topology) and setChannelMode (runtime change). Does NOT re-negotiate — the
|
||||
@@ -251,11 +274,30 @@ private:
|
||||
std::int64_t lastSeenBankGeneration_ = -1;
|
||||
|
||||
// S-VIEW-4 preview-trigger velocity (MIDI 1..127). Persisted in component state (v6) so the
|
||||
// user's chosen strike velocity survives a project save/reload. No concurrent writer before
|
||||
// Wave 2 (no editor knob yet) — setState and getState are the sole accessors, both on the
|
||||
// load/save thread (host-serialized). Default kPreviewVelocityDefault (64).
|
||||
// user's chosen strike velocity survives a project save/reload. Since Wave 2 the Sample-view
|
||||
// velocity knob writes it on the UI thread, so it is guarded by previewMutex_; setState and
|
||||
// getState (load/save thread) share the same guard. Default kPreviewVelocityDefault (64). NOT
|
||||
// read on the audio thread.
|
||||
std::mutex previewMutex_;
|
||||
std::uint8_t previewVelocity_ = kPreviewVelocityDefault;
|
||||
|
||||
// --- S-VIEW-4 preview-trigger mailbox (off-thread -> audio thread, lock-free) ---------
|
||||
// The editor's preview-trigger button posts a note-on/off request from the UI thread; process()
|
||||
// drains it at block start and drives the live engine. ONE slot per direction, each a packed
|
||||
// request whose high bits are a monotonically-incrementing sequence so process() detects a NEW
|
||||
// request by comparing against the last sequence it consumed (never re-firing a stale one). The
|
||||
// low 8 bits carry the note (on) / note (off); the on request also carries the velocity in the
|
||||
// next 8 bits, latched at post time so the audio thread reads no shared velocity field. A single
|
||||
// relaxed atomic load per block on the audio thread — RT-safe (no alloc, no lock).
|
||||
// packed = (seq << 16) | (velocity << 8) | note [note-on]
|
||||
// packed = (seq << 16) | note [note-off]
|
||||
std::atomic<std::uint32_t> previewOnRequest_{0}; // 0 = no request posted yet
|
||||
std::atomic<std::uint32_t> previewOffRequest_{0};
|
||||
std::uint16_t previewOnSeq_ = 0; // UI-thread post counter (never 0 after first post)
|
||||
std::uint16_t previewOffSeq_ = 0;
|
||||
std::uint16_t previewOnConsumed_ = 0; // audio-thread: last on-seq fired
|
||||
std::uint16_t previewOffConsumed_ = 0; // audio-thread: last off-seq fired
|
||||
|
||||
// Latched from setupProcessing so setActive/reload can size against it. Read
|
||||
// off-thread only. 0.0 is explicitly invalid — setupProcessing sets the real host rate
|
||||
// before any audio, and reloadFromBank guards on it before use.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// trigger_seam.cpp — PURE Trigger-mode frames↔fraction converter (see trigger_seam.h).
|
||||
|
||||
#include "trigger_seam.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
std::int64_t triggerPlayLength(double lengthFraction,
|
||||
std::int64_t frameCount,
|
||||
std::int64_t startFrame) {
|
||||
const std::int64_t postStart = (std::max)(std::int64_t{0}, frameCount - startFrame);
|
||||
if (postStart <= 0 || lengthFraction <= 0.0) return 0;
|
||||
return static_cast<std::int64_t>(lengthFraction * static_cast<double>(postStart) + 0.5);
|
||||
}
|
||||
|
||||
double framesToFadeFraction(std::int64_t fadeFrames, std::int64_t playLength) {
|
||||
if (playLength <= 0) return 0.0;
|
||||
return static_cast<double>(fadeFrames) / static_cast<double>(playLength);
|
||||
}
|
||||
|
||||
std::int64_t fadeFractionToFrames(double fadeFraction, std::int64_t playLength) {
|
||||
if (playLength <= 0) return 0;
|
||||
return static_cast<std::int64_t>(fadeFraction * static_cast<double>(playLength) + 0.5);
|
||||
}
|
||||
|
||||
} // namespace reasampler::vst
|
||||
@@ -0,0 +1,51 @@
|
||||
// trigger_seam.h — PURE Trigger-mode frames↔fraction converter for the S-VIEW-3 envelope seam.
|
||||
// NO VST3, NO REAPER, NO SWELL/LICE types at the boundary.
|
||||
//
|
||||
// The TRIGGER SEAM (documented in envelope_overlay.h) converts between the two representations
|
||||
// of Trigger fade lengths:
|
||||
//
|
||||
// ENGINE domain (TriggerParams / sampler_core): SOURCE FRAMES — int64_t absolute frame counts
|
||||
// that anchor directly to the voice's source-timeline read pointer.
|
||||
//
|
||||
// OVERLAY domain (AmpEnvelope / envelope_overlay): FRACTIONS — doubles in [0,1] of the played
|
||||
// span, where the played span is:
|
||||
// playLengthFrames = round(lengthFraction * (frameCount - startFrame))
|
||||
// The overlay stores fractions so the drawn shape stays invariant across sample-rate changes;
|
||||
// the engine stores frames so the voice advances correctly at the live rate.
|
||||
//
|
||||
// This module owns the one shared formula so the pack (frames->fractions) and unpack
|
||||
// (fractions->frames) paths are provably consistent and unit-tested independently of the shell.
|
||||
// The shell (reasampler_editor.cpp) calls these two functions from packEnvelope / unpackEnvelope.
|
||||
//
|
||||
// S-VIEW-F2 safety: the fractions produced here are in [0,1] by construction; a caller that
|
||||
// clamps the fractions to [0,1] before writing the AmpEnvelope preserves the slider-range
|
||||
// invariant (a drag can never produce a value a slider couldn't reach).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
// The source-frame length of the Trigger played span:
|
||||
// postStart = max(0, frameCount - startFrame)
|
||||
// playLength = round(lengthFraction * postStart)
|
||||
// `frameCount` is the total decoded sample length in source frames.
|
||||
// `startFrame` is the effective start point (zone.startPoint, or 0 when absent).
|
||||
// `lengthFraction` is TriggerParams::lengthFraction — (0,1], the fraction of the post-start span.
|
||||
// Returns 0 when postStart == 0 or lengthFraction <= 0.
|
||||
std::int64_t triggerPlayLength(double lengthFraction,
|
||||
std::int64_t frameCount,
|
||||
std::int64_t startFrame);
|
||||
|
||||
// Convert a source-frame fade count to a fraction of the play span (PACK direction, draw path).
|
||||
// Returns 0.0 when playLength == 0 (degenerate sample or zero %-length); the fraction is
|
||||
// NOT clamped — the caller clamps to [0,1] when filling AmpEnvelope so the overlay clamp logic
|
||||
// stays in envelope_edit, not here.
|
||||
double framesToFadeFraction(std::int64_t fadeFrames, std::int64_t playLength);
|
||||
|
||||
// Convert a fade fraction to a source-frame count (UNPACK direction, commit path).
|
||||
// Rounds to nearest integer frame. Returns 0 when playLength == 0.
|
||||
std::int64_t fadeFractionToFrames(double fadeFraction, std::int64_t playLength);
|
||||
|
||||
} // namespace reasampler::vst
|
||||
@@ -0,0 +1,161 @@
|
||||
// Standalone tests for reasampler::vst::trigger_seam — no VST3, no REAPER, no framework.
|
||||
// Same fast assert loop as the sibling pure tests.
|
||||
//
|
||||
// Covers: triggerPlayLength (zero play length, startFrame set, startFrame past frameCount,
|
||||
// rounding); framesToFadeFraction (zero play length, basic ratio); fadeFractionToFrames
|
||||
// (zero play length, rounding); round-trip fidelity; the Finding 1 regression (start-point
|
||||
// set — the case that was broken before this module existed).
|
||||
|
||||
#include "../src/vst/trigger_seam.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
using namespace reasampler::vst;
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
// --- triggerPlayLength --------------------------------------------------------
|
||||
|
||||
static void testPlayLengthNoStartPoint() {
|
||||
// startFrame == 0 (no start marker): postStart = frameCount.
|
||||
// 1000 frames, lengthFraction 1.0 -> 1000.
|
||||
CHECK(triggerPlayLength(1.0, 1000, 0) == 1000);
|
||||
// 1000 frames, lengthFraction 0.5 -> round(500.0) = 500.
|
||||
CHECK(triggerPlayLength(0.5, 1000, 0) == 500);
|
||||
// 1000 frames, lengthFraction 0.333 -> round(333.0) = 333.
|
||||
CHECK(triggerPlayLength(0.333, 1000, 0) == 333);
|
||||
}
|
||||
|
||||
static void testPlayLengthWithStartPoint() {
|
||||
// The Finding 1 regression: startFrame set, play length must be shorter.
|
||||
// frameCount=1000, startFrame=200 -> postStart=800.
|
||||
// lengthFraction 1.0 -> 800 (NOT 1000 as the pre-fix code produced).
|
||||
CHECK(triggerPlayLength(1.0, 1000, 200) == 800);
|
||||
// lengthFraction 0.5 -> round(400.0) = 400.
|
||||
CHECK(triggerPlayLength(0.5, 1000, 200) == 400);
|
||||
}
|
||||
|
||||
static void testPlayLengthZeroFrameCount() {
|
||||
// No decoded audio -> 0.
|
||||
CHECK(triggerPlayLength(1.0, 0, 0) == 0);
|
||||
}
|
||||
|
||||
static void testPlayLengthStartFramePastEnd() {
|
||||
// startFrame >= frameCount -> postStart clamped to 0 -> play length 0.
|
||||
CHECK(triggerPlayLength(1.0, 500, 500) == 0);
|
||||
CHECK(triggerPlayLength(1.0, 500, 600) == 0);
|
||||
}
|
||||
|
||||
static void testPlayLengthRounding() {
|
||||
// round(0.5) = 1 (round half up via +0.5 truncation: 0.5 + 0.5 = 1.0 -> 1).
|
||||
CHECK(triggerPlayLength(0.5, 1, 0) == 1);
|
||||
// round(lengthFraction * 3): 0.4 * 3 = 1.2 -> 1.
|
||||
CHECK(triggerPlayLength(0.4, 3, 0) == 1);
|
||||
// 0.6 * 3 = 1.8 -> 2.
|
||||
CHECK(triggerPlayLength(0.6, 3, 0) == 2);
|
||||
}
|
||||
|
||||
// --- framesToFadeFraction -----------------------------------------------------
|
||||
|
||||
static void testFramesToFadeFractionBasic() {
|
||||
// 100 frames fade over 1000 play length -> 0.1.
|
||||
const double frac = framesToFadeFraction(100, 1000);
|
||||
CHECK(frac > 0.0999 && frac < 0.1001);
|
||||
}
|
||||
|
||||
static void testFramesToFadeFractionZeroPlayLength() {
|
||||
// Degenerate: zero play length -> 0.0 (no division by zero).
|
||||
CHECK(framesToFadeFraction(100, 0) == 0.0);
|
||||
CHECK(framesToFadeFraction(0, 0) == 0.0);
|
||||
}
|
||||
|
||||
static void testFramesToFadeFractionFullSpan() {
|
||||
// fadeFrames == playLength -> fraction 1.0.
|
||||
const double frac = framesToFadeFraction(500, 500);
|
||||
CHECK(frac > 0.9999 && frac < 1.0001);
|
||||
}
|
||||
|
||||
// --- fadeFractionToFrames -----------------------------------------------------
|
||||
|
||||
static void testFadeFractionToFramesBasic() {
|
||||
// 0.1 of 1000 play length -> round(100.0) = 100.
|
||||
CHECK(fadeFractionToFrames(0.1, 1000) == 100);
|
||||
}
|
||||
|
||||
static void testFadeFractionToFramesZeroPlayLength() {
|
||||
// Degenerate: play length 0 -> 0 frames.
|
||||
CHECK(fadeFractionToFrames(0.5, 0) == 0);
|
||||
}
|
||||
|
||||
static void testFadeFractionToFramesRounding() {
|
||||
// 0.333... of 3 -> round(1.0) = 1.
|
||||
CHECK(fadeFractionToFrames(1.0 / 3.0, 3) == 1);
|
||||
// 0.5 of 3 -> round(1.5) = 2.
|
||||
CHECK(fadeFractionToFrames(0.5, 3) == 2);
|
||||
}
|
||||
|
||||
// --- Round-trip ---------------------------------------------------------------
|
||||
|
||||
static void testRoundTripNoStartPoint() {
|
||||
// Pack then unpack: fadeInFrames should survive (within 1 frame of rounding).
|
||||
// frameCount=44100, startFrame=0, lengthFraction=1.0 -> playLength=44100.
|
||||
// fadeInFrames = 2205 (5% of 44100).
|
||||
const std::int64_t fadeIn = 2205;
|
||||
const std::int64_t playLen = triggerPlayLength(1.0, 44100, 0);
|
||||
const double frac = framesToFadeFraction(fadeIn, playLen);
|
||||
const std::int64_t recovered = fadeFractionToFrames(frac, playLen);
|
||||
// Should be exact (2205 / 44100 * 44100 = 2205.0).
|
||||
CHECK(recovered == fadeIn);
|
||||
}
|
||||
|
||||
static void testRoundTripWithStartPoint() {
|
||||
// The Finding 1 case: startFrame set. frameCount=44100, startFrame=8820 (20%).
|
||||
// postStart=35280, lengthFraction=1.0 -> playLength=35280.
|
||||
// fadeInFrames = 1764 (5% of 35280).
|
||||
const std::int64_t frameCount = 44100;
|
||||
const std::int64_t startFrame = 8820;
|
||||
const std::int64_t fadeIn = 1764;
|
||||
const std::int64_t playLen = triggerPlayLength(1.0, frameCount, startFrame);
|
||||
CHECK(playLen == 35280);
|
||||
const double frac = framesToFadeFraction(fadeIn, playLen);
|
||||
const std::int64_t recovered = fadeFractionToFrames(frac, playLen);
|
||||
CHECK(recovered == fadeIn);
|
||||
}
|
||||
|
||||
static void testRoundTripFadeGreaterThanSpan() {
|
||||
// fadeFrames > playLength -> fraction > 1 (returned unclamped; the overlay clamps at draw).
|
||||
// The shell is responsible for clamping before writing AmpEnvelope.
|
||||
const std::int64_t playLen = 100;
|
||||
const std::int64_t fadeIn = 150;
|
||||
const double frac = framesToFadeFraction(fadeIn, playLen);
|
||||
CHECK(frac > 1.0); // intentionally unclamped from this module's perspective
|
||||
// The round-trip still recovers the original fade, so the shell can clamp after.
|
||||
const std::int64_t recovered = fadeFractionToFrames(frac, playLen);
|
||||
CHECK(recovered == fadeIn);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testPlayLengthNoStartPoint();
|
||||
testPlayLengthWithStartPoint();
|
||||
testPlayLengthZeroFrameCount();
|
||||
testPlayLengthStartFramePastEnd();
|
||||
testPlayLengthRounding();
|
||||
|
||||
testFramesToFadeFractionBasic();
|
||||
testFramesToFadeFractionZeroPlayLength();
|
||||
testFramesToFadeFractionFullSpan();
|
||||
|
||||
testFadeFractionToFramesBasic();
|
||||
testFadeFractionToFramesZeroPlayLength();
|
||||
testFadeFractionToFramesRounding();
|
||||
|
||||
testRoundTripNoStartPoint();
|
||||
testRoundTripWithStartPoint();
|
||||
testRoundTripFadeGreaterThanSpan();
|
||||
|
||||
if (g_fail == 0) std::printf("trigger_seam: all tests passed\n");
|
||||
else std::printf("trigger_seam: %d FAILED\n", g_fail);
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user