37affc6a70
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.
133 lines
4.9 KiB
C++
133 lines
4.9 KiB
C++
// tail_control — pure implementation. See tail_control.h. NO REAPER / SWELL / vendor.
|
|
|
|
#include "tail_control.h"
|
|
|
|
#include <algorithm>
|
|
#include <cerrno>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
|
|
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);
|
|
}
|
|
|
|
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: {
|
|
// 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
|