First Steps

Table of Contents


[1]:
# Import everyting in the maelzel.core namespace
from maelzel.core import *

# pitchtools defines many useful conversion functions from/to notenames, frequencies, midinotes, etc
from pitchtools import f2m

# the display function allows to display the html representation of objects
from IPython.display import display

Notes

The simplest note possible is just a pitch.

For notation/playback a default duration will be assumed if no duration is given

[2]:
Note("C4")
[2]:
4C:1♩

Playback

A Note can be played via its .play method. By default, a piano soundfont (distributed with maelzel) will be used as the default instrument.

NB: when this notebook is displayed as part of the documentation it is made static so any interactive elements are removed. For that purpose we render the results in order for the generated audio to be embedded in the html

[4]:
note = Note("C4")
note.play()
[4]:
SynthGroup - start: 0.038, dur: 1.000, synths: 1

instr: preset:piano - 1 synths

p1startdur45:idataidx_6:inumbps7:ibplen8:igain9:ichan10:ipos11:ifadein12:ifadeout13:ipchintrp_14:ifadekind1516...
501.0003 𝍪0.0381.0000152311-10.020.02010...

By just replacing .play with .rec it can also be recorded to a soundfile

[4]:
note.rec()
[4]:
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2023-03-28T15:17:29.603.wav", 2 channels, 1.02 secs, 44100 Hz)

Pitch

The pitch of a note can be given as a fractional midinote or as a string. If the pitch deviates from a round 1/8 tone, the cents deviation from the nearest semitone will be shown as a text label (see the 4th note, which is a E4 20 cents higher). Pitches can also be given as an absolute frequency and as a frequency deviation from a semitone. The actual enharmonic representation is not fixed by how the pitch was entered: it depends on the context (surrounding pitches, chords, etc) (see Enharmonic Spelling).

NB: a Chain creates a sequence of notes/chords

[5]:
# All these are valid ways to indicate a pitch

seq = Chain([
    Note(60),
    Note("c#4+"),
    Note("4Eb"),
    Note("4E+20"),
    Note("4G-10hz"),
    Note("442hz"),
])
seq.show()

# Record output as mono, add some sustain to simulate legato and some extra time
# at the end of the recording (this avoids a sudden stop in playback in the browser)
seq.rec(nchnls=1, sustain=0.1, extratime=0.5)
../_images/notebooks_firststeps_10_0.png
[5]:
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2023-03-28T15:17:32.545.wav", 1 channels, 6.60 secs, 44100 Hz)

An interval can be added to a note. This will result in a transposed note

[6]:
c4 = Note("c4")
c4 + 2
[6]:
4D:1♩
[7]:
c4 - 0.5
[7]:
3B+:1♩

A note can be compared against another note, pitch or string (as long as the string can be interpreted as a notename)

[8]:
c4 > "b3"
[8]:
True
[9]:
c4 < 72
[9]:
True

Duration

The duration of a Note is given in quarternotes. This duration is symbolic: its real time duration in seconds depends on tempo.

[10]:
setTempo(60)

note = Note(67.5, dur=2.5)
note.show()
note.rec(nchnls=1)

../_images/notebooks_firststeps_18_0.png
[10]:
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2023-03-28T15:17:35.930.wav", 1 channels, 2.52 secs, 44100 Hz)

The same note with a faster tempo results in the same notation (minus the metronome mark) but playback is shortened)

[11]:
setTempo(90)

note.show()
note.rec(nchnls=1).show()

# Reset the tempo to 60
setTempo(60)
../_images/notebooks_firststeps_20_0.png
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2023-03-28T15:17:37.246.wav", 1 channels, 1.69 secs, 44100 Hz)

[12]:
pitches = ("C4", 60.25, "4c#", 62.5, "4Eb", f2m(400))
sequence = Chain([Note(p, 0.5) for p in pitches])

# Outside of jupyter you need to call `.show()` in order to display the object.
# Inside jupyter you can simply set the object as the last line of a cell
sequence
[12]:
Chain([4C:0.5♩, 4C>:0.5♩, 4C#:0.5♩, 4D+:0.5♩, 4D#:0.5♩, 4G+27:0.5♩], dur=3)
[13]:
sequence.rec(nchnls=1, sustain=4)
[13]:
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2023-03-28T15:17:38.521.wav", 1 channels, 7.02 secs, 44100 Hz)

Any duration can be given. For example, triplets:

[14]:
Chain([Note(p, 1/3) for p in pitches])
[14]:
Chain([4C:0.333♩, 4C>:0.333♩, 4C#:0.333♩, 4D+:0.333♩, 4D#:0.333♩, 4G+27:0.333♩], dur=2)

We can test different durations. The quantization engine will try to find the best representation for each rhythm

Notice how, for nested tuplets, the quantization algorithm decides to place the smaller tuplet (in these cases the triplet) as the outter tuplet. This makes the rhythms easier to read.

[15]:
durations = [
    "3/7",    # By entering a fraction as a string we ensure that there are no rounding errors
    F(2, 3),
    F(1, 5)*F(1, 3),
    F(1, 3)*F(2, 7),
]

for duration in durations:
    chain = Chain([Note(p, duration) for p in pitches])
    chain.show()
../_images/notebooks_firststeps_26_0.png
../_images/notebooks_firststeps_26_1.png
../_images/notebooks_firststeps_26_2.png
../_images/notebooks_firststeps_26_3.png

When entering notes there are some shortcuts possible. For example, when using a string to set the pitch of a note, it is possible to set the duration as well by using the following formats:

<pitch>:<duration in quarternotes>   (example: 4Db:1.5 -> a dotted quarternote)
<pitch>/<musical duration>           (example: 4Db/4.  -> a dotted quarternote)
  • In the first format, the duration is given as quarternotes, so 0.5 is an eighth note, 0.25 is a 16th note, etc.

  • In the second format, the duration is given as symbol: 4 represents a quarter-note, 8 represents an 8th-note, 16 a 16th-note, etc. Dots can also be added

[16]:
Chain([
    Note("4C", 0.5),
    Note("4C:0.5"),
    Note("4C/8"),

    Note("4D", 0.75),
    Note("4D:.75"),
    Note("4D/8."),

    Note("4E", 0.25),
    "4E:.25",           # The string is enough to construct a note within a Chain
    "4E/16",
])

[16]:
Chain([4C:0.5♩, 4C:0.5♩, 4C:0.5♩, 4D:0.75♩, 4D:0.75♩, 4D:0.75♩, 4E:0.25♩, 4E:0.25♩, 4E:0.25♩], dur=4.5)

Explicit Offset

Notes can have an explicit start time called offset. This can be useful when there is a need to sync with other time-based media, such as an audio file, a video, etc. For notation the quantizer will find the most appropriate subdivision according to your settings (the configuration of notational complexity will be discused later). Playback will respect your given start time, independently of the rounding effects regarding notation.

All time indications are given in beat time (also known as quarternote time). An offset or a duration express the time in terms of quarternotes. The absolute duration in seconds will depend on the tempo at the given moment

[17]:
offsets = [1.4, 1.42, 1.45, 1.47, 1.5, 1.55, 1.6, 1.7]
for offset in offsets:
    n = Note("4G", dur=2.5, offset=offset, gliss="4B", label=str(offset))
    n.show()
../_images/notebooks_firststeps_30_0.png
../_images/notebooks_firststeps_30_1.png
../_images/notebooks_firststeps_30_2.png
../_images/notebooks_firststeps_30_3.png
../_images/notebooks_firststeps_30_4.png
../_images/notebooks_firststeps_30_5.png
../_images/notebooks_firststeps_30_6.png
../_images/notebooks_firststeps_30_7.png

Pure Notation

Many purely notational aspects of a note/chord can be customized. These elements do not have an impact regarding playback. For example, a note can have an articulation, a size, color, etc. Here are some examples

[18]:
n = Note("4F+25", offset=1/5, dur=7/5)
n.addSymbol(symbols.Color('#0000f0'))
n.addSymbol(symbols.Notehead('square', size=1.3))
n.addSymbol(symbols.Articulation('accent'))
n
[18]:
4F>:1.4♩:offset=0.2:symbols=[Color(value=#0000f0), Notehead(square, size=1.3), Articulation(kind=accent)]
[19]:
n = Note("4G", dur=F(4, 5), offset="1/5")
n.addSymbol(symbols.Notehead(shape='slash', color='green'))
n.addSymbol(symbols.Articulation('snappizz'))
n
[19]:
4G:0.8♩:offset=0.2:symbols=[Notehead(slash, color=green), Articulation(kind=snappizz)]

Chords

A Chord is very similar to a Note but it allows multiple pitches.

A chord contains one or more Notes. These are interpreted as pitches and have no individual duration (the .dur attribute is ignored). On any other aspects they are fully fledged notes and can be customized individually

[2]:
setScoreStruct(timesig='4/4', tempo=60)
ch = Chord("E2 G#2 B2 E3 G3 Bb3 Db4 Eb4", dur=4)
ch.show()
ch.rec("tmp/stravinsky.ogg", fade=(0, 0.2))
../_images/notebooks_firststeps_35_0.png
[2]:
OfflineRenderer(outfile="/home/em/dev/python/maelzel/docs/notebooks/tmp/stravinsky.ogg", 2 channels, 4.20 secs, 44100 Hz)

The same chord distributed explicitely between two voices

Notice how the enharmonic spelling algorithm chooses a different spelling here in comparison to the example above. This is because above the chord is is distributed between two systems, whereas in this example the chords are manually split and unaware of each other (the spelling algorithm works at the part level)

[3]:
left = Chord("E2 G#2 B2 E3", 4)
right = Chord("G3 Bb3 Db4 Eb4", 4)
score = Score([right, left])
score.show()
../_images/notebooks_firststeps_37_0.png

A Chord is made out of Notes, so each note can be given own parameters.

[4]:
chord = Chord("3f 3b 4d# 4g#", dur=4)
chord.show()

chord.rec("tmp/tristan.ogg").show()

# Chords are always sorted by pitch. The last note is the highest
chord[-1].amp = 0.8

for note in chord[:-1]:
    note.amp = 0.1

chord.rec("tmp/chord-with-different-amplitudes.ogg").show()
../_images/notebooks_firststeps_39_0.png
OfflineRenderer(outfile="/home/em/dev/python/maelzel/docs/notebooks/tmp/tristan.ogg", 2 channels, 4.02 secs, 44100 Hz)

OfflineRenderer(outfile="/home/em/dev/python/maelzel/docs/notebooks/tmp/chord-with-different-amplitudes.ogg", 2 channels, 4.02 secs, 44100 Hz)

Enharmonic Spelling

A small detour about spelling: in general the enharmonic spelling routine chooses the best spelling for any notes according to the context. Even if the pitch is given as a notename, by default the engine will still insist in using its own spelling. See for example:

[5]:
chord = Chord("G4 Bb4 D#4", 4)
chord

[5]:
‹4Eb 4G 4Bb 4♩›

In order to force the spelling there are two different ways:

  1. Append a ! sign to the notename

  2. Set the config key fixStringNotenames to True (getConfig()['fixStringNotenames'] = True). If this is set to True any pitch given as a string is interpreted as being fixed (as if it had an implicit ! sign)

Notice that fixing a pitch spelling has consequences for the context of that pitch. In the previous case, fixing the spelling of the D# modifies the spelling of the rest of the chord. One would need to fix all the pitches in order to force the spelling as is.

NB: there are some enharmonic spellings which are not used by default. These include double alterations in the form of Dbb (enharmonic of C) or spellings like E# (enharmonic of F)

[18]:
Chord("G4 Bb4 D#4!", 4, label='The Bb is changed to A#').show()
Chord("G4 Bb4! D#4!", 4, label='Now the Bb is fixed').show()
Chord("4C 4E#!", 4, label='An E# is never supported').show()
../_images/notebooks_firststeps_43_0.png
../_images/notebooks_firststeps_43_1.png
../_images/notebooks_firststeps_43_2.png

Glissandi

The pitch of a Note or Chord does not need to be static. In each case it is possible to define a glissando (the .gliss attribute) for the given duration of the event.

NB: you can set an explicit target for a glissando or you can set the .gliss attribute to True, in which case the following event in the sequence determines the target pitch of the glissando

[20]:
Note("4G", gliss="4B")
[20]:
4G:1♩:gliss=4B
[5]:
chain = Chain([
    Note("4G", 2/3, gliss=True),
    Note("4A", 4/3, gliss="4F#")
])
chain.show()
chain.rec(nchnls=1)
../_images/notebooks_firststeps_46_0.png
[5]:
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2023-03-28T15:41:04.547.wav", 1 channels, 2.02 secs, 44100 Hz)

[6]:
ch = Chord("4G 4B", 3, gliss="4G- 4A")
ch.show()
ch.rec(instr='tri', fade=0.5, nchnls=1, extratime=0.55)
../_images/notebooks_firststeps_47_0.png
[6]:
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2023-03-28T15:41:07.342.wav", 1 channels, 3.55 secs, 44100 Hz)

Series of chord glissandi are also possible. Here we render using a triangular wave

[7]:
chain = Chain([
    Chord("4G 4B", 4/3, gliss=True),
    Chord("4G- 4A", 5/3, gliss="4G+ 4Ab")
])
chain.show()
chain.rec(instr='tri', nchnls=1, fade=0.1, extratime=0.15)
../_images/notebooks_firststeps_49_0.png
[7]:
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2023-03-28T15:41:12.169.wav", 1 channels, 3.15 secs, 44100 Hz)

Chain

As seen in many previous examples, a Chain is used to represent a sequence of notes or chords

[9]:
chain = Chain("3f 3b 4Eb 4g#".split())
chain.show()
chain.rec(nchnls=1, sustain=0.2)
../_images/notebooks_firststeps_51_0.png
[9]:
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2023-03-24T21:08:35.367.wav", 1 channels, 4.22 secs, 44100 Hz)

Notes within a chain can have different durations

[6]:
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#", 1/3),
    Note("6Eb", 1/3),
    Note("6F", 1/3)
])
chain.show()
# sustain adds an extra duration to the playback. This helps when playing a sequence legato
chain.rec(sustain=0.15, fade=(0, 0.15))
../_images/notebooks_firststeps_53_0.png
[6]:
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2023-03-28T13:36:14.239.wav", 2 channels, 7.30 secs, 44100 Hz)

Operations on Chains

Chains are useful to express operations on a sequence of notes or pitches

[7]:
chainReversed = Chain(chain.items[::-1])
chainReversed.show()
# Add sustain to simulate the pedal being down
chainReversed.rec(sustain=4, extratime=2)

../_images/notebooks_firststeps_55_0.png
[7]:
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2023-03-28T13:36:19.726.wav", 2 channels, 13.00 secs, 44100 Hz)

[19]:
chainInv = chain.invertPitch(pivot="5C")
chainInv.show()
# A little sustain can be used to simulate legato
chainInv.rec(extratime=0.1, sustain=0.1)
../_images/notebooks_firststeps_56_0.png
[19]:
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2023-03-03T09:48:53.111.wav", 2 channels, 7.20 secs, 44100 Hz)

Offset

Within a Chain, notes are stacked together, as long as they do not have an explicit offset. A gap within items in a chain can be achieved by either an explicit offset or by a Rest

Any start time within a Chain is interpreted as relative to the beginning of the chain (a Chain can have also an explicit offset or be placed within another Chain)

[3]:
chain = Chain([
    Note("4C", label='default dur'),
    Note("4D", dur=0.5),
    Note("4F", dur=2, offset=5)
])
chain
[3]:
Chain([4C:1♩, 4D:0.5♩, 4F:2♩:offset=5], dur=7)

A chain itself can have an explicit offset. The offset of each note within a chain is relative to the beginning of the chain.

[5]:
chain1 = Chain([
    "4G:1",
    "4Eb:1"
])
chain1.addSpanner('slur')
chain1[0].label='chain1'

chain2 = Chain([
    Note("4C", label='default dur'),
    Note("4D", dur=0.5),
    Note("4F", dur=2, offset=5),
    chain1
], offset=2)
chain2
[5]:
Chain([4C:1♩, 4D:0.5♩, 4F:2♩:offset=5, Chain([4G:1♩:symbols=[Slur(kind=start, uuid=r33b92cj, linetype=solid)], 4D#:1♩:symbols=[Slur(kind=end, uuid=r33b92cj, linetype=solid)]])], dur=9, offset=2)

Multiple Voices

A Voice is very similar to a Chain.

  • A Voice contains a sequence of non-overlapping objects (notes, chords, rests, clips)

  • A Voice can also contain other Chains.

  • A voice has always an offset of 0.

  • A Voice corresponds to a part in a Score - they can be stacked together to create a Score.

[6]:
from maelzel.core import *

# It is possible to create notes/chords in a variety of ways

righthand = Voice([
    Rest(0.25),
    Chord("4F 5E", 0.5),
    Rest(0.25),
    Note("5C#", .25),
    Rest(.5),
    "4Eb:.25",
    "4C,5D:.25"
])

lefthand = Voice([
    "R/8",
    "3B/16",
    "3F#,4G:.25",
    "R/8",
    "2A,3Bb/8",
    "R/16",
    "4G#/16"
])

score = Score([righthand, lefthand])

# Create a score structure with a time signature of 3/16 with tempo ♪. = 40
setScoreStruct(timesig=(3, 16), tempo=30)

score.show()
score.rec(sustain=0.1, fade=(0, 0.1))
../_images/notebooks_firststeps_62_0.png
[6]:
OfflineRenderer(outfile="/home/em/.local/share/maelzel/recordings/rec-2023-03-24T21:11:41.437.wav", 2 channels, 5.20 secs, 44100 Hz)