Source code for music21.capella.fromCapellaXML

# ------------------------------------------------------------------------------
# Name:         fromCapellaXML.py
# Purpose:      Module for importing capellaXML (.capx) files.
#
# Authors:      Michael Scott Asato Cuthbert
#
# Copyright:    Copyright © 2012-2026 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
'''
A beta version of a complete .capx to music21 converter.

Currently only handles one <voice> per <staff> and does not deal with
slurs, dynamics, ornamentation, etc.

Does not handle pickup notes, which are defined simply with an early barline
(the same as incomplete bars at the end).
'''
from __future__ import annotations

import pathlib
import typing as t
import unittest
import xml.etree.ElementTree as ET
import zipfile

from music21 import bar
from music21 import chord
from music21 import clef
from music21 import common
from music21 import duration
from music21 import exceptions21
from music21 import layout
from music21 import key
from music21 import meter
from music21 import note
from music21 import pitch
from music21 import stream
from music21 import tie

# capellaDynamics = {'r': 'ppp',
#                   'q': 'pp',
#                   'p': 'p',
#                   'i': 'mp',
#                   'j': 'mf',
#                   'f': 'f',
#                   'g': 'ff',
#                   'h': 'fff',
#                   's': 'sf',
#                   'z': 'sfz',
#                   '{': 'fz',
#                   '|': 'fp',
#                   }
#
# isSegno1 = lambda char: char == 'y'
# isSegno2 = lambda char: char == '$'
# isSegno  = lambda char: isSegno1(char) or isSegno2(char)
# isCodaLarge = lambda char: char == 'n'
# isCodaSmall = lambda char: char == 'o'
# isCoda = lambda char: isCodaLarge(char) or isCodaSmall(char)
# isPedalStart = lambda char: char == 'a'
# isPedalStop = lambda char: char == 'b'
# isFermataAbove = lambda char: char == 'u'
# isFermataBelow = lambda char: char == 'k'
# isFermata = lambda char: isFermataAbove(char) or isFermataBelow(char)
#
# isUpbow = lambda char: char == 'Z'
# isDownbow = lambda char: char == 'Y'
#
# isTrill = lambda char: char == 't'
# isInvertedMordent = lambda char: char == 'l'
# isMordent = lambda char: char == 'x'
# isTurn = lambda char: char == 'w'
# isOrnament = lambda char: (isTrill(char) or isInvertedMordent(char) or
#        isMordent(char) or isTurn(char))


class CapellaImportException(exceptions21.Music21Exception):
    pass


[docs] class CapellaImporter: ''' Object for importing .capx, CapellaXML files into music21 (from which they can be converted to musicxml, MIDI, lilypond, etc.) Note that Capella stores files closer to their printed versions -- that is to say, Systems enclose all the parts for that system and have new clefs, etc. ''' def __init__(self) -> None: self.xmlText: str|bytes|None = None self.zipFilename: str|None = None self.mainDom: ET.Element|None = None
[docs] def scoreFromFile( self, filename: str|pathlib.Path, systemScore: bool = False ) -> stream.Score: ''' Main program: opens a file given by filename and returns a complete music21 Score from it. If systemScore is True then it skips the step of making Parts from Systems and Measures within Parts. ''' self.readCapellaXMLFile(filename) mainDom = self.parseXMLText() scoreObj = self.systemScoreFromScore(mainDom) if systemScore: return scoreObj else: partScore = self.partScoreFromSystemScore(scoreObj) return partScore
[docs] def readCapellaXMLFile(self, filename: str|pathlib.Path) -> bytes: ''' Reads in a .capx file at `filename`, stores it as self.zipFilename, unzips it, extracts the score.xml embedded file, sets self.xmlText to the contents. Returns self.xmlText. ''' self.zipFilename = str(filename) with zipfile.ZipFile(str(filename), 'r') as zipFileHandle: xmlText = zipFileHandle.read('score.xml') self.xmlText = xmlText return xmlText
[docs] def parseXMLText(self, xmlText: str|bytes|None = None) -> ET.Element: ''' Takes the string (or unicode string) in xmlText and parses it with `xml.etree`. Sets `self.mainDom` to the dom object and returns the dom object. ''' if xmlText is None: xmlText = self.xmlText if xmlText is None: raise CapellaImportException( 'No XML text to parse; call readCapellaXMLFile first.') if not isinstance(xmlText, str): xmlText = xmlText.decode('utf-8') rootElement = ET.fromstring(xmlText) for el in rootElement.iter(): if '}' in el.tag: el.tag = el.tag.split('}', 1)[1] # strip all namespaces self.mainDom = rootElement return rootElement
[docs] def domElementFromText(self, xmlText: str|None = None) -> ET.Element: ''' Utility method, especially for the documentation examples/tests, which uses `xml.etree.ElementTree` to parse the string and returns its root object. Not used by the main parser. >>> ci = capella.fromCapellaXML.CapellaImporter() >>> funnyTag = ci.domElementFromText( ... '<funny yes="definitely"><greg/>hi<greg><ha>ha</ha>' ... '<greg type="embedded"/></greg></funny>') >>> funnyTag <Element 'funny' at 0x...> ``iter`` searches recursively: >>> len(list(funnyTag.iter('greg'))) 3 ``findall`` does not: >>> len(funnyTag.findall('greg')) 2 ''' if xmlText is None: raise CapellaImportException('No XML text provided to parse.') return ET.fromstring(xmlText)
[docs] def partScoreFromSystemScore(self, systemScore: stream.Score) -> stream.Score: ''' Take a :class:`~music21.stream.Score` object which is organized by Systems and return a new `Score` object which is organized by Parts. ''' # this line is redundant currently, since all we have in systemScore # are Systems, but later there will be other things. systemStream = systemScore.getElementsByClass(layout.System) partDictById: dict[str|int, dict[str, t.Any]] = {} for thisSystem in systemStream: # this line is redundant currently, since all we have in # thisSystem are Parts, but later there will be other things. systemOffset = systemScore.elementOffset(thisSystem) partStream = thisSystem.getElementsByClass(stream.Part) for j, thisPart in enumerate(partStream): if thisPart.id not in partDictById: newPart = stream.Part() newPart.id = thisPart.id partDictById[thisPart.id] = {'part': newPart, 'number': j} else: newPart = partDictById[thisPart.id]['part'] for el in thisPart: # no need for recurse newPart.coreInsert(common.opFrac(el.offset + systemOffset), el) newPart.coreElementsChanged() newScore = stream.Score() # ORDERED DICT parts: list[stream.Part|None] = [None for i in range(len(partDictById))] for partId in partDictById: partDict = partDictById[partId] parts[partDict['number']] = partDict['part'] for p in parts: # remove redundant Clef and KeySignatures if p is None: print('part entries do not match partDict!') continue clefs = p.getElementsByClass(clef.Clef) keySignatures = p.getElementsByClass(key.KeySignature) lastClef = None lastKeySignature = None for c in clefs: if c == lastClef: p.remove(c) else: lastClef = c for ks in keySignatures: if ks == lastKeySignature: p.remove(ks) else: lastKeySignature = ks p.makeMeasures(inPlace=True) # for m in p.getElementsByClass(stream.Measure): # barLines = m.getElementsByClass(bar.Barline) # for bl in barLines: # blOffset = bl.offset # if blOffset == 0.0: # m.remove(bl) # m.leftBarline = bl # elif blOffset == m.highestTime: # m.remove(bl) # m.rightBarline = bl # will not yet work for double repeats! newScore.coreInsert(0, p) newScore.coreElementsChanged() return newScore
[docs] def systemScoreFromScore( self, scoreElement: ET.Element, scoreObj: stream.Score|None = None ) -> stream.Score: ''' Returns an :class:`~music21.stream.Score` object from a <score> tag. The Score object is not a standard music21 Score object which contains parts, then measures, then voices, but instead contains systems which optionally contain voices, which contain parts. No measures have yet been created. ''' if scoreObj is None: scoreObj = stream.Score() systemsList = scoreElement.findall('systems') if not systemsList: raise CapellaImportException( 'Cannot find a <systems> tag in the <score> object') if len(systemsList) > 1: raise CapellaImportException( 'Found more than one <systems> tag in the <score> object, what does this mean?') systemsElement = systemsList[0] systemList = systemsElement.findall('system') if not systemList: raise CapellaImportException( 'Cannot find any <system> tags in the <systems> tag in the <score> object') for systemNumber, thisSystem in enumerate(systemList): systemObj = self.systemFromSystem(thisSystem) systemObj.systemNumber = systemNumber + 1 # 1 indexed, like musicians think scoreObj.coreAppend(systemObj) scoreObj.coreElementsChanged() return scoreObj
[docs] def systemFromSystem( self, systemElement: ET.Element, systemObj: layout.System|None = None ) -> layout.System: r''' Returns a :class:`~music21.stream.System` object from a <system> tag. The System object will contain :class:`~music21.stream.Part` objects which will have the notes, etc. contained in it. TODO: Handle multiple <voices> ''' if systemObj is None: systemObj = layout.System() stavesList = systemElement.findall('staves') if not stavesList: raise CapellaImportException('No <staves> tag found in this <system> element') if len(stavesList) > 1: raise CapellaImportException( 'More than one <staves> tag found in this <system> element') stavesElement = stavesList[0] staffList = stavesElement.findall('staff') if not stavesList: raise CapellaImportException( 'No <staff> tag found in the <staves> element for this <system> element') for thisStaffElement in staffList: # do something with defaultTime partId = 'UnknownPart' if 'layout' in thisStaffElement.attrib: partId = thisStaffElement.attrib['layout'] partObj = stream.Part() partObj.id = partId voicesList = thisStaffElement.findall('voices') if not voicesList: raise CapellaImportException( 'No <voices> tag found in the <staff> tag for the <staves> element ' + 'for this <system> element') voicesElement = voicesList[0] voiceList = voicesElement.findall('voice') if not voiceList: raise CapellaImportException( 'No <voice> tag found in the <voices> tag for the <staff> tag for the ' + '<staves> element for this <system> element') if len(voiceList) == 1: # single voice staff perfect! thisVoiceElement = voiceList[0] noteObjectsList = thisVoiceElement.findall('noteObjects') if not noteObjectsList: raise CapellaImportException( 'No <noteObjects> tag found in the <voice> tag found in the ' + '<voices> tag for the <staff> tag for the <staves> element for ' + 'this <system> element') if len(noteObjectsList) > 1: raise CapellaImportException( 'More than one <noteObjects> tag found in the <voice> tag found ' + 'in the <voices> tag for the <staff> tag for the <staves> element ' + 'for this <system> element') thisNoteObject = noteObjectsList[0] self.streamFromNoteObjects(thisNoteObject, partObj) systemObj.insert(0, partObj) return systemObj
[docs] def streamFromNoteObjects( self, noteObjectsElement: ET.Element, streamObj: stream.Stream|None = None ) -> stream.Stream: # noinspection PyShadowingNames r''' Converts a <noteObjects> tag into a :class:`~music21.stream.Stream` object which is returned. A Stream can be given as an optional argument, in which case the objects of this Stream are appended to this object. >>> ci = capella.fromCapellaXML.CapellaImporter() >>> noteObjectsString = r""" ... <noteObjects> ... <clefSign clef="G2-"/> ... <keySign fifths="-1"/> ... <chord> ... <duration base="1/2"/> ... <lyric> ... <verse i="0">das,</verse> ... <verse i="1">scherz,</verse> ... </lyric> ... <heads> ... <head pitch="G4"/> ... </heads> ... </chord> ... <chord> ... <duration base="1/2"/> ... <lyric> ... <verse i="0">so</verse> ... <verse i="1">der</verse> ... </lyric> ... <heads> ... <head pitch="A4"/> ... </heads> ... </chord> ... <barline type="end"/> ... </noteObjects> ... """ >>> noteObjectsElement = ci.domElementFromText(noteObjectsString) >>> streamObj = ci.streamFromNoteObjects(noteObjectsElement) >>> streamObj.show('text') {0.0} <music21.clef.Treble8vbClef> {0.0} <music21.key.KeySignature of 1 flat> {0.0} <music21.note.Note G> {2.0} <music21.note.Note A> {4.0} <music21.bar.Barline type=final> >>> streamObj.highestTime 4.0 ''' s: stream.Stream if streamObj is None: s = stream.Stream() else: s = streamObj mapping: dict[str, t.Callable[[ET.Element], t.Any]] = { 'clefSign': self.clefFromClefSign, 'keySign': self.keySignatureFromKeySign, 'timeSign': self.timeSignatureFromTimeSign, 'rest': self.restFromRest, 'chord': self.chordOrNoteFromChord, 'barline': self.barlineListFromBarline, } for d in noteObjectsElement: el = None dTag = d.tag if dTag not in mapping: print(f'Unknown tag type: {dTag}') else: el = mapping[dTag](d) if isinstance(el, list): # barlineList returns a list for elSub in el: s.coreAppend(elSub) elif el is None: pass else: s.coreAppend(el) s.coreElementsChanged() return s
[docs] def restFromRest(self, restElement: ET.Element) -> note.Rest: # noinspection PyShadowingNames ''' Returns a :class:`~music21.rest.Rest` object from a <rest> tag. >>> ci = capella.fromCapellaXML.CapellaImporter() >>> restElement = ci.domElementFromText('<rest><duration base="1/2"/></rest>') >>> r = ci.restFromRest(restElement) >>> r <music21.note.Rest half> >>> r.duration.type 'half' ''' r = note.Rest() durationList = restElement.findall('duration') r.duration = self.durationFromDuration(durationList[0]) return r
[docs] def chordOrNoteFromChord(self, chordElement: ET.Element) -> note.Note|chord.Chord: # noinspection PyShadowingNames ''' Returns a :class:`~music21.note.Note` or :class:`~music21.chord.Chord` from a chordElement -- a `Note` is returned if the <chord> has one <head> element, a `Chord` is returned if there are multiple <head> elements. >>> ci = capella.fromCapellaXML.CapellaImporter() >>> chordElement = ci.domElementFromText( ... '<chord><duration base="1/1"/><heads><head pitch="G4"/></heads></chord>') >>> n = ci.chordOrNoteFromChord(chordElement) >>> n <music21.note.Note G> >>> n.duration <music21.duration.Duration 4.0> This one is an actual chord: >>> chordElement = ci.domElementFromText( ... '<chord><duration base="1/8"/>' ... '<heads><head pitch="G4"/><head pitch="A5"/></heads></chord>') >>> c = ci.chordOrNoteFromChord(chordElement) >>> c <music21.chord.Chord G3 A4> >>> c.duration <music21.duration.Duration 0.5> ''' # TODO: test Lyrics durationList = chordElement.findall('duration') headsList = chordElement.findall('heads') if len(durationList) != 1 or len(headsList) != 1: raise CapellaImportException('Malformed chord!') notesList = self.notesFromHeads(headsList[0]) if not notesList: raise CapellaImportException('Malformed chord!') noteOrChord: note.Note|chord.Chord if len(notesList) == 1: noteOrChord = notesList[0] # a Note object else: noteOrChord = chord.Chord(notesList) noteOrChord.duration = self.durationFromDuration(durationList[0]) lyricsList = chordElement.findall('lyric') if lyricsList: noteOrChord.lyrics = self.lyricListFromLyric(lyricsList[0]) return noteOrChord
[docs] def notesFromHeads(self, headsElement: ET.Element) -> list[note.Note]: # noinspection PyShadowingNames ''' Returns a list of :class:`~music21.note.Note` elements for each <head> in <heads>. >>> ci = capella.fromCapellaXML.CapellaImporter() >>> headsElement = ci.domElementFromText( ... '<heads><head pitch="B7"><alter step="-1"/></head><head pitch="C2"/></heads>') >>> ci.notesFromHeads(headsElement) [<music21.note.Note B->, <music21.note.Note C>] ''' notes: list[note.Note] = [] headDomList = headsElement.findall('head') for headElement in headDomList: notes.append(self.noteFromHead(headElement)) return notes
[docs] def noteFromHead(self, headElement: ET.Element) -> note.Note: # noinspection PyShadowingNames ''' Return a :class:`~music21.note.Note` object from a <head> element. This will become part of Chord._notes if there are multiple, but in any case, it needs to be a Note not a Pitch for now, because it could have Tie information. >>> ci = capella.fromCapellaXML.CapellaImporter() >>> headElement = ci.domElementFromText( ... '<head pitch="B7"><alter step="-1"/><tie end="true"/></head>') >>> n = ci.noteFromHead(headElement) >>> n <music21.note.Note B-> >>> n.octave # capella octaves are one higher than written 6 >>> n.tie <music21.tie.Tie stop> ''' if 'pitch' not in headElement.attrib: raise CapellaImportException('Cannot deal with <head> element without pitch!') noteNameWithOctave = headElement.attrib['pitch'] n = note.Note() n.nameWithOctave = noteNameWithOctave n.octave = n.pitch.implicitOctave - 1 # capella octaves are 1 off alters = headElement.findall('alter') if len(alters) > 1: raise CapellaImportException('Cannot deal with multiple <alter> elements!') if len(alters) == 1: acc = self.accidentalFromAlter(alters[0]) n.pitch.accidental = acc ties = headElement.findall('tie') if len(ties) > 1: raise CapellaImportException('Cannot deal with multiple <tie> elements!') if len(ties) == 1: thisTie = self.tieFromTie(ties[0]) n.tie = thisTie return n
[docs] def accidentalFromAlter(self, alterElement: ET.Element) -> pitch.Accidental: ''' Return a :class:`~music21.pitch.Accidental` object from an <alter> tag. >>> ci = capella.fromCapellaXML.CapellaImporter() >>> alter = ci.domElementFromText('<alter step="-1"/>') >>> ci.accidentalFromAlter(alter) <music21.pitch.Accidental flat> The only known display type is "suppress". >>> alter = ci.domElementFromText('<alter step="2" display="suppress"/>') >>> accidentalObject = ci.accidentalFromAlter(alter) >>> accidentalObject <music21.pitch.Accidental double-sharp> >>> accidentalObject.displayType 'never' ''' if 'step' in alterElement.attrib: alteration = int(alterElement.attrib['step']) else: print('No alteration...') alteration = 0 acc = pitch.Accidental(alteration) if 'display' in alterElement.attrib and alterElement.attrib['display'] == 'suppress': acc.displayType = 'never' return acc
[docs] def tieFromTie(self, tieElement: ET.Element) -> tie.Tie|None: ''' Returns a :class:`~music21.tie.Tie` element from a <tie> tag. If begin == 'true' then Tie.type = start. >>> ci = capella.fromCapellaXML.CapellaImporter() >>> tieEl = ci.domElementFromText('<tie begin="true"/>') >>> ci.tieFromTie(tieEl) <music21.tie.Tie start> If end == 'true' then Tie.type = stop. >>> tieEl = ci.domElementFromText('<tie end="true"/>') >>> ci.tieFromTie(tieEl) <music21.tie.Tie stop> If begin == 'true' and end == 'true' then Tie.type = continue (is this right?). >>> tieEl = ci.domElementFromText('<tie begin="true" end="true"/>') >>> ci.tieFromTie(tieEl) <music21.tie.Tie continue> ''' begin = False end = False if 'begin' in tieElement.attrib and tieElement.attrib['begin'] == 'true': begin = True if 'end' in tieElement.attrib and tieElement.attrib['end'] == 'true': end = True tieType = None if begin and end: tieType = 'continue' elif begin: tieType = 'start' elif end: tieType = 'stop' else: return None tieObj = tie.Tie(tieType) return tieObj
[docs] def lyricListFromLyric(self, lyricElement: ET.Element) -> list[note.Lyric]: ''' Returns a list of :class:`~music21.note.Lyric` objects from a <lyric> tag. >>> ci = capella.fromCapellaXML.CapellaImporter() >>> lyricEl = ci.domElementFromText( ... '<lyric><verse i="0" hyphen="true">di</verse>' ... '<verse i="1">man,</verse><verse i="2">frau,</verse></lyric>') >>> ci.lyricListFromLyric(lyricEl) [<music21.note.Lyric number=1 syllabic=begin text='di'>, <music21.note.Lyric number=2 syllabic=single text='man,'>, <music21.note.Lyric number=3 syllabic=single text='frau,'>] ''' lyricList: list[note.Lyric] = [] verses = lyricElement.findall('verse') for d in verses: thisLyric = self.lyricFromVerse(d) if thisLyric is not None: lyricList.append(thisLyric) return lyricList
[docs] def lyricFromVerse(self, verse: ET.Element) -> note.Lyric|None: # noinspection PyShadowingNames ''' Returns a :class:`~music21.note.Lyric` object from a <verse> tag. >>> ci = capella.fromCapellaXML.CapellaImporter() >>> verse = ci.domElementFromText('<verse i="0" hyphen="true">di&quot;</verse>') >>> ci.lyricFromVerse(verse) <music21.note.Lyric number=1 syllabic=begin text='di"'> Does not yet support the 'align' attribute. If the text is empty, returns None. ''' verseNumber = 1 syllabic: t.Literal['begin', 'single'] = 'single' if 'i' in verse.attrib: verseNumber = int(verse.attrib['i']) + 1 if 'hyphen' in verse.attrib and verse.attrib['hyphen'] == 'true': syllabic = 'begin' text = verse.text if text is None or text == '': return None else: lyric = note.Lyric(text=text, number=verseNumber, syllabic=syllabic, applyRaw=True) return lyric
# i = number - 1 # align # hyphen = true clefMapping = {'treble': clef.TrebleClef, 'bass': clef.BassClef, 'alto': clef.AltoClef, 'tenor': clef.TenorClef, 'G2-': clef.Treble8vbClef, }
[docs] def clefFromClefSign(self, clefSign: ET.Element) -> clef.Clef|None: # noinspection PyShadowingNames ''' Returns a :class:`~music21.clef.Clef` object or subclass from a <clefSign> tag. >>> ci = capella.fromCapellaXML.CapellaImporter() >>> clefSign = ci.domElementFromText('<clefSign clef="treble"/>') >>> ci.clefFromClefSign(clefSign) <music21.clef.TrebleClef> >>> clefSign = ci.domElementFromText('<clefSign clef="G2-"/>') >>> ci.clefFromClefSign(clefSign) <music21.clef.Treble8vbClef> >>> clefSign = ci.domElementFromText('<clefSign clef="F1+"/>') >>> clefObject = ci.clefFromClefSign(clefSign) >>> clefObject <music21.clef.FClef> >>> clefObject.sign 'F' >>> clefObject.line 1 >>> clefObject.octaveChange 1 ''' if 'clef' in clefSign.attrib: clefValue = clefSign.attrib['clef'] if clefValue in self.clefMapping: return self.clefMapping[clefValue]() elif clefValue[0] == 'p': return clef.PercussionClef() elif len(clefValue) > 1: clefSignAndLine = clefValue[0:2] clefOctaveChange = 0 if len(clefValue) > 2: if clefValue[2] == '+': clefOctaveChange = 1 elif clefValue[2] == '-': clefOctaveChange = -1 clefObj = clef.clefFromString(clefSignAndLine, clefOctaveChange) return clefObj return None
[docs] def keySignatureFromKeySign(self, keySign: ET.Element) -> key.KeySignature|None: # noinspection PyShadowingNames ''' Returns a :class:`~music21.key.KeySignature` object from a keySign tag. >>> ci = capella.fromCapellaXML.CapellaImporter() >>> keySign = ci.domElementFromText('<keySign fifths="-1"/>') >>> ci.keySignatureFromKeySign(keySign) <music21.key.KeySignature of 1 flat> ''' if 'fifths' in keySign.attrib: keyFifths = int(keySign.attrib['fifths']) return key.KeySignature(keyFifths) return None
[docs] def timeSignatureFromTimeSign(self, timeSign: ET.Element) -> meter.TimeSignature|None: # noinspection PyShadowingNames ''' Returns a :class:`~music21.meter.TimeSignature` object from a timeSign tag. >>> ci = capella.fromCapellaXML.CapellaImporter() >>> timeSign = ci.domElementFromText('<timeSign time="4/4"/>') >>> ci.timeSignatureFromTimeSign(timeSign) <music21.meter.TimeSignature 4/4> >>> timeSign = ci.domElementFromText('<timeSign time="infinite"/>') >>> ci.timeSignatureFromTimeSign(timeSign) is None True ''' if 'time' in timeSign.attrib: timeString = timeSign.attrib['time'] if timeString != 'infinite': return meter.TimeSignature(timeString) else: return None else: return None
[docs] def durationFromDuration(self, durationElement: ET.Element) -> duration.Duration: ''' Return a music21.duration.Duration element from an XML Element representing a duration. >>> ci = capella.fromCapellaXML.CapellaImporter() >>> durationTag = ci.domElementFromText('<duration base="1/32" dots="1"/>') >>> durationObj = ci.durationFromDuration(durationTag) >>> durationObj <music21.duration.Duration 0.1875> >>> durationObj.type '32nd' >>> durationObj.dots 1 Here with Tuplets: >>> durationTag2 = ci.domElementFromText( ... '<duration base="1/4"><tuplet count="3"/></duration>') >>> d2 = ci.durationFromDuration(durationTag2) >>> d2 <music21.duration.Duration 2/3> >>> d2.type 'quarter' >>> d2.tuplets (<music21.duration.Tuplet 3/2>,) Does not handle noDuration='true', display, churchStyle on rest durations. ''' dur = duration.Duration() if 'base' in durationElement.attrib: baseValue = durationElement.attrib['base'] slashIndex = baseValue.find('/') if slashIndex != -1: firstNumber = int(baseValue[0:slashIndex]) secondNumber = int(baseValue[slashIndex + 1:]) quarterLength = (4.0 * firstNumber) / secondNumber dur.quarterLength = quarterLength if 'dots' in durationElement.attrib: dotNumber = int(durationElement.attrib['dots']) dur.dots = dotNumber tuplets = durationElement.findall('tuplet') for d in tuplets: tuplet = self.tupletFromTuplet(d) dur.appendTuplet(tuplet) return dur
[docs] def tupletFromTuplet(self, tupletElement: ET.Element) -> duration.Tuplet: ''' Returns a :class:`~music21.duration.Tuplet` object from a <tuplet> tag. >>> ci = capella.fromCapellaXML.CapellaImporter() >>> tupletTag = ci.domElementFromText('<tuplet count="3"/>') >>> ci.tupletFromTuplet(tupletTag) <music21.duration.Tuplet 3/2> Does not handle 'tripartite' = True. ''' numerator = 1 denominator = 1 if 'count' in tupletElement.attrib: numerator = int(tupletElement.attrib['count']) denominator = 1 while numerator > denominator * 2: denominator *= 2 if 'prolong' in tupletElement.attrib and tupletElement.attrib['count'] == 'true': denominator *= 2 if 'tripartite' in tupletElement.attrib: print( "WE DON'T HANDLE TRIPARTITE YET! Email the file and a pdf so I can figure it out") tup = duration.Tuplet(numerator, denominator) return tup
barlineMap = {'single': 'normal', 'double': 'double', 'end': 'final', 'repEnd': 'end', 'repBegin': 'start', 'repEndBegin': 'end-start', }
[docs] def barlineListFromBarline(self, barlineElement: ET.Element) -> list[bar.Barline]: ''' Indication that the barline at this point should be something other than normal. Capella does not indicate measure breaks or barline breaks normally, so the only barlines that are indicated are unusual ones. Returns a LIST of :class:`~music21.bar.Barline` or :class:`~music21.bar.Repeat` objects because the `repEndBegin` type requires two `bar.Repeat` objects to encode in `music21`. >>> ci = capella.fromCapellaXML.CapellaImporter() >>> barlineTag = ci.domElementFromText('<barline type="end"/>') >>> ci.barlineListFromBarline(barlineTag) [<music21.bar.Barline type=final>] >>> repeatTag = ci.domElementFromText('<barline type="repEndBegin"/>') >>> ci.barlineListFromBarline(repeatTag) [<music21.bar.Repeat direction=end>, <music21.bar.Repeat direction=start>] ''' barlineList: list[bar.Barline] = [] hasRepeatEnd = False if 'type' in barlineElement.attrib: barlineType = barlineElement.attrib['type'] if barlineType.startswith('rep'): # begins with rep if barlineType in self.barlineMap: repeatType = self.barlineMap[barlineType] if repeatType.find('end') > -1: barlineList.append(bar.Repeat('end')) hasRepeatEnd = True if repeatType.find('start') > -1: startRep = bar.Repeat('start') if hasRepeatEnd: startRep.priority = 1 barlineList.append(startRep) else: if barlineType in self.barlineMap: barlineList.append(bar.Barline(self.barlineMap[barlineType])) return barlineList
[docs] def slurFromDrawObjSlur(self, drawObj: ET.Element) -> None: ''' Not implemented. ''' pass
class Test(unittest.TestCase): def testComplete(self): from music21 import text ci = CapellaImporter() capellaDirPath = common.getSourceFilePath() / 'capella' oswaldPath = capellaDirPath / r'Nu_rue_mit_sorgen.capx' partScore = ci.scoreFromFile(oswaldPath) self.assertEqual(len(partScore.parts), 3) self.assertGreater(len(partScore.recurse().notes), 20) self.assertIn('mass!', text.assembleLyrics(partScore.parts[0], 1)) self.assertIn('scherz', text.assembleLyrics(partScore.parts[0], 2)) class TestExternal(unittest.TestCase): show = True def testComplete(self): ci = CapellaImporter() capellaDirPath = common.getSourceFilePath() / 'capella' oswaldPath = capellaDirPath / r'Nu_rue_mit_sorgen.capx' partScore = ci.scoreFromFile(oswaldPath) if self.show: partScore.show() def xtestImportSorgen(self): ci = CapellaImporter() capellaDirPath = common.getSourceFilePath() / 'capella' oswaldPath = capellaDirPath / r'Nu_rue_mit_sorgen.capx' ci.readCapellaXMLFile(oswaldPath) ci.parseXMLText() # firstSystemObject = ci.mainDom.documentElement.getElementsByTagName('system')[0] # m21SystemObj = ci.systemFromSystem(firstSystemObject) # m21SystemObj.show('text') # scoreElement = ci.mainDom.documentElement.getElementsByTagName('score')[0] scoreObj = ci.systemScoreFromScore(ci.mainDom.documentElement) partScore = ci.partScoreFromSystemScore(scoreObj) if self.show: partScore.show() # ci.walkNodes() # print(ci.xmlText) if __name__ == '__main__': import music21 music21.mainTest(Test) # , TestExternal)