Reflow the deck into two categorical rows plus a double-height MASTER bus deck
Row membership is now the group's own property, not a wrap outcome. FILTER's Band|Notch moves to its caption slack, which is what makes the sound row fit. MASTER gains the limiter toggle, the output meter and the GR lamp.
This commit is contained in:
@@ -12,7 +12,7 @@ The pure engine/geometry core this shell wraps (`sampler_core`, `pitch_shift`,
|
||||
`sample_chrome`, `keyboard_strip`, `waveform_view`, `loop_marks`, `capture_browser`, `browser_scroll`,
|
||||
`param_slider`, `param_taper`, `trigger_seam`, `velocity_curve`, `embed_strip`, `knob_deck`,
|
||||
`deck_groups`, `deck_values`, `bake_hold`, `curve_popup`, `spline_edit`, `master_gain`,
|
||||
`limiter`, `meter_ballistics`, `reasampler_uid.h`) lives in `core/instrument/*` and
|
||||
`limiter`, `meter_ballistics`, `master_meter`, `reasampler_uid.h`) lives in `core/instrument/*` and
|
||||
`core/wire` and is documented there — this directory consumes it but does not own it.
|
||||
|
||||
## Invariants
|
||||
@@ -124,6 +124,13 @@ 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 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 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
|
||||
|
||||
@@ -89,7 +89,7 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
|
||||
waveform_view loop_marks bank_sync browser_scroll param_slider tooltip
|
||||
theme component_geometry bank_grid trigger_seam envelope_overlay envelope_edit
|
||||
knob_deck deck_groups deck_values curve_popup spline_edit master_gain sample_usage
|
||||
limiter meter_ballistics bake_hold
|
||||
limiter meter_ballistics master_meter bake_hold
|
||||
file_bytes curve_law stroke_aa
|
||||
curve_tessellate
|
||||
bake_plan bake_render bake_reset bake_wire wav_codec)
|
||||
|
||||
@@ -75,10 +75,10 @@ double curveExponentFor(int id, const PlaySeconds& play) {
|
||||
ReaSamplerEditor::FaceLayout ReaSamplerEditor::faceLayout(int w, int h) const {
|
||||
// The ONE resolve every paint and hit-test path goes through, so the band stack, the
|
||||
// chrome interior, and the deck descriptors can never be derived three different ways.
|
||||
// The deck's own wrapped height is the only interior measurement the allocator needs.
|
||||
// The deck's own height is the only interior measurement the allocator needs.
|
||||
FaceLayout fl;
|
||||
fl.deckDescs = sampleDeckGroups(params_.play.playMode);
|
||||
fl.bands = computeSampleBands(w, h, deckHeight(fl.deckDescs, w - 2 * kPad));
|
||||
fl.bands = computeSampleBands(w, h, deckHeight(fl.deckDescs));
|
||||
fl.chrome = chromeRects(fl.bands.chrome, kDeckKnobSize);
|
||||
return fl;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,18 @@ bool ReaSamplerEditor::mouseDownDeck(const FaceLayout& fl, int x, int y) {
|
||||
applyParamControl(hit.id, 0.0, hit.segment);
|
||||
commitAndReload();
|
||||
break;
|
||||
case ParamControl::kLimiterEnable: {
|
||||
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);
|
||||
}
|
||||
invalidate();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// Parameter-set toggles (play mode / pitch engine / pitch-env + filter enable,
|
||||
// and the three env-mode toggles).
|
||||
@@ -80,6 +92,14 @@ bool ReaSamplerEditor::mouseDownDeck(const FaceLayout& fl, int x, int y) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (hit.kind == DeckHitKind::Column) {
|
||||
// The meter's ONLY gesture: clear the latched clip cap. Both latches go — the audio
|
||||
// thread's is what the next tick would otherwise re-latch the UI's from.
|
||||
masterMeter_ = clearMasterMeterClip(masterMeter_);
|
||||
processor_->clearMasterBusClip();
|
||||
invalidate();
|
||||
return true;
|
||||
}
|
||||
if (hit.kind == DeckHitKind::Knob) {
|
||||
// Knobs of a disabled group are drawn but inert.
|
||||
if (deckKnobDisabled(hit.id)) return true;
|
||||
@@ -179,6 +199,9 @@ ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverDeck(const FaceLayout& fl,
|
||||
const DeckLayout dl = layoutDeck(fl.deckDescs, band.x, band.y, band.width);
|
||||
const DeckHit dh = hitTestDeck(dl, x, y);
|
||||
if (dh.kind == DeckHitKind::None) return {};
|
||||
// The meter reports its own state continuously; a hover on it would only mean "the clip
|
||||
// cap is clearable", which the cap's presence already says.
|
||||
if (dh.kind == DeckHitKind::Column) return {};
|
||||
if (dh.kind == DeckHitKind::CaptionRadio) return {HoverKind::kEnvRadio, dh.id};
|
||||
if (dh.kind == DeckHitKind::Knob && dh.inner &&
|
||||
curveParamFor(static_cast<ParamControl>(dh.id)) != ParamControl::kCount) {
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
|
||||
#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 "shell/instrument/editor_internal.h" // kit adapters + knob face
|
||||
#include "shell/instrument/reasampler_processor.h"
|
||||
|
||||
@@ -24,6 +26,75 @@ using namespace reasampler::instrument::ui; // deck geometry
|
||||
// are chrome you read once — this is the readout you read while turning something.
|
||||
constexpr Font kCellLabelFont = Font::Label;
|
||||
|
||||
namespace {
|
||||
|
||||
// One tick's numeral, in whole dB ("0", "-12"). No unit suffix — the column is 22px wide and
|
||||
// the scale's unit is stated once, by the caption.
|
||||
std::string tickLabel(int db) {
|
||||
char buf[8];
|
||||
std::snprintf(buf, sizeof(buf), "%d", db);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// The MASTER column: dB scale in the label gutter, one or two bars, the held peak tick, and
|
||||
// the latched clip cap. `split` is waveformSurface's own lane decision — see master_meter.h.
|
||||
void paintMeterColumn(LICE_IBitmap* bmp, const Rect& column, const MasterMeterUi& state,
|
||||
LaneSplit split) {
|
||||
if (column.width <= 0 || column.height <= 0) return;
|
||||
const MeterRects m = meterRects(column, split);
|
||||
fillSurface(bmp, toKitBox(m.field), Role::BgCell, InteractionState::Rest);
|
||||
|
||||
// Scale: a rule every 6 dB, numeralled every 12 with 0 dB heavier — the reference the
|
||||
// limiter-off case is read against.
|
||||
const LICE_pixel hairline = toLice(roleColor(Role::LineHairline));
|
||||
for (int db = static_cast<int>(instrument::engine::kMeterTopDb);
|
||||
db >= static_cast<int>(instrument::engine::kMeterFloorDb);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// The bars. A single-lane surface shows ONE bar off the louder channel: the two are the
|
||||
// same signal there (dual-mono), so two bars would be a duplicate rather than a reading.
|
||||
const LICE_pixel barInk = toLice(roleColor(Role::AccentPrimary));
|
||||
const LICE_pixel holdInk = toLice(roleColor(Role::TextPrimary));
|
||||
const auto drawBar = [&](const Rect& bar, const instrument::engine::MeterState& ch) {
|
||||
if (bar.empty()) return;
|
||||
const int top = meterDbToY(bar, ch.levelDb);
|
||||
if (top < bar.bottom()) {
|
||||
LICE_FillRect(bmp, bar.x, top, bar.width, bar.bottom() - top, barInk, 1.0f, 0);
|
||||
}
|
||||
if (ch.holdDb > instrument::engine::kMeterFloorDb) {
|
||||
// Clamped so the 2px tick cannot hang past the bar when the hold sits on the floor.
|
||||
const int hold = (std::min)(meterDbToY(bar, ch.holdDb), bar.bottom() - 2);
|
||||
LICE_FillRect(bmp, bar.x, hold, bar.width, 2, holdInk, 1.0f, 0);
|
||||
}
|
||||
};
|
||||
if (split == LaneSplit::Single) {
|
||||
const instrument::engine::MeterState& loudest =
|
||||
state.left.levelDb >= state.right.levelDb ? state.left : state.right;
|
||||
drawBar(m.barA, loudest);
|
||||
} else {
|
||||
drawBar(m.barA, state.left);
|
||||
drawBar(m.barB, state.right);
|
||||
}
|
||||
|
||||
// The clip cap: latched over the whole field, click to clear.
|
||||
if (meterClipped(state)) {
|
||||
LICE_FillRect(bmp, m.field.x, m.field.y, m.field.width, 3,
|
||||
toLice(roleColor(Role::Warn)), 1.0f, 0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
|
||||
const Rect& deckArea = fl.bands.decks;
|
||||
if (deckArea.width <= 0 || deckArea.height <= 0) return;
|
||||
@@ -31,6 +102,14 @@ 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;
|
||||
|
||||
// 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.
|
||||
@@ -122,9 +201,19 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
|
||||
}
|
||||
kitText(bmp, g.caption, caption, Font::Micro, Role::TextDim);
|
||||
|
||||
// The gain-reduction lamp. ROUND, where the overlay radios in this same slot are
|
||||
// square, so it reads as a lamp rather than a control.
|
||||
if (g.captionRadio.id >= 0 && g.captionRadio.passive) {
|
||||
const Rect& rb = g.captionRadio.box;
|
||||
const float r = rb.width / 2.0f - 0.5f;
|
||||
LICE_FillCircle(bmp, rb.x + rb.width / 2.0f, rb.y + rb.height / 2.0f, r,
|
||||
toLice(roleColor(grLampLit(masterMeter_) ? Role::Warn
|
||||
: Role::LineHairline)),
|
||||
1.0f, 0, true);
|
||||
}
|
||||
// The overlay-select radio: filled in the tertiary accent (the colour the overlay
|
||||
// traces in) when this group's envelope is the one on the waveform, hollow otherwise.
|
||||
if (g.captionRadio.id >= 0) {
|
||||
if (g.captionRadio.id >= 0 && !g.captionRadio.passive) {
|
||||
// overlayEnvForRadio returns kNone for BOTH "not a radio id" and "no selection" —
|
||||
// a non-radio id must never read as lit just because nothing is selected, so the
|
||||
// picked env has to be checked against kNone itself, not just matched by equality.
|
||||
@@ -164,6 +253,15 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
|
||||
case ParamControl::kFilterEnable:
|
||||
drawToggle(t, "Off", "On", play.filter.enabled, false);
|
||||
break;
|
||||
case ParamControl::kFilterLaw:
|
||||
drawToggle(t, "Band", "Notch",
|
||||
play.filter.settings.morphLaw ==
|
||||
instrument::engine::filter::MorphLaw::HighNotchLow,
|
||||
!play.filter.enabled);
|
||||
break;
|
||||
case ParamControl::kLimiterEnable:
|
||||
drawToggle(t, "Off", "On", params_.limiterEnabled, false);
|
||||
break;
|
||||
case ParamControl::kAmpEnvMode:
|
||||
drawToggle(t, "Stg", "Spl", play.ampSpline.mode == EnvMode::Spline, false);
|
||||
break;
|
||||
@@ -176,19 +274,14 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
// Row toggles: VOICE's Retrig|Legato (live only in Mono) and FILTER's morph law.
|
||||
// The one row toggle left: VOICE's Retrig|Legato, live only in Mono.
|
||||
if (g.rowToggle.id >= 0) {
|
||||
if (static_cast<ParamControl>(g.rowToggle.id) == ParamControl::kFilterLaw) {
|
||||
drawToggle(g.rowToggle, "Band", "Notch",
|
||||
play.filter.settings.morphLaw ==
|
||||
instrument::engine::filter::MorphLaw::HighNotchLow,
|
||||
!play.filter.enabled);
|
||||
} else {
|
||||
drawToggle(g.rowToggle, "Retrig", "Legato",
|
||||
monoTrigger_ == MonoTrigger::Legato, !isMono);
|
||||
}
|
||||
drawToggle(g.rowToggle, "Retrig", "Legato",
|
||||
monoTrigger_ == MonoTrigger::Legato, !isMono);
|
||||
}
|
||||
|
||||
if (g.column.id >= 0) paintMeterColumn(bmp, g.column.box, masterMeter_, meterSplit);
|
||||
|
||||
// The knobs. A dependent group's knobs draw Disabled (not hidden) — stable geometry.
|
||||
// The predicate is the input side's, so the drawn state and the inert grab agree.
|
||||
for (const DeckCellLayout& c : g.cells) {
|
||||
|
||||
@@ -107,6 +107,26 @@ void ReaSamplerEditor::onSyncTimer() {
|
||||
// rebuild the instrument and repaint under the cursor, yanking the edit — the next
|
||||
// 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.
|
||||
{
|
||||
const unsigned long long now = GetTickCount64();
|
||||
const double elapsed = meterTickMs_ == 0
|
||||
? 0.0
|
||||
: static_cast<double>(now - meterTickMs_) / 1000.0;
|
||||
meterTickMs_ = now;
|
||||
const MasterBusMeter bus = processor_->masterBusMeter();
|
||||
const instrument::ui::MasterMeterUi advanced = instrument::ui::advanceMasterMeter(
|
||||
masterMeter_,
|
||||
{bus.peakL, bus.peakR, bus.minGain, bus.clip},
|
||||
elapsed);
|
||||
const bool changed = !instrument::ui::meterDrawEqual(advanced, masterMeter_);
|
||||
masterMeter_ = advanced;
|
||||
if (changed) invalidate();
|
||||
}
|
||||
|
||||
if (drag_ != DragKind::kNone) return; // defer past the in-flight edit
|
||||
|
||||
// Resolve the bake affordance's availability on the SAME tick that paints it, so it
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "core/instrument/ui/envelope_overlay.h" // StageEnvelope / EnvNode (envelope overlay draw seam)
|
||||
#include "core/instrument/ui/knob_deck.h" // DeckGroupDesc / DeckLayout (the deck band)
|
||||
#include "core/instrument/ui/loop_marks.h" // LoopMarks (the loop enable's state machine)
|
||||
#include "core/instrument/ui/master_meter.h" // MasterMeterUi (the bus meter's UI-side state)
|
||||
#include "core/instrument/ui/sample_bands.h" // SampleBands (the band-stack allocator)
|
||||
#include "core/instrument/ui/waveform_view.h" // WaveMark / WaveMarks (the overlay's marks)
|
||||
#include "core/instrument/ui/spline_edit.h" // the shared point-editing grammar
|
||||
@@ -498,6 +499,11 @@ private:
|
||||
std::string searchQuery_; // type-to-filter narrow; "" = no search
|
||||
bool searchFocused_ = false; // whether the search box has keyboard focus
|
||||
|
||||
// The MASTER deck's meter, advanced from the published block magnitudes on the sync tick
|
||||
// (see onSyncTimer for why it runs mid-drag too). meterTickMs_ 0 = never advanced.
|
||||
instrument::ui::MasterMeterUi masterMeter_;
|
||||
unsigned long long meterTickMs_ = 0;
|
||||
|
||||
// Hover state (transient, never persisted).
|
||||
HoverTarget hover_; // the interactive element under the pointer
|
||||
#ifdef _WIN32
|
||||
|
||||
Reference in New Issue
Block a user