Skip to content

Commit 52fc784

Browse files
authored
Merge pull request #1778 from cuthbertLab/midi_translate_type_simplify
midi.translate improvements Followup from #1769 gave me an opportunity to find places to improve midi translate. Pinging @oxygen-dioxide since some changes took place since then * Change `getNotesFromEvents()` from potentially O(n^2) to O(n) while retaining sort order. * Pass encoding to parsing of instrument names in addition to lyrics (renamed "encoding_type" to "encoding" -- missed in prior review) * create TimingNoteEvent which is a typed NamedTuple that keeps track of when a MidiEvent NoteOn started, stopped, and what the NoteOn event was. Replaced the old tuple of tuples. * Faster gathering of Chords (remove potential O(n^2) search). * Remove unused special case for single-note streams. * opFrac many offsets and durations in MIDI for absolutely correct tuplets, etc. * Misc: Update braille.translate SegmentKey to typing.NamedTuple * Misc: Search code for places where we were still casting to float for Python 2 division! * Misc: Add typing to text/Trigram and some layout things This is part of the non-backwards compatible MIDI.translate guts rewrite discussed on the music21 list. Coverage % is expected to drop because the amount of code has been reduced a lot.
2 parents cbdd142 + 15df42c commit 52fc784

11 files changed

Lines changed: 334 additions & 270 deletions

File tree

music21/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
'''
5151
from __future__ import annotations
5252

53-
__version__ = '9.6.0b4'
53+
__version__ = '9.6.0b5'
5454

5555
def get_version_tuple(vv):
5656
v = vv.split('.')

music21/alpha/analysis/aligner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,7 @@ def calculateChangesList(self):
862862
raise AlignmentTracebackException('Traceback of best alignment did not end properly')
863863

864864
self.changesCount = Counter(elem[2] for elem in self.changes)
865-
self.similarityScore = float(self.changesCount[ChangeOps.NoChange]) / len(self.changes)
865+
self.similarityScore = self.changesCount[ChangeOps.NoChange] / len(self.changes)
866866

867867
def showChanges(self, show=False):
868868
'''

music21/analysis/discrete.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ def _getLikelyKeys(self, keyResults, differences) -> list[t.Any]|None:
407407
# environLocal.printDebug(['added likely key', likelyKeys[pc]])
408408
return likelyKeys
409409

410-
def _getDifference(self, keyResults, pcDistribution, weightType) -> None|list[int|float]:
410+
def _getDifference(self, keyResults, pcDistribution, weightType) -> None|list[float]:
411411
'''
412412
Takes in a list of numerical probable key results and returns the
413413
difference of the top two keys.
@@ -416,14 +416,14 @@ def _getDifference(self, keyResults, pcDistribution, weightType) -> None|list[in
416416
if keyResults is None:
417417
return None
418418

419-
solution: list[int|float] = [0.0] * 12
419+
solution: list[float] = [0.0] * 12
420420
top = [0.0] * 12
421421
bottomRight = [0.0] * 12
422422
bottomLeft = [0.0] * 12
423423

424424
toneWeights = self.getWeights(weightType)
425-
profileAverage = float(sum(toneWeights)) / len(toneWeights)
426-
histogramAverage = float(sum(pcDistribution)) / len(pcDistribution)
425+
profileAverage = sum(toneWeights) / len(toneWeights)
426+
histogramAverage = sum(pcDistribution) / len(pcDistribution)
427427

428428
for i in range(len(solution)):
429429
for j in range(len(toneWeights)):
@@ -437,9 +437,9 @@ def _getDifference(self, keyResults, pcDistribution, weightType) -> None|list[in
437437
pcDistribution[j] - histogramAverage) ** 2)
438438

439439
if bottomRight[i] == 0 or bottomLeft[i] == 0:
440-
solution[i] = 0
440+
solution[i] = 0.0
441441
else:
442-
solution[i] = float(top[i]) / ((bottomRight[i] * bottomLeft[i]) ** 0.5)
442+
solution[i] = float(top[i] / ((bottomRight[i] * bottomLeft[i]) ** 0.5))
443443
return solution
444444

445445
def solutionLegend(self, compress=False):

music21/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
<class 'music21.base.Music21Object'>
2828
2929
>>> music21.VERSION_STR
30-
'9.6.0b4'
30+
'9.6.0b5'
3131
3232
Alternatively, after doing a complete import, these classes are available
3333
under the module "base":

music21/braille/segment.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,11 @@ def setGroupingGlobals():
147147

148148
_ThreeDigitNumber = namedtuple('_ThreeDigitNumber', ['hundreds', 'tens', 'ones'])
149149

150-
SegmentKey = namedtuple('SegmentKey', ['measure', 'ordinal', 'affinity', 'hand'])
151-
SegmentKey.__new__.__defaults__ = (0, 0, None, None)
150+
class SegmentKey(t.NamedTuple):
151+
measure: int = 0
152+
ordinal: int = 0
153+
affinity: Affinity|None = None
154+
hand: str|None = None
152155

153156

154157
# ------------------------------------------------------------------------------

music21/converter/subConverters.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class SubConverter:
7979

8080
def __init__(self, **keywords) -> None:
8181
self._stream: stream.Score|stream.Part|stream.Opus = stream.Score()
82+
# TODO: unify keywords so that they are used by both parseFile and parseData
8283
self.keywords: dict[str, t.Any] = keywords
8384

8485
def parseData(self, dataString, number: int|None = None):
@@ -1023,7 +1024,11 @@ class ConverterMidi(SubConverter):
10231024
registerInputExtensions = ('mid', 'midi')
10241025
registerOutputExtensions = ('mid',)
10251026

1026-
def parseData(self, strData, number=None):
1027+
def __init__(self, *, encoding='utf-8', **keywords):
1028+
self.encoding = encoding
1029+
super().__init__(**keywords)
1030+
1031+
def parseData(self, strData, number=None, *, encoding: str = ''):
10271032
'''
10281033
Get MIDI data from a binary string representation.
10291034
@@ -1033,13 +1038,22 @@ def parseData(self, strData, number=None):
10331038
`quantizePost` controls whether to quantize the output. (Default: True)
10341039
`quarterLengthDivisors` allows for overriding the default quantization units
10351040
in defaults.quantizationQuarterLengthDivisors. (Default: (4, 3)).
1041+
1042+
If encoding is not provided use the encoding of the Converter
1043+
(default "utf-8")
10361044
'''
10371045
from music21.midi import translate as midiTranslate
1038-
self.stream = midiTranslate.midiStringToStream(strData, **self.keywords)
1046+
self.stream = midiTranslate.midiStringToStream(
1047+
strData,
1048+
encoding=encoding or self.encoding,
1049+
**self.keywords
1050+
)
10391051

10401052
def parseFile(self,
10411053
filePath: pathlib.Path|str,
10421054
number: int|None = None,
1055+
*,
1056+
encoding: str = '',
10431057
**keywords):
10441058
'''
10451059
Get MIDI data from a file path.
@@ -1050,9 +1064,17 @@ def parseFile(self,
10501064
`quantizePost` controls whether to quantize the output. (Default: True)
10511065
`quarterLengthDivisors` allows for overriding the default quantization units
10521066
in defaults.quantizationQuarterLengthDivisors. (Default: (4, 3)).
1067+
1068+
If encoding is not provided use the encoding of the Converter
1069+
(default "utf-8")
10531070
'''
10541071
from music21.midi import translate as midiTranslate
1055-
midiTranslate.midiFilePathToStream(filePath, inputM21=self.stream, **keywords)
1072+
midiTranslate.midiFilePathToStream(
1073+
filePath,
1074+
inputM21=self.stream,
1075+
encoding=encoding or self.encoding,
1076+
**keywords
1077+
)
10561078

10571079
def write(self,
10581080
obj,

music21/layout.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -176,26 +176,29 @@ def __init__(self,
176176
if staffLayoutList is not None:
177177
self.staffLayoutList = staffLayoutList
178178

179-
def tenthsToMillimeters(self, tenths):
179+
def tenthsToMillimeters(self, tenths: int|float) -> int|float:
180180
'''
181181
given the scalingMillimeters and scalingTenths,
182182
return the value in millimeters of a number of
183183
musicxml "tenths" where a tenth is a tenth of the distance
184-
from one staff line to another
184+
from one staff line to another.
185185
186186
returns 0.0 if either of scalingMillimeters or scalingTenths
187187
is undefined.
188188
189-
190189
>>> sl = layout.ScoreLayout(scalingMillimeters=2.0, scalingTenths=10)
191-
>>> print(sl.tenthsToMillimeters(10))
190+
>>> sl.tenthsToMillimeters(10)
192191
2.0
193-
>>> print(sl.tenthsToMillimeters(17)) # printing to round
192+
193+
Numbers are rounded to a maximum of 6 digits (but because they are floats,
194+
there may be inaccuracies.
195+
196+
>>> sl.tenthsToMillimeters(17)
194197
3.4
195198
'''
196199
if self.scalingMillimeters is None or self.scalingTenths is None:
197200
return 0.0
198-
millimetersPerTenth = float(self.scalingMillimeters) / self.scalingTenths
201+
millimetersPerTenth = self.scalingMillimeters / self.scalingTenths
199202
return round(millimetersPerTenth * tenths, 6)
200203

201204

music21/midi/base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,6 +1283,9 @@ def matchedNoteOff(self, other: MidiEvent) -> bool:
12831283
>>> me2.channel = 12
12841284
>>> me1.matchedNoteOff(me2)
12851285
False
1286+
1287+
Note that this method is no longer used in MIDI Parsing
1288+
because it is inefficient.
12861289
'''
12871290
if self.isNoteOn() and other.isNoteOff():
12881291
if self.pitch == other.pitch and self.channel == other.channel:

music21/midi/tests.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,20 @@
2323
MidiFile,
2424
)
2525
from music21.midi.translate import (
26+
TimedNoteEvent,
27+
TranslateWarning,
28+
channelInstrumentData,
29+
conductorStream,
30+
getMetaEvents,
2631
midiAsciiStringToBinaryString,
27-
noteToMidiEvents,
32+
midiEventsToInstrument,
2833
midiEventsToNote,
29-
streamHierarchyToMidiTracks, midiFileToStream, conductorStream,
30-
streamToMidiFile, prepareStreamForMidi, midiEventsToInstrument, getMetaEvents,
31-
TranslateWarning, channelInstrumentData, packetStorageFromSubstreamList,
34+
midiFileToStream,
35+
noteToMidiEvents,
36+
packetStorageFromSubstreamList,
37+
prepareStreamForMidi,
38+
streamHierarchyToMidiTracks,
39+
streamToMidiFile,
3240
updatePacketStorageWithChannelInfo,
3341
)
3442
from music21.musicxml import testPrimitive
@@ -323,7 +331,7 @@ def testImportWithRunningStatus(self):
323331
# dealing with midi files that use running status compression
324332
s = converter.parse(fp)
325333
self.assertEqual(len(s.parts), 2)
326-
self.assertEqual(len(s.parts[0].recurse().notes), 704)
334+
self.assertEqual(len(s.parts[0].recurse().notes), 702)
327335
self.assertEqual(len(s.parts[1].recurse().notes), 856)
328336

329337
# for n in s.parts[0].notes:
@@ -403,8 +411,7 @@ def testNote(self):
403411
self.assertIsInstance(eventList[3], MidiEvent)
404412

405413
# translate eventList back to a note
406-
n2 = midiEventsToNote(((eventList[0].time, eventList[1]),
407-
(eventList[2].time, eventList[2])))
414+
n2 = midiEventsToNote(TimedNoteEvent(eventList[0].time, eventList[2].time, eventList[1]))
408415
self.assertEqual(n2.pitch.nameWithOctave, 'A4')
409416
self.assertEqual(n2.quarterLength, 2.0)
410417

@@ -1538,23 +1545,24 @@ def testExportUnpitched(self):
15381545

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

15601568

0 commit comments

Comments
 (0)