refactor(capture): expose offline tail as docked-panel toggle
Replace the ...-with-tail variant actions with a pure tail_control module + a docked-panel footer toggle (Off/Auto/Manual) read by CAPTURE_ITEM/TRACK. Default None (byte-identical); Manual fixed 2s, in-memory session lifetime. Tail mechanism unchanged; retired the variant ids.
This commit is contained in:
+17
-1
@@ -96,6 +96,18 @@ add_library(render_settings STATIC src/render_settings.cpp)
|
|||||||
target_include_directories(render_settings PUBLIC src)
|
target_include_directories(render_settings PUBLIC src)
|
||||||
target_link_libraries(render_settings PUBLIC bank_model)
|
target_link_libraries(render_settings PUBLIC bank_model)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2f') Pure tail_control library — NO REAPER, NO SWELL. The docked bank_panel's
|
||||||
|
# tail-mode toggle logic (T1 exposure): TailSetting state, cycle order
|
||||||
|
# (None->Auto->Manual->None), the manual-length clamp to the 8 s cap, and the
|
||||||
|
# toggle label text. Split out so the toggle's cycle/clamp/label is unit-tested
|
||||||
|
# outside the DAW; the bank_panel footer that draws it + routes clicks is
|
||||||
|
# DAW-verified. Depends on render_settings for the pure TailMode enum + caps.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
add_library(tail_control STATIC src/tail_control.cpp)
|
||||||
|
target_include_directories(tail_control PUBLIC src)
|
||||||
|
target_link_libraries(tail_control PUBLIC render_settings)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 2g) Pure realtime_record library — NO REAPER, NO SWELL. The M8 realtime-record
|
# 2g) Pure realtime_record library — NO REAPER, NO SWELL. The M8 realtime-record
|
||||||
# logic: capture scope + FX-tap point -> I_RECMODE / I_RECMODE_FLAGS values,
|
# logic: capture scope + FX-tap point -> I_RECMODE / I_RECMODE_FLAGS values,
|
||||||
@@ -148,6 +160,10 @@ add_executable(render_settings_tests tests/test_render_settings.cpp)
|
|||||||
target_link_libraries(render_settings_tests PRIVATE render_settings)
|
target_link_libraries(render_settings_tests PRIVATE render_settings)
|
||||||
add_test(NAME render_settings_tests COMMAND render_settings_tests)
|
add_test(NAME render_settings_tests COMMAND render_settings_tests)
|
||||||
|
|
||||||
|
add_executable(tail_control_tests tests/test_tail_control.cpp)
|
||||||
|
target_link_libraries(tail_control_tests PRIVATE tail_control)
|
||||||
|
add_test(NAME tail_control_tests COMMAND tail_control_tests)
|
||||||
|
|
||||||
add_executable(realtime_record_tests tests/test_realtime_record.cpp)
|
add_executable(realtime_record_tests tests/test_realtime_record.cpp)
|
||||||
target_link_libraries(realtime_record_tests PRIVATE realtime_record)
|
target_link_libraries(realtime_record_tests PRIVATE realtime_record)
|
||||||
add_test(NAME realtime_record_tests COMMAND realtime_record_tests)
|
add_test(NAME realtime_record_tests COMMAND realtime_record_tests)
|
||||||
@@ -183,7 +199,7 @@ add_library(reaper_reasampler MODULE
|
|||||||
src/track_guid.cpp
|
src/track_guid.cpp
|
||||||
src/actions.cpp
|
src/actions.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid mode_switch view_mode_model insert_plan render_settings realtime_record)
|
target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid mode_switch view_mode_model insert_plan render_settings tail_control realtime_record)
|
||||||
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
|
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
|
||||||
set_target_properties(reaper_reasampler PROPERTIES PREFIX "" OUTPUT_NAME "reaper_reasampler")
|
set_target_properties(reaper_reasampler PROPERTIES PREFIX "" OUTPUT_NAME "reaper_reasampler")
|
||||||
|
|
||||||
|
|||||||
@@ -384,14 +384,18 @@ Neither is in scope now; both are single-constant seams so promotion is cheap.
|
|||||||
trailing edge, and the tail is appended after `ENDPOS`. But a range that itself
|
trailing edge, and the tail is appended after `ENDPOS`. But a range that itself
|
||||||
ends in near-silence before a loud transient is the edge case to check the trim
|
ends in near-silence before a loud transient is the edge case to check the trim
|
||||||
doesn't over-eat.)
|
doesn't over-eat.)
|
||||||
- **Auto as the action default?** Should the two shipped capture actions
|
- **Auto as the action default? — DECIDED (2026-07-23).** The two shipped capture
|
||||||
(`CAPTURE_ITEM` / `CAPTURE_TRACK`, currently all TailMode::None
|
actions (`CAPTURE_ITEM` / `CAPTURE_TRACK`) stay `TailMode::None` by default; the
|
||||||
per `render_settings.cpp §captureActionTable`) flip to Auto tail by default once
|
tail mode is exposed as a **settings toggle in the docked bank panel** (None / Auto
|
||||||
this ships, or should tail be a separate action variant / a modifier? Product call
|
/ Manual — the footer strip, `bank_panel.cpp` + pure `tail_control`), and the plain
|
||||||
for Daniel. **Leaning:** a per-action-family toggle or a paired "…with tail"
|
capture actions READ that toggle when building the `CaptureRequest`. Chosen over the
|
||||||
variant rather than silently changing the existing actions' behavior — the current
|
earlier "…with tail" paired-action lean: one toggle covers all three states without
|
||||||
exact-bounds default is a documented contract and some captures (chops, wavetable
|
doubling the action count, and the exact-bounds contract still holds because the
|
||||||
grabs) want no tail. Not blocking the offline implementation; the request-level
|
toggle defaults to None. The setting is an extension-session setting (default None;
|
||||||
contract (three tail states) is independent of which action sets which.
|
persists across project loads and panel open/close within a REAPER session; resets to
|
||||||
|
None only on extension unload — i.e. fresh REAPER session; project persistence across
|
||||||
|
REAPER restarts is a follow-on). Manual ships a fixed 2 s default; a
|
||||||
|
fine-adjust affordance (+/- click zones or scroll) is a follow-on. The verify /
|
||||||
|
null-test capture still always runs `TailMode::None` regardless of the toggle.
|
||||||
- **Realtime tail sequencing.** Confirmed a **follow-on** to the offline tail — do
|
- **Realtime tail sequencing.** Confirmed a **follow-on** to the offline tail — do
|
||||||
not block offline on it. Filed as a separate PLAN point.
|
not block offline on it. Filed as a separate PLAN point.
|
||||||
|
|||||||
+98
-5
@@ -42,6 +42,7 @@
|
|||||||
#include "mode_switch.h"
|
#include "mode_switch.h"
|
||||||
#include "peaks.h"
|
#include "peaks.h"
|
||||||
#include "persist.h"
|
#include "persist.h"
|
||||||
|
#include "tail_control.h" // TailSetting, cycleTailMode, tailToggleLabel (pure)
|
||||||
#include "view.h" // applyMode — the D2/D4 mode-activation entrypoint the switch fires
|
#include "view.h" // applyMode — the D2/D4 mode-activation entrypoint the switch fires
|
||||||
|
|
||||||
// SWELL / LICE. On macOS/Linux SWELL is provided by the host (SWELL_PROVIDED_BY_APP);
|
// SWELL / LICE. On macOS/Linux SWELL is provided by the host (SWELL_PROVIDED_BY_APP);
|
||||||
@@ -134,6 +135,20 @@ const LICE_pixel kColSegBorder = LICE_RGBA(70, 70, 76, 255); // segment di
|
|||||||
const COLORREF kRgbSegText = RGB(170, 170, 176); // inactive label
|
const COLORREF kRgbSegText = RGB(170, 170, 176); // inactive label
|
||||||
const COLORREF kRgbSegActiveText = RGB(220, 235, 228); // active label
|
const COLORREF kRgbSegActiveText = RGB(220, 235, 228); // active label
|
||||||
|
|
||||||
|
// --- Tail-mode footer (T1 exposure) -------------------------------------------
|
||||||
|
// A fixed-height strip at the BOTTOM of the client area holding the tail-mode
|
||||||
|
// toggle ("Tail: Off / Auto / Manual"). Clicking anywhere in it cycles the mode
|
||||||
|
// (None -> Auto -> Manual -> None). Display/settings only: it mutates the panel's
|
||||||
|
// in-memory tail setting the plain capture actions read — NEVER the project/bank/
|
||||||
|
// arrange. The cycle/label logic is the pure tail_control module; only the draw +
|
||||||
|
// click routing is here. The grid viewport is shortened by this strip's height so
|
||||||
|
// cells never draw under it.
|
||||||
|
constexpr int kFooterHeight = 26; // px; fixed strip at the bottom
|
||||||
|
|
||||||
|
const LICE_pixel kColFooterBg = LICE_RGBA(20, 20, 22, 255); // footer strip fill
|
||||||
|
const LICE_pixel kColFooterBorder = LICE_RGBA(70, 70, 76, 255); // top divider
|
||||||
|
const COLORREF kRgbFooterText = RGB(190, 205, 198); // toggle label
|
||||||
|
|
||||||
// --- Panel state --------------------------------------------------------------
|
// --- Panel state --------------------------------------------------------------
|
||||||
|
|
||||||
// A computed thumbnail: the per-channel envelope at a known width. Held in the
|
// A computed thumbnail: the per-channel envelope at a known width. Held in the
|
||||||
@@ -173,6 +188,12 @@ struct PanelState {
|
|||||||
// clear the selection rather than risk indices pointing past the new count.
|
// clear the selection rather than risk indices pointing past the new count.
|
||||||
int selItemCount = 0;
|
int selItemCount = 0;
|
||||||
|
|
||||||
|
// --- Tail-mode toggle (T1 exposure) ---------------------------------------
|
||||||
|
// The current tail setting the plain capture actions read (bankPanelTailSetting).
|
||||||
|
// Default None (exact bounds). In-memory only — resets on panel teardown; project
|
||||||
|
// persistence is a noted follow-on. Mutated ONLY by a click in the footer strip.
|
||||||
|
TailSetting tail;
|
||||||
|
|
||||||
// --- Audition preview (Wave B) --------------------------------------------
|
// --- Audition preview (Wave B) --------------------------------------------
|
||||||
//
|
//
|
||||||
// The stock preview register we hand to PlayPreview/StopPreview. Its cs/mutex
|
// The stock preview register we hand to PlayPreview/StopPreview. Its cs/mutex
|
||||||
@@ -424,6 +445,45 @@ void drawModeSwitch(LICE_IBitmap* bmp, int w) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The tail-toggle footer rect for a client of width `w` and height `h`: the
|
||||||
|
// full-width strip of fixed height pinned to the BOTTOM. A RECT (not HeaderRect)
|
||||||
|
// since the whole strip is one hit target — a click anywhere in it cycles the mode.
|
||||||
|
// Shared by paint and click routing so both agree on the band. Degenerate (empty)
|
||||||
|
// when the client is too short to host it above the header.
|
||||||
|
RECT panelFooter(int w, int h) {
|
||||||
|
RECT rc{};
|
||||||
|
rc.left = 0;
|
||||||
|
rc.right = w;
|
||||||
|
rc.top = h - kFooterHeight;
|
||||||
|
rc.bottom = h;
|
||||||
|
// Clamp so the footer never rides up into (or above) the header band on a very
|
||||||
|
// short panel — it collapses to empty rather than overlapping the mode switch.
|
||||||
|
if (rc.top < kHeaderHeight) rc.top = rc.bottom; // empty: top == bottom
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws the tail-mode toggle into the footer strip: a filled band, a top divider,
|
||||||
|
// and the current mode's label ("Tail: Off / Auto / Manual") from the pure
|
||||||
|
// tail_control module. READ-ONLY: reads g_panel.tail; the click handler mutates it.
|
||||||
|
void drawTailFooter(LICE_IBitmap* bmp, int w, int h) {
|
||||||
|
const RECT f = panelFooter(w, h);
|
||||||
|
if (f.top >= f.bottom) return; // no room — skip (short panel)
|
||||||
|
|
||||||
|
LICE_FillRect(bmp, f.left, f.top, w, kFooterHeight, kColFooterBg, 1.0f, 0);
|
||||||
|
// Top divider so the strip reads as distinct from the grid above it.
|
||||||
|
LICE_Line(bmp, f.left, f.top, f.right, f.top, kColFooterBorder, 1.0f, 0, false);
|
||||||
|
|
||||||
|
HDC dc = bmp->getDC();
|
||||||
|
if (!dc) return;
|
||||||
|
const std::string label = tailToggleLabel(g_panel.tail);
|
||||||
|
RECT rc = f;
|
||||||
|
rc.left += 8; // small left pad so the label is not flush against the edge
|
||||||
|
SetTextColor(dc, kRgbFooterText);
|
||||||
|
SetBkMode(dc, TRANSPARENT);
|
||||||
|
DrawText(dc, label.c_str(), -1, &rc,
|
||||||
|
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
|
||||||
|
}
|
||||||
|
|
||||||
// The cell rects for the panel's CURRENT client width and bank size, translated
|
// The cell rects for the panel's CURRENT client width and bank size, translated
|
||||||
// DOWN by the header height so the grid sits below the mode switch. Both paint and
|
// DOWN by the header height so the grid sits below the mode switch. Both paint and
|
||||||
// mouse hit-testing call this so they share identical geometry (no drift between
|
// mouse hit-testing call this so they share identical geometry (no drift between
|
||||||
@@ -469,11 +529,15 @@ void paintPanel(HWND hwnd, HDC hdc) {
|
|||||||
// Draw each cell's thumbnail. Inner drawable width == cell width - inset;
|
// Draw each cell's thumbnail. Inner drawable width == cell width - inset;
|
||||||
// compute the envelope at the cell's inner column count so bins map 1:1.
|
// compute the envelope at the cell's inner column count so bins map 1:1.
|
||||||
const int binWidth = kGrid.cellWidth - 4;
|
const int binWidth = kGrid.cellWidth - 4;
|
||||||
|
// Cells must not draw under the footer strip: the visible grid stops at the
|
||||||
|
// footer top (or the client bottom when the panel is too short for a footer).
|
||||||
|
const RECT footer = panelFooter(w, h);
|
||||||
|
const int gridBottom = footer.top < footer.bottom ? footer.top : h;
|
||||||
for (std::size_t i = 0; i < rects.size(); ++i) {
|
for (std::size_t i = 0; i < rects.size(); ++i) {
|
||||||
const CellRect& rect = rects[i];
|
const CellRect& rect = rects[i];
|
||||||
// Skip cells entirely below the viewport (Wave A has no scroll; this
|
// Skip cells entirely below the visible grid area (Wave A has no scroll;
|
||||||
// just avoids computing thumbnails that cannot be seen).
|
// this just avoids computing thumbnails that cannot be seen).
|
||||||
if (rect.y >= h) continue;
|
if (rect.y >= gridBottom) continue;
|
||||||
const int idx = static_cast<int>(i);
|
const int idx = static_cast<int>(i);
|
||||||
const bool selected = g_panel.selection.contains(idx);
|
const bool selected = g_panel.selection.contains(idx);
|
||||||
const bool focused = g_panel.selection.focus == idx;
|
const bool focused = g_panel.selection.focus == idx;
|
||||||
@@ -482,9 +546,10 @@ void paintPanel(HWND hwnd, HDC hdc) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The mode switch draws LAST so its header band overlays the top of the grid /
|
// The mode switch and tail footer draw LAST so their bands overlay the top/bottom
|
||||||
// empty-state area regardless of which branch ran above.
|
// of the grid / empty-state area regardless of which branch ran above.
|
||||||
drawModeSwitch(&bmp, w);
|
drawModeSwitch(&bmp, w);
|
||||||
|
drawTailFooter(&bmp, w, h);
|
||||||
|
|
||||||
BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY);
|
BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY);
|
||||||
}
|
}
|
||||||
@@ -675,6 +740,23 @@ void handleClick(int x, int y) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tail-mode footer: a click anywhere in the bottom strip cycles the tail mode
|
||||||
|
// (None -> Auto -> Manual -> None) and repaints. Settings-only — it mutates the
|
||||||
|
// panel's in-memory tail setting the capture actions read, and NOTHING in the
|
||||||
|
// project/bank/arrange. Checked before the grid so a footer click never selects.
|
||||||
|
{
|
||||||
|
RECT cr{};
|
||||||
|
GetClientRect(g_panel.hwnd, &cr);
|
||||||
|
const int w = cr.right - cr.left;
|
||||||
|
const int h = cr.bottom - cr.top;
|
||||||
|
const RECT f = panelFooter(w, h);
|
||||||
|
if (f.top < f.bottom && x >= f.left && x < f.right && y >= f.top && y < f.bottom) {
|
||||||
|
g_panel.tail.mode = cycleTailMode(g_panel.tail.mode);
|
||||||
|
invalidatePanel();
|
||||||
|
return; // footer click consumed; do NOT fall through to grid selection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const std::vector<CellRect> rects = panelRects();
|
const std::vector<CellRect> rects = panelRects();
|
||||||
const int hit = hitTestCell(x, y, rects);
|
const int hit = hitTestCell(x, y, rects);
|
||||||
const int count = bankItemCount();
|
const int count = bankItemCount();
|
||||||
@@ -906,6 +988,17 @@ void bankPanelRefresh() {
|
|||||||
InvalidateRect(g_panel.hwnd, nullptr, FALSE);
|
InvalidateRect(g_panel.hwnd, nullptr, FALSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TailSetting bankPanelTailSetting() {
|
||||||
|
// In-memory for the extension's lifetime (g_panel is static): the toggle's mode
|
||||||
|
// survives panel open/close and bank changes, and resets to the default None only
|
||||||
|
// on extension unload. Persistence across project reload is a noted follow-on.
|
||||||
|
// manualMs is clamped here so a caller always receives a within-cap length, even
|
||||||
|
// if a future fine-adjust UI stored an over-cap value.
|
||||||
|
TailSetting s = g_panel.tail;
|
||||||
|
s.manualMs = clampManualMs(s.manualMs);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
void bankPanelShutdown() {
|
void bankPanelShutdown() {
|
||||||
closePanel(); // stops audition + destroys the window
|
closePanel(); // stops audition + destroys the window
|
||||||
deinitPreview(); // destroy the preview lock (after the last stop)
|
deinitPreview(); // destroy the preview lock (after the last stop)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "tail_control.h" // TailSetting — the panel's tail-mode toggle state
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
class ReaSamplerSession;
|
class ReaSamplerSession;
|
||||||
@@ -48,6 +50,17 @@ std::vector<std::string> bankPanelSelectedSampleIds();
|
|||||||
// reflected without the panel diffing the bank itself.
|
// reflected without the panel diffing the bank itself.
|
||||||
void bankPanelRefresh();
|
void bankPanelRefresh();
|
||||||
|
|
||||||
|
// The panel's current tail-mode setting (mode + Manual length), read by the plain
|
||||||
|
// CAPTURE_ITEM / CAPTURE_TRACK actions when building a CaptureRequest so a capture
|
||||||
|
// applies whatever the panel toggle is set to. Default None (exact bounds) — a
|
||||||
|
// capture with no explicit choice stays byte-identical to today. Extension-session
|
||||||
|
// setting: persists across project loads and panel open/close within a REAPER session;
|
||||||
|
// resets to None only when the extension unloads (fresh REAPER session). Project
|
||||||
|
// persistence across REAPER restarts is a noted follow-on.
|
||||||
|
// Safe to call before the panel has ever opened (returns the default). READ of panel
|
||||||
|
// state only; the toggle is mutated by a click inside the panel, never here.
|
||||||
|
TailSetting bankPanelTailSetting();
|
||||||
|
|
||||||
// Tears the panel down on extension unload: destroys the window and releases any
|
// Tears the panel down on extension unload: destroys the window and releases any
|
||||||
// cached thumbnails / PCM handles. Mirror of bankPanelInit; safe if never opened.
|
// cached thumbnails / PCM handles. Mirror of bankPanelInit; safe if never opened.
|
||||||
void bankPanelShutdown();
|
void bankPanelShutdown();
|
||||||
|
|||||||
+13
-2
@@ -69,12 +69,17 @@ static std::vector<gaccel_register_t> g_captureAccels;
|
|||||||
// * CAPTURE_MASTER and CAPTURE_MASTER_REALTIME — the master offline scope and the
|
// * CAPTURE_MASTER and CAPTURE_MASTER_REALTIME — the master offline scope and the
|
||||||
// master realtime action are REMOVED (capture is now item + track only; realtime
|
// master realtime action are REMOVED (capture is now item + track only; realtime
|
||||||
// taps the selected track). Their shipped ids are retired so old keybindings clear.
|
// taps the selected track). Their shipped ids are retired so old keybindings clear.
|
||||||
|
// * CAPTURE_ITEM_TAIL and CAPTURE_TRACK_TAIL — the former per-action tail variants
|
||||||
|
// are REMOVED; tail is now a panel-setting toggle, not a paired action. Retired so
|
||||||
|
// old keybindings clear.
|
||||||
static const char* const kRetiredCaptureCmdStrings[] = {
|
static const char* const kRetiredCaptureCmdStrings[] = {
|
||||||
"CEREBELLUM_REASAMPLER_CAPTURE_TRACKS_WET",
|
"CEREBELLUM_REASAMPLER_CAPTURE_TRACKS_WET",
|
||||||
"CEREBELLUM_REASAMPLER_CAPTURE_ITEMS_WET",
|
"CEREBELLUM_REASAMPLER_CAPTURE_ITEMS_WET",
|
||||||
"CEREBELLUM_REASAMPLER_CAPTURE_RAZOR_WET",
|
"CEREBELLUM_REASAMPLER_CAPTURE_RAZOR_WET",
|
||||||
"CEREBELLUM_REASAMPLER_CAPTURE_MASTER",
|
"CEREBELLUM_REASAMPLER_CAPTURE_MASTER",
|
||||||
"CEREBELLUM_REASAMPLER_CAPTURE_MASTER_REALTIME",
|
"CEREBELLUM_REASAMPLER_CAPTURE_MASTER_REALTIME",
|
||||||
|
"CEREBELLUM_REASAMPLER_CAPTURE_ITEM_TAIL",
|
||||||
|
"CEREBELLUM_REASAMPLER_CAPTURE_TRACK_TAIL",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Command id for "ReaSampler: toggle bank panel" (M5). FOREVER-STABLE string.
|
// Command id for "ReaSampler: toggle bank panel" (M5). FOREVER-STABLE string.
|
||||||
@@ -505,13 +510,19 @@ static void RunCapture(const reasampler::CaptureActionDef& def)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The tail mode is a PANEL SETTING (docked bank panel's toggle), not a per-action
|
||||||
|
// variant: the plain capture actions apply whatever the panel is set to. Default
|
||||||
|
// is None (exact bounds / byte-identical to today) until the user opts in via the
|
||||||
|
// toggle. tailMs is meaningful only for Manual and is pre-clamped by the panel.
|
||||||
|
const reasampler::TailSetting tail = reasampler::bankPanelTailSetting();
|
||||||
|
|
||||||
reasampler::CaptureRequest req;
|
reasampler::CaptureRequest req;
|
||||||
req.sourceMode = reasampler::sourceModeForScope(def.scope);
|
req.sourceMode = reasampler::sourceModeForScope(def.scope);
|
||||||
req.startSeconds = src.startSeconds; // exact bounds — no rounding
|
req.startSeconds = src.startSeconds; // exact bounds — no rounding
|
||||||
req.endSeconds = src.endSeconds;
|
req.endSeconds = src.endSeconds;
|
||||||
req.wetDry = 1.0; // wet post the FX left enabled by the scope
|
req.wetDry = 1.0; // wet post the FX left enabled by the scope
|
||||||
req.tailMode = def.tailMode; // None (exact bounds) or Auto ("…with tail" variant)
|
req.tailMode = tail.mode; // None / Auto / Manual, from the panel toggle
|
||||||
req.tailMs = 0.0; // Manual-only; no Manual action yet (future config/UI)
|
req.tailMs = tail.manualMs; // Manual-only (clamped); ignored for None/Auto
|
||||||
req.sampleRate = 0; // follow project rate
|
req.sampleRate = 0; // follow project rate
|
||||||
req.channelCount = 2;
|
req.channelCount = 2;
|
||||||
req.bitDepth = reasampler::WavBitDepth::Float32; // deterministic, no dither
|
req.bitDepth = reasampler::WavBitDepth::Float32; // deterministic, no dither
|
||||||
|
|||||||
+13
-26
@@ -179,38 +179,25 @@ RazorRange razorUnionBounds(const std::vector<RazorRange>& ranges) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<CaptureActionDef>& captureActionTable() {
|
const std::vector<CaptureActionDef>& captureActionTable() {
|
||||||
// Built once (function-local static): two SCOPE actions x two tail variants.
|
// Built once (function-local static): two SCOPE actions, item + track. Both
|
||||||
// The None rows are exact bounds (byte-identical to today); the …_TAIL rows are
|
// exact bounds by default; the tail mode a capture applies is read from the
|
||||||
// the paired TailMode::Auto "…with tail" variants (Daniel's lean over silently
|
// docked-panel setting at fire time (tail_control + bank_panel), so tail is NOT
|
||||||
// flipping the exact-bounds default — spec §Open questions). Ids are FOREVER-
|
// a per-action variant. Ids are FOREVER-STABLE — never edit a shipped string.
|
||||||
// STABLE — never edit a shipped string. Each action infers its range
|
// Each action infers its range (razor-else-time) at fire time and enforces its
|
||||||
// (razor-else-time) at fire time and enforces its FX-scope invariant via
|
// FX-scope invariant via fxBypassPlanFor. The M7 CAPTURE_TRACKS_WET /
|
||||||
// fxBypassPlanFor. The M7 CAPTURE_TRACKS_WET / CAPTURE_ITEMS_WET /
|
// CAPTURE_ITEMS_WET / CAPTURE_RAZOR_WET ids are RETIRED (mirror-unregistered in
|
||||||
// CAPTURE_RAZOR_WET ids are RETIRED (mirror-unregistered in main.cpp); the
|
// main.cpp); the CAPTURE_MASTER scope action is REMOVED (its id is likewise
|
||||||
// CAPTURE_MASTER scope action is REMOVED (its id is likewise mirror-
|
// mirror-unregistered) — to capture the master you render a track.
|
||||||
// unregistered) — to capture the master you render a track.
|
|
||||||
static const std::vector<CaptureActionDef> table = {
|
static const std::vector<CaptureActionDef> table = {
|
||||||
// Item scope, exact bounds — item/take FX only.
|
// Item scope — item/take FX only.
|
||||||
{"CEREBELLUM_REASAMPLER_CAPTURE_ITEM",
|
{"CEREBELLUM_REASAMPLER_CAPTURE_ITEM",
|
||||||
"ReaSampler: capture selected item(s)", "item",
|
"ReaSampler: capture selected item(s)", "item",
|
||||||
CaptureScope::Item, TailMode::None},
|
CaptureScope::Item},
|
||||||
|
|
||||||
// Track scope, exact bounds — item FX + the track's own FX.
|
// Track scope — item FX + the track's own FX.
|
||||||
{"CEREBELLUM_REASAMPLER_CAPTURE_TRACK",
|
{"CEREBELLUM_REASAMPLER_CAPTURE_TRACK",
|
||||||
"ReaSampler: capture selected track(s)", "track",
|
"ReaSampler: capture selected track(s)", "track",
|
||||||
CaptureScope::Track, TailMode::None},
|
CaptureScope::Track},
|
||||||
|
|
||||||
// Item scope with Auto tail — captures the take-FX decay past the range end,
|
|
||||||
// trimmed to -72 dB. NEW forever-stable id.
|
|
||||||
{"CEREBELLUM_REASAMPLER_CAPTURE_ITEM_TAIL",
|
|
||||||
"ReaSampler: capture selected item(s) with tail", "item",
|
|
||||||
CaptureScope::Item, TailMode::Auto},
|
|
||||||
|
|
||||||
// Track scope with Auto tail — captures the track's own reverb/delay decay
|
|
||||||
// past the range end, trimmed to -72 dB. NEW forever-stable id.
|
|
||||||
{"CEREBELLUM_REASAMPLER_CAPTURE_TRACK_TAIL",
|
|
||||||
"ReaSampler: capture selected track(s) with tail", "track",
|
|
||||||
CaptureScope::Track, TailMode::Auto},
|
|
||||||
};
|
};
|
||||||
return table;
|
return table;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-13
@@ -89,7 +89,7 @@ double autoTrimEndRatio();
|
|||||||
// None — exact bounds, no tail. Byte-identical to the pre-tail capture. The
|
// None — exact bounds, no tail. Byte-identical to the pre-tail capture. The
|
||||||
// default and the ONLY mode for null-test / verify captures.
|
// default and the ONLY mode for null-test / verify captures.
|
||||||
// Auto — generous 8 s tail then trim trailing silence to -72 dB (surgical
|
// Auto — generous 8 s tail then trim trailing silence to -72 dB (surgical
|
||||||
// normalize). The user-facing "…with tail" default.
|
// normalize). The user-facing tail-on option (panel toggle).
|
||||||
// Manual — a fixed tail length (clamped to the 8 s cap), no trim.
|
// Manual — a fixed tail length (clamped to the 8 s cap), no trim.
|
||||||
enum class TailMode {
|
enum class TailMode {
|
||||||
None,
|
None,
|
||||||
@@ -213,13 +213,11 @@ RazorRange razorUnionBounds(const std::vector<RazorRange>& ranges);
|
|||||||
|
|
||||||
// --- Capture-action taxonomy (the bindable set main.cpp registers) -----------
|
// --- Capture-action taxonomy (the bindable set main.cpp registers) -----------
|
||||||
//
|
//
|
||||||
// One row per bindable SCOPE action. Two scopes (item / track), each in two tail
|
// One row per bindable SCOPE action: item and track. The range each captures
|
||||||
// variants: an exact-bounds row (TailMode::None) and a paired "…with tail" row
|
// (razor-else-time) is inferred at fire time, not a mode. TAIL is NOT a per-action
|
||||||
// (TailMode::Auto). The range each captures (razor-else-time) is inferred at fire
|
// variant — the tail MODE (None/Auto/Manual) is a panel SETTING the capture reads
|
||||||
// time, not a mode. The tail variants are additive — the None rows keep their
|
// at fire time (see tail_control + bank_panel), so a single pair of actions covers
|
||||||
// documented exact-bounds contract byte-for-byte; the Auto variants are a separate
|
// every tail state. Bounded, discoverable, NO dialogs (the tool's no-clutter ethos).
|
||||||
// opt-in action rather than a silent default flip (Daniel's lean, spec §Open
|
|
||||||
// questions). Bounded, discoverable, NO dialogs (the tool's no-clutter ethos).
|
|
||||||
//
|
//
|
||||||
// commandString is FOREVER-STABLE (user keybindings key off it) — never change a
|
// commandString is FOREVER-STABLE (user keybindings key off it) — never change a
|
||||||
// shipped value. baseName feeds the file stem (sanitized by capture_paths).
|
// shipped value. baseName feeds the file stem (sanitized by capture_paths).
|
||||||
@@ -228,17 +226,16 @@ struct CaptureActionDef {
|
|||||||
const char* description; // Actions-list label
|
const char* description; // Actions-list label
|
||||||
const char* baseName; // file-stem base for this capture
|
const char* baseName; // file-stem base for this capture
|
||||||
CaptureScope scope; // FX scope (item / track)
|
CaptureScope scope; // FX scope (item / track)
|
||||||
TailMode tailMode; // None (exact bounds) or Auto ("…with tail")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// The capture-action table. Iterated by main.cpp to register the family and route
|
// The capture-action table. Iterated by main.cpp to register the family and route
|
||||||
// each fired command back to its definition. Kept here (pure) so the taxonomy is
|
// each fired command back to its definition. Kept here (pure) so the taxonomy is
|
||||||
// one testable list, not scattered registration code.
|
// one testable list, not scattered registration code.
|
||||||
//
|
//
|
||||||
// Four rows: CAPTURE_ITEM / CAPTURE_TRACK (exact bounds, TailMode::None) plus the
|
// Two rows: CAPTURE_ITEM / CAPTURE_TRACK. There is no master capture — to capture
|
||||||
// paired CAPTURE_ITEM_TAIL / CAPTURE_TRACK_TAIL variants (TailMode::Auto). There is
|
// the master you render a track. Razor is an inferred range, not a mode, and each
|
||||||
// no master capture — to capture the master you render a track. Razor is an inferred
|
// scope enforces its FX-scope invariant via fxBypassPlanFor. The tail mode each
|
||||||
// range, not a mode, and each scope enforces its FX-scope invariant via fxBypassPlanFor.
|
// capture applies is read from the docked-panel setting, not baked into the row.
|
||||||
const std::vector<CaptureActionDef>& captureActionTable();
|
const std::vector<CaptureActionDef>& captureActionTable();
|
||||||
|
|
||||||
} // namespace reasampler
|
} // namespace reasampler
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// tail_control — pure implementation. See tail_control.h. NO REAPER / SWELL / vendor.
|
||||||
|
|
||||||
|
#include "tail_control.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
TailMode cycleTailMode(TailMode current) {
|
||||||
|
switch (current) {
|
||||||
|
case TailMode::None: return TailMode::Auto;
|
||||||
|
case TailMode::Auto: return TailMode::Manual;
|
||||||
|
case TailMode::Manual: return TailMode::None;
|
||||||
|
}
|
||||||
|
return TailMode::None; // unreachable for a valid enum; fail to the safe default
|
||||||
|
}
|
||||||
|
|
||||||
|
double clampManualMs(double manualMs) {
|
||||||
|
// Same runaway guard the pure tailRenderSettingsFor applies to Manual: floor a
|
||||||
|
// negative request to 0, cap at the 8 s ceiling.
|
||||||
|
return std::clamp(manualMs, 0.0, kMaxTailMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string tailToggleLabel(const TailSetting& setting) {
|
||||||
|
switch (setting.mode) {
|
||||||
|
case TailMode::None: return "Tail: Off";
|
||||||
|
case TailMode::Auto: return "Tail: Auto";
|
||||||
|
case TailMode::Manual: return "Tail: Manual";
|
||||||
|
}
|
||||||
|
return "Tail: Off"; // unreachable for a valid enum; fail to the safe default
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
// tail_control — the REAPER-free logic behind the docked bank_panel's tail-mode
|
||||||
|
// toggle. The panel shell (bank_panel.cpp) owns the SWELL window, LICE drawing, and
|
||||||
|
// click hit-testing; what is NOT DAW-bound — the cycle order, the manual-length
|
||||||
|
// clamp, and the toggle's label text — lives here so it is unit-tested outside the
|
||||||
|
// DAW (CLAUDE.md §load-bearing split). Mirror of bank_grid / mode_switch.
|
||||||
|
//
|
||||||
|
// PURE MODULE: NO REAPER types, NO SWELL, NO vendor/ includes. Standard library
|
||||||
|
// only (plus render_settings for the pure TailMode enum). Builds and unit-tests
|
||||||
|
// without REAPER.
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "render_settings.h" // TailMode (pure enum) — the three-state tail contract
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
// The Manual-mode starting length. 2 s is a musically useful default tail (a bar of
|
||||||
|
// reverb throw at a moderate tempo) that is well under the 8 s cap. A fine-adjust UI
|
||||||
|
// (+/- click zones or scroll) is a noted follow-on; this pass ships a fixed default.
|
||||||
|
inline constexpr double kDefaultManualTailMs = 2000.0;
|
||||||
|
|
||||||
|
// The panel's current tail setting: the mode plus the length used ONLY when the
|
||||||
|
// mode is Manual. Held as in-memory panel/session state (bank_panel.cpp), default
|
||||||
|
// None so a capture with no explicit choice stays exact-bounds / byte-identical to
|
||||||
|
// today. `manualMs` is a stored default a future fine-adjust UI can tune; it is
|
||||||
|
// clamped to the 8 s cap (kMaxTailMs) before it ever reaches a CaptureRequest.
|
||||||
|
struct TailSetting {
|
||||||
|
TailMode mode = TailMode::None;
|
||||||
|
double manualMs = kDefaultManualTailMs;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cycles the tail mode: None -> Auto -> Manual -> None. Pure so the wrap order is
|
||||||
|
// pinned by a test and the panel's click handler owns no enum arithmetic of its own.
|
||||||
|
// An out-of-range value (unreachable for a valid enum) cycles back to None.
|
||||||
|
TailMode cycleTailMode(TailMode current);
|
||||||
|
|
||||||
|
// The effective manual length a Manual capture uses: `manualMs` clamped to
|
||||||
|
// [0, kMaxTailMs] (the runaway guard the pure tailRenderSettingsFor also applies).
|
||||||
|
// Exposed so the panel can show the clamped value and main.cpp hands a pre-clamped
|
||||||
|
// tailMs into the CaptureRequest. Meaningful only for TailMode::Manual.
|
||||||
|
double clampManualMs(double manualMs);
|
||||||
|
|
||||||
|
// The toggle's label for a setting, e.g. "Tail: Off", "Tail: Auto", "Tail: Manual".
|
||||||
|
// (Manual omits the length here — the panel is unobtrusive; a length readout can be
|
||||||
|
// added with the fine-adjust follow-on.) Pure so the exact strings are test-pinned.
|
||||||
|
std::string tailToggleLabel(const TailSetting& setting);
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -216,17 +216,16 @@ static void testTrackScopeKeepsSelfBypassesAncestorsAndMaster() {
|
|||||||
CHECK(p.bypassMaster); // no master FX
|
CHECK(p.bypassMaster); // no master FX
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- captureActionTable: the scope x tail-variant taxonomy -------------------
|
// --- captureActionTable: the scope taxonomy ----------------------------------
|
||||||
|
|
||||||
static void testTableHasScopeAndTailVariants() {
|
static void testTableHasBothScopes() {
|
||||||
const auto& table = captureActionTable();
|
const auto& table = captureActionTable();
|
||||||
// Four rows: item/track x (None exact-bounds, Auto "…with tail"). No master scope.
|
// Two rows: item + track. No master scope, and NO tail variants — tail is a
|
||||||
CHECK(table.size() == 4);
|
// panel setting the capture reads at fire time, not a per-action row.
|
||||||
|
CHECK(table.size() == 2);
|
||||||
|
|
||||||
std::set<std::string> ids;
|
std::set<std::string> ids;
|
||||||
// Count each (scope, tailMode) pairing so we assert the FULL matrix is present,
|
int item = 0, track = 0;
|
||||||
// not merely that some item + some track row exist.
|
|
||||||
int itemNone = 0, itemAuto = 0, trackNone = 0, trackAuto = 0;
|
|
||||||
for (const auto& def : table) {
|
for (const auto& def : table) {
|
||||||
// Every id is a non-empty CEREBELLUM_REASAMPLER_ string and is UNIQUE
|
// Every id is a non-empty CEREBELLUM_REASAMPLER_ string and is UNIQUE
|
||||||
// (duplicate ids would collide on registration).
|
// (duplicate ids would collide on registration).
|
||||||
@@ -235,38 +234,26 @@ static void testTableHasScopeAndTailVariants() {
|
|||||||
CHECK(ids.insert(id).second); // false if duplicate
|
CHECK(ids.insert(id).second); // false if duplicate
|
||||||
// Every scope resolves to a supported offline source.
|
// Every scope resolves to a supported offline source.
|
||||||
CHECK(renderSettingsFor(sourceModeForScope(def.scope), 1.0).supported);
|
CHECK(renderSettingsFor(sourceModeForScope(def.scope), 1.0).supported);
|
||||||
// Tail variants only ever ship None or Auto (Manual has no dedicated action yet).
|
|
||||||
CHECK(def.tailMode == TailMode::None || def.tailMode == TailMode::Auto);
|
|
||||||
|
|
||||||
if (def.scope == CaptureScope::Item && def.tailMode == TailMode::None) ++itemNone;
|
if (def.scope == CaptureScope::Item) ++item;
|
||||||
if (def.scope == CaptureScope::Item && def.tailMode == TailMode::Auto) ++itemAuto;
|
if (def.scope == CaptureScope::Track) ++track;
|
||||||
if (def.scope == CaptureScope::Track && def.tailMode == TailMode::None) ++trackNone;
|
|
||||||
if (def.scope == CaptureScope::Track && def.tailMode == TailMode::Auto) ++trackAuto;
|
|
||||||
}
|
}
|
||||||
// Exactly one row per (scope, tail) cell — the full 2x2 matrix, no dupes/gaps.
|
// Exactly one row per scope — no dupes, no gaps, no tail variants.
|
||||||
CHECK(itemNone == 1);
|
CHECK(item == 1);
|
||||||
CHECK(itemAuto == 1);
|
CHECK(track == 1);
|
||||||
CHECK(trackNone == 1);
|
|
||||||
CHECK(trackAuto == 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void testTailVariantIdsAreDistinctFromExactRows() {
|
static void testScopeActionIdsAreTheShippedStrings() {
|
||||||
// The …_TAIL variants must be NEW forever-stable ids, not a rename of the exact
|
// Pin the shipped CAPTURE_ITEM / CAPTURE_TRACK ids so a future edit that silently
|
||||||
// rows (renaming would break shipped keybindings on the exact-bounds actions).
|
// changes them (breaking user keybindings) fails the gate.
|
||||||
const auto& table = captureActionTable();
|
const auto& table = captureActionTable();
|
||||||
std::string itemNoneId, itemAutoId, trackNoneId, trackAutoId;
|
std::string itemId, trackId;
|
||||||
for (const auto& def : table) {
|
for (const auto& def : table) {
|
||||||
if (def.scope == CaptureScope::Item && def.tailMode == TailMode::None) itemNoneId = def.commandString;
|
if (def.scope == CaptureScope::Item) itemId = def.commandString;
|
||||||
if (def.scope == CaptureScope::Item && def.tailMode == TailMode::Auto) itemAutoId = def.commandString;
|
if (def.scope == CaptureScope::Track) trackId = def.commandString;
|
||||||
if (def.scope == CaptureScope::Track && def.tailMode == TailMode::None) trackNoneId = def.commandString;
|
|
||||||
if (def.scope == CaptureScope::Track && def.tailMode == TailMode::Auto) trackAutoId = def.commandString;
|
|
||||||
}
|
}
|
||||||
// The exact-bounds ids are the shipped CAPTURE_ITEM / CAPTURE_TRACK strings —
|
CHECK(itemId == "CEREBELLUM_REASAMPLER_CAPTURE_ITEM");
|
||||||
// pin them so a future edit that silently changes them fails the gate.
|
CHECK(trackId == "CEREBELLUM_REASAMPLER_CAPTURE_TRACK");
|
||||||
CHECK(itemNoneId == "CEREBELLUM_REASAMPLER_CAPTURE_ITEM");
|
|
||||||
CHECK(trackNoneId == "CEREBELLUM_REASAMPLER_CAPTURE_TRACK");
|
|
||||||
CHECK(itemAutoId == "CEREBELLUM_REASAMPLER_CAPTURE_ITEM_TAIL");
|
|
||||||
CHECK(trackAutoId == "CEREBELLUM_REASAMPLER_CAPTURE_TRACK_TAIL");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
@@ -289,8 +276,8 @@ int main() {
|
|||||||
testRangeInference();
|
testRangeInference();
|
||||||
testItemScopeBypassesEverythingButTake();
|
testItemScopeBypassesEverythingButTake();
|
||||||
testTrackScopeKeepsSelfBypassesAncestorsAndMaster();
|
testTrackScopeKeepsSelfBypassesAncestorsAndMaster();
|
||||||
testTableHasScopeAndTailVariants();
|
testTableHasBothScopes();
|
||||||
testTailVariantIdsAreDistinctFromExactRows();
|
testScopeActionIdsAreTheShippedStrings();
|
||||||
|
|
||||||
if (g_fail == 0) std::printf("render_settings: all tests passed\n");
|
if (g_fail == 0) std::printf("render_settings: all tests passed\n");
|
||||||
else std::printf("render_settings: %d CHECK(s) FAILED\n", g_fail);
|
else std::printf("render_settings: %d CHECK(s) FAILED\n", g_fail);
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
// Standalone tests for reasampler::tail_control — no REAPER, no framework. Covers
|
||||||
|
// the pure pieces behind the docked panel's tail-mode toggle: the cycle order
|
||||||
|
// (None -> Auto -> Manual -> None), the manual-length clamp to the 8 s cap, and the
|
||||||
|
// toggle label text. The drawing / click hit-testing is DAW-verified in bank_panel.
|
||||||
|
|
||||||
|
#include "../src/tail_control.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using namespace reasampler;
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
// --- cycleTailMode: the toggle order ------------------------------------------
|
||||||
|
|
||||||
|
static void testCycleOrderIsNoneAutoManualNone() {
|
||||||
|
// None -> Auto -> Manual -> None, wrapping. The click handler relies on exactly
|
||||||
|
// this order; a reorder (e.g. skipping Manual) would fail here.
|
||||||
|
CHECK(cycleTailMode(TailMode::None) == TailMode::Auto);
|
||||||
|
CHECK(cycleTailMode(TailMode::Auto) == TailMode::Manual);
|
||||||
|
CHECK(cycleTailMode(TailMode::Manual) == TailMode::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testCycleThreeStepsReturnsToStart() {
|
||||||
|
// Three cycles from any state land back on that state (a full lap of the 3-cycle).
|
||||||
|
TailMode m = TailMode::None;
|
||||||
|
m = cycleTailMode(m);
|
||||||
|
m = cycleTailMode(m);
|
||||||
|
m = cycleTailMode(m);
|
||||||
|
CHECK(m == TailMode::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- clampManualMs: the runaway guard -----------------------------------------
|
||||||
|
|
||||||
|
static void testManualClampInRangeIsUnchanged() {
|
||||||
|
// A value inside [0, kMaxTailMs] passes through untouched.
|
||||||
|
CHECK(clampManualMs(kDefaultManualTailMs) == kDefaultManualTailMs);
|
||||||
|
CHECK(clampManualMs(0.0) == 0.0);
|
||||||
|
CHECK(clampManualMs(kMaxTailMs) == kMaxTailMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testManualClampCapsAtEightSeconds() {
|
||||||
|
// Over the 8 s cap clamps to kMaxTailMs; negative floors to 0. This mirrors the
|
||||||
|
// clamp in tailRenderSettingsFor, so the panel and the render agree on the bound.
|
||||||
|
CHECK(clampManualMs(kMaxTailMs + 5000.0) == kMaxTailMs);
|
||||||
|
CHECK(clampManualMs(-100.0) == 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- tailToggleLabel: the exact strings the panel draws -----------------------
|
||||||
|
|
||||||
|
static void testLabelStringsPerMode() {
|
||||||
|
TailSetting off; off.mode = TailMode::None;
|
||||||
|
TailSetting autoM; autoM.mode = TailMode::Auto;
|
||||||
|
TailSetting man; man.mode = TailMode::Manual;
|
||||||
|
CHECK(tailToggleLabel(off) == "Tail: Off");
|
||||||
|
CHECK(tailToggleLabel(autoM) == "Tail: Auto");
|
||||||
|
CHECK(tailToggleLabel(man) == "Tail: Manual");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testDefaultSettingIsOff() {
|
||||||
|
// The zero-value setting is None (Off) with the 2 s manual default — the safe
|
||||||
|
// default the panel starts in so captures stay exact-bounds until opt-in.
|
||||||
|
TailSetting s;
|
||||||
|
CHECK(s.mode == TailMode::None);
|
||||||
|
CHECK(s.manualMs == kDefaultManualTailMs);
|
||||||
|
CHECK(tailToggleLabel(s) == "Tail: Off");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testCycleOrderIsNoneAutoManualNone();
|
||||||
|
testCycleThreeStepsReturnsToStart();
|
||||||
|
testManualClampInRangeIsUnchanged();
|
||||||
|
testManualClampCapsAtEightSeconds();
|
||||||
|
testLabelStringsPerMode();
|
||||||
|
testDefaultSettingIsOff();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("tail_control: all tests passed\n");
|
||||||
|
else std::printf("tail_control: %d CHECK(s) FAILED\n", g_fail);
|
||||||
|
return g_fail == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user