Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minimize gaps produced by quantization algorithm #1540

Merged

Conversation

jacobtylerwalls
Copy link
Member

@jacobtylerwalls jacobtylerwalls commented Mar 25, 2023

Fixes #1536 by improving the algorithm that "looks ahead" to minimize gaps when quantizing durations from its first draft in #1062, as long as the stream is sorted first.

Looking ahead was the right idea, but using the same divisor was not right, because in a passage of triplets, on beats, you have a tie between divisors (for calculating offsets). For the screenshotted example below, that means the last triplet-eighths of a beat were quantized as sixteenths (because there was a tie for the "best divisor" for the quantization of the offset at the X.0 beat).

This refactor solves the reported issue and also includes a simpler way of achieving #641's requirement of nonzero durations on non-grace notes.

  • This also uncovered some infelicities with Stream.insert(ignoreSort=True), adding an explicit test.
  • Now streams parsed from MIDI are explicitly sorted.

Before
Screenshot 2023-03-25 at 5 53 43 PM

After
Screenshot 2023-03-25 at 5 53 19 PM

@jacobtylerwalls jacobtylerwalls marked this pull request as ready for review March 25, 2023 22:47
@coveralls
Copy link

coveralls commented Mar 25, 2023

Coverage Status

Coverage: 93.101% (-4.0e-05%) from 93.101% when pulling df97c67 on jacobtylerwalls:quantization-improvements into 1573e37 on cuthbertLab:master.

@@ -86,7 +86,7 @@

BestQuantizationMatch = namedtuple(
'BestQuantizationMatch',
['error', 'tick', 'match', 'signedError', 'divisor']
['remainingGap', 'error', 'tick', 'match', 'signedError', 'divisor']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, but generally add new attributes to a namedtuple at the end so anyone using [0] still gets the same result.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Unfortunately we could no longer use it for sorting (unless there's something clever I'm not thinking of, like layering a sort function on top, but that's what I took the namedtuple to function as.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally I've used namedtuples because before it was a standard (anonymous) tuple, so people might still be using access by index or destructuring paradigms.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, yes that's fine. You could sort with key=lambda bqm: (bqm[-1], *bqm) but yeah, too much trouble for backwards compatibility on an internal object.

I did though change it to use min -- there's no need to sort the whole thing if you're just getting the minimum

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah! That was old code back when the m21 team didn't know its own algorithmic complexity

Comment on lines 4741 to 4746
def testInsertIgnoreSort(self):
'''A sorted stream does not become unsorted when ignoreSort=True.'''
s = Stream()
s.repeatAppend(note.Note(), 4)
s.insert(1, tempo.MetronomeMark(), ignoreSort=True)
self.assertTrue(s.isSorted)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems strange to me -- can you explain this? The Stream is no longer sorted -- why is it reporting True for isSorted? I don't think I like this. isSorted should be something that should be able to be trusted unless private methods are being used.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I think I just botched the setup of the test case. Inserting something at the end would make a more clear test case, I'll fix it.

What motivated this: the docs for insert say:

If ignoreSort is True then the inserting does not
change whether the Stream is sorted or not (much faster if you're
going to be inserting dozens
of items that don't change the sort status)

But this test fails on master, which is to say, providing ignoreSort=True does change the sort status, even if the stream stays sorted (as I'll update the test to actually demonstrate!) This is because insert() was unconditionally calling coreElementsChanged with clearIsSorted=True.

isSorted should be something that should be able to be trusted unless private methods are being used.

Yeah, maybe ignoreSort needs to become exposed on coreInsert only?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How I ended up here was:

  1. I noticed that MIDI files have a tendency to produce simultaneities that don't collapse into chords, because of differing durations. So iterating elements and checking the element at i + 1 might still give you the same offset; I need the next distinct offset. I didn't want to have to look backwards in the elements array, so I realized I wanted sorted streams.
  2. So I made MIDI parsing produce a stream with isSorted reported as True.
  3. Then I went to add a test case to testQuantize, which builds streams with insert(). I then had to debug why the stream was reporting itself as unsorted. I chose between using ignoreSort=True versus doing an explicit sort.
  4. ignoreSort=True wasn't preserving the value of isSorted (I agree that feels like a should-be-core thing, though)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, maybe ignoreSort needs to become exposed on coreInsert only?

yeah, I think that's a much better place. And note that coreInsert still computes and returns whether the stream is guaranteed to be sorted.

But we shouldn't expose anything to the users where they could end up with a stream that needs manual sorting unless they set isSorted = False. Feel free to remove. Or just ignore the test and remove calls from this PR and I'll remove ignoreSort in insert later.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted changes and just went with an explicit sort 👍 . I think I misdiagnosed exactly what was unsorted in the first place anyway.

@mscuthbert
Copy link
Member

Ah -- I never did a re-review. Is this set for reviewing again?

@jacobtylerwalls
Copy link
Member Author

Yep!

Copy link
Member

@mscuthbert mscuthbert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look good

Comment on lines +4030 to +4031
# Must be sorted for quantizing to work optimally.
s.sort()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tiny bit confused by this addition -- Streams are autosorted by default. This seems to be a problem of quantize if it somehow bypasses the normal "sort before iterate/access" paradigm.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might have been an artifact from an earlier commit. Does look unnecessary now. 😅

@mscuthbert mscuthbert merged commit 696545a into cuthbertLab:master May 1, 2023
5 of 7 checks passed
@jacobtylerwalls jacobtylerwalls deleted the quantization-improvements branch May 1, 2023 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Trailing rests created that don't agree with the quantization scheme
3 participants