Merge pS-fb1-sampleview: FB1 (r11) Sample-view recompose — knob deck, full-width hero, curve popup, post-mixer master gain (v8)
This commit is contained in:
+46
-2
@@ -754,7 +754,9 @@ target_link_libraries(embed_strip PUBLIC editor_geometry)
|
|||||||
# calls these; the process callback stays allocation-free.
|
# calls these; the process callback stays allocation-free.
|
||||||
add_library(sample_map STATIC src/vst/sample_map.cpp)
|
add_library(sample_map STATIC src/vst/sample_map.cpp)
|
||||||
target_include_directories(sample_map PUBLIC src/vst src)
|
target_include_directories(sample_map PUBLIC src/vst src)
|
||||||
target_link_libraries(sample_map PUBLIC bank_book wav_trim sampler_core)
|
# master_gain: the v8 component-state master-gain field validates against the pure taper's
|
||||||
|
# linear cap at the (de)serialization boundary (one cap, shared with the knob + the processor).
|
||||||
|
target_link_libraries(sample_map PUBLIC bank_book wav_trim sampler_core master_gain)
|
||||||
|
|
||||||
# capture_browser (Phase S10) — PURE card-grid + bank-filter-tab layout + hit-test for the
|
# capture_browser (Phase S10) — PURE card-grid + bank-filter-tab layout + hit-test for the
|
||||||
# capture-first editor's default face. The mirror of mode_switch/editor_geometry: the fiddly
|
# capture-first editor's default face. The mirror of mode_switch/editor_geometry: the fiddly
|
||||||
@@ -843,6 +845,29 @@ add_library(envelope_edit STATIC src/vst/envelope_edit.cpp)
|
|||||||
target_include_directories(envelope_edit PUBLIC src/vst)
|
target_include_directories(envelope_edit PUBLIC src/vst)
|
||||||
target_link_libraries(envelope_edit PUBLIC envelope_overlay)
|
target_link_libraries(envelope_edit PUBLIC envelope_overlay)
|
||||||
|
|
||||||
|
# knob_deck (Wave B FB1, r11) — PURE knob-deck layout + hit-test for the recomposed Sample face:
|
||||||
|
# fenced groups (caption row + compact caption toggles + fixed 48x58 knob cells + optional row
|
||||||
|
# toggle), deterministic whole-group wrap, point -> control-id hit-test. The mirror of
|
||||||
|
# action_bar / param_slider; links editor_geometry for the shared Rect. Engine-free — cells and
|
||||||
|
# toggles carry opaque shell-owned control ids. NEITHER SDK.
|
||||||
|
add_library(knob_deck STATIC src/vst/knob_deck.cpp)
|
||||||
|
target_include_directories(knob_deck PUBLIC src/vst)
|
||||||
|
target_link_libraries(knob_deck PUBLIC editor_geometry)
|
||||||
|
|
||||||
|
# curve_popup (Wave B FB1, r11) — PURE centered-sheet geometry for the velocity-curve popup
|
||||||
|
# editor: size clamps (60%/55% of window, 360..520 x 260..380), title row + close button, the
|
||||||
|
# curve-box border rect, and the outside-sheet dismissal test. The mirror of overflow_menu;
|
||||||
|
# links editor_geometry for the shared Rect. NEITHER SDK.
|
||||||
|
add_library(curve_popup STATIC src/vst/curve_popup.cpp)
|
||||||
|
target_include_directories(curve_popup PUBLIC src/vst)
|
||||||
|
target_link_libraries(curve_popup PUBLIC editor_geometry)
|
||||||
|
|
||||||
|
# master_gain (Wave B FB1) — PURE dB<->linear<->knob-taper math for the post-mixer master gain
|
||||||
|
# (-inf..+24 dB; norm 0 = TRUE zero linear). One formula shared by the editor's Gain knob, the
|
||||||
|
# v8 component-state wire cap, and the processor's applied gain. Standard library only. NEITHER SDK.
|
||||||
|
add_library(master_gain STATIC src/vst/master_gain.cpp)
|
||||||
|
target_include_directories(master_gain PUBLIC src/vst)
|
||||||
|
|
||||||
add_executable(editor_geometry_tests tests/test_editor_geometry.cpp)
|
add_executable(editor_geometry_tests tests/test_editor_geometry.cpp)
|
||||||
target_link_libraries(editor_geometry_tests PRIVATE editor_geometry)
|
target_link_libraries(editor_geometry_tests PRIVATE editor_geometry)
|
||||||
add_test(NAME editor_geometry_tests COMMAND editor_geometry_tests)
|
add_test(NAME editor_geometry_tests COMMAND editor_geometry_tests)
|
||||||
@@ -917,6 +942,21 @@ add_executable(envelope_edit_tests tests/test_envelope_edit.cpp)
|
|||||||
target_link_libraries(envelope_edit_tests PRIVATE envelope_edit)
|
target_link_libraries(envelope_edit_tests PRIVATE envelope_edit)
|
||||||
add_test(NAME envelope_edit_tests COMMAND envelope_edit_tests)
|
add_test(NAME envelope_edit_tests COMMAND envelope_edit_tests)
|
||||||
|
|
||||||
|
# knob_deck (Wave B FB1): the pure r11 deck layout/wrap/hit-test. Links ONLY knob_deck — NEITHER SDK.
|
||||||
|
add_executable(knob_deck_tests tests/test_knob_deck.cpp)
|
||||||
|
target_link_libraries(knob_deck_tests PRIVATE knob_deck)
|
||||||
|
add_test(NAME knob_deck_tests COMMAND knob_deck_tests)
|
||||||
|
|
||||||
|
# curve_popup (Wave B FB1): the pure r11 popup-sheet geometry at the size clamps. NEITHER SDK.
|
||||||
|
add_executable(curve_popup_tests tests/test_curve_popup.cpp)
|
||||||
|
target_link_libraries(curve_popup_tests PRIVATE curve_popup)
|
||||||
|
add_test(NAME curve_popup_tests COMMAND curve_popup_tests)
|
||||||
|
|
||||||
|
# master_gain (Wave B FB1): the pure dB<->linear<->knob taper. NEITHER SDK.
|
||||||
|
add_executable(master_gain_tests tests/test_master_gain.cpp)
|
||||||
|
target_link_libraries(master_gain_tests PRIVATE master_gain)
|
||||||
|
add_test(NAME master_gain_tests COMMAND master_gain_tests)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 4) The REAPER extension — a loadable module (dlopen'd by REAPER, not linked).
|
# 4) The REAPER extension — a loadable module (dlopen'd by REAPER, not linked).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1123,10 +1163,14 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
|
|||||||
# and the node hit-test + pixel-delta -> clamped-param inverse map the Sample-view envelope
|
# and the node hit-test + pixel-delta -> clamped-param inverse map the Sample-view envelope
|
||||||
# overlay draws + drags against; envelope_edit links envelope_overlay transitively (shared
|
# overlay draws + drags against; envelope_edit links envelope_overlay transitively (shared
|
||||||
# node vocabulary + timeToX/levelToY). Both engine-free, DAW-verified in the shell.
|
# node vocabulary + timeToX/levelToY). Both engine-free, DAW-verified in the shell.
|
||||||
|
# knob_deck + curve_popup + master_gain (Wave B FB1, r11): the pure deck layout/hit-test,
|
||||||
|
# the curve-popup sheet geometry, and the master-gain taper the recomposed Sample face
|
||||||
|
# draws + routes against (master_gain also rides in via sample_map for the v8 wire cap).
|
||||||
target_link_libraries(reasampler_vst PRIVATE vst3_sdk editor_geometry bridge_marshal
|
target_link_libraries(reasampler_vst PRIVATE vst3_sdk editor_geometry bridge_marshal
|
||||||
sample_map capture_paths embed_strip app_version capture_browser keyboard_strip
|
sample_map capture_paths embed_strip app_version capture_browser keyboard_strip
|
||||||
waveform_view bank_sync browser_scroll note_entry param_slider
|
waveform_view bank_sync browser_scroll note_entry param_slider
|
||||||
theme component_geometry bank_grid trigger_seam envelope_overlay envelope_edit)
|
theme component_geometry bank_grid trigger_seam envelope_overlay envelope_edit
|
||||||
|
knob_deck curve_popup master_gain)
|
||||||
# SDK_INC gives reaper_vst3_interfaces.h + reaper_plugin_functions.h for the bridge;
|
# SDK_INC gives reaper_vst3_interfaces.h + reaper_plugin_functions.h for the bridge;
|
||||||
# WDL_INC gives LICE for the editor. The VST3 SDK headers come from vst3_sdk PUBLIC.
|
# WDL_INC gives LICE for the editor. The VST3 SDK headers come from vst3_sdk PUBLIC.
|
||||||
target_include_directories(reasampler_vst PRIVATE ${SDK_INC} ${WDL_INC})
|
target_include_directories(reasampler_vst PRIVATE ${SDK_INC} ${WDL_INC})
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// curve_popup.cpp — see curve_popup.h. Pure arithmetic; no LICE/VST3/REAPER includes.
|
||||||
|
|
||||||
|
#include "curve_popup.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace reasampler::vst {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
int clampDim(int want, int lo, int hi, int windowDim) {
|
||||||
|
const int clamped = (std::max)(lo, (std::min)(hi, want));
|
||||||
|
return (std::min)(clamped, (std::max)(0, windowDim));
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
CurvePopupLayout computeCurvePopup(int w, int h) {
|
||||||
|
CurvePopupLayout out;
|
||||||
|
const int sheetW = clampDim((w * 60) / 100, kCurvePopupMinW, kCurvePopupMaxW, w);
|
||||||
|
const int sheetH = clampDim((h * 55) / 100, kCurvePopupMinH, kCurvePopupMaxH, h);
|
||||||
|
const int left = (w - sheetW) / 2;
|
||||||
|
const int top = (h - sheetH) / 2;
|
||||||
|
out.sheet = Rect{left, top, left + sheetW, top + sheetH};
|
||||||
|
|
||||||
|
const int titleBottom = out.sheet.top + kCurvePopupTitleH;
|
||||||
|
const int closeTop = out.sheet.top + (kCurvePopupTitleH - kCurvePopupCloseSize) / 2;
|
||||||
|
out.close = Rect{out.sheet.right - kCurvePopupPad - kCurvePopupCloseSize, closeTop,
|
||||||
|
out.sheet.right - kCurvePopupPad, closeTop + kCurvePopupCloseSize};
|
||||||
|
out.title = Rect{out.sheet.left + kCurvePopupPad, out.sheet.top,
|
||||||
|
out.close.left - kCurvePopupPad, titleBottom};
|
||||||
|
|
||||||
|
out.curveBox = Rect{out.sheet.left + kCurvePopupPad, titleBottom + 2,
|
||||||
|
out.sheet.right - kCurvePopupPad,
|
||||||
|
out.sheet.bottom - kCurvePopupPad};
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool popupOutsideSheet(const CurvePopupLayout& layout, int x, int y) {
|
||||||
|
return !contains(layout.sheet, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler::vst
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// curve_popup.h — PURE sheet geometry + dismissal test for the r11 velocity-curve popup
|
||||||
|
// editor (Wave B, FB1). NO VST3, NO REAPER, NO SWELL/LICE types at the boundary. The mirror
|
||||||
|
// of overflow_menu: the size-clamp / centering / title-row arithmetic lives here, unit-tested
|
||||||
|
// at the clamps outside the DAW, while the editor shell draws the wash + sheet through the
|
||||||
|
// L1 kit and routes clicks (close / curve box / outside-sheet dismiss) via these rects.
|
||||||
|
//
|
||||||
|
// THE POPUP (CONTEXT.md §S-VIEW r11). Summoned by the mini curve-preview button, a CENTERED
|
||||||
|
// SHEET over the Sample face (a 0.50-alpha bg/base wash behind it — lighter than Browse's
|
||||||
|
// 0.82; a focused sub-editor, not a view change): width clamp(60% of window, 360..520),
|
||||||
|
// height clamp(55% of window, 260..380). Inside: a ~22px title row ("VELOCITY -> AMP"
|
||||||
|
// micro-caps left, an 18x18 Close button right) over the full-size curve box filling the
|
||||||
|
// remainder. The curve box rect here is the BORDER rect — the shell derives the mapping box
|
||||||
|
// through its ONE curveBoxFromRect formula (the landed inset grammar), so the popup editor
|
||||||
|
// and the Zone-panel inline editor share coordinates by construction.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "editor_geometry.h" // Rect, contains
|
||||||
|
|
||||||
|
namespace reasampler::vst {
|
||||||
|
|
||||||
|
// Fixed popup metrics (spec r11), exposed so the shell and tests agree.
|
||||||
|
inline constexpr int kCurvePopupMinW = 360;
|
||||||
|
inline constexpr int kCurvePopupMaxW = 520;
|
||||||
|
inline constexpr int kCurvePopupMinH = 260;
|
||||||
|
inline constexpr int kCurvePopupMaxH = 380;
|
||||||
|
inline constexpr int kCurvePopupTitleH = 22;
|
||||||
|
inline constexpr int kCurvePopupCloseSize = 18;
|
||||||
|
inline constexpr int kCurvePopupPad = 8; // sheet inner padding (title inset + box margins)
|
||||||
|
|
||||||
|
struct CurvePopupLayout {
|
||||||
|
Rect sheet; // the bg/panel sheet, centered in the window
|
||||||
|
Rect title; // the caption text rect (left part of the title row)
|
||||||
|
Rect close; // the 18x18 Close (x) button, right-anchored in the title row
|
||||||
|
Rect curveBox; // the full-size curve editor BORDER rect (shell insets via curveBoxFromRect)
|
||||||
|
};
|
||||||
|
|
||||||
|
// The popup geometry for a (w x h) window: sheet width clamp(60% w, 360..520) and height
|
||||||
|
// clamp(55% h, 260..380) — each additionally capped at the window dimension so a degenerate
|
||||||
|
// window never yields an overhanging sheet — centered; title row + close button at the top;
|
||||||
|
// the curve box filling the remainder inside kCurvePopupPad margins. Pure.
|
||||||
|
CurvePopupLayout computeCurvePopup(int w, int h);
|
||||||
|
|
||||||
|
// True when (x, y) lands OUTSIDE the sheet (on the wash) — the click-outside dismissal test.
|
||||||
|
// The shell additionally gates on "no drag in flight" (spec). Pure.
|
||||||
|
bool popupOutsideSheet(const CurvePopupLayout& layout, int x, int y);
|
||||||
|
|
||||||
|
} // namespace reasampler::vst
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
// knob_deck.cpp — see knob_deck.h. Pure arithmetic; no LICE/VST3/REAPER includes.
|
||||||
|
|
||||||
|
#include "knob_deck.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace reasampler::vst {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// The knob-row width of a group: cells side by side (no inter-cell gap — the 48px cell
|
||||||
|
// already carries its own breathing room around the 28px knob), plus the optional row
|
||||||
|
// toggle after a kDeckToggleGap.
|
||||||
|
int knobRowWidth(const DeckGroupDesc& g) {
|
||||||
|
int w = static_cast<int>(g.cellIds.size()) * kDeckCellW;
|
||||||
|
if (g.rowToggle.id >= 0) {
|
||||||
|
if (w > 0) w += kDeckToggleGap;
|
||||||
|
w += 2 * g.rowToggle.segWidth;
|
||||||
|
}
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The caption-row width: the caption reserve plus the optional caption toggle.
|
||||||
|
int captionRowWidth(const DeckGroupDesc& g) {
|
||||||
|
int w = g.captionWidth;
|
||||||
|
if (g.captionToggle.id >= 0) w += kDeckToggleGap + 2 * g.captionToggle.segWidth;
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place one group's inner geometry given its box.
|
||||||
|
DeckGroupLayout layoutGroup(const DeckGroupDesc& g, const Rect& box) {
|
||||||
|
DeckGroupLayout out;
|
||||||
|
out.id = g.id;
|
||||||
|
out.box = box;
|
||||||
|
|
||||||
|
const int captionTop = box.top + kDeckGroupPadY;
|
||||||
|
const int innerLeft = box.left + kDeckGroupPadX;
|
||||||
|
const int innerRight = box.right - kDeckGroupPadX;
|
||||||
|
|
||||||
|
// Caption row: text left, compact toggle right-anchored (r11 — the not-full-width home).
|
||||||
|
out.caption = Rect{innerLeft, captionTop, innerRight, captionTop + kDeckCaptionH};
|
||||||
|
if (g.captionToggle.id >= 0) {
|
||||||
|
const int segW = g.captionToggle.segWidth;
|
||||||
|
const int togTop = captionTop + (kDeckCaptionH - kDeckToggleH) / 2;
|
||||||
|
const Rect seg1{innerRight - segW, togTop, innerRight, togTop + kDeckToggleH};
|
||||||
|
const Rect seg0{seg1.left - segW, togTop, seg1.left, togTop + kDeckToggleH};
|
||||||
|
out.captionToggle = DeckToggleLayout{g.captionToggle.id, seg0, seg1};
|
||||||
|
out.caption.right = seg0.left - kDeckToggleGap; // caption text stops at the toggle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Knob row: fixed cells left-to-right, then the optional row toggle.
|
||||||
|
const int cellTop = captionTop + kDeckCaptionH + kDeckCaptionGap;
|
||||||
|
int x = innerLeft;
|
||||||
|
for (int id : g.cellIds) {
|
||||||
|
DeckCellLayout c;
|
||||||
|
c.id = id;
|
||||||
|
c.cell = Rect{x, cellTop, x + kDeckCellW, cellTop + kDeckCellH};
|
||||||
|
const int knobLeft = x + (kDeckCellW - kDeckKnobSize) / 2;
|
||||||
|
const int knobTop = cellTop + 4;
|
||||||
|
c.knob = Rect{knobLeft, knobTop, knobLeft + kDeckKnobSize, knobTop + kDeckKnobSize};
|
||||||
|
const int labelTop = knobTop + kDeckKnobSize + 4;
|
||||||
|
c.label = Rect{c.cell.left, labelTop, c.cell.right, labelTop + kDeckCellLabelH};
|
||||||
|
out.cells.push_back(c);
|
||||||
|
x += kDeckCellW;
|
||||||
|
}
|
||||||
|
if (g.rowToggle.id >= 0) {
|
||||||
|
if (!g.cellIds.empty()) x += kDeckToggleGap;
|
||||||
|
const int segW = g.rowToggle.segWidth;
|
||||||
|
const int togTop = cellTop + (kDeckCellH - kDeckToggleH) / 2;
|
||||||
|
const Rect seg0{x, togTop, x + segW, togTop + kDeckToggleH};
|
||||||
|
const Rect seg1{seg0.right, togTop, seg0.right + segW, togTop + kDeckToggleH};
|
||||||
|
out.rowToggle = DeckToggleLayout{g.rowToggle.id, seg0, seg1};
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int deckGroupWidth(const DeckGroupDesc& g) {
|
||||||
|
return (std::max)(captionRowWidth(g), knobRowWidth(g)) + 2 * kDeckGroupPadX;
|
||||||
|
}
|
||||||
|
|
||||||
|
int deckRowCount(const std::vector<DeckGroupDesc>& groups, int availWidth) {
|
||||||
|
if (groups.empty()) return 0;
|
||||||
|
int rows = 1;
|
||||||
|
int x = 0;
|
||||||
|
for (const DeckGroupDesc& g : groups) {
|
||||||
|
const int w = deckGroupWidth(g);
|
||||||
|
if (x > 0 && x + kDeckGroupGap + w > availWidth) {
|
||||||
|
++rows;
|
||||||
|
x = w;
|
||||||
|
} else {
|
||||||
|
x += (x > 0 ? kDeckGroupGap : 0) + w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
int deckHeight(const std::vector<DeckGroupDesc>& groups, int availWidth) {
|
||||||
|
const int rows = deckRowCount(groups, availWidth);
|
||||||
|
if (rows == 0) return 0;
|
||||||
|
return rows * kDeckGroupH + (rows - 1) * kDeckRowGap;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckLayout layoutDeck(const std::vector<DeckGroupDesc>& groups, int left, int top,
|
||||||
|
int availWidth) {
|
||||||
|
DeckLayout out;
|
||||||
|
if (groups.empty()) return out;
|
||||||
|
int x = left;
|
||||||
|
int y = top;
|
||||||
|
bool rowHasGroup = false;
|
||||||
|
out.rowCount = 1;
|
||||||
|
for (const DeckGroupDesc& g : groups) {
|
||||||
|
const int w = deckGroupWidth(g);
|
||||||
|
if (rowHasGroup && (x + kDeckGroupGap + w) > (left + availWidth)) {
|
||||||
|
// Wrap: whole trailing group onto the next row (mirror of deckRowCount).
|
||||||
|
++out.rowCount;
|
||||||
|
x = left;
|
||||||
|
y += kDeckGroupH + kDeckRowGap;
|
||||||
|
rowHasGroup = false;
|
||||||
|
}
|
||||||
|
if (rowHasGroup) x += kDeckGroupGap;
|
||||||
|
const Rect box{x, y, x + w, y + kDeckGroupH};
|
||||||
|
out.groups.push_back(layoutGroup(g, box));
|
||||||
|
x = box.right;
|
||||||
|
rowHasGroup = true;
|
||||||
|
}
|
||||||
|
out.height = out.rowCount * kDeckGroupH + (out.rowCount - 1) * kDeckRowGap;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckHit hitTestDeck(const DeckLayout& layout, int x, int y) {
|
||||||
|
for (const DeckGroupLayout& g : layout.groups) {
|
||||||
|
if (!contains(g.box, x, y)) continue;
|
||||||
|
if (g.captionToggle.id >= 0) {
|
||||||
|
if (contains(g.captionToggle.seg0, x, y))
|
||||||
|
return {DeckHitKind::CaptionToggle, g.captionToggle.id, 0};
|
||||||
|
if (contains(g.captionToggle.seg1, x, y))
|
||||||
|
return {DeckHitKind::CaptionToggle, g.captionToggle.id, 1};
|
||||||
|
}
|
||||||
|
if (g.rowToggle.id >= 0) {
|
||||||
|
if (contains(g.rowToggle.seg0, x, y))
|
||||||
|
return {DeckHitKind::RowToggle, g.rowToggle.id, 0};
|
||||||
|
if (contains(g.rowToggle.seg1, x, y))
|
||||||
|
return {DeckHitKind::RowToggle, g.rowToggle.id, 1};
|
||||||
|
}
|
||||||
|
for (const DeckCellLayout& c : g.cells) {
|
||||||
|
if (c.id >= 0 && contains(c.cell, x, y)) return {DeckHitKind::Knob, c.id, -1};
|
||||||
|
}
|
||||||
|
return {}; // inside the box but on fence/padding/blank — a miss (groups never overlap)
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler::vst
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
// knob_deck.h — PURE knob-deck layout + hit-test for the r11 Sample-face recomposition
|
||||||
|
// (Wave B, FB1). NO VST3, NO REAPER, NO SWELL/LICE types at the boundary, and — like
|
||||||
|
// param_slider — NO engine types: cells and toggles carry opaque shell-owned control ids.
|
||||||
|
// The mirror of action_bar / param_slider: the fiddly group-box / caption-row / cell-grid
|
||||||
|
// arithmetic lives here, unit-tested outside the DAW, while the editor shell draws each
|
||||||
|
// group (fence, caption, compact toggles, knobs) through the L1 kit and routes clicks/drags
|
||||||
|
// via the hit-test. The KNOB PRIMITIVE itself (value<->needle-angle, vertical drag) is
|
||||||
|
// param_slider's (FA4); a knob cell here is just a rect — the shell composes the two.
|
||||||
|
//
|
||||||
|
// THE DECK (CONTEXT.md §S-VIEW r11). A horizontal run of FENCED GROUPS, left -> right, each
|
||||||
|
// a hairline-bordered bg/panel box with a CAPTION ROW (micro-caps caption left; the group's
|
||||||
|
// compact mode toggle right-anchored IN the caption row — this is where the not-full-width
|
||||||
|
// toggles live) over a KNOB ROW of fixed 48x58 cells (28px knob centered, 12px label band
|
||||||
|
// beneath). A group may additionally place one 18px-tall two-segment toggle IN the knob row
|
||||||
|
// after its cells (the VOICE group's Retrig|Legato — same Mono/Stereo segment grammar,
|
||||||
|
// vertically centered). Groups that must keep stable geometry across a mode flip reserve
|
||||||
|
// blank cells (id -1): the AMP ENVELOPE group always spans 5 cells so Gate<->Trigger never
|
||||||
|
// reflows its neighbours.
|
||||||
|
//
|
||||||
|
// WRAP (deterministic): groups place left-to-right with kDeckGroupGap between; a group that
|
||||||
|
// does not fit the remaining width starts a new deck row (whole groups only, never split).
|
||||||
|
// The first group of a row always places even if wider than the row (degenerate width).
|
||||||
|
// deckHeight() exposes the resulting height so the shell can bottom-anchor the deck band and
|
||||||
|
// give the ELASTIC HERO the rest (r11 band order).
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "editor_geometry.h" // Rect, contains — the shared geometry idiom
|
||||||
|
|
||||||
|
namespace reasampler::vst {
|
||||||
|
|
||||||
|
// Fixed deck metrics (spec r11), exposed so the shell and tests agree.
|
||||||
|
inline constexpr int kDeckCellW = 48; // one knob cell
|
||||||
|
inline constexpr int kDeckCellH = 58;
|
||||||
|
inline constexpr int kDeckKnobSize = 28; // knob diameter inside the cell
|
||||||
|
inline constexpr int kDeckCellLabelH = 12; // the Micro label band under the knob
|
||||||
|
inline constexpr int kDeckCaptionH = 20; // the group caption row
|
||||||
|
inline constexpr int kDeckToggleH = 18; // compact toggle segment height
|
||||||
|
inline constexpr int kDeckGroupPadX = 6; // group box horizontal inner padding
|
||||||
|
inline constexpr int kDeckGroupPadY = 4; // group box vertical inner padding
|
||||||
|
inline constexpr int kDeckCaptionGap = 2; // caption row -> knob row gap
|
||||||
|
inline constexpr int kDeckToggleGap = 4; // caption text -> toggle / cells -> row toggle gap
|
||||||
|
inline constexpr int kDeckGroupGap = 12; // gap between groups on a row
|
||||||
|
inline constexpr int kDeckRowGap = 8; // gap between wrapped deck rows
|
||||||
|
// One group box: padding + caption + gap + cell row + padding.
|
||||||
|
inline constexpr int kDeckGroupH =
|
||||||
|
kDeckGroupPadY + kDeckCaptionH + kDeckCaptionGap + kDeckCellH + kDeckGroupPadY;
|
||||||
|
|
||||||
|
// A two-segment compact toggle (always 2 segments — the Mono/Stereo grammar). id -1 = absent.
|
||||||
|
struct DeckToggleDesc {
|
||||||
|
int id = -1; // shell control id returned by the hit-test; -1 = no toggle
|
||||||
|
int segWidth = 44; // px per segment
|
||||||
|
};
|
||||||
|
|
||||||
|
// One fenced group, in deck order. `cellIds` are the knob cells left-to-right; an id of -1
|
||||||
|
// is a RESERVED BLANK cell (geometry held, never hit — the AMP ENVELOPE Trigger face).
|
||||||
|
// `captionWidth` is the px the shell reserves for the caption text (this module does not
|
||||||
|
// measure text — the house constant-metrics pattern).
|
||||||
|
struct DeckGroupDesc {
|
||||||
|
int id = 0; // shell group id (opaque here)
|
||||||
|
int captionWidth = 60;
|
||||||
|
DeckToggleDesc captionToggle; // right-anchored in the caption row; id -1 = none
|
||||||
|
std::vector<int> cellIds; // knob cells; -1 = blank reserve
|
||||||
|
DeckToggleDesc rowToggle; // in the knob row after the cells; id -1 = none
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Laid-out geometry ---------------------------------------------------------------
|
||||||
|
|
||||||
|
struct DeckToggleLayout {
|
||||||
|
int id = -1;
|
||||||
|
Rect seg0; // left segment
|
||||||
|
Rect seg1; // right segment
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DeckCellLayout {
|
||||||
|
int id = -1;
|
||||||
|
Rect cell; // the full 48x58 cell
|
||||||
|
Rect knob; // the centered kDeckKnobSize square (the knob circle inscribes it)
|
||||||
|
Rect label; // the 12px label band beneath the knob
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DeckGroupLayout {
|
||||||
|
int id = 0;
|
||||||
|
Rect box; // the fenced group box
|
||||||
|
Rect caption; // caption text rect (left part of the caption row)
|
||||||
|
DeckToggleLayout captionToggle; // id -1 when absent (rects empty)
|
||||||
|
std::vector<DeckCellLayout> cells;
|
||||||
|
DeckToggleLayout rowToggle; // id -1 when absent
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DeckLayout {
|
||||||
|
std::vector<DeckGroupLayout> groups;
|
||||||
|
int rowCount = 0;
|
||||||
|
int height = 0; // rowCount * kDeckGroupH + (rowCount-1) * kDeckRowGap; 0 for no groups
|
||||||
|
};
|
||||||
|
|
||||||
|
// The width of one group box: the wider of its caption row (caption + gap + toggle) and its
|
||||||
|
// knob row (cells + gap + row toggle), plus the horizontal padding. Pure.
|
||||||
|
int deckGroupWidth(const DeckGroupDesc& g);
|
||||||
|
|
||||||
|
// The number of deck rows the groups occupy at `availWidth` under the greedy whole-group
|
||||||
|
// wrap (a group that does not fit the remaining row width starts a new row; the first group
|
||||||
|
// of a row always places). 0 for an empty group list. Pure — the wrap is deterministic.
|
||||||
|
int deckRowCount(const std::vector<DeckGroupDesc>& groups, int availWidth);
|
||||||
|
|
||||||
|
// The total deck height at `availWidth` (rows * kDeckGroupH + inter-row gaps). 0 for an
|
||||||
|
// empty list. The shell bottom-anchors a band of exactly this height. Pure.
|
||||||
|
int deckHeight(const std::vector<DeckGroupDesc>& groups, int availWidth);
|
||||||
|
|
||||||
|
// Lay the groups out from (left, top) within `availWidth`, wrapping per deckRowCount's rule.
|
||||||
|
// Every rect is absolute. Pure — same inputs, same layout.
|
||||||
|
DeckLayout layoutDeck(const std::vector<DeckGroupDesc>& groups, int left, int top,
|
||||||
|
int availWidth);
|
||||||
|
|
||||||
|
// --- Hit-test --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
enum class DeckHitKind { None, Knob, CaptionToggle, RowToggle };
|
||||||
|
|
||||||
|
struct DeckHit {
|
||||||
|
DeckHitKind kind = DeckHitKind::None;
|
||||||
|
int id = -1; // the control id of the hit element (cell id / toggle id)
|
||||||
|
int segment = -1; // 0/1 for a toggle hit; -1 otherwise
|
||||||
|
};
|
||||||
|
|
||||||
|
// The deck element a point lands on: a knob CELL (the whole 48x58 cell — friendlier than the
|
||||||
|
// bare knob circle; the shell anchors the vertical drag wherever the grab lands), a caption-
|
||||||
|
// toggle segment, or a row-toggle segment. Blank cells (id -1) and everything else miss.
|
||||||
|
// Pure — the shell's routing entry point.
|
||||||
|
DeckHit hitTestDeck(const DeckLayout& layout, int x, int y);
|
||||||
|
|
||||||
|
} // namespace reasampler::vst
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// master_gain.cpp — see master_gain.h. Pure math; no LICE/VST3/REAPER includes.
|
||||||
|
|
||||||
|
#include "master_gain.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace reasampler::vst {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
double clamp01(double v) { return v < 0.0 ? 0.0 : (v > 1.0 ? 1.0 : v); }
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
double masterGainMaxLinear() { return std::pow(10.0, kMasterGainMaxDb / 20.0); }
|
||||||
|
|
||||||
|
double masterGainDbFromNorm(double norm) {
|
||||||
|
norm = clamp01(norm);
|
||||||
|
if (norm <= 0.0) return -std::numeric_limits<double>::infinity();
|
||||||
|
return kMasterGainMinDb + norm * (kMasterGainMaxDb - kMasterGainMinDb);
|
||||||
|
}
|
||||||
|
|
||||||
|
double masterGainNormFromDb(double db) {
|
||||||
|
if (!(db > kMasterGainMinDb)) return 0.0; // -inf, NaN, and the floor all read 0
|
||||||
|
return clamp01((db - kMasterGainMinDb) / (kMasterGainMaxDb - kMasterGainMinDb));
|
||||||
|
}
|
||||||
|
|
||||||
|
double masterGainLinearFromNorm(double norm) {
|
||||||
|
norm = clamp01(norm);
|
||||||
|
if (norm <= 0.0) return 0.0; // TRUE silence at the bottom — not an epsilon
|
||||||
|
return std::pow(10.0, masterGainDbFromNorm(norm) / 20.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
double masterGainNormFromLinear(double linear) {
|
||||||
|
if (!std::isfinite(linear) || linear <= 0.0) return 0.0;
|
||||||
|
return masterGainNormFromDb(20.0 * std::log10(linear));
|
||||||
|
}
|
||||||
|
|
||||||
|
void formatMasterGainLabel(double norm, char* buf, std::size_t len) {
|
||||||
|
if (!buf || len == 0) return;
|
||||||
|
norm = clamp01(norm);
|
||||||
|
if (norm <= 0.0) {
|
||||||
|
std::snprintf(buf, len, "-inf");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const double db = masterGainDbFromNorm(norm);
|
||||||
|
std::snprintf(buf, len, "%+.1fdB", db);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler::vst
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// master_gain.h — PURE dB<->linear<->knob-taper math for the FB1 post-mixer master gain.
|
||||||
|
// NO VST3, NO REAPER, NO SWELL/LICE types. The mirror of trigger_seam: one tiny module owns
|
||||||
|
// the ONE formula both sides of a seam share — here the editor's Gain knob (normalized 0..1)
|
||||||
|
// and the processor's stored/applied linear gain — so the drawn needle, the persisted value,
|
||||||
|
// and the audio-thread multiply can never drift.
|
||||||
|
//
|
||||||
|
// THE CONTROL (Daniel, FB1). A post-mixer master gain, range -inf .. +24 dB, dB-scaled taper
|
||||||
|
// with -inf at the BOTTOM of the knob: normalized 0 maps to TRUE ZERO linear gain (silence,
|
||||||
|
// not a tiny epsilon), and the remaining travel maps linearly in dB from kMasterGainMinDb
|
||||||
|
// (the finite taper floor) up to kMasterGainMaxDb. Unity (0 dB) sits at norm
|
||||||
|
// kMasterGainMinDb/(kMasterGainMinDb - kMasterGainMaxDb) ~= 0.714 — most of the throw is
|
||||||
|
// usable trim, the last stretch is boost. The PERSISTED value is the LINEAR gain (a plain
|
||||||
|
// finite double, 0 = silence — no -inf on the wire); the taper is a UI-side view of it.
|
||||||
|
//
|
||||||
|
// RT DISCIPLINE: the processor applies the linear gain as one multiply over the summed
|
||||||
|
// output — these functions run on the UI/state threads only.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
namespace reasampler::vst {
|
||||||
|
|
||||||
|
// The dB taper endpoints. norm 0 is -inf (true zero); norm just above 0 starts at the
|
||||||
|
// finite floor kMasterGainMinDb and sweeps linearly in dB to kMasterGainMaxDb at norm 1.
|
||||||
|
inline constexpr double kMasterGainMinDb = -60.0;
|
||||||
|
inline constexpr double kMasterGainMaxDb = 24.0;
|
||||||
|
|
||||||
|
// The largest linear gain the control can produce (kMasterGainMaxDb as a ratio, ~15.849).
|
||||||
|
double masterGainMaxLinear();
|
||||||
|
|
||||||
|
// Knob taper: normalized [0,1] -> dB. norm <= 0 -> -infinity; else the linear-in-dB sweep
|
||||||
|
// [kMasterGainMinDb, kMasterGainMaxDb]. norm is clamped to [0,1]. Pure.
|
||||||
|
double masterGainDbFromNorm(double norm);
|
||||||
|
|
||||||
|
// Inverse taper: dB -> normalized [0,1]. -infinity (or any dB at or below kMasterGainMinDb,
|
||||||
|
// including below-floor values like -80 dB) maps to norm 0 (the -inf bottom detent) — the
|
||||||
|
// finite sweep only covers the range above kMasterGainMinDb; everything at or below it collapses
|
||||||
|
// to the same true-zero bottom. +24 -> 1. Pure.
|
||||||
|
double masterGainNormFromDb(double db);
|
||||||
|
|
||||||
|
// Knob taper composed with dB->ratio: normalized [0,1] -> LINEAR gain. norm 0 -> exactly
|
||||||
|
// 0.0 (true silence); norm 1 -> masterGainMaxLinear(). Pure.
|
||||||
|
double masterGainLinearFromNorm(double norm);
|
||||||
|
|
||||||
|
// Inverse: LINEAR gain -> normalized [0,1]. linear <= 0 -> 0 (the -inf bottom); a linear at
|
||||||
|
// or below the kMasterGainMinDb floor (e.g. 0.001 = -60 dB, or anything below) also maps to 0
|
||||||
|
// — the floor IS the -inf detent; values between true-zero and the floor cannot be represented
|
||||||
|
// on the knob and collapse to the bottom. unity -> ~0.714; masterGainMaxLinear() -> 1.
|
||||||
|
// Out-of-range/non-finite input clamps. Pure.
|
||||||
|
double masterGainNormFromLinear(double linear);
|
||||||
|
|
||||||
|
// The knob's hover/drag value label for a normalized value: "-inf" at the bottom, else a
|
||||||
|
// signed one-decimal dB string ("-12.0dB", "+0.0dB", "+2.4dB"). Writes at most `len` bytes
|
||||||
|
// including the terminator. Pure.
|
||||||
|
void formatMasterGainLabel(double norm, char* buf, std::size_t len);
|
||||||
|
|
||||||
|
} // namespace reasampler::vst
|
||||||
+735
-290
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,8 @@
|
|||||||
#include "editor_geometry.h" // Rect (the shell's sub-rect type, shared with the pure modules)
|
#include "editor_geometry.h" // Rect (the shell's sub-rect type, shared with the pure modules)
|
||||||
#include "envelope_edit.h" // EnvClampBounds / NodeHit (S-VIEW-3 envelope node hit-test/edit)
|
#include "envelope_edit.h" // EnvClampBounds / NodeHit (S-VIEW-3 envelope node hit-test/edit)
|
||||||
#include "envelope_overlay.h" // AmpEnvelope / EnvNode (S-VIEW-3 envelope overlay draw seam)
|
#include "envelope_overlay.h" // AmpEnvelope / EnvNode (S-VIEW-3 envelope overlay draw seam)
|
||||||
#include "param_slider.h" // ControlRow (the S12/S15/S16 control-surface geometry)
|
#include "knob_deck.h" // DeckGroupDesc / DeckLayout (r11 Sample-face knob deck, FB1)
|
||||||
|
#include "param_slider.h" // ControlRow + the FA4 radial-knob primitive (S12/S15/S16 + r11)
|
||||||
#include "peaks.h" // Envelope (the cached peak thumbnail)
|
#include "peaks.h" // Envelope (the cached peak thumbnail)
|
||||||
#include "sample_map.h" // SampleChoice, BankChoice, PerformanceMap (the shell's snapshot)
|
#include "sample_map.h" // SampleChoice, BankChoice, PerformanceMap (the shell's snapshot)
|
||||||
#include "velocity_curve.h" // VelocityCurve (S-VIEW-10 transfer-curve editor state)
|
#include "velocity_curve.h" // VelocityCurve (S-VIEW-10 transfer-curve editor state)
|
||||||
@@ -80,9 +81,11 @@ private:
|
|||||||
// marker on the S11 waveform surface (which marker is in waveMarker_); kEnvNode is a
|
// marker on the S11 waveform surface (which marker is in waveMarker_); kEnvNode is a
|
||||||
// draggable envelope breakpoint on the Sample-view hero overlay (S-VIEW-3, which node in
|
// draggable envelope breakpoint on the Sample-view hero overlay (S-VIEW-3, which node in
|
||||||
// envNode_); kCurveNode is a draggable velocity-curve control point in the S-VIEW-10
|
// envNode_); kCurveNode is a draggable velocity-curve control point in the S-VIEW-10
|
||||||
// transfer-curve editor (which point in curvePointIndex_).
|
// transfer-curve editor (which point in curvePointIndex_); kDeckKnob is a GRAB-ANCHORED
|
||||||
|
// vertical radial-knob drag on the r11 Sample-face deck/cluster (which control in
|
||||||
|
// dragParamId_; the value at grab in dragKnobStartValue_ — no jump on grab, FA4).
|
||||||
enum class DragKind { kNone, kRootMarker, kZoneLow, kZoneHigh, kZoneBody, kWaveMarker,
|
enum class DragKind { kNone, kRootMarker, kZoneLow, kZoneHigh, kZoneBody, kWaveMarker,
|
||||||
kScrollThumb, kParamSlider, kEnvNode, kCurveNode };
|
kScrollThumb, kParamSlider, kEnvNode, kCurveNode, kDeckKnob };
|
||||||
|
|
||||||
// The parameter controls on the setup surface (S12 AHDSR + the S15/S16 control surfaces).
|
// The parameter controls on the setup surface (S12 AHDSR + the S15/S16 control surfaces).
|
||||||
// The int value is the ControlDesc id the pure param_slider hit-test returns; the shell
|
// The int value is the ControlDesc id the pure param_slider hit-test returns; the shell
|
||||||
@@ -103,6 +106,12 @@ private:
|
|||||||
kPitchEnvDecay, // AD pitch decay (S16)
|
kPitchEnvDecay, // AD pitch decay (S16)
|
||||||
kPitchEnvDepth, // AD pitch depth in +/- semitones (S16)
|
kPitchEnvDepth, // AD pitch depth in +/- semitones (S16)
|
||||||
kKeyTrack, // S-VIEW-6 key-tracking 0..200% (lives on PerformanceZone, not ZonePlaySeconds)
|
kKeyTrack, // S-VIEW-6 key-tracking 0..200% (lives on PerformanceZone, not ZonePlaySeconds)
|
||||||
|
// r11 deck-only controls (FB1): processor-side per-instance params, NOT zone params —
|
||||||
|
// routed to the processor setters, never through applyZoneControl / the map.
|
||||||
|
kVoiceCount, // Phase S polyphony bound (1..32) — a stepped knob in the VOICE group
|
||||||
|
kVoiceMode, // Poly | Mono caption toggle (VOICE group)
|
||||||
|
kMonoTrigger, // Retrig | Legato row toggle (VOICE group; live only in Mono)
|
||||||
|
kMasterGain, // FB1 post-mixer master gain knob (-inf..+24 dB taper, MASTER group)
|
||||||
kCount
|
kCount
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -134,8 +143,11 @@ private:
|
|||||||
kPreview, // the Sample-view preview-trigger button
|
kPreview, // the Sample-view preview-trigger button
|
||||||
kAddZone, // the "+ Add Zone" button
|
kAddZone, // the "+ Add Zone" button
|
||||||
kDeleteZone, // the "Delete" zone button
|
kDeleteZone, // the "Delete" zone button
|
||||||
kControl, // a param-panel control row (index = ControlDesc id)
|
kControl, // a param-panel control row / deck element (index = control id)
|
||||||
kCurveNode, // a velocity-curve control point (index = point index, S-VIEW-10)
|
kCurveNode, // a velocity-curve control point (index = point index, S-VIEW-10)
|
||||||
|
kVelKnob, // the cluster preview-velocity radial knob (r11)
|
||||||
|
kCurveButton, // the cluster mini curve-preview button (r11 — opens the popup)
|
||||||
|
kPopupClose, // the curve popup's Close (x) button (r11)
|
||||||
};
|
};
|
||||||
struct HoverTarget {
|
struct HoverTarget {
|
||||||
HoverKind kind = HoverKind::kNone;
|
HoverKind kind = HoverKind::kNone;
|
||||||
@@ -146,11 +158,19 @@ private:
|
|||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
void paint(HDC hdc);
|
void paint(HDC hdc);
|
||||||
void paintSample(LICE_IBitmap* bmp, int w, int h); // S-VIEW-2 home face
|
void paintSample(LICE_IBitmap* bmp, int w, int h); // S-VIEW-2/r11 home face
|
||||||
void paintBrowse(LICE_IBitmap* bmp, int w, int h); // S-VIEW-5 modal picker overlay
|
void paintBrowse(LICE_IBitmap* bmp, int w, int h); // S-VIEW-5 modal picker overlay
|
||||||
void paintZone(LICE_IBitmap* bmp, int w, int h); // S-VIEW-8 zone surface
|
void paintZone(LICE_IBitmap* bmp, int w, int h); // S-VIEW-8 zone surface
|
||||||
void paintEmptyState(LICE_IBitmap* bmp, const Rect& area);
|
void paintEmptyState(LICE_IBitmap* bmp, const Rect& area);
|
||||||
void paintControls(LICE_IBitmap* bmp, const Rect& panel, const PerformanceZone& zone); // S12/S15/S16 + keyTrack
|
void paintControls(LICE_IBitmap* bmp, const Rect& panel, const PerformanceZone& zone); // S12/S15/S16 + keyTrack (Zone surface; the Sample face uses the r11 knob deck)
|
||||||
|
|
||||||
|
// --- r11 Sample-face recomposition (FB1) ---------------------------------------
|
||||||
|
// The knob deck: the fenced task groups (AMP ENVELOPE / PITCH / PITCH ENV / VOICE /
|
||||||
|
// MASTER) drawn through the L1 kit — group fence + caption + compact caption toggles +
|
||||||
|
// radial knobs (param_slider's FA4 primitive) with label<->value swap on hover/drag.
|
||||||
|
void paintKnobDeck(LICE_IBitmap* bmp, const Rect& deckArea, const PerformanceZone& zone);
|
||||||
|
// The centered curve-popup sheet (wash + title + close + full-size curve editor).
|
||||||
|
void paintCurvePopup(LICE_IBitmap* bmp, int w, int h);
|
||||||
// Trace the S-VIEW-3 amp-envelope overlay + its draggable node handles over `waveArea` for
|
// Trace the S-VIEW-3 amp-envelope overlay + its draggable node handles over `waveArea` for
|
||||||
// `zone`'s play params, at the sample's wall-clock duration. Shared by the Sample hero band.
|
// `zone`'s play params, at the sample's wall-clock duration. Shared by the Sample hero band.
|
||||||
void paintEnvelopeOverlay(LICE_IBitmap* bmp, const Rect& waveArea, const PerformanceZone& zone,
|
void paintEnvelopeOverlay(LICE_IBitmap* bmp, const Rect& waveArea, const PerformanceZone& zone,
|
||||||
@@ -159,7 +179,9 @@ private:
|
|||||||
// Y = amp 0-1), the monotone spline traced by eval, one draggable node handle per control
|
// Y = amp 0-1), the monotone spline traced by eval, one draggable node handle per control
|
||||||
// point. Shared by the Sample face (beside the hero) and the Zone param panel; all mapping /
|
// point. Shared by the Sample face (beside the hero) and the Zone param panel; all mapping /
|
||||||
// hit-test / clamp math lives in the pure velocity_curve module. `r` empty -> draws nothing.
|
// hit-test / clamp math lives in the pure velocity_curve module. `r` empty -> draws nothing.
|
||||||
void paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r, const PerformanceZone& zone);
|
// drawCaption: false suppresses the "Vel curve" corner label (the popup draws its own title).
|
||||||
|
void paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r, const PerformanceZone& zone,
|
||||||
|
bool drawCaption = true);
|
||||||
|
|
||||||
// Route a mouse-down inside curve-editor box `r` editing map_.zones[zoneIndex]: a node grab
|
// Route a mouse-down inside curve-editor box `r` editing map_.zones[zoneIndex]: a node grab
|
||||||
// starts a kCurveNode drag; Alt-click on an interior node deletes it (committed at once);
|
// starts a kCurveNode drag; Alt-click on an interior node deletes it (committed at once);
|
||||||
@@ -170,6 +192,11 @@ private:
|
|||||||
void onMouseDown(int x, int y);
|
void onMouseDown(int x, int y);
|
||||||
void onMouseMove(int x, int y);
|
void onMouseMove(int x, int y);
|
||||||
void onMouseUp(int x, int y);
|
void onMouseUp(int x, int y);
|
||||||
|
// r11: right-click — the curve popup's PRIMARY node-delete affordance (issue 3c). Only
|
||||||
|
// acts while the popup is open; a right-click on a popup curve node deletes it through
|
||||||
|
// the same commit path as Alt-click (deletePoint's endpoint guard makes endpoint
|
||||||
|
// right-clicks a safe no-op). Everything else ignores right-clicks.
|
||||||
|
void onMouseRDown(int x, int y);
|
||||||
|
|
||||||
// Route a click at (x,y) into the param control panel `panel` editing map_.zones[zoneIndex]:
|
// Route a click at (x,y) into the param control panel `panel` editing map_.zones[zoneIndex]:
|
||||||
// a toggle segment commits immediately, a slider grab starts a live param-drag (kParamSlider),
|
// a toggle segment commits immediately, a slider grab starts a live param-drag (kParamSlider),
|
||||||
@@ -337,6 +364,37 @@ private:
|
|||||||
// needs a concrete zone to write. Returns -1 if selectedId_ is empty.
|
// needs a concrete zone to write. Returns -1 if selectedId_ is empty.
|
||||||
int ensureSampleZone();
|
int ensureSampleZone();
|
||||||
|
|
||||||
|
// --- r11 knob-deck plumbing (FB1) ---------------------------------------------
|
||||||
|
//
|
||||||
|
// The deck is the r11 replacement for the Sample face's slider control strip: the pure
|
||||||
|
// knob_deck module lays out the fenced groups, param_slider's FA4 primitive owns the
|
||||||
|
// value<->needle map, and these members own the control-id <-> value binding (the same
|
||||||
|
// division of labor paintControls/applyControl use for the Zone surface's sliders).
|
||||||
|
|
||||||
|
// The deck group descriptors for the current mode: AMP ENVELOPE (Gate: A/H/D/S/R;
|
||||||
|
// Trigger: Fade In / Length % / Fade Out + two RESERVED blanks so a mode flip never
|
||||||
|
// reflows the neighbours) / PITCH (Key Track) / PITCH ENV (P.Attack/P.Decay/P.Depth) /
|
||||||
|
// VOICE (Voices knob + Poly|Mono caption toggle + Retrig|Legato row toggle) / MASTER
|
||||||
|
// (the FB1 post-mixer Gain knob).
|
||||||
|
std::vector<DeckGroupDesc> deckGroupDescs(const ZonePlaySeconds& play) const;
|
||||||
|
|
||||||
|
// The normalized [0,1] value a deck knob shows for `zone` — zone params route through
|
||||||
|
// controlValue/keyTrack; the processor-side ids (voice count, master gain, and the
|
||||||
|
// cluster's preview velocity via the -2 sentinel) read the processor's live value, so
|
||||||
|
// the knob and its storage are two views on one model (re-read each paint).
|
||||||
|
double deckControlNorm(int id, const PerformanceZone& zone) const;
|
||||||
|
|
||||||
|
// Apply a deck-knob value: zone params write map_.zones[zoneIndex] (live-drag semantics,
|
||||||
|
// commit on release); processor params (voice count / master gain / preview velocity)
|
||||||
|
// write through the processor setters immediately (transient — no map edit, no reload).
|
||||||
|
// zoneIndex is ignored for processor-side ids.
|
||||||
|
void applyDeckKnob(int zoneIndex, int id, double norm);
|
||||||
|
|
||||||
|
// The knob's live value label (shown in place of the name label during hover/drag):
|
||||||
|
// seconds ("0.123s"), percents ("85%"), source frames ("8820f"), signed semitones
|
||||||
|
// ("+3.5st"), a voice count ("16"), or the master-gain dB ("-inf"/"+2.4dB").
|
||||||
|
std::string deckValueLabel(int id, const PerformanceZone& zone) const;
|
||||||
|
|
||||||
ReaSamplerProcessor* processor_ = nullptr;
|
ReaSamplerProcessor* processor_ = nullptr;
|
||||||
|
|
||||||
// --- Snapshot of the live bank (drawn each paint; refreshed off the audio thread) ---
|
// --- Snapshot of the live bank (drawn each paint; refreshed off the audio thread) ---
|
||||||
@@ -444,6 +502,15 @@ private:
|
|||||||
Rect dragCurveRect_{};
|
Rect dragCurveRect_{};
|
||||||
int dragCurveZone_ = -1;
|
int dragCurveZone_ = -1;
|
||||||
|
|
||||||
|
// r11 deck-knob drag (FB1): the control's normalized value AT GRAB — knobDragValue maps
|
||||||
|
// the vertical pixel delta from this anchor, so a grab never jumps the value (FA4).
|
||||||
|
double dragKnobStartValue_ = 0.0;
|
||||||
|
|
||||||
|
// r11 curve popup (FB1): open flag — editor-local, never persisted. The popup edits the
|
||||||
|
// picked capture's one-zone site (effectiveSampleZone / ensureSampleZone), re-resolved
|
||||||
|
// each paint so a sync-tick refresh mid-open stays coherent.
|
||||||
|
bool curvePopupOpen_ = false;
|
||||||
|
|
||||||
// --- Peak-thumbnail cache (mirror of bank_panel; id -> envelope at a bin width) ------
|
// --- Peak-thumbnail cache (mirror of bank_panel; id -> envelope at a bin width) ------
|
||||||
// Keyed by "id|binCount" so a resize recomputes at the new width. Cleared on refresh so
|
// Keyed by "id|binCount" so a resize recomputes at the new width. Cleared on refresh so
|
||||||
// a bank edit (a re-captured or deleted sample) does not show a stale thumbnail.
|
// a bank edit (a re-captured or deleted sample) does not show a stale thumbnail.
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include "bank_sync.h" // S9/S8 pure decisions: parseBankGeneration, consumeDecision
|
#include "bank_sync.h" // S9/S8 pure decisions: parseBankGeneration, consumeDecision
|
||||||
#include "capture_paths.h" // resolveBankFile (shared M4 path resolution)
|
#include "capture_paths.h" // resolveBankFile (shared M4 path resolution)
|
||||||
#include "ext_keys.h" // kProjExtBanksKey / kProjExtBankGenKey / kProjExtAssignKey (shared wire contract)
|
#include "ext_keys.h" // kProjExtBanksKey / kProjExtBankGenKey / kProjExtAssignKey (shared wire contract)
|
||||||
|
#include "master_gain.h" // masterGainMaxLinear (FB1 post-mixer gain clamp)
|
||||||
#include "reasampler_editor.h"
|
#include "reasampler_editor.h"
|
||||||
#include "reasampler_embed.h" // S6 embed shell + IReaperUIEmbedInterface (its iid DEF'd there)
|
#include "reasampler_embed.h" // S6 embed shell + IReaperUIEmbedInterface (its iid DEF'd there)
|
||||||
#include "sample_map.h" // selectSample, resolvePerformance, buildZonedKeymap, state (de)ser
|
#include "sample_map.h" // selectSample, resolvePerformance, buildZonedKeymap, state (de)ser
|
||||||
@@ -42,6 +43,13 @@ namespace {
|
|||||||
// FIXED so raising the voice count never multiplies shifter CPU past the profiled budget.
|
// FIXED so raising the voice count never multiplies shifter CPU past the profiled budget.
|
||||||
constexpr std::size_t kPreserveVoiceCap = 8;
|
constexpr std::size_t kPreserveVoiceCap = 8;
|
||||||
|
|
||||||
|
// FB1 post-mixer gain ramp rate (per sample). gainCurrent_ converges to masterGain_ at this
|
||||||
|
// linear step; it ramps from 0 to unity (or vice versa) in ~20 ms at 48 kHz. The early-out
|
||||||
|
// (|current - target| < threshold) snaps to the target and avoids the ramp loop on idle blocks.
|
||||||
|
// kGainRampSnap is the threshold below which we snap to the target (avoids long sub-LSB creep).
|
||||||
|
constexpr float kGainRampRate = 1.0f / 960.0f; // 960 samples @ 48 kHz ≈ 20 ms
|
||||||
|
constexpr float kGainRampSnap = kGainRampRate * 0.5f;
|
||||||
|
|
||||||
// Read a whole file into a byte buffer. Off-thread only (blocking file I/O). Empty on
|
// Read a whole file into a byte buffer. Off-thread only (blocking file I/O). Empty on
|
||||||
// any failure — the caller treats an unreadable WAV as "nothing to play".
|
// any failure — the caller treats an unreadable WAV as "nothing to play".
|
||||||
std::vector<std::uint8_t> readFileBytes(const std::string& path) {
|
std::vector<std::uint8_t> readFileBytes(const std::string& path) {
|
||||||
@@ -224,6 +232,10 @@ tresult PLUGIN_API ReaSamplerProcessor::setState(IBStream* state) {
|
|||||||
voiceMode_ = cs.voiceMode;
|
voiceMode_ = cs.voiceMode;
|
||||||
monoTrigger_ = cs.monoTrigger;
|
monoTrigger_ = cs.monoTrigger;
|
||||||
}
|
}
|
||||||
|
// FB1: restore the post-mixer master gain (v8; older blobs lift to unity in
|
||||||
|
// deserializeComponentState — pre-FB1 output). One atomic store; the audio thread picks
|
||||||
|
// it up at the next block start.
|
||||||
|
setMasterGainLinear(cs.masterGainLinear);
|
||||||
// Rebuild from the restored state (off-thread — setState is a load-time call).
|
// Rebuild from the restored state (off-thread — setState is a load-time call).
|
||||||
reloadFromBank();
|
reloadFromBank();
|
||||||
return kResultOk;
|
return kResultOk;
|
||||||
@@ -252,6 +264,7 @@ tresult PLUGIN_API ReaSamplerProcessor::getState(IBStream* state) {
|
|||||||
state_out.voiceMode = voiceMode_;
|
state_out.voiceMode = voiceMode_;
|
||||||
state_out.monoTrigger = monoTrigger_;
|
state_out.monoTrigger = monoTrigger_;
|
||||||
}
|
}
|
||||||
|
state_out.masterGainLinear = masterGainLinear(); // FB1: persist the post-mixer gain (v8)
|
||||||
const std::vector<std::uint8_t> bytes = serializeComponentState(state_out);
|
const std::vector<std::uint8_t> bytes = serializeComponentState(state_out);
|
||||||
if (!bytes.empty()) {
|
if (!bytes.empty()) {
|
||||||
const tresult wr = state->write(const_cast<std::uint8_t*>(bytes.data()),
|
const tresult wr = state->write(const_cast<std::uint8_t*>(bytes.data()),
|
||||||
@@ -351,6 +364,16 @@ void ReaSamplerProcessor::setMonoTrigger(MonoTrigger trigger) {
|
|||||||
rebuildVoiceEngine();
|
rebuildVoiceEngine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ReaSamplerProcessor::setMasterGainLinear(double linear) {
|
||||||
|
// Clamp to the control's legal span (the master_gain taper: 0 = -inf/silence, cap =
|
||||||
|
// +24 dB). One relaxed atomic store — the audio thread reads it at the next block start;
|
||||||
|
// no rebuild, no lock (a post-sum output trim is not a keymap fact).
|
||||||
|
if (!(linear >= 0.0)) linear = 0.0; // also catches NaN
|
||||||
|
const double maxLin = masterGainMaxLinear();
|
||||||
|
if (linear > maxLin) linear = maxLin;
|
||||||
|
masterGain_.store(static_cast<float>(linear), std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
void ReaSamplerProcessor::previewNoteOn(int note) {
|
void ReaSamplerProcessor::previewNoteOn(int note) {
|
||||||
if (note < 0) note = 0;
|
if (note < 0) note = 0;
|
||||||
if (note > 127) note = 127;
|
if (note > 127) note = 127;
|
||||||
@@ -877,6 +900,30 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
|||||||
drain->engine.render(ch0, ch1, static_cast<std::size_t>(frames));
|
drain->engine.render(ch0, ch1, static_cast<std::size_t>(frames));
|
||||||
drain->preview.render(ch0, ch1, static_cast<std::size_t>(frames));
|
drain->preview.render(ch0, ch1, static_cast<std::size_t>(frames));
|
||||||
}
|
}
|
||||||
|
// FB1 post-mixer master gain: ramp gainCurrent_ toward the atomic target per-sample so
|
||||||
|
// continuous knob drags produce no zipper noise and the true-zero bottom causes no click.
|
||||||
|
// Applied AFTER the voice sum and BEFORE the extra-channel mirror + peak so both see the
|
||||||
|
// actual output. Branch-free inner loop; early-out when already at target. RT-safe.
|
||||||
|
{
|
||||||
|
const float gTarget = masterGain_.load(std::memory_order_relaxed);
|
||||||
|
const float diff = gTarget - gainCurrent_;
|
||||||
|
if (diff < -kGainRampSnap || diff > kGainRampSnap) {
|
||||||
|
// Ramp toward target: step per sample, then apply the per-sample gain.
|
||||||
|
for (int32 i = 0; i < frames; ++i) {
|
||||||
|
const float d = gTarget - gainCurrent_;
|
||||||
|
if (d > kGainRampRate) gainCurrent_ += kGainRampRate;
|
||||||
|
else if (d < -kGainRampRate) gainCurrent_ -= kGainRampRate;
|
||||||
|
else gainCurrent_ = gTarget;
|
||||||
|
ch0[i] *= gainCurrent_;
|
||||||
|
ch1[i] *= gainCurrent_;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gainCurrent_ = gTarget;
|
||||||
|
if (gTarget != 1.f) {
|
||||||
|
for (int32 i = 0; i < frames; ++i) { ch0[i] *= gTarget; ch1[i] *= gTarget; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Any channels beyond the first two mirror ch0 (defensive — REAPER negotiates 1 or 2).
|
// Any 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]) {
|
||||||
@@ -904,6 +951,26 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
|||||||
drain->engine.render(ch0, static_cast<std::size_t>(frames));
|
drain->engine.render(ch0, static_cast<std::size_t>(frames));
|
||||||
drain->preview.render(ch0, static_cast<std::size_t>(frames));
|
drain->preview.render(ch0, static_cast<std::size_t>(frames));
|
||||||
}
|
}
|
||||||
|
// FB1 post-mixer master gain (mono path) — same ramp contract as the stereo branch:
|
||||||
|
// post-sum, pre-peak/replicate, per-sample gainCurrent_ ramp toward target, RT-safe.
|
||||||
|
{
|
||||||
|
const float gTarget = masterGain_.load(std::memory_order_relaxed);
|
||||||
|
const float diff = gTarget - gainCurrent_;
|
||||||
|
if (diff < -kGainRampSnap || diff > kGainRampSnap) {
|
||||||
|
for (int32 i = 0; i < frames; ++i) {
|
||||||
|
const float d = gTarget - gainCurrent_;
|
||||||
|
if (d > kGainRampRate) gainCurrent_ += kGainRampRate;
|
||||||
|
else if (d < -kGainRampRate) gainCurrent_ -= kGainRampRate;
|
||||||
|
else gainCurrent_ = gTarget;
|
||||||
|
ch0[i] *= gainCurrent_;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gainCurrent_ = gTarget;
|
||||||
|
if (gTarget != 1.f) {
|
||||||
|
for (int32 i = 0; i < frames; ++i) ch0[i] *= gTarget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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];
|
||||||
|
|||||||
@@ -210,6 +210,17 @@ public:
|
|||||||
MonoTrigger monoTrigger();
|
MonoTrigger monoTrigger();
|
||||||
void setMonoTrigger(MonoTrigger trigger);
|
void setMonoTrigger(MonoTrigger trigger);
|
||||||
|
|
||||||
|
// --- FB1 post-mixer master gain (per-instance, persisted in component state v8) ---------
|
||||||
|
// LINEAR gain in [0, masterGainMaxLinear()] (0.0 = -inf/true silence, 1.0 = unity, cap =
|
||||||
|
// +24 dB; the pure master_gain module owns the dB knob taper). Held in an atomic so the
|
||||||
|
// audio thread applies it with ONE relaxed load per block as a post-sum multiply over the
|
||||||
|
// rendered output (engine + drain + preview) — no lock, no rebuild, no per-voice cost.
|
||||||
|
// Written by the editor's Gain knob (UI thread) and setState; read by getState + process().
|
||||||
|
double masterGainLinear() const {
|
||||||
|
return static_cast<double>(masterGain_.load(std::memory_order_relaxed));
|
||||||
|
}
|
||||||
|
void setMasterGainLinear(double linear); // clamped to [0, masterGainMaxLinear()]
|
||||||
|
|
||||||
// Fire a one-shot PREVIEW note-on / note-off through the live instrument's PREVIEW CARD
|
// Fire a one-shot PREVIEW note-on / note-off through the live instrument's PREVIEW CARD
|
||||||
// (S-VIEW-4; Phase S isolation) — a dedicated single voice structurally OUTSIDE the MIDI
|
// (S-VIEW-4; Phase S isolation) — a dedicated single voice structurally OUTSIDE the MIDI
|
||||||
// pool, so a full pool never drops a preview and a preview never steals a playing voice.
|
// pool, so a full pool never drops a preview and a preview never steals a playing voice.
|
||||||
@@ -355,6 +366,16 @@ private:
|
|||||||
VoiceMode voiceMode_ = VoiceMode::Poly;
|
VoiceMode voiceMode_ = VoiceMode::Poly;
|
||||||
MonoTrigger monoTrigger_ = MonoTrigger::Retrigger;
|
MonoTrigger monoTrigger_ = MonoTrigger::Retrigger;
|
||||||
|
|
||||||
|
// FB1 post-mixer master gain (LINEAR; persisted in component state v8). A lock-free
|
||||||
|
// atomic — the target the UI thread writes; the audio thread ramps gainCurrent_ toward
|
||||||
|
// it per-sample each block (linear interpolation, ~20 ms at 48 kHz / 256-frame block)
|
||||||
|
// so sudden knob moves produce no zipper noise and the true-zero bottom causes no click.
|
||||||
|
std::atomic<float> masterGain_{1.0f};
|
||||||
|
// The audio-thread running gain value: tracks masterGain_ across blocks, stepping at
|
||||||
|
// most kGainRampRate per sample toward the target. Starts at unity (pre-FB1 default).
|
||||||
|
// Written and read exclusively on the audio thread — no atomics needed.
|
||||||
|
float gainCurrent_ = 1.0f;
|
||||||
|
|
||||||
// --- S-VIEW-4 preview-trigger mailbox (off-thread -> audio thread, lock-free) ---------
|
// --- S-VIEW-4 preview-trigger mailbox (off-thread -> audio thread, lock-free) ---------
|
||||||
// The editor's preview-trigger button posts a note-on/off request from the UI thread; process()
|
// The editor's preview-trigger button posts a note-on/off request from the UI thread; process()
|
||||||
// drains it at block start and drives the live instrument's PREVIEW CARD (Phase S — never the
|
// drains it at block start and drives the live instrument's PREVIEW CARD (Phase S — never the
|
||||||
|
|||||||
+30
-3
@@ -5,9 +5,12 @@
|
|||||||
|
|
||||||
#include <algorithm> // std::min
|
#include <algorithm> // std::min
|
||||||
#include <cassert> // assert
|
#include <cassert> // assert
|
||||||
|
#include <cmath> // std::isfinite (v8 master-gain validation)
|
||||||
#include <cstring> // std::memcpy
|
#include <cstring> // std::memcpy
|
||||||
#include <utility> // std::move
|
#include <utility> // std::move
|
||||||
|
|
||||||
|
#include "master_gain.h" // masterGainMaxLinear — the v8 master-gain wire cap
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -615,6 +618,17 @@ std::vector<std::uint8_t> serializeComponentState(const ComponentState& state) {
|
|||||||
out.push_back(static_cast<std::uint8_t>(vc));
|
out.push_back(static_cast<std::uint8_t>(vc));
|
||||||
out.push_back(state.voiceMode == VoiceMode::Mono ? 1 : 0);
|
out.push_back(state.voiceMode == VoiceMode::Mono ? 1 : 0);
|
||||||
out.push_back(state.monoTrigger == MonoTrigger::Legato ? 1 : 0);
|
out.push_back(state.monoTrigger == MonoTrigger::Legato ? 1 : 0);
|
||||||
|
// v8 envelope addition (FB1 master gain): the post-mixer LINEAR gain as an IEEE-754 double
|
||||||
|
// (bit-cast to u64 LE), following the voice bytes so a v7 blob is a strict prefix up to
|
||||||
|
// here (see the v7 lift). The WRITER never emits an out-of-range value: non-finite or
|
||||||
|
// negative falls back to unity; above the +24 dB cap clamps to the cap.
|
||||||
|
{
|
||||||
|
double g = state.masterGainLinear;
|
||||||
|
const double maxLin = vst::masterGainMaxLinear();
|
||||||
|
if (!std::isfinite(g) || g < 0.0) g = 1.0;
|
||||||
|
if (g > maxLin) g = maxLin;
|
||||||
|
putU64le(out, doubleToBits(g));
|
||||||
|
}
|
||||||
// Length-prefixed selection id (it precedes the zones payload, so it MUST be framed —
|
// Length-prefixed selection id (it precedes the zones payload, so it MUST be framed —
|
||||||
// unlike the v1 selection blob where the id ran to end-of-stream).
|
// unlike the v1 selection blob where the id ran to end-of-stream).
|
||||||
putU32le(out, static_cast<std::uint32_t>(state.selectionId.size()));
|
putU32le(out, static_cast<std::uint32_t>(state.selectionId.size()));
|
||||||
@@ -691,11 +705,12 @@ ComponentState deserializeComponentState(const std::vector<std::uint8_t>& bytes,
|
|||||||
return out; // previewVelocity stays at the mid default (pre-S-VIEW-4)
|
return out; // previewVelocity stays at the mid default (pre-S-VIEW-4)
|
||||||
}
|
}
|
||||||
if (version != kComponentStateVersion &&
|
if (version != kComponentStateVersion &&
|
||||||
|
version != kSelectionZonesModeMarkerVelVoiceV7Version &&
|
||||||
version != kSelectionZonesModeMarkerVelV6Version) {
|
version != kSelectionZonesModeMarkerVelV6Version) {
|
||||||
return out; // unknown -> empty
|
return out; // unknown -> empty
|
||||||
}
|
}
|
||||||
|
|
||||||
// v6/v7 shared prefix: the channel-mode byte, then the 8-byte consumed-assignment marker,
|
// v6/v7/v8 shared prefix: the channel-mode byte, then the 8-byte consumed-assignment marker,
|
||||||
// then the 1-byte preview velocity, precede the v3 body. A non-{0,1} mode byte is treated
|
// then the 1-byte preview velocity, precede the v3 body. A non-{0,1} mode byte is treated
|
||||||
// as mono (conservative default) rather than rejected — a corrupt mode never silences the
|
// as mono (conservative default) rather than rejected — a corrupt mode never silences the
|
||||||
// instance.
|
// instance.
|
||||||
@@ -711,9 +726,9 @@ ComponentState deserializeComponentState(const std::vector<std::uint8_t>& bytes,
|
|||||||
out.previewVelocity = (previewVel >= 1 && previewVel <= 127)
|
out.previewVelocity = (previewVel >= 1 && previewVel <= 127)
|
||||||
? previewVel
|
? previewVel
|
||||||
: kPreviewVelocityDefault;
|
: kPreviewVelocityDefault;
|
||||||
// v7 (Phase S): the three voice-system bytes. A v6 blob (pre-Phase-S) skips them — the
|
// v7+ (Phase S): the three voice-system bytes. A v6 blob (pre-Phase-S) skips them — the
|
||||||
// construction defaults {16, Poly, Retrigger} hold, reproducing pre-Phase-S behavior.
|
// construction defaults {16, Poly, Retrigger} hold, reproducing pre-Phase-S behavior.
|
||||||
if (version == kComponentStateVersion) {
|
if (version >= kSelectionZonesModeMarkerVelVoiceV7Version) {
|
||||||
const std::uint8_t vc = r.u8();
|
const std::uint8_t vc = r.u8();
|
||||||
const std::uint8_t vm = r.u8();
|
const std::uint8_t vm = r.u8();
|
||||||
const std::uint8_t mt = r.u8();
|
const std::uint8_t mt = r.u8();
|
||||||
@@ -726,6 +741,18 @@ ComponentState deserializeComponentState(const std::vector<std::uint8_t>& bytes,
|
|||||||
out.voiceMode = (vm == 1) ? VoiceMode::Mono : VoiceMode::Poly;
|
out.voiceMode = (vm == 1) ? VoiceMode::Mono : VoiceMode::Poly;
|
||||||
out.monoTrigger = (mt == 1) ? MonoTrigger::Legato : MonoTrigger::Retrigger;
|
out.monoTrigger = (mt == 1) ? MonoTrigger::Legato : MonoTrigger::Retrigger;
|
||||||
}
|
}
|
||||||
|
// v8 (FB1): the master-gain LINEAR double. A v7 blob (pre-FB1) skips it — the construction
|
||||||
|
// default (unity) holds, reproducing pre-FB1 output exactly. A non-finite, negative, or
|
||||||
|
// above-cap value (a corrupt blob) falls back to unity rather than silencing/blasting.
|
||||||
|
if (version == kComponentStateVersion) {
|
||||||
|
const double g = bitsToDouble(r.u64());
|
||||||
|
if (!r.ok) return out; // truncated inside the gain double — unity holds (out already
|
||||||
|
// carries mode/marker/velocity/voice fields from above)
|
||||||
|
out.masterGainLinear =
|
||||||
|
(std::isfinite(g) && g >= 0.0 && g <= vst::masterGainMaxLinear() * (1.0 + 1e-9))
|
||||||
|
? g
|
||||||
|
: 1.0;
|
||||||
|
}
|
||||||
const std::uint32_t idLen = r.u32();
|
const std::uint32_t idLen = r.u32();
|
||||||
out.selectionId = r.str(idLen);
|
out.selectionId = r.str(idLen);
|
||||||
if (!r.ok) { out.selectionId.clear(); return out; } // truncated id -> empty
|
if (!r.ok) { out.selectionId.clear(); return out; } // truncated id -> empty
|
||||||
|
|||||||
+29
-14
@@ -447,22 +447,26 @@ PerformanceMap deserializePerformance(const std::vector<std::uint8_t>& bytes,
|
|||||||
// instance with NO pick and NO zones restores EMPTY (silence + the "pick a capture" empty
|
// instance with NO pick and NO zones restores EMPTY (silence + the "pick a capture" empty
|
||||||
// state), never auto-playing sample #1.
|
// state), never auto-playing sample #1.
|
||||||
//
|
//
|
||||||
// Format (envelope v7): 4-byte LE version tag (== 7), then a 1-byte channel-mode field (0 = mono,
|
// Format (envelope v8): 4-byte LE version tag (== 8), then a 1-byte channel-mode field (0 = mono,
|
||||||
// 1 = stereo), then an 8-byte LE last-consumed-assignment generation (S8/S9 reader marker), then a
|
// 1 = stereo), then an 8-byte LE last-consumed-assignment generation (S8/S9 reader marker), then a
|
||||||
// 1-byte preview-trigger velocity (S-VIEW-4, MIDI 1..127), then the THREE Phase-S voice-system
|
// 1-byte preview-trigger velocity (S-VIEW-4, MIDI 1..127), then the THREE Phase-S voice-system
|
||||||
// bytes: a 1-byte voice count (1..32), a 1-byte voice mode (0 = Poly, 1 = Mono), a 1-byte mono
|
// bytes: a 1-byte voice count (1..32), a 1-byte voice mode (0 = Poly, 1 = Mono), a 1-byte mono
|
||||||
// trigger (0 = Retrigger, 1 = Legato), then a 4-byte LE selection-id length + id bytes, then the
|
// trigger (0 = Retrigger, 1 = Legato), then the FB1 8-byte LE master-gain LINEAR value (IEEE-754
|
||||||
// CURRENT zones payload (identical to serializePerformance's body — its own self-describing
|
// double, bit-cast; 0.0 = -inf/silence, 1.0 = unity, cap ~15.849 = +24 dB), then a 4-byte LE
|
||||||
// version, see the ZONES-PAYLOAD block). The three voice bytes are the ONLY envelope-v7 addition
|
// selection-id length + id bytes, then the CURRENT zones payload (identical to
|
||||||
// over envelope-v6 — the envelope grew fields, the zones payload is untouched (a PARALLEL track
|
// serializePerformance's body — its own self-describing version, see the ZONES-PAYLOAD block).
|
||||||
// owns zone-record extension under its own versioning; the two version numbers are independent
|
// The master-gain double is the ONLY envelope-v8 addition over v7 — the envelope grew a field,
|
||||||
// axes — do NOT bump the zones-payload version for an envelope field). An out-of-range voice
|
// the zones payload is untouched (a PARALLEL track owns zone-record extension under its own
|
||||||
// byte (a corrupt blob) falls back to the field's default rather than silencing the instance
|
// versioning; the two version numbers are independent axes — do NOT bump the zones-payload
|
||||||
// (the previewVelocity precedent). BACK-COMPAT on read (every older blob lifts to channelMode =
|
// version for an envelope field). An out-of-range voice byte or a non-finite/out-of-range
|
||||||
// MONO, lastConsumedAssignGeneration = 0, previewVelocity = kPreviewVelocityDefault, and the
|
// master-gain double (a corrupt blob) falls back to the field's default rather than silencing
|
||||||
// Phase-S voice defaults {16 voices, Poly, Retrigger} — which reproduce pre-Phase-S behavior
|
// the instance (the previewVelocity precedent). BACK-COMPAT on read (every older blob lifts to
|
||||||
// exactly — preserving current behavior for already-saved instances):
|
// channelMode = MONO, lastConsumedAssignGeneration = 0, previewVelocity =
|
||||||
// * v7 blob -> {channelMode, marker, previewVelocity, voiceCount, voiceMode, monoTrigger, selectionId, zones} direct.
|
// kPreviewVelocityDefault, the Phase-S voice defaults {16 voices, Poly, Retrigger}, and unity
|
||||||
|
// master gain — which reproduce pre-v8 behavior exactly — preserving current behavior for
|
||||||
|
// already-saved instances):
|
||||||
|
// * v8 blob -> {channelMode, marker, previewVelocity, voice bytes, masterGainLinear, selectionId, zones} direct.
|
||||||
|
// * v7 blob -> {channelMode, marker, previewVelocity, voiceCount, voiceMode, monoTrigger, selectionId, zones}: pre-FB1 (unity master gain).
|
||||||
// * v6 blob -> {channelMode, marker, previewVelocity, selectionId, zones}: pre-Phase-S (voice defaults).
|
// * v6 blob -> {channelMode, marker, previewVelocity, selectionId, zones}: pre-Phase-S (voice defaults).
|
||||||
// * v5 blob -> {channelMode, lastConsumedAssignGeneration, mid, selectionId, zones}: pre-S-VIEW-4 (no velocity).
|
// * v5 blob -> {channelMode, lastConsumedAssignGeneration, mid, selectionId, zones}: pre-S-VIEW-4 (no velocity).
|
||||||
// * v4 blob -> {channelMode, 0, mid, selectionId, zones}: pre-S8/S9 reader (no marker).
|
// * v4 blob -> {channelMode, 0, mid, selectionId, zones}: pre-S8/S9 reader (no marker).
|
||||||
@@ -497,9 +501,20 @@ struct ComponentState {
|
|||||||
int voiceCount = kDefaultVoiceCount; // polyphony bound, kMinVoiceCount..kMaxVoiceCount
|
int voiceCount = kDefaultVoiceCount; // polyphony bound, kMinVoiceCount..kMaxVoiceCount
|
||||||
VoiceMode voiceMode = VoiceMode::Poly; // Poly | Mono (last-note-priority held stack)
|
VoiceMode voiceMode = VoiceMode::Poly; // Poly | Mono (last-note-priority held stack)
|
||||||
MonoTrigger monoTrigger = MonoTrigger::Retrigger; // mono takeover: Retrigger | Legato
|
MonoTrigger monoTrigger = MonoTrigger::Retrigger; // mono takeover: Retrigger | Legato
|
||||||
|
// FB1 (Wave B) post-mixer master gain, stored LINEAR (0.0 = -inf/true silence; 1.0 = unity;
|
||||||
|
// up to ~15.849 = +24 dB — the master_gain module owns the dB taper). PER-INSTANCE output
|
||||||
|
// trim applied by process() AFTER the voice sum (engine + drain + preview) — never per
|
||||||
|
// voice, never a keymap fact. Default unity reproduces pre-FB1 output byte-identically,
|
||||||
|
// so an older blob lifting to 1.0 plays exactly as it did.
|
||||||
|
double masterGainLinear = 1.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline constexpr std::uint32_t kComponentStateVersion = 7;
|
inline constexpr std::uint32_t kComponentStateVersion = 8;
|
||||||
|
|
||||||
|
// The pre-FB1 combined-state version (selection + zones + channel mode + consumed marker +
|
||||||
|
// preview velocity + voice system, no master gain). Retained so deserializeComponentState can
|
||||||
|
// lift a v7 blob to unity master gain.
|
||||||
|
inline constexpr std::uint32_t kSelectionZonesModeMarkerVelVoiceV7Version = 7;
|
||||||
|
|
||||||
// The pre-Phase-S combined-state version (selection + zones + channel mode + consumed marker +
|
// The pre-Phase-S combined-state version (selection + zones + channel mode + consumed marker +
|
||||||
// preview velocity, no voice-system fields). Retained so deserializeComponentState can lift a
|
// preview velocity, no voice-system fields). Retained so deserializeComponentState can lift a
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// Standalone tests for reasampler::vst::curve_popup — no VST3, no REAPER, no framework. Same
|
||||||
|
// fast assert loop as the sibling pure tests. Assert the r11 popup-sheet geometry at the size
|
||||||
|
// clamps (the spec's width clamp(60%, 360..520) / height clamp(55%, 260..380)), the centering,
|
||||||
|
// the title-row/close-button placement, the curve-box remainder, and the outside-sheet
|
||||||
|
// dismissal test.
|
||||||
|
|
||||||
|
#include "../src/vst/curve_popup.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
using namespace reasampler::vst;
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
static void testDefaultWindowMidClamp() {
|
||||||
|
// 840x620: 60% = 504 (inside 360..520), 55% = 341 (inside 260..380).
|
||||||
|
const CurvePopupLayout pl = computeCurvePopup(840, 620);
|
||||||
|
CHECK(pl.sheet.width() == 504);
|
||||||
|
CHECK(pl.sheet.height() == 341);
|
||||||
|
// Centered (within the integer-division pixel).
|
||||||
|
CHECK(pl.sheet.left == (840 - 504) / 2);
|
||||||
|
CHECK(pl.sheet.top == (620 - 341) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testMinClamp() {
|
||||||
|
// The 560x460 constraint floor: 60% = 336 -> clamps UP to 360; 55% = 253 -> up to 260.
|
||||||
|
const CurvePopupLayout pl = computeCurvePopup(560, 460);
|
||||||
|
CHECK(pl.sheet.width() == kCurvePopupMinW);
|
||||||
|
CHECK(pl.sheet.height() == kCurvePopupMinH);
|
||||||
|
CHECK(pl.sheet.left >= 0 && pl.sheet.right <= 560);
|
||||||
|
CHECK(pl.sheet.top >= 0 && pl.sheet.bottom <= 460);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testMaxClamp() {
|
||||||
|
// A large window: 60% of 1600 = 960 -> clamps DOWN to 520; 55% of 900 = 495 -> down to 380.
|
||||||
|
const CurvePopupLayout pl = computeCurvePopup(1600, 900);
|
||||||
|
CHECK(pl.sheet.width() == kCurvePopupMaxW);
|
||||||
|
CHECK(pl.sheet.height() == kCurvePopupMaxH);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testDegenerateWindowNeverOverhangs() {
|
||||||
|
// A window smaller than the min clamp: the sheet caps at the window dimension (defensive —
|
||||||
|
// below checkSizeConstraint, but geometry must stay sane).
|
||||||
|
const CurvePopupLayout pl = computeCurvePopup(300, 200);
|
||||||
|
CHECK(pl.sheet.width() == 300);
|
||||||
|
CHECK(pl.sheet.height() == 200);
|
||||||
|
CHECK(pl.sheet.left == 0 && pl.sheet.top == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testTitleRowAndCurveBox() {
|
||||||
|
const CurvePopupLayout pl = computeCurvePopup(840, 620);
|
||||||
|
// Close: 18x18, right-anchored inside the title row.
|
||||||
|
CHECK(pl.close.width() == kCurvePopupCloseSize && pl.close.height() == kCurvePopupCloseSize);
|
||||||
|
CHECK(pl.close.right == pl.sheet.right - kCurvePopupPad);
|
||||||
|
CHECK(pl.close.top >= pl.sheet.top);
|
||||||
|
CHECK(pl.close.bottom <= pl.sheet.top + kCurvePopupTitleH);
|
||||||
|
// Title text: left of the close button, in the title row.
|
||||||
|
CHECK(pl.title.left == pl.sheet.left + kCurvePopupPad);
|
||||||
|
CHECK(pl.title.right <= pl.close.left);
|
||||||
|
// Curve box: fills the remainder below the title row, inside the sheet margins.
|
||||||
|
CHECK(pl.curveBox.top >= pl.sheet.top + kCurvePopupTitleH);
|
||||||
|
CHECK(pl.curveBox.left == pl.sheet.left + kCurvePopupPad);
|
||||||
|
CHECK(pl.curveBox.right == pl.sheet.right - kCurvePopupPad);
|
||||||
|
CHECK(pl.curveBox.bottom == pl.sheet.bottom - kCurvePopupPad);
|
||||||
|
CHECK(pl.curveBox.width() > 0 && pl.curveBox.height() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testOutsideSheetDismissTest() {
|
||||||
|
const CurvePopupLayout pl = computeCurvePopup(840, 620);
|
||||||
|
// On the wash: outside.
|
||||||
|
CHECK(popupOutsideSheet(pl, 0, 0));
|
||||||
|
CHECK(popupOutsideSheet(pl, pl.sheet.left - 1, pl.sheet.top + 10));
|
||||||
|
CHECK(popupOutsideSheet(pl, pl.sheet.right, pl.sheet.top + 10)); // half-open right edge
|
||||||
|
// On the sheet (title row, curve box, padding): inside.
|
||||||
|
CHECK(!popupOutsideSheet(pl, pl.sheet.left, pl.sheet.top));
|
||||||
|
CHECK(!popupOutsideSheet(pl, pl.curveBox.left + 5, pl.curveBox.top + 5));
|
||||||
|
CHECK(!popupOutsideSheet(pl, pl.sheet.right - 1, pl.sheet.bottom - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testDefaultWindowMidClamp();
|
||||||
|
testMinClamp();
|
||||||
|
testMaxClamp();
|
||||||
|
testDegenerateWindowNeverOverhangs();
|
||||||
|
testTitleRowAndCurveBox();
|
||||||
|
testOutsideSheetDismissTest();
|
||||||
|
if (g_fail) {
|
||||||
|
std::printf("%d FAILURE(S)\n", g_fail);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::printf("curve_popup tests passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
// Standalone tests for reasampler::vst::knob_deck — no VST3, no REAPER, no framework. Same fast
|
||||||
|
// assert loop as the sibling pure tests. Assert the r11 deck layout HARD:
|
||||||
|
//
|
||||||
|
// * group width — caption row vs knob row max + padding; row-toggle and caption-toggle widths.
|
||||||
|
// * layout — caption toggle right-anchored IN the caption row; cells fixed 48x58 left-to-right
|
||||||
|
// inside the box; knob square centered; label band beneath; row toggle after the cells.
|
||||||
|
// * wrap — deterministic whole-group wrap at a narrowing width (the r11 "PITCH ENV onto row
|
||||||
|
// two at the 560 floor" behavior); the first group of a row always places; deckHeight
|
||||||
|
// consistency with deckRowCount.
|
||||||
|
// * hit-test — knob cell hit (whole cell), toggle segment 0/1 boundaries, blank (-1) cells
|
||||||
|
// and fence padding miss, outside-deck miss.
|
||||||
|
|
||||||
|
#include "../src/vst/knob_deck.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace reasampler::vst;
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
// A representative deck shaped like the shell's: AMP (5 cells + caption toggle), PITCH
|
||||||
|
// (1 cell + caption toggle), PITCH ENV (3 cells + caption toggle), VOICE (1 cell + caption
|
||||||
|
// toggle + row toggle), MASTER (1 cell, no toggle).
|
||||||
|
static std::vector<DeckGroupDesc> shellLikeDeck() {
|
||||||
|
std::vector<DeckGroupDesc> g;
|
||||||
|
g.push_back({0, 78, {100, 44}, {1, 2, 3, 4, 5}, {}});
|
||||||
|
g.push_back({1, 38, {101, 48}, {6}, {}});
|
||||||
|
g.push_back({2, 58, {102, 32}, {7, 8, 9}, {}});
|
||||||
|
g.push_back({3, 38, {103, 40}, {10}, {104, 44}});
|
||||||
|
g.push_back({4, 46, {}, {11}, {}});
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testGroupWidth() {
|
||||||
|
// Knob row dominates: 5 cells (240) > caption row (78 + 4 + 88 = 170) -> 240 + 2*6.
|
||||||
|
DeckGroupDesc amp{0, 78, {100, 44}, {1, 2, 3, 4, 5}, {}};
|
||||||
|
CHECK(deckGroupWidth(amp) == 5 * kDeckCellW + 2 * kDeckGroupPadX);
|
||||||
|
// Caption row dominates: 38 + 4 + 96 = 138 > 48 -> 138 + 12.
|
||||||
|
DeckGroupDesc pitch{1, 38, {101, 48}, {6}, {}};
|
||||||
|
CHECK(deckGroupWidth(pitch) == 38 + kDeckToggleGap + 2 * 48 + 2 * kDeckGroupPadX);
|
||||||
|
// Row toggle counts into the knob row: 48 + 4 + 88 = 140 > caption 38+4+80=122.
|
||||||
|
DeckGroupDesc voice{3, 38, {103, 40}, {10}, {104, 44}};
|
||||||
|
CHECK(deckGroupWidth(voice) ==
|
||||||
|
kDeckCellW + kDeckToggleGap + 2 * 44 + 2 * kDeckGroupPadX);
|
||||||
|
// No toggles: max(caption, cells) + padding.
|
||||||
|
DeckGroupDesc master{4, 46, {}, {11}, {}};
|
||||||
|
CHECK(deckGroupWidth(master) == kDeckCellW + 2 * kDeckGroupPadX);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testShellDeckFitsOneRowAtDefaultWidth() {
|
||||||
|
// The r11 default window is 840 with kPad=8 margins -> 824 available. The five shell
|
||||||
|
// groups must fit ONE deck row there (the elastic hero keeps ~430px — the layout spec's
|
||||||
|
// premise). Locks the constants against accidental growth.
|
||||||
|
const auto deck = shellLikeDeck();
|
||||||
|
int total = 0;
|
||||||
|
for (const auto& g : deck) total += deckGroupWidth(g);
|
||||||
|
total += (static_cast<int>(deck.size()) - 1) * kDeckGroupGap;
|
||||||
|
CHECK(total <= 824);
|
||||||
|
CHECK(deckRowCount(deck, 824) == 1);
|
||||||
|
CHECK(deckHeight(deck, 824) == kDeckGroupH);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testWrapAtNarrowWidthIsDeterministic() {
|
||||||
|
// At the 560x460 checkSizeConstraint floor (544 available) the deck wraps to TWO rows,
|
||||||
|
// whole trailing groups only.
|
||||||
|
const auto deck = shellLikeDeck();
|
||||||
|
CHECK(deckRowCount(deck, 544) == 2);
|
||||||
|
CHECK(deckHeight(deck, 544) == 2 * kDeckGroupH + kDeckRowGap);
|
||||||
|
const DeckLayout dl = layoutDeck(deck, 8, 100, 544);
|
||||||
|
CHECK(dl.rowCount == 2);
|
||||||
|
CHECK(dl.height == deckHeight(deck, 544));
|
||||||
|
CHECK(dl.groups.size() == 5);
|
||||||
|
// Row membership: groups on row 1 share the first top; the wrapped groups sit one row
|
||||||
|
// pitch lower and restart at the left margin.
|
||||||
|
const int row0Top = dl.groups[0].box.top;
|
||||||
|
const int row1Top = row0Top + kDeckGroupH + kDeckRowGap;
|
||||||
|
CHECK(dl.groups[0].box.top == row0Top);
|
||||||
|
CHECK(dl.groups[1].box.top == row0Top);
|
||||||
|
bool sawWrap = false;
|
||||||
|
for (std::size_t i = 1; i < dl.groups.size(); ++i) {
|
||||||
|
if (dl.groups[i].box.top == row1Top && dl.groups[i - 1].box.top == row0Top) {
|
||||||
|
CHECK(dl.groups[i].box.left == 8); // wrapped row restarts at the left edge
|
||||||
|
sawWrap = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CHECK(sawWrap);
|
||||||
|
// Every box stays within the available width (no group straddles the right edge).
|
||||||
|
for (const auto& g : dl.groups) CHECK(g.box.right <= 8 + 544);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testFirstGroupAlwaysPlaces() {
|
||||||
|
// A group wider than the row still places (degenerate width) — exactly one row per group.
|
||||||
|
const auto deck = shellLikeDeck();
|
||||||
|
CHECK(deckRowCount(deck, 100) == 5);
|
||||||
|
CHECK(deckHeight(deck, 100) == 5 * kDeckGroupH + 4 * kDeckRowGap);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testGroupInnerGeometry() {
|
||||||
|
const auto deck = shellLikeDeck();
|
||||||
|
const DeckLayout dl = layoutDeck(deck, 8, 50, 824);
|
||||||
|
const DeckGroupLayout& amp = dl.groups[0];
|
||||||
|
// Caption row at the top padding; caption toggle right-anchored inside the box.
|
||||||
|
CHECK(amp.caption.top == amp.box.top + kDeckGroupPadY);
|
||||||
|
CHECK(amp.captionToggle.id == 100);
|
||||||
|
CHECK(amp.captionToggle.seg1.right == amp.box.right - kDeckGroupPadX);
|
||||||
|
CHECK(amp.captionToggle.seg0.right == amp.captionToggle.seg1.left);
|
||||||
|
CHECK(amp.captionToggle.seg0.width() == 44 && amp.captionToggle.seg1.width() == 44);
|
||||||
|
CHECK(amp.captionToggle.seg0.height() == kDeckToggleH);
|
||||||
|
// The caption text rect stops before the toggle.
|
||||||
|
CHECK(amp.caption.right <= amp.captionToggle.seg0.left);
|
||||||
|
// Cells: five, fixed size, abutting, inside the box, below the caption row.
|
||||||
|
CHECK(static_cast<int>(amp.cells.size()) == 5);
|
||||||
|
for (std::size_t i = 0; i < amp.cells.size(); ++i) {
|
||||||
|
const DeckCellLayout& c = amp.cells[i];
|
||||||
|
CHECK(c.cell.width() == kDeckCellW && c.cell.height() == kDeckCellH);
|
||||||
|
CHECK(c.cell.top == amp.box.top + kDeckGroupPadY + kDeckCaptionH + kDeckCaptionGap);
|
||||||
|
if (i > 0) CHECK(c.cell.left == amp.cells[i - 1].cell.right);
|
||||||
|
// Knob square centered horizontally, label band beneath it, both inside the cell.
|
||||||
|
CHECK(c.knob.width() == kDeckKnobSize && c.knob.height() == kDeckKnobSize);
|
||||||
|
CHECK(c.knob.left - c.cell.left == c.cell.right - c.knob.right);
|
||||||
|
CHECK(c.label.top >= c.knob.bottom);
|
||||||
|
CHECK(c.label.bottom <= c.cell.bottom);
|
||||||
|
}
|
||||||
|
// VOICE group's row toggle sits after its cell, vertically centered in the cell row.
|
||||||
|
const DeckGroupLayout& voice = dl.groups[3];
|
||||||
|
CHECK(voice.rowToggle.id == 104);
|
||||||
|
CHECK(voice.rowToggle.seg0.left == voice.cells[0].cell.right + kDeckToggleGap);
|
||||||
|
CHECK(voice.rowToggle.seg0.height() == kDeckToggleH);
|
||||||
|
CHECK(voice.rowToggle.seg0.top > voice.cells[0].cell.top);
|
||||||
|
// MASTER has no toggles.
|
||||||
|
CHECK(dl.groups[4].captionToggle.id == -1);
|
||||||
|
CHECK(dl.groups[4].rowToggle.id == -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testHitTest() {
|
||||||
|
const auto deck = shellLikeDeck();
|
||||||
|
const DeckLayout dl = layoutDeck(deck, 8, 50, 824);
|
||||||
|
const DeckGroupLayout& amp = dl.groups[0];
|
||||||
|
|
||||||
|
// Knob hit: anywhere in the cell (including the label band) resolves to the cell id.
|
||||||
|
const DeckCellLayout& c0 = amp.cells[0];
|
||||||
|
DeckHit h = hitTestDeck(dl, c0.cell.left + 1, c0.cell.top + 1);
|
||||||
|
CHECK(h.kind == DeckHitKind::Knob && h.id == 1 && h.segment == -1);
|
||||||
|
h = hitTestDeck(dl, c0.label.left + 2, c0.label.top + 2);
|
||||||
|
CHECK(h.kind == DeckHitKind::Knob && h.id == 1);
|
||||||
|
|
||||||
|
// Caption toggle segments 0/1 at their boundary: last px of seg0, first px of seg1.
|
||||||
|
h = hitTestDeck(dl, amp.captionToggle.seg0.right - 1, amp.captionToggle.seg0.top + 1);
|
||||||
|
CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 100 && h.segment == 0);
|
||||||
|
h = hitTestDeck(dl, amp.captionToggle.seg1.left, amp.captionToggle.seg1.top + 1);
|
||||||
|
CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 100 && h.segment == 1);
|
||||||
|
|
||||||
|
// Row toggle.
|
||||||
|
const DeckGroupLayout& voice = dl.groups[3];
|
||||||
|
h = hitTestDeck(dl, voice.rowToggle.seg1.left + 1, voice.rowToggle.seg1.top + 1);
|
||||||
|
CHECK(h.kind == DeckHitKind::RowToggle && h.id == 104 && h.segment == 1);
|
||||||
|
|
||||||
|
// A blank cell (id -1) misses even though its rect exists.
|
||||||
|
std::vector<DeckGroupDesc> trig;
|
||||||
|
trig.push_back({0, 78, {100, 44}, {20, 21, 22, -1, -1}, {}});
|
||||||
|
const DeckLayout tl = layoutDeck(trig, 0, 0, 824);
|
||||||
|
const DeckCellLayout& blank = tl.groups[0].cells[4];
|
||||||
|
CHECK(blank.id == -1);
|
||||||
|
h = hitTestDeck(tl, blank.cell.left + 5, blank.cell.top + 5);
|
||||||
|
CHECK(h.kind == DeckHitKind::None);
|
||||||
|
|
||||||
|
// The fence padding inside the box misses; outside the deck misses.
|
||||||
|
h = hitTestDeck(dl, amp.box.left + 1, amp.box.bottom - 1);
|
||||||
|
CHECK(h.kind == DeckHitKind::None);
|
||||||
|
h = hitTestDeck(dl, -50, -50);
|
||||||
|
CHECK(h.kind == DeckHitKind::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testEmptyDeck() {
|
||||||
|
const std::vector<DeckGroupDesc> none;
|
||||||
|
CHECK(deckRowCount(none, 800) == 0);
|
||||||
|
CHECK(deckHeight(none, 800) == 0);
|
||||||
|
const DeckLayout dl = layoutDeck(none, 0, 0, 800);
|
||||||
|
CHECK(dl.groups.empty() && dl.rowCount == 0 && dl.height == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testGroupWidth();
|
||||||
|
testShellDeckFitsOneRowAtDefaultWidth();
|
||||||
|
testWrapAtNarrowWidthIsDeterministic();
|
||||||
|
testFirstGroupAlwaysPlaces();
|
||||||
|
testGroupInnerGeometry();
|
||||||
|
testHitTest();
|
||||||
|
testEmptyDeck();
|
||||||
|
if (g_fail) {
|
||||||
|
std::printf("%d FAILURE(S)\n", g_fail);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::printf("knob_deck tests passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
// Standalone tests for reasampler::vst::master_gain — no VST3, no REAPER, no framework. Same
|
||||||
|
// fast assert loop as the sibling pure tests. Assert the FB1 post-mixer gain taper HARD:
|
||||||
|
//
|
||||||
|
// * -inf bottom — norm 0 maps to -infinity dB and TRUE ZERO linear (silence, not an epsilon);
|
||||||
|
// linear <= 0 maps back to norm 0.
|
||||||
|
// * endpoints — norm 1 = +24 dB = masterGainMaxLinear() (~15.849); the finite floor at -60 dB.
|
||||||
|
// * unity — 0 dB round-trips exactly through norm<->dB<->linear (the knob's unity detent
|
||||||
|
// position 60/84 of the travel).
|
||||||
|
// * monotonicity — more norm never means less gain.
|
||||||
|
// * label — "-inf" at the bottom, signed one-decimal dB elsewhere.
|
||||||
|
|
||||||
|
#include "../src/vst/master_gain.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
using namespace reasampler::vst;
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
static bool near(double a, double b, double eps = 1e-9) { return std::fabs(a - b) <= eps; }
|
||||||
|
|
||||||
|
static void testBottomIsTrueSilence() {
|
||||||
|
CHECK(std::isinf(masterGainDbFromNorm(0.0)) && masterGainDbFromNorm(0.0) < 0.0);
|
||||||
|
CHECK(masterGainLinearFromNorm(0.0) == 0.0); // exactly zero — the -inf contract
|
||||||
|
CHECK(masterGainNormFromLinear(0.0) == 0.0);
|
||||||
|
CHECK(masterGainNormFromLinear(-1.0) == 0.0);
|
||||||
|
CHECK(masterGainNormFromDb(-1e9) == 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testEndpoints() {
|
||||||
|
CHECK(near(masterGainDbFromNorm(1.0), kMasterGainMaxDb));
|
||||||
|
CHECK(near(masterGainLinearFromNorm(1.0), masterGainMaxLinear()));
|
||||||
|
CHECK(near(masterGainMaxLinear(), std::pow(10.0, 24.0 / 20.0)));
|
||||||
|
CHECK(near(masterGainNormFromDb(kMasterGainMaxDb), 1.0));
|
||||||
|
CHECK(near(masterGainNormFromLinear(masterGainMaxLinear()), 1.0));
|
||||||
|
// Out-of-range norms clamp.
|
||||||
|
CHECK(near(masterGainDbFromNorm(2.0), kMasterGainMaxDb));
|
||||||
|
CHECK(masterGainLinearFromNorm(-0.5) == 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testUnityRoundTrip() {
|
||||||
|
// 0 dB sits at norm -minDb/(maxDb-minDb) = 60/84 of the travel.
|
||||||
|
const double unityNorm = masterGainNormFromDb(0.0);
|
||||||
|
CHECK(near(unityNorm, 60.0 / 84.0, 1e-12));
|
||||||
|
CHECK(near(masterGainDbFromNorm(unityNorm), 0.0, 1e-9));
|
||||||
|
CHECK(near(masterGainLinearFromNorm(unityNorm), 1.0, 1e-9));
|
||||||
|
CHECK(near(masterGainNormFromLinear(1.0), unityNorm, 1e-9));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testNormLinearRoundTripAcrossTravel() {
|
||||||
|
for (int i = 1; i <= 20; ++i) {
|
||||||
|
const double norm = i / 20.0;
|
||||||
|
const double lin = masterGainLinearFromNorm(norm);
|
||||||
|
CHECK(lin > 0.0);
|
||||||
|
CHECK(near(masterGainNormFromLinear(lin), norm, 1e-9));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testMonotonic() {
|
||||||
|
double prev = -1.0;
|
||||||
|
for (int i = 0; i <= 100; ++i) {
|
||||||
|
const double lin = masterGainLinearFromNorm(i / 100.0);
|
||||||
|
CHECK(lin > prev); // strictly increasing over the whole travel (0 at the bottom)
|
||||||
|
prev = lin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testNonFiniteLinearClamps() {
|
||||||
|
CHECK(masterGainNormFromLinear(std::nan("")) == 0.0);
|
||||||
|
CHECK(near(masterGainNormFromLinear(1e9), 1.0)); // above the cap clamps to 1
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testBelowFloorCollapsesToBottom() {
|
||||||
|
// Any linear gain at or below the kMasterGainMinDb floor collapses to norm 0 (the -inf
|
||||||
|
// bottom detent). The floor IS the bottom of the finite sweep, so -61 dB, -80 dB, and
|
||||||
|
// a vanishingly small positive linear all map to the same place as true zero.
|
||||||
|
const double floorLinear = std::pow(10.0, kMasterGainMinDb / 20.0); // 10^(-60/20) = 0.001
|
||||||
|
CHECK(masterGainNormFromLinear(floorLinear) == 0.0); // exactly at the floor -> bottom
|
||||||
|
CHECK(masterGainNormFromLinear(floorLinear * 0.5) == 0.0); // below the floor -> bottom
|
||||||
|
CHECK(masterGainNormFromLinear(1e-9) == 0.0); // tiny positive -> bottom
|
||||||
|
// The dB path agrees: anything at or below kMasterGainMinDb maps to norm 0.
|
||||||
|
CHECK(masterGainNormFromDb(kMasterGainMinDb) == 0.0);
|
||||||
|
CHECK(masterGainNormFromDb(kMasterGainMinDb - 20.0) == 0.0); // -80 dB -> bottom
|
||||||
|
// Just ABOVE the floor (by epsilon) returns a non-zero norm.
|
||||||
|
CHECK(masterGainNormFromDb(kMasterGainMinDb + 1e-9) > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testLabels() {
|
||||||
|
char buf[24];
|
||||||
|
formatMasterGainLabel(0.0, buf, sizeof(buf));
|
||||||
|
CHECK(std::strcmp(buf, "-inf") == 0);
|
||||||
|
formatMasterGainLabel(1.0, buf, sizeof(buf));
|
||||||
|
CHECK(std::strcmp(buf, "+24.0dB") == 0);
|
||||||
|
formatMasterGainLabel(masterGainNormFromDb(0.0), buf, sizeof(buf));
|
||||||
|
CHECK(std::strcmp(buf, "+0.0dB") == 0);
|
||||||
|
formatMasterGainLabel(masterGainNormFromDb(-12.0), buf, sizeof(buf));
|
||||||
|
CHECK(std::strcmp(buf, "-12.0dB") == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testBottomIsTrueSilence();
|
||||||
|
testEndpoints();
|
||||||
|
testUnityRoundTrip();
|
||||||
|
testNormLinearRoundTripAcrossTravel();
|
||||||
|
testMonotonic();
|
||||||
|
testNonFiniteLinearClamps();
|
||||||
|
testBelowFloorCollapsesToBottom();
|
||||||
|
testLabels();
|
||||||
|
if (g_fail) {
|
||||||
|
std::printf("%d FAILURE(S)\n", g_fail);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::printf("master_gain tests passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
|
|
||||||
#include "../src/bank_book.h"
|
#include "../src/bank_book.h"
|
||||||
#include "../src/bank_model.h"
|
#include "../src/bank_model.h"
|
||||||
|
#include "../src/vst/master_gain.h" // masterGainMaxLinear (the v8 master-gain wire cap)
|
||||||
|
|
||||||
using namespace reasampler;
|
using namespace reasampler;
|
||||||
|
|
||||||
@@ -1220,6 +1221,105 @@ static void testComponentStateV7TruncatedVoiceBytes() {
|
|||||||
CHECK(back.selectionId.empty() && back.map.zones.empty());
|
CHECK(back.selectionId.empty() && back.map.zones.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- v8 component state: the FB1 post-mixer master gain (linear double) -----------------------
|
||||||
|
|
||||||
|
static void testComponentStateMasterGainRoundTrip() {
|
||||||
|
// A non-default gain proves the bytes are read back, not defaulted; the envelope
|
||||||
|
// neighbours (voice bytes, velocity, selection, zones) ride alongside intact.
|
||||||
|
ComponentState s;
|
||||||
|
s.selectionId = "pick";
|
||||||
|
s.previewVelocity = 99;
|
||||||
|
s.voiceCount = 5;
|
||||||
|
s.masterGainLinear = 0.25; // -12.04 dB
|
||||||
|
s.map.zones.push_back(zone("z0", 0, 127, /*override=*/std::nullopt));
|
||||||
|
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
|
||||||
|
CHECK(std::fabs(back.masterGainLinear - 0.25) < 1e-12); // an exact double round-trip
|
||||||
|
CHECK(back.voiceCount == 5);
|
||||||
|
CHECK(back.previewVelocity == 99);
|
||||||
|
CHECK(back.selectionId == "pick");
|
||||||
|
CHECK(back.map.zones.size() == 1 && back.map.zones[0].sampleId == "z0");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testComponentStateMasterGainDefaultAndZeroRoundTrip() {
|
||||||
|
// Default unity round-trips (pre-FB1 output); the -inf bottom (TRUE zero) round-trips
|
||||||
|
// exactly — a user who pulled the gain to silence gets silence back after a save/load.
|
||||||
|
const ComponentState defBack =
|
||||||
|
deserializeComponentState(serializeComponentState(ComponentState{}), 44100.0);
|
||||||
|
CHECK(defBack.masterGainLinear == 1.0);
|
||||||
|
ComponentState zero;
|
||||||
|
zero.masterGainLinear = 0.0;
|
||||||
|
const ComponentState zeroBack =
|
||||||
|
deserializeComponentState(serializeComponentState(zero), 44100.0);
|
||||||
|
CHECK(zeroBack.masterGainLinear == 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testComponentStateMasterGainWriterClamps() {
|
||||||
|
// The WRITER never emits an out-of-range value: above the +24 dB cap clamps to the cap;
|
||||||
|
// a negative/non-finite value (a programming error upstream) falls back to unity.
|
||||||
|
ComponentState hi;
|
||||||
|
hi.masterGainLinear = 1000.0;
|
||||||
|
CHECK(std::fabs(deserializeComponentState(serializeComponentState(hi), 44100.0)
|
||||||
|
.masterGainLinear -
|
||||||
|
vst::masterGainMaxLinear()) < 1e-9);
|
||||||
|
ComponentState lo;
|
||||||
|
lo.masterGainLinear = -5.0;
|
||||||
|
CHECK(deserializeComponentState(serializeComponentState(lo), 44100.0).masterGainLinear ==
|
||||||
|
1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testComponentStateV7LiftsUnityMasterGain() {
|
||||||
|
// A GENUINE v7 blob (version tag 7: mode, marker, velocity, voice bytes, id, zones — NO
|
||||||
|
// master-gain double) lifts to unity, its other fields intact. Hand-built
|
||||||
|
// (serializeComponentState now emits v8, so it cannot make a v7 blob). Proves an
|
||||||
|
// already-saved pre-FB1 instance restores playing at exactly its old output level.
|
||||||
|
std::vector<std::uint8_t> v7;
|
||||||
|
v7.push_back(7); v7.push_back(0); v7.push_back(0); v7.push_back(0); // version 7
|
||||||
|
v7.push_back(1); // channel mode = stereo
|
||||||
|
for (int i = 0; i < 8; ++i) v7.push_back(0); // marker = 0
|
||||||
|
v7.push_back(111); // preview velocity
|
||||||
|
v7.push_back(5); // voice count
|
||||||
|
v7.push_back(1); // voice mode = mono
|
||||||
|
v7.push_back(1); // trigger = legato
|
||||||
|
const std::string id = "saved";
|
||||||
|
v7.push_back(static_cast<std::uint8_t>(id.size())); v7.push_back(0); v7.push_back(0); v7.push_back(0);
|
||||||
|
v7.insert(v7.end(), id.begin(), id.end());
|
||||||
|
v7.push_back(0); v7.push_back(0); v7.push_back(0); v7.push_back(0); // zone count 0
|
||||||
|
const ComponentState back = deserializeComponentState(v7, 44100.0);
|
||||||
|
CHECK(back.masterGainLinear == 1.0);
|
||||||
|
CHECK(back.voiceCount == 5);
|
||||||
|
CHECK(back.voiceMode == VoiceMode::Mono);
|
||||||
|
CHECK(back.monoTrigger == MonoTrigger::Legato);
|
||||||
|
CHECK(back.previewVelocity == 111);
|
||||||
|
CHECK(back.channelMode == ChannelMode::Stereo);
|
||||||
|
CHECK(back.selectionId == "saved");
|
||||||
|
CHECK(back.map.zones.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testComponentStateV8CorruptMasterGainFallsBack() {
|
||||||
|
// A corrupt gain double (NaN) in a v8 blob falls back to unity (the previewVelocity
|
||||||
|
// corrupt-byte precedent) — never silences or blasts the instance. Build v8 by
|
||||||
|
// serializing, then vandalize the 8 gain bytes in place (offsets: 4 version + 1 mode +
|
||||||
|
// 8 marker + 1 velocity + 3 voice bytes = 17..24).
|
||||||
|
ComponentState s;
|
||||||
|
s.masterGainLinear = 0.5;
|
||||||
|
std::vector<std::uint8_t> bytes = serializeComponentState(s);
|
||||||
|
for (int i = 0; i < 8; ++i) bytes[17 + i] = 0xFF; // 0xFFFF... = a negative NaN pattern
|
||||||
|
const ComponentState back = deserializeComponentState(bytes, 44100.0);
|
||||||
|
CHECK(back.masterGainLinear == 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testComponentStateV8TruncatedMasterGain() {
|
||||||
|
// A v8 blob cut INSIDE the gain double -> empty, defaults holding (bounded read).
|
||||||
|
std::vector<std::uint8_t> t{8, 0, 0, 0, 0}; // version 8, mode byte
|
||||||
|
for (int i = 0; i < 8; ++i) t.push_back(0); // full marker
|
||||||
|
t.push_back(64); // velocity byte
|
||||||
|
t.push_back(16); t.push_back(0); t.push_back(0); // the three voice bytes
|
||||||
|
t.push_back(0); t.push_back(0); t.push_back(0); // gain cut mid-double
|
||||||
|
const ComponentState back = deserializeComponentState(t, 44100.0);
|
||||||
|
CHECK(back.masterGainLinear == 1.0);
|
||||||
|
CHECK(back.selectionId.empty() && back.map.zones.empty());
|
||||||
|
}
|
||||||
|
|
||||||
// --- MERGE COMPOSITION (S9 v5 marker envelope x S15/S16 v3 play-param payload) ----------------
|
// --- MERGE COMPOSITION (S9 v5 marker envelope x S15/S16 v3 play-param payload) ----------------
|
||||||
//
|
//
|
||||||
// The merge of ps-w9-t1-sync (envelope v5, adds the consumed-assignment marker) and
|
// The merge of ps-w9-t1-sync (envelope v5, adds the consumed-assignment marker) and
|
||||||
@@ -2030,6 +2130,12 @@ int main() {
|
|||||||
testComponentStateV6LiftsVoiceDefaults();
|
testComponentStateV6LiftsVoiceDefaults();
|
||||||
testComponentStateV7CorruptVoiceBytesFallBack();
|
testComponentStateV7CorruptVoiceBytesFallBack();
|
||||||
testComponentStateV7TruncatedVoiceBytes();
|
testComponentStateV7TruncatedVoiceBytes();
|
||||||
|
testComponentStateMasterGainRoundTrip();
|
||||||
|
testComponentStateMasterGainDefaultAndZeroRoundTrip();
|
||||||
|
testComponentStateMasterGainWriterClamps();
|
||||||
|
testComponentStateV7LiftsUnityMasterGain();
|
||||||
|
testComponentStateV8CorruptMasterGainFallsBack();
|
||||||
|
testComponentStateV8TruncatedMasterGain();
|
||||||
testV5EnvelopeWithMarkerAndPlayParamsRoundTrip();
|
testV5EnvelopeWithMarkerAndPlayParamsRoundTrip();
|
||||||
testV4BlobWithPlayParamsLiftsMarkerZeroKeepsPlay();
|
testV4BlobWithPlayParamsLiftsMarkerZeroKeepsPlay();
|
||||||
testReconcileKeepsOnlySelectedFullRangeZone();
|
testReconcileKeepsOnlySelectedFullRangeZone();
|
||||||
|
|||||||
Reference in New Issue
Block a user