Grouping: placing elements in time
The five primitives adorn things the client already had. The Group is
the one genuinely new structure of the arrangement: it places elements by an
offset, recursively — a group is itself an element, so a phrase sits inside a
section inside a piece, and the same operations work at every level.
The song tree
Each lane of the piece is a group placing one element (the drums place the take twice), and the song is a group placing the lanes:
from clausters.form import Group
drums = Group([(0.0, take), (4.0, take)], name="drums")
bass_lane = Group([(0.0, bass)], name="bass")
lead_lane = Group([(0.0, melody)], name="lead")
song = Group([
(0.0, drums),
(0.0, bass_lane),
(2.0, lead_lane), # the lead enters two beats in
], name="song")
A child is seeded as a (offset, element) pair, a bare Element (placed at
offset 0), or a (offset, dur, element) triple — the third form also fixes a
placement length, which you will meet below. Every offset is in beats,
relative to the group: the melody sits at 0.0 inside its lane, and the lane
at 2.0 inside the song, so the melody starts at absolute beat 2.0. Nothing in
the tree is absolute until rendering adds the offsets up.
Placements are edits: the member handle
A group is not frozen. add places an element and returns a handle — a
stable identity you can move or remove later, however much else has
changed around it:
member = drums.handles[1] # the second take (the handles of the seeds)
drums.move(member, 6.0) # push it two beats later
print(drums.members) # [(0.0, None, <Buffer>), (6.0, None, <Buffer>)]
drums.move(member, 4.0) # and back
members reads the placements as (offset, dur, element) triples; handles
returns the stable member objects that move/remove take. This handle is
exactly what the multitrack editor will hold on to per clip — a dragged clip
becomes a move on its handle, nothing more.
Note the placement dur is None above: the take is its own length (its
duration, 2.0). The two lengths are different things, and the difference is
the next rule.
A placement's length is what you hear of it
A placement dur trims what the element plays — the DAW rule: pull in a
clip's edge and you hear less of it, but the element underneath is untouched.
Watch it on the data directly:
from clausters.form import flatten
drums.move(member, 4.0, 1.0) # same place, but only 1 beat of it
print(flatten(drums)) # the second event now has dur=1.0
print(take.to_event()["dur"]) # 2.0 — the element is untouched
drums.move(member, 4.0, 2.0) # restore the full take
The trim never rewrites the element: the shortened event is a copy made at flatten time, and lengthening the placement again restores everything, because the source was never touched. Events that fall entirely past a placement's end are dropped the same way — shorten a melody's placement and its last notes simply do not play.
The derived temporal relation
From how its members sit in time, a group derives its temporal relation — you never set it, you read it:
print(drums.temporal_relation()) # 'mixed' — a gap between the two takes
print(Group([(0.0, 2.0, take), (2.0, 2.0, take)]).temporal_relation())
# 'successive' — they tile contiguously
print(song.temporal_relation()) # 'mixed'
successive means the members tile — each begins exactly where the previous
ends; simultaneous means they all start and end together; anything else is
mixed. The interesting one is simultaneous: members that start and end
together are one thing on the timeline, which is how an envelope gets attached
to the voice it shapes — the automation page uses it.
(There is a second grouping kind besides the default concrete: a
logical group, whose members relate by processing rather than by time. It
gets its own page.)
Inspecting the whole piece: flatten
Rendering will flatten the tree — accumulate every nested offset into absolute beats. You can run that walk yourself, any time, as a pure inspection:
for beat, item in flatten(song):
print(f"{beat:4} {item.get('instrument', 'default'):8}"
f" midinote={item.get('midinote')} dur={item.get('dur')}")
Every event of the piece, in absolute beats, sorted: the takes at 0 and 4, the
bass — bounced from its pattern in this very walk, the change of state
before your eyes — on every beat from 0 to 7, the melody from beat 2. The bass
is the longest lane: its last note starts at beat 7, so the piece sounds
until beat 7.8 — a note's sounding time is its dur scaled by legato
(default 0.8), and a length here is what you hear, a rule you will meet
again. to_timeline(song) builds the same thing as a Timeline, which is
precisely what a playhead plays — and that is the next page.