Add finite stored sequencer patterns - #1148
Conversation
|
I get the general idea, that this is a mechanism for transferring entire timed collections of events into AMY which can then be triggered as a unit by single commands. However, I don't entirely understand the abstractions, and I find the text in |
|
Hi, I have 3 use cases where is functionality is needed: a) dynamic fills b) arpeggio's c) African rhythms (not implemented yet in the omnichord) |
|
Added some extra documentation: Musical use cases (only the simple ones): A step by step howto with wire commands and api calls: Abstraction and implementation overview: |
|
Thanks for the added documentation, I understand things better now. My concern is that this adds a lot of additional concepts to the AMY interface and may not be completely "in tune" with the existing AMY abstractions. I recognize, however, the functionality of adding whole sequences as objects in AMY both necessitates new concepts and is useful. As I understand it, the key functionality here is to have a new kind of AMY object, a "sequencer pattern", consisting of multiple sequenced notes, which can be scheduled and played as a unit. The patterns can be defined before they are actually played, and can be started and stopped on phrase boundaries rather than mid-stream/asynchronously. My first instinct is: Maybe we can simply add a 4th argument to the Right now, when Maybe that covers it? We add a 4th argument, I think you have a mechanism for directly substituting one playing group for another by reusing a tag, but I think it's OK to replace that with two commands, one to stop one group, and another to start another. I prefer this design because it adds the minimum to the existing AMY API and seems to reuse most of the syntax. Do you think it covers your use-cases? |
|
Thanks, I think this is a good direction, and considering how I actually use the current implementation from LB_Omnichord, I think there is a useful middle ground here. Your main concern: my current PR introduces more new AMY concepts. From the application side, I do not particularly care whether AMY exposes a "stored pattern" object as a separate abstraction. What I really need is a collection of sequencer events that can be prepared ahead of time and then activated as a unit. So I like the idea of extending the existing ticks abstraction with a group_tag, and having a general sequence_control operation instead of the current collection of pattern begin/add/commit/trigger/stop commands. I think that could make the public AMY interface significantly simpler. There are, however, a few semantics from the current implementation that I think I still need. If these can be expressed as properties of sequencer groups rather than as a separate nested-pattern abstraction, I would be very happy to rework the PR that way. The first one is trigger-relative phase. For the drum fills, I preload hundreds of phrases, but when a fill is triggered it has to start from its own local tick 0. Its phase must not depend on where the absolute AMY sequencer clock happens to be. The same is true for the small arpeggio phrases. So I think there are really two useful operations on a group: temporarily mute/unmute a running group while preserving its phase; The first is useful for normal accompaniment layers. The second is what I currently use ONE_SHOT for. The second requirement is finite execution. For example, I need to be able to say, conceptually: play group 37 once I do not mind whether this is represented as one-shot/N-shot/loop modes or simply as a repeat count in sequence_control. In fact, a repeat count may fit the existing AMY style better. This would also solve the possible future African-rhythm use case more cleanly than a special one-shot abstraction. The third requirement, and probably the most important one for live interaction, is lifetime ownership of an execution that has already started. For example, an arpeggio child may contain: tick 0 note on If the user changes the arpeggio rate at tick 5, I want to replace the future triggers immediately, but the note which already started must still receive its original note-off at tick 17. Likewise, if a fill has already started and the user deselects that fill, I want to remove future launches without cutting off the fill which is currently playing. So I think an active finite execution needs to retain the group definition/revision which existed when it started. Editing or replacing the stored group should affect future executions, but not mutate an execution which is already in progress. That does not necessarily have to be visible as a new API concept. It could just be the internal semantics of sequence_control. The fourth requirement is finite onset muting while preserving phase. During a fill, some of the normal percussion roles should temporarily stop producing new hits, but: currently ringing audio should not be killed; The current PR implements that as a finite tagged mute. I think this could also naturally become one of the sequence_control operations on a group rather than a pattern-specific command. There is one other implementation detail which I think matters if we use group_tag. At the moment the sequencer tag is effectively the index in the root sequences[] table. For LB I want hundreds of stored groups, and each group naturally wants its own small local tag namespace. For example: group 100: event tags 0..20 I would not want to flatten those into tens of thousands of global sequencer tags. Similarly, inactive preloaded groups should not all be visited on every sequencer tick. The current LB catalogue already has 270 fills and is designed for substantially more, and the African material will add many more small phrases. So internally I suspect there still needs to be a distinction between stored group definitions and currently active executions, even if that distinction disappears completely from the public API. I also need stored group definitions to survive RESET_SEQUENCER. In LB, Start deliberately resets the transport/timebase and current scheduled events, but it should not resend hundreds of preloaded phrase definitions every time transport is restarted. So the model I have in mind is roughly: ticks + group_tag sequence_control with an active execution having: a local phase starting at zero; The public API could still be almost exactly what you proposed. For example, conceptually: amy.send(ticks='0,48,0,37', ...) and then some form of: sequence_control(group=37, start=True, repeats=1) I am not attached to that exact syntax; I think the important part is the semantics. I also agree that direct substitution of one group for another does not need its own primitive. Two control operations scheduled for the same boundary are fine. If this interpretation fits what you had in mind, I think I would prefer it over the current PR design. It would let me remove most of the new public "pattern" vocabulary while retaining the properties that actually made the LB code simple. Internally AMY may still need a small stored-definition table and a bounded active-execution table, but those could become implementation details of the existing sequencer instead of a second user-visible sequencer abstraction. Does that sound closer to the kind of extension you would be comfortable adding to AMY? |
|
One additional thought after considering the abstraction more carefully: I think this can remain completely generic and should not need any LB Omnichord-specific semantics. I would define a sequence group simply as a persistent named collection of ordinary AMY sequencer events with a local time coordinate. A group can have independent executions, each starting from local tick 0, with a finite repeat count or continuous repetition. An execution retains the definition revision with which it was started, so later edits only affect future executions. I would also avoid defining anything like an "onset mute". More generally, AMY could support event gating while the execution clock continues. AMY would not interpret the payload or distinguish note-ons, note-offs, drums, fills, etc.; it would simply suppress dispatch of events from the gated group/execution while preserving phase. That gives two deliberately separate generic operations: launch: create a new execution from local tick 0; LB Omnichord fills, percussion suppression and arpeggios then become just applications of those primitives, while AMY itself makes no assumptions about their musical purpose. |
|
Superseded by #1151, which reworks the proposal around the sequencer-group abstraction suggested in this discussion. The replacement extends the existing ticks syntax with a group tag and uses one sequence_control family, without retaining the previous nested-pattern API. |
Motivation
I added stored patterns because the root sequencer is intentionally a flat
table of events, while some musical applications need to recall a complete
phrase from one quantized event. Without a stored child phrase, a host must
either resend the entire phrase for every occurrence or reproduce AMY's clock
and stream it at exactly the right time.
The motivating downstream example is the
LB Omnichord rhythm engine,
which keeps repeating rhythm parts running while selecting short fills from a
preloaded catalogue. I used that application to validate the design, but kept
all knowledge of drums, fills, roles, selection and continuation policy out of
AMY. The implementation stores and schedules ordinary AMY events and is usable
for any reusable musical phrase.
I also use stored one-shots for the Omnichord's arpeggios to get better control
over their start and stop behavior. Each sounding arpeggio note is a short
ONE_SHOTcontaining its note-on and matching note-off. A live rate, directionor pitch change can replace future root triggers or definitions without
deleting the release owned by a child which has already started; that child
keeps its immutable definition and ends at its original gate. This avoids both
an abrupt all-off and a host timer without adding any arpeggiator-specific
behavior to AMY. The downstream wire and overlap tests are documented in the
Omnichord sequencer contract.
Design
semantics.
LOOPorONE_SHOT; there is no separatefill abstraction.
suppress selected running patterns without resetting their phase.
pattern, but a stored pattern cannot trigger or schedule another pattern.
This covers the musical hierarchy I needed while preventing recursive cycles
and keeping execution, lifetime and memory bounded.
zQextended-controlfamily. Stored events use
zQE<pattern>,<tick>[,<period>[,<tag>]]<event>Z; no new top-level commandfamily is introduced and
Hremains the root-scheduling envelope.The portable defaults remain deliberately small: 32 definitions, 64 local
event tags per definition and 32 active or pending instances. All limits are
configurable, and
max_patterns=0disables the feature. LB Omnichord configures1,024 definition slots because its public test catalogue currently preloads 270
fills and its reserved ID space is intended for more than 700. Definition slots
and active players remain separate limits, so 1,024 slots do not create 1,024
players.
Compatibility
Existing sequencer behavior is a hard requirement for this change. The new
wire operations and C/Python calls are opt-in. Existing
Hwire messages andamy_add_event()tick scheduling retain their previous parsing and executionpaths.
The native regression test covers legacy
Hmodulo timing, repetition, tagreplacement and clearing, anonymous coexistence, and absolute C-API tick
delivery. It also covers one-shot/loop timing, quantization, immutable commits,
reset and timebase behavior, rollover, nesting rejection and configured bounds.
The existing Shorepine C and Python/audio suites pass unchanged.
Validation
make ctestAMY_TEST_THRESHOLD_DB=-70.0 make test— all 134 Python/audio tests passthe grouped
zQEwire operationand long chord holds; running notes complete their original gate without
hanging or being cut short