Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion music21/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
'''
from __future__ import annotations

__version__ = '11.0.0b7'
__version__ = '11.0.0b8'

def get_version_tuple(vv):
v = vv.split('.')
Expand Down
97 changes: 79 additions & 18 deletions music21/abcFormat/translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ def abcToStreamPart(

barCount = 0
measureNumber = 1
lyricTargets: list[note.GeneralNote] = []
# merged handler are ABCHandlerBar objects, defining attributes for barlines

for mh in mergedHandlers:
Expand Down Expand Up @@ -181,7 +182,9 @@ def abcToStreamPart(
# environLocal.printDebug([mh, 'dst', dst])
# ql = 0 # might not be zero if there is a pickup

postTransposition, clefSet = parseTokens(mh, dst, p, useMeasures)
postTransposition, clefSet = parseTokens(
mh, dst, p, useMeasures, lyricTargets
)

# append measure to part; in the case of trailing metadata
# dst may be part, even though useMeasures is True
Expand Down Expand Up @@ -253,7 +256,8 @@ def parseTokens(
mh: abcFormat.ABCHandler,
dst: stream.Measure|stream.Part,
p: stream.Part,
useMeasures: bool
useMeasures: bool,
lyricTargets: list[note.GeneralNote],
) -> tuple[int, bool]:
'''
Parses all the tokens in a measure or part.
Expand All @@ -265,11 +269,16 @@ def parseTokens(
clefSet = False
for t in mh.tokens:
if isinstance(t, abcFormat.ABCMetadata):
if t.tag == 'w':
addABCLyrics(t.data, lyricTargets)
lyricTargets.clear()
postTransposition, clefSet = metadataToM21Object(
t, dst, postTransposition, clefSet, useMeasures
)
elif isinstance(t, abcFormat.ABCNote):
parseABCNote(t, dst)
m21Note = parseABCNote(t, dst)
if m21Note is not None and not isinstance(m21Note, note.Rest) and not t.inGrace:
lyricTargets.append(m21Note)
elif isinstance(t, abcFormat.ABCSlurStart):
if typing.TYPE_CHECKING:
assert t.slurObj is not None
Expand All @@ -285,10 +294,36 @@ def parseTokens(
dst.coreElementsChanged()
return postTransposition, clefSet


def addABCLyrics(lyricData: str, lyricTargets: list[note.GeneralNote]) -> None:
'''
Attach simple ``w:`` ABC lyric syllables to their corresponding notes.

AI-assisted implementation.
'''
lyricTargetIndex = 0
for lyricWord in lyricData.split():
if lyricWord in ('*', '-'):
lyricTargetIndex += 1
continue

syllables = lyricWord.split('-')
for syllableIndex, syllable in enumerate(syllables):
if not syllable:
lyricTargetIndex += 1
continue
if lyricTargetIndex >= len(lyricTargets):
return
prefix = '-' if syllableIndex else ''
suffix = '-' if syllableIndex < len(syllables) - 1 else ''
lyricTargets[lyricTargetIndex].addLyric(prefix + syllable + suffix)
lyricTargetIndex += 1


def parseABCNote(
t: abcFormat.ABCNote,
dst: stream.Measure|stream.Part,
) -> None:
) -> note.GeneralNote|None:
'''
Parse an ABCNote object and add it to the destination stream.
'''
Expand All @@ -307,7 +342,7 @@ def parseABCNote(
if cs_name in ('NC', 'N.C.', 'No Chord', 'None'):
cs = harmony.NoChord(cs_name)
elif cs_name.startswith('>'):
return # fingering diagram? Appears in some pieces, ryans-Neumedia
return None # fingering diagram? Appears in some pieces, ryans-Neumedia
else:
cs = harmony.ChordSymbol(cs_name)
dst.coreAppend(cs, setActiveSite=False)
Expand All @@ -319,7 +354,7 @@ def parseABCNote(
if isinstance(t, abcFormat.ABCChord):
# Skip an empty chord
if not t.subTokens:
return
return None

# may have more than notes?
pitchNameList: list[str] = []
Expand All @@ -345,6 +380,7 @@ def parseABCNote(
if acc is not None:
acc.displayStatus = accStatusList[pIndex]
dst.coreAppend(c)
return c

# ql += t.quarterLength
else:
Expand Down Expand Up @@ -388,6 +424,7 @@ def parseABCNote(
n.articulations.append(m21ArticulationObj)

dst.coreAppend(n, setActiveSite=False)
return n


def metadataToM21Object(
Expand Down Expand Up @@ -885,23 +922,47 @@ def testOpusImport(self):
self.assertEqual(len(op), 8)

def testLyrics(self):
# TODO(msc) -- test better

from music21 import abcFormat
from music21.abcFormat import testFiles
af = abcFormat.ABCFile()
s = abcToStreamScore(af.readstr('''
X:1
T:Lyrics
M:4/4
L:1/4
K:C
C D E F|G A B c|
w:hel-lo world * one two three four
'''))
assert s is not None

tf = testFiles.sicutRosa
notes = s.parts[0].recurse().notes
self.assertEqual(notes[0].lyric, 'hel')
self.assertEqual(notes[0].lyrics[0].syllabic, 'begin')
self.assertEqual(notes[1].lyric, 'lo')
self.assertEqual(notes[1].lyrics[0].syllabic, 'end')
self.assertEqual(notes[2].lyric, 'world')
self.assertEqual(notes[3].lyrics, [])
self.assertEqual([m21Note.lyric for m21Note in notes[4:]],
['one', 'two', 'three', 'four'])

def testLyricsIgnoreSyllablesAfterLastNote(self):
'''AI-assisted regression coverage for surplus lyric syllables.'''
from music21 import abcFormat
af = abcFormat.ABCFile()
s = abcToStreamScore(af.readstr(tf))
s = abcToStreamScore(af.readstr('''
X:1
T:Lyrics with extra words
M:4/4
L:1/4
K:C
C D|
w:first second third
'''))
assert s is not None

# s.show()
# self.assertEqual(len(s.parts), 3)
# self.assertEqual(len(s.parts[0].notesAndRests), 6)
# self.assertEqual(len(s.parts[1].notesAndRests), 20)
# self.assertEqual(len(s.parts[2].notesAndRests), 6)
# s.show()
# s.show('midi')
notes = s.parts[0].recurse().notes
self.assertEqual([m21Note.lyric for m21Note in notes],
['first', 'second'])

def testMultiWorkImported(self):

Expand Down
Loading