sc-stream-enum: Streaming a New Enum in shortcircuit-xt
Repo: surge-synthesizer/shortcircuit-xt. Paths are relative to the repo root.
This skill implements the full pattern for making an enum JSON-streamable in shortcircuit-xt. There are three touch points: the header (declaration), the .cpp (implementation), and the traits header (tao::json wiring).
For the wider serialization picture see the shortcircuit-streaming skill.
The Three-Part Pattern
Part 1: Header declaration (DECLARE_ENUM_STRING)
Inside the class or struct that owns the enum, add the macro immediately after the enum definition:
enum PlayMode
{
NORMAL,
ONE_SHOT,
ON_RELEASE,
};
DECLARE_ENUM_STRING(PlayMode);
DECLARE_ENUM_STRING(E) expands to:
static std::string toStringE(const E &);
static E fromStringE(const std::string &);
Both functions are static members of the enclosing class. The macro is defined in src/scxt-core/utils.h.
Part 2: .cpp implementation
Add two functions to the owning class's .cpp file. Use short, lowercase JSON-friendly names
for the string values (no spaces, avoid underscores where possible):
std::string OwnerClass::toStringPlayMode(const PlayMode &p)
{
switch (p)
{
case NORMAL:
return "normal";
case ONE_SHOT:
return "oneshot";
case ON_RELEASE:
return "onrelease";
}
return "normal"; // default = first/safest value
}
OwnerClass::PlayMode OwnerClass::fromStringPlayMode(const std::string &s)
{
static auto inverse = makeEnumInverse<OwnerClass::PlayMode, OwnerClass::toStringPlayMode>(
OwnerClass::PlayMode::NORMAL, OwnerClass::PlayMode::ON_RELEASE);
auto p = inverse.find(s);
if (p == inverse.end())
return NORMAL; // default = first/safest value
return p->second;
}
Key rules:
toStringXmust handle every enumerator and have a fallbackreturnafter the switch.fromStringXusesmakeEnumInverse<EnumType, toStringFn>(firstValue, lastValue)— the range must be contiguous integers (standard C++ enum without explicit values, or with consecutive values).- The
static auto inverseis constructed once (lazy, thread-safe in C++11+). - If the enum is not in a class, drop the
OwnerClass::prefix; both functions become free functions.
Part 3: Traits wiring (STREAM_ENUM in *_traits.h)
Add STREAM_ENUM(...) to the appropriate traits header in src/scxt-core/json/:
| Data domain | Traits file |
|---|---|
engine::Zone, engine::Group, engine::Part, engine::Bus, group triggers, clipboard |
engine_traits.h |
Modulation (modulation::*, LFO, step seq, etc.) |
modulation_traits.h |
| Top-level / cross-cutting | scxt_traits.h |
In practice nearly every STREAM_ENUM is in engine_traits.h, with a handful in
modulation_traits.h. Confirm where a sibling type went before picking:
grep -rn 'STREAM_ENUM(' src/scxt-core/json/
STREAM_ENUM(engine::Zone::PlayMode, engine::Zone::toStringPlayMode,
engine::Zone::fromStringPlayMode);
The macro arguments are: (1) fully-qualified enum type, (2) fully-qualified toString fn, (3) fully-qualified fromString fn.
STREAM_ENUM vs STREAM_ENUM_WITH_DEFAULT:
STREAM_ENUM— standard; always writes{"e": "value"}to JSON.STREAM_ENUM_WITH_DEFAULT— if the value equals the specified default, writes{}instead. Use sparingly; only when the enum is used in a single, tightly-controlled context. The default value is the second argument:STREAM_ENUM_WITH_DEFAULT(MyEnum, MyEnum::DEFAULT_VAL, toStr, fromStr).
Checklist
- Enum defined in the header with contiguous integer values (no explicit gaps)
-
DECLARE_ENUM_STRING(EnumName)added in the header, inside the owning class -
toStringswitch covers all enumerators; uses short lowercase strings; has a fallback return -
fromStringusesmakeEnumInversewith correct first/last enumerator range - Default in
fromStringmatches the first/safest enumerator -
STREAM_ENUM(...)added to the correct*_traits.hfile with fully-qualified names - Build passes (the macro expansion instantiates a
scxt_traits<E>specialization — duplicateSTREAM_ENUMfor the same type will cause a compile error)
Complete Example: Adding Zone::PlayMode
src/scxt-core/engine/zone.h (inside class Zone):
enum PlayMode
{
NORMAL,
ONE_SHOT,
ON_RELEASE,
};
DECLARE_ENUM_STRING(PlayMode);
src/scxt-core/engine/zone.cpp:
std::string Zone::toStringPlayMode(const PlayMode &p)
{
switch (p)
{
case NORMAL:
return "normal";
case ONE_SHOT:
return "oneshot";
case ON_RELEASE:
return "onrelease";
}
return "normal";
}
Zone::PlayMode Zone::fromStringPlayMode(const std::string &s)
{
static auto inverse = makeEnumInverse<Zone::PlayMode, Zone::toStringPlayMode>(
Zone::PlayMode::NORMAL, Zone::PlayMode::ON_RELEASE);
auto p = inverse.find(s);
if (p == inverse.end())
return NORMAL;
return p->second;
}
src/scxt-core/json/engine_traits.h (near other Zone STREAM_ENUMs):
STREAM_ENUM(engine::Zone::PlayMode, engine::Zone::toStringPlayMode,
engine::Zone::fromStringPlayMode);
Applying This Skill
When the user provides an enum, do the following:
- Read the header file where the enum is declared to confirm its enumerators and owning class.
- Read the .cpp file (same basename as the header) to see where other toString/fromString implementations live — add the new ones nearby for consistency.
- Identify the correct traits file from the domain table above. If uncertain, search for
existing
STREAM_ENUMcalls for sibling types and use the same file. - Generate all three pieces and insert them. Group
STREAM_ENUMlines with related ones. - Build to verify — a missing
#include "utils.h"or type mismatch inmakeEnumInversewill produce a clear compile error.
If the enum is a free-standing enum (not inside a class), the static keyword is dropped from
the function declarations and the OwnerClass:: prefix is absent everywhere.
Backward compatibility
The string is the wire format, so renaming one breaks every saved patch containing it.
fromString returns the default for anything unrecognized, which means a rename does not
crash — it silently resets the value. If you must rename, keep the old string accepted in
fromString and only change what toString emits.
Adding a new enumerator is safe in the other direction too: older builds reading a newer file fall back to the default rather than failing the load.