Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ amy-message: $(OBJECTS) src/amy-message.o
# Plain C tests for things the audio-rendering suite can't reach -- e.g. clock
# rollovers 50 days out, which you can only hit by fast-forwarding the counters.
CTESTS = tests/test_clock_wrap tests/test_sequencer_active tests/test_sequencer_bounds \
tests/test_sequence_groups \
tests/test_bus_config tests/test_patch_slots \
tests/test_synth_readout tests/test_log2_lut tests/test_clone_on_grow \
tests/test_timebase_reset tests/test_osc_free_on_release \
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ AMY was built by [DAn Ellis](https://research.google/people/DanEllis/) and [Bria
* [**Interactive AMY tutorial**](https://shorepine.github.io/amy/tutorial.html)
* [**AMY API**](docs/api.md)
* [**AMY Synthesizer Details**](docs/synth.md)
* [**AMY Sequencer Groups**](docs/sequencer-groups.md)
* [**Distortion in AMY**](docs/distortions.md)
* [**AMY's MIDI specification**](docs/midi.md)
* [**AMY in Arduino Getting Started**](docs/arduino.md)
Expand Down Expand Up @@ -171,6 +172,7 @@ It's good to understand what wire messages are but you don't need to construct t
* [**Interactive AMY tutorial**](https://shorepine.github.io/amy/tutorial.html)
* [**AMY API**](docs/api.md)
* [**AMY Synthesizer Details**](docs/synth.md)
* [**AMY Sequencer Groups**](docs/sequencer-groups.md)
* [**Distortion in AMY**](docs/distortions.md)
* [**AMY's MIDI specification**](docs/midi.md)
* [**AMY in Arduino Getting Started**](docs/arduino.md)
Expand Down
1 change: 1 addition & 0 deletions amy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ def str_of_int(arg):
('algo_source', 'OL'), ('load_sample', 'zL'), ('transfer_file', 'zTL'), ('disk_sample', 'zFL'),
('algorithm', 'oI'), ('chorus', 'kL'), ('reverb', 'hL'), ('echo', 'ML'), ('patch', 'KI'),
('external_channel', 'WI'), ('portamento', 'mI'), ('tempo', 'jF'), ('sequencer_run', 'zYI'),
('sequence_control', 'zQL'),
('external_midi_sync', 'zCI'),
('synth', 'iI'), ('pedal', 'ipI'), ('synth_flags', 'ifI'), ('num_voices', 'ivI'), ('oscs_per_voice', 'inI'),
('synth_level', 'iVF'),
Expand Down
6 changes: 6 additions & 0 deletions amy/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@
TICKS_TICK=0
TICKS_PERIOD=1
TICKS_TAG=2
TICKS_GROUP=3
SEQUENCE_CONTROL_STOP=0
SEQUENCE_CONTROL_START=1
SEQUENCE_CONTROL_GATE=2
SEQUENCE_CONTROL_PUBLISH=3
SEQUENCE_CONTROL_CLEAR=4
RESET_SEQUENCER=4096
RESET_ALL_OSCS=8192
RESET_TIMEBASE=16384
Expand Down
6 changes: 5 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ amy_start(amy_config);
| `max_oscs` | Int | 180 | How many oscillators to support |
| `max_buses` | Int | 4 | How many FX buses to support. No compile-time ceiling — every bus-indexed table is allocated from this at `amy_start`. Each bus costs a few KB of mix buffers even when idle, plus whatever its effects allocate once switched on |
| `max_sequencer_tags` | Int | 256 | How many sequencer items to handle |
| `max_sequence_groups` | Int | 32 | Number of persistent sequencer groups; group tags are 1 through this value |
| `max_sequence_group_tags` | Int | 64 | Addressable local event tags in each allocated group definition |
| `max_sequence_group_executions` | Int | 32 | Maximum active or quantized-pending group executions |
| `max_voices` | Int | 64 | How many voices |
| `max_synths` | Int | 64 | How many synths |
| `max_memory_patches` | Int | 32 | How many in memory patches to supprot |
Expand Down Expand Up @@ -503,8 +506,9 @@ At bus scope only the constant term of `GD`/`GM` is used; a bus sum has no per-n

| Wire code | C `amy_event` | Python / JS | Type-range | Notes |
| ------ | -------- | ---------- | ---------- | ------------------------------------- |
| `H` | `ticks[3]` | `ticks` | int[,int[,tag]] | Tick, period, tag for sequencing (see "AMY's sequencer" in synth.md). `tag` omitted: stored but not individually cancelable. `period` also omitted: a one-off event at that tick. **If used in a wire string message**, the `H` **must** be the first character of the message. |
| `H` | `ticks[4]` | `ticks` | int[,int[,tag[,group]]] | Tick, period and tag for root sequencing. A nonzero fourth value instead addresses a persistent [sequencer group](sequencer-groups.md), with the third value as its local event tag. `tag` omitted at root: stored but not individually cancelable. `period` also omitted: a one-off event at that tick. **If used in a wire string message**, the `H` **must** be the first character of the message. |
| `j` | `tempo` | `tempo` | float | The tempo (BPM, quarter notes) of the sequencer. Defaults to 108.0. |
| `zQ` | — | `sequence_control` | group,action,value,quantize[,execution_tag] | Publish, start, stop, gate or clear a [sequencer group](sequencer-groups.md). |
| `zY` | **TODO** | `sequencer_run` | 0/1 | Sequencer transport: `zY1` starts the sequencer, `zY0` stops it. Lets a host drive playback without MIDI clock sync (see `external_midi_sync`). |
| `zC` | **TODO** | `external_midi_sync` | 0/1/2 | MIDI clock sync: 1 = the sequencer follows incoming MIDI realtime clock/start/stop (0xF8/0xFA/0xFC); 2 = AMY is the clock master, sending those messages (0xF8 at 24 PPQ from the internal tempo, 0xFA/0xFC on transport start/stop); 0 (default) = internal clock, neither follows nor sends. |
| `N` | `latency_ms`| `latency_ms` | uint | Sets latency in ms. default 0 (see LATENCY) |
Expand Down
153 changes: 153 additions & 0 deletions docs/sequencer-groups-abstractions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Sequencer-group abstractions and implementation

AMY's root sequencer stores ordinary events on one global musical timeline.
Sequencer groups add one reusable, bounded phrase level below that timeline: a
root event can start a finite or repeating group of ordinary AMY events. They
do not add a drum machine, arpeggiator, song model, or scheduler hierarchy.

For concrete applications, see the [musical use cases](sequencer-groups-musical-use-cases.md).
For exact messages, see the [step-by-step how-to](sequencer-groups-howto.md).
The concise argument reference is in [Sequencer groups](sequencer-groups.md).

## The model

The model separates stored content, scheduled starts, and active playback:

| Object | Purpose | Lifetime |
| --- | --- | --- |
| Root sequencer event | Decides when a group starts | Existing `H` tick/period/tag semantics |
| Group tag | Selects one reusable definition slot | From 1 through the configured group capacity |
| Staging revision | Receives local event edits privately | Until published or cleared |
| Published revision | Supplies immutable content to future starts | Until replaced or cleared |
| Execution | Plays one captured revision | Until its repeat count completes or it is stopped |
| Execution tag | Optionally addresses live or pending executions | Supplied by the start operation |
| Local event tag | Replaces or clears one event in one group's staging revision | Scoped to that group only |

Root tags, group tags, execution tags, and local event tags are separate
identities. For example, replacing a tagged root event changes which phrase
will start in the future. It does not edit the phrase definition or shorten an
execution that has already started.

## Authoring and publication

The existing `ticks` tuple accepts an optional fourth value:

```text
tick,period,event_tag,group_tag
```

With a nonzero `group_tag`, the `H` message edits that group's private staging
revision instead of the root sequencer. The first edit after publication clones
the current published revision, so a host can replace only the local tags that
changed. A local tag is cleared with `tick=0,period=0`, exactly like a tagged
root event.

Because that pair means clear, an event at local tick zero must use a nonzero
period. Using the group length as its period is usually the clearest choice; a
finite execution still fires it only once per repetition.

Publication uses action 3 of the `sequence_control` family:

```text
zQ<group>,3,<length>Z
```

The length is explicit. AMY validates every staged event against it, then
publishes the complete revision atomically. Playback therefore never observes
a partly rewritten phrase. AMY does not infer a potentially expensive least
common multiple from event periods.

## Execution lifetime

A start captures the currently published revision. Its repeat value is:

- `1` for one performance;
- `N` for exactly N performances;
- `0` for indefinite repetition.

Editing, publishing, or clearing the group afterward affects future starts
only. Every active execution retains a reference to the revision it captured
and can deliver the note-offs or other closing events already stored in that
revision. This is the key guarantee for glitch-free live phrase changes.

Starts and stops can be quantized to the next multiple of a sequencer tick
interval. A zero quantization value means the next sequencer tick for a direct
command. When a root event starts a group, local tick zero is processed on that
same root tick.

An optional execution tag gives live playback a stable control identity. A new
start with the same group and execution tag replaces the matching execution at
the requested boundary. Untagged starts may overlap. Stop and gate operations
can address one execution tag or, when the tag is omitted, all executions of a
group.

## Finite event gates

Gate action 2 suppresses event dispatch for a duration while the execution's
local clock continues advancing. It does not stop already-sounding audio. When
the gate ends, the next event occurs at its original phase rather than at a
restarted phase. A zero duration releases a current gate.

A group may contain a gate control as a leaf event. This lets one finite phrase
temporarily suppress events from another tagged repeating layer. AMY assigns no
musical meaning to either layer; the controller owns that policy.

## Bounded scheduling

The root sequencer may start a group. A group may contain ordinary AMY events
and finite gate controls, but it cannot start, publish, or clear a group. This
provides the two useful musical levels—global arrangement and reusable
phrase—without cycles or variable scheduling depth.

The configured limits independently bound:

- persistent group slots;
- local event tags in each allocated definition;
- active or quantized-pending executions.

The portable defaults are 32 groups, 64 local tags per group, and 32 active or
pending executions. Definition storage is allocated only when a group is
authored. The audio-time tick path scans only the fixed execution pool, not all
stored groups, so an application can choose a larger definition catalogue
without making every inactive definition part of per-tick work.

## Implementation outline

The implementation in [`src/sequencer.c`](../src/sequencer.c) deliberately
reuses the normal event path:

- grouped `H` messages store the same wire payloads AMY already parses;
- staged and published definitions use fixed-capacity local-tag tables;
- published revisions are reference-counted and remain alive while captured by
an execution;
- an independently bounded execution pool owns start phase, repeat count,
execution identity, pending stop, and gate state;
- root events are processed before group events, which makes a root launch and
its local tick-zero payload sample-clock coherent;
- group-to-group lifecycle operations are rejected while a grouped payload is
firing.

The public configuration fields and constants are declared in
[`src/amy.h`](../src/amy.h). The group engine entry points are in
[`src/sequencer.h`](../src/sequencer.h), and Python uses the existing
`amy.send(ticks=...)` and `amy.send(sequence_control=...)` interface.

## Compatibility contract

An absent or zero fourth `ticks` value follows the existing root-sequencer path.
Existing three-field `H` messages, anonymous root events, tag replacement and
clear behavior, modulo periods, and `amy_add_event()` scheduling are unchanged.

`RESET_SEQUENCER` and `RESET_TIMEBASE` discard active and pending executions
but preserve published group definitions. Full AMY shutdown releases the
definitions.

The native group regression test exercises legacy root behavior and group
behavior in the same process. It covers the unchanged three-value C and wire
formats, root/group namespace isolation, one/N/infinite repetition,
quantization, tagged replacement, selective stop and gate, early ungate,
atomic publication, repair after rejected publication, immutable active
revisions, same-tick root launches, non-recursive lifecycle controls, allowed
leaf controls, resets, 32-bit clock rollover, disabled configuration, and
configured storage and execution bounds. The existing AMY C and audio suites
remain the broader backward-compatibility tests.
Loading
Loading