cuthbertLab / music21

music21 is a Toolkit for Computational Musicology
https://www.music21.org/
Other
2.12k stars 402 forks source link

Conform MIDI chord-vs.-voice boundary to the quantization units being used #809

Closed jacobtylerwalls closed 3 years ago

jacobtylerwalls commented 3 years ago

music21 version

6.7

Problem summary MIDI import is too quick to create voices and too reluctant to create chords because the "chord subbing" tolerance is different from the quantization units. The "chord subbing" is not configurable--a 64th note, regardless of the quantization units, meaning any onsets after a 64th note window jump to another voice instead of being collected onto a chord. Then when quantization runs later, it's too late--you've got voices when chords would have been appropriate, given the quantization scheme.

music21's MIDI parsing is very literal, which is great, but I think there are a couple of low-effort batteries-included improvements we could do in this area. Eventually I'll write up the other idea I have on quantization.

Steps to reproduce File on #684

s = converter.parse('example-from-684'.mid)
s.parts[4].voices[0].show('t')

Expected vs. actual behavior Before: {5.0} After this proposal : {5.0} <music21.chord.Chord E4 C#4>

Suggested fix

index b2c8606da..e4eab20d4 100644
--- a/music21/midi/translate.py
+++ b/music21/midi/translate.py
@@ -1755,6 +1755,12 @@ def midiTrackToStream(
     i = 0
     iGathered = []  # store a list of indexes of gathered values put into chords
     voicesRequired = False
+
+    if 'quarterLengthDivisors' in keywords:
+        quarterLengthDivisors = keywords['quarterLengthDivisors']
+    else:
+        quarterLengthDivisors = defaults.quantizationQuarterLengthDivisors
+
     if len(notes) > 1:
         # environLocal.printDebug(['\n', 'midiTrackToStream(): notes', notes])
         while i < len(notes):
@@ -1779,10 +1785,15 @@ def midiTrackToStream(
                 tSub, unused_eSub = onSub
                 tOffSub, unused_eOffSub = offSub

-                # can set a tolerance for chordSubbing; here at 1/16th
-                # of a quarter
-                chunkTolerance = ticksPerQuarter / 16
-                if abs(tSub - t) <= chunkTolerance:
+                # let tolerance for chord subbing follow the quantization
+                if quantizePost:
+                    divisor = max(quarterLengthDivisors)
+                # fallback: 1/16 of a quarter (64th)
+                else:
+                    divisor = 16
+                chunkTolerance = ticksPerQuarter / divisor
+                # must be strictly less than the quantization unit
+                if abs(tSub - t) < chunkTolerance:
                     # isolate case where end time is not w/n tolerance
                     if abs(tOffSub - tOff) > chunkTolerance:
                         # need to store this as requiring movement to a diff
@@ -1839,10 +1850,6 @@ def midiTrackToStream(
     s.coreElementsChanged()
     # quantize to nearest 16th
     if quantizePost:
-        if 'quarterLengthDivisors' in keywords:
-            quarterLengthDivisors = keywords['quarterLengthDivisors']
-        else:
-            quarterLengthDivisors = None
         s.quantize(quarterLengthDivisors=quarterLengthDivisors,
                    processOffsets=True,
                    processDurations=True,
mscuthbert commented 3 years ago

Just started version 7 -- now's the chance to really change things around on MIDI and Quantization.