Merge tail follow-ons: Manual fine-adjust (scroll) + per-project persistence
This commit is contained in:
+98
-28
@@ -77,6 +77,7 @@
|
|||||||
#define REAPERAPI_WANT_DockWindowActivate
|
#define REAPERAPI_WANT_DockWindowActivate
|
||||||
#define REAPERAPI_WANT_DockWindowRemove
|
#define REAPERAPI_WANT_DockWindowRemove
|
||||||
#define REAPERAPI_WANT_EnumProjects
|
#define REAPERAPI_WANT_EnumProjects
|
||||||
|
#define REAPERAPI_WANT_MarkProjectDirty // mark dirty when the tail toggle changes (saves with the project)
|
||||||
#define REAPERAPI_WANT_GetMainHwnd
|
#define REAPERAPI_WANT_GetMainHwnd
|
||||||
#define REAPERAPI_WANT_PCM_Source_CreateFromFile
|
#define REAPERAPI_WANT_PCM_Source_CreateFromFile
|
||||||
#define REAPERAPI_WANT_PCM_Source_Destroy
|
#define REAPERAPI_WANT_PCM_Source_Destroy
|
||||||
@@ -204,11 +205,13 @@ 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) ---------------------------------------
|
// --- Tail-mode toggle -----------------------------------------------------
|
||||||
// The current tail setting the plain capture actions read (bankPanelTailSetting).
|
// The authoritative tail setting now lives in ReaSamplerSession (session->tail()),
|
||||||
// Default None (exact bounds). In-memory only — resets on panel teardown; project
|
// NOT in panel state, so it travels inside the .rpp (persist serializes it on save,
|
||||||
// persistence is a noted follow-on. Mutated ONLY by a click in the footer strip.
|
// restores it on project load). The panel reads it for drawing and mutates it via
|
||||||
TailSetting tail;
|
// the footer click (cycle mode) and scroll-wheel (Manual fine-adjust), marking the
|
||||||
|
// project dirty so the choice saves. bankPanelTailSetting is the read seam for the
|
||||||
|
// capture actions. Held here only through the session pointer above.
|
||||||
|
|
||||||
// --- Audition preview (Wave B) --------------------------------------------
|
// --- Audition preview (Wave B) --------------------------------------------
|
||||||
//
|
//
|
||||||
@@ -492,9 +495,15 @@ RECT panelFooter(int w, int h) {
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The session's live tail setting (default None / 2 s when no session). Single read
|
||||||
|
// point so draw, wheel-adjust, and the capture read seam all agree on the source.
|
||||||
|
TailSetting currentTail() {
|
||||||
|
return g_panel.session ? g_panel.session->tail() : TailSetting{};
|
||||||
|
}
|
||||||
|
|
||||||
// Draws the tail-mode toggle into the footer strip: a filled band, a top divider,
|
// 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
|
// and the current mode's label ("Tail: Off / Auto / Manual Xs") from the pure
|
||||||
// tail_control module. READ-ONLY: reads g_panel.tail; the click handler mutates it.
|
// tail_control module. READ-ONLY: reads session->tail(); the input handlers mutate it.
|
||||||
void drawTailFooter(LICE_IBitmap* bmp, int w, int h) {
|
void drawTailFooter(LICE_IBitmap* bmp, int w, int h) {
|
||||||
const RECT f = panelFooter(w, h);
|
const RECT f = panelFooter(w, h);
|
||||||
if (f.top >= f.bottom) return; // no room — skip (short panel)
|
if (f.top >= f.bottom) return; // no room — skip (short panel)
|
||||||
@@ -505,7 +514,7 @@ void drawTailFooter(LICE_IBitmap* bmp, int w, int h) {
|
|||||||
|
|
||||||
HDC dc = bmp->getDC();
|
HDC dc = bmp->getDC();
|
||||||
if (!dc) return;
|
if (!dc) return;
|
||||||
const std::string label = tailToggleLabel(g_panel.tail);
|
const std::string label = tailToggleLabel(currentTail());
|
||||||
RECT rc = f;
|
RECT rc = f;
|
||||||
rc.left += 8; // small left pad so the label is not flush against the edge
|
rc.left += 8; // small left pad so the label is not flush against the edge
|
||||||
SetTextColor(dc, kRgbFooterText);
|
SetTextColor(dc, kRgbFooterText);
|
||||||
@@ -514,6 +523,30 @@ void drawTailFooter(LICE_IBitmap* bmp, int w, int h) {
|
|||||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
|
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// True iff client-relative (x, y) falls inside the (non-degenerate) footer strip.
|
||||||
|
// Shared by the footer click (cycle mode) and the scroll-wheel (Manual fine-adjust)
|
||||||
|
// so both agree on the hit target.
|
||||||
|
bool pointInFooter(int x, int y) {
|
||||||
|
if (!g_panel.hwnd) return false;
|
||||||
|
RECT cr{};
|
||||||
|
GetClientRect(g_panel.hwnd, &cr);
|
||||||
|
const RECT f = panelFooter(cr.right - cr.left, cr.bottom - cr.top);
|
||||||
|
return f.top < f.bottom && x >= f.left && x < f.right && y >= f.top && y < f.bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commits the current tail setting to ext state and marks the active project dirty
|
||||||
|
// so the change travels inside the .rpp on Ctrl+S. saveToActiveProject() is the only
|
||||||
|
// path that calls SetProjExtState for the tail key — calling it here closes the gap
|
||||||
|
// where toggle/scroll would dirty the project but the new value was never written.
|
||||||
|
// On an unsaved project saveToActiveProject() no-ops cleanly (documented in persist.h).
|
||||||
|
// MarkProjectDirty runs unconditionally so REAPER knows a save is owed either way.
|
||||||
|
// NON-DESTRUCTIVE: touches nothing in the bank/arrange.
|
||||||
|
void markTailDirty() {
|
||||||
|
if (g_panel.session) g_panel.session->saveToActiveProject();
|
||||||
|
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||||
|
if (proj) MarkProjectDirty(proj);
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -902,20 +935,17 @@ void handleClick(int x, int y) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tail-mode footer: a click anywhere in the bottom strip cycles the tail mode
|
// 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
|
// (None -> Auto -> Manual -> None) and repaints. It mutates the SESSION's tail
|
||||||
// panel's in-memory tail setting the capture actions read, and NOTHING in the
|
// setting (which the capture actions read and persist saves with the project) and
|
||||||
// project/bank/arrange. Checked before the grid so a footer click never selects.
|
// marks the project dirty so the choice travels inside the .rpp — it touches
|
||||||
{
|
// NOTHING in the bank/arrange. Checked before the grid so a footer click never
|
||||||
RECT cr{};
|
// selects.
|
||||||
GetClientRect(g_panel.hwnd, &cr);
|
if (g_panel.session && pointInFooter(x, y)) {
|
||||||
const int w = cr.right - cr.left;
|
TailSetting& tail = g_panel.session->tail();
|
||||||
const int h = cr.bottom - cr.top;
|
tail.mode = cycleTailMode(tail.mode);
|
||||||
const RECT f = panelFooter(w, h);
|
markTailDirty();
|
||||||
if (f.top < f.bottom && x >= f.left && x < f.right && y >= f.top && y < f.bottom) {
|
invalidatePanel();
|
||||||
g_panel.tail.mode = cycleTailMode(g_panel.tail.mode);
|
return; // footer click consumed; do NOT fall through to grid selection
|
||||||
invalidatePanel();
|
|
||||||
return; // footer click consumed; do NOT fall through to grid selection
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<CellRect> rects = panelRects();
|
const std::vector<CellRect> rects = panelRects();
|
||||||
@@ -938,6 +968,34 @@ void handleClick(int x, int y) {
|
|||||||
invalidatePanel();
|
invalidatePanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handles a scroll-wheel notch over client (x, y) with signed wheel delta `delta`.
|
||||||
|
// Fine-adjusts the Manual tail length in kManualStepMs steps ONLY when the cursor is
|
||||||
|
// over the footer strip AND the mode is Manual — wheel up lengthens, down shortens,
|
||||||
|
// clamped to [0, kMaxTailMs]. In Off/Auto (or off the footer) it does nothing (returns
|
||||||
|
// false so the caller can let REAPER/the docker handle the wheel normally). On a real
|
||||||
|
// change it mutates the SESSION's tail setting, marks the project dirty (so it saves),
|
||||||
|
// and repaints the live length. Returns true iff the wheel was consumed.
|
||||||
|
bool handleWheel(int x, int y, int delta) {
|
||||||
|
if (!g_panel.session) return false;
|
||||||
|
if (!pointInFooter(x, y)) return false;
|
||||||
|
|
||||||
|
TailSetting& tail = g_panel.session->tail();
|
||||||
|
if (tail.mode != TailMode::Manual) return false; // fine-adjust is Manual-only
|
||||||
|
|
||||||
|
// One notch is WHEEL_DELTA (120); accumulate whole notches so a high-res trackpad
|
||||||
|
// that sends fractional deltas still steps predictably. Sign carries direction.
|
||||||
|
const int notches = delta / 120;
|
||||||
|
if (notches == 0) return false; // sub-notch movement — nothing to apply yet
|
||||||
|
|
||||||
|
const double before = tail.manualMs;
|
||||||
|
tail.manualMs = adjustManualMs(tail.manualMs, notches, kManualStepMs);
|
||||||
|
if (tail.manualMs == before) return true; // already at a bound — consumed, no change
|
||||||
|
|
||||||
|
markTailDirty();
|
||||||
|
invalidatePanel(); // label shows the new length live
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// The column count for the panel's CURRENT client width (nav needs the same wrap
|
// The column count for the panel's CURRENT client width (nav needs the same wrap
|
||||||
// the layout uses). >= 1.
|
// the layout uses). >= 1.
|
||||||
int columnsNow() {
|
int columnsNow() {
|
||||||
@@ -1047,6 +1105,19 @@ WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
|||||||
handleClick(x, y);
|
handleClick(x, y);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
case WM_MOUSEWHEEL: {
|
||||||
|
// Fine-adjust the Manual tail length when the wheel is over the footer.
|
||||||
|
// UNLIKE the button messages, WM_MOUSEWHEEL carries SCREEN coordinates in
|
||||||
|
// lParam (Win32 and SWELL agree — swell-generic-gdk.cpp §WM_MOUSEWHEEL), so
|
||||||
|
// convert to client space before hit-testing the footer. The signed wheel
|
||||||
|
// delta is the HIWORD of wParam (SWELL packs it as (delta<<16), delta=+/-120,
|
||||||
|
// matching GET_WHEEL_DELTA_WPARAM). Consume (return 1) only when the footer
|
||||||
|
// handler acts, so scrolling elsewhere in the dock still behaves normally.
|
||||||
|
POINT pt{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
|
||||||
|
ScreenToClient(hwnd, &pt);
|
||||||
|
const int delta = static_cast<short>(HIWORD(wParam));
|
||||||
|
return handleWheel(pt.x, pt.y, delta) ? 1 : 0;
|
||||||
|
}
|
||||||
case WM_DESTROY:
|
case WM_DESTROY:
|
||||||
// REAPER closed the dock (user X'd it). Stop any audition (window-close
|
// REAPER closed the dock (user X'd it). Stop any audition (window-close
|
||||||
// stop path — no preview may outlive the window) and reflect closed
|
// stop path — no preview may outlive the window) and reflect closed
|
||||||
@@ -1156,12 +1227,11 @@ void bankPanelRefresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TailSetting bankPanelTailSetting() {
|
TailSetting bankPanelTailSetting() {
|
||||||
// In-memory for the extension's lifetime (g_panel is static): the toggle's mode
|
// The authoritative setting lives in the session (session->tail()) so it travels
|
||||||
// survives panel open/close and bank changes, and resets to the default None only
|
// inside the .rpp: it loads per project and saves with the project. This stays the
|
||||||
// on extension unload. Persistence across project reload is a noted follow-on.
|
// read seam for the capture actions. manualMs is clamped here so a caller always
|
||||||
// manualMs is clamped here so a caller always receives a within-cap length, even
|
// receives a within-cap length regardless of what was stored/scrolled.
|
||||||
// if a future fine-adjust UI stored an over-cap value.
|
TailSetting s = currentTail();
|
||||||
TailSetting s = g_panel.tail;
|
|
||||||
s.manualMs = clampManualMs(s.manualMs);
|
s.manualMs = clampManualMs(s.manualMs);
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,6 +184,13 @@ void ReaSamplerSession::saveToActiveProject() {
|
|||||||
SetProjExtState(static_cast<ReaProject*>(proj), kProjExtNamespace,
|
SetProjExtState(static_cast<ReaProject*>(proj), kProjExtNamespace,
|
||||||
kProjExtViewKey, viewJson.c_str());
|
kProjExtViewKey, viewJson.c_str());
|
||||||
|
|
||||||
|
// Additive: the docked panel's tail setting rides alongside in its own key, so the
|
||||||
|
// tail choice travels inside the .rpp. Independent write — does not disturb the
|
||||||
|
// bank_index or view_state above.
|
||||||
|
const std::string tailJson = serializeTailSetting(tail_);
|
||||||
|
SetProjExtState(static_cast<ReaProject*>(proj), kProjExtNamespace,
|
||||||
|
kProjExtTailKey, tailJson.c_str());
|
||||||
|
|
||||||
MarkProjectDirty(static_cast<ReaProject*>(proj));
|
MarkProjectDirty(static_cast<ReaProject*>(proj));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,6 +215,23 @@ ViewModeModel loadViewModel(ReaProject* proj) {
|
|||||||
return std::move(*loaded);
|
return std::move(*loaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the tail setting from a project's tail_setting key, or return the default. An
|
||||||
|
// absent/empty key (older / never-adjusted project) yields the default setting (None /
|
||||||
|
// 2 s manual) — graceful, never a crash. Malformed JSON is warned and also falls back
|
||||||
|
// to default, mirroring the bank's and view's malformed handling.
|
||||||
|
TailSetting loadTailSetting(ReaProject* proj) {
|
||||||
|
if (!proj) return TailSetting{};
|
||||||
|
const std::string tailJson =
|
||||||
|
getProjExtStateString(proj, kProjExtNamespace, kProjExtTailKey);
|
||||||
|
if (tailJson.empty()) return TailSetting{}; // no stored setting -> default
|
||||||
|
std::optional<TailSetting> loaded = deserializeTailSetting(tailJson);
|
||||||
|
if (!loaded) {
|
||||||
|
ShowConsoleMsg("ReaSampler: stored tail setting is malformed — ignoring.\n");
|
||||||
|
return TailSetting{};
|
||||||
|
}
|
||||||
|
return *loaded;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void ReaSamplerSession::loadFromProject(void* proj, const std::string& projectDir) {
|
void ReaSamplerSession::loadFromProject(void* proj, const std::string& projectDir) {
|
||||||
@@ -225,6 +249,11 @@ void ReaSamplerSession::loadFromProject(void* proj, const std::string& projectDi
|
|||||||
// only — no visibility/processing is applied here (that is D4).
|
// only — no visibility/processing is applied here (that is D4).
|
||||||
view_ = loadViewModel(static_cast<ReaProject*>(proj));
|
view_ = loadViewModel(static_cast<ReaProject*>(proj));
|
||||||
|
|
||||||
|
// The tail setting is restored on EVERY load path too (peer-symmetry): switching
|
||||||
|
// to a project with no stored setting must fall back to the default, not inherit
|
||||||
|
// the previous project's choice (this REPLACES the old session-carry behavior).
|
||||||
|
tail_ = loadTailSetting(static_cast<ReaProject*>(proj));
|
||||||
|
|
||||||
if (!proj) {
|
if (!proj) {
|
||||||
bank_ = BankIndex{};
|
bank_ = BankIndex{};
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "bank_model.h"
|
#include "bank_model.h"
|
||||||
|
#include "tail_control.h"
|
||||||
#include "view_mode_model.h"
|
#include "view_mode_model.h"
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
@@ -38,6 +39,13 @@ inline constexpr const char* kProjExtIndexKey = "bank_index";
|
|||||||
// FOREVER-STABLE: changing it orphans every already-saved project's view state.
|
// FOREVER-STABLE: changing it orphans every already-saved project's view state.
|
||||||
inline constexpr const char* kProjExtViewKey = "view_state";
|
inline constexpr const char* kProjExtViewKey = "view_state";
|
||||||
|
|
||||||
|
// The ext-state key the docked panel's TailSetting JSON (mode + manualMs) is stored
|
||||||
|
// under, so the tail choice travels inside the .rpp and loads per project. Distinct
|
||||||
|
// from the index/view keys — one namespace, three keys. FOREVER-STABLE: changing it
|
||||||
|
// orphans every already-saved project's tail setting (which then falls back to the
|
||||||
|
// default — graceful, but the user's saved choice would be lost).
|
||||||
|
inline constexpr const char* kProjExtTailKey = "tail_setting";
|
||||||
|
|
||||||
// The ext-state key holding a GUID we mint per project to establish CONTENT-BASED
|
// The ext-state key holding a GUID we mint per project to establish CONTENT-BASED
|
||||||
// project identity (REAPER exposes no stable per-project GUID). poll() uses it to
|
// project identity (REAPER exposes no stable per-project GUID). poll() uses it to
|
||||||
// tell a genuine Save-As (same GUID, new .rpp path) apart from a project switch
|
// tell a genuine Save-As (same GUID, new .rpp path) apart from a project switch
|
||||||
@@ -80,6 +88,15 @@ public:
|
|||||||
ViewModeModel& view() { return view_; }
|
ViewModeModel& view() { return view_; }
|
||||||
const ViewModeModel& view() const { return view_; }
|
const ViewModeModel& view() const { return view_; }
|
||||||
|
|
||||||
|
// The docked panel's tail setting (mode + manualMs), authoritative here — NOT in
|
||||||
|
// panel state — so it travels inside the .rpp: persist serializes it on save and
|
||||||
|
// replaces it on project load exactly as it treats the bank and view model. The
|
||||||
|
// panel reads/writes it through this seam (bank_panel holds the session), and the
|
||||||
|
// capture actions read it via bankPanelTailSetting. Default None / 2 s manual for
|
||||||
|
// an unsaved or pre-feature project (no stored key -> this default survives load).
|
||||||
|
TailSetting& tail() { return tail_; }
|
||||||
|
const TailSetting& tail() const { return tail_; }
|
||||||
|
|
||||||
// Serialize the current bank to the active project's ext state (namespace
|
// Serialize the current bank to the active project's ext state (namespace
|
||||||
// "reasampler"). Non-destructive beyond writing our own ext-state key. Safe
|
// "reasampler"). Non-destructive beyond writing our own ext-state key. Safe
|
||||||
// to call when there is no active/saved project (it no-ops).
|
// to call when there is no active/saved project (it no-ops).
|
||||||
@@ -110,6 +127,11 @@ private:
|
|||||||
// view_state (older project), so an absent key is graceful, not a crash.
|
// view_state (older project), so an absent key is graceful, not a crash.
|
||||||
ViewModeModel view_;
|
ViewModeModel view_;
|
||||||
|
|
||||||
|
// The tail setting. Default None / kDefaultManualTailMs; loadFromProject resets it
|
||||||
|
// to this default when a project has no stored tail_setting key (older / never-
|
||||||
|
// adjusted project), so an absent key is graceful. Peer to bank_/view_.
|
||||||
|
TailSetting tail_;
|
||||||
|
|
||||||
// The project identity last observed by poll(), used to detect load/Save-As.
|
// The project identity last observed by poll(), used to detect load/Save-As.
|
||||||
// The GUID is the PRIMARY signal (a different stored GUID = a different project
|
// The GUID is the PRIMARY signal (a different stored GUID = a different project
|
||||||
// of record = Load, immune to pointer recycling). The pointer disambiguates the
|
// of record = Load, immune to pointer recycling). The pointer disambiguates the
|
||||||
|
|||||||
+102
-3
@@ -3,6 +3,10 @@
|
|||||||
#include "tail_control.h"
|
#include "tail_control.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
@@ -21,13 +25,108 @@ double clampManualMs(double manualMs) {
|
|||||||
return std::clamp(manualMs, 0.0, kMaxTailMs);
|
return std::clamp(manualMs, 0.0, kMaxTailMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double adjustManualMs(double current, int notches, double stepMs) {
|
||||||
|
// Clamp the stepped value so both scroll directions saturate at the bounds rather
|
||||||
|
// than running away (the same [0, kMaxTailMs] guard clampManualMs enforces).
|
||||||
|
return clampManualMs(current + notches * stepMs);
|
||||||
|
}
|
||||||
|
|
||||||
std::string tailToggleLabel(const TailSetting& setting) {
|
std::string tailToggleLabel(const TailSetting& setting) {
|
||||||
switch (setting.mode) {
|
switch (setting.mode) {
|
||||||
case TailMode::None: return "Tail: Off";
|
case TailMode::None: return "Tail: Off";
|
||||||
case TailMode::Auto: return "Tail: Auto";
|
case TailMode::Auto: return "Tail: Auto";
|
||||||
case TailMode::Manual: return "Tail: Manual";
|
case TailMode::Manual: {
|
||||||
|
// Append the CLAMPED length in seconds to one decimal so the readout can
|
||||||
|
// never show an over-cap value even if manualMs was stored past the cap.
|
||||||
|
const double seconds = clampManualMs(setting.manualMs) / 1000.0;
|
||||||
|
char buf[32];
|
||||||
|
std::snprintf(buf, sizeof(buf), "Tail: Manual %.1fs", seconds);
|
||||||
|
return std::string(buf);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return "Tail: Off"; // unreachable for a valid enum; fail to the safe default
|
return "Tail: Off"; // unreachable for a valid enum; fail to the safe default
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// JSON round-trip
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// The setting is a flat object of one enum + one double, so a compact hand-rolled
|
||||||
|
// writer + a tolerant minimal reader is the simplest thing that works (mirroring
|
||||||
|
// bank_model's dependency-free JSON choice). manualMs is emitted with 17 significant
|
||||||
|
// digits (%.17g) — the shortest form that round-trips every IEEE-754 double exactly —
|
||||||
|
// so deserialize(serialize(x)) == x holds bit-for-bit. deserialize is deliberately
|
||||||
|
// forgiving: any parse failure returns nullopt so the caller falls back to a default,
|
||||||
|
// exactly as an absent ext-state key does.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// The persisted integer for a mode. Stable forever (stored in the .rpp): never
|
||||||
|
// renumber these values or an already-saved project reads back the wrong mode.
|
||||||
|
int modeToInt(TailMode m) {
|
||||||
|
switch (m) {
|
||||||
|
case TailMode::None: return 0;
|
||||||
|
case TailMode::Auto: return 1;
|
||||||
|
case TailMode::Manual: return 2;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<TailMode> modeFromInt(int v) {
|
||||||
|
switch (v) {
|
||||||
|
case 0: return TailMode::None;
|
||||||
|
case 1: return TailMode::Auto;
|
||||||
|
case 2: return TailMode::Manual;
|
||||||
|
default: return std::nullopt; // unknown enumerant -> malformed -> default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the value token following `"key":` in `json`. Returns a pointer just past the
|
||||||
|
// colon (skipping whitespace) or nullptr if the key is absent. Minimal: the writer
|
||||||
|
// emits exactly one flat object with unique keys, so a substring search is sufficient
|
||||||
|
// and there is no nesting to confuse it.
|
||||||
|
const char* valueAfterKey(const std::string& json, const char* key) {
|
||||||
|
const std::string needle = std::string("\"") + key + "\"";
|
||||||
|
const std::size_t pos = json.find(needle);
|
||||||
|
if (pos == std::string::npos) return nullptr;
|
||||||
|
const char* p = json.c_str() + pos + needle.size();
|
||||||
|
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') ++p;
|
||||||
|
if (*p != ':') return nullptr;
|
||||||
|
++p;
|
||||||
|
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') ++p;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::string serializeTailSetting(const TailSetting& setting) {
|
||||||
|
char buf[128];
|
||||||
|
std::snprintf(buf, sizeof(buf), "{\"mode\":%d,\"manualMs\":%.17g}",
|
||||||
|
modeToInt(setting.mode), setting.manualMs);
|
||||||
|
return std::string(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<TailSetting> deserializeTailSetting(const std::string& json) {
|
||||||
|
const char* modeTok = valueAfterKey(json, "mode");
|
||||||
|
const char* msTok = valueAfterKey(json, "manualMs");
|
||||||
|
if (!modeTok || !msTok) return std::nullopt; // absent key -> malformed -> default
|
||||||
|
|
||||||
|
char* end = nullptr;
|
||||||
|
errno = 0;
|
||||||
|
const long modeVal = std::strtol(modeTok, &end, 10);
|
||||||
|
if (end == modeTok || errno != 0) return std::nullopt;
|
||||||
|
const std::optional<TailMode> mode = modeFromInt(static_cast<int>(modeVal));
|
||||||
|
if (!mode) return std::nullopt;
|
||||||
|
|
||||||
|
end = nullptr;
|
||||||
|
errno = 0;
|
||||||
|
const double ms = std::strtod(msTok, &end);
|
||||||
|
if (end == msTok || errno != 0) return std::nullopt;
|
||||||
|
|
||||||
|
TailSetting out;
|
||||||
|
out.mode = *mode;
|
||||||
|
out.manualMs = ms;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace reasampler
|
} // namespace reasampler
|
||||||
|
|||||||
+26
-5
@@ -9,6 +9,7 @@
|
|||||||
// only (plus render_settings for the pure TailMode enum). Builds and unit-tests
|
// only (plus render_settings for the pure TailMode enum). Builds and unit-tests
|
||||||
// without REAPER.
|
// without REAPER.
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "render_settings.h" // TailMode (pure enum) — the three-state tail contract
|
#include "render_settings.h" // TailMode (pure enum) — the three-state tail contract
|
||||||
@@ -16,10 +17,15 @@
|
|||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
// The Manual-mode starting length. 2 s is a musically useful default tail (a bar of
|
// 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
|
// reverb throw at a moderate tempo) that is well under the 8 s cap. Also the value a
|
||||||
// (+/- click zones or scroll) is a noted follow-on; this pass ships a fixed default.
|
// project with no stored tail setting (older / never-adjusted) falls back to on load.
|
||||||
inline constexpr double kDefaultManualTailMs = 2000.0;
|
inline constexpr double kDefaultManualTailMs = 2000.0;
|
||||||
|
|
||||||
|
// The fine-adjust step per scroll-wheel notch in Manual mode. 250 ms is coarse enough
|
||||||
|
// that a few notches cover the useful range, fine enough to dial a length precisely.
|
||||||
|
// Daniel-set. The panel maps one wheel notch to +/- this many ms via adjustManualMs.
|
||||||
|
inline constexpr double kManualStepMs = 250.0;
|
||||||
|
|
||||||
// The panel's current tail setting: the mode plus the length used ONLY when the
|
// 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
|
// 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
|
// None so a capture with no explicit choice stays exact-bounds / byte-identical to
|
||||||
@@ -41,9 +47,24 @@ TailMode cycleTailMode(TailMode current);
|
|||||||
// tailMs into the CaptureRequest. Meaningful only for TailMode::Manual.
|
// tailMs into the CaptureRequest. Meaningful only for TailMode::Manual.
|
||||||
double clampManualMs(double manualMs);
|
double clampManualMs(double manualMs);
|
||||||
|
|
||||||
// The toggle's label for a setting, e.g. "Tail: Off", "Tail: Auto", "Tail: Manual".
|
// Applies `notches` scroll-wheel steps of `stepMs` each to `current`, clamped to
|
||||||
// (Manual omits the length here — the panel is unobtrusive; a length readout can be
|
// [0, kMaxTailMs]. Positive notches lengthen, negative shorten. Pure so the fine-adjust
|
||||||
// added with the fine-adjust follow-on.) Pure so the exact strings are test-pinned.
|
// arithmetic (and its clamp at both bounds) is unit-tested; the panel wheel handler
|
||||||
|
// owns no arithmetic of its own. Meaningful only for TailMode::Manual.
|
||||||
|
double adjustManualMs(double current, int notches, double stepMs);
|
||||||
|
|
||||||
|
// The toggle's label for a setting, e.g. "Tail: Off", "Tail: Auto". In Manual mode the
|
||||||
|
// clamped length is appended in seconds to one decimal, e.g. "Tail: Manual 2.0s" —
|
||||||
|
// Off/Auto carry no length. Pure so the exact strings (and the Manual format) are
|
||||||
|
// test-pinned, including the boundary lengths (0.0s, 8.0s).
|
||||||
std::string tailToggleLabel(const TailSetting& setting);
|
std::string tailToggleLabel(const TailSetting& setting);
|
||||||
|
|
||||||
|
// JSON round-trip of a TailSetting (mode + manualMs), for persist to store the tail
|
||||||
|
// setting per-project alongside the bank and view model. Kept pure/testable here —
|
||||||
|
// the natural home, mirroring bank_model's serialize/deserialize. serialize emits a
|
||||||
|
// compact object; deserialize returns std::nullopt on malformed input so the caller
|
||||||
|
// (persist) falls back to a default setting, exactly as an absent key does.
|
||||||
|
std::string serializeTailSetting(const TailSetting& setting);
|
||||||
|
std::optional<TailSetting> deserializeTailSetting(const std::string& json);
|
||||||
|
|
||||||
} // namespace reasampler
|
} // namespace reasampler
|
||||||
|
|||||||
@@ -49,15 +49,53 @@ static void testManualClampCapsAtEightSeconds() {
|
|||||||
CHECK(clampManualMs(-100.0) == 0.0);
|
CHECK(clampManualMs(-100.0) == 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- adjustManualMs: the scroll-wheel fine-adjust arithmetic ------------------
|
||||||
|
|
||||||
|
static void testAdjustUpAndDownBySteps() {
|
||||||
|
// Positive notches lengthen, negative shorten, in whole kManualStepMs increments.
|
||||||
|
CHECK(adjustManualMs(2000.0, 1, kManualStepMs) == 2250.0);
|
||||||
|
CHECK(adjustManualMs(2000.0, -1, kManualStepMs) == 1750.0);
|
||||||
|
CHECK(adjustManualMs(2000.0, 4, kManualStepMs) == 3000.0); // 4 * 250
|
||||||
|
CHECK(adjustManualMs(2000.0, 0, kManualStepMs) == 2000.0); // no notch, no move
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testAdjustClampsAtUpperBound() {
|
||||||
|
// Scrolling up past the 8 s cap saturates AT the cap, never beyond.
|
||||||
|
CHECK(adjustManualMs(kMaxTailMs, 1, kManualStepMs) == kMaxTailMs);
|
||||||
|
CHECK(adjustManualMs(kMaxTailMs - 100.0, 10, kManualStepMs) == kMaxTailMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testAdjustClampsAtLowerBound() {
|
||||||
|
// Scrolling down past 0 floors at 0, never negative.
|
||||||
|
CHECK(adjustManualMs(0.0, -1, kManualStepMs) == 0.0);
|
||||||
|
CHECK(adjustManualMs(100.0, -10, kManualStepMs) == 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
// --- tailToggleLabel: the exact strings the panel draws -----------------------
|
// --- tailToggleLabel: the exact strings the panel draws -----------------------
|
||||||
|
|
||||||
static void testLabelStringsPerMode() {
|
static void testLabelStringsPerMode() {
|
||||||
TailSetting off; off.mode = TailMode::None;
|
TailSetting off; off.mode = TailMode::None;
|
||||||
TailSetting autoM; autoM.mode = TailMode::Auto;
|
TailSetting autoM; autoM.mode = TailMode::Auto;
|
||||||
TailSetting man; man.mode = TailMode::Manual;
|
// Off/Auto carry NO length regardless of manualMs.
|
||||||
|
off.manualMs = 5000.0;
|
||||||
|
autoM.manualMs = 5000.0;
|
||||||
CHECK(tailToggleLabel(off) == "Tail: Off");
|
CHECK(tailToggleLabel(off) == "Tail: Off");
|
||||||
CHECK(tailToggleLabel(autoM) == "Tail: Auto");
|
CHECK(tailToggleLabel(autoM) == "Tail: Auto");
|
||||||
CHECK(tailToggleLabel(man) == "Tail: Manual");
|
}
|
||||||
|
|
||||||
|
static void testManualLabelRendersLengthInSeconds() {
|
||||||
|
// Manual appends the length in seconds to one decimal — pin the format and the
|
||||||
|
// boundary values (0.0s, the 2 s default, the 8 s cap).
|
||||||
|
TailSetting man; man.mode = TailMode::Manual;
|
||||||
|
man.manualMs = 0.0;
|
||||||
|
CHECK(tailToggleLabel(man) == "Tail: Manual 0.0s");
|
||||||
|
man.manualMs = kDefaultManualTailMs; // 2000 ms
|
||||||
|
CHECK(tailToggleLabel(man) == "Tail: Manual 2.0s");
|
||||||
|
man.manualMs = kMaxTailMs; // 8000 ms
|
||||||
|
CHECK(tailToggleLabel(man) == "Tail: Manual 8.0s");
|
||||||
|
// An over-cap stored value renders at the CLAMPED length, never past the cap.
|
||||||
|
man.manualMs = kMaxTailMs + 3000.0;
|
||||||
|
CHECK(tailToggleLabel(man) == "Tail: Manual 8.0s");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void testDefaultSettingIsOff() {
|
static void testDefaultSettingIsOff() {
|
||||||
@@ -69,13 +107,65 @@ static void testDefaultSettingIsOff() {
|
|||||||
CHECK(tailToggleLabel(s) == "Tail: Off");
|
CHECK(tailToggleLabel(s) == "Tail: Off");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- serialize/deserialize: per-project persistence round-trip ----------------
|
||||||
|
|
||||||
|
static bool settingsEqual(const TailSetting& a, const TailSetting& b) {
|
||||||
|
return a.mode == b.mode && a.manualMs == b.manualMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testRoundTripNoneDefault() {
|
||||||
|
TailSetting s; // None + 2 s default
|
||||||
|
auto back = deserializeTailSetting(serializeTailSetting(s));
|
||||||
|
CHECK(back.has_value());
|
||||||
|
CHECK(back && settingsEqual(*back, s));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testRoundTripManualArbitraryMs() {
|
||||||
|
// A non-round manual length must round-trip bit-for-bit (17-sig-digit emit).
|
||||||
|
TailSetting s; s.mode = TailMode::Manual; s.manualMs = 3141.592653589793;
|
||||||
|
auto back = deserializeTailSetting(serializeTailSetting(s));
|
||||||
|
CHECK(back.has_value());
|
||||||
|
CHECK(back && settingsEqual(*back, s));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testRoundTripAuto() {
|
||||||
|
TailSetting s; s.mode = TailMode::Auto; s.manualMs = 500.0;
|
||||||
|
auto back = deserializeTailSetting(serializeTailSetting(s));
|
||||||
|
CHECK(back.has_value());
|
||||||
|
CHECK(back && settingsEqual(*back, s));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testDeserializeEmptyIsDefault() {
|
||||||
|
// An absent/empty stored value (older project) -> nullopt, so the caller falls
|
||||||
|
// back to the default. This is the graceful-old-project path the brief requires.
|
||||||
|
CHECK(!deserializeTailSetting("").has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testDeserializeMalformedIsDefault() {
|
||||||
|
// Garbage, a missing key, or an unknown mode enumerant -> nullopt (no crash).
|
||||||
|
CHECK(!deserializeTailSetting("not json at all").has_value());
|
||||||
|
CHECK(!deserializeTailSetting("{\"mode\":1}").has_value()); // manualMs missing
|
||||||
|
CHECK(!deserializeTailSetting("{\"manualMs\":2000}").has_value()); // mode missing
|
||||||
|
CHECK(!deserializeTailSetting("{\"mode\":9,\"manualMs\":2000}").has_value()); // bad enum
|
||||||
|
CHECK(!deserializeTailSetting("{\"mode\":x,\"manualMs\":2000}").has_value()); // non-numeric
|
||||||
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
testCycleOrderIsNoneAutoManualNone();
|
testCycleOrderIsNoneAutoManualNone();
|
||||||
testCycleThreeStepsReturnsToStart();
|
testCycleThreeStepsReturnsToStart();
|
||||||
testManualClampInRangeIsUnchanged();
|
testManualClampInRangeIsUnchanged();
|
||||||
testManualClampCapsAtEightSeconds();
|
testManualClampCapsAtEightSeconds();
|
||||||
|
testAdjustUpAndDownBySteps();
|
||||||
|
testAdjustClampsAtUpperBound();
|
||||||
|
testAdjustClampsAtLowerBound();
|
||||||
testLabelStringsPerMode();
|
testLabelStringsPerMode();
|
||||||
|
testManualLabelRendersLengthInSeconds();
|
||||||
testDefaultSettingIsOff();
|
testDefaultSettingIsOff();
|
||||||
|
testRoundTripNoneDefault();
|
||||||
|
testRoundTripManualArbitraryMs();
|
||||||
|
testRoundTripAuto();
|
||||||
|
testDeserializeEmptyIsDefault();
|
||||||
|
testDeserializeMalformedIsDefault();
|
||||||
|
|
||||||
if (g_fail == 0) std::printf("tail_control: all tests passed\n");
|
if (g_fail == 0) std::printf("tail_control: all tests passed\n");
|
||||||
else std::printf("tail_control: %d CHECK(s) FAILED\n", g_fail);
|
else std::printf("tail_control: %d CHECK(s) FAILED\n", g_fail);
|
||||||
|
|||||||
Reference in New Issue
Block a user