8686
8787BestQuantizationMatch = namedtuple(
8888 'BestQuantizationMatch',
89- ['error', 'tick', 'match', 'signedError', 'divisor']
89+ ['remainingGap', ' error', 'tick', 'match', 'signedError', 'divisor']
9090)
9191
9292class StreamDeprecationWarning(UserWarning):
@@ -9395,18 +9395,43 @@ def quantize(
93959395 # this presently is not trying to avoid overlaps that
93969396 # result from quantization; this may be necessary
93979397
9398- def bestMatch(target, divisors):
9399- found = []
9398+ def bestMatch(
9399+ target,
9400+ divisors,
9401+ zeroAllowed=True,
9402+ gapToFill=0.0
9403+ ) -> BestQuantizationMatch:
9404+ found: list[BestQuantizationMatch] = []
94009405 for div in divisors:
9401- match, error, signedErrorInner = common.nearestMultiple(target, (1 / div))
9402- # Sort by unsigned error, then "tick" (divisor expressed as QL, e.g. 0.25)
9403- found.append(BestQuantizationMatch(error, 1 / div, match, signedErrorInner, div))
9404- # get first, and leave out the error
9405- bestMatchTuple = sorted(found)[0]
9406+ tick = 1 / div # divisor expressed as QL, e.g. 0.25
9407+ match, error, signedErrorInner = common.nearestMultiple(target, tick)
9408+ if not zeroAllowed and match == 0.0:
9409+ match = tick
9410+ signedErrorInner = round(target - match, 7)
9411+ error = abs(signedErrorInner)
9412+ if gapToFill % tick == 0:
9413+ remainingGap = 0.0
9414+ else:
9415+ remainingGap = max(gapToFill - match, 0.0)
9416+ # Sort by remainingGap, then unsigned error, then tick
9417+ found.append(
9418+ BestQuantizationMatch(
9419+ remainingGap, error, tick, match, signedErrorInner, div))
9420+ # get smallest remainingGap, error, tick
9421+ bestMatchTuple = min(found)
94069422 return bestMatchTuple
94079423
9408- # if we have a min of 0.25 (sixteenth)
9409- # quarterLengthMin = quarterLengthDivisors[0]
9424+ def findNextElementNotCoincident(
9425+ useStream: Stream,
9426+ startIndex: int,
9427+ startOffset: OffsetQL,
9428+ ) -> tuple[base.Music21Object | None, BestQuantizationMatch | None]:
9429+ for next_el in useStream._elements[startIndex:]:
9430+ next_offset = useStream.elementOffset(next_el)
9431+ look_ahead_result = bestMatch(float(next_offset), quarterLengthDivisors)
9432+ if look_ahead_result.match > startOffset:
9433+ return next_el, look_ahead_result
9434+ return None, None
94109435
94119436 if inPlace is False:
94129437 returnStream = self.coreCopyAsDerivation('quantize')
@@ -9419,6 +9444,11 @@ def bestMatch(target, divisors):
94199444
94209445 rests_lacking_durations: list[note.Rest] = []
94219446 for useStream in useStreams:
9447+ # coreSetElementOffset() will immediately set isSorted = False,
9448+ # but we need to know if the stream was originally sorted to know
9449+ # if it's worth "looking ahead" to the next offset. If a stream
9450+ # is unsorted originally, this "looking ahead" could become O(n^2).
9451+ originallySorted = useStream.isSorted
94229452 for i, e in enumerate(useStream._elements):
94239453 if processOffsets:
94249454 o = useStream.elementOffset(e)
@@ -9427,35 +9457,27 @@ def bestMatch(target, divisors):
94279457 sign = -1
94289458 o = -1 * o
94299459 o_matchTuple = bestMatch(float(o), quarterLengthDivisors)
9430- useStream.coreSetElementOffset(e, o_matchTuple.match * sign)
9460+ o = o_matchTuple.match * sign
9461+ useStream.coreSetElementOffset(e, o)
94319462 if hasattr(e, 'editorial') and o_matchTuple.signedError != 0:
94329463 e.editorial.offsetQuantizationError = o_matchTuple.signedError * sign
94339464 if processDurations:
94349465 ql = e.duration.quarterLength
94359466 ql = max(ql, 0) # negative ql possible in buggy MIDI files?
9436- d_matchTuple = bestMatch(float(ql), quarterLengthDivisors)
9437- # Check that any gaps from this quantized duration to the next onset
9438- # are at least as large as the smallest quantization unit (the largest divisor)
9439- # If not, then re-quantize this duration with the divisor
9440- # that will be used to quantize the next element's offset
9441- if processOffsets and i + 1 < len(useStream._elements):
9442- next_element = useStream._elements[i + 1]
9443- next_offset = useStream.elementOffset(next_element)
9444- look_ahead_result = bestMatch(float(next_offset), quarterLengthDivisors)
9445- next_offset = look_ahead_result.match
9446- next_divisor = look_ahead_result.divisor
9447- if (0 < next_offset - (e.offset + d_matchTuple.match)
9448- < 1 / max(quarterLengthDivisors)):
9449- # Overwrite the earlier matchTuple with a better result
9450- d_matchTuple = bestMatch(float(ql), (next_divisor,))
9451- # Enforce nonzero duration for non-grace notes
9452- if (d_matchTuple.match == 0
9453- and isinstance(e, note.NotRest)
9454- and not e.duration.isGrace):
9455- e.quarterLength = 1 / max(quarterLengthDivisors)
9456- if hasattr(e, 'editorial'):
9457- e.editorial.quarterLengthQuantizationError = ql - e.quarterLength
9458- elif d_matchTuple.match == 0 and isinstance(e, note.Rest):
9467+ zeroAllowed = not isinstance(e, note.NotRest) or e.duration.isGrace
9468+ if processOffsets and originallySorted:
9469+ next_element, look_ahead_result = (
9470+ findNextElementNotCoincident(
9471+ useStream=useStream, startIndex=i + 1, startOffset=o))
9472+ if next_element is not None and look_ahead_result is not None:
9473+ gapToFill = opFrac(look_ahead_result.match - e.offset)
9474+ d_matchTuple = bestMatch(
9475+ float(ql), quarterLengthDivisors, zeroAllowed, gapToFill)
9476+ else:
9477+ d_matchTuple = bestMatch(float(ql), quarterLengthDivisors, zeroAllowed)
9478+ else:
9479+ d_matchTuple = bestMatch(float(ql), quarterLengthDivisors, zeroAllowed)
9480+ if d_matchTuple.match == 0 and isinstance(e, note.Rest):
94599481 rests_lacking_durations.append(e)
94609482 else:
94619483 e.duration.quarterLength = d_matchTuple.match
0 commit comments