Symbols and notation

Many of the aspects of maelzel.core’s notation output can be customized. There are two main entry points for this:

  1. The method setSymbol which adds or modifies a Symbol to a Note/Chord

  2. addSpanner adds a line/slur/bracket to two or more notes.

Symbols / Properties

Symbols are any elements/attributes used to customize the symbolic representation of music. A text expression, the notehead shape, an articulation: all these are symbols. Also properties (the color of a note, its size, etc) are seen as symbols.

[1]:
from maelzel.core import *

Many symbols can only be added to a note/chord (they are note attached)

[2]:
notes = [
    Note(60.5, dur=1.5, offset=2).setPlay(position=0),
    Note(70, dur=2.5).setPlay(position=1)
]
notes[0].addSymbol('color', 'blue')
notes[1].addSymbol('color', 'red')

chain = Chain(notes)

# This will throw an error since a chain cannot have a text element, only an item of the chain
chain.addSymbol(symbols.Text('error'))
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[2], line 11
      8 chain = Chain(notes)
     10 # This will throw an error since a chain cannot have a text element, only an item of the chain
---> 11 chain.addSymbol(symbols.Text('error'))

File ~/dev/python/maelzel/maelzel/core/mobj.py:1421, in MObj.addSymbol(self, symbol, *args, **kws)
   1417     symboldef = _symbols.makeSymbol(symbol, *args, **kws)
   1419 if isinstance(symbol, _symbols.NoteAttachedSymbol) \
   1420         and not self._acceptsNoteAttachedSymbols:
-> 1421     raise ValueError(f"A {type(self)} does not accept note attached symbols")
   1422 if self.symbols is None:
   1423     self.symbols = [symboldef]

ValueError: A <class 'maelzel.core.chain.Chain'> does not accept note attached symbols

Instead, add it to the first event (note, chord, rest) of the chain

[3]:
chain.firstEvent().addSymbol(symbols.Text("first"))
chain
[3]:
Chain([4C+:1.5♩:offset=2:symbols=[Color(value=blue), Text('first', placement=above)], 4A#:2.5♩:symbols=[Color(value=red)]], dur=6)
[4]:
chain = chain.copy()
chain.fillGaps()
chain.firstEvent().addSymbol(symbols.Text('start'))
chain
[4]:
Chain([Rest:2♩, 4C+:1.5♩:offset=2:symbols=[Color(value=blue), Text('first', placement=above)], 4A#:2.5♩:symbols=[Color(value=red)]], dur=6)

Calls to addSymbol can be chained. Also, a Symbol itself can be used as argument

[5]:
from maelzel.core import symbols

n = Note(60.5, dur=0.5)

n.addSymbol('notehead', color='red', size=1.4).addSymbol('accidental', parenthesis=True)
n.addSymbol(symbols.Articulation('accent'))
print(n.symbols)
n.show()
[Notehead(color=red, size=1.4), Accidental(parenthesis=True), Articulation(kind=accent)]
../_images/notebooks_maelzel-core-symbols_8_1.png

Notice the difference between resizing the notehead alone and resizing the note itself

[6]:
n = Note(60.5, dur=0.5)
n.addSymbol('sizefactor', 1.4)
n.addSymbol('accidental', parenthesis=True)
n.addSymbol('articulation', 'accent')
n
[6]:
4C+:0.5♩:symbols=[SizeFactor(value=1.4), Accidental(parenthesis=True), Articulation(kind=accent)]

Some symbols (like size or color) are exclusive, others can be accumulated

Symbol

Exclusive?

Color

Yes

SizeFactor

Yes

Articulation

Yes

Notehead

Yes

Text

No

NB: dynamics are not symbols. They are treated as a constituent part of a note/chord

Color is exclusive. Only the last call to setSymbol('color', ...) has effect, the previous ones are overwritten

[7]:
Note("4G+", dur=2, dynamic='ff').addSymbol('color', 'blue').addSymbol('color', 'red')
[7]:
4G+:2♩:symbols=[Color(value=red)]

Text symbols, on the other hand, can be accumulated (notice the order of appearance)

[8]:
Note("4G+", dur=2).addSymbol(symbols.Text('text1')).addSymbol(symbols.Text('text2'))
[8]:
4G+:2♩:symbols=[Text('text1', placement=above), Text('text2', placement=above)]
[9]:
from IPython.display import display

with ScoreStruct(timesig='5/8', tempo=80):
    ch = Chain([Note(60+m*0.25, dur='3/4') for m in range(24)])
    ch.addSymbol('color', '#00A0A0')
    ch.setPlay(fade=0, instr='piano')
    for n in ch:
        n.setPlay(gain=0.3)
    for n in ch[::3]:
        n.addSymbol('articulation', 'accent')
        n.setPlay(gain=0.9)
    ch.show()
    display(ch.rec(extratime=0.5, nchnls=1))

../_images/notebooks_maelzel-core-symbols_16_0.png
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2023-03-28T12:04:03.761.wav", 1 channels, 14.00 secs, 44100 Hz)

[10]:
ch = Chain([Note(m, dur=0.5) for m in range(60, 72)])
ch.addSymbol('notehead', 'square', color='#c02020')
ch
[10]:
Chain([4C:0.5♩, 4C#:0.5♩, 4D:0.5♩, 4D#:0.5♩, 4E:0.5♩, 4F:0.5♩, 4F#:0.5♩, 4G:0.5♩, 4G#:0.5♩, 4A:0.5♩, …], dur=6)
[11]:
def rgbtohex(r, g, b):
    return '#%02x%02x%02x'% (r, g, b)

ch = Chain([Note(m, 0.25) for m in range(60, 72)])
for i, n in enumerate(ch):
    di = i/len(ch)
    r = di*0.5
    col = rgbtohex(int(255*r), int(255*r), int(255*r))
    n.addSymbol('notehead', color=col, size=0.5+(1-di)*1.0)
ch
[11]:
Chain([4C:0.25♩:symbols=[Notehead(color=#000000, size=1.5)], 4C#:0.25♩:symbols=[Notehead(color=#0a0a0a, size=1.41667)], 4D:0.25♩:symbols=[Notehead(color=#151515, size=1.33333)], 4D#:0.25♩:symbols=[Notehead(color=#1f1f1f, size=1.25)], 4E:0.25♩:symbols=[Notehead(color=#2a2a2a, size=1.16667)], 4F:0.25♩:symbols=[Notehead(color=#353535, size=1.08333)], 4F#:0.25♩:symbols=[Notehead(color=#3f3f3f)], 4G:0.25♩:symbols=[Notehead(color=#4a4a4a, size=0.916667)], 4G#:0.25♩:symbols=[Notehead(color=#555555, size=0.833333)], 4A:0.25♩:symbols=[Notehead(color=#5f5f5f, size=0.75)], …], dur=3)