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:
+4
-1
@@ -970,7 +970,10 @@ from `knob_deck.h` into `sample_bands.h` alongside the min-width/min-height pair
|
|||||||
is a window fact rather than a deck one; the deck's own width-budget constants — the row
|
is a window fact rather than a deck one; the deck's own width-budget constants — the row
|
||||||
block (1020) and MASTER's reserved width (142) — stay in `knob_deck.h`. The floor is
|
block (1020) and MASTER's reserved width (142) — stay in `knob_deck.h`. The floor is
|
||||||
derived rather than asserted as a literal: `1020 + 12 (gap) + 142 + 2×8 (pad) = 1190`,
|
derived rather than asserted as a literal: `1020 + 12 (gap) + 142 + 2×8 (pad) = 1190`,
|
||||||
leaving 90 px of headroom against the 1280 px ceiling. Row membership becomes a property of
|
leaving 90 px of headroom against the 1280 px ceiling. (**Both numbers moved afterwards:**
|
||||||
|
Γ-W3-T1 widened the block to 1028 and the floor to 1198 — 82 px of headroom — so the
|
||||||
|
justification law makes the filter tie-line exact. This paragraph is what W1-T4 landed.)
|
||||||
|
Row membership becomes a property of
|
||||||
the group id — `DeckRow { Sound, Contour, Spanning }` plus `deckRowFor(DeckGroupId)`, an
|
the group id — `DeckRow { Sound, Contour, Spanning }` plus `deckRowFor(DeckGroupId)`, an
|
||||||
exhaustive switch (Sound = PITCH/RATE, FILTER, VELOCITY, VOICE; Contour = PITCH ENV, FILTER
|
exhaustive switch (Sound = PITCH/RATE, FILTER, VELOCITY, VOICE; Contour = PITCH ENV, FILTER
|
||||||
ENV, AMP ENVELOPE; Spanning = MASTER) so a future group left unclassified is a compile
|
ENV, AMP ENVELOPE; Spanning = MASTER) so a future group left unclassified is a compile
|
||||||
|
|||||||
+26
-17
@@ -625,7 +625,7 @@ folded into the tracks below:
|
|||||||
table freezes. The editor's visual rows after the reflow were the rejected alternative.
|
table freezes. The editor's visual rows after the reflow were the rejected alternative.
|
||||||
**The reason, because a future reader will ask why the id order does not match the screen:**
|
**The reason, because a future reader will ask why the id order does not match the screen:**
|
||||||
the editor's layout has already moved twice (Θ-W6-T1 grew the floor 840 → 980; Γ-W3-T1 takes
|
the editor's layout has already moved twice (Θ-W6-T1 grew the floor 840 → 980; Γ-W3-T1 takes
|
||||||
it to 1190 and re-rows every group) and within-row order is settled by width fitting, not by
|
it to 1198 and re-rows every group) and within-row order is settled by width fitting, not by
|
||||||
meaning — so **binding a permanently-frozen id order to a demonstrably mobile layout
|
meaning — so **binding a permanently-frozen id order to a demonstrably mobile layout
|
||||||
guarantees the two drift apart**, after which the order is neither logical nor matching.
|
guarantees the two drift apart**, after which the order is neither logical nor matching.
|
||||||
Signal flow is the axis that does not move. Full argument and the accepted residual cost:
|
Signal flow is the axis that does not move. Full argument and the accepted residual cost:
|
||||||
@@ -693,7 +693,7 @@ see "The wave shape after Ruling 1" below.
|
|||||||
|
|
||||||
1. **The reflow is split, canvas from arrangement.** The window floor and the width budget it
|
1. **The reflow is split, canvas from arrangement.** The window floor and the width budget it
|
||||||
is derived from land **early** (Γ-W1-T4), so every other UI track in the phase is drawn,
|
is derived from land **early** (Γ-W1-T4), so every other UI track in the phase is drawn,
|
||||||
tested and judged at the final 1190 × 680 window instead of at a size a later wave changes
|
tested and judged at the final 1198 × 680 window instead of at a size a later wave changes
|
||||||
under it. The two-row *arrangement* stays late (Γ-W3-T1), because it can only be measured
|
under it. The two-row *arrangement* stays late (Γ-W3-T1), because it can only be measured
|
||||||
once the final PITCH/RATE and MASTER descriptors exist. The seam is stated at Γ-W1-T4.
|
once the final PITCH/RATE and MASTER descriptors exist. The seam is stated at Γ-W1-T4.
|
||||||
2. **`preserve-time-stretch` moved W4 → W1-T5.** It is the longest pole in the phase and has
|
2. **`preserve-time-stretch` moved W4 → W1-T5.** It is the longest pole in the phase and has
|
||||||
@@ -825,10 +825,13 @@ exact interim layout; do not "fix" it in a track that does not own it.
|
|||||||
- **The exposed parameter set is DERIVED, never hand-maintained.** A control is a parameter
|
- **The exposed parameter set is DERIVED, never hand-maintained.** A control is a parameter
|
||||||
if and only if its commit class is `Live` or `NoteOnLatched`. There is no second table
|
if and only if its commit class is `Live` or `NoteOnLatched`. There is no second table
|
||||||
beside `deckParamCommit` / `liveCommitFor`, and no list that can drift from it.
|
beside `deckParamCommit` / `liveCommitFor`, and no list that can drift from it.
|
||||||
- **The window floor is 1190 × 680 and must not exceed 1280 × 720.** **Γ-W1-T4 sets it, in
|
- **The window floor is 1198 × 680 and must not exceed 1280 × 720.** **Γ-W1-T4 set it at
|
||||||
wave 1; no other track in the phase may move it**, and from that point every track is
|
1190, in wave 1; no other track in the phase may move it** — with ONE ruled exception,
|
||||||
|
Daniel 2026-08-02: Γ-W3-T1 widened the row block 1020 → 1028 and the floor 1190 → 1198, so
|
||||||
|
the justification law puts both rows' filter edges on one pixel (spec §1.3). That is the
|
||||||
|
only reopening, and only these two constants moved. From that point every track is
|
||||||
authored and judged at it. A track that pushes the floor past 1280 has failed, not overrun.
|
authored and judged at it. A track that pushes the floor past 1280 has failed, not overrun.
|
||||||
**`kEditorMinHeight` stays 680** (Γ-F1). The remaining **90 px of width headroom is the
|
**`kEditorMinHeight` stays 680** (Γ-F1). The remaining **82 px of width headroom is the
|
||||||
budget for the life of this layout** — one deck cell is 60 px, so there is room for exactly
|
budget for the life of this layout** — one deck cell is 60 px, so there is room for exactly
|
||||||
one more, once. Spec §1.6 states the ledger; read it before adding any control. Chrome-row
|
one more, once. Spec §1.6 states the ledger; read it before adding any control. Chrome-row
|
||||||
additions are a **separate purse** (they are paid for out of the title slot, not the floor)
|
additions are a **separate purse** (they are paid for out of the title slot, not the floor)
|
||||||
@@ -900,7 +903,9 @@ into `sample_bands.h`) — leaving 90 px of headroom. Row membership becomes a p
|
|||||||
group id via an exhaustive `deckRowFor(DeckGroupId)` switch (Sound / Contour / Spanning),
|
group id via an exhaustive `deckRowFor(DeckGroupId)` switch (Sound / Contour / Spanning),
|
||||||
consumed by no one yet — **that consumption, and the fit inside the 1020 block, is
|
consumed by no one yet — **that consumption, and the fit inside the 1020 block, is
|
||||||
Γ-W3-T1's** to assert. No drawing code, descriptor, parameter, or audio changed in this
|
Γ-W3-T1's** to assert. No drawing code, descriptor, parameter, or audio changed in this
|
||||||
track.
|
track. **Superseded in part:** Γ-W3-T1 asserted the fit and found 1020 could not deliver the
|
||||||
|
tie-line it was chosen for, so the block is now 1028 and the floor 1198 — see that track's
|
||||||
|
block below.
|
||||||
|
|
||||||
#### Γ-W1-T5 — `preserve-time-stretch`
|
#### Γ-W1-T5 — `preserve-time-stretch`
|
||||||
|
|
||||||
@@ -1063,14 +1068,18 @@ waveform band.
|
|||||||
divided equally among the row's (n−1) gutters, integer residue to the leftmost;
|
divided equally among the row's (n−1) gutters, integer residue to the leftmost;
|
||||||
**no gutter narrower than `kDeckGroupGap` (12)**. **Decks are never stretched.** MASTER is
|
**no gutter narrower than `kDeckGroupGap` (12)**. **Decks are never stretched.** MASTER is
|
||||||
not part of either row's justification.
|
not part of either row's justification.
|
||||||
- **Row block = 1020 px at the floor**, giving row 1 gutters 12/14/14 and row 2 gutters 72/72,
|
- **Row block = 1028 px at the floor** (widened from the originally specified 1020 — Daniel,
|
||||||
at which width **FILTER's right edge and FILTER ENV's right edge both land on x = 636**.
|
2026-08-02), giving row 1 gutters 16/16/16 and row 2 gutters 76/76, at which width
|
||||||
That tie-line, row 2's equal gutters, and row 1's minimum gutter being exactly
|
**FILTER's right edge and FILTER ENV's right edge both land on x = 640**. The tie-line and
|
||||||
`kDeckGroupGap` all hold at 1020 and only at 1020 — **this is why the floor is 1190 and not
|
both rows' equal gutters hold at 1028 because each row's slack divides by its gutter count
|
||||||
1186.** Above the floor the tie-line drifts and that is accepted (spec §1.3).
|
with no residue. **This is why the floor is 1198 and not 1190.** The originally specified
|
||||||
- **The floor is already 1190 × 680 and the bands are already 216 / 358** — Γ-W1-T4 landed all
|
1020 delivered NEITHER the tie-line (638 vs 636) nor the claimed exactly-`kDeckGroupGap`
|
||||||
|
smallest gutter (13); the three properties were never simultaneously satisfiable, and 12 is
|
||||||
|
a floor rather than a target — spec §1.3 records all three deviations. Above the floor the
|
||||||
|
tie-line drifts and that is accepted (spec §1.3).
|
||||||
|
- **The bands are already 216 / 358 and the floor was already 1190 × 680** — Γ-W1-T4 landed all
|
||||||
four in wave 1, and the greedy wrap happened to reach two rows at that width. **This track
|
four in wave 1, and the greedy wrap happened to reach two rows at that width. **This track
|
||||||
changes none of those numbers; it makes them true by construction instead of by coincidence.**
|
changes only the row block and the floor (see above); the rest it makes true by construction rather than by coincidence.**
|
||||||
Row 1's natural width fits the block **only after this track's `Band|Notch` move**: 1030
|
Row 1's natural width fits the block **only after this track's `Band|Notch` move**: 1030
|
||||||
today, +42 from W2-T1's PITCH/RATE, −92 here, = **980**. That is this track's fit assertion
|
today, +42 from W2-T1's PITCH/RATE, −92 here, = **980**. That is this track's fit assertion
|
||||||
and W1-T4 deliberately left it open.
|
and W1-T4 deliberately left it open.
|
||||||
@@ -1109,10 +1118,10 @@ waveform band.
|
|||||||
natural width is mode-stable at 876 because the reserve slots hold FILTER ENV and AMP at
|
natural width is mode-stable at 876 because the reserve slots hold FILTER ENV and AMP at
|
||||||
312 in both modes — assert it).
|
312 in both modes — assert it).
|
||||||
- Row 1 and row 2 are **flush left and flush right**; at the floor width the filter tie-line
|
- Row 1 and row 2 are **flush left and flush right**; at the floor width the filter tie-line
|
||||||
is exact (both edges at x = 636) and row 2's two gutters are equal.
|
is exact (both edges at x = 640) and BOTH rows' gutters are equal.
|
||||||
- **Row 1's natural width is 980 and fits the 1020 block** — the fit Γ-W1-T4 could not yet
|
- **Row 1's natural width is 980 and fits the 1028 block** — the fit Γ-W1-T4 could not yet
|
||||||
assert, closed here by the `Band|Notch` move.
|
assert, closed here by the `Band|Notch` move.
|
||||||
- **`kEditorMinWidth` is still 1190 and the floor is still ≤ 1280 × 720** — unchanged by this
|
- **`kEditorMinWidth` is 1198 and the floor is still ≤ 1280 × 720** — moved 1190 → 1198 by this
|
||||||
track, verified against Γ-W1-T4's derived test rather than a second copy of it.
|
track, verified against Γ-W1-T4's derived test rather than a second copy of it.
|
||||||
- The waveform band is **358 px at the floor**, and the deck band is 216 — **unchanged from the
|
- The waveform band is **358 px at the floor**, and the deck band is 216 — **unchanged from the
|
||||||
interim, now reached by construction**: `deckRowCount` at and above the floor is 2 because the
|
interim, now reached by construction**: `deckRowCount` at and above the floor is 2 because the
|
||||||
@@ -3633,7 +3642,7 @@ Phase Γ — The instrument's control surface (none of the seventeen; ends
|
|||||||
+ 10 s ceiling + AHDSR schematic axis [Ruling 2]
|
+ 10 s ceiling + AHDSR schematic axis [Ruling 2]
|
||||||
T2 master-bus-audio ........... limiter + meter ballistics + dynamic PDC [rung 1]
|
T2 master-bus-audio ........... limiter + meter ballistics + dynamic PDC [rung 1]
|
||||||
T3 contour-trace-curves ....... staged traces draw curved, knot on its trace
|
T3 contour-trace-curves ....... staged traces draw curved, knot on its trace
|
||||||
T4 editor-floor-and-row-law ... floor 1190x680 + budget constants + row predicate
|
T4 editor-floor-and-row-law ... floor 1190x680 (W3-T1: 1198) + budget constants + row predicate
|
||||||
T5 preserve-time-stretch ...... real stretcher [measure-and-report gate]
|
T5 preserve-time-stretch ...... real stretcher [measure-and-report gate]
|
||||||
T6 exhaustive-switch gate on pure libraries ... /we4062, -Werror=switch on
|
T6 exhaustive-switch gate on pure libraries ... /we4062, -Werror=switch on
|
||||||
pure libraries [no PLAN entry — see COMPLETED.md]
|
pure libraries [no PLAN entry — see COMPLETED.md]
|
||||||
|
|||||||
@@ -31,14 +31,15 @@ own width formula, not carried over from a prior measurement. The stale geometry
|
|||||||
**PITCH/RATE | FILTER | VELOCITY | VOICE** (sound). Row 2 is **PITCH ENV | FILTER ENV |
|
**PITCH/RATE | FILTER | VELOCITY | VOICE** (sound). Row 2 is **PITCH ENV | FILTER ENV |
|
||||||
AMP ENVELOPE** (contour). **MASTER spans both rows on the far right.**
|
AMP ENVELOPE** (contour). **MASTER spans both rows on the far right.**
|
||||||
- **The arithmetic closes, with room.** Minimum/default window goes **980 × 680 →
|
- **The arithmetic closes, with room.** Minimum/default window goes **980 × 680 →
|
||||||
1190 × 680**, inside the settled 1280 × 720 ceiling with **90 px of headroom**. The deck
|
1198 × 680**, inside the settled 1280 × 720 ceiling with **82 px of headroom**. The deck
|
||||||
band drops **328 → 216 px**, returning **112 px to the waveform** (246 → 358 px at the
|
band drops **328 → 216 px**, returning **112 px to the waveform** (246 → 358 px at the
|
||||||
floor). **That 90 px is the governing budget for every future control addition** — one
|
floor). **That 82 px is the governing budget for every future control addition** — one
|
||||||
deck cell is 60 px, so the layout has room for exactly one more, once. §1.6.
|
deck cell is 60 px, so the layout has room for exactly one more, once. §1.6.
|
||||||
- **The two rows align exactly, not nearly.** At the floor width the row block is 1020 px,
|
- **The two rows align exactly, not nearly.** At the floor width the row block is 1028 px,
|
||||||
and at that width row 2's two gutters are equal (72 px each) *and* FILTER's right edge
|
and at that width BOTH rows' gutters are equal (16/16/16 and 76/76) *and* FILTER's right
|
||||||
lands exactly on FILTER ENV's right edge (both at x = 636). That is the aesthetic tie
|
edge lands exactly on FILTER ENV's right edge (both at x = 640). That is the aesthetic tie
|
||||||
between the rows and it falls out of the arithmetic — §1.3.
|
between the rows and it falls out of the arithmetic — §1.3, which also records the three
|
||||||
|
properties the originally-specified 1020 block was claimed to deliver and did not.
|
||||||
- **PITCH becomes PITCH/RATE**: three knobs (`Key Trk | Rate | Pitch`) under the existing
|
- **PITCH becomes PITCH/RATE**: three knobs (`Key Trk | Rate | Pitch`) under the existing
|
||||||
Varisp|Presrv toggle. Rate 50–200 % exponential, Pitch ±24 st.
|
Varisp|Presrv toggle. Rate 50–200 % exponential, Pitch ±24 st.
|
||||||
- **MASTER becomes the post-voice-mixer deck it was always reserved to be**: limiter
|
- **MASTER becomes the post-voice-mixer deck it was always reserved to be**: limiter
|
||||||
@@ -117,30 +118,31 @@ and `knobRowWidth = |cellIds|·kDeckCellW (+ 4 + 2·segWidth for a rowToggle)`.
|
|||||||
|
|
||||||
| | Natural content | Gutters at floor | **Row width** |
|
| | Natural content | Gutters at floor | **Row width** |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Row 1 | 192 + 432 + 192 + 164 = **980** | 12 + 14 + 14 = 40 | **1020** |
|
| Row 1 | 192 + 432 + 192 + 164 = **980** | 16 + 16 + 16 = 48 | **1028** |
|
||||||
| Row 2 | 252 + 312 + 312 = **876** | 72 + 72 = 144 | **1020** |
|
| Row 2 | 252 + 312 + 312 = **876** | 76 + 76 = 152 | **1028** |
|
||||||
|
|
||||||
**Window floor.**
|
**Window floor.**
|
||||||
|
|
||||||
```
|
```
|
||||||
deck band width = 1020 (row block) + 12 (kDeckGroupGap) + 142 (MASTER) = 1174
|
deck band width = 1028 (row block) + 12 (kDeckGroupGap) + 142 (MASTER) = 1182
|
||||||
kEditorMinWidth = 1174 + 2·kPad(8) = 1190
|
kEditorMinWidth = 1182 + 2·kPad(8) = 1198
|
||||||
kEditorMinHeight = 680 (unchanged)
|
kEditorMinHeight = 680 (unchanged)
|
||||||
deck band height = 2·kDeckGroupH(104) + kDeckRowGap(8) = 216 (was 328)
|
deck band height = 2·kDeckGroupH(104) + kDeckRowGap(8) = 216 (was 328)
|
||||||
waveform band at the floor = 680 − 90 (chrome) − 4 − 4 − 8 − 216 = 358 (was 246)
|
waveform band at the floor = 680 − 90 (chrome) − 4 − 4 − 8 − 216 = 358 (was 246)
|
||||||
```
|
```
|
||||||
|
|
||||||
**1190 × 680, against a 1280 × 720 ceiling — 90 px of width headroom, 40 px of height.**
|
**1198 × 680, against a 1280 × 720 ceiling — 82 px of width headroom, 40 px of height.**
|
||||||
|
|
||||||
> **Who lands which half.** The floor, the three budget constants it is derived from
|
> **Who lands which half.** The floor, the three budget constants it is derived from
|
||||||
> (row block 1020 · MASTER 142 · ceiling 1280) and each group's row membership land in
|
> (row block · MASTER 142 · ceiling 1280) and each group's row membership land in
|
||||||
> **Γ-W1-T4**, in wave 1, so the rest of the phase is authored at the final window. The
|
> **Γ-W1-T4**, in wave 1, so the rest of the phase is authored at the final window. The
|
||||||
> arrangement *inside* that budget — the justification law, the gutters, the tie-line,
|
> arrangement *inside* that budget — the justification law, the gutters, the tie-line,
|
||||||
> MASTER's interior — is **Γ-W3-T1**, because every one of those measures a descriptor that
|
> MASTER's interior — is **Γ-W3-T1**, because every one of those measures a descriptor that
|
||||||
> does not exist until Γ-W2-T1 and Γ-W3-T1 create it. **Row 1's natural width does not fit
|
> does not exist until Γ-W2-T1 and Γ-W3-T1 create it. **Row 1's natural width does not fit
|
||||||
> the 1020 block until Γ-W3-T1**: it is 1030 today, +42 from PITCH/RATE, −92 from FILTER's
|
> the block until Γ-W3-T1**: it is 1030 today, +42 from PITCH/RATE, −92 from FILTER's
|
||||||
> `Band|Notch` caption move, = 980. Row 2's 876 already fits. `docs/PLAN.md` at Γ-W1-T4
|
> `Band|Notch` caption move, = 980. Row 2's 876 already fits. Γ-W1-T4 set the block at 1020
|
||||||
> states the seam and the interim layout in full.
|
> and the floor at 1190; the widen recorded below moved both, and it is the ONLY number of
|
||||||
|
> W1-T4's that this phase reopened. `docs/PLAN.md` at Γ-W1-T4 states the seam in full.
|
||||||
|
|
||||||
Three corrections to the arithmetic in the brief, all small and all in our favour:
|
Three corrections to the arithmetic in the brief, all small and all in our favour:
|
||||||
|
|
||||||
@@ -148,9 +150,13 @@ Three corrections to the arithmetic in the brief, all small and all in our favou
|
|||||||
ceiling with zero slack. 142 is what the deck's own content actually needs (§1.4) and
|
ceiling with zero slack. 142 is what the deck's own content actually needs (§1.4) and
|
||||||
it banks 94 px. MASTER may grow to **236** before the ceiling binds; that is the
|
it banks 94 px. MASTER may grow to **236** before the ceiling binds; that is the
|
||||||
meter's growth room, not a target.
|
meter's growth room, not a target.
|
||||||
2. **The row block is 1020, not 1016.** The extra 4 px is deliberate and is what makes the
|
2. **The row block is 1028, not 1016 and not the 1020 originally specified.** 1020 was
|
||||||
two rows align exactly rather than 2 px apart — §1.3. It is the single cheapest
|
chosen to make the two rows align exactly; it does not — 1020 leaves row 1 a 40 px slack
|
||||||
aesthetic purchase in the phase.
|
that three gutters cannot divide evenly, so the justification law produces 14/13/13 and
|
||||||
|
leaves FILTER's right edge 2 px past FILTER ENV's. **1028 is the width at which the law
|
||||||
|
itself makes the tie-line exact**, with no residue in either row (§1.3). The 8 px is the
|
||||||
|
single cheapest aesthetic purchase in the phase, and it is spent from the headroom
|
||||||
|
ledger in §1.6.
|
||||||
3. **VOICE keeps its row toggle** — confirmed. Moving `Retrig|Legato` to the caption gives
|
3. **VOICE keeps its row toggle** — confirmed. Moving `Retrig|Legato` to the caption gives
|
||||||
`38 + 4 + 80 + 4 + 88 = 214` → **226 px**, wider than 164, because VOICE's caption row is
|
`38 + 4 + 80 + 4 + 88 = 214` → **226 px**, wider than 164, because VOICE's caption row is
|
||||||
the binding side and its knob row is nearly empty. Leave it.
|
the binding side and its knob row is nearly empty. Leave it.
|
||||||
@@ -182,11 +188,24 @@ approximate:
|
|||||||
share a right edge.
|
share a right edge.
|
||||||
2. **The filter tie-line.** At the floor width the two rows' filter groups end on the same
|
2. **The filter tie-line.** At the floor width the two rows' filter groups end on the same
|
||||||
pixel:
|
pixel:
|
||||||
`row 1: 192 + 12 + 432 = 636` · `row 2: 252 + 72 + 312 = 636`.
|
`row 1: 192 + 16 + 432 = 640` · `row 2: 252 + 76 + 312 = 640`.
|
||||||
That is not a coincidence to be preserved by a special rule — it is what row-block
|
That is not a coincidence preserved by a special rule — it is what row-block width
|
||||||
width **1020** buys, and at 1020 row 2's two gutters are *also* exactly equal (72/72)
|
**1028** buys, and at 1028 both rows' gutters are *also* exactly equal (16/16/16 and
|
||||||
and row 1's smallest gutter is *exactly* `kDeckGroupGap`. Three good properties at one
|
76/76), because 1028 leaves each row a slack its gutter count divides with no residue.
|
||||||
width. **This is why the floor is 1190 and not 1186.**
|
**This is why the floor is 1198 and not 1190.**
|
||||||
|
|
||||||
|
> **Corrected 2026-08-02 — this paragraph previously claimed THREE properties at 1020,
|
||||||
|
> and none of the three held there.** It said the tie-line landed at 636, that row 2's
|
||||||
|
> gutters were equal, and that row 1's *smallest gutter was exactly* `kDeckGroupGap` (12).
|
||||||
|
> What 1020 actually produced: row 1's slack is 40 over three gutters, so the law's
|
||||||
|
> equal-division-plus-leftmost-residue rule gives **14/13/13** — not 12/14/14 as §1.2's
|
||||||
|
> table stated, and not a smallest gutter of 12 — and FILTER's right edge lands on **638**
|
||||||
|
> against row 2's 636. Only row 2's equal gutters held. The three were never
|
||||||
|
> simultaneously satisfiable: the tie-line needs 1028, an exactly-12 smallest gutter needs
|
||||||
|
> 1016, and 1020 delivered neither. **`kDeckGroupGap` is a FLOOR — "no gutter narrower
|
||||||
|
> than 12" — never a target**, so the 16 px gutters at 1028 satisfy the real rule and the
|
||||||
|
> third property is withdrawn rather than traded away. Two properties hold at 1028, both
|
||||||
|
> exactly, and the law is what makes them hold.
|
||||||
3. **Shared horizontal baselines.** Every group is `kDeckGroupH` with identical interior
|
3. **Shared horizontal baselines.** Every group is `kDeckGroupH` with identical interior
|
||||||
offsets, so across both rows the caption text, the knob centrelines and the label bands
|
offsets, so across both rows the caption text, the knob centrelines and the label bands
|
||||||
sit on the same four lines. The reflow must not break this — it is free today and
|
sit on the same four lines. The reflow must not break this — it is free today and
|
||||||
@@ -249,32 +268,37 @@ Horizontally the group is `6 + 60 + 8 + 62 + 6 = 142`.
|
|||||||
| Deck rows at the floor width | 3 (by greedy wrap) | **2 (by construction)** |
|
| Deck rows at the floor width | 3 (by greedy wrap) | **2 (by construction)** |
|
||||||
| Deck band height | 328 | **216** |
|
| Deck band height | 328 | **216** |
|
||||||
| Waveform band at the floor | 246 | **358** |
|
| Waveform band at the floor | 246 | **358** |
|
||||||
| Minimum / default window | 980 × 680 | **1190 × 680** |
|
| Minimum / default window | 980 × 680 | **1198 × 680** |
|
||||||
| Ceiling headroom | — | **90 px wide, 40 px tall** |
|
| Ceiling headroom | — | **82 px wide, 40 px tall** |
|
||||||
|
|
||||||
**Costs, named.** The floor width grows by 210 px — an existing saved instance's window
|
**Costs, named.** The floor width grows by 218 px — an existing saved instance's window
|
||||||
grows on open (the same one-time effect Θ-W6-T1 already shipped at 840 → 980, so the
|
grows on open (the same one-time effect Θ-W6-T1 already shipped at 840 → 980, so the
|
||||||
behaviour is precedented, not new). The deck's wrap mechanism stops being the thing that
|
behaviour is precedented, not new). The deck's wrap mechanism stops being the thing that
|
||||||
decides row membership at the floor width (§7.3). And the phase spends its ceiling headroom
|
decides row membership at the floor width (§7.3). And the phase spends its ceiling headroom
|
||||||
budget — §1.6.
|
budget — §1.6.
|
||||||
|
|
||||||
### 1.6 The 90 px headroom is the budget, and it governs every future control
|
### 1.6 The 82 px headroom is the budget, and it governs every future control
|
||||||
|
|
||||||
**Read this before proposing any new knob.** The floor is **1190** against Daniel's hard
|
**Read this before proposing any new knob.** The floor is **1198** against Daniel's hard
|
||||||
**1280** ceiling. That is **90 px of width headroom for the life of this layout**, and it is
|
**1280** ceiling. That is **82 px of width headroom for the life of this layout**, and it is
|
||||||
the single constraint every later addition spends from:
|
the single constraint every later addition spends from:
|
||||||
|
|
||||||
| Purchase | Cost | Headroom after |
|
| Purchase | Cost | Headroom after |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| One more 60 px deck cell on row 1 | 60 | 30 |
|
| One more 60 px deck cell on row 1 | 60 | 22 |
|
||||||
| One more caption toggle on a group whose caption row is the binding side | 0–48 | 42–90 |
|
| One more caption toggle on a group whose caption row is the binding side | 0–48 | 34–82 |
|
||||||
| Widening MASTER to a two-cell left column | 60 | 30 |
|
| Widening MASTER to a two-cell left column | 60 | 22 |
|
||||||
| A second cell *and* a wider MASTER | 120 | **over ceiling** |
|
| A second cell *and* a wider MASTER | 120 | **over ceiling** |
|
||||||
|
|
||||||
|
**The ledger was 90 until the row block widened 1020 → 1028** (§1.2 correction 2, §1.3). Its
|
||||||
|
*purchasing power* is unchanged: one more 60 px deck cell remains affordable (82 − 60 = 22),
|
||||||
|
which is the only purchase this ledger has ever promised, and the second one was already over
|
||||||
|
the ceiling at 90. The 8 px came out of the spare change, not out of the budget's one slot.
|
||||||
|
|
||||||
**This is why MASTER's reserved lower-left slot is ONE cell and not two** (Γ-F5, ruled by
|
**This is why MASTER's reserved lower-left slot is ONE cell and not two** (Γ-F5, ruled by
|
||||||
Daniel 2026-08-01). A two-cell reserve would spend 60 of the 90 up front, on a control
|
Daniel 2026-08-01). A two-cell reserve would spend 60 of the 82 up front, on a control
|
||||||
nobody has named yet, and would effectively freeze row 1 forever: any later row-1 addition
|
nobody has named yet, and would effectively freeze row 1 forever: any later row-1 addition
|
||||||
would then need the remaining 30 px and would not have it. One cell keeps the spare. If the
|
would then need the remaining 22 px and would not have it. One cell keeps the spare. If the
|
||||||
future master-bus control turns out to be two knobs, widening MASTER **then** costs the same
|
future master-bus control turns out to be two knobs, widening MASTER **then** costs the same
|
||||||
60 px it would cost now, and by then the trade is being made against a real control instead
|
60 px it would cost now, and by then the trade is being made against a real control instead
|
||||||
of a guess. **Reserving capacity you have not designed a use for is not free here — it is
|
of a guess. **Reserving capacity you have not designed a use for is not free here — it is
|
||||||
@@ -1167,8 +1191,8 @@ Three reasons for that exact slot:
|
|||||||
2. **Browse stays rightmost.** It is navigation, not a mode — moving it would break the
|
2. **Browse stays rightmost.** It is navigation, not a mode — moving it would break the
|
||||||
established right-edge reading.
|
established right-edge reading.
|
||||||
3. **It costs zero window width.** The run is right-anchored and the title slot absorbs it,
|
3. **It costs zero window width.** The run is right-anchored and the title slot absorbs it,
|
||||||
so `kEditorMinWidth` does not move and **none of §1.6's 90 px headroom is spent.**
|
so `kEditorMinWidth` does not move and **none of §1.6's 82 px headroom is spent.**
|
||||||
*Constraint:* the title slot must still hold its text at the 1190 floor. If it will not,
|
*Constraint:* the title slot must still hold its text at the 1198 floor. If it will not,
|
||||||
the enable's segments narrow — the floor does not move. That is a hard rule, because the
|
the enable's segments narrow — the floor does not move. That is a hard rule, because the
|
||||||
floor is a phase-wide acceptance criterion.
|
floor is a phase-wide acceptance criterion.
|
||||||
|
|
||||||
@@ -1402,7 +1426,7 @@ ceiling.
|
|||||||
| **Γ-F2** | Limiter lookahead, or zero-latency? | **Lookahead with DYNAMIC reported latency** — zero when off, the lookahead when on, reported to the host's PDC. *Overrides this doc's zero-lookahead recommendation.* | **§3.1.1** (new), §7.10 |
|
| **Γ-F2** | Limiter lookahead, or zero-latency? | **Lookahead with DYNAMIC reported latency** — zero when off, the lookahead when on, reported to the host's PDC. *Overrides this doc's zero-lookahead recommendation.* | **§3.1.1** (new), §7.10 |
|
||||||
| **Γ-F3** | Does the log taper raise the 2 s stage-time ceiling? | **REVERSED, same day. Ruled first "not in this phase — stays 2.0 s"; then Daniel: _"extend the stage lengths to 10s."_ The ceiling moves 2.0 → 10.0 in Γ-W1-T1.** The reversal's cause is Ruling 1: parameters now ship in-phase, so the ceiling is a one-way door that has to be walked through *before* them. | **§4.3.1** (new), §4.3; `docs/TODO.md` entry discharged |
|
| **Γ-F3** | Does the log taper raise the 2 s stage-time ceiling? | **REVERSED, same day. Ruled first "not in this phase — stays 2.0 s"; then Daniel: _"extend the stage lengths to 10s."_ The ceiling moves 2.0 → 10.0 in Γ-W1-T1.** The reversal's cause is Ruling 1: parameters now ship in-phase, so the ceiling is a one-way door that has to be walked through *before* them. | **§4.3.1** (new), §4.3; `docs/TODO.md` entry discharged |
|
||||||
| **Γ-F4** | Explicit loop enable? | **Yes — on the CHROME ROW.** Not a deck cell; loop is a waveform-overlay concept and has no deck. | **§6.4** (new), §6.5, §7.9 |
|
| **Γ-F4** | Explicit loop enable? | **Yes — on the CHROME ROW.** Not a deck cell; loop is a waveform-overlay concept and has no deck. | **§6.4** (new), §6.5, §7.9 |
|
||||||
| **Γ-F5** | MASTER's reserved slot: one cell or two? | **One cell.** Two would spend 60 of the 90 px headroom on an unnamed control and freeze row 1 forever. | **§1.6** (new), §1.4 |
|
| **Γ-F5** | MASTER's reserved slot: one cell or two? | **One cell.** Two would spend 60 of the 82 px headroom on an unnamed control and freeze row 1 forever. | **§1.6** (new), §1.4 |
|
||||||
| **Γ-F6** | Is the `kLatencyChanged` deactivate/reactivate acceptable as the cost of the toggle? | **Yes — ship dynamic latency as ruled.** No constant-latency fallback, no measurement gate. *Corrected this doc's analysis: the cost is self-inflicted, not SDK-imposed.* | **§3.1.1** (rewritten), §7.10, §7.11, `docs/TODO.md` |
|
| **Γ-F6** | Is the `kLatencyChanged` deactivate/reactivate acceptable as the cost of the toggle? | **Yes — ship dynamic latency as ruled.** No constant-latency fallback, no measurement gate. *Corrected this doc's analysis: the cost is self-inflicted, not SDK-imposed.* | **§3.1.1** (rewritten), §7.10, §7.11, `docs/TODO.md` |
|
||||||
| **Γ-F7** | VST3 parameter ORDER: signal flow, or the editor's visual rows? | **Signal flow** — *"signal flow order."* The frozen id numbering and the presentation index both follow the deck's own rule; the visual layout is too mobile to freeze against. | **§8.3**; `parameter-automation.md` §6.4 (argument) and §6.2 (the 44-id table) |
|
| **Γ-F7** | VST3 parameter ORDER: signal flow, or the editor's visual rows? | **Signal flow** — *"signal flow order."* The frozen id numbering and the presentation index both follow the deck's own rule; the visual layout is too mobile to freeze against. | **§8.3**; `parameter-automation.md` §6.4 (argument) and §6.2 (the 44-id table) |
|
||||||
|
|
||||||
@@ -1534,7 +1558,7 @@ ceiling into W1 (§4.3.1) and turned the Ξ ordering constraint into an owned co
|
|||||||
|
|
||||||
1. **Item B splits: canvas early, arrangement late.** The window floor, the width budget it
|
1. **Item B splits: canvas early, arrangement late.** The window floor, the width budget it
|
||||||
derives from, and each group's row membership land in W1-T4 so every other UI track is
|
derives from, and each group's row membership land in W1-T4 so every other UI track is
|
||||||
drawn, tested and judged at the final 1190 × 680 window. The two-row layout itself stays in
|
drawn, tested and judged at the final 1198 × 680 window. The two-row layout itself stays in
|
||||||
W3-T1, because it can only be measured once the final PITCH/RATE and MASTER descriptors
|
W3-T1, because it can only be measured once the final PITCH/RATE and MASTER descriptors
|
||||||
exist. The exact seam — what W1-T4 can assert, what it cannot, and what the editor looks
|
exist. The exact seam — what W1-T4 can assert, what it cannot, and what the editor looks
|
||||||
like in between — is in `docs/PLAN.md` at Γ-W1-T4.
|
like in between — is in `docs/PLAN.md` at Γ-W1-T4.
|
||||||
|
|||||||
@@ -499,7 +499,7 @@ will eventually propose "fixing" that. The answer is that the two *cannot* both
|
|||||||
forever, and only one of the two axes holds still:
|
forever, and only one of the two axes holds still:
|
||||||
|
|
||||||
> **The editor's visual layout has already moved twice** — Θ-W6-T1 grew the window floor
|
> **The editor's visual layout has already moved twice** — Θ-W6-T1 grew the window floor
|
||||||
> 840 → 980, and Γ-W3-T1 takes it to 1190 and re-rows every group into two categorical rows
|
> 840 → 980, and Γ-W3-T1 takes it to 1198 and re-rows every group into two categorical rows
|
||||||
> with a double-height MASTER. Within-row order is decided by *width fitting*, not by meaning.
|
> with a double-height MASTER. Within-row order is decided by *width fitting*, not by meaning.
|
||||||
> **Binding a permanently-frozen id order to a demonstrably mobile layout guarantees the two
|
> **Binding a permanently-frozen id order to a demonstrably mobile layout guarantees the two
|
||||||
> drift apart** — and after the first drift the order is neither logical *nor* matching, which
|
> drift apart** — and after the first drift the order is neither logical *nor* matching, which
|
||||||
|
|||||||
@@ -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.
|
- `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.
|
- `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.
|
- `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.
|
- **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.
|
- **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.
|
- `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
|
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
|
parameter set does not carry (key-track, voice count, master gain, preview velocity) and the
|
||||||
labels for them.
|
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.
|
- `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.
|
- `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.
|
- `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.
|
||||||
|
|||||||
@@ -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_pure_library(keyboard_strip SOURCES keyboard_strip.cpp LINK PUBLIC editor_geometry)
|
||||||
reasampler_test(keyboard_strip LINK keyboard_strip sample_bands sample_chrome)
|
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
|
# sample_bands is PUBLIC since resolveLaneSplit answers in its LaneSplit — the meter's bar
|
||||||
# header needs it.
|
# count consumes that answer, so the type is part of this module's surface, not an internal.
|
||||||
reasampler_pure_library(waveform_view
|
reasampler_pure_library(waveform_view
|
||||||
SOURCES waveform_view.cpp
|
SOURCES waveform_view.cpp
|
||||||
LINK PUBLIC editor_geometry peaks PRIVATE sample_bands)
|
LINK PUBLIC editor_geometry peaks sample_bands)
|
||||||
# sample_bands is linked directly here because the test exercises the lane metrics that
|
|
||||||
# waveform_view does not re-export.
|
|
||||||
reasampler_test(waveform_view LINK waveform_view 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
|
# 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
|
# 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,
|
# 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.
|
# 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
|
reasampler_pure_library(deck_groups
|
||||||
SOURCES deck_groups.cpp
|
SOURCES deck_groups.cpp
|
||||||
LINK PUBLIC knob_deck velocity_curve peaks curve_law)
|
LINK PUBLIC knob_deck velocity_curve peaks curve_law PRIVATE master_meter)
|
||||||
# sample_bands is linked directly for the test only: the deck-fits-the-floor-window assertion
|
# sample_bands and master_meter are linked directly for the test: the deck-fits-the-floor-window
|
||||||
# needs the band allocator deck_groups itself has no reason to depend on.
|
# assertion needs the band allocator, and the MASTER-reserve identity needs the column width the
|
||||||
reasampler_test(deck_groups LINK deck_groups sample_bands)
|
# 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
|
# 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).
|
# envelope_overlay/envelope_edit, which stay engine-free — the staged envelopes touch no curve).
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#include "core/instrument/ui/master_meter.h" // kMeterColumnW (what the column's interior needs)
|
||||||
|
|
||||||
namespace reasampler::instrument::ui {
|
namespace reasampler::instrument::ui {
|
||||||
|
|
||||||
namespace {
|
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
|
// 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.
|
// the CONTOUR row, which has 144px of slack, not the SOUND row.
|
||||||
constexpr int kEnvModeSegW = 23;
|
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
|
} // namespace
|
||||||
|
|
||||||
double deckBipolarFromNorm(double norm) { return clamp(norm, 0.0, 1.0) * 2.0 - 1.0; }
|
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.captionRadio = {id(DeckParam::kMasterGr), /*passive=*/true};
|
||||||
master.captionToggle = {id(DeckParam::kLimiterEnable), 32};
|
master.captionToggle = {id(DeckParam::kLimiterEnable), 32};
|
||||||
master.cellIds = {id(DeckParam::kMasterGain), -1};
|
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));
|
out.push_back(std::move(master));
|
||||||
}
|
}
|
||||||
for (DeckGroupDesc& d : out) d.row = deckRowFor(static_cast<DeckGroupId>(d.id));
|
for (DeckGroupDesc& d : out) d.row = deckRowFor(static_cast<DeckGroupId>(d.id));
|
||||||
|
|||||||
@@ -57,10 +57,14 @@ enum class DeckRow { Sound, Contour, Spanning };
|
|||||||
// from the first two — kDeckRowBlockW + kDeckGroupGap + kDeckSpanningW + 2*kPad — and the
|
// 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
|
// identity is asserted in test_deck_groups.cpp rather than coded, so the allocator keeps no
|
||||||
// include edge to this header.
|
// 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
|
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
|
// 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
|
// 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.
|
// instrument-control-surface.md §1.6 before spending any of it.
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,14 @@
|
|||||||
|
|
||||||
namespace reasampler::instrument::ui {
|
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 meterRects(const Rect& column, LaneSplit split) {
|
||||||
MeterRects r;
|
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());
|
r.labels = Rect::ltrb(column.x, column.y, column.x + kMeterLabelW, column.bottom());
|
||||||
const int fieldLeft = column.x + kMeterLabelW + kMeterLabelGap;
|
const int fieldLeft = column.x + kMeterLabelW + kMeterLabelGap;
|
||||||
r.field = Rect::ltrb(fieldLeft, column.y, fieldLeft + kMeterFieldW, column.bottom());
|
r.field = Rect::ltrb(fieldLeft, column.y, fieldLeft + kMeterFieldW, column.bottom());
|
||||||
@@ -21,6 +26,18 @@ MeterRects meterRects(const Rect& column, LaneSplit split) {
|
|||||||
return r;
|
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) {
|
int meterDbToY(const Rect& field, double db) {
|
||||||
const double norm = engine::meterNormFromDb(db);
|
const double norm = engine::meterNormFromDb(db);
|
||||||
const int y = field.bottom() - static_cast<int>(norm * field.height + 0.5);
|
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;
|
MasterMeterUi next;
|
||||||
next.left = engine::advanceMeter(prev.left, block.peakL, elapsedSeconds);
|
next.left = engine::advanceMeter(prev.left, block.peakL, elapsedSeconds);
|
||||||
next.right = engine::advanceMeter(prev.right, block.peakR, 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
|
// ORed in unconditionally. Now that the peaks accumulate, advanceMeter's own >= 0 dBFS
|
||||||
// ever samples the last block before the UI woke. A clip that came and went between two
|
// check sees the same window and would latch too — but the published flag stays the
|
||||||
// frames is invisible to the peaks above and would otherwise be lost.
|
// definitive one, and it is the half clearMasterBusClip resets.
|
||||||
if (block.clip) {
|
if (block.clip) {
|
||||||
next.left.clip = true;
|
next.left.clip = true;
|
||||||
next.right.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 dt = (elapsedSeconds > 0.0) ? elapsedSeconds : 0.0;
|
||||||
const double reduction = -engine::meterDbFromLinear(block.minGain);
|
const double reduction = -engine::meterDbFromLinear(block.minGain);
|
||||||
const double fallen = prev.reductionDb - engine::kMeterFallDbPerSecond * dt;
|
// Same hold-then-release shape as the peak tick, for the same reason: at the UI period the
|
||||||
next.reductionDb = reduction > fallen ? reduction : fallen;
|
// 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;
|
if (next.reductionDb < 0.0) next.reductionDb = 0.0;
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
@@ -59,6 +91,17 @@ MasterMeterUi clearMasterMeterClip(MasterMeterUi prev) {
|
|||||||
return next;
|
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 grLampLit(const MasterMeterUi& m) { return m.reductionDb >= kGrLampFloorDb; }
|
||||||
|
|
||||||
bool meterDrawEqual(const MasterMeterUi& a, const MasterMeterUi& b) {
|
bool meterDrawEqual(const MasterMeterUi& a, const MasterMeterUi& b) {
|
||||||
|
|||||||
@@ -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 kMeterFieldW = 36; // one 36px bar, or two 17px bars kMeterBarGap apart
|
||||||
inline constexpr int kMeterBarGap = 2;
|
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.
|
// 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;
|
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 {
|
struct MeterRects {
|
||||||
Rect labels; // the numeral gutter
|
Rect labels; // the numeral gutter
|
||||||
Rect field; // the whole bar field
|
Rect field; // the whole bar field
|
||||||
@@ -27,28 +36,39 @@ struct MeterRects {
|
|||||||
Rect barB; // empty() unless Stereo
|
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
|
// 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.
|
// 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);
|
MeterRects meterRects(const Rect& column, LaneSplit split);
|
||||||
|
|
||||||
// y of `db` inside the bar field — kMeterTopDb at the top edge, kMeterFloorDb at the bottom,
|
// y of `db` inside the bar field — kMeterTopDb at the top edge, kMeterFloorDb at the bottom,
|
||||||
// linear in dB between, clamped outside.
|
// linear in dB between, clamped outside.
|
||||||
int meterDbToY(const Rect& field, double db);
|
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
|
// The per-instance UI state behind the column. One clip latch per channel (the cap is drawn
|
||||||
// once, over whichever of them tripped).
|
// once, over whichever of them tripped).
|
||||||
struct MasterMeterUi {
|
struct MasterMeterUi {
|
||||||
engine::MeterState left;
|
engine::MeterState left;
|
||||||
engine::MeterState right;
|
engine::MeterState right;
|
||||||
double reductionDb = 0.0; // how far the limiter is pulling gain down; 0 = not working
|
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 {
|
struct MasterMeterBlock {
|
||||||
double peakL = 0.0;
|
double peakL = 0.0;
|
||||||
double peakR = 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
|
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);
|
bool meterClipped(const MasterMeterUi& m);
|
||||||
MasterMeterUi clearMasterMeterClip(MasterMeterUi prev);
|
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
|
// 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
|
// idle editor costs nothing. Compares what the column shows — the bar, the held tick, the cap
|
||||||
// and the lamp — not every stored double.
|
// and the lamp — not every stored double.
|
||||||
bool meterDrawEqual(const MasterMeterUi& a, const MasterMeterUi& b);
|
bool meterDrawEqual(const MasterMeterUi& a, const MasterMeterUi& b);
|
||||||
|
|
||||||
// The lamp reports "the limiter is working", not "a sample grazed the threshold", so it needs
|
// 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;
|
inline constexpr double kGrLampFloorDb = 0.5;
|
||||||
bool grLampLit(const MasterMeterUi& m);
|
bool grLampLit(const MasterMeterUi& m);
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ inline constexpr int kPad = 8;
|
|||||||
// this allocator is deliberately independent of the deck (it takes deckHeight as a parameter
|
// 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
|
// 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.
|
// 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;
|
inline constexpr int kEditorMinHeight = 680;
|
||||||
|
|
||||||
// The hard ceiling the floor above may not exceed; the window itself still grows freely above
|
// 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
|
// 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
|
// 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.
|
// that already includes both this header and knob_deck.h.
|
||||||
inline constexpr int kEditorCeilingWidth = 1280;
|
inline constexpr int kEditorCeilingWidth = 1280;
|
||||||
|
|||||||
@@ -24,13 +24,15 @@ OverlayArea waveformOverlayArea(const Rect& band) {
|
|||||||
return OverlayArea{band.empty() ? 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 waveformSurface(const Rect& band, bool stereoMode, int sourceChannels) {
|
||||||
WaveformSurface s;
|
WaveformSurface s;
|
||||||
if (band.empty()) return s;
|
if (band.empty()) return s;
|
||||||
s.overlay = waveformOverlayArea(band);
|
s.overlay = waveformOverlayArea(band);
|
||||||
const bool twoLanes = stereoMode && sourceChannels >= 2;
|
const WaveformLanes lanes = waveformLanes(band, resolveLaneSplit(stereoMode, sourceChannels));
|
||||||
const WaveformLanes lanes =
|
|
||||||
waveformLanes(band, twoLanes ? LaneSplit::Stereo : LaneSplit::Single);
|
|
||||||
s.upper = lanes.upper;
|
s.upper = lanes.upper;
|
||||||
s.lower = lanes.lower;
|
s.lower = lanes.lower;
|
||||||
// Derived from the resolved lanes, not `twoLanes` — a stereo split's integer division
|
// Derived from the resolved lanes, not `twoLanes` — a stereo split's integer division
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
#include "core/instrument/ui/editor_geometry.h" // Rect, OverlayArea, contains
|
#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
|
#include "core/audio/peaks.h" // AudioSample (float), Envelope
|
||||||
|
|
||||||
namespace reasampler::instrument::ui {
|
namespace reasampler::instrument::ui {
|
||||||
@@ -35,9 +36,16 @@ struct WaveformSurface {
|
|||||||
// the band-stack allocator's kWaveformMinHeight floor.
|
// the band-stack allocator's kWaveformMinHeight floor.
|
||||||
};
|
};
|
||||||
|
|
||||||
// Resolves the surface for a waveform band. Two lanes need BOTH stereo mode and a source
|
// THE lane-split decision, free of any pixel geometry: two lanes need BOTH stereo mode and a
|
||||||
// that has a second channel to show: a mono source under stereo mode is dual-mono, so a
|
// source that has a second channel to show, since a mono source under stereo mode is dual-mono
|
||||||
// second lane would be the redundant duplicate single-lane mode exists to avoid.
|
// 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);
|
WaveformSurface waveformSurface(const Rect& band, bool stereoMode, int sourceChannels);
|
||||||
|
|
||||||
// THE overlay area, standalone — same value as WaveformSurface::overlay, for the hit-test
|
// THE overlay area, standalone — same value as WaveformSurface::overlay, for the hit-test
|
||||||
|
|||||||
@@ -107,12 +107,13 @@ declared ahead of the instrument slots at that member in `reasampler_processor.h
|
|||||||
## Modules
|
## 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`.
|
- `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_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).
|
- `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.
|
- `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.
|
- `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.
|
- `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.
|
- `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.)*
|
- `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
|
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
|
in-instance — it is NOT a cross-process poller/nonce handshake, and it must not grow into
|
||||||
one.
|
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
|
- **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
|
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
|
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.
|
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:
|
**The published block state is therefore ACCUMULATED, not sampled**: at 48 kHz / 512 frames
|
||||||
correct against `meter_ballistics`' contract, coarse to the eye. A meter-rate timer is a
|
~47 blocks elapse per tick, so the processor folds a per-channel max and a min limiter gain
|
||||||
separate change and is not in yet.
|
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
|
- 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
|
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
|
applicability (`resolveBakeHoldNeeded`) rides the same tick for the same reason, and
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ void ReaSamplerEditor::dragBrowse(int x, int y) {
|
|||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverBrowse(int w, int h, int x,
|
HoverTarget ReaSamplerEditor::hoverBrowse(int w, int h, int x,
|
||||||
int y) const {
|
int y) const {
|
||||||
const BrowseModal bm = computeBrowseModal(w, h);
|
const BrowseModal bm = computeBrowseModal(w, h);
|
||||||
if (contains(bm.back, x, y)) return {HoverKind::kBack, -1};
|
if (contains(bm.back, x, y)) return {HoverKind::kBack, -1};
|
||||||
if (contains(bm.cancel, x, y)) return {HoverKind::kBrowseCancel, -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
|
invalidate(); // live feedback; the commit lands on WM_LBUTTONUP
|
||||||
}
|
}
|
||||||
|
|
||||||
ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverChrome(const FaceLayout& fl, int x,
|
HoverTarget ReaSamplerEditor::hoverChrome(const FaceLayout& fl, int x,
|
||||||
int y) const {
|
int y) const {
|
||||||
const ChromeRects& cr = fl.chrome;
|
const ChromeRects& cr = fl.chrome;
|
||||||
if (contains(cr.navBrowse, x, y)) return {HoverKind::kNavBrowse, -1};
|
if (contains(cr.navBrowse, x, y)) return {HoverKind::kNavBrowse, -1};
|
||||||
if (selectedId_.empty()) return {}; // empty state — no interactive surfaces beyond nav
|
if (selectedId_.empty()) return {}; // empty state — no interactive surfaces beyond nav
|
||||||
|
|||||||
@@ -114,8 +114,8 @@ void ReaSamplerEditor::onMouseRDown(int x, int y) {
|
|||||||
/*addOnEmptySpace=*/false);
|
/*addOnEmptySpace=*/false);
|
||||||
}
|
}
|
||||||
|
|
||||||
ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverCurvePopup(int w, int h, int x,
|
HoverTarget ReaSamplerEditor::hoverCurvePopup(int w, int h, int x,
|
||||||
int y) const {
|
int y) const {
|
||||||
const CurvePopupLayout pl = computeCurvePopup(w, h);
|
const CurvePopupLayout pl = computeCurvePopup(w, h);
|
||||||
if (contains(pl.close, x, y)) return {HoverKind::kPopupClose, -1};
|
if (contains(pl.close, x, y)) return {HoverKind::kPopupClose, -1};
|
||||||
if (!contains(pl.curveBox, x, y)) return {};
|
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);
|
const bool on = (hit.segment == 1);
|
||||||
if (on != params_.limiterEnabled) {
|
if (on != params_.limiterEnabled) {
|
||||||
params_.limiterEnabled = on;
|
params_.limiterEnabled = on;
|
||||||
// The processor's own funnel mirrors the audio-thread flag and requests the
|
// ARMED here, run on the sync tick — the same treatment the bake gets, and
|
||||||
// host's latency restart; a reload would re-decode a WAV the toggle cannot
|
// for the same reason: the processor's funnel requests the host's latency
|
||||||
// change. The local snapshot moves with it so a later commit agrees.
|
// restart, whose deactivate/reactivate calls setActive(true) and re-decodes
|
||||||
processor_->setLimiterEnabled(on);
|
// the WAV. Inline, that whole cycle would run nested inside this mouse
|
||||||
|
// handler with SetCapture held.
|
||||||
|
limiterPending_ = on;
|
||||||
}
|
}
|
||||||
invalidate();
|
invalidate();
|
||||||
break;
|
break;
|
||||||
@@ -192,8 +194,8 @@ void ReaSamplerEditor::dragDeck(int x, int y) {
|
|||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverDeck(const FaceLayout& fl, int x,
|
HoverTarget ReaSamplerEditor::hoverDeck(const FaceLayout& fl, int x,
|
||||||
int y) const {
|
int y) const {
|
||||||
const Rect& band = fl.bands.decks;
|
const Rect& band = fl.bands.decks;
|
||||||
if (!contains(band, x, y)) return {};
|
if (!contains(band, x, y)) return {};
|
||||||
const DeckLayout dl = layoutDeck(fl.deckDescs, band.x, band.y, band.width);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverWaveform(const FaceLayout& fl, int x,
|
HoverTarget ReaSamplerEditor::hoverWaveform(const FaceLayout& fl, int x,
|
||||||
int y) {
|
int y) {
|
||||||
// Caps only: the cap is the grip, so it is the one thing on the overlay a resting pointer
|
// 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.
|
// 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/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/knob_deck.h" // deck layout + kDeckKnobSize
|
||||||
#include "core/instrument/ui/master_meter.h" // the bus meter's column interior + ballistics
|
#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/editor_internal.h" // kit adapters + knob face
|
||||||
#include "shell/instrument/reasampler_processor.h"
|
#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)) {
|
db -= static_cast<int>(kMeterTickStepDb)) {
|
||||||
const int y = meterDbToY(m.field, db);
|
const int y = meterDbToY(m.field, db);
|
||||||
const bool zero = (db == 0);
|
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,
|
LICE_FillRect(bmp, m.field.x, y, m.field.width, zero ? 2 : 1,
|
||||||
zero ? toLice(roleColor(Role::TextDim)) : hairline, 1.0f, 0);
|
zero ? toLice(roleColor(Role::TextDim)) : hairline, 1.0f, 0);
|
||||||
if (numeralled) {
|
if (meterTickNumeralled(db)) {
|
||||||
kitText(bmp, Rect::ltrb(m.labels.x, y - 5, m.labels.right(), y + 5),
|
kitText(bmp, meterNumeralRect(m.labels, y), tickLabel(db).c_str(), Font::Micro,
|
||||||
tickLabel(db).c_str(), Font::Micro, Role::TextDim, Align::Right);
|
Role::TextDim, Align::Right);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,9 +77,7 @@ void paintMeterColumn(LICE_IBitmap* bmp, const Rect& column, const MasterMeterUi
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (split == LaneSplit::Single) {
|
if (split == LaneSplit::Single) {
|
||||||
const instrument::engine::MeterState& loudest =
|
drawBar(m.barA, meterSingleLaneState(state));
|
||||||
state.left.levelDb >= state.right.levelDb ? state.left : state.right;
|
|
||||||
drawBar(m.barA, loudest);
|
|
||||||
} else {
|
} else {
|
||||||
drawBar(m.barA, state.left);
|
drawBar(m.barA, state.left);
|
||||||
drawBar(m.barB, state.right);
|
drawBar(m.barB, state.right);
|
||||||
@@ -102,14 +99,12 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
|
|||||||
const PlaySeconds& play = params_.play;
|
const PlaySeconds& play = params_.play;
|
||||||
const bool isMono = (voiceMode_ == VoiceMode::Mono);
|
const bool isMono = (voiceMode_ == VoiceMode::Mono);
|
||||||
const LICE_pixel hairline = toLice(roleColor(Role::LineHairline));
|
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
|
// The meter's bar count is the SAME resolved decision the waveform's lane split is —
|
||||||
// off waveformSurface rather than re-derived, so it can never become a second rule.
|
// resolveLaneSplit is the one home of it. Asked directly rather than read back off
|
||||||
const LaneSplit meterSplit =
|
// waveformSurface, whose laneCount additionally folds in the waveform BAND's pixel height,
|
||||||
waveformSurface(fl.bands.waveform, channelMode_ == ChannelMode::Stereo,
|
// which decides nothing about how many channels the bus is carrying.
|
||||||
channelPcmFor(selectedId_).channelCount)
|
const LaneSplit meterSplit = resolveLaneSplit(channelMode_ == ChannelMode::Stereo,
|
||||||
.laneCount == 2
|
channelPcmFor(selectedId_).channelCount);
|
||||||
? LaneSplit::Stereo
|
|
||||||
: LaneSplit::Single;
|
|
||||||
|
|
||||||
// One compact-toggle draw (the Mono/Stereo segment grammar at Micro scale). Disabled
|
// 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.
|
// 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.
|
// tick picks up the change after release.
|
||||||
if (!processor_) return;
|
if (!processor_) return;
|
||||||
|
|
||||||
// The meter runs on EVERY tick, the in-flight-drag case included: a drag suppresses the
|
// Ahead of the drag guard on purpose: a drag suppresses the reload poll below, but the bus
|
||||||
// reload poll below, but the bus keeps sounding and a frozen bar would misreport it.
|
// 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 unsigned long long now = GetTickCount64();
|
||||||
const double elapsed = meterTickMs_ == 0
|
const double elapsed = meterTickMs_ == 0
|
||||||
@@ -129,6 +128,15 @@ void ReaSamplerEditor::onSyncTimer() {
|
|||||||
|
|
||||||
if (drag_ != DragKind::kNone) return; // defer past the in-flight edit
|
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
|
// 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.
|
// can never be enabled on one tick and refuse on the next.
|
||||||
const bool available = bakeAvailable(processor_->bridge());
|
const bool available = bakeAvailable(processor_->bridge());
|
||||||
|
|||||||
@@ -183,11 +183,16 @@ void ReaSamplerProcessor::setLimiterEnabled(bool on) {
|
|||||||
setInstrumentParams(params);
|
setInstrumentParams(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
MasterBusMeter ReaSamplerProcessor::masterBusMeter() const {
|
MasterBusMeter ReaSamplerProcessor::masterBusMeter() {
|
||||||
MasterBusMeter m;
|
MasterBusMeter m;
|
||||||
m.peakL = meterPeakL_.load(std::memory_order_relaxed);
|
// Exchange, not load: the accumulators hold the window since this was last called, and
|
||||||
m.peakR = meterPeakR_.load(std::memory_order_relaxed);
|
// clearing them here is what starts the next window. The audio thread's own fold is a
|
||||||
m.minGain = meterMinGain_.load(std::memory_order_relaxed);
|
// 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);
|
m.clip = meterClip_.load(std::memory_order_relaxed);
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#include "core/audio/peaks.h" // Envelope (the cached peak thumbnail)
|
#include "core/audio/peaks.h" // Envelope (the cached peak thumbnail)
|
||||||
#include "core/instrument/map/sample_map.h" // SampleChoice, BankChoice, InstrumentParams
|
#include "core/instrument/map/sample_map.h" // SampleChoice, BankChoice, InstrumentParams
|
||||||
#include "core/instrument/engine/velocity_curve.h" // VelocityCurve (transfer-curve editor state)
|
#include "core/instrument/engine/velocity_curve.h" // VelocityCurve (transfer-curve editor state)
|
||||||
|
#include "shell/instrument/editor_interaction.h" // DragKind / HoverKind / HoverTarget
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
@@ -79,15 +80,6 @@ private:
|
|||||||
// picker overlaid on it.
|
// picker overlaid on it.
|
||||||
enum class View { kSample, kBrowse };
|
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
|
// 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
|
// whole state machine are the pure deck_groups module's; this alias keeps the shell's
|
||||||
// spelling.
|
// spelling.
|
||||||
@@ -115,41 +107,6 @@ private:
|
|||||||
// What those four marks are showing — see pickedMarkers.
|
// What those four marks are showing — see pickedMarkers.
|
||||||
using SetupMarkers = instrument::ui::LoopMarks;
|
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
|
// 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
|
// paint/hit-test path derives both through this one call so draw and hit-test can never
|
||||||
// disagree about where a band is.
|
// disagree about where a band is.
|
||||||
@@ -489,6 +446,11 @@ private:
|
|||||||
std::string bakeMessage_; // last outcome, shown in the title band
|
std::string bakeMessage_; // last outcome, shown in the title band
|
||||||
int bakeMessageTicks_ = 0; // sync ticks the message survives
|
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):
|
// 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
|
// 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.
|
// 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.
|
// The chain's last stage before the bus, after the gain above.
|
||||||
const float minGain = limiter_.process(ch0, ch1, frames);
|
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).
|
// Channels beyond the first two mirror ch0 (defensive — REAPER negotiates 1 or 2).
|
||||||
for (int32 ch = 2; ch < out.numChannels; ++ch) {
|
for (int32 ch = 2; ch < out.numChannels; ++ch) {
|
||||||
if (float* buf = out.channelBuffers32[ch]) {
|
if (float* buf = out.channelBuffers32[ch]) {
|
||||||
@@ -324,8 +324,9 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
|||||||
if (a0 > peakL) peakL = a0;
|
if (a0 > peakL) peakL = a0;
|
||||||
if (a1 > peakR) peakR = a1;
|
if (a1 > peakR) peakR = a1;
|
||||||
}
|
}
|
||||||
meterPeakL_.store(peakL, std::memory_order_relaxed);
|
foldPeak(meterPeakL_, peakL);
|
||||||
meterPeakR_.store(peakR, std::memory_order_relaxed);
|
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);
|
if (peakL >= 1.f || peakR >= 1.f) meterClip_.store(true, std::memory_order_relaxed);
|
||||||
} else if (ch0) {
|
} else if (ch0) {
|
||||||
// Mono: render into channel 0, replicate to any extra channels (defensive).
|
// 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);
|
const float minGain = limiter_.process(ch0, nullptr, frames);
|
||||||
meterMinGain_.store(minGain, std::memory_order_relaxed);
|
foldMinGain(meterMinGain_, minGain);
|
||||||
float peak = 0.f;
|
float peak = 0.f;
|
||||||
for (int32 i = 0; i < frames; ++i) {
|
for (int32 i = 0; i < frames; ++i) {
|
||||||
const float a = ch0[i] < 0.f ? -ch0[i] : ch0[i];
|
const float a = ch0[i] < 0.f ? -ch0[i] : ch0[i];
|
||||||
if (a > peak) peak = a;
|
if (a > peak) peak = a;
|
||||||
}
|
}
|
||||||
meterPeakL_.store(peak, std::memory_order_relaxed);
|
foldPeak(meterPeakL_, peak);
|
||||||
meterPeakR_.store(peak, std::memory_order_relaxed);
|
foldPeak(meterPeakR_, peak);
|
||||||
|
advisoryPeak_.store(peak, std::memory_order_relaxed);
|
||||||
if (peak >= 1.f) meterClip_.store(true, std::memory_order_relaxed);
|
if (peak >= 1.f) meterClip_.store(true, std::memory_order_relaxed);
|
||||||
for (int32 ch = 1; ch < out.numChannels; ++ch) {
|
for (int32 ch = 1; ch < out.numChannels; ++ch) {
|
||||||
if (float* buf = out.channelBuffers32[ch]) {
|
if (float* buf = out.channelBuffers32[ch]) {
|
||||||
|
|||||||
@@ -128,16 +128,17 @@ public:
|
|||||||
Steinberg::tresult PLUGIN_API queryInterface(const Steinberg::TUID iid,
|
Steinberg::tresult PLUGIN_API queryInterface(const Steinberg::TUID iid,
|
||||||
void** obj) override;
|
void** obj) override;
|
||||||
|
|
||||||
// The embedded-strip activity level (0..1) for the embed shell, UI thread. The loudest of
|
// The embedded-strip activity level (0..1) for the embed shell, UI thread. The last block's
|
||||||
// the two published bus peaks — one publication serves the strip and the meter.
|
// 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 {
|
double embedActivityLevel() const {
|
||||||
const float l = meterPeakL_.load(std::memory_order_relaxed);
|
return static_cast<double>(advisoryPeak_.load(std::memory_order_relaxed));
|
||||||
const float r = meterPeakR_.load(std::memory_order_relaxed);
|
|
||||||
return static_cast<double>(l > r ? l : r);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// What the audio thread published about the output bus last block. UI thread.
|
// What the audio thread published since the LAST call: peaks maxed and minGain minimised
|
||||||
MasterBusMeter masterBusMeter() const;
|
// 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();
|
void clearMasterBusClip();
|
||||||
|
|
||||||
// Resolves the selection against the instance-owned SampleRefs, decodes its WAV
|
// Resolves the selection against the instance-owned SampleRefs, decodes its WAV
|
||||||
@@ -284,13 +285,28 @@ private:
|
|||||||
// Requires reloadMutex_ held — shared by reloadInstrument and rebuildVoiceEngine.
|
// Requires reloadMutex_ held — shared by reloadInstrument and rebuildVoiceEngine.
|
||||||
void publishBuiltLocked(std::unique_ptr<LoadedInstrument> built);
|
void publishBuiltLocked(std::unique_ptr<LoadedInstrument> built);
|
||||||
|
|
||||||
// Publishes a silent block to the meter. EVERY process() path that emits no audio calls
|
// Folds one block's reading into the accumulator it belongs to — a running max for a peak,
|
||||||
// this, or the bar freezes at the last peak it saw. The clip latch is deliberately not
|
// a running min for the limiter's gain. Read-modify-write rather than a bare store, so the
|
||||||
// touched — it survives silence until the user clears it.
|
// ~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() {
|
void publishSilentMeterBlock() {
|
||||||
meterPeakL_.store(0.f, std::memory_order_relaxed);
|
advisoryPeak_.store(0.f, std::memory_order_relaxed);
|
||||||
meterPeakR_.store(0.f, std::memory_order_relaxed);
|
|
||||||
meterMinGain_.store(1.f, std::memory_order_relaxed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mirrors the persisted limiter enable onto the audio thread and the latency reader. Called
|
// Mirrors the persisted limiter enable onto the audio thread and the latency reader. Called
|
||||||
@@ -456,13 +472,21 @@ private:
|
|||||||
instrument::engine::Limiter limiter_;
|
instrument::engine::Limiter limiter_;
|
||||||
std::atomic<bool> limiterEnabled_{false};
|
std::atomic<bool> limiterEnabled_{false};
|
||||||
|
|
||||||
// What the audio thread publishes about the output bus each block, relaxed — peaks, the
|
// What the audio thread publishes about the output bus each block, relaxed. The peaks and
|
||||||
// latched clip, and the limiter's smallest gain. No dB, no ballistics, no hold timer here;
|
// 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.
|
// the UI runs those off these values and its own elapsed time.
|
||||||
std::atomic<float> meterPeakL_{0.f};
|
std::atomic<float> meterPeakL_{0.f};
|
||||||
std::atomic<float> meterPeakR_{0.f};
|
std::atomic<float> meterPeakR_{0.f};
|
||||||
std::atomic<float> meterMinGain_{1.f};
|
std::atomic<float> meterMinGain_{1.f};
|
||||||
std::atomic<bool> meterClip_{false};
|
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
|
} // namespace reasampler::vst
|
||||||
|
|||||||
+35
-25
@@ -12,6 +12,7 @@
|
|||||||
// and which selections are inert).
|
// and which selections are inert).
|
||||||
|
|
||||||
#include "../src/core/instrument/ui/deck_groups.h"
|
#include "../src/core/instrument/ui/deck_groups.h"
|
||||||
|
#include "../src/core/instrument/ui/master_meter.h" // kMeterColumnW: MASTER's reserve IS this
|
||||||
#include "../src/core/instrument/ui/sample_bands.h"
|
#include "../src/core/instrument/ui/sample_bands.h"
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -310,12 +311,15 @@ static void testTheEditorFloorIsDerivedFromTheDeckWidthBudget() {
|
|||||||
CHECK(kDeckRowBlockW + kDeckGroupGap + kDeckSpanningW + 2 * kPad == kEditorMinWidth);
|
CHECK(kDeckRowBlockW + kDeckGroupGap + kDeckSpanningW + 2 * kPad == kEditorMinWidth);
|
||||||
// The budget: what is left between the derived floor and the hard ceiling, and it is spent
|
// The budget: what is left between the derived floor and the hard ceiling, and it is spent
|
||||||
// once. A cell costs 60 of it.
|
// once. A cell costs 60 of it.
|
||||||
CHECK(kEditorCeilingWidth - kEditorMinWidth == 90);
|
CHECK(kEditorCeilingWidth - kEditorMinWidth == 82);
|
||||||
|
// 82 still buys one more deck cell (60), which is the only purchase the ledger promises —
|
||||||
|
// the widen below spent 8 px of slack, not the layout's purchasing power.
|
||||||
|
CHECK(kEditorCeilingWidth - kEditorMinWidth >= kDeckCellW);
|
||||||
// The reflow's 112 px goes entirely to the waveform, so the height does not move.
|
// The reflow's 112 px goes entirely to the waveform, so the height does not move.
|
||||||
CHECK(kEditorMinHeight == 680);
|
CHECK(kEditorMinHeight == 680);
|
||||||
// The floor did not move to make the reflow fit — the reflow was fitted to the floor. This
|
// 1190 + 8: the row block was widened 1020 -> 1028 to put the two rows' filter edges on
|
||||||
// wave spends the budget it was handed; it does not widen it.
|
// one pixel, which is the only reason the floor moved off Γ-W1-T4's number.
|
||||||
CHECK(kEditorMinWidth == 1190);
|
CHECK(kEditorMinWidth == 1198);
|
||||||
CHECK(kEditorMinWidth <= kEditorCeilingWidth);
|
CHECK(kEditorMinWidth <= kEditorCeilingWidth);
|
||||||
CHECK(kEditorMinHeight <= 720);
|
CHECK(kEditorMinHeight <= 720);
|
||||||
// And the row block really is what the two rows justify inside — derived from the floor
|
// And the row block really is what the two rows justify inside — derived from the floor
|
||||||
@@ -382,11 +386,11 @@ static void testBothRowsAndTheSpanningDeckFitTheBudget() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The gutters the justification law produces at the floor, and the alignment they buy. The
|
// The gutters the justification law produces at the floor, and the alignment they buy. The
|
||||||
// SPEC (instrument-control-surface.md §1.2/§1.3) states row 1 as 12/14/14 with both filter
|
// At the 1028 block the justification law makes the tie-line exact by arithmetic rather than
|
||||||
// edges at x = 636; equal division of 40 px over three gutters cannot produce that, so what is
|
// by a special rule: row 1's slack is 48 over three gutters (16 each, no residue) and row 2's
|
||||||
// pinned here is what the LAW produces — 14/13/13, filter edge 638 — with row 2 exact at
|
// is 152 over two (76 each), which lands both filter edges on 640. Only two of the three
|
||||||
// 72/72 and 636. The 2 px is flagged for review; a row block of 1028 (floor 1198, still under
|
// properties §1.3 once claimed can hold at once — a smallest gutter of exactly kDeckGroupGap
|
||||||
// the 1280 ceiling) is the width at which the law puts both edges on 640.
|
// needs a 1016 block — and 12 is a floor, not a target, so 16 satisfies the real rule.
|
||||||
static void testGutterArithmeticAndTheFilterTieLineAtTheFloor() {
|
static void testGutterArithmeticAndTheFilterTieLineAtTheFloor() {
|
||||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
||||||
const DeckLayout dl = layoutDeck(g, kPad, 0, kAvailAtMinWidth);
|
const DeckLayout dl = layoutDeck(g, kPad, 0, kAvailAtMinWidth);
|
||||||
@@ -394,24 +398,25 @@ static void testGutterArithmeticAndTheFilterTieLineAtTheFloor() {
|
|||||||
const auto box = [&](int id) {
|
const auto box = [&](int id) {
|
||||||
return dl.groups[static_cast<std::size_t>(indexOfGroup(g, id))].box;
|
return dl.groups[static_cast<std::size_t>(indexOfGroup(g, id))].box;
|
||||||
};
|
};
|
||||||
// Row 1: flush left, flush right on the block, gutters 14/13/13.
|
// Row 1: flush left, flush right on the block, and three EQUAL gutters — 48 divides by 3
|
||||||
|
// with no residue, so no gutter carries a leftover pixel.
|
||||||
CHECK(box(kGroupPitch).x == kPad);
|
CHECK(box(kGroupPitch).x == kPad);
|
||||||
CHECK(box(kGroupFilter).x - box(kGroupPitch).right() == 14);
|
CHECK(box(kGroupFilter).x - box(kGroupPitch).right() == 16);
|
||||||
CHECK(box(kGroupVelocity).x - box(kGroupFilter).right() == 13);
|
CHECK(box(kGroupVelocity).x - box(kGroupFilter).right() == 16);
|
||||||
CHECK(box(kGroupVoice).x - box(kGroupVelocity).right() == 13);
|
CHECK(box(kGroupVoice).x - box(kGroupVelocity).right() == 16);
|
||||||
CHECK(box(kGroupVoice).right() == kPad + kDeckRowBlockW);
|
CHECK(box(kGroupVoice).right() == kPad + kDeckRowBlockW);
|
||||||
|
|
||||||
// Row 2: flush left, flush right, and its two gutters exactly equal — the property the
|
// Row 2: flush left, flush right, two gutters exactly equal.
|
||||||
// 1020 block was chosen for, and the one it does deliver.
|
|
||||||
CHECK(box(kGroupPitchEnv).x == kPad);
|
CHECK(box(kGroupPitchEnv).x == kPad);
|
||||||
CHECK(box(kGroupFilterEnv).x - box(kGroupPitchEnv).right() == 72);
|
CHECK(box(kGroupFilterEnv).x - box(kGroupPitchEnv).right() == 76);
|
||||||
CHECK(box(kGroupAmpEnv).x - box(kGroupFilterEnv).right() == 72);
|
CHECK(box(kGroupAmpEnv).x - box(kGroupFilterEnv).right() == 76);
|
||||||
CHECK(box(kGroupAmpEnv).right() == kPad + kDeckRowBlockW);
|
CHECK(box(kGroupAmpEnv).right() == kPad + kDeckRowBlockW);
|
||||||
|
|
||||||
// The filter tie-line, block-relative. Row 2 lands on the specified 636; row 1 lands 2 px
|
// The tie-line, block-relative: both filter edges on ONE pixel, which is what the widen
|
||||||
// past it. See this test's header.
|
// bought. Pinned as an identity too, so a group-width change cannot pass by moving both.
|
||||||
CHECK(box(kGroupFilterEnv).right() - kPad == 636);
|
CHECK(box(kGroupFilterEnv).right() - kPad == 640);
|
||||||
CHECK(box(kGroupFilter).right() - kPad == 638);
|
CHECK(box(kGroupFilter).right() - kPad == 640);
|
||||||
|
CHECK(box(kGroupFilter).right() == box(kGroupFilterEnv).right());
|
||||||
|
|
||||||
// MASTER is right-anchored outside the block, one kDeckGroupGap clear of it.
|
// MASTER is right-anchored outside the block, one kDeckGroupGap clear of it.
|
||||||
CHECK(box(kGroupMaster).x - box(kGroupVoice).right() == kDeckGroupGap);
|
CHECK(box(kGroupMaster).x - box(kGroupVoice).right() == kDeckGroupGap);
|
||||||
@@ -450,8 +455,8 @@ static void testGuttersHoldTheirMinimumAndTheTieLineDriftsAboveTheFloor() {
|
|||||||
CHECK(drift <= lastDrift);
|
CHECK(drift <= lastDrift);
|
||||||
lastDrift = drift;
|
lastDrift = drift;
|
||||||
}
|
}
|
||||||
// It really does open up, and by far more than the 2 px it starts at — separation
|
// It really does open up: the tie-line is exact AT the floor and separates above it,
|
||||||
// above the floor is the accepted outcome, not a near-miss to be pinned back.
|
// which is the accepted outcome rather than a near-miss to be pinned back.
|
||||||
CHECK(lastDrift < -50);
|
CHECK(lastDrift < -50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -467,8 +472,13 @@ static void testTheMasterDeckInteriorLandsOnBothRowBaselines() {
|
|||||||
|
|
||||||
CHECK(m.box.width == 142);
|
CHECK(m.box.width == 142);
|
||||||
CHECK(m.box.height == 216);
|
CHECK(m.box.height == 216);
|
||||||
// 6 + 60 + 8 + 62 + 6 — the decomposition, not just the total.
|
// 6 + 60 + 8 + 62 + 6 — the decomposition, not just the total, and the 62 is the meter
|
||||||
CHECK(kDeckGroupPadX + kDeckCellW + kDeckColumnGap + 62 + kDeckGroupPadX == 142);
|
// module's own kMeterColumnW rather than a copy of it. That link is the whole point: the
|
||||||
|
// column is banked to GROW (§1.2), and a reserve that did not track it would leave the
|
||||||
|
// interior underfilling or overrunning with every test still green.
|
||||||
|
CHECK(kDeckGroupPadX + kDeckCellW + kDeckColumnGap + kMeterColumnW + kDeckGroupPadX == 142);
|
||||||
|
CHECK(m.column.id == cell(DeckParam::kMasterMeter));
|
||||||
|
CHECK(m.column.box.width == kMeterColumnW);
|
||||||
|
|
||||||
// One cell drawn (gain) and one slot RESERVED below it: the reserve is height at a fixed
|
// One cell drawn (gain) and one slot RESERVED below it: the reserve is height at a fixed
|
||||||
// position and draws nothing.
|
// position and draws nothing.
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ static void testGutterAtTheSawtoothMaximumResidueWidth() {
|
|||||||
// nor the floor is covered by another test firing if either ever changes. Derived from
|
// nor the floor is covered by another test firing if either ever changes. Derived from
|
||||||
// kEditorMinWidth/kEditorMinHeight (editor_session.cpp's ViewRect default IS the floor) rather
|
// kEditorMinWidth/kEditorMinHeight (editor_session.cpp's ViewRect default IS the floor) rather
|
||||||
// than a hardcoded window size, so a floor change fails HERE instead of silently moving the
|
// than a hardcoded window size, so a floor change fails HERE instead of silently moving the
|
||||||
// shipped gutter out from under it. The numbers below are today's floor (1190x680); re-derive
|
// shipped gutter out from under it. The numbers below are today's floor (1198x680); re-derive
|
||||||
// them by hand if the floor ever moves.
|
// them by hand if the floor ever moves.
|
||||||
static void testGutterAtTheShippedDefaultWindowSize() {
|
static void testGutterAtTheShippedDefaultWindowSize() {
|
||||||
// Derive rootStrip's width the same way the shell does, through the real allocator +
|
// Derive rootStrip's width the same way the shell does, through the real allocator +
|
||||||
@@ -315,14 +315,14 @@ static void testGutterAtTheShippedDefaultWindowSize() {
|
|||||||
const ChromeRects chrome = chromeRects(bands.chrome, /*knobSize=*/24);
|
const ChromeRects chrome = chromeRects(bands.chrome, /*knobSize=*/24);
|
||||||
const int stripW = chrome.rootStrip.width;
|
const int stripW = chrome.rootStrip.width;
|
||||||
CHECK(stripW == kEditorMinWidth - 2 * kPad);
|
CHECK(stripW == kEditorMinWidth - 2 * kPad);
|
||||||
CHECK(stripW == 1174);
|
CHECK(stripW == 1182);
|
||||||
|
|
||||||
const StripLayout L = layoutStrip(stripW, 30);
|
const StripLayout L = layoutStrip(stripW, 30);
|
||||||
CHECK(L.whiteWidth == 15);
|
CHECK(L.whiteWidth == 15);
|
||||||
const int margins = L.band.width - L.keys.width;
|
const int margins = L.band.width - L.keys.width;
|
||||||
CHECK(margins == 49);
|
CHECK(margins == 57);
|
||||||
const int leftMargin = L.keys.x - L.band.x;
|
const int leftMargin = L.keys.x - L.band.x;
|
||||||
CHECK(leftMargin == 24);
|
CHECK(leftMargin == 28);
|
||||||
}
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
|
|||||||
+124
-22
@@ -1,14 +1,15 @@
|
|||||||
// Standalone tests for reasampler::instrument::ui::master_meter — no VST3, no REAPER, no
|
// Standalone tests for reasampler::instrument::ui::master_meter — no VST3, no REAPER, no
|
||||||
// framework. Assert:
|
// framework. Assert:
|
||||||
//
|
//
|
||||||
// * column interior — the 22/4/36 decomposition, the mono bar taking the whole field, the
|
// * column interior — the 22/4/36 decomposition, the exported column width the deck reserves,
|
||||||
// two stereo bars at 17 px and kMeterBarGap apart, all inside the column.
|
// the mono bar taking the whole field, the two stereo bars, all inside the column.
|
||||||
// * bar count — the SAME LaneSplit waveformSurface folds, over channel mode x source
|
// * bar count — the SAME LaneSplit resolveLaneSplit folds, over channel mode x source
|
||||||
// channel count, so it can never become a second rule.
|
// channel count, so it can never become a second rule.
|
||||||
// * the dB axis — top/floor land on the field's edges, it is monotone, and it clamps.
|
// * the dB axis — top/floor on the field's edges, an interior value, and the clamps.
|
||||||
// * ballistics — instantaneous rise, 20 dB/s fall, the 1.5 s hold and its release; the
|
// * ballistics — instantaneous rise, 20 dB/s fall, the 1.5 s hold and its release AT RATE;
|
||||||
// audio thread's clip latch surviving a UI frame that never sampled the loud block.
|
// the audio thread's clip latch surviving a UI frame; the per-field single-lane fold.
|
||||||
// * the GR lamp — lit only while the limiter actually reduces, and decaying afterwards.
|
// * the GR lamp — lit only while the limiter reduces, held, and surviving the 500 ms tick the
|
||||||
|
// editor actually runs it at.
|
||||||
|
|
||||||
#include "../src/core/instrument/ui/master_meter.h"
|
#include "../src/core/instrument/ui/master_meter.h"
|
||||||
#include "../src/core/instrument/ui/waveform_view.h"
|
#include "../src/core/instrument/ui/waveform_view.h"
|
||||||
@@ -37,8 +38,11 @@ static void testColumnDividesIntoGutterAndBarField() {
|
|||||||
CHECK(m.field.x == m.labels.right() + kMeterLabelGap);
|
CHECK(m.field.x == m.labels.right() + kMeterLabelGap);
|
||||||
CHECK(m.field.width == kMeterFieldW);
|
CHECK(m.field.width == kMeterFieldW);
|
||||||
// The three parts account for the column exactly — a residue would leave dead pixels the
|
// The three parts account for the column exactly — a residue would leave dead pixels the
|
||||||
// scale's numerals would then be centred against.
|
// scale's numerals would then be centred against. Asserted against the EXPORTED width the
|
||||||
CHECK(kMeterLabelW + kMeterLabelGap + kMeterFieldW == kColumn.width);
|
// deck reserves, not against this fixture's literal rect: the deck reading the same
|
||||||
|
// constant is what keeps the reserve and the interior from drifting apart.
|
||||||
|
CHECK(kMeterLabelW + kMeterLabelGap + kMeterFieldW == kMeterColumnW);
|
||||||
|
CHECK(kMeterColumnW == kColumn.width);
|
||||||
CHECK(m.field.right() == kColumn.right());
|
CHECK(m.field.right() == kColumn.right());
|
||||||
// Full height in both rects: the column spans both row baselines as ONE readout.
|
// Full height in both rects: the column spans both row baselines as ONE readout.
|
||||||
CHECK(m.labels.y == kColumn.y && m.labels.bottom() == kColumn.bottom());
|
CHECK(m.labels.y == kColumn.y && m.labels.bottom() == kColumn.bottom());
|
||||||
@@ -62,18 +66,19 @@ static void testMonoDrawsOneWideBarAndStereoDrawsTwo() {
|
|||||||
CHECK(st.barA.y == st.field.y && st.barB.bottom() == st.field.bottom());
|
CHECK(st.barA.y == st.field.y && st.barB.bottom() == st.field.bottom());
|
||||||
}
|
}
|
||||||
|
|
||||||
// The bar count is NOT a second rule: it is whatever waveformSurface resolved for the same
|
// The bar count is NOT a second rule: it is resolveLaneSplit's answer for the same (mode,
|
||||||
// (mode, source) pair. A mono source under stereo mode is dual-mono — one source, two views.
|
// source) pair the waveform asks about. A mono source under stereo mode is dual-mono — one
|
||||||
|
// source, two views.
|
||||||
static void testBarCountFollowsTheWaveformsOwnLaneSplit() {
|
static void testBarCountFollowsTheWaveformsOwnLaneSplit() {
|
||||||
const Rect band = Rect::ltrb(8, 100, 1182, 458);
|
const Rect band = Rect::ltrb(8, 100, 1182, 458);
|
||||||
for (bool stereoMode : {false, true}) {
|
for (bool stereoMode : {false, true}) {
|
||||||
for (int sourceChannels : {1, 2}) {
|
for (int sourceChannels : {1, 2}) {
|
||||||
const WaveformSurface s = waveformSurface(band, stereoMode, sourceChannels);
|
const LaneSplit split = resolveLaneSplit(stereoMode, sourceChannels);
|
||||||
const LaneSplit split =
|
|
||||||
s.laneCount == 2 ? LaneSplit::Stereo : LaneSplit::Single;
|
|
||||||
const MeterRects m = meterRects(kColumn, split);
|
const MeterRects m = meterRects(kColumn, split);
|
||||||
const int bars = m.barB.empty() ? 1 : 2;
|
const int bars = m.barB.empty() ? 1 : 2;
|
||||||
CHECK(bars == s.laneCount);
|
// The waveform's own surface folds the SAME call, so on a band tall enough to
|
||||||
|
// divide the two answers agree by construction rather than by coincidence.
|
||||||
|
CHECK(bars == waveformSurface(band, stereoMode, sourceChannels).laneCount);
|
||||||
// Spelled out per combination so a regression names which one broke.
|
// Spelled out per combination so a regression names which one broke.
|
||||||
const bool expectTwo = stereoMode && sourceChannels >= 2;
|
const bool expectTwo = stereoMode && sourceChannels >= 2;
|
||||||
CHECK(bars == (expectTwo ? 2 : 1));
|
CHECK(bars == (expectTwo ? 2 : 1));
|
||||||
@@ -95,6 +100,49 @@ static void testDbAxisSpansTheFieldAndClamps() {
|
|||||||
// Clamped outside the scale rather than drawn off the field.
|
// Clamped outside the scale rather than drawn off the field.
|
||||||
CHECK(meterDbToY(m.field, kMeterTopDb + 40.0) == m.field.y);
|
CHECK(meterDbToY(m.field, kMeterTopDb + 40.0) == m.field.y);
|
||||||
CHECK(meterDbToY(m.field, kMeterFloorDb - 40.0) == m.field.bottom());
|
CHECK(meterDbToY(m.field, kMeterFloorDb - 40.0) == m.field.bottom());
|
||||||
|
|
||||||
|
// One INTERIOR point, because endpoints plus monotonicity are satisfied by any log or
|
||||||
|
// piecewise map through them, and the scale is specified LINEAR in dB. −27 is the
|
||||||
|
// midpoint of −60…+6, so it must land on the field's own midpoint: 186 x 0.5 = 93.
|
||||||
|
CHECK(meterDbToY(m.field, -27.0) == m.field.bottom() - 93);
|
||||||
|
// And a quarter of the way up, which fixes the slope rather than just the centre.
|
||||||
|
CHECK(meterDbToY(m.field, -43.5) == m.field.bottom() - 47); // round(0.25 x 186) = 47
|
||||||
|
}
|
||||||
|
|
||||||
|
// The numeral SET is spec-pinned (0, −12, −24, −36, −48, −60) as a property of the scale, so it
|
||||||
|
// is asserted here rather than left as a modulo inside the painter.
|
||||||
|
static void testEveryOtherTickCarriesANumeral() {
|
||||||
|
const int expected[] = {6, -6, -18, -30, -42, -54};
|
||||||
|
for (int db : expected) CHECK(!meterTickNumeralled(db));
|
||||||
|
const int numeralled[] = {0, -12, -24, -36, -48, -60};
|
||||||
|
for (int db : numeralled) CHECK(meterTickNumeralled(db));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The floor tick sits ON the field's bottom edge, so an unclamped y±5 numeral box hangs below
|
||||||
|
// the column and into the deck's bottom padding.
|
||||||
|
static void testTheFloorNumeralStaysInsideTheGutter() {
|
||||||
|
const MeterRects m = meterRects(kColumn, LaneSplit::Single);
|
||||||
|
const Rect floorLabel = meterNumeralRect(m.labels, meterDbToY(m.field, kMeterFloorDb));
|
||||||
|
CHECK(floorLabel.bottom() <= m.labels.bottom());
|
||||||
|
CHECK(floorLabel.y >= m.labels.y);
|
||||||
|
CHECK(floorLabel.height == 10); // clamped, not squashed — the numeral still has its band
|
||||||
|
const Rect topLabel = meterNumeralRect(m.labels, meterDbToY(m.field, kMeterTopDb));
|
||||||
|
CHECK(topLabel.y >= m.labels.y);
|
||||||
|
CHECK(topLabel.height == 10);
|
||||||
|
// An interior tick is centred on its rule, which is the case the clamp must not disturb.
|
||||||
|
const int midY = meterDbToY(m.field, -24.0);
|
||||||
|
CHECK(meterNumeralRect(m.labels, midY).y == midY - 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A column narrower than the interior needs yields NOTHING rather than a field overrunning it.
|
||||||
|
// Reachable only if the deck's reserve and this module's interior ever disagree — which is
|
||||||
|
// exactly what kMeterColumnW exists to prevent.
|
||||||
|
static void testAColumnTooNarrowForTheInteriorDrawsNothing() {
|
||||||
|
const Rect narrow = Rect::ltrb(0, 0, kMeterColumnW - 1, 186);
|
||||||
|
const MeterRects m = meterRects(narrow, LaneSplit::Stereo);
|
||||||
|
CHECK(m.field.empty() && m.barA.empty() && m.barB.empty());
|
||||||
|
// Exactly the needed width still lays out.
|
||||||
|
CHECK(!meterRects(Rect::ltrb(0, 0, kMeterColumnW, 186), LaneSplit::Stereo).field.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
static void testPeakRisesAtOnceAndFallsAtTwentyDbPerSecond() {
|
static void testPeakRisesAtOnceAndFallsAtTwentyDbPerSecond() {
|
||||||
@@ -120,9 +168,12 @@ static void testPeakHoldSitsForItsFullWindowThenReleases() {
|
|||||||
CHECK(s.left.levelDb < held - 20.0);
|
CHECK(s.left.levelDb < held - 20.0);
|
||||||
CHECK(std::fabs(s.left.holdDb - held) < 1e-9);
|
CHECK(std::fabs(s.left.holdDb - held) < 1e-9);
|
||||||
|
|
||||||
// Past it, the tick releases at the same 20 dB/s the bar uses.
|
// Past it, the tick releases at the SAME 20 dB/s the bar uses — pinned by value, not as an
|
||||||
|
// inequality: a slower release would satisfy "it fell" and still be the wrong meter. The
|
||||||
|
// frame spends the 0.01 s of hold it had left and releases for the remaining 0.49 s, which
|
||||||
|
// is also what proves the release does not quantize to whole UI frames.
|
||||||
s = advanceMasterMeter(s, {0.0, 0.0, 1.0, false}, 0.5);
|
s = advanceMasterMeter(s, {0.0, 0.0, 1.0, false}, 0.5);
|
||||||
CHECK(s.left.holdDb < held);
|
CHECK(std::fabs(s.left.holdDb - (held - kMeterFallDbPerSecond * 0.49)) < 1e-9);
|
||||||
CHECK(s.left.holdDb >= s.left.levelDb);
|
CHECK(s.left.holdDb >= s.left.levelDb);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,21 +202,67 @@ static void testGrLampLitOnlyWhileTheLimiterReduces() {
|
|||||||
CHECK(s.reductionDb == 0.0);
|
CHECK(s.reductionDb == 0.0);
|
||||||
CHECK(!grLampLit(s));
|
CHECK(!grLampLit(s));
|
||||||
|
|
||||||
// ~6 dB of reduction lights it.
|
// ~6 dB of reduction lights it, and arms the hold.
|
||||||
s = advanceMasterMeter(s, {0.5, 0.5, 0.5, false}, 0.1);
|
s = advanceMasterMeter(s, {0.5, 0.5, 0.5, false}, 0.1);
|
||||||
CHECK(std::fabs(s.reductionDb - 6.0206) < 1e-3);
|
CHECK(std::fabs(s.reductionDb - 6.0206) < 1e-3);
|
||||||
CHECK(grLampLit(s));
|
CHECK(grLampLit(s));
|
||||||
|
CHECK(s.reductionHoldSeconds == kMeterPeakHoldSeconds);
|
||||||
|
|
||||||
// It decays at the meter's own rate rather than snapping dark, so a transient catch is
|
// Held flat, not decaying, for its whole window — the peak tick's own contract.
|
||||||
// visible for more than the single frame it happened on.
|
s = advanceMasterMeter(s, {0.5, 0.5, 1.0, false}, kMeterPeakHoldSeconds - 0.01);
|
||||||
s = advanceMasterMeter(s, {0.5, 0.5, 1.0, false}, 0.1);
|
CHECK(std::fabs(s.reductionDb - 6.0206) < 1e-3);
|
||||||
CHECK(grLampLit(s));
|
CHECK(grLampLit(s));
|
||||||
CHECK(s.reductionDb < 6.0206);
|
|
||||||
|
// Past the window it releases at the meter's 20 dB/s, and 6 dB of catch is gone inside a
|
||||||
|
// third of a second of release.
|
||||||
s = advanceMasterMeter(s, {0.5, 0.5, 1.0, false}, 1.0);
|
s = advanceMasterMeter(s, {0.5, 0.5, 1.0, false}, 1.0);
|
||||||
CHECK(!grLampLit(s));
|
CHECK(!grLampLit(s));
|
||||||
CHECK(s.reductionDb == 0.0);
|
CHECK(s.reductionDb == 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The cadence the lamp ACTUALLY runs at is editor_platform's 500 ms sync tick, and the whole
|
||||||
|
// point of the hold is that the lamp survives it. Without one, a 6 dB catch decays 20 x 0.5 =
|
||||||
|
// 10 dB on the very next frame and clamps to 0 — lit for exactly one repaint. Pinned in frames,
|
||||||
|
// because "how many times does this draw lit" is arithmetic, not a look.
|
||||||
|
static void testGrLampSurvivesTheFiveHundredMillisecondTick() {
|
||||||
|
constexpr double kTick = 0.5; // editor_platform.cpp's kSyncTimerIntervalMs
|
||||||
|
MasterMeterUi s = advanceMasterMeter(MasterMeterUi{}, {0.5, 0.5, 0.5, false}, kTick);
|
||||||
|
CHECK(grLampLit(s));
|
||||||
|
|
||||||
|
int litFrames = 1;
|
||||||
|
for (int i = 0; i < 20 && grLampLit(s); ++i) {
|
||||||
|
s = advanceMasterMeter(s, {0.5, 0.5, 1.0, false}, kTick);
|
||||||
|
if (grLampLit(s)) ++litFrames;
|
||||||
|
}
|
||||||
|
// 1.5 s of hold spans the tick that armed it plus three more, and the release then takes
|
||||||
|
// 6.02 dB below the 0.5 dB floor within one further 10 dB step.
|
||||||
|
CHECK(litFrames == 4);
|
||||||
|
CHECK(!grLampLit(s));
|
||||||
|
|
||||||
|
// A catch the previous frame does not shorten: a SECOND catch re-arms the full window.
|
||||||
|
MasterMeterUi t = advanceMasterMeter(MasterMeterUi{}, {0.5, 0.5, 0.5, false}, kTick);
|
||||||
|
t = advanceMasterMeter(t, {0.5, 0.5, 1.0, false}, kTick);
|
||||||
|
t = advanceMasterMeter(t, {0.5, 0.5, 0.5, false}, kTick);
|
||||||
|
CHECK(t.reductionHoldSeconds == kMeterPeakHoldSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The one bar a single-lane column draws folds the two channels per FIELD. Picking whichever
|
||||||
|
// channel won on level would draw the OTHER channel's hold tick and clip nowhere.
|
||||||
|
static void testSingleLaneStateFoldsBothChannelsPerField() {
|
||||||
|
MasterMeterUi m;
|
||||||
|
m.left.levelDb = -30.0;
|
||||||
|
m.left.holdDb = -2.0; // left is quieter now but held the loudest peak
|
||||||
|
m.right.levelDb = -10.0;
|
||||||
|
m.right.holdDb = -8.0;
|
||||||
|
m.left.clip = true; // and only left ever clipped
|
||||||
|
m.right.clip = false;
|
||||||
|
|
||||||
|
const instrument::engine::MeterState s = meterSingleLaneState(m);
|
||||||
|
CHECK(s.levelDb == -10.0); // the louder channel's bar
|
||||||
|
CHECK(s.holdDb == -2.0); // but the higher hold tick, which is the other channel's
|
||||||
|
CHECK(s.clip); // and the clip, which a level pick would have dropped
|
||||||
|
}
|
||||||
|
|
||||||
// The tick repaints only on a change, so what counts as a change has to cover every drawn
|
// The tick repaints only on a change, so what counts as a change has to cover every drawn
|
||||||
// quantity — and only those.
|
// quantity — and only those.
|
||||||
static void testDrawEqualityCoversTheDrawnQuantities() {
|
static void testDrawEqualityCoversTheDrawnQuantities() {
|
||||||
@@ -200,10 +297,15 @@ int main() {
|
|||||||
testMonoDrawsOneWideBarAndStereoDrawsTwo();
|
testMonoDrawsOneWideBarAndStereoDrawsTwo();
|
||||||
testBarCountFollowsTheWaveformsOwnLaneSplit();
|
testBarCountFollowsTheWaveformsOwnLaneSplit();
|
||||||
testDbAxisSpansTheFieldAndClamps();
|
testDbAxisSpansTheFieldAndClamps();
|
||||||
|
testEveryOtherTickCarriesANumeral();
|
||||||
|
testTheFloorNumeralStaysInsideTheGutter();
|
||||||
|
testAColumnTooNarrowForTheInteriorDrawsNothing();
|
||||||
testPeakRisesAtOnceAndFallsAtTwentyDbPerSecond();
|
testPeakRisesAtOnceAndFallsAtTwentyDbPerSecond();
|
||||||
testPeakHoldSitsForItsFullWindowThenReleases();
|
testPeakHoldSitsForItsFullWindowThenReleases();
|
||||||
testClipLatchesFromThePublishedFlagAndClearsOnDemand();
|
testClipLatchesFromThePublishedFlagAndClearsOnDemand();
|
||||||
|
testSingleLaneStateFoldsBothChannelsPerField();
|
||||||
testGrLampLitOnlyWhileTheLimiterReduces();
|
testGrLampLitOnlyWhileTheLimiterReduces();
|
||||||
|
testGrLampSurvivesTheFiveHundredMillisecondTick();
|
||||||
testDrawEqualityCoversTheDrawnQuantities();
|
testDrawEqualityCoversTheDrawnQuantities();
|
||||||
testDegenerateColumnYieldsNothing();
|
testDegenerateColumnYieldsNothing();
|
||||||
if (g_fail) {
|
if (g_fail) {
|
||||||
|
|||||||
Reference in New Issue
Block a user