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')