# ------------------------------------------------------------------------------
# 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"</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)