Z
+```
+
+An aligned stop captures the executions which exist when the command is sent.
+Replacing the definition changes future starts, while an execution which
+already began retains its immutable snapshot. This lets a wrapper migrate
+without tracking AMY's current tick, active note state, or definition version.
+The wrapper must still choose its musical update boundary: replacing on the
+next full period is simple and phase-stable, but may have more latency than an
+application-specific mid-cycle update.
+
+One known first-party consumer of the replace-on-tag behavior is Tulip's
+`AMYSequenceEvent` wrapper. Its `update()` and `remove()` operations need the
+explicit stop/reset/append/start lifecycle above. That migration is localized,
+but its live-edit boundary is a product choice and should be tested together
+with the consumers of that wrapper.
+
+## Other source-compatibility details
+
+`amy_config_t` appends `max_sequence_events` and
+`max_sequence_executions`. Appending preserves the offsets of existing
+members, but changing the size of a public C structure is not a binary ABI
+promise. Applications should be recompiled against the matching header and
+library. As with other AMY configuration, begin with `amy_default_config()` so
+new fields receive supported defaults.
+
+Limits are explicit. `max_sequencer_tags` bounds identities,
+`max_sequence_events` bounds one definition, and
+`max_sequence_executions` bounds active or alignment-pending executions.
+Exhaustion, invalid tags, malformed actions, publication allocation failure,
+and cyclic start graphs reject the affected operation without corrupting the
+previously published generation. Callers which deliberately choose small
+limits should treat a rejected operation as a normal bounded-resource failure.
+
+A multi-message upload is not a wire-level transaction. `define_sequence()`
+validates every Python event before sending its reset, but a target-side
+capacity or transport failure during the subsequent messages can leave the
+successfully accepted prefix as the new definition. A protocol which needs
+acknowledged all-or-nothing remote upload must add that acknowledgement above
+AMY's one-way wire command stream; after a detected failure, reset the tag
+before retrying.
+
+Resetting a definition does not stop an execution which already holds a
+snapshot. `RESET_TIMEBASE` removes active and pending executions while
+retaining definitions. `RESET_SEQUENCER` clears direct events, definitions,
+and executions.
+
+## Automated validation
+
+The host test suite covers:
+
+- unchanged one- and two-value direct scheduling;
+- cumulative definitions, explicit reset, finite and repeating executions;
+- overlapping executions and more than two simultaneously retained snapshot
+ generations;
+- same-tick control ordering, alignment, tick rollover, gate phase, and global
+ reset behavior;
+- current-execution capture for aligned stop and gate;
+- arbitrary payloads, sequence composition, bounded cycles, and exhausted
+ execution pools;
+- allocation failure during pool initialization, new-definition creation and
+ candidate cloning, with recovery and no partial single-event publication;
+- two competing writers, including checked publication and retry;
+- Python validation and exact wire serialization;
+- executable JavaScript serialization and generated binding freshness.
+
+The reusable-sequence C tests run as part of `make ctest`. Python API coverage
+is in `tests/test_sequence_api.py`, and generated API checks are included in
+`make check-c-api` and `make js-api-test`.
+
+## Target-dependent validation still required
+
+The ownership design keeps definition allocation, cloning, string copying,
+and destruction off the render path and outside the shared render-lock
+critical section. That is a source-level real-time property, not a substitute
+for measuring a complete device.
+
+On an ESP32 target, validate the intended sample rate, block and DMA sizes,
+memory capabilities, effects load, and authoring traffic. Record maximum
+render time, missed DMA deadlines, publication critical-section time, heap
+low-water mark, largest free block, and maximum retired-list depth. At 48 kHz
+and 128 samples, the block deadline is approximately 2.67 ms.
+
+Generated Godot source is checked for freshness and syntax when the parser is
+available. An executable Godot runtime behavior test remains target-dependent;
+the sequence behavior itself is implemented in the common C core.
+
+See [Abstractions and implementation](sequencer-sequences-abstractions.md) for
+the snapshot publication and deferred-reclamation design.
diff --git a/docs/sequencer-sequences.md b/docs/sequencer-sequences.md
new file mode 100644
index 00000000..843eb398
--- /dev/null
+++ b/docs/sequencer-sequences.md
@@ -0,0 +1,112 @@
+# Reusable sequences
+
+A sequencer tag identifies a reusable sequence of ordinary AMY events. Sending
+more than one event with the same tag accumulates those events, in the same way
+that repeated `synth=` messages configure one synth. Tagged events use local
+ticks and remain inactive until the sequence is started.
+
+Untagged `ticks` events keep their direct scheduling behavior on the global
+sequencer clock.
+
+## Defining a sequence
+
+The Python convenience API replaces all future contents at a tag:
+
+```python
+amy.define_sequence(40, [
+ dict(ticks=(0,), synth=2, note=60, vel=1),
+ dict(ticks=(12,), synth=2, note=60, vel=0),
+])
+```
+
+Each event uses normal AMY keyword arguments. Its `ticks` value is local to the
+start of the sequence and contains `tick` plus an optional `period`.
+`define_sequence()` validates every event, resets the tag, then sends ordinary
+tagged `ticks` messages:
+
+```python
+amy.send(sequence_reset=40)
+amy.send(ticks=(0, 0, 40), synth=2, note=60, vel=1)
+amy.send(ticks=(12, 0, 40), synth=2, note=60, vel=0)
+```
+
+Repeating tag `40` accumulates both events. `sequence_reset=40` explicitly
+replaces the definition; the empty wire form `H0,0,40Z` is an equivalent reset.
+With an event payload, `ticks=(0, 0, 40)` is a valid local tick-zero event.
+
+## Starting and stopping
+
+```python
+amy.send(sequence=40, action='start', alignment_period=1)
+amy.send(sequence=40, action='stop', alignment_period=48)
+amy.send(sequence=40, action='gate', duration=24, alignment_period=1)
+```
+
+The named actions expose the complete control model: `start` creates an
+execution, `stop` terminates the selected executions, and `gate` temporarily
+suppresses their ordinary events for the required `duration`. `vel` keeps its
+usual meaning of note velocity. At the lower-level `sequence_control` API and
+on the wire, actions use integers: stop `0`, start `1`, and gate `2`.
+Fractional values are invalid. The optional `alignment_period` is the alignment
+quantum. `0` or `1` acts at the next available sequencer tick for a direct
+command. A larger value selects the next global tick divisible by that period.
+When a sequenced parent starts a child, the child's local tick zero participates
+in the same tick.
+
+A start creates a bounded execution. Finite executions of one tag may overlap,
+so callers do not need execution IDs or note-lifetime bookkeeping. Stop targets
+all executions of that tag which are active when the command is sent. If the
+stop is aligned to a future boundary, a separate execution started after that
+command does not inherit its pending stop. This avoids hidden per-tag control
+state. Stopping a parent prevents future child starts, while children already
+started retain their own event pairs.
+
+## Finite and repeating lifetime
+
+Lifetime follows directly from the periods of the stored events:
+
+- a definition containing only `period=0` events is finite and retires after
+ its last event;
+- an event with nonzero `period` repeats on its local period until stopped;
+- a finite controller sequence can start a periodic child and stop it after a
+ chosen number of periods.
+
+## Temporary event gating
+
+```python
+amy.send(sequence=40, action='gate', duration=24, alignment_period=1)
+```
+
+This suppresses ordinary event dispatch from active executions of tag `40` for
+24 ticks. Local phase continues, and dispatch resumes on the original phase.
+Audio already ringing is not cut off. Sequence-control payloads remain active,
+so a controller sequence can still complete its lifecycle. Duration zero
+removes a gate at the selected boundary.
+
+Gated ordinary events are skipped and are not replayed. That rule also applies
+to note-offs and parameter-restoration events. Keep required cleanup outside
+the interval or in a separately started finite gesture. Duration and alignment
+must not exceed 2,147,483,647 ticks so their boundaries remain unambiguous
+across the wrapping 32-bit tick clock.
+
+## Reset behavior
+
+- `amy.send(sequence_reset=tag)` removes the future definition. Active
+ executions retain the snapshot they started with and may finish.
+- `RESET_TIMEBASE` discards active or pending executions because their absolute
+ activation ticks cannot be rebased, but retains stored definitions.
+- `RESET_SEQUENCER` clears untagged events, tagged definitions, and executions.
+
+## Capacity and realtime behavior
+
+`max_sequencer_tags` bounds public tag identities. `max_sequence_events` bounds
+the number of events in one definition, and `max_sequence_executions` bounds
+active or alignment-pending executions. Definitions allocate only when used;
+inactive definitions are not scanned on each tick.
+
+See the [implementation model](sequencer-sequences-abstractions.md),
+[musical use cases](sequencer-sequences-musical-use-cases.md), and
+[step-by-step examples](sequencer-sequences-howto.md). The
+[status and compatibility guide](sequencer-sequences-status.md) records the
+intentional tagged-scheduling change, migration path, test coverage, and
+target-dependent validation boundary.
diff --git a/docs/synth.md b/docs/synth.md
index cf0e6e39..fbf646ab 100644
--- a/docs/synth.md
+++ b/docs/synth.md
@@ -221,12 +221,13 @@ AMY starts a musical sequencer that works on `ticks` from startup. You can reset
Ticks run at 48 PPQ at the set tempo. The tempo defaults to 108 BPM. This means there are 108 quarter notes a minute, and `48 * 108 = 5184` ticks a minute, 86 ticks a second. The tempo can be changed with `amy.send(tempo=120)`.
-You can schedule an event with `amy.send(..., ticks="tick,period,tag")`. All three values are optional past `tick`:
+You can schedule an event with `amy.send(..., ticks="tick,period,tag")`.
+`period` and `tag` are optional. As in other AMY list fields, an empty numeric
+field means zero, so `ticks=",24,7"` is the compact spelling for a tick-zero
+event with period 24 and tag 7:
```python
amy.send(osc=0, wave=amy.SAW_UP, eg0="0,1,500,0,500,0") # Pluck tone
amy.send(osc=0, note=50, vel=1, ticks=amy.sequencer_ticks() + 96) # one-off: fires once, ~1s from now
-amy.send(osc=0, note=38, vel=1, ticks="0,24,7") # repeating, cancelable via tag 7
-amy.send(osc=0, ticks="0,0,7") # cancel tag 7
amy.send(osc=0, note=72, vel=1, ticks="0,24") # repeating, not individually cancelable
amy.reset() # Stop everything
```
@@ -237,9 +238,30 @@ You can schedule repeating events (like a step sequencer or drum machine) with `
For pattern sequencers like drum machines, you will also want to use `tick` alongside `period`. If both are given and `period` is nonzero, `tick` is assumed to be an offset on the `period`. For example, for a 16-step drum machine pattern running on eighth notes (PPQ/2), you would use a `period` of `16 * 24 = 384`. The first slot of the drum machine would have a `tick` of 0, the 2nd would have a `tick` offset of 24, and so on.
-`tag` is optional. If you give one, you can cancel that event later by sending `ticks="0,0,tag"` with the same `tag`. If you omitted `tag` when setting up the sequence (a 1- or 2-value `ticks=`), the event is still scheduled and still fires, but it isn't addressable by any tag -- there's no way to cancel or replace it individually (only by something like `amy.reset()`, discarding all sequenced events), so only omit `tag` for events you don't need to manage later.
+`tag` is optional. Without one, an event is scheduled directly on the global
+sequencer clock and cannot be addressed individually. With a tag, the event is
+added to a reusable sequence and its tick becomes local to each start of that
+sequence. Repeating a tag accumulates events; reset the tag explicitly before
+replacing its contents.
-If you are including AMY in a program, you can set the [hook `void (*amy_external_sequencer_hook)(uint32_t)`](docs/api.md) to any function. This will be called at every tick with the current tick number as an argument.
+If you are including AMY in a program, you can set the [hook `void (*amy_external_sequencer_hook)(uint32_t)`](api.md) to any function. This will be called at every tick with the current tick number as an argument.
+
+### Reusable tagged sequences
+
+A sequencer tag holds one or more ordinary events with local tick values.
+Repeated three-value `ticks=(tick, period, tag)` messages cumulate behind the
+same tag. `amy.define_sequence(tag, events)` is the convenient replace-as-a-list
+operation. `sequence_control` starts, stops, aligns, or temporarily gates an
+active tagged sequence. Component periods define looping; a definition
+containing only period-zero events finishes after its last event.
+
+See [Reusable sequences](sequencer-sequences.md) for the concise API
+and lifecycle reference. The accompanying guides explain the
+[abstractions and implementation](sequencer-sequences-abstractions.md),
+[musical use cases](sequencer-sequences-musical-use-cases.md), and a
+[step-by-step Python example](sequencer-sequences-howto.md). See
+[status and compatibility](sequencer-sequences-status.md) when migrating
+existing tagged scheduling or configuring a target build.
## Core oscillators
@@ -475,7 +497,3 @@ amy.start_sample(preset=1024, source=amy.SAMPLE_FROM_OUTPUT, max_frames=11025, m
amy.send(osc=0, wave=amy.PCM_LEFT, preset=1024, pan=0, note=72, vel=1) # play back AUDIO_IN sample an octave higher
amy.send(osc=1, wave=amy.PCM_RIGHT, preset=1024, pan=1, note=72, vel=1)
```
-
-
-
-
diff --git a/docs/tutorial.html b/docs/tutorial.html
index 6f0bdcae..035cdf50 100644
--- a/docs/tutorial.html
+++ b/docs/tutorial.html
@@ -162,16 +162,23 @@ AMY sequencer
amy.send(osc=0, vel=1, wave=amy.PCM, preset=0, ticks=",24,1") # play a PCM drum every eighth note.
amy.send(osc=1, vel=1, wave=amy.PCM, preset=3, ticks=",48,2") # play a different PCM drum every quarter note.
+amy.send(sequence=1, action='start', alignment_period=1)
+amy.send(sequence=2, action='start', alignment_period=1)
- You can remove or update sequence events by addressing their tag number
+ Events with the same tag cumulate into a reusable sequence. Stop and reset a tag before replacing its contents:
-amy.send(ticks=",,1") # remove the eighth note sequence
-amy.send(osc=1, vel=1, wave=amy.PCM, preset=3, note=70, ticks=",48,2") # change the quarter note event
+amy.send(sequence=1, action='stop', alignment_period=1)
+amy.send(sequence_reset=1)
+amy.send(osc=0, vel=1, wave=amy.PCM, preset=0, note=70, ticks=",48,1")
+amy.send(sequence=1, action='start', alignment_period=1)
For patterns you want to also address their "slots", which is the offset within the pattern, like this
+amy.send(sequence=1, action='stop', alignment_period=1)
+amy.send(sequence_reset=1)
amy.send(osc=0, vel=1, wave=amy.PCM, preset=0, ticks="0,384,1") # first slot of a 16 1/8th note drum machine
-amy.send(osc=1, vel=1, wave=amy.PCM, preset=3, ticks="216,384,2") # ninth slot of a 16 1/8th note drum machine
+amy.send(osc=1, vel=1, wave=amy.PCM, preset=3, ticks="216,384,1") # ninth slot in the same tagged sequence
+amy.send(sequence=1, action='start', alignment_period=384)
@@ -287,5 +294,3 @@ Python Error
<