Add tail manual fine-adjust (scroll) + per-project tail persistence
Relocate TailSetting into ReaSamplerSession; persist serializes it to a new forever-stable "tail_setting" ext-state key (load-per-project, save-on-save). Footer scroll-wheel adjusts Manual length in 250ms steps; label now shows "Tail: Manual X.Xs". Pure step/label/JSON round-trip covered by tests.
This commit is contained in:
+102
-3
@@ -3,6 +3,10 @@
|
||||
#include "tail_control.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
@@ -21,13 +25,108 @@ double clampManualMs(double manualMs) {
|
||||
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) {
|
||||
switch (setting.mode) {
|
||||
case TailMode::None: return "Tail: Off";
|
||||
case TailMode::Auto: return "Tail: Auto";
|
||||
case TailMode::Manual: return "Tail: Manual";
|
||||
case TailMode::None: return "Tail: Off";
|
||||
case TailMode::Auto: return "Tail: Auto";
|
||||
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
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user