[1]:
from maelzel.core import *

A Chain is a sequence of events (notes, chords, clips, rests)

[2]:
chain = Chain([
    Note("3G", 0.5),
    Note("3Bb", 0.5),
    Note("4D", 0.75),
    Note("4F#", 0.75),
    Note("4A", 0.5),
    Note("5C", 0.5),
    Note("5E", 0.5),
    Note("5G#", 1),
    Note("5B", 1),
    Note("6C#", F(1, 3)),
    Note("6Eb", F(1, 3)),
    Note("6F", F(1, 3))
])
chain
[2]:
Chain([3G:0.5♩, 3A#:0.5♩, 4D:0.75♩, 4F#:0.75♩, 4A:0.5♩, 5C:0.5♩, 5E:0.5♩, 5G#:1♩, 5B:1♩, 6C#:0.333♩, …], dur=7)

Events (Notes, Chords, Clips, …) can have an explicit offset. This offset represents the start time of the event relative to the start of the Chain

[3]:
chain = Chain([
    Note("3G", 0.5),
    Note("3Bb", 0.5),
    Note("4D", 0.75),
    Note("4F#", 0.75),
    Note("4A", 0.5),
    Note("5C", 0.5),
    Note("5E", 0.5),
    Note("5G#", 1, offset=6),        # <------ explicit offset
    Note("5B", 1),
    Note("6C#", F(1, 3)),
    Note("6Eb", F(1, 3)),
    Note("6F", F(1, 3))
])
chain
[3]:
Chain([3G:0.5♩, 3A#:0.5♩, 4D:0.75♩, 4F#:0.75♩, 4A:0.5♩, 5C:0.5♩, 5E:0.5♩, 5G#:1♩:offset=6, 5B:1♩, 6C#:0.333♩, …], dur=9)

Chains within Chains

A Chain can contain other Chains. Notice the effect of the offset now: it represents the start of the object relative to the parent Chain, resulting in a silence of 6 quarternotes.

[4]:
chain = Chain([
    Note("3G", 0.5),
    Note("3Bb", 0.5),
    Note("4D", 0.75),
    Note("4F#", 0.75),
    Note("4A", 0.5),
    Note("5C", 0.5),
    Note("5E", 0.5),
    Chain([
        Note("5G#", 1, offset=6),
        Note("5B", 1),
        Note("6C#", F(1, 3)),
        Note("6Eb", F(1, 3)),
        Note("6F", F(1, 3))
    ])
])
chain
[4]:
Chain([3G:0.5♩, 3A#:0.5♩, 4D:0.75♩, 4F#:0.75♩, 4A:0.5♩, 5C:0.5♩, 5E:0.5♩, Chain([5G#:1♩:offset=6, 5B:1♩, 6C#:0.333♩, 6D#:0.333♩, 6F:0.333♩])], dur=13)

A Chain itself can also have an offset. To reproduce the original example, we can set the offset in the subchain itself

[5]:
chain = Chain([
    Note("3G", 0.5),
    Note("3Bb", 0.5),
    Note("4D", 0.75),
    Note("4F#", 0.75),
    Note("4A", 0.5),
    Note("5C", 0.5),
    Note("5E", 0.5),
    Chain([
        Note("5G#", 1),
        Note("5B", 1),
        Note("6C#", F(1, 3)),
        Note("6Eb", F(1, 3)),
        Note("6F", F(1, 3))
    ], offset=6)
])
chain
[5]:
Chain([3G:0.5♩, 3A#:0.5♩, 4D:0.75♩, 4F#:0.75♩, 4A:0.5♩, 5C:0.5♩, 5E:0.5♩, Chain([5G#:1♩, 5B:1♩, 6C#:0.333♩, 6D#:0.333♩, 6F:0.333♩], offset=6)], dur=9)

Side-Note: dumping for debugging purposes

If you have any doubt as to how the times are calculated, it is possible to .dump() a Chain. This will recursively dump any subchains. At the offset column it is possible to see the implicit offset (in parenthesis), which is always relative to the parent, and the calculated beat, which is the absolute offset measured in quarter notes. The location represents the placement of the object within the active score structure

[6]:
chain.dump()
Chain - beat: 0, offset: None, dur: 9
location beat offset dur name gliss dyn playargs info
0:0 0 (0) 0.5 3G False None -
0:0.5 0.5 (1/2) 0.5 3A# False None -
0:1 1 (1) 0.75 4D False None -
0:1.75 1.75 (7/4) 0.75 4F# False None -
0:2.5 2.5 (5/2) 0.5 4A False None -
0:3 3 (3) 0.5 5C False None -
0:3.5 3.5 (7/2) 0.5 5E False None -
Chain - beat: 6, offset: 6, dur: 3
location beat offset dur name gliss dyn playargs info
1:2 6 (0) 1 5G# False None -
1:3 7 (1) 1 5B False None -
2:0 8 (2) 0.333 6C# False None -
2:0.333 8.333 (7/3) 0.333 6D# False None -
2:0.667 8.667 (8/3) 0.333 6F False None -

It is possible to flatten a Chain. As a consequence of flattening, any implicit offset is turned explicit (notice in the dump that the offsets are not printed inside parenthesis, indicating that they are indeed explicit)

[7]:
chain.flat().dump()
Chain - beat: 0, offset: None, dur: 9
location beat offset dur name gliss dyn playargs info
0:0 0 0 0.5 3G False None -
0:0.5 0.5 0.5 0.5 3A# False None -
0:1 1 1 0.75 4D False None -
0:1.75 1.75 1.75 0.75 4F# False None -
0:2.5 2.5 2.5 0.5 4A False None -
0:3 3 3 0.5 5C False None -
0:3.5 3.5 3.5 0.5 5E False None -
1:2 6 6 1 5G# False None -
1:3 7 7 1 5B False None -
2:0 8 8 0.333 6C# False None -
2:0.333 8.333 8.333 0.333 6D# False None -
2:0.667 8.667 8.667 0.333 6F False None -

Side Note 2: Shorthand

Any note or chord can be represented as a string in shorthand. Each event can be its own string or all events can be placed within a multiline string with each event using one line

[2]:
chain1 = Chain([
    "4C:1",
    "4D:0.5",
    "4E,4G:1+1/3",  # a chord
    "4F~:1/3",      # a tied note
    "4F:1/2:gliss",
    "4A+:0",        # a gracenote
    "3B-"
])
chain1.show()

chain2 = Chain(r"""
    3G:0.5
    3Bb:0.5
    4D:3/4
    4F#:3/4:ff:dim   # This dim. hairpin will stretch until the next dynamic
    4A:1/2
    5C:1/2
    5E:1/2:p
    5G#:1:accent
    5B,6D-:1
    6C#:1/3:slur     # starts a slur
    6Eb:1/3
    6F:1/3:~slur     # ends the previous slur
    rest/16          # A 16th note
    6E-/8.           # A dotted 8th note
""")
chain2.show()
../_images/notebooks_reference-chain_14_0.png
../_images/notebooks_reference-chain_14_1.png

There are actions which can only be performed on voices. A Voice is very similar to a Chain, but always has an offset of 0. They are covered later on.

In this example we convert to a Voice in order to break a beam at a given boundary

[9]:
v = chain2.asVoice()
v.breakBeam(3)
v
[9]:
Voice([3G:0.5♩, 3A#:0.5♩, 4D:0.75♩, 4F#:0.75♩:symbols=[Hairpin(anchor=, direction=>, kind=start, linetype=solid, uuid=wnnpd4w8)], 4A:0.5♩, 5C:0.5♩, 5E:0.5♩, 5G#:1♩:symbols=[Articulation(kind=accent)], ‹5B 6D- 1♩›, 6C#:0.333♩:symbols=[Slur(anchor=, kind=start, linetype=solid, uuid=cqw5o70x)], …], dur=8, offset=0)

Operations on Chains

[10]:
chain2.invertPitch(pivot="4F#")
[10]:
Chain([5F:0.5♩, 5D:0.5♩, 4A#:0.75♩, 4F#:0.75♩:symbols=[Hairpin(anchor=, direction=>, kind=start, linetype=solid, uuid=wnnpd4w8)], 4D#:0.5♩, 4C:0.5♩, 3G#:0.5♩, 3E:1♩:symbols=[Articulation(kind=accent)], ‹3C# 2B- 1♩›, 2B:0.333♩:symbols=[Slur(anchor=, kind=start, linetype=solid, uuid=cqw5o70x)], …], dur=8)
[3]:
with CoreConfig({'quant.complexity': 'highest'}):
    chain2.timeScale(factor=F(4,3)).show()
../_images/notebooks_reference-chain_19_0.png
[9]:
chain2 = Chain(r"""
    3G:0.5
    3Bb:0.5
    4D:3/4
    4F#:3/4:ff:dim   # This dim. hairpin will stretch until the next dynamic
    4A:1/2
    5C:1/2
    5E:1/2:p
    5G#:1:accent
    5B,6D-:1
    6C#:1/3:slur     # starts a slur
    6Eb:1/3
    6F:1/3:~slur     # ends the previous slur
    rest/16          # A 16th note
    6E-/8.           # A dotted 8th note
""")

chain2.show()

chain3 = chain2.timeScale(factor=F(4, 3))
chain3.show()

../_images/notebooks_reference-chain_20_0.png
../_images/notebooks_reference-chain_20_1.png