ScoreStruct

In maelzel.core there is a division of concerns between music data (notes, chords, lines, voices, etc) and a score structure. The score structure (the ScoreStruct class) consists of a set of measure definitions (time signature, tempo).

[1]:
from maelzel.core import *
from IPython.display import display

Defining a score structure

A ScoreStruct can be defined by adding measure definitions or from a string.

Notice at the html representation of the ScoreStruct that there are time signatures which appear between parenthesis: these are inherited time signatures. The same applies to tempo: if the measure does not define an explicit time signature or tempo, then these attributes are inherited from the previous measure with an explicit tempo

[2]:
struct1 = ScoreStruct(r'''
4/4, 70
5/8
3/4, 140, Section B
3/8
# A first value indicates the measure number.
# Measures start at 0
10, 4/4, 60
5/16
.      # a period repeats the last measure
7/16
3/8
# Three periods at the end indicate that this score extends indefinitely
...
''')
struct1

[2]:

ScoreStruct

Meas. IndexTimesigTempo (quarter note)LabelRehearsalBarline
04/470
15/8
23/4140Section B
33/8
4(3/8)
5(3/8)
6(3/8)
7(3/8)
8(3/8)
9(3/8)
104/460
115/16
12(5/16)
137/16
143/8
...

A ScoreStruct can be displayed as notation:

[3]:
struct1.show()
../_images/notebooks_maelzel-core-scorestruct_5_0.png

A ScoreStruct can be exported to multiple formats: musicxml, MIDI, pdf, png, lilypond. For example, by saving it as MIDI it is possible to import the score structure in a DAW

[4]:
struct1.write('struct.mid')

This is how the exported MIDI file looks in MuseScore. NB: A note is added to the last measure of the score because an empty score is not imported correctly in many applications

image0

Content / Structure

As said before, the score structure defines the time signatures and tempi of the measures in a score. The same content can be represented using different score structures. In the example below a score structure is created and used as ad-hoc context. The second call to .show is outside the context and the default score structure is used

[3]:
getConfig()['quant.complexity'] = 'highest'

struct2 = ScoreStruct(r'''
4/4, 120
7/8
3/4
5/8(2-3)  # It is possible to set the internal subdivisions
5/8(3-2)

...
''')
notes = Chain([Note(m, dur=6/5) for i, m in enumerate(range(60, 72))])

# Used as a context manager, the scorestruct is temporary: the previous
# scorestruct is restored after the context manager exits
with struct2:
    notes.show()

notes.show()

../_images/notebooks_maelzel-core-scorestruct_10_0.png
../_images/notebooks_maelzel-core-scorestruct_10_1.png

Finding notes

A Chain / Voice can be queried to locate events at a given time or position in the score. These locations refer to the active scorestruct. In the code below, for example, we search for the event present at the beginning of the 4th measure.

[4]:
n = notes.eventAt((3, 0))
n
[4]:
4A#:1.2♩

The same but with another scorestruct. Notice that in this case the event found is the active event at the given time but the event itself has a start time prior to the given location.

[5]:
with struct2:
    n = notes.eventAt((3, 0))
    n.show()
../_images/notebooks_maelzel-core-scorestruct_14_0.png

In order to get the object at precisely the given time, use split=True. This will split the returned event at the offset requested, keeping the part tied. This makes it possible to add attributes to the returned part

[6]:
with struct2:
    n = notes.eventAt((3, 0), split=True)
    n.addText("split")
    n.show()
    notes.show()
../_images/notebooks_maelzel-core-scorestruct_16_0.png
../_images/notebooks_maelzel-core-scorestruct_16_1.png

Same content, different score structures. In the next example only the tempo is modified. The resulting notation is the same, because objects in maelzel.core define their start time and duration in terms of beats (quarternotes). The playback is modified

[3]:
notes2 = Chain([Note(m, dur=F(1, 1)) for i, m in enumerate(range(60, 72))])
notes2.show()
display(notes2.rec(nchnls=1))


with ScoreStruct(timesig='4/4', tempo=88):
    notes2.show()
    display(notes2.rec(nchnls=1))

../_images/notebooks_maelzel-core-scorestruct_18_0.png
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2024-01-21T19:01:58.548.wav", 1 channels, 12.02 secs, 44100 Hz)

../_images/notebooks_maelzel-core-scorestruct_18_2.png
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2024-01-21T19:02:00.907.wav", 1 channels, 8.20 secs, 44100 Hz)

The opposite operation is to fix the events in absolute time (seconds) and make them fit a given scorestruct. This is useful when working with fixed media, where a user might be interested in placing an event at a given moment in absolute time. Notice that the playback stays unmodified.

[4]:
struct3 = ScoreStruct(timesig='3/4', tempo=144)
struct = notes2.scorestruct(resolve=True)
remappedNotes = []
for n in notes2:
    offset, dur = struct3.remapSpan(struct, n.absOffset(), n.dur)
    remappedNotes.append(n.clone(offset=offset, dur=dur))

chain3 = Chain(remappedNotes)
with struct3:
    chain3.show()
    display(chain3.rec(nchnls=1))
../_images/notebooks_maelzel-core-scorestruct_20_0.png
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2024-01-21T19:02:06.746.wav", 1 channels, 12.02 secs, 44100 Hz)

There is a shorthand for this operation: .remap. It can be called at the Chain/Voice level, resulting in all events in chain being remapped to the new score structure:

[21]:
chain3 = notes2.remap(struct3)
with struct3:
    chain3.show()
    display(chain3.rec(nchnls=1))
../_images/notebooks_maelzel-core-scorestruct_22_0.png
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2024-01-21T20:27:51.160.wav", 1 channels, 12.02 secs, 44100 Hz)

The Active ScoreStruct

While working on a specific musical task it is possible to set a ScoreStruct as active, meaning that any subsequent action will use that as default if not otherwise specified.

[7]:
setScoreStruct(struct3)

This modifies the active Workspace to use the given scorestruct and is in fact a shortcut to:

getWorkspace().scorestruct = struct2
[8]:
notes2.show()
../_images/notebooks_maelzel-core-scorestruct_26_0.png

Attaching a ScoreStruct to an object

Instead of relying on the active score, a score structure can be explicitely attached to a Score object.

[23]:
sco = Score(voices=[notes2], scorestruct=struct1)
sco
[23]:
Score(1 voices)

Creating a click track from a ScoreStruct

It is possible to create a click track for any defined ScoreStruct

[11]:
scorestruct = ScoreStruct(r'''
4/4, 70
5/8
3/4, 140
3/8
4/4, 60
5/16
.
7/16
3/8
...
''')

clicktrack = scorestruct.makeClickTrack()
clicktrack.show()
clicktrack.rec(nchnls=1)

../_images/notebooks_maelzel-core-scorestruct_31_0.png
[11]:
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2024-01-20T19:12:31.084.wav", 1 channels, 17.35 secs, 44100 Hz)

Exporting

Both the clicktrack and the scorestruct can be exported to many formats: musicxml, MIDI, pdf, png, lilypond. To export to music21, export first to musicxml and import that into music21

[10]:
clicktrack.write('clicktrack.xml')

The clicktrack imported in MuseScore

image0


Sidenote: Musical time vs Real time

To place a musical event at a specific moment in real time (time in seconds, not in quarter notes), use the ScoreStruct to convert between both domains.

  • beat: returns the beat in quarternotes corresponding to the given time in seconds

  • beatDelta: returns a duration in quarternotes corresponding to the elapsed time between the two given times in seconds

[24]:
# Create a one second event starting at 4 seconds
setScoreStruct(tempo=120)
s = getScoreStruct()
Note("4G", offset=s.beat(4), dur=s.beatDelta(4, 5))
[24]:
4G:2♩:offset=8

The same but with another tempo

[25]:
setTempo(90)
Note("4G", offset=s.beat(4), dur=s.beatDelta(4, 5))
[25]:
4G:1.5♩:offset=6