import openmusickit as omk
from openmusickit.systems import wsmn
from openmusickit.systems.wsmn.symbols import *Examples
Imports
The top-level openmusickit namespace holds what any piece of music is built from, whatever its musical system: the graph, the objects placed in it, and the values that belong to no one system. A musical system is opted into separately. Western Standard Music Notation (WSMN) has its types in wsmn and its ready-made vocabulary (pitches, intervals, chord types, durations, time signatures, marks, clefs) in wsmn.symbols.
Tones
In WSMN, a tone is a TonalVector: a tuple of (diatonic letter index, chromatic pitch class, octave). Middle C is octave 0 internally, but strings and display default to the usual middle C = C4 convention.
c4 = wsmn.TonalVector.from_string("C4")
fs5 = wsmn.TonalVector.from_string("F#5")
bb3 = wsmn.TonalVector.from_string("Bb3")
print(repr(c4))
print(repr(fs5))
print(f"{c4} {fs5} {bb3}")
print(f"{fs5:ascii}")TonalVector((0, 0, 0))
TonalVector((3, 6, 1))
C4 F♯5 B♭3
F#5
Pitch-class symbols with no octave are ready-made in wsmn.symbols. An x is a sharp and a b is a flat.
print(f"{C} {Cx} {Eb} {G}")
print(f"{C.qualify_octave(0)}")C C♯ E♭ G
C4
Intervals and transposition
A TonalVector is also an interval, and the same class represents both. Transpose a tone by adding an interval to it, or by calling transpose with a direction.
print(f"{c4 + M3}")
print(f"{c4 + P5}")
print(f"{c4.transpose(M3, wsmn.TonalDirection.DOWN)}")
print(f"{c4 - m7}")E4
G4
A♭3
D3
The interval between two tones is their distance.
e4 = c4 + M3
gap = c4.distance(e4)
print(gap.interval.unicode, "=", abs(gap), "half steps")major 3 = 4 half steps
Durations
A MetricalDuration is a fraction of a whole note, with optional dots. wsmn.symbols provides the common ones by name.
print(repr(wsmn.MetricalDuration(1, 4)))
print(repr(wsmn.MetricalDuration(3, 4)))
print(dotted_half.rational_length)
print(repr(quarter + eighth))MetricalDuration(1, 4)
MetricalDuration(1, 2, dots=1)
3/4
MetricalDuration(1, 4, dots=1)
Notes and rests
A NoteEvent holds a set of tones (one for a single note, several for a chord in one voice) and a duration. A Rest is a NoteEvent holding a silent tone.
note = omk.NoteEvent(tones={c4}, duration=quarter)
rest = omk.Rest(quarter)
print(note)
print(rest)
print(rest.is_rest)NoteEvent(tones=[TonalVector((0, 0, 0))], duration=MetricalDuration(1, 4))
Rest(duration=MetricalDuration(1, 4))
True
Transposing a note transforms its tones in place.
note.transform_tones(wsmn.TonalVector.transpose, P5)
print(f"{next(iter(note.tones))}")G4
Chords and keys
A chord is a chord type realized at a root. Calling a root with a chord type, or a chord type with a root, does the same thing.
print(C(maj), G(dom7), D(min_))
print(C(maj) / E)
print([f"{t}" for t in C(maj)])C G7 Dmin
C/E
['C', 'E', 'G']
A key is a tonic and a mode.
d_major = wsmn.Key.of(D, Major)
print(d_major.name)
print(d_major.signature)
print([f"{t}" for t in d_major.tones])D Major
KeySignature(c=1, f=1)
['D', 'E', 'F♯', 'G', 'A', 'B', 'C♯']
Lyrics
parse_lyrics splits text into syllables. Hyphens split a word into syllables and whitespace separates words.
for syllable in omk.parse_lyrics("Twin-kle, twin-kle, lit-tle star"):
print(f"{str(syllable):10} {syllable.placement.value:8} {syllable.word}")Twin - beginning Twinkle,
- kle, end Twinkle,
twin - beginning twinkle,
- kle, end twinkle,
lit - beginning little
- tle end little
star whole star
A small score graph
Musical objects become nodes in an OmkGraph. A line is a chain of events joined by NEXT edges. Independent lines are related in time by pinning one event to another with a SIMULTANEOUS edge. Lyrics are a line of syllables, each attached to the note where it begins with a LYRIC edge. A Score carries the work’s metadata and names the component: it points at the heads of its lines. A clef, like a key or a meter, is a zero-duration ClefEvent at the point in the line where it takes effect.
Here is the first phrase of Twinkle, Twinkle, Little Star: a melody under a treble clef, its lyrics, and a chord line above it.
graph = omk.OmkGraph(omk.GraphMeta())
score = omk.Score(title="Twinkle, Twinkle, Little Star", lyricist="Jane Taylor")
# The melody: a clef, then one line of NoteEvents.
pitches = "C4 C4 G4 G4 A4 A4 G4 F4 F4 E4 E4 D4 D4 C4".split()
rhythm = [quarter] * 6 + [half] + [quarter] * 6 + [half]
melody = [
omk.NoteEvent(tones={wsmn.TonalVector.from_string(p)}, duration=d)
for p, d in zip(pitches, rhythm, strict=True)
]
clef = omk.ClefEvent(clef=treble_clef)
graph.add_line([clef, *melody])
graph.add_line_to_score(score, clef) # the clef is the head of the line
# The harmony: its own line, pinned to the first note of the melody.
chords = [
omk.ChordEvent(chord=C(maj), duration=whole),
omk.ChordEvent(chord=F(maj), duration=half),
omk.ChordEvent(chord=C(maj), duration=half),
omk.ChordEvent(chord=F(maj), duration=half),
omk.ChordEvent(chord=C(maj), duration=half),
omk.ChordEvent(chord=G(dom7), duration=half),
omk.ChordEvent(chord=C(maj), duration=half),
]
graph.add_line(chords)
graph.add_simultaneous(melody[0], chords[0])
graph.add_line_to_score(score, chords[0])
# The lyrics: a line of syllables, zipped onto the notes one syllable per onset.
verse = omk.LyricSection(section_type="verse", section_number=1)
syllables = graph.add_lyrics(
"Twin-kle, twin-kle, lit-tle star, how I won-der what you are",
section=verse,
)
graph.zip_lyrics_to_objects(syllables[0], melody[0])Walking a line yields its events in order. Every position is relative to some reference event; here the reference is the first note, and onsets are shown as fractions of a whole note.
def lyric_on(event):
return " ".join(
str(s) for s in syllables
if list(graph.edges_between(event, s, omk.EdgeType.LYRIC))
)
print(f"{'onset':<8}{'tone':<6}{'duration':<28}lyric")
for event in graph.walk_line(melody[0]):
onset = graph.relative_onset(melody[0], event).rational_length
tone = next(iter(event.tones))
print(f"{str(onset):<8}{format(tone, 'ascii'):<6}{event.duration!r:<28}{lyric_on(event)}")onset tone duration lyric
0 C4 MetricalDuration(1, 4) Twin -
1/4 C4 MetricalDuration(1, 4) - kle,
1/2 G4 MetricalDuration(1, 4) twin -
3/4 G4 MetricalDuration(1, 4) - kle,
1 A4 MetricalDuration(1, 4) lit -
5/4 A4 MetricalDuration(1, 4) - tle
3/2 G4 MetricalDuration(1, 2) star,
2 F4 MetricalDuration(1, 4) how
9/4 F4 MetricalDuration(1, 4) I
5/2 E4 MetricalDuration(1, 4) won -
11/4 E4 MetricalDuration(1, 4) - der
3 D4 MetricalDuration(1, 4) what
13/4 D4 MetricalDuration(1, 4) you
7/2 C4 MetricalDuration(1, 2) are
The chord line is walked the same way, and its onsets are still measured from the first melody note because the two lines are pinned together.
for event in graph.walk_line(chords[0]):
onset = graph.relative_onset(melody[0], event).rational_length
print(f"{str(onset):<8}{event.chord}")0 C
1 F
3/2 C
2 F
5/2 C
3 G7
7/2 C
The score names the whole: its title, every event of every line it holds, and the clef, which takes no time.
print(score.title)
print(len(list(graph.walk_score(score))), "events in the score")
print(graph.relative_onset(clef, melody[0]))Twinkle, Twinkle, Little Star
22 events in the score
ZeroDuration()
Transposing a span of the graph transforms every tone in it.
graph.transform_tones(melody[0], melody[-1], wsmn.TonalVector.transpose, P5)
print(" ".join(format(next(iter(e.tones)), "ascii") for e in graph.walk_line(melody[0])))G4 G4 D5 D5 E5 E5 D5 C5 C5 B4 B4 A4 A4 G4
Drawing the graph
A graph can be drawn. GraphRenderer lays it out with Graphviz: NEXT advances along the page, everything that a timing walk places at one instant shares a column, chords sit above the line they are pinned to, lyrics below the line they are sung to, and a score is a title block. to_dot() needs nothing installed; to_svg() and to_png() run Graphviz’s dot, which must be on the PATH. This page hands the DOT to Quarto, which renders it with its own copy of Graphviz. The melody is drawn as it now stands, a fifth up.
from openmusickit.visualization import GraphRenderer
# Quarto sizes a dot figure by these options (inches), else squeezes it into 7 by 5;
# the values are the drawing's natural size, and the div lets a wide one scroll.
print(":::: {.omk-drawing}")
print("```{dot}\n//| fig-width: 12\n//| fig-height: 2.6\n" + GraphRenderer(graph).to_dot() + "\n```")
print("::::")A drum kit
Unpitched percussion has no pitch to name, so a PercussionTone is a relative pitch and a stroke, either or both left out: PercussionTone() is a plain hit on whatever the part is, and the ready-made ones have names. The instrument is the PercussionPart, which may carry a palette of the tones it is expected to use. A drum kit is one part per drum, each on its own line, held together by a LineGroup: the origin its lines are measured from, so they need no pins to be aligned. Percussion tones have their own symbols module, because some of their names (heel, toe) are also the names of marks.
from openmusickit.systems.wsmn.percussion.symbols import (
bass_drums, closed, hi_hats, hit, open_hat, snare_drums,
)
def line(*tones, duration):
return [
omk.Rest(duration) if tone is None else omk.NoteEvent(tones={tone}, duration=duration)
for tone in tones
]
kit = omk.OmkGraph(omk.GraphMeta())
hi_hat = line(*[closed] * 7, open_hat, duration=eighth)
snare = line(None, hit, None, hit, duration=quarter)
kick = line(hit, None, hit, None, duration=quarter)
for drum in (hi_hat, snare, kick):
kit.add_line(drum)
drums = omk.LineGroup(name="Drum kit")
kit.add_group(drums, [hi_hat[0], snare[0], kick[0]])
kit.add_stint(omk.PercussionPart(name="Hi-hat", palette=hi_hats), omk.Stint(), hi_hat[0])
kit.add_stint(omk.PercussionPart(name="Snare", palette=snare_drums), omk.Stint(), snare[0])
kit.add_stint(omk.PercussionPart(name="Kick", palette=bass_drums), omk.Stint(), kick[0])Each line is walked on its own, and onsets on any line can be measured from any event of another, because the group relates them.
for name, drum in (("hi-hat", hi_hat), ("snare", snare), ("kick", kick)):
cells = []
for event in kit.walk_line(drum[0]):
onset = kit.relative_onset(hi_hat[0], event).rational_length
sound = "rest" if event.is_rest else f"{next(iter(event.tones))}"
cells.append(f"{str(onset):>4} {sound}")
print(f"{name:<7}", " | ".join(cells))hi-hat 0 closed | 1/8 closed | 1/4 closed | 3/8 closed | 1/2 closed | 5/8 closed | 3/4 closed | 7/8 open
snare 0 rest | 1/4 hit | 1/2 rest | 3/4 hit
kick 0 hit | 1/4 rest | 1/2 hit | 3/4 rest
A tone prints as its readable form, and a remap over a line is an ordinary tone operation.
print(f"{closed}", "/", f"{open_hat}", "/", f"{hit}")
kit.transform_tones(hi_hat[0], None, lambda tone: open_hat if tone == closed else tone)
print(" ".join(f"{next(iter(e.tones))}" for e in kit.walk_line(hi_hat[0])))
print(len(list(kit.walk_group(drums))), "events in the kit")closed / open / hit
open open open open open open open open
16 events in the kit
Drawn top to bottom, the three lines run down the page in the order the group holds them, their heads on one row because the group is their common origin, and each part sits before the line it plays.
from openmusickit.visualization import SequenceDirection, VisualizationConfig
config = VisualizationConfig(direction=SequenceDirection.TOP_TO_BOTTOM)
print(":::: {.omk-drawing}")
print("```{dot}\n//| fig-width: 2.5\n//| fig-height: 6.2\n" + GraphRenderer(kit, config).to_dot() + "\n```")
print("::::")