// tail_control — pure implementation. See tail_control.h. NO REAPER / SWELL / vendor. #include "tail_control.h" #include #include #include #include #include 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 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 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 mode = modeFromInt(static_cast(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