-
Notifications
You must be signed in to change notification settings - Fork 210
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
Fix BPM <-> MIDI tempo conversions #114
Conversation
92290e1
to
9a18de8
Compare
9adf766
to
a4678d1
Compare
mido/midifiles/units.py
Outdated
@@ -17,7 17,7 @@ def second2tick(second, ticks_per_beat, tempo): | |||
note) and tempo (microseconds per beat). | |||
""" | |||
scale = tempo * 1e-6 / ticks_per_beat | |||
return second / scale | |||
return int(round(second / scale)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change will break a program of mine where I let the user decrease the PPQ of MIDI files (for rendering them as piano rolls of reasonable dimensions).
ticks = int(round(mido.second2tick(message.time, ppq, tempo)))
assert ticks > 0 or message.time == 0, "New PPQ is too low."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to see which part of the assert fails (I am not sure if this makes any difference, though), then it's easier to discuss this further.
The rationale behind this change is that "the MIDI standard" requires QQN PPQ to be an integer. And until now this function could return a float value.
I am not sure why ticks should be greater 0 or message timings should not be 0. Both are legitimate values as far as I can see. What's the intention behind these checks?
The problem seems to be that different Python versions handle the /-operator differently. Anyways, I see that this change breaks code (although I never thought it would), but IMHO the question remains if your assertion assumes the "right thing".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rationale behind this change is that "the MIDI standard" requires QQN to be an integer. And until now this function could return a float value.
How should we handle the case when second will be truncated to zero for the given PPQ and tempo (or heavily shifted in time, ceil
doesn't really help)?
What's the intention behind these checks?
The assertion checks that there will be at least one tick with the given ppq
when message.time
is larger than 0.0 seconds.
When PPQ is chosen too low there could be messages that don't move the tick count at all even though they should, which causes a slight global drift AFAIK.
The problem stems from MAPS having ridiculously high PPQ (craffel/pretty-midi#112), while just setting it to 960 is fine because no message actually seems to require that much time resolution to fit into a piano roll (although the messages might shift just slightly forward and/or backwards in time, which I don't bother to check).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw, what does QQN stand for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo, PPQ = parts (ticks) per quarter note.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
High PPQ (e.g. in the MAPS dataset) should be fine, since they result in large tick values. On the other hand, having low PPQ values is a (known) problem, but I am still not sure how this is affected by the proposed change.
The assertion checks that there will be at least one tick with the given ppq when message.time is larger than 0.0 seconds.
Ok, but I don't see how moving int(round(...))
inside the function should alter the result since you apply exactly the same rounding and casting to int by yourself. Are you sure that this change is the culprit and not something else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, but I don't see how moving the int(round(...)) thing inside the function should alter the result since you apply exactly the same rounding and casting to int by yourself.
Haha, you're absolutely right... Sorry. 😆
Could we have something like this internally maybe?
if second != 0 and tick == 0:
warnings.warn(f"{second} seconds is zero ticks at {ppq} PPQ.")
a4678d1
to
4008d43
Compare
Status? |
58b87fd
to
83caf8d
Compare
83caf8d
to
c086918
Compare
Depending on the chosen time signature a bar contains a different number of | ||
beats. These beats are multiples/fractions of a quarter note, thus the | ||
returned tempo depends on the time signature. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is good to mention here that what constitutes a beat is defined by the denominator of the time signature, and give an example: in 2/4, 3/4, 4/4, ... the beat is a quarter note. In 3/8, 6/8, ... the beat is an eight note, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can confirm PR fixes the bpm2tempo, tempo2bpm, tick2second, and second2tick functions by factoring in the time signature. Previously the functions implicitly assumed the beat unit equals a quarter note.
Although I am not a collaborator on this project, I hope this review can contribute to the merging of the PR.
assert tempo2bpm(1000000) == 60 | ||
# x/4 time signature: 4 beats (quarter notes) per bar | ||
# 60 bpm: 1 beat / sec = 1 quarter note / sec: 1 sec / quarter note | ||
assert bpm2tempo(1000000, time_signature=(4, 4)) == 60 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this one and the following be tempo2bpm
not bpm2tempo
!
assert second2tick(0.005, ticks_per_beat=100, tempo=500000) == 1 | ||
assert second2tick(0.002, ticks_per_beat=100, tempo=100000) == 2 | ||
# TODO: Python 2 and 3 rounds differently, find a solution? | ||
# The result produced by Python 3 seems the way to go |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are dropping Python 2 support for the next release. You can safely remove this TODO.
I have added the fix I was suggesting in #454 and merged your changes. Thank you for your contribution! |
Thanks a lot! |
This PR addresses #102 and contains the fixes needed to correctly consider the time signature when converting between BPM (beats per minute) and MIDI tempo (microseconds per quarter note).
Before being able to be merged, these points should be discussed/addressed:
ticks_per_beat
is a bit misleading since it rather should beticks_per_quarter_note
. Renaming the variable, however, may break existing code, so this is a rather bad option. Maybe adding a new variable (and deprecate the old one, but keep it around for backwards compatibility) is a better approach.midi_files.rst
.