theallelectricsmartgrid

The Encoder System

The Smart Grid One relies entirely on a software-defined parameter system. All “knob” state lives in software, allowing for complete recall, deep modulation, macro control (gestures), and A/B scene morphing.

The core implementation spans private/src/Encoder.hpp, private/src/EncoderBank.hpp, and private/src/EncoderBankBank.hpp.

Ownership lives in EncoderBankBank: it owns a flat array of BankedEncoderCell instances indexed by SmartGridOneEncoders::Param. Each EncoderBankInternal starts with null base cells and receives raw pointers through PlaceEncoder(...). This makes encoder swapping explicit and keeps base cells empty until they are placed.

Serialization follows ownership: EncoderBankBank::ToJSON() iterates the full encoder array and writes each named parameter, and FromJSON() does the inverse by looking up each name and updating the encoder state.

Construction

The shared SmartGridOneContext is a constructor dependency throughout the encoder ownership chain. It provides the scene manager, recorder, and parameter event logger:

The context must outlive these objects. Encoder cells serialize through EncoderBankBank; the discrete-control State/StateSaver storage stays separate.

Parameter recording

SetAndRecordValue updates a stored scene/track value and emits EncoderSet while recording. The event converts normalized storage through ToValue, matching ordinary patch JSON before smoothing or modulation. SetActive records gesture activation; when activation inherits a parent’s value, that copied value also passes through SetAndRecordValue. Root names identify parameters, and each nested path hop identifies a gesture or modulator index.

The recorder groups these assignments with StateChange, GestureSet (fader), and BlendSet events. The Python reader reconstructs values and activation, including neutral nested nodes first created after the header snapshot. FromJSON writes raw values and activation flags; one PatchLoad event captures the whole load, including replacement/removal of children. Gestures are always leaves; normal modulators may nest or have gesture leaves. See the exact recording protocol and limitations.

Base Structure: Tracks and Voices

To accommodate polyphony and quadraphonic effects, the parameter system is structured hierarchically.

Crucially, the base value of a knob is identical for all voices within a track. However, modulation and gestures can apply different offsets to each voice within that track.

Base Encoders vs. Banked Encoders

Every state encoder has an m_bipolar flag. Scene storage, gesture targets, computed outputs, slew state, and UI/MIDI knob coordinates stay in [0, 1]. DSP GetValue and GetValueNoSlew getters automatically return 2u - 1 for a bipolar encoder and u for a unipolar encoder. Switch indexing still uses the normalized position. The parameter declaration’s trailing bipolar flag and default value configure this behavior; defaults are in the exposed knob domain and are converted to normalized state at creation. Existing top-level parameters remain unipolar. Gesture target cells inherit their parent’s polarity.

JSON saves the knob position, in [-1, 1] for bipolar encoders and [0, 1] for unipolar encoders. Loading converts signed values back to normalized storage. Polarity comes from the parameter declaration or child role, not a saved field; there is no patch version or legacy-depth conversion.

Deep Modulation

Every BankedEncoderCell can act as a modulation destination.

Negative depths crossfade toward the inverted source, 1 - m. For each voice, let d be each curved depth multiplied by its source amplitude and W = sum(abs(d)). The output is p * max(0, 1 - W) + (sum(d * m) + sum(max(0, -d))) / max(1, W). The negative-depth offset keeps the result normalized, while total absolute weight above one produces a normalized modulation mix. Source amplitude changes invalidate the cached result even when the waveform value stays constant.

Depth reset, activity detection, and garbage collection use the neutral position 0.5. Non-neutral saved scenes and active nested modulation remain preserved, even when the current scene blend produces zero depth. Bipolar encoder rings show a center marker; encoder-set messages and MIDI values remain normalized.

Old saved positive modulation depths are read as positive signed knob positions. An old 0 stays neutral and 1 stays full depth, while an old 0.5 now produces depth 0.25. This intentionally weakens intermediate old depths, including those in nested modulation. Saving writes the knob position, so repeated save/load does not repeatedly apply the exponential curve. Changing an existing top-level parameter to bipolar likewise requires deliberately updating its DSP callers and accepting the new interpretation of its old saved values.

Gestures (Macros)

Gestures are macro controls mapped to physical analog inputs (Like sliders and joysticks).

Scene Morphing

The entire state of all encoders (base values, modulation depths, and gesture targets) is stored across 8 persistent Scenes (SceneManager::x_numScenes, in SceneManager.hpp). Each scene is a complete snapshot of the synthesizer.

UI State and Parameter Slew

Frequency-Dependent Quad Parameters

The Partial Machine uses a Quad bank differently from Delay and Reverb. Its four lanes are interpreted as anchors for FrequencyDependentParameter, not just four speaker channels. If all four lanes share the same base value, the parameter behaves like a normal scalar. If modulation produces different values per lane, each spectral partial interpolates a unique value from its frequency, allowing the same knob to create frequency-dependent attack, decay, density, bandwidth, panning, unison, pitch, and volume behavior.

See Partial Machine for the DSP-side mapping.

Machine-Specific Parameters

Each parameter in the Voice banks can specify which source and filter machines it applies to via bit vectors (MachineFlags). Parameters are defined in ForEachSmartGridOneParam.hpp with sourceMachines and filterMachines arguments (e.g. MachineFlags::x_dualWaveShapingVCOOnly for parameters that only affect the Dual Wave Shaping VCO source).

When the user selects a different source machine (e.g. Thru) or filter machine, UpdateEncodersForMachine() runs and swaps the actual encoder pointers in the grid. Parameters that do not apply are removed by placing nullptr in those positions, which leaves the cell empty and disconnected. This keeps the encoder grid relevant to the active machine while preserving encoder state in the owner array. The update is triggered on machine change (config page) and track change (since each track can have different machines).