# ------------------------------------------------------------------------------
# Name: musedata.translate.py
# Purpose: Translate MuseData into music21 objects
#
# Authors: Christopher Ariza
# Michael Scott Asato Cuthbert
#
# Copyright: Copyright © 2010-2012 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
'''
**N.B. in Dec. 2014 MuseData access was removed from music21 because the rights conflicted with
access computationally from music21. This module is retained for anyone who has such access,
however it is completely untested now and errors cannot and will not be fixed.**
Functions for translating music21 objects and
:class:`~music21.musedata.base.MuseDataHandler` instances. Mostly,
these functions are for advanced, low level usage. For basic importing of MuseData
files from a file or URL to a :class:`~music21.stream.Stream`, use the music21
converter module's :func:`~music21.converter.parse` function.
'''
from __future__ import annotations
import unittest
from music21 import clef
from music21 import environment
environLocal = environment.Environment('musedata.translate')
# ------------------------------------------------------------------------------
def _musedataBeamToBeams(beamSymbol):
'''
Given a musedata beam symbol, converter to a music21 Beams object representation.
>>> from music21.musedata import translate
>>> translate._musedataBeamToBeams('[[')
<music21.beam.Beams <music21.beam.Beam 1/start>/<music21.beam.Beam 2/start>>
>>> translate._musedataBeamToBeams('===')
<music21.beam.Beams <music21.beam.Beam 1/continue>/<music21.beam.Beam
2/continue>/<music21.beam.Beam 3/continue>>
>>> translate._musedataBeamToBeams(r']/') # must escape backslash
<music21.beam.Beams <music21.beam.Beam 1/stop>/<music21.beam.Beam 2/partial/right>>
'''
from music21 import beam
beamsObj = beam.Beams()
# assume that we are starting with 8th, even if given as a space
for char in beamSymbol:
direction = None
if char == '[':
beamType = 'start'
elif char == ']':
beamType = 'stop'
elif char == '=':
beamType = 'continue'
elif char == '/': # forward is right
beamType = 'partial'
direction = 'right'
elif char in ('\\', r'\\'): # backward is left
beamType = 'partial'
direction = 'left'
else:
environLocal.printDebug(['cannot interpret beams char:', char])
continue
# will automatically increment number
# note that this does not permit defining 16th and not defining 8th
beamsObj.append(beamType, direction)
return beamsObj
def _musedataRecordListToNoteOrChord(records, previousElement=None):
'''
Given a list of MuseDataRecord objects, return a configured
:class:`~music21.note.Note` or :class:`~music21.chord.Chord`.
Optionally pass a previous element, which may be music21 Note, Chord, or Rest;
this is used to determine tie status
'''
from music21 import note
from music21 import chord
from music21 import tie
if len(records) == 1:
post = note.Note()
# directly assign pitch object; will already have accidental
post.pitch = records[0].getPitchObject()
else:
# environLocal.printDebug(['attempting chord creation: records', len(records)])
# can supply a list of Pitch objects at creation
post = chord.Chord([r.getPitchObject() for r in records])
# if a chord, we are assuming that all durations are the same
post.quarterLength = records[0].getQuarterLength()
# see if there are nay lyrics; not sure what to do if lyrics are defined
# for multiple chord tones
lyricList = records[0].getLyrics()
if lyricList is not None:
# cyclically calling addLyric will auto increment lyric number assigned
for lyric in lyricList:
post.addLyric(lyric)
# see if there are any beams; again, get from first record only
beamsChars = records[0].getBeams()
if beamsChars is not None:
post.beams = _musedataBeamToBeams(beamsChars)
# get accents and expressions; assumes all on first
# returns an empty list of None
for a in records[0].getArticulationObjects():
post.articulations.append(a)
for e in records[0].getExpressionObjects():
post.expressions.append(e)
dynamicObjs = [] # stored in stream, not Note
for d in records[0].getDynamicObjects():
dynamicObjs.append(d)
# presently this sets a single tie for a chord; may be different cases
if records[0].isTied():
post.tie = tie.Tie('start') # can be start or continue;
if previousElement is not None and previousElement.tie is not None:
# if previous is a "start" or a "continue"; this has to be a "continue"
# as musedata does not mark the end of a tie
if previousElement.tie.type in ['start', 'continue']:
post.tie = tie.Tie('continue')
else: # if no tie indication in the musedata record
if previousElement is not None and previousElement.tie is not None:
if previousElement.tie.type in ['start', 'continue']:
post.tie = tie.Tie('stop') # can be start, end, continue
return post, dynamicObjs
def _processPending(hasVoices, pendingRecords, eLast, m, vActive):
e, dynamicObjs = _musedataRecordListToNoteOrChord(pendingRecords, eLast)
# place dynamics at same position as element
if hasVoices:
vActive.coreAppend(e)
for d in dynamicObjs:
vActive.coreInsert(e.getOffsetBySite(vActive), d)
else:
m.coreAppend(e)
for d in dynamicObjs:
m.coreInsert(e.getOffsetBySite(m), d)
return e
[docs]
def musedataPartToStreamPart(museDataPart, inputM21=None):
'''
Translate a musedata part to a :class:`~music21.stream.Part`.
'''
from music21 import stream
from music21 import note
from music21 import tempo
if inputM21 is None:
s = stream.Score()
else:
s = inputM21
p = stream.Part()
p.id = museDataPart.getPartName()
p.partName = p.id
# create and store objects
mdmObjs = museDataPart.getMeasures()
# environLocal.printDebug(['first measure parent', mdmObjs[0].parent])
barCount = 0
# get each measure
# store last Note/Chord/Rest for tie comparisons; span measures
eLast = None
for mIndex, mdm in enumerate(mdmObjs):
# environLocal.printDebug(['processing:', mdm.src])
if not mdm.hasNotes():
continue
if mdm.hasVoices():
hasVoices = True
vActive = stream.Voice()
else:
hasVoices = False
vActive = None
# m = stream.Measure()
# get a measure object with a left configured bar line
if mIndex <= len(mdmObjs) - 2:
mdmNext = mdmObjs[mIndex + 1]
else:
mdmNext = None
m = mdm.getMeasureObject()
# conditions for a final measure definition defining the last bar
if mdmNext is not None and not mdmNext.hasNotes():
# environLocal.printDebug(['got mdmNext not none and not has notes'])
# get bar from next measure definition
m.rightBarline = mdmNext.getBarObject()
if barCount == 0: # only for when no bars are defined
# the parent of the measure is the part
c = mdm.parent.getClefObject()
if c is not None:
m.clef = mdm.parent.getClefObject()
m.timeSignature = mdm.parent.getTimeSignatureObject()
m.keySignature = mdm.parent.getKeySignature()
# look for a tempo indication
directive = mdm.parent.getDirective()
if directive is not None:
tt = tempo.TempoText(directive)
# if this appears to be a tempo indication, then get metro
if tt.isCommonTempoText():
mm = tt.getMetronomeMark()
m.insert(0, mm)
# get all records; may be notes or note components
mdrObjs = mdm.getRecords()
# store pairs of pitches and durations for making chords after a
# new note has been found
pendingRecords = []
# get notes in each record
for mdr in mdrObjs:
# environLocal.printDebug(['processing:', mdr.src])
if mdr.isBack():
# the current use of back assumes tt back assumes tt we always
# return to the start of the measure; this may not be the case
if pendingRecords:
eLast = _processPending(hasVoices, pendingRecords, eLast, m, vActive)
pendingRecords = []
# every time we encounter a back, we need to store
# our existing voice and create a new one
m.insert(0, vActive)
vActive = stream.Voice()
if mdr.isRest():
# environLocal.printDebug(['got mdr rest, parent:', mdr.parent])
# check for pending records first
if pendingRecords:
eLast = _processPending(hasVoices, pendingRecords, eLast, m, vActive)
pendingRecords = []
# create rest after clearing pending records
r = note.Rest()
r.quarterLength = mdr.getQuarterLength()
if hasVoices:
vActive.coreAppend(r)
else:
m.coreAppend(r)
eLast = r
continue
# a note is note as chord, but may have chord tones
# attached to it that follow
elif mdr.isChord():
# simply append if a chord; do not clear or change pending
pendingRecords.append(mdr)
elif mdr.isNote():
# either this is a note alone, or this is the first
# note found that is not a chord; if first not a chord
# need to append immediately
if pendingRecords:
# this could be a Chord or Note
eLast = _processPending(hasVoices, pendingRecords, eLast, m, vActive)
pendingRecords = []
# need to append this record for the current note
pendingRecords.append(mdr)
# check for any remaining single notes (if last) or chords
if pendingRecords:
eLast = _processPending(hasVoices, pendingRecords, eLast, m, vActive)
# may be bending elements in a voice to append to a measure
if vActive is not None and vActive:
vActive.coreElementsChanged()
m.coreInsert(0, vActive)
m.coreElementsChanged()
if barCount == 0 and m.timeSignature is not None: # easy case
# can only do this b/c ts is defined
if m.barDurationProportion() < 1.0:
m.padAsAnacrusis()
# environLocal.printDebug(['incompletely filled Measure found on musedata import; ',
# 'interpreting as a anacrusis:', 'paddingLeft:', m.paddingLeft])
p.coreAppend(m)
barCount += 1
p.coreElementsChanged()
# for now, make all imports a c-score on import;
tInterval = museDataPart.getTranspositionIntervalObject()
# environLocal.printDebug(['got transposition interval', p.id, tInterval])
if tInterval is not None:
p.flatten().transpose(tInterval,
classFilterList=['Note', 'Chord', 'KeySignature'],
inPlace=True)
# need to call make accidentals to correct new issues
p.makeAccidentals(inPlace=True)
if museDataPart.stage == 1:
# cannot yet get stage 1 clef data
p.getElementsByClass(stream.Measure).first().clef = clef.bestClef(p, recurse=True)
p.makeBeams(inPlace=True)
# will call overridden method on Part
p.makeAccidentals(inPlace=True)
# assume that beams and clefs are defined in all stage 2
s.insert(0, p)
return s
[docs]
def museDataWorkToStreamScore(museDataWork, inputM21=None):
'''
Given an museDataWork object, build into a multi-part
:class:`~music21.stream.Score` with metadata.
This assumes that this MuseDataHandler defines a single work
(with 1 or fewer reference numbers).
if the optional parameter inputM21 is given a music21 Stream subclass,
it will use that object
as the outermost object. However, inner parts will always be
made :class:`~music21.stream.Part` objects.
'''
from music21 import stream
from music21 import metadata
if inputM21 is None:
s = stream.Score()
else:
s = inputM21
# each musedata part has complete metadata, so must get first
mdpObjs = museDataWork.getParts()
md = metadata.Metadata()
s.insert(0, md)
md.title = mdpObjs[0].getWorkTitle()
md.movementNumber = mdpObjs[0].getMovementNumber()
md.movementName = mdpObjs[0].getMovementTitle()
# not obvious where composer is stored
# md.composer = mdpObjs[0].getWorkNumber()
# md.localeOfComposition = mdpObjs[0].getWorkNumber()
md.number = mdpObjs[0].getWorkNumber()
for mdPart in mdpObjs:
musedataPartToStreamPart(mdPart, s)
return s
# ------------------------------------------------------------------------------
class Test(unittest.TestCase):
def testBasic(self):
from music21 import musedata
from music21 import common
fp1 = (common.getSourceFilePath()
/ 'musedata' / 'testPrimitive' / 'test01' / '01.md')
mdw = musedata.MuseDataWork()
mdw.addFile(fp1)
s = museDataWorkToStreamScore(mdw)
# post = s.musicxml
# s.show()
self.assertEqual(len(s.parts), 1)
self.assertEqual(s.parts[0].id, 'Clarinet in A')
self.assertEqual(len(s.parts[0].recurse().notesAndRests), 54)
# # try stage 1
# mdw = musedata.MuseDataWork()
# mdw.addString(testFiles.bachContrapunctus1_part1)
# mdw.addString(testFiles.bachContrapunctus1_part2)
#
# s = museDataWorkToStreamScore(mdw)
# self.assertEqual(len(s.parts[0].recurse().notesAndRests), 291)
# self.assertEqual(len(s.parts[1].recurse().notesAndRests), 293)
# def testGetMetaData(self):
#
# from music21 import musedata
# from music21.musedata import testFiles
# def testGetLyrics(self):
# from music21 import corpus
#
# s = corpus.parse('hwv56', '1-08')
# self.assertEqual(len(s.parts), 2)
# self.assertEqual(s.parts[0].id, 'Contr\'alto')
# self.assertEqual(s.parts[1].id, 'Bassi')
#
# self.assertEqual(len(s.parts[0].recurse().notesAndRests), 34)
# self.assertEqual(len(s.parts[1].recurse().notesAndRests), 9)
#
# # note that hyphens are stripped on import
# self.assertEqual(s.parts[0].flatten().notesAndRests[2].lyric, 'Be')
# self.assertEqual(s.parts[0].flatten().notesAndRests[3].lyric, 'hold,')
# s.show()
def testGetBeams(self):
# try single character conversion
post = _musedataBeamToBeams('=')
self.assertEqual(str(post),
'<music21.beam.Beams <music21.beam.Beam 1/continue>>')
post = _musedataBeamToBeams(']\\')
self.assertEqual(
str(post),
'<music21.beam.Beams <music21.beam.Beam 1/stop>/<music21.beam.Beam 2/partial/left>>')
post = _musedataBeamToBeams(']/')
self.assertEqual(
str(post),
'<music21.beam.Beams <music21.beam.Beam 1/stop>/<music21.beam.Beam 2/partial/right>>')
# s = corpus.parse('hwv56', '1-18')
# self.assertEqual(len(s.parts), 5)
# # the fourth part is vocal, and has no beams defined
# self.assertEqual(str(s.parts[3].getElementsByClass(
# 'Measure')[3].notesAndRests[0].beams), '<music21.beam.Beams >')
# self.assertEqual(str(s.parts[3].getElementsByClass(
# 'Measure')[3].notesAndRests[0].lyric), 'sud')
#
# # the bottom part has 8ths beamed two to a bar
# self.assertEqual(str(s.parts[4].getElementsByClass(
# 'Measure')[3].notesAndRests[0].beams),
# '<music21.beam.Beams <music21.beam.Beam 1/start>>')
# self.assertEqual(str(s.parts[4].getElementsByClass(
# 'Measure')[3].notesAndRests[1].beams),
# '<music21.beam.Beams <music21.beam.Beam 1/continue>>')
# self.assertEqual(str(s.parts[4].getElementsByClass(
# 'Measure')[3].notesAndRests[2].beams),
# '<music21.beam.Beams <music21.beam.Beam 1/continue>>')
# self.assertEqual(str(s.parts[4].getElementsByClass(
# 'Measure')[3].notesAndRests[3].beams),
# '<music21.beam.Beams <music21.beam.Beam 1/stop>>')
#
# # s.show()
# # test that stage1 files continue to have makeBeams called
# s = corpus.parse('bwv1080', '16')
# # measure two has 9/16 beamed in three beats of 16ths
# self.assertEqual(len(s.parts), 2)
#
# # s.parts[0].getElementsByClass(stream.Measure)[1].show()
#
# self.assertEqual(str(s.parts[0].getElementsByClass(
# 'Measure')[1].notesAndRests[0].beams),
# '<music21.beam.Beams <music21.beam.Beam 1/start>/<music21.beam.Beam 2/start>>')
# self.assertEqual(str(s.parts[0].getElementsByClass(
# 'Measure')[1].notesAndRests[1].beams),
# '<music21.beam.Beams <music21.beam.Beam 1/continue>/<music21.beam.Beam 2/continue>>')
# self.assertEqual(str(s.parts[0].getElementsByClass(
# 'Measure')[1].notesAndRests[2].beams),
# '<music21.beam.Beams <music21.beam.Beam 1/stop>/<music21.beam.Beam 2/stop>>')
def testAccidentals(self):
'''
testing a piece with 1 flat to make sure that sharps appear but normal B-flats do not.
'''
pass
# s = corpus.parse('bwv1080', '16')
# self.assertEqual(len(s.parts[0].getKeySignatures()), 1)
# self.assertEqual(str(s.parts[0].getKeySignatures()[0]),
# '<music21.key.KeySignature of 1 flat>')
#
# notes = s.parts[0].flatten().notesAndRests
# self.assertEqual(str(notes[2].accidental), '<music21.pitch.Accidental sharp>')
# self.assertTrue(notes[2].accidental.displayStatus)
#
# # from key signature
# # B-, thus no flat should appear.
# self.assertEqual(str(notes[16].accidental), '<music21.pitch.Accidental flat>')
# self.assertFalse(notes[16].accidental.displayStatus)
#
# # cautionary from within measure, the C follows a C#
# notes = s.parts[1].measure(13).flatten().notesAndRests
# self.assertEqual(str(notes[8].accidental), '<music21.pitch.Accidental natural>')
# self.assertTrue(notes[8].accidental.displayStatus)
# s.show()
def testBackBasic(self):
from music21 import converter
from music21 import common
fpDir = common.getSourceFilePath() / 'musedata' / 'testPrimitive' / 'test01'
s = converter.parse(fpDir)
# note: this is a multi-staff work, but presently gets encoded
# as multiple voices
measures = s.parts[0].measures(1, 5)
self.assertEqual(len(measures[0].recurse().notesAndRests), 2)
self.assertEqual(len(measures[1].recurse().notesAndRests), 5)
self.assertEqual(len(measures[2].recurse().notesAndRests), 5)
self.assertEqual(len(measures[3].recurse().notesAndRests), 6)
self.assertEqual(len(measures[4].recurse().notesAndRests), 4)
# s.show()
# s.show()
# def testMuseDataStage1A(self):
# from music21 import corpus
# s = corpus.parse('k168', 1)
#
# self.assertEqual(len(s.parts), 4)
# self.assertEqual(str(s.parts[0][meter.TimeSignature].first()),
# '<music21.meter.TimeSignature 4/4>')
#
# self.assertEqual(
# [n.offset for n in s.parts[0].getElementsByClass(stream.Measure)[0].notes],
# [0.0, 3.0, 3.5, 3.75])
#
# self.assertEqual([n.nameWithOctave for
# n in s.parts[0].getElementsByClass(stream.Measure)[0].notes],
# ['F5', 'F5', 'E5', 'D5'])
#
# self.assertEqual(
# [n.offset for n in s.parts[1].getElementsByClass(stream.Measure)[0].notes],
# [1.0, 2.0, 3.0])
# def testMuseDataStage1B(self):
# from music21 import corpus
# s = corpus.parse('k169', 3)
#
# self.assertEqual(len(s.parts), 4)
# self.assertEqual(str(s.parts[0][meter.TimeSignature].first()),
# '<music21.meter.TimeSignature 3/4>')
#
# self.assertEqual(
# [n.offset for n in s.parts[0].getElementsByClass(stream.Measure)[0].notes],
# [0.0, 2.0])
#
# self.assertEqual([n.nameWithOctave for
# n in s.parts[0].getElementsByClass(stream.Measure)[0].notes], ['A4', 'B4'])
#
# self.assertEqual([n.offset
# for n in s.parts[2].getElementsByClass(stream.Measure)[0].notes],
# [0.0, 1.0, 2.0])
# def testMuseDataImportTempoA(self):
# from music21 import corpus
# # a small file
# s = corpus.parse('movement2-09.md')
# self.assertEqual(len(s.parts), 5)
# # the tempo is found in the 4th part here
# self.assertEqual(str(
# s.parts[3][tempo.TempoIndication].first()),
# '<music21.tempo.MetronomeMark Largo e piano Quarter=46>')
# # s.show()
#
# s = corpus.parse('movement2-07.md')
# self.assertEqual(str(
# s[tempo.TempoIndication].first()),
# '<music21.tempo.MetronomeMark Largo Quarter=46>')
# def testMuseDataImportDynamicsA(self):
# # note: this is importing a large work, but this seems to presently
# # be the only one with dynamics
#
# # TODO: Turn back on when a smaller work is found
# from music21 import corpus
# s = corpus.parse('symphony94', 3)
# sFlat = s.flatten()
# # s.show()
# self.assertEqual(len(sFlat.getElementsByClass(dynamics.Dynamic)), 79)
# def testMuseDataImportErrorA(self):
# from music21 import corpus
# # this files was crashing in the handling of an error in beam notation
# s = corpus.parse('haydn/opus55no1/movement2.md')
# self.assertEqual(len(s[note.Note]), 1735)
#
# # s.show('t')
# def testMuseDataImportErrorB(self):
# # this file has a malformed END repeated twice
# from music21 import corpus
# s = corpus.parse('haydn/opus71no1/movement1.zip')
# self.assertEqual(len(s[note.Note]), 2792)
# ------------------------------------------------------------------------------
# define presented order in documentation
_DOC_ORDER = [museDataWorkToStreamScore]
if __name__ == '__main__':
# sys.arg test options will be used in mainTest()
import music21
music21.mainTest(Test)