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:
2026-08-02 08:27:05 -04:00
parent 0627398bbb
commit df10ddacc2
29 changed files with 552 additions and 250 deletions
+2 -2
View File
@@ -330,7 +330,7 @@ anything for a trigger shape.
- `sample_chrome` — the CHROME band's interior: the toolbar row (title + the whole right-anchored control run — bake Hold cell, bake, preview, velocity knob cell, loop enable, channel toggle, Browse) over the strip row, which the piano strip owns outright. The title takes what the run leaves; the strip takes its whole row, inset only by the shared band pad so it lines up with the waveform band beneath. Every run member's width is RESERVED unconditionally, the Hold cell included — the only conditionally-drawn one, and the leftmost, so what its reservation buys is a title slot that does not re-measure when a loop is dialled in or out (`sample_chrome.h` records the cost). Also `previewGlyph`, the preview button's play triangle — three vertices for one filled-triangle draw, so the button's label needs no font metric and no image asset.
- `bake_hold` — the Hold knob's value domain and nothing else: the knob's normalized [0,1] mapped onto the note-length ladder and back, ordered by LENGTH rather than by the ladder's presentation order. Split from `sample_chrome` on the same axis `deck_values` was split from `knob_deck` — that says where the cell is, this says what its position means.
- `keyboard_strip` — piano-keyboard strip: true white/black key geometry (whites tiled at one width, blacks overlaid at one width and height, straddling their boundary), hit-test resolving black-over-white by zone, root-marker rect, the absolute-position drag resolver, and MIDI note naming under the C4 convention. **Same-class keys are one integer width by construction; the residue of an indivisible band width (`w % 75`, up to 74 px) lands in symmetric end margins, never in a key** — uniform widths and gap-free edge-to-edge tiling cannot both hold, and uniformity wins.
- `waveform_view` — the WAVEFORM band's interior: `waveformSurface` resolves the drawn lane(s) (two stacked lanes, L over R, only when the mode is stereo AND the source has a second channel — a mono source under stereo mode is dual-mono and draws one lane) plus **the** overlay area, and `laneEnvelope` splits one multi-channel envelope pass per lane. Also maps frame span linearly across a rect; generic named draggable markers with drag-delta resolver, clamp, and zero-crossing snap, plus `markerHandleRect` — a top-strip grab tab distinct from a marker's full-height column, so two markers that share a frame stay independently grabbable (the column goes to the first in draw order; the tab, asked first, resolves the other).
- `waveform_view` — the WAVEFORM band's interior: `resolveLaneSplit` is THE lane-split decision (two lanes only when the mode is stereo AND the source has a second channel — a mono source under stereo mode is dual-mono and draws one lane), free of any pixel geometry so the meter's bar count can ask the same question without a band rect; `waveformSurface` folds it and then measures it against the band, which is why its `laneCount` can still report 1 for a Stereo split on a band too thin to divide. It also yields **the** overlay area, and `laneEnvelope` splits one multi-channel envelope pass per lane. Also maps frame span linearly across a rect; generic named draggable markers with drag-delta resolver, clamp, and zero-crossing snap, plus `markerHandleRect` — a top-strip grab tab distinct from a marker's full-height column, so two markers that share a frame stay independently grabbable (the column goes to the first in draw order; the tab, asked first, resolves the other).
- **Overlay contract (consumed by later waveform work).** `WaveformSurface::overlay` — equivalently the standalone `waveformOverlayArea(band)` — is the FULL band in both modes. Everything riding the waveform (the amp-envelope trace and its node handles, the start/loop markers, the loop region) draws ONCE into it, spanning both stacked lanes; hit-testing resolves against the same area so a grab in the lower lane reaches them. Anything drawn or hit-tested per lane is a duplicate and a defect — structurally enforced: `overlay` is the distinct `OverlayArea` type (`editor_geometry`), not `Rect`, so every overlay-consuming API (`frameToX`/`markerAtPoint`/`resolveDragFrame`, `envelope_edit`'s `nodeAtPoint`/`resolveNodeDrag`, `envelope_overlay`'s `buildEnvelopePolyline`) rejects a lane rect at compile time rather than silently accepting one.
- **The four marks.** One grammar — line + shaped cap + label — over START / LOOP / END / XFADE. `markerHandleRect` IS the cap: every mark's is the same rect shape, only the glyph inside differs, which is what keeps the claim arbitration seeing one nominal cap area. `capAtPoint` resolves caps in the REVERSE of the column order, so any coincident PAIR stays separable (one answers its cap, the other its column) and the crossfade — the one mark with no column — can never be shadowed. `layoutMarkLabels` places the promoted (grabbed/hovered) mark first and suppresses any box that would overlap one already placed. `crossfadeWedgeHeight` is the ONE ramp both the audible region and the ingredient ghost draw, because they are the same fade weight over the two spans it mixes.
- `loop_marks` — the loop enable's state machine, split from the geometry above on the axis the surface already has: that says where a mark is, this says what the loop IS. `SampleLoop::hasLoop` is the single authority and `resolveLoopMarks`/`applyLoopMarks` are its only two folds — the resolve re-parks on `defaultLoopBounds` only when the span is one `resolveLoop` would refuse (so a user's off keeps its positions and `parked` separates the two OFF states), and the write folds collapse-to-off in and ties the crossfade to the SPAN rather than to the enable. Links `loop_span` so the span the user is offered and the span the engine accepts stay one definition.
@@ -357,7 +357,7 @@ anything for a trigger shape.
drag the bank model and the WAV codec in behind it. The shell keeps only the controls the
parameter set does not carry (key-track, voice count, master gain, preview velocity) and the
labels for them.
- `master_meter` — the MASTER column's interior, split from `knob_deck` on the axis `sample_chrome` has to `sample_bands`: that says where the column is, this lays out inside it (22 px numeral gutter · 4 · 36 px bar field) and holds the per-instance UI state the bars draw from. **Bar count takes a RESOLVED `LaneSplit`, the same value `waveformSurface` folds** — a mono source under stereo mode is dual-mono, and two identical bars would be a lie. Composes `engine/meter_ballistics` per channel and adds the gain-reduction lamp's own decay; the audio thread's clip flag is ORed in because it is the only latch that sees the blocks between two UI frames. `meterDrawEqual` is what lets the UI tick repaint on change alone.
- `master_meter` — the MASTER column's interior, split from `knob_deck` on the axis `sample_chrome` has to `sample_bands`: that says where the column is, this lays out inside it (22 px numeral gutter · 4 · 36 px bar field) and holds the per-instance UI state the bars draw from. `kMeterColumnW` is the SUM of those three, exported so `deck_groups`' MASTER descriptor reserves exactly what the interior consumes — the column is banked to grow, and a reserve that did not track it would underfill or overrun silently. **Bar count takes a RESOLVED `LaneSplit`, the same value `waveform_view`'s `resolveLaneSplit` answers** — a mono source under stereo mode is dual-mono, and two identical bars would be a lie. Also owns `meterTickNumeralled` (the spec-pinned 0/−12/−24/−36/−48/−60 numeral set, beside the tick step it derives from), `meterNumeralRect` (bottom-clamped, so the floor tick's numeral cannot hang out of the gutter), and `meterSingleLaneState` — the one bar folds both channels PER FIELD, never picking a whole channel by level. Composes `engine/meter_ballistics` per channel and gives the gain-reduction lamp the peak tick's own hold-then-release, without which a catch smaller than 20 dB × the UI period is dark again before it has been drawn twice; the audio thread's clip flag is ORed in because it is the only latch that sees every block. `meterDrawEqual` is what lets the UI tick repaint on change alone.
- `deck_groups` — also home to `deckParamCommit` and `liveCommitFor`, the editor's whole commit-tier routing decision (see "Live parameter delivery" above), and to `OverlayEnv` + `nextOverlaySelection`/`overlayEnvEnabled`/`overlayEnvInert`, the whole overlay-selection state machine (exclusivity, the none resting state, and which selections a disabled or DRAWN group makes inert); WHICH groups the Sample face's deck carries, split from `knob_deck`'s HOW they lay out: the `DeckParam` control-id space (the editor's `ParamControl` is an alias of it), the `DeckGroupId` list, `sampleDeckGroups` in signal-flow order (**pitch → filter → amp**, then velocity/voice/master), and the deck's bipolar-knob law. Reads `PlayMode` for the AMP group's Gate/Trigger face, which is why this and not `knob_deck` is the module that touches the engine's value layer. Also home to `CurveTarget` + `curveTargetFor` — the VELOCITY group's three cells are popup openers, not dials, and that predicate is the ONE place they are named, so paint, hit-test routing and the popup's title all agree. MASTER is reserved for post-voice-mixer concerns, which is why the curves sit in their own group immediately left of VOICE rather than there; it now discharges that reservation as the double-height bus deck — gain, the limiter enable, one reserved slot, the meter column and the GR lamp. FILTER's `Band|Notch` rides its caption slack rather than the knob row: that is the −92 px that makes the SOUND row fit its block, and putting it back breaks the fit. VOICE's `Retrig|Legato` deliberately stays in the knob row — VOICE's caption row is the binding side, so moving it there makes the group 226 rather than 164.
- `spline_edit` — THE point-editing grammar, and the one place it is written down: left-click grabs a node and adds one in empty space, right-click deletes, control-click toggles hard/smooth. Both spline consumers — the velocity-curve popup and the spline EG overlay — route their mouse-down through `resolveSplineEdit`, so the two cannot drift into two grammars. The endpoint and point-count rules are NOT restated here: `deletePoint` and `addPoint` own them, and the caller applies the resolved action to the curve. Also home to `splineOverlayBox`, the contour's mapping box inside the waveform overlay — the FULL area, no inset, so the drawn contour stays 1:1 with the sample's time axis. Spline points are excluded from `param_taper`'s Shift/Ctrl modifier law like waveform markers are: a point is a normalized position with no displayed unit, and control-click there is already claimed by the hard/smooth toggle above.
- `curve_popup` — pure curve-popup geometry + dismissal test (FB1): centered sheet over the Sample face — width/height clamps, title row, Close button rect, curve-box rect, outside-sheet dismissal test. Mirror of `overflow_menu`; no LICE or REAPER types.
+10 -9
View File
@@ -20,13 +20,11 @@ reasampler_test(capture_browser LINK capture_browser)
reasampler_pure_library(keyboard_strip SOURCES keyboard_strip.cpp LINK PUBLIC editor_geometry)
reasampler_test(keyboard_strip LINK keyboard_strip sample_bands sample_chrome)
# sample_bands is PRIVATE: the lane split is used internally and nothing in the public
# header needs it.
# sample_bands is PUBLIC since resolveLaneSplit answers in its LaneSplit — the meter's bar
# count consumes that answer, so the type is part of this module's surface, not an internal.
reasampler_pure_library(waveform_view
SOURCES waveform_view.cpp
LINK PUBLIC editor_geometry peaks PRIVATE sample_bands)
# sample_bands is linked directly here because the test exercises the lane metrics that
# waveform_view does not re-export.
LINK PUBLIC editor_geometry peaks sample_bands)
reasampler_test(waveform_view LINK waveform_view sample_bands)
# The loop enable's state machine. Links loop_span for the park bounds — the span the user is
@@ -72,12 +70,15 @@ reasampler_test(master_meter LINK master_meter waveform_view)
# PlayMode). velocity_curve is the filter's own curve field; peaks is play_params.h's
# AudioSample dependency. play_params.h also drags in filter/'s headers (FilterSettings,
# MorphLaw) for the v9 filter tail -- plain value types, no filter symbol linked.
# master_meter is PRIVATE: MASTER's descriptor reserves the meter column's own kMeterColumnW,
# but nothing in deck_groups.h names a meter type, so the edge stops at this TU.
reasampler_pure_library(deck_groups
SOURCES deck_groups.cpp
LINK PUBLIC knob_deck velocity_curve peaks curve_law)
# sample_bands is linked directly for the test only: the deck-fits-the-floor-window assertion
# needs the band allocator deck_groups itself has no reason to depend on.
reasampler_test(deck_groups LINK deck_groups sample_bands)
LINK PUBLIC knob_deck velocity_curve peaks curve_law PRIVATE master_meter)
# sample_bands and master_meter are linked directly for the test: the deck-fits-the-floor-window
# assertion needs the band allocator, and the MASTER-reserve identity needs the column width the
# PRIVATE edge above does not re-export.
reasampler_test(deck_groups LINK deck_groups sample_bands master_meter)
# The point-editing grammar both spline consumers share, so it links the curve itself (unlike
# envelope_overlay/envelope_edit, which stay engine-free — the staged envelopes touch no curve).
+5 -5
View File
@@ -4,6 +4,8 @@
#include <utility>
#include "core/instrument/ui/master_meter.h" // kMeterColumnW (what the column's interior needs)
namespace reasampler::instrument::ui {
namespace {
@@ -15,10 +17,6 @@ double clamp(double v, double lo, double hi) { return v < lo ? lo : (v > hi ? hi
// row at 47 (AMP, the next tightest, at 55). Well inside the ceiling — raising it would widen
// the CONTOUR row, which has 144px of slack, not the SOUND row.
constexpr int kEnvModeSegW = 23;
// The output meter's column, right of MASTER's cell slots. 62 + the 60px cell + the gap + the
// group's own padding is exactly kDeckSpanningW.
constexpr int kMasterMeterW = 62;
} // namespace
double deckBipolarFromNorm(double norm) { return clamp(norm, 0.0, 1.0) * 2.0 - 1.0; }
@@ -142,7 +140,9 @@ std::vector<DeckGroupDesc> sampleDeckGroups(PlayMode playMode) {
master.captionRadio = {id(DeckParam::kMasterGr), /*passive=*/true};
master.captionToggle = {id(DeckParam::kLimiterEnable), 32};
master.cellIds = {id(DeckParam::kMasterGain), -1};
master.column = {id(DeckParam::kMasterMeter), kMasterMeterW};
// The reserve IS what the interior consumes — read from master_meter rather than
// restated, so the two cannot drift when the column grows into MASTER's banked room.
master.column = {id(DeckParam::kMasterMeter), kMeterColumnW};
out.push_back(std::move(master));
}
for (DeckGroupDesc& d : out) d.row = deckRowFor(static_cast<DeckGroupId>(d.id));
+6 -2
View File
@@ -57,10 +57,14 @@ enum class DeckRow { Sound, Contour, Spanning };
// from the first two — kDeckRowBlockW + kDeckGroupGap + kDeckSpanningW + 2*kPad — and the
// identity is asserted in test_deck_groups.cpp rather than coded, so the allocator keeps no
// include edge to this header.
inline constexpr int kDeckRowBlockW = 1020; // the block both categorical rows justify inside
// 1028 is the width at which the justification law puts BOTH rows' filter groups on the same
// right edge (x = 640 block-relative) AND divides row 1's slack into three equal gutters. The
// narrower 1020 delivered neither: 40 px over three gutters is 13⅓, so the law produced
// 14/13/13 and left row 1's filter edge 2 px past row 2's.
inline constexpr int kDeckRowBlockW = 1028; // the block both categorical rows justify inside
inline constexpr int kDeckSpanningW = 142; // the right-anchored spanning deck, outside the block
// The hard ceiling the floor may not exceed lives beside the floor itself, in sample_bands.h's
// kEditorCeilingWidth — a window fact, not a deck one. Today's gap between the two is 90px,
// kEditorCeilingWidth — a window fact, not a deck one. Today's gap between the two is 82px,
// the whole width budget for the life of this layout (asserted in test_deck_groups.cpp) — see
// instrument-control-surface.md §1.6 before spending any of it.
+49 -6
View File
@@ -4,9 +4,14 @@
namespace reasampler::instrument::ui {
bool meterTickNumeralled(int db) {
// Every OTHER 6 dB tick, which is the 0/−12/−24/−36/−48/−60 set the scale is specified as.
return db % (2 * static_cast<int>(kMeterTickStepDb)) == 0;
}
MeterRects meterRects(const Rect& column, LaneSplit split) {
MeterRects r;
if (column.width <= 0 || column.height <= 0) return r;
if (column.width < kMeterColumnW || column.height <= 0) return r;
r.labels = Rect::ltrb(column.x, column.y, column.x + kMeterLabelW, column.bottom());
const int fieldLeft = column.x + kMeterLabelW + kMeterLabelGap;
r.field = Rect::ltrb(fieldLeft, column.y, fieldLeft + kMeterFieldW, column.bottom());
@@ -21,6 +26,18 @@ MeterRects meterRects(const Rect& column, LaneSplit split) {
return r;
}
Rect meterNumeralRect(const Rect& labels, int y) {
if (labels.empty()) return {};
int top = y - 5;
if (top < labels.y) top = labels.y;
int bottom = top + 10;
if (bottom > labels.bottom()) {
bottom = labels.bottom();
top = bottom - 10 < labels.y ? labels.y : bottom - 10;
}
return Rect::ltrb(labels.x, top, labels.right(), bottom);
}
int meterDbToY(const Rect& field, double db) {
const double norm = engine::meterNormFromDb(db);
const int y = field.bottom() - static_cast<int>(norm * field.height + 0.5);
@@ -34,9 +51,9 @@ MasterMeterUi advanceMasterMeter(MasterMeterUi prev, const MasterMeterBlock& blo
MasterMeterUi next;
next.left = engine::advanceMeter(prev.left, block.peakL, elapsedSeconds);
next.right = engine::advanceMeter(prev.right, block.peakR, elapsedSeconds);
// The audio thread's latch is the authoritative one: it sees every block, where this only
// ever samples the last block before the UI woke. A clip that came and went between two
// frames is invisible to the peaks above and would otherwise be lost.
// ORed in unconditionally. Now that the peaks accumulate, advanceMeter's own >= 0 dBFS
// check sees the same window and would latch too — but the published flag stays the
// definitive one, and it is the half clearMasterBusClip resets.
if (block.clip) {
next.left.clip = true;
next.right.clip = true;
@@ -44,8 +61,23 @@ MasterMeterUi advanceMasterMeter(MasterMeterUi prev, const MasterMeterBlock& blo
const double dt = (elapsedSeconds > 0.0) ? elapsedSeconds : 0.0;
const double reduction = -engine::meterDbFromLinear(block.minGain);
const double fallen = prev.reductionDb - engine::kMeterFallDbPerSecond * dt;
next.reductionDb = reduction > fallen ? reduction : fallen;
// Same hold-then-release shape as the peak tick, for the same reason: at the UI period the
// lamp actually runs at, a bare decay retires a catch before it has been drawn twice.
if (reduction >= prev.reductionDb) {
next.reductionDb = reduction;
next.reductionHoldSeconds = engine::kMeterPeakHoldSeconds;
} else {
next.reductionDb = prev.reductionDb;
next.reductionHoldSeconds = prev.reductionHoldSeconds - dt;
if (next.reductionHoldSeconds < 0.0) {
// Spend the overshoot as fall time so the release does not quantize to whichever
// UI frame the hold happened to expire on.
const double fallen =
next.reductionDb - engine::kMeterFallDbPerSecond * -next.reductionHoldSeconds;
next.reductionDb = fallen > reduction ? fallen : reduction;
next.reductionHoldSeconds = 0.0;
}
}
if (next.reductionDb < 0.0) next.reductionDb = 0.0;
return next;
}
@@ -59,6 +91,17 @@ MasterMeterUi clearMasterMeterClip(MasterMeterUi prev) {
return next;
}
engine::MeterState meterSingleLaneState(const MasterMeterUi& m) {
engine::MeterState s;
s.levelDb = m.left.levelDb > m.right.levelDb ? m.left.levelDb : m.right.levelDb;
s.holdDb = m.left.holdDb > m.right.holdDb ? m.left.holdDb : m.right.holdDb;
s.holdRemainingSeconds = m.left.holdRemainingSeconds > m.right.holdRemainingSeconds
? m.left.holdRemainingSeconds
: m.right.holdRemainingSeconds;
s.clip = m.left.clip || m.right.clip;
return s;
}
bool grLampLit(const MasterMeterUi& m) { return m.reductionDb >= kGrLampFloorDb; }
bool meterDrawEqual(const MasterMeterUi& a, const MasterMeterUi& b) {
+32 -4
View File
@@ -17,9 +17,18 @@ inline constexpr int kMeterLabelGap = 4;
inline constexpr int kMeterFieldW = 36; // one 36px bar, or two 17px bars kMeterBarGap apart
inline constexpr int kMeterBarGap = 2;
// What the interior consumes, and therefore the width the deck must RESERVE for the column.
// knob_deck's MASTER descriptor reads this rather than restating 62 — §1.2 banks MASTER's
// growth to 236 as the meter's growth room, so this constant is expected to move.
inline constexpr int kMeterColumnW = kMeterLabelW + kMeterLabelGap + kMeterFieldW;
// A tick every 6 dB up the scale; every other one carries a numeral, and 0 dB draws heavier.
inline constexpr double kMeterTickStepDb = 6.0;
// Whether the tick at `db` carries a numeral. The numeral SET is spec-pinned (0, −12, −24,
// −36, −48, −60), so it lives beside the step it is derived from rather than in the painter.
bool meterTickNumeralled(int db);
struct MeterRects {
Rect labels; // the numeral gutter
Rect field; // the whole bar field
@@ -27,28 +36,39 @@ struct MeterRects {
Rect barB; // empty() unless Stereo
};
// `split` is the RESOLVED lane decision waveformSurface already folds (channel mode AND the
// `split` is the RESOLVED lane decision resolveLaneSplit already folds (channel mode AND the
// source's channel count), not "is the instrument in stereo mode": a mono source under stereo
// mode is dual-mono, and two identical bars would be a lie. One source, two views, one rule.
// A column narrower than kMeterColumnW yields nothing rather than an overrunning field.
MeterRects meterRects(const Rect& column, LaneSplit split);
// y of `db` inside the bar field — kMeterTopDb at the top edge, kMeterFloorDb at the bottom,
// linear in dB between, clamped outside.
int meterDbToY(const Rect& field, double db);
// The numeral's label rect for the tick at `y`, kept inside the gutter: the floor tick sits ON
// the field's bottom edge, and an unclamped y±5 box would hang below the column.
Rect meterNumeralRect(const Rect& labels, int y);
// The per-instance UI state behind the column. One clip latch per channel (the cap is drawn
// once, over whichever of them tripped).
struct MasterMeterUi {
engine::MeterState left;
engine::MeterState right;
double reductionDb = 0.0; // how far the limiter is pulling gain down; 0 = not working
// The lamp's hold, on the SAME principle (and the same window) as the peak tick's: without
// it a catch smaller than kMeterFallDbPerSecond x the UI period is fully decayed by the
// next frame and the lamp never draws lit at all.
double reductionHoldSeconds = 0.0;
};
// What the audio thread published about the last block, in this module's own vocabulary.
// What the audio thread published SINCE THE LAST READ, in this module's own vocabulary — the
// peaks are a max over every block in that window and minGain a min, so no block is discarded
// unseen between two UI frames.
struct MasterMeterBlock {
double peakL = 0.0;
double peakR = 0.0;
double minGain = 1.0; // the limiter's smallest gain over the block; 1 = no reduction
double minGain = 1.0; // the limiter's smallest gain over the window; 1 = no reduction
bool clip = false; // the AUDIO thread's own latch
};
@@ -58,13 +78,21 @@ MasterMeterUi advanceMasterMeter(MasterMeterUi prev, const MasterMeterBlock& blo
bool meterClipped(const MasterMeterUi& m);
MasterMeterUi clearMasterMeterClip(MasterMeterUi prev);
// What the ONE bar shows on a single-lane column: the two channels folded per FIELD, not the
// louder channel's whole state. Picking a channel by level would draw a hold tick and a clip
// belonging to whichever won on level — inert while L ≡ R on every path that reaches Single,
// and wrong the moment they diverge.
engine::MeterState meterSingleLaneState(const MasterMeterUi& m);
// Whether two states would DRAW the same, so the UI tick can repaint only on a change and an
// idle editor costs nothing. Compares what the column shows — the bar, the held tick, the cap
// and the lamp — not every stored double.
bool meterDrawEqual(const MasterMeterUi& a, const MasterMeterUi& b);
// The lamp reports "the limiter is working", not "a sample grazed the threshold", so it needs
// a floor rather than a bare non-zero test.
// a floor rather than a bare non-zero test. 0.5 dB is a CHOSEN floor, not a measurement — the
// spec asks only for "a small floor". Lowering it makes the lamp flicker on limiting too slight
// to hear; raising it hides genuine catches, since the limiter's ceiling is only −0.3 dBTP.
inline constexpr double kGrLampFloorDb = 0.5;
bool grLampLit(const MasterMeterUi& m);
+2 -2
View File
@@ -21,12 +21,12 @@ inline constexpr int kPad = 8;
// this allocator is deliberately independent of the deck (it takes deckHeight as a parameter
// for exactly that reason), so the derivation is asserted in test_deck_groups.cpp — the one
// place that already includes both headers — rather than coded as an include edge.
inline constexpr int kEditorMinWidth = 1190;
inline constexpr int kEditorMinWidth = 1198;
inline constexpr int kEditorMinHeight = 680;
// The hard ceiling the floor above may not exceed; the window itself still grows freely above
// it. A window fact, sibling of kEditorMinWidth/kEditorMinHeight, not a deck one — moved here
// from knob_deck.h for that reason. The gap to the floor (today: 90px) is the deck's whole
// from knob_deck.h for that reason. The gap to the floor (today: 82px) is the deck's whole
// width budget, spent once; the identity is asserted in test_deck_groups.cpp, the one place
// that already includes both this header and knob_deck.h.
inline constexpr int kEditorCeilingWidth = 1280;
+5 -3
View File
@@ -24,13 +24,15 @@ OverlayArea waveformOverlayArea(const Rect& band) {
return OverlayArea{band.empty() ? Rect{} : band};
}
LaneSplit resolveLaneSplit(bool stereoMode, int sourceChannels) {
return (stereoMode && sourceChannels >= 2) ? LaneSplit::Stereo : LaneSplit::Single;
}
WaveformSurface waveformSurface(const Rect& band, bool stereoMode, int sourceChannels) {
WaveformSurface s;
if (band.empty()) return s;
s.overlay = waveformOverlayArea(band);
const bool twoLanes = stereoMode && sourceChannels >= 2;
const WaveformLanes lanes =
waveformLanes(band, twoLanes ? LaneSplit::Stereo : LaneSplit::Single);
const WaveformLanes lanes = waveformLanes(band, resolveLaneSplit(stereoMode, sourceChannels));
s.upper = lanes.upper;
s.lower = lanes.lower;
// Derived from the resolved lanes, not `twoLanes` — a stereo split's integer division
+11 -3
View File
@@ -11,6 +11,7 @@
#include <cstdint>
#include "core/instrument/ui/editor_geometry.h" // Rect, OverlayArea, contains
#include "core/instrument/ui/sample_bands.h" // LaneSplit (resolveLaneSplit's answer)
#include "core/audio/peaks.h" // AudioSample (float), Envelope
namespace reasampler::instrument::ui {
@@ -35,9 +36,16 @@ struct WaveformSurface {
// the band-stack allocator's kWaveformMinHeight floor.
};
// Resolves the surface for a waveform band. Two lanes need BOTH stereo mode and a source
// that has a second channel to show: a mono source under stereo mode is dual-mono, so a
// second lane would be the redundant duplicate single-lane mode exists to avoid.
// THE lane-split decision, free of any pixel geometry: two lanes need BOTH stereo mode and a
// source that has a second channel to show, since a mono source under stereo mode is dual-mono
// and a second lane would be the redundant duplicate single-lane mode exists to avoid. The one
// home of that rule — the meter's bar count is the SAME question and reads it here, rather than
// inferring it from a band rect it has no business knowing about.
LaneSplit resolveLaneSplit(bool stereoMode, int sourceChannels);
// Resolves the surface for a waveform band, folding the split above and then measuring it
// against the band: WaveformSurface::laneCount can still report 1 for a Stereo split on a band
// too thin to divide, which is a geometry fact and not a second rule.
WaveformSurface waveformSurface(const Rect& band, bool stereoMode, int sourceChannels);
// THE overlay area, standalone — same value as WaveformSurface::overlay, for the hit-test