Skip to content

Commit cbdd142

Browse files
authored
Merge pull request #1769 from oxygen-dioxide/midilyrics
Support loading lyrics from midi
2 parents 41b6b6f + 032c51f commit cbdd142

6 files changed

Lines changed: 56 additions & 7 deletions

File tree

614 Bytes
Binary file not shown.
10.4 KB
Binary file not shown.
620 Bytes
Binary file not shown.
10.4 KB
Binary file not shown.

music21/midi/tests.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1536,6 +1536,27 @@ def testExportUnpitched(self):
15361536
self.assertEqual(len(note_ons), 3)
15371537
self.assertEqual([ev.pitch for ev in note_ons], [67, 60, 60])
15381538

1539+
def testMidiImportLyrics(self):
1540+
lyricFactZh = ['明', '山', '涌', '水', '郁', '郁', '葱', '', '葱',
1541+
'钟', '灵', '毓', '秀', '海', '天', '', '东',
1542+
'济', '济', '多', '士', '四', '方', '所', '', '崇',
1543+
'早', '', '育', '', '文', '明', '', '种']
1544+
lyricFactKo = ['빛', '날', '세', '라', '영', '웅', '열', '', '사',
1545+
'만', '세', '불', '망', '하', '실', '', '이',
1546+
'옛', '적', '이', '나', '지', '금', '이', '', '나',
1547+
'항', '상', '앙', '모', '합', '니', '', '다']
1548+
testCases = [
1549+
('test18.mid', 'utf-8', lyricFactZh),
1550+
('test19.mid', 'gbk', lyricFactZh),
1551+
('test20.mid', 'utf-8', lyricFactKo),
1552+
('test21.mid', 'euc-kr', lyricFactKo),
1553+
]
1554+
for (filename, encoding, lyricFact) in testCases:
1555+
fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / filename
1556+
s = converter.parse(fp, encoding_type=encoding)
1557+
for (n, l) in zip(s.flat.notes, lyricFact):
1558+
self.assertEqual(n.lyric, l)
1559+
15391560

15401561
# ------------------------------------------------------------------------------
15411562
if __name__ == '__main__':

music21/midi/translate.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1870,6 +1870,21 @@ def getNotesFromEvents(
18701870
# 'midiTrackToStream(): cannot find a note off for a note on', e])
18711871
return notes
18721872

1873+
def getLyricsFromEvents(
1874+
events: list[tuple[int, MidiEvent]],
1875+
encoding_type: str = 'utf-8',
1876+
) -> dict[int, str]:
1877+
lyrics: dict[int, str] = {}
1878+
for time, e in events:
1879+
if e.type == MetaEvents.LYRIC and isinstance(e.data, bytes):
1880+
try:
1881+
lyrics[time] = e.data.decode(encoding_type)
1882+
except UnicodeDecodeError:
1883+
warnings.warn(
1884+
f'Unable to decode lyrics from {e}',
1885+
TranslateWarning)
1886+
return lyrics
1887+
18731888

18741889
def getMetaEvents(
18751890
events: list[tuple[int, MidiEvent]]
@@ -1936,6 +1951,7 @@ def midiTrackToStream(
19361951
conductorPart: stream.Part|None = None,
19371952
isFirst: bool = False,
19381953
quarterLengthDivisors: Sequence[int] = (),
1954+
encoding_type: str = 'utf-8',
19391955
**keywords
19401956
) -> stream.Part:
19411957
# noinspection PyShadowingNames
@@ -2013,6 +2029,7 @@ def midiTrackToStream(
20132029
# need to build chords and notes
20142030
notes = getNotesFromEvents(events)
20152031
metaEvents = getMetaEvents(events)
2032+
lyricsDict = getLyricsFromEvents(events, encoding_type=encoding_type)
20162033

20172034
# first create meta events
20182035
for tick, obj in metaEvents:
@@ -2089,9 +2106,12 @@ def midiTrackToStream(
20892106
if chordSub:
20902107
# composite.append(chordSub)
20912108
c = midiEventsToChord(chordSub, ticksPerQuarter)
2092-
o = notes[i][0][0] / ticksPerQuarter
2093-
c.editorial.midiTickStart = notes[i][0][0]
2094-
2109+
tickStart = notes[i][0][0]
2110+
o = tickStart / ticksPerQuarter
2111+
c.editorial.midiTickStart = tickStart
2112+
lyric = lyricsDict.get(tickStart)
2113+
if (lyric is not None):
2114+
c.lyric = lyric
20952115
s.coreInsert(o, c)
20962116
# iSkip = len(chordSub) # amount of accumulated chords
20972117
chordSub = []
@@ -2100,8 +2120,12 @@ def midiTrackToStream(
21002120
n: note.NotRest = midiEventsToNote(notes[i], ticksPerQuarter)
21012121
# the time is the first value in the first pair
21022122
# need to round, as floating point error is likely
2103-
o = notes[i][0][0] / ticksPerQuarter
2104-
n.editorial.midiTickStart = notes[i][0][0]
2123+
tickStart = notes[i][0][0]
2124+
o = tickStart / ticksPerQuarter
2125+
n.editorial.midiTickStart = tickStart
2126+
lyric = lyricsDict.get(tickStart)
2127+
if (lyric is not None):
2128+
n.lyric = lyric
21052129

21062130
s.coreInsert(o, n)
21072131
# iSkip = 1
@@ -2112,8 +2136,12 @@ def midiTrackToStream(
21122136
singleN: note.NotRest = midiEventsToNote(notes[0], ticksPerQuarter)
21132137
# the time is the first value in the first pair
21142138
# need to round, as floating point error is likely
2115-
o = notes[0][0][0] / ticksPerQuarter
2116-
singleN.editorial.midiTickStart = notes[0][0][0]
2139+
tickStart = notes[i][0][0]
2140+
o = tickStart / ticksPerQuarter
2141+
singleN.editorial.midiTickStart = tickStart
2142+
lyric = lyricsDict.get(tickStart)
2143+
if (lyric is not None):
2144+
singleN.lyric = lyric
21172145
s.coreInsert(o, singleN)
21182146

21192147
s.sort(force=True) # will also run coreElementsChanged()

0 commit comments

Comments
 (0)