Widen the deck row block to 1028 so the filter tie-line is exact, and accumulate the meter's block peaks instead of sampling one in 47
This commit is contained in:
@@ -107,12 +107,13 @@ declared ahead of the instrument slots at that member in `reasampler_processor.h
|
||||
## Modules
|
||||
|
||||
- `reaper_bridge` — READ-ONLY bank consumer: receives bank snapshots from the extension and exposes them as a read-only view. **Never writes to the extension's bank** — this is a load-bearing invariant; no mutation path exists in this module. It owns TWO prefix-guarded ext-state write entry points, `writeUsageExtState` (`rsusage_`) and `writeBakeExtState` (`rsbake_`), each refusing every other key; neither weakens the read-only-*bank* invariant, because neither payload is bank state and `banks`/`view`/`tail`/`assign` stay structurally unwritable. Both PROVE the write by reading the key back (`wire::extStateWriteLanded`) — `SetProjExtState`'s own return cannot speak for one key, so testing it was a guard that could never fire, and the bake's "could not publish" refusal was consequently unreachable. It also owns the bake crossing — `extensionActionAvailable` / `invokeExtensionAction` (`NamedCommandLookup` + `Main_OnCommandEx` with `getReaperParent(3)`, the instance's OWN project tab, as `proj` — a request, not a DAW-verified guarantee; see the header) and `projectTempoBpm`.
|
||||
- `reasampler_processor` (`shell/instrument/`: `reasampler_processor.cpp` lifecycle + `process()`, `processor_state.cpp` component-state I/O + UI-thread parameter accessors, `processor_reload.cpp` the off-audio-thread `reloadInstrument`/publish family — Q-W2v, T4-12 split; `process()` and its per-block work stay ONE TU on purpose, no cross-TU call on the per-sample path) — VST3 `SingleComponentEffect` shell: declares event-input bus + **permanently stereo** output (GA fix: dynamic mono↔stereo bus renegotiation deleted; `ChannelMode` is now decode-only), marshals MIDI note-on/off into the VoiceEngine, renders audio; owns off-audio-thread `reloadInstrument` + atomic pointer swap so `process()` does no allocation, no file I/O, no bridge calls. The instance state is `{loaded capture id, one InstrumentParams}`, and `reloadInstrument` resolves + decodes exactly that one capture into the `SampleData` the engine plays. **Self-contained playback (pS):** `ComponentState` v10 adds a `SampleRefs` table — per referenced sample, a project-relative path + decode intrinsics (root, loop, channels, displayName); `reloadInstrument` decodes directly from `SampleRefs`, bank-free (plays with the extension absent). The bank/bridge is a browser source: loading a capture copies its reference in; the reopen-heal timer + poll-to-play apparatus are removed. `retireIdleDrain()` retires fully-idle drain snapshots on the UI-timer cadence. Voice-param edits (`setVoiceCount`/`setVoiceMode`/`setMonoTrigger`) rebuild the engine from the already-decoded `SampleData` via the drain-slot swap — no bank re-read, no WAV re-decode, no audible cut to ringing tails. **FB1:** applies the post-mixer `masterGainLinear` (from `ComponentState` v8) as a per-sample ramp over the summed output — no zipper noise. **GA v9:** `channelModeExplicit_` flag persisted; `channelModeFor()` auto-defaults the mode from the loaded capture's channel count when the flag is not set. **pS:** `ComponentState` bumped v9→v10 (`SampleRefs` table); pre-v10 blobs lift to empty refs and re-save self-contained. **pS-usage:** publishes instance usage (held `SampleRefs` paths) to `rsusage_<instanceGuid>` at the tail of `reloadInstrument` (off audio thread) via `reaper_bridge::writeUsageExtState`; `ComponentState` bumped v10→**v11** (`instanceGuid` field); pre-v11 blobs mint guid on first publish. **The master bus:** the summed output runs `voice mixer → master gain → limiter (core/instrument/engine/limiter) → output bus`, with the meter tapped at the bus output POST-limiter and published per block as relaxed atomics (per-channel peak, latched clip, the block's smallest limiter gain). The limiter's enable is persisted in the parameter set (params payload v15) and mirrored onto the audio thread by `setInstrumentParams`, the single funnel every writer already goes through. That mirror is also what `getLatencySamples()` answers from — the plugin's FIRST latency reporting: 0 bypassed, the lookahead engaged. `setLimiterEnabled` requests the host's `restartComponent(kLatencyChanged)`, UI thread only and never from `process()`; it is a LATENCY restart with the bus untouched, NOT the retired per-mode `kIoChanged` bus renegotiation the invariant above forbids.
|
||||
- `reasampler_processor` (`shell/instrument/`: `reasampler_processor.cpp` lifecycle + `process()`, `processor_state.cpp` component-state I/O + UI-thread parameter accessors, `processor_reload.cpp` the off-audio-thread `reloadInstrument`/publish family — Q-W2v, T4-12 split; `process()` and its per-block work stay ONE TU on purpose, no cross-TU call on the per-sample path) — VST3 `SingleComponentEffect` shell: declares event-input bus + **permanently stereo** output (GA fix: dynamic mono↔stereo bus renegotiation deleted; `ChannelMode` is now decode-only), marshals MIDI note-on/off into the VoiceEngine, renders audio; owns off-audio-thread `reloadInstrument` + atomic pointer swap so `process()` does no allocation, no file I/O, no bridge calls. The instance state is `{loaded capture id, one InstrumentParams}`, and `reloadInstrument` resolves + decodes exactly that one capture into the `SampleData` the engine plays. **Self-contained playback (pS):** `ComponentState` v10 adds a `SampleRefs` table — per referenced sample, a project-relative path + decode intrinsics (root, loop, channels, displayName); `reloadInstrument` decodes directly from `SampleRefs`, bank-free (plays with the extension absent). The bank/bridge is a browser source: loading a capture copies its reference in; the reopen-heal timer + poll-to-play apparatus are removed. `retireIdleDrain()` retires fully-idle drain snapshots on the UI-timer cadence. Voice-param edits (`setVoiceCount`/`setVoiceMode`/`setMonoTrigger`) rebuild the engine from the already-decoded `SampleData` via the drain-slot swap — no bank re-read, no WAV re-decode, no audible cut to ringing tails. **FB1:** applies the post-mixer `masterGainLinear` (from `ComponentState` v8) as a per-sample ramp over the summed output — no zipper noise. **GA v9:** `channelModeExplicit_` flag persisted; `channelModeFor()` auto-defaults the mode from the loaded capture's channel count when the flag is not set. **pS:** `ComponentState` bumped v9→v10 (`SampleRefs` table); pre-v10 blobs lift to empty refs and re-save self-contained. **pS-usage:** publishes instance usage (held `SampleRefs` paths) to `rsusage_<instanceGuid>` at the tail of `reloadInstrument` (off audio thread) via `reaper_bridge::writeUsageExtState`; `ComponentState` bumped v10→**v11** (`instanceGuid` field); pre-v11 blobs mint guid on first publish. **The master bus:** the summed output runs `voice mixer → master gain → limiter (core/instrument/engine/limiter) → output bus`, with the meter tapped at the bus output POST-limiter and published as relaxed atomics — per-channel peak and smallest limiter gain ACCUMULATED across every block since the UI last read, plus the latched clip (the meter Gotcha below owns why). The limiter's enable is persisted in the parameter set (params payload v15) and mirrored onto the audio thread by `setInstrumentParams`, the single funnel every writer already goes through. That mirror is also what `getLatencySamples()` answers from — the plugin's FIRST latency reporting: 0 bypassed, the lookahead engaged. `setLimiterEnabled` requests the host's `restartComponent(kLatencyChanged)`, UI thread only and never from `process()`; it is a LATENCY restart with the bus untouched, NOT the retired per-mode `kIoChanged` bus renegotiation the invariant above forbids.
|
||||
- `reasampler_editor` — VST3 `IPlugView` LICE editor shell: hosts a LICE-drawn child window; the Sample face is home and Browse is a modal picker over it. Split on the Sample face's BAND axis, mirroring the pure `sample_bands` allocator: `editor_session` (session/bridge state, caches, commit-and-reload), `editor_controls` (the ONE `faceLayout` band resolve every paint and hit-test path shares, the node-drag bounds, the value labels, and the per-instance controls the parameter set does not carry — the parameter-set binding itself is the pure `core/instrument/ui/deck_values` module this only adapts int ids onto), `editor_models` (the orthogonal half: which stored struct each transient editor selection names — the staged-envelope pack/unpack, the drawn contour, and the three velocity curves), then matching paint and input sets — `editor_paint`/`editor_input` (dispatch + drag router + hover dispatch), `_chrome`, `_waveform`, `_deck` — plus the two band-independent surfaces (`_browse` for the modal picker, `_curve` for the velocity-curve popup) and `editor_platform` (IPlugView/Win32 window plumbing). Shared internals in `editor_internal.h`, no TU of its own. Drop-onto-editor ingest is NOT shipped (deferred).
|
||||
- `reasampler_embed` — implements `IReaperUIEmbedInterface` so the instrument draws inline in the TCP/MCP without a plugin-owned HWND; delegates layout to `embed_strip`. A read-only readout: the loaded capture across the keyboard span with its root marked, plus the activity level. It takes no mouse input (there is nothing on the strip to select).
|
||||
- `editor_stroke` — the editor's LICE side of the analytic stroker: builds a coverage mask with the pure `core/ui/stroke_aa` and blends it into the bitmap ONCE, writing straight to the bitmap's bits (the arithmetic matches LICE's own mode-0 combine, so a stroke composites identically to every other kit draw). Every radial and spline stroke on the editor routes through `strokeArcAA` / `strokePolylineAA` / `strokeLineAA`. Holds the draw-thread-only scratch mask and arc point list — reuse, not a hidden dependency: threading a canvas through the eight paint sites would grow those signatures to carry an allocation detail. Deliberately does NOT touch `shell/panel/draw_kit`: the waveform stroke, the docked bank panel and the browse cards are out of this seam's blast radius.
|
||||
- `instrument_bake` — the instrument's half of the resample chain, on the UI thread: render the dialed sound through the pure `core/instrument/bake` modules at the instance's PERSISTED PREVIEW VELOCITY (the velocity the user has been auditioning at — three velocity curves are live, so it is a property of the sound and not a render detail), stage the WAV OUTSIDE the bank folder, publish one `rsbake_<guid>` request, invoke the extension's landing action SYNCHRONOUSLY, read the outcome back over the same key, then adopt + reset in one act. What that key holds afterwards is classified by `core/wire`'s pure `classifyBakeAnswer`, and each of its five non-answers gets its OWN sentence — a silent no-answer stays a failure, but the user is told whether nothing wrote over the key, a stale generation was answered, the answer came in a wire this build cannot read, the request was cleared, or it was refused. All five name the key, because the extension prints one console line per key it scanned and the key is what correlates the two in a multi-instance session. None of them claims the landing never ran — nothing on this side can observe that. Two stack-RAII guards mirror `FxBypassGuard`'s discipline: the staged file and the request key are both cleared on every exit path, so a failed bake leaves no temp, no bank entry and no parameter reset. `bakeAvailable` is the affordance's paint gate. A cloned `instanceGuid` (two instances sharing one `rsbake_` key) is NOT handled here — the residual is contained by pre-existing tracking machinery instead: `planUsagePublish`'s sticky `unioned` poison plus `tiedUsageExists` (`core/tracking/tracking_authority.cpp`) force a clone's bake to `AddDistinct` rather than silently replacing a sibling's entry.
|
||||
- `vst_entry` — VST3 entry point: `GetPluginFactory` export, class registration, channel-forked class UIDs.
|
||||
- `editor_interaction.h` — the editor's INTERACTION VOCABULARY: `DragKind` (what a gesture in flight is editing) and `HoverKind`/`HoverTarget` (what the pointer can be over). Split out of `reasampler_editor.h`, which had grown past the ~600-line ceiling with no seam — these two catalogues are produced by the input TUs and read by the paint TUs, and neither is behaviour, which is what makes them a responsibility rather than a bisection. Namespace-scope, so the editor's own members still spell them unqualified. Internal to this TU family, like `editor_internal.h`.
|
||||
- `editor_internal.h` — INTERNAL shared helpers for the `reasampler_editor` TU family, included only by the editor's own shell TUs (`editor_session` / `editor_controls` / `editor_paint_*` / `editor_input_*` / `editor_platform`), never a public seam: the `Rect`↔kit adapters, small draw primitives (knob face / title band), label helpers, the velocity-curve box derivation, and `dragModifiers()` — THE modifier read for every drag surface and gesture resolver, so the editor cannot grow a second modifier grammar — the helpers more than one band TU needs. The deck's control ids, group ids and group composition are the pure `deck_groups` module's, not this file's. The piano-strip and root-key draws live in `editor_paint_chrome`, their only consumer, not here.
|
||||
- `reasampler_vst.h` — shared identity constants for the ReaSampler VST3 instrument (Phase S): the plugin's class UID (the channel-selected `Steinberg::FUID`, built from the FOREVER-FROZEN macros in `core/wire/reasampler_uid.h`), vendor name/URL/email, so the processor, factory, and editor agree. A class UID is FOREVER-STABLE once shipped — minted once, never regenerated. *(Newly authored per this dispatch's brief — no existing root-CLAUDE.md bullet; verified by reading `src/shell/instrument/reasampler_vst.h` directly.)*
|
||||
|
||||
@@ -124,13 +125,27 @@ declared ahead of the instrument slots at that member in `reasampler_processor.h
|
||||
the very instance whose frame is on the stack. Deferring by one tick is same-thread and
|
||||
in-instance — it is NOT a cross-process poller/nonce handshake, and it must not grow into
|
||||
one.
|
||||
- **The limiter toggle arms on the same principle, and for the same reason.** Its commit
|
||||
requests the host's `restartComponent(kLatencyChanged)`; a host that services that
|
||||
synchronously runs `setActive(false)`/`setActive(true)`, and OUR `setActive(true)` calls
|
||||
`reloadInstrument()` — a WAV re-decode plus disk I/O. Inline from `WM_LBUTTONDOWN` that
|
||||
whole cycle runs with `SetCapture` held. The click writes the editor's own snapshot and
|
||||
paints at once; the sync tick calls `setLimiterEnabled`, so **the audio and the reported
|
||||
latency follow the click by up to one tick.** It sits AFTER the drag guard with the bake:
|
||||
the restart rebuilds the instance, which mid-drag would yank the edit surface exactly as a
|
||||
reload would. Every other writer of the parameter set (`setState`, the bake's adopt) still
|
||||
commits and restarts immediately — none of them is inside a mouse handler.
|
||||
- **The MASTER meter's ballistics ride the sync tick, and that tick is 500 ms.** They run
|
||||
BEFORE the tick's in-flight-drag guard on purpose — a drag suppresses the reload poll, but
|
||||
the bus keeps sounding. Elapsed time is measured (`GetTickCount64`), never assumed from the
|
||||
timer's period, and the tick repaints only when `meterDrawEqual` says the picture changed.
|
||||
At that cadence the bar falls 10 dB per redraw and the 1.5 s hold spans three frames:
|
||||
correct against `meter_ballistics`' contract, coarse to the eye. A meter-rate timer is a
|
||||
separate change and is not in yet.
|
||||
**The published block state is therefore ACCUMULATED, not sampled**: at 48 kHz / 512 frames
|
||||
~47 blocks elapse per tick, so the processor folds a per-channel max and a min limiter gain
|
||||
across them and `masterBusMeter()` clears the accumulators as it reads. A plain overwriting
|
||||
store displayed one block in ~47 and lost the rest — the specified "a peak displays on the
|
||||
first UI frame after it occurs" is what the fold restores. `masterBusMeter()` is CONSUMING,
|
||||
so exactly one caller may hold it; the embed strip reads its own non-consuming
|
||||
`embedActivityLevel()`. A meter-rate timer remains a separate change and is not in.
|
||||
- The bake's availability probe runs on the SAME tick that paints the button, so the
|
||||
control can never be enabled on one tick and refuse on the next. The bake Hold control's
|
||||
applicability (`resolveBakeHoldNeeded`) rides the same tick for the same reason, and
|
||||
|
||||
@@ -98,8 +98,8 @@ void ReaSamplerEditor::dragBrowse(int x, int y) {
|
||||
invalidate();
|
||||
}
|
||||
|
||||
ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverBrowse(int w, int h, int x,
|
||||
int y) const {
|
||||
HoverTarget ReaSamplerEditor::hoverBrowse(int w, int h, int x,
|
||||
int y) const {
|
||||
const BrowseModal bm = computeBrowseModal(w, h);
|
||||
if (contains(bm.back, x, y)) return {HoverKind::kBack, -1};
|
||||
if (contains(bm.cancel, x, y)) return {HoverKind::kBrowseCancel, -1};
|
||||
|
||||
@@ -152,8 +152,8 @@ void ReaSamplerEditor::dragChrome(const FaceLayout& fl, int x, int y) {
|
||||
invalidate(); // live feedback; the commit lands on WM_LBUTTONUP
|
||||
}
|
||||
|
||||
ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverChrome(const FaceLayout& fl, int x,
|
||||
int y) const {
|
||||
HoverTarget ReaSamplerEditor::hoverChrome(const FaceLayout& fl, int x,
|
||||
int y) const {
|
||||
const ChromeRects& cr = fl.chrome;
|
||||
if (contains(cr.navBrowse, x, y)) return {HoverKind::kNavBrowse, -1};
|
||||
if (selectedId_.empty()) return {}; // empty state — no interactive surfaces beyond nav
|
||||
|
||||
@@ -114,8 +114,8 @@ void ReaSamplerEditor::onMouseRDown(int x, int y) {
|
||||
/*addOnEmptySpace=*/false);
|
||||
}
|
||||
|
||||
ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverCurvePopup(int w, int h, int x,
|
||||
int y) const {
|
||||
HoverTarget ReaSamplerEditor::hoverCurvePopup(int w, int h, int x,
|
||||
int y) const {
|
||||
const CurvePopupLayout pl = computeCurvePopup(w, h);
|
||||
if (contains(pl.close, x, y)) return {HoverKind::kPopupClose, -1};
|
||||
if (!contains(pl.curveBox, x, y)) return {};
|
||||
|
||||
@@ -68,10 +68,12 @@ bool ReaSamplerEditor::mouseDownDeck(const FaceLayout& fl, int x, int y) {
|
||||
const bool on = (hit.segment == 1);
|
||||
if (on != params_.limiterEnabled) {
|
||||
params_.limiterEnabled = on;
|
||||
// The processor's own funnel mirrors the audio-thread flag and requests the
|
||||
// host's latency restart; a reload would re-decode a WAV the toggle cannot
|
||||
// change. The local snapshot moves with it so a later commit agrees.
|
||||
processor_->setLimiterEnabled(on);
|
||||
// ARMED here, run on the sync tick — the same treatment the bake gets, and
|
||||
// for the same reason: the processor's funnel requests the host's latency
|
||||
// restart, whose deactivate/reactivate calls setActive(true) and re-decodes
|
||||
// the WAV. Inline, that whole cycle would run nested inside this mouse
|
||||
// handler with SetCapture held.
|
||||
limiterPending_ = on;
|
||||
}
|
||||
invalidate();
|
||||
break;
|
||||
@@ -192,8 +194,8 @@ void ReaSamplerEditor::dragDeck(int x, int y) {
|
||||
invalidate();
|
||||
}
|
||||
|
||||
ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverDeck(const FaceLayout& fl, int x,
|
||||
int y) const {
|
||||
HoverTarget ReaSamplerEditor::hoverDeck(const FaceLayout& fl, int x,
|
||||
int y) const {
|
||||
const Rect& band = fl.bands.decks;
|
||||
if (!contains(band, x, y)) return {};
|
||||
const DeckLayout dl = layoutDeck(fl.deckDescs, band.x, band.y, band.width);
|
||||
|
||||
@@ -188,8 +188,8 @@ bool ReaSamplerEditor::splineOverlayClick(const OverlayArea& waveArea, int x, in
|
||||
return true;
|
||||
}
|
||||
|
||||
ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverWaveform(const FaceLayout& fl, int x,
|
||||
int y) {
|
||||
HoverTarget ReaSamplerEditor::hoverWaveform(const FaceLayout& fl, int x,
|
||||
int y) {
|
||||
// Caps only: the cap is the grip, so it is the one thing on the overlay a resting pointer
|
||||
// can be "on". A hovered mark promotes its own label past the suppression rule.
|
||||
//
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// editor_interaction.h — the Sample editor's INTERACTION VOCABULARY: what a drag can be
|
||||
// editing, and what the pointer can be over. Two catalogues of the editor's interactive
|
||||
// surface, produced by the input TUs and read by the paint TUs; neither is behaviour, which is
|
||||
// why they are named here rather than buried inside the editor class between its paint and
|
||||
// layout declarations. Internal to the reasampler_editor TU family, like editor_internal.h.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
// What a mouse drag is currently editing. kWaveMarker/kEnvNode/kCurveNode track their grabbed
|
||||
// item in the editor's waveMarker_/envNode_/curvePointIndex_; kDeckKnob is a grab-anchored knob
|
||||
// drag (control in dragParamId_, grab value in dragKnobStartValue_). kSplineNode is the
|
||||
// overlay's peer of kCurveNode: the same VelocityCurve point drag, over the waveform overlay's
|
||||
// box and the overlay-active envelope's contour rather than the popup's box and curve.
|
||||
enum class DragKind { kNone, kRootMarker, kWaveMarker, kScrollThumb, kEnvNode,
|
||||
kCurveNode, kSplineNode, kDeckKnob };
|
||||
|
||||
// The interactive element under the pointer, resolved live in WM_MOUSEMOVE.
|
||||
enum class HoverKind {
|
||||
kNone,
|
||||
kNavBrowse, // the chrome "Browse" toolbar button (opens the Browse modal)
|
||||
kBack, // the Browse "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
|
||||
kLoopOff, // the loop enable's Off segment
|
||||
kLoopOn, // the loop enable's On segment
|
||||
kWaveMark, // a waveform overlay mark (index = WaveMark ordinal); promotes its label
|
||||
kPreview, // the preview-trigger button
|
||||
kBake, // the resample-bake trigger
|
||||
kControl, // a knob-deck element (index = control id)
|
||||
kInnerDial, // a knob cell's inner curve dial (index = the OUTER control id)
|
||||
kEnvRadio, // an envelope deck's overlay-select radio (index = radio control id)
|
||||
kCurveNode, // a velocity-curve control point (index = point index)
|
||||
kVelKnob, // the chrome preview-velocity radial knob
|
||||
kHoldKnob, // the chrome bake-Hold radial knob
|
||||
kStripKey, // a piano-strip key (index = MIDI note); carries the name tooltip
|
||||
kPopupClose, // the curve popup's Close (x) button
|
||||
};
|
||||
|
||||
// `index` disambiguates within a kind (tab ordinal, visible-card index, control-row id); -1
|
||||
// when not applicable.
|
||||
struct HoverTarget {
|
||||
HoverKind kind = HoverKind::kNone;
|
||||
int index = -1;
|
||||
bool operator==(const HoverTarget& o) const { return kind == o.kind && index == o.index; }
|
||||
bool operator!=(const HoverTarget& o) const { return !(*this == o); }
|
||||
};
|
||||
|
||||
} // namespace reasampler::vst
|
||||
@@ -13,7 +13,7 @@
|
||||
#include "core/instrument/engine/filter/filter_morph.h" // MorphLaw (the law toggle's state)
|
||||
#include "core/instrument/ui/knob_deck.h" // deck layout + kDeckKnobSize
|
||||
#include "core/instrument/ui/master_meter.h" // the bus meter's column interior + ballistics
|
||||
#include "core/instrument/ui/waveform_view.h" // waveformSurface (THE lane-split fold)
|
||||
#include "core/instrument/ui/waveform_view.h" // resolveLaneSplit (THE lane-split fold)
|
||||
#include "shell/instrument/editor_internal.h" // kit adapters + knob face
|
||||
#include "shell/instrument/reasampler_processor.h"
|
||||
|
||||
@@ -52,12 +52,11 @@ void paintMeterColumn(LICE_IBitmap* bmp, const Rect& column, const MasterMeterUi
|
||||
db -= static_cast<int>(kMeterTickStepDb)) {
|
||||
const int y = meterDbToY(m.field, db);
|
||||
const bool zero = (db == 0);
|
||||
const bool numeralled = zero || (db % 12 == 0);
|
||||
LICE_FillRect(bmp, m.field.x, y, m.field.width, zero ? 2 : 1,
|
||||
zero ? toLice(roleColor(Role::TextDim)) : hairline, 1.0f, 0);
|
||||
if (numeralled) {
|
||||
kitText(bmp, Rect::ltrb(m.labels.x, y - 5, m.labels.right(), y + 5),
|
||||
tickLabel(db).c_str(), Font::Micro, Role::TextDim, Align::Right);
|
||||
if (meterTickNumeralled(db)) {
|
||||
kitText(bmp, meterNumeralRect(m.labels, y), tickLabel(db).c_str(), Font::Micro,
|
||||
Role::TextDim, Align::Right);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,9 +77,7 @@ void paintMeterColumn(LICE_IBitmap* bmp, const Rect& column, const MasterMeterUi
|
||||
}
|
||||
};
|
||||
if (split == LaneSplit::Single) {
|
||||
const instrument::engine::MeterState& loudest =
|
||||
state.left.levelDb >= state.right.levelDb ? state.left : state.right;
|
||||
drawBar(m.barA, loudest);
|
||||
drawBar(m.barA, meterSingleLaneState(state));
|
||||
} else {
|
||||
drawBar(m.barA, state.left);
|
||||
drawBar(m.barB, state.right);
|
||||
@@ -102,14 +99,12 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
|
||||
const PlaySeconds& play = params_.play;
|
||||
const bool isMono = (voiceMode_ == VoiceMode::Mono);
|
||||
const LICE_pixel hairline = toLice(roleColor(Role::LineHairline));
|
||||
// The meter's bar count is the SAME resolved decision the waveform's lane split is — read
|
||||
// off waveformSurface rather than re-derived, so it can never become a second rule.
|
||||
const LaneSplit meterSplit =
|
||||
waveformSurface(fl.bands.waveform, channelMode_ == ChannelMode::Stereo,
|
||||
channelPcmFor(selectedId_).channelCount)
|
||||
.laneCount == 2
|
||||
? LaneSplit::Stereo
|
||||
: LaneSplit::Single;
|
||||
// The meter's bar count is the SAME resolved decision the waveform's lane split is —
|
||||
// resolveLaneSplit is the one home of it. Asked directly rather than read back off
|
||||
// waveformSurface, whose laneCount additionally folds in the waveform BAND's pixel height,
|
||||
// which decides nothing about how many channels the bus is carrying.
|
||||
const LaneSplit meterSplit = resolveLaneSplit(channelMode_ == ChannelMode::Stereo,
|
||||
channelPcmFor(selectedId_).channelCount);
|
||||
|
||||
// One compact-toggle draw (the Mono/Stereo segment grammar at Micro scale). Disabled
|
||||
// segments draw inert so the dependency (Retrig|Legato needs Mono) reads at a glance.
|
||||
|
||||
@@ -108,9 +108,8 @@ void ReaSamplerEditor::onSyncTimer() {
|
||||
// tick picks up the change after release.
|
||||
if (!processor_) return;
|
||||
|
||||
// The meter runs on EVERY tick, the in-flight-drag case included: a drag suppresses the
|
||||
// reload poll below, but the bus keeps sounding and a frozen bar would misreport it.
|
||||
// Elapsed time is measured rather than assumed — WM_TIMER's period is a request.
|
||||
// Ahead of the drag guard on purpose: a drag suppresses the reload poll below, but the bus
|
||||
// keeps sounding and a frozen bar would misreport it.
|
||||
{
|
||||
const unsigned long long now = GetTickCount64();
|
||||
const double elapsed = meterTickMs_ == 0
|
||||
@@ -129,6 +128,15 @@ void ReaSamplerEditor::onSyncTimer() {
|
||||
|
||||
if (drag_ != DragKind::kNone) return; // defer past the in-flight edit
|
||||
|
||||
// The limiter click armed it; this is where it runs. Past the drag guard with the bake,
|
||||
// because the restart it requests makes the host rebuild this instance — mid-drag that
|
||||
// would yank the edit surface exactly as a reload would.
|
||||
if (limiterPending_) {
|
||||
const bool on = *limiterPending_;
|
||||
limiterPending_.reset();
|
||||
processor_->setLimiterEnabled(on);
|
||||
}
|
||||
|
||||
// Resolve the bake affordance's availability on the SAME tick that paints it, so it
|
||||
// can never be enabled on one tick and refuse on the next.
|
||||
const bool available = bakeAvailable(processor_->bridge());
|
||||
|
||||
@@ -183,11 +183,16 @@ void ReaSamplerProcessor::setLimiterEnabled(bool on) {
|
||||
setInstrumentParams(params);
|
||||
}
|
||||
|
||||
MasterBusMeter ReaSamplerProcessor::masterBusMeter() const {
|
||||
MasterBusMeter ReaSamplerProcessor::masterBusMeter() {
|
||||
MasterBusMeter m;
|
||||
m.peakL = meterPeakL_.load(std::memory_order_relaxed);
|
||||
m.peakR = meterPeakR_.load(std::memory_order_relaxed);
|
||||
m.minGain = meterMinGain_.load(std::memory_order_relaxed);
|
||||
// Exchange, not load: the accumulators hold the window since this was last called, and
|
||||
// clearing them here is what starts the next window. The audio thread's own fold is a
|
||||
// load-max-store, so a store landing between this exchange and that store can retain one
|
||||
// window's peak for one extra frame — it can never LOSE one, which is the property that
|
||||
// matters for a peak meter.
|
||||
m.peakL = meterPeakL_.exchange(0.f, std::memory_order_relaxed);
|
||||
m.peakR = meterPeakR_.exchange(0.f, std::memory_order_relaxed);
|
||||
m.minGain = meterMinGain_.exchange(1.f, std::memory_order_relaxed);
|
||||
m.clip = meterClip_.load(std::memory_order_relaxed);
|
||||
return m;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "core/audio/peaks.h" // Envelope (the cached peak thumbnail)
|
||||
#include "core/instrument/map/sample_map.h" // SampleChoice, BankChoice, InstrumentParams
|
||||
#include "core/instrument/engine/velocity_curve.h" // VelocityCurve (transfer-curve editor state)
|
||||
#include "shell/instrument/editor_interaction.h" // DragKind / HoverKind / HoverTarget
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -79,15 +80,6 @@ private:
|
||||
// picker overlaid on it.
|
||||
enum class View { kSample, kBrowse };
|
||||
|
||||
// What a mouse drag is currently editing. kWaveMarker/kEnvNode/kCurveNode track their
|
||||
// grabbed item in waveMarker_/envNode_/curvePointIndex_; kDeckKnob is a grab-anchored
|
||||
// knob drag (control in dragParamId_, grab value in dragKnobStartValue_).
|
||||
// kSplineNode is the overlay's peer of kCurveNode: the same VelocityCurve point drag, over
|
||||
// the waveform overlay's box and the overlay-active envelope's contour rather than the
|
||||
// popup's box and curve.
|
||||
enum class DragKind { kNone, kRootMarker, kWaveMarker, kScrollThumb, kEnvNode,
|
||||
kCurveNode, kSplineNode, kDeckKnob };
|
||||
|
||||
// Which envelope the waveform overlay is drawing and editing. The selection type and its
|
||||
// whole state machine are the pure deck_groups module's; this alias keeps the shell's
|
||||
// spelling.
|
||||
@@ -115,41 +107,6 @@ private:
|
||||
// What those four marks are showing — see pickedMarkers.
|
||||
using SetupMarkers = instrument::ui::LoopMarks;
|
||||
|
||||
// The interactive element under the pointer, resolved live in WM_MOUSEMOVE. `index`
|
||||
// disambiguates within a kind (tab ordinal, visible-card index, control-row id); -1 when
|
||||
// not applicable.
|
||||
enum class HoverKind {
|
||||
kNone,
|
||||
kNavBrowse, // the chrome "Browse" toolbar button (opens the Browse modal)
|
||||
kBack, // the Browse "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
|
||||
kLoopOff, // the loop enable's Off segment
|
||||
kLoopOn, // the loop enable's On segment
|
||||
kWaveMark, // a waveform overlay mark (index = WaveMark ordinal); promotes its label
|
||||
kPreview, // the preview-trigger button
|
||||
kBake, // the resample-bake trigger
|
||||
kControl, // a knob-deck element (index = control id)
|
||||
kInnerDial, // a knob cell's inner curve dial (index = the OUTER control id)
|
||||
kEnvRadio, // an envelope deck's overlay-select radio (index = radio control id)
|
||||
kCurveNode, // a velocity-curve control point (index = point index)
|
||||
kVelKnob, // the chrome preview-velocity radial knob
|
||||
kHoldKnob, // the chrome bake-Hold radial knob
|
||||
kStripKey, // a piano-strip key (index = MIDI note); carries the name tooltip
|
||||
kPopupClose, // the curve popup's Close (x) button
|
||||
};
|
||||
struct HoverTarget {
|
||||
HoverKind kind = HoverKind::kNone;
|
||||
int index = -1;
|
||||
bool operator==(const HoverTarget& o) const { return kind == o.kind && index == o.index; }
|
||||
bool operator!=(const HoverTarget& o) const { return !(*this == o); }
|
||||
};
|
||||
|
||||
// The three-band stack for the current client size, plus the chrome interior. Every
|
||||
// paint/hit-test path derives both through this one call so draw and hit-test can never
|
||||
// disagree about where a band is.
|
||||
@@ -489,6 +446,11 @@ private:
|
||||
std::string bakeMessage_; // last outcome, shown in the title band
|
||||
int bakeMessageTicks_ = 0; // sync ticks the message survives
|
||||
|
||||
// The limiter toggle, armed by the click and run on the sync tick — the commit requests a
|
||||
// host latency restart, which is the same nested-inside-a-mouse-handler hazard the bake
|
||||
// defers for. Empty = nothing armed.
|
||||
std::optional<bool> limiterPending_;
|
||||
|
||||
// The editor-drop -> extension-ingest relay is not shipped (the bridge is read-only):
|
||||
// an OS drop just flashes a "drop on the panel instead" banner (dropHintTicks_ counts
|
||||
// down via the sync tick). Never ingests, never inserts a timeline item.
|
||||
|
||||
@@ -309,7 +309,7 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
||||
}
|
||||
// The chain's last stage before the bus, after the gain above.
|
||||
const float minGain = limiter_.process(ch0, ch1, frames);
|
||||
meterMinGain_.store(minGain, std::memory_order_relaxed);
|
||||
foldMinGain(meterMinGain_, minGain);
|
||||
// Channels beyond the first two mirror ch0 (defensive — REAPER negotiates 1 or 2).
|
||||
for (int32 ch = 2; ch < out.numChannels; ++ch) {
|
||||
if (float* buf = out.channelBuffers32[ch]) {
|
||||
@@ -324,8 +324,9 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
||||
if (a0 > peakL) peakL = a0;
|
||||
if (a1 > peakR) peakR = a1;
|
||||
}
|
||||
meterPeakL_.store(peakL, std::memory_order_relaxed);
|
||||
meterPeakR_.store(peakR, std::memory_order_relaxed);
|
||||
foldPeak(meterPeakL_, peakL);
|
||||
foldPeak(meterPeakR_, peakR);
|
||||
advisoryPeak_.store(peakL > peakR ? peakL : peakR, std::memory_order_relaxed);
|
||||
if (peakL >= 1.f || peakR >= 1.f) meterClip_.store(true, std::memory_order_relaxed);
|
||||
} else if (ch0) {
|
||||
// Mono: render into channel 0, replicate to any extra channels (defensive).
|
||||
@@ -354,14 +355,15 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
||||
}
|
||||
}
|
||||
const float minGain = limiter_.process(ch0, nullptr, frames);
|
||||
meterMinGain_.store(minGain, std::memory_order_relaxed);
|
||||
foldMinGain(meterMinGain_, minGain);
|
||||
float peak = 0.f;
|
||||
for (int32 i = 0; i < frames; ++i) {
|
||||
const float a = ch0[i] < 0.f ? -ch0[i] : ch0[i];
|
||||
if (a > peak) peak = a;
|
||||
}
|
||||
meterPeakL_.store(peak, std::memory_order_relaxed);
|
||||
meterPeakR_.store(peak, std::memory_order_relaxed);
|
||||
foldPeak(meterPeakL_, peak);
|
||||
foldPeak(meterPeakR_, peak);
|
||||
advisoryPeak_.store(peak, std::memory_order_relaxed);
|
||||
if (peak >= 1.f) meterClip_.store(true, std::memory_order_relaxed);
|
||||
for (int32 ch = 1; ch < out.numChannels; ++ch) {
|
||||
if (float* buf = out.channelBuffers32[ch]) {
|
||||
|
||||
@@ -128,16 +128,17 @@ public:
|
||||
Steinberg::tresult PLUGIN_API queryInterface(const Steinberg::TUID iid,
|
||||
void** obj) override;
|
||||
|
||||
// The embedded-strip activity level (0..1) for the embed shell, UI thread. The loudest of
|
||||
// the two published bus peaks — one publication serves the strip and the meter.
|
||||
// The embedded-strip activity level (0..1) for the embed shell, UI thread. The last block's
|
||||
// loudest channel — a non-consuming read, so it stays correct however often the editor's
|
||||
// meter drains its own accumulators (or never does, with no editor open).
|
||||
double embedActivityLevel() const {
|
||||
const float l = meterPeakL_.load(std::memory_order_relaxed);
|
||||
const float r = meterPeakR_.load(std::memory_order_relaxed);
|
||||
return static_cast<double>(l > r ? l : r);
|
||||
return static_cast<double>(advisoryPeak_.load(std::memory_order_relaxed));
|
||||
}
|
||||
|
||||
// What the audio thread published about the output bus last block. UI thread.
|
||||
MasterBusMeter masterBusMeter() const;
|
||||
// What the audio thread published since the LAST call: peaks maxed and minGain minimised
|
||||
// across every block in that window. CONSUMING — it resets the accumulators as it reads, so
|
||||
// exactly one reader may call it, and that reader is the editor's meter tick. UI thread.
|
||||
MasterBusMeter masterBusMeter();
|
||||
void clearMasterBusClip();
|
||||
|
||||
// Resolves the selection against the instance-owned SampleRefs, decodes its WAV
|
||||
@@ -284,13 +285,28 @@ private:
|
||||
// Requires reloadMutex_ held — shared by reloadInstrument and rebuildVoiceEngine.
|
||||
void publishBuiltLocked(std::unique_ptr<LoadedInstrument> built);
|
||||
|
||||
// Publishes a silent block to the meter. EVERY process() path that emits no audio calls
|
||||
// this, or the bar freezes at the last peak it saw. The clip latch is deliberately not
|
||||
// touched — it survives silence until the user clears it.
|
||||
// Folds one block's reading into the accumulator it belongs to — a running max for a peak,
|
||||
// a running min for the limiter's gain. Read-modify-write rather than a bare store, so the
|
||||
// ~47 blocks that elapse between two 500 ms UI frames at 48 kHz/512 all reach the meter
|
||||
// instead of the one it happened to sample. Relaxed throughout: the accumulators are
|
||||
// advisory, and no other state is ordered against them. Block rate, never per frame.
|
||||
static void foldPeak(std::atomic<float>& acc, float blockPeak) {
|
||||
if (blockPeak > acc.load(std::memory_order_relaxed)) {
|
||||
acc.store(blockPeak, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
static void foldMinGain(std::atomic<float>& acc, float blockMinGain) {
|
||||
if (blockMinGain < acc.load(std::memory_order_relaxed)) {
|
||||
acc.store(blockMinGain, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
// Publishes a silent block. EVERY process() path that emits no audio calls this. It clears
|
||||
// only the ADVISORY level, which is a last-block reading: the meter accumulators need
|
||||
// nothing here, because a block that emitted no audio contributes no peak and no gain
|
||||
// reduction, and the UI's own read is what resets them.
|
||||
void publishSilentMeterBlock() {
|
||||
meterPeakL_.store(0.f, std::memory_order_relaxed);
|
||||
meterPeakR_.store(0.f, std::memory_order_relaxed);
|
||||
meterMinGain_.store(1.f, std::memory_order_relaxed);
|
||||
advisoryPeak_.store(0.f, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// Mirrors the persisted limiter enable onto the audio thread and the latency reader. Called
|
||||
@@ -456,13 +472,21 @@ private:
|
||||
instrument::engine::Limiter limiter_;
|
||||
std::atomic<bool> limiterEnabled_{false};
|
||||
|
||||
// What the audio thread publishes about the output bus each block, relaxed — peaks, the
|
||||
// latched clip, and the limiter's smallest gain. No dB, no ballistics, no hold timer here;
|
||||
// What the audio thread publishes about the output bus each block, relaxed. The peaks and
|
||||
// minGain ACCUMULATE (max / min) across every block since the UI last read, and
|
||||
// masterBusMeter() resets them as it reads — the fix for a bar that displayed roughly one
|
||||
// block in fifty. No dB, no ballistics, no hold timer here;
|
||||
// the UI runs those off these values and its own elapsed time.
|
||||
std::atomic<float> meterPeakL_{0.f};
|
||||
std::atomic<float> meterPeakR_{0.f};
|
||||
std::atomic<float> meterMinGain_{1.f};
|
||||
std::atomic<bool> meterClip_{false};
|
||||
|
||||
// The embed strip's activity level: the LAST block's loudest channel, plainly overwritten.
|
||||
// Separate from the accumulators above because it answers a different question ("how loud
|
||||
// is it now") and has a different reader — sharing one would make each reader's reset
|
||||
// silently truncate the other's window.
|
||||
std::atomic<float> advisoryPeak_{0.f};
|
||||
};
|
||||
|
||||
} // namespace reasampler::vst
|
||||
|
||||
Reference in New Issue
Block a user