Add serializeShapeSeqState/restoreShapeSeqState for complete ShapeSeq
state capture (chain config, primitive states, projection knobs, voice
mode, MLP weights, freeze state, clock config). SessionPresetManager
lazily imports the session module when a ShapeSeq engine is registered,
including ShapeSeq state in save/load without a hard dependency.
When frozen, param rows dim to indicate frozen state. Tapping a row
toggles it to "live" (highlighted, receives ML deltas). Unfreezing
clears all visual indicators.
Replace the inline chain builder with a full-screen slide-up drawer
(overlay + glass backdrop + drag handle). Param sliders now subscribe
to a vanilla signal (createArraySignal) for O(changed params) updates
instead of querySelectorAll sweeps. Card list uses keyed reconciliation.
Mono mode (default) kills the previous note before firing a new one —
classic monophonic synth behavior. Poly mode retriggers same-pitch
notes without killing others. All active notes are released on mode
switch.
Unified mode: a single MLP drives both timbre and sequence — the timbre
MLP's outputs are partitioned and a configurable slice feeds the
sequencer pipeline via setSequenceOutputsFromTimbre().
Dual mode (default): separate MLPs for timbre and sequence with
independent training. The sequencer owns its own IML instance.
Two freeze modes for capturing and preserving musical state:
- Algorithm freeze: snapshot all primitive params + seeds. Individual
params can be selectively unfrozen for ML-driven exploration via
DeltaController (applies MLP deltas to frozen base values with
per-param boundary enforcement).
- Pattern freeze: snapshot the realized note pattern. The sequencer
bypasses the primitive chain entirely and loops the frozen pattern.
Extracts _runPipeline() from setSequenceInputs() for reuse.
The dirty-check that skips re-evaluation when inputs haven't changed now
also checks a generation counter. Config changes (tempo, step count,
projection, chain edits) bump the counter, ensuring the pipeline re-runs
even when joystick position is static.
The sequence MLP output count is now configurable (default 16). Hidden
layer widths scale with the output count to keep inference cheap while
providing sufficient capacity. The sequencer exposes setOutputCount()
to resize the MLP at runtime.
Replace the composable transform chain (VelocityCurve, GateThreshold,
RangeMap, OctaveFolder, StutterMap, presets) with 3 straightforward
post-chain knobs: velocityCurve, gateThreshold, and pitchRange. The
old system was over-engineered for a research prototype — the new API
is easier to wire to UI controls and reason about.
PitchWalker was classified as a generator but always consumed the
incoming pattern's triggers. Reclassify as a processor so it modifies
the pattern produced by an upstream generator (clonePattern instead of
createPattern). Add reEvalOnLoop flag on the Primitive base class; when
set, the sequencer re-evaluates the chain at each loop start so
stateful primitives produce evolving patterns across loops.
Generators can now have independent step counts. When merging patterns
of different lengths, both are tiled to their LCM. Chain exposes
setGeneratorStepCount/getGeneratorStepCount for polyrhythmic control.
Replace the pitch*127 encoding hack with a dedicated midiNote field on
pattern steps. IntervalLock now writes integer MIDI notes to step.midiNote
while preserving the original [0,1] pitch for other consumers. The clock
and sequencer bridge pass midiNote through the event pipeline.
Also reworks mergePatterns to be trigger-aware: when only one generator
fires at a step, use its values directly instead of averaging with the
other generator's defaults.
The EventBus was duplicated inside shapeseq/ but is used by other
playground code too. Move the canonical implementation to js/event-bus.js
and turn the shapeseq copy into a re-export shim.
Four modules that build on the Layer 0 foundation:
- primitive.js: base class with category system (generator/processor/
timing/converter), param schema validation, boundary enforcement
(clamp/wrap/scaled), symbolic process() interface, state management
for freeze, and applyBoundary() utility for delta control
- clock.js: AudioContext lookahead scheduling (25ms interval, 100ms
window) replacing setTimeout arpeggiator. Reads pattern descriptions
for per-step swing (timeOffset) and ratchet (subdivisions). Emits
seq.noteOn/noteOff/step/loopStart via event bus.
- projection.js: composable transform chain with 5 transforms
(VelocityCurve, GateThreshold, RangeMap, OctaveFolder, StutterMap)
and 3 presets (expressive, percussive, fullRange). Pitch quantization
deliberately excluded (handled by Interval Lock primitive).
- step-viz.js: Canvas2D circular step visualizer with even angular
spacing for any step count. Pitch→radius, velocity→node size,
accent→color. 60fps-friendly with pre-allocated coordinate buffers.
Tap interaction for step toggling.
Five independent modules with no interdependencies:
- event-bus.js: namespaced pub/sub (seq.*/ml.*/ui.*) with wildcard
subscriptions and automatic AudioContext/performance.now timestamps
- prng.js: seedable mulberry32 PRNG with pure-functional API, fork()
for independent per-primitive streams, serializable state for freeze
- pattern.js: symbolic pattern description data structure with
create/clone/merge/validate, additive and multiplicative merge modes
- param-map.js: maps fixed 16-output MLP to variable-count primitive
params via linear interpolation, with optional per-param min/max scaling
- seq-iml.js: factory for second WasmIML instance (2 inputs, 16 outputs,
[16,16,16] hidden layers) following existing imlJoy/imlHand pattern
All modules are ES modules with no build step. PRNG, pattern, and
param-map are port-ready (explicit state, typed arrays, no closures).