diff --git a/music21/lily/lilyObjects.py b/music21/lily/lilyObjects.py index c9a4eeb24..77eadfa0a 100644 --- a/music21/lily/lilyObjects.py +++ b/music21/lily/lilyObjects.py @@ -867,23 +867,34 @@ class LyTempoEvent(LyObject): r''' tempo_event: "\tempo" steno_duration '=' tempo_range | "\tempo" scalar steno_duration '=' tempo_range + | "\tempo" steno_duration '=' scalar | "\tempo" scalar >>> lte = lily.lilyObjects.LyTempoEvent(scalar='40') >>> str(lte) '\\tempo 40' - More complex: + A steno_duration paired with a single bpm scalar (and no tempoRange) + is the common case for a music21 MetronomeMark, e.g. quarter = 87: - >>> steno = lily.lilyObjects.LyStenoDuration('quarter') + >>> steno = lily.lilyObjects.LyStenoDuration('4') + >>> lte = lily.lilyObjects.LyTempoEvent(stenoDuration=steno, scalar=87) + >>> str(lte) + '\\tempo 4 = 87' + + More complex, with a tempo range. Note that steno_duration takes a + Lilypond duration number such as '4' for a quarter note, not the + English name 'quarter': + + >>> steno = lily.lilyObjects.LyStenoDuration('4') >>> tempoRange = lily.lilyObjects.LyTempoRange(70, 100) >>> lte = lily.lilyObjects.LyTempoEvent(tempoRange=tempoRange, stenoDuration=steno) >>> str(lte) - '\\tempo quarter = 70~100 ' + '\\tempo 4 = 70~100 ' >>> lte.scalar = 85 >>> str(lte) - '\\tempo 85 quarter = 70~100 ' + '\\tempo 85 4 = 70~100 ' ''' def __init__(self, tempoRange=None, stenoDuration=None, scalar=None): @@ -906,6 +917,11 @@ def stringOutput(self): else: return ' '.join([base, self.stenoDuration.stringOutput(), '=', self.tempoRange.stringOutput()]) + elif self.stenoDuration is not None: + if self.scalar is None: # pragma: no cover + raise LilyObjectsException( + 'If tempoRange is not defined but stenoDuration is, need a scalar') + return ' '.join([base, self.stenoDuration.stringOutput(), '=', str(self.scalar)]) elif self.scalar is None: # pragma: no cover raise LilyObjectsException('If tempoRange is not defined then need scalar') diff --git a/music21/lily/translate.py b/music21/lily/translate.py index 2975dcb20..9f307aaa4 100644 --- a/music21/lily/translate.py +++ b/music21/lily/translate.py @@ -33,6 +33,7 @@ from music21 import key from music21 import note from music21 import stream +from music21 import tempo from music21 import variant from music21.lily import lilyObjects as lyo @@ -1096,6 +1097,11 @@ def appendM21ObjectToContext(self, thisObject): lyObject = self.lyEmbeddedScmFromTimeSignature(thisObject) currentMusicList.append(lyObject) lyObject.setParent(contextObject) + elif 'MetronomeMark' in c: + lyObject = self.lyEmbeddedScmFromMetronomeMark(thisObject) + if lyObject is not None: + currentMusicList.append(lyObject) + lyObject.setParent(contextObject) elif 'Variant' in c: self.appendContextFromVariant(thisObject, coloredVariants=self.coloredVariants) elif 'SystemLayout' in c: @@ -1680,6 +1686,38 @@ def lyEmbeddedScmFromTimeSignature(self, ts): lpEmbeddedScm.content = keyScheme return lpEmbeddedScm + def lyEmbeddedScmFromMetronomeMark(self, mm: tempo.MetronomeMark) -> lyo.LyEmbeddedScm|None: + # noinspection PyShadowingNames + r''' + convert a :class:`~music21.tempo.MetronomeMark` object + to a lilyObjects.LyEmbeddedScm object + + * New in v11: MetronomeMark objects are now written out when + converting a Stream to Lilypond; previously they were silently dropped. + + >>> mm = tempo.MetronomeMark(number=87, referent=note.Note(type='quarter')) + >>> conv = lily.translate.LilypondConverter() + >>> print(conv.lyEmbeddedScmFromMetronomeMark(mm)) + \tempo 4 = 87 + + A MetronomeMark without a number produces no output: + + >>> mm = tempo.MetronomeMark() + >>> mm.number is None + True + >>> conv.lyEmbeddedScmFromMetronomeMark(mm) is None + True + ''' + if mm.number is None: + return None + + multipliedDuration = self.lyMultipliedDurationFromDuration(mm.referent) + tempoEvent = lyo.LyTempoEvent(stenoDuration=multipliedDuration.stenoDur, scalar=mm.number) + + lpEmbeddedScm = lyo.LyEmbeddedScm() + lpEmbeddedScm.content = tempoEvent.stringOutput() + lpEmbeddedScm.newlineIndent + return lpEmbeddedScm + def setContextForTupletStart(self, inObj): r''' if the inObj has tuplets then we set a new context @@ -2596,6 +2634,27 @@ def testColors(self): "c' 4 " ) + def testMetronomeMark(self): + mm = tempo.MetronomeMark(number=87, referent=note.Note(type='quarter')) + lpEmbeddedScm = LilypondConverter().lyEmbeddedScmFromMetronomeMark(mm) + self.assertEqual(str(lpEmbeddedScm).strip(), r'\tempo 4 = 87') + + # a MetronomeMark with no number produces no output + textOnly = tempo.MetronomeMark() + self.assertIsNone(LilypondConverter().lyEmbeddedScmFromMetronomeMark(textOnly)) + + def testMetronomeMarkWrittenInStream(self): + # https://github.com/cuthbertLab/music21/issues/1852 + from music21 import key + from music21 import meter + keysig = key.Key('a-') + mm = tempo.MetronomeMark(number=87, referent=note.Note(type='quarter')) + timesig = meter.TimeSignature('3/4') + s = stream.Stream([keysig, mm, timesig, note.Note()]) + lpc = LilypondConverter() + lpc.loadObjectFromScore(s, makeNotation=False) + self.assertIn(r'\tempo 4 = 87', str(lpc.topLevelObject)) + class TestExternal(unittest.TestCase): show = True