Source code for music21.volpiano
# ------------------------------------------------------------------------------
# Name: volpiano.py
# Purpose: music21 classes for converting to and from volpiano
#
# Authors: Michael Scott Asato Cuthbert
#
# Copyright: Copyright © 2017 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
'''
The Volpiano font is a specialized font for encoding Western Plainchant
easily with immediate visual feedback (see the CANTUS database).
This module parses chants encoded in Volpiano and can generate Volpiano
from any music21 Stream.
This module will move to a medren package hopefully by v7.
'''
from __future__ import annotations
import enum
from music21 import bar
from music21 import base
from music21 import clef
from music21 import environment
from music21 import exceptions21
from music21 import layout
from music21 import note
from music21 import pitch
from music21 import spanner
from music21 import stream
environLocal = environment.Environment('volpiano.py')
class VolpianoException(exceptions21.Music21Exception):
pass
# JetBrains does not understand this form of Enum
# noinspection PyArgumentList
ErrorLevel = enum.Enum('ErrorLevel', 'WARN LOG')
[docs]
class Neume(spanner.Spanner):
'''
A spanner that represents a Neume. No name of the neume, just that it is a Neume.
'''
[docs]
class LineBreak(base.Music21Object):
'''
Indicates that the line breaks at this point in the manuscript.
Denoted by one 7.
'''
pass
[docs]
class PageBreak(base.Music21Object):
'''
Indicates that the page breaks at this point in the manuscript.
Denoted by two 7s.
'''
pass
[docs]
class ColumnBreak(base.Music21Object):
'''
Indicates that the page breaks at this point in the manuscript.
Denoted by three 7s.
'''
pass
classByNumBreakTokens = [None, LineBreak, PageBreak, ColumnBreak]
classByNumBreakTokensLayout = [None, layout.SystemLayout, layout.PageLayout, ColumnBreak]
normalPitches = '9abcdefghjklmnopqrs'
liquescentPitches = ')ABCDEFGHJKLMNOPQRS'
eflatTokens = 'wx'
bflatTokens = 'iyz'
flatTokens = eflatTokens + bflatTokens
naturalTokens = flatTokens.upper()
accidentalTokens = flatTokens + naturalTokens
[docs]
def toPart(volpianoText, *, breaksToLayout=False):
# noinspection PyShadowingNames
'''
Returns a music21 Part from volpiano text.
>>> veniSancti = volpiano.toPart('1---c--d---f--d---ed--c--d---f'
... '---g--h--j---hgf--g--h---')
>>> veniSancti.show('text')
{0.0} <music21.stream.Measure 0 offset=0.0>
{0.0} <music21.clef.TrebleClef>
{0.0} <music21.note.Note C>
{1.0} <music21.note.Note D>
{2.0} <music21.note.Note F>
{3.0} <music21.note.Note D>
{4.0} <music21.note.Note E>
{5.0} <music21.note.Note D>
{6.0} <music21.volpiano.Neume <music21.note.Note E><music21.note.Note D>>
{6.0} <music21.note.Note C>
{7.0} <music21.note.Note D>
{8.0} <music21.note.Note F>
{9.0} <music21.note.Note G>
{10.0} <music21.note.Note A>
{11.0} <music21.note.Note B>
{12.0} <music21.note.Note A>
{13.0} <music21.note.Note G>
{14.0} <music21.note.Note F>
{15.0} <music21.volpiano.Neume <music21.note.Note A><music21.note.Note G>>
{15.0} <music21.note.Note G>
{16.0} <music21.note.Note A>
Clefs!
>>> clefTest = volpiano.toPart('1---c--2---c')
>>> clefTest.show('text')
{0.0} <music21.stream.Measure 0 offset=0.0>
{0.0} <music21.clef.TrebleClef>
{0.0} <music21.note.Note C>
{1.0} <music21.clef.BassClef>
{1.0} <music21.note.Note E>
>>> for n in clefTest.recurse().notes:
... n.nameWithOctave
'C4'
'E2'
Flats and Naturals:
>>> accTest = volpiano.toPart('1---e--we--e--We--e')
>>> [n.name for n in accTest.recurse().notes]
['E', 'E-', 'E-', 'E', 'E']
Breaks and barlines
>>> breakTest = volpiano.toPart('1---e-7-e-77-e-777-e-3-e-4')
>>> breakTest.show('text')
{0.0} <music21.stream.Measure 0 offset=0.0>
{0.0} <music21.clef.TrebleClef>
{0.0} <music21.note.Note E>
{1.0} <music21.volpiano.LineBreak object at 0x105250fd0>
{1.0} <music21.note.Note E>
{2.0} <music21.volpiano.PageBreak object at 0x105262128>
{2.0} <music21.note.Note E>
{3.0} <music21.volpiano.ColumnBreak object at 0x105262240>
{3.0} <music21.note.Note E>
{4.0} <music21.bar.Barline type=regular>
{4.0} <music21.stream.Measure 0 offset=4.0>
{0.0} <music21.note.Note E>
{1.0} <music21.bar.Barline type=double>
As layout objects using breaksToLayout=True
>>> breakTest = volpiano.toPart('1---e-7-e-77-e-777-e-3-e-4', breaksToLayout=True)
>>> breakTest.show('text')
{0.0} <music21.stream.Measure 0 offset=0.0>
{0.0} <music21.clef.TrebleClef>
{0.0} <music21.note.Note E>
{1.0} <music21.layout.SystemLayout>
{1.0} <music21.note.Note E>
{2.0} <music21.layout.PageLayout>
{2.0} <music21.note.Note E>
{3.0} <music21.volpiano.ColumnBreak object at 0x105262240>
{3.0} <music21.note.Note E>
{4.0} <music21.bar.Barline type=regular>
{4.0} <music21.stream.Measure 0 offset=4.0>
{0.0} <music21.note.Note E>
{1.0} <music21.bar.Barline type=double>
Liquescence test:
>>> breakTest = volpiano.toPart('1---e-E-')
>>> breakTest.recurse().notes[0].editorial.liquescence
False
>>> breakTest.recurse().notes[0].notehead
'normal'
>>> breakTest.recurse().notes[1].editorial.liquescence
True
>>> breakTest.recurse().notes[1].notehead
'x'
* Changed in v5.7: corrected spelling of liquescence.
'''
p = stream.Part()
m = stream.Measure()
currentMeasure = m
currentNeumeSpanner = None
noteThatWouldGoInSpanner = None
lastClef = clef.TrebleClef()
continuousNumberOfBreakTokens = 0
bIsFlat = False
eIsFlat = False
for token in volpianoText:
if token == '7':
continuousNumberOfBreakTokens += 1
continue
elif continuousNumberOfBreakTokens > 0:
if not breaksToLayout: # default
breakClass = classByNumBreakTokens[continuousNumberOfBreakTokens]
breakToken = breakClass() # pylint: disable=not-callable
else:
breakClass = classByNumBreakTokensLayout[continuousNumberOfBreakTokens]
if continuousNumberOfBreakTokens < 3:
breakToken = breakClass(isNew=True) # pylint: disable=not-callable
else:
breakToken = breakClass() # pylint: disable=not-callable
currentMeasure.append(breakToken)
continuousNumberOfBreakTokens = 0
if token == '-':
noteThatWouldGoInSpanner = None
if currentNeumeSpanner:
currentMeasure.append(currentNeumeSpanner)
currentNeumeSpanner = None
continue
if token in '1234':
noteThatWouldGoInSpanner = None
currentNeumeSpanner = None
if token in '12':
if token == '1':
c = clef.TrebleClef()
else:
c = clef.BassClef()
lastClef = c
m.append(c)
elif token in '34':
bl = bar.Barline()
if token == '4':
bl.type = 'double'
m.rightBarline = bl
p.append(m)
m = stream.Measure()
elif token in normalPitches or token in liquescentPitches:
n = note.Note()
n.stemDirection = 'noStem'
if token in normalPitches:
distanceFromLowestLine = normalPitches.index(token) - 5
n.editorial.liquescence = False
else:
distanceFromLowestLine = liquescentPitches.index(token) - 5
n.notehead = 'x'
n.editorial.liquescence = True
clefLowestLine = lastClef.lowestLine
diatonicNoteNum = clefLowestLine + distanceFromLowestLine
n.pitch.diatonicNoteNum = diatonicNoteNum
if n.pitch.step == 'B' and bIsFlat:
n.pitch.accidental = pitch.Accidental('flat')
elif n.pitch.step == 'E' and eIsFlat:
n.pitch.accidental = pitch.Accidental('flat')
m.append(n)
if noteThatWouldGoInSpanner is not None:
currentNeumeSpanner = Neume([noteThatWouldGoInSpanner, n])
noteThatWouldGoInSpanner = None
else:
noteThatWouldGoInSpanner = n
elif token in accidentalTokens:
if token.lower() in eflatTokens and token in naturalTokens:
eIsFlat = False
elif token.lower() in bflatTokens and token in naturalTokens:
bIsFlat = False
elif token.lower() in eflatTokens and token in flatTokens:
eIsFlat = True
elif token.lower() in bflatTokens and token in flatTokens:
bIsFlat = True
else: # pragma: no cover
raise VolpianoException(
'Unknown accidental: ' + token + ': Should not happen')
if continuousNumberOfBreakTokens > 0:
breakClass = classByNumBreakTokens[continuousNumberOfBreakTokens]
breakToken = breakClass()
currentMeasure.append(breakToken)
if m:
p.append(m)
return p
[docs]
def fromStream(s, *, layoutToBreaks=False):
'''
Convert a Stream to Volpiano.
These tests show how the same input converts back out:
>>> volpianoInput = '1--c--d---f--d---ed--c--d---f---g--h--j---hgf--g--h---'
>>> veniSancti = volpiano.toPart(volpianoInput)
>>> volpiano.fromStream(veniSancti)
'1---c-d-f-d-ed-c-d-f-g-h-j-hg-f-g-h-'
>>> breakTest = volpiano.toPart('1---e-E--')
>>> volpiano.fromStream(breakTest)
'1---e-E-'
>>> accTest = volpiano.toPart('1---e--we--e--We--e')
>>> volpiano.fromStream(accTest)
'1---e-we-e-We-e-'
'''
volpianoTokens = []
def error(innerEl, errorLevel=ErrorLevel.LOG):
msg = f'Could not convert token {innerEl!r} to Volpiano.'
if errorLevel == ErrorLevel.WARN:
environLocal.warn(msg + ' this can lead to incorrect data.')
else:
environLocal.printDebug(msg)
def ap(tokens):
for t in tokens:
volpianoTokens.append(t)
def popHyphens():
while volpianoTokens and volpianoTokens[-1] == '-':
volpianoTokens.pop()
distToAccidental = {
-3: 'y',
0: 'w',
4: 'i',
7: 'x',
11: 'z',
}
def setAccFromPitch(dist, setNatural=False):
if dist not in distToAccidental:
error(f'{dist} above lowest line', ErrorLevel.WARN)
return
accidentalToken = distToAccidental[dist]
if setNatural:
accidentalToken = accidentalToken.upper()
ap(accidentalToken)
lastClef = clef.TrebleClef()
bIsFlat = False
eIsFlat = False
for el in s.recurse():
elClasses = el.classes
if 'Clef' in elClasses:
lastClef = el
if 'TrebleClef' in elClasses:
ap('1---')
elif 'BassClef' in elClasses:
ap('2---')
else:
error(el, ErrorLevel.WARN)
elif 'Barline' in elClasses:
if el.type in ('double', 'final'):
ap('---4')
else:
ap('---3')
elif 'Note' in elClasses:
n = el
p = n.pitch
dnn = p.diatonicNoteNum
distanceFromLowestLine = dnn - lastClef.lowestLine
indexInPitchString = distanceFromLowestLine + 5
if indexInPitchString < 0 or indexInPitchString >= len(normalPitches):
error(n, ErrorLevel.WARN)
continue
if n.notehead == 'x' or (n.hasEditorialInformation
and 'liquescence' in n.editorial
and n.editorial.liquescence):
tokenName = liquescentPitches[indexInPitchString]
else:
tokenName = normalPitches[indexInPitchString]
if p.accidental is not None and p.accidental.alter != 0:
if p.step not in ('B', 'E'):
error(el, ErrorLevel.WARN)
ap(tokenName)
continue
elif p.accidental.alter != -1:
error(el, ErrorLevel.WARN)
ap(tokenName)
continue
if p.step == 'B' and not bIsFlat:
setAccFromPitch(distanceFromLowestLine)
bIsFlat = True
elif p.step == 'E' and not eIsFlat:
setAccFromPitch(distanceFromLowestLine)
eIsFlat = True
elif p.name == 'B' and bIsFlat:
setAccFromPitch(distanceFromLowestLine, setNatural=True)
bIsFlat = False
elif p.name == 'E' and eIsFlat:
setAccFromPitch(distanceFromLowestLine, setNatural=True)
eIsFlat = False
ap(tokenName)
neumeSpanner = n.getSpannerSites('Neume')
if neumeSpanner and n is not neumeSpanner[0].getLast():
pass
elif not n.lyric:
ap('-')
else:
lyricObject = n.lyrics[0]
syl = lyricObject.syllabic
if syl in ('single', 'end'):
ap('---')
else:
ap('--')
elif 'SystemLayout' in elClasses and layoutToBreaks:
popHyphens()
ap('7---')
elif 'PageLayout' in elClasses and layoutToBreaks:
popHyphens()
ap('77---')
elif 'LineBreak' in elClasses:
popHyphens()
ap('7---')
elif 'PageBreak' in elClasses:
popHyphens()
ap('77---')
elif 'ColumnBreak' in elClasses:
popHyphens()
ap('777---')
else:
error(el, ErrorLevel.LOG)
return ''.join(volpianoTokens)
if __name__ == '__main__':
import music21
# allow things like "fromStream" to be called in doctests as "fromStream"
# and not just "volpiano.fromStream"
music21.mainTest('importPlusRelative')