Closed leleogere closed 3 months ago
It's a reasonable feature. But the definition of the function in pretty_midi is not very clear. For example, if the time interval corresponding to the beginning and end of a note contains a time in original_times
, what should we do with the end time of the note?
And it seems that it could also be a member function for both Track and all kinds of list of time events like NoteList
It's a reasonable feature. But the definition of the function in pretty_midi is not very clear. For example, if the time interval corresponding to the beginning and end of a note contains a time in
original_times
, what should we do with the end time of the note?
I would say that the first part of the note get stretched/shrunk by interpolating in the first interval, while the second part is stretched/shrunk by interpolating in the second interval. I did the following example (I might have made a mistake, the problem is simple, but it is very easy to get confused!)
The bottom line is the new timing, based on the original timing. The red lines correspond to events that are specifically indicated in original_times
and new_times
, while the gray ones are other times that are simply interpolated between the red lines. So the original note duration was 4 seconds, and the new duration is 7.5 seconds.
Here is another example:
And it seems that it could also be a member function for both Track and all kinds of list of time events like NoteList
Yes I agree that it makes sense for all kind list of time events.
Ok, I understand. I'll try to implement it.
@leleogere I have added this function in the synth branch (it will be released in 0.4.0 with the new Synthesizer) You could try it by:
git clone -b synth https://github.com/Yikai-Liao/symusic.git --recursive
pip install ./symusic
Note that you need a c++ compiler supporting c++20 to build symusic from source, like g++11.
from symusic import Score
s = Score("path to midi")
end = s.end()
# All the events are sorted by time when loading
# So you could pass is_sorted=True here.
# adjust_time is not an inplace operation, calling it will create a new object
s2 = s.adjust_time([0, end], [0, end // 2], is_sorted=True)
# Track and List of events all get this member function
notes = s.tracks[0].notes
notes2 = notes.adjust_time([0, end], [0, end // 2], is_sorted=True)
If things work as what you expect, I'll release it.
Thank you!
I haven't been able to build symusic for now, I will try to find out why by end the of the week.
Could you offer me your OS and compiler versions? The new version havn't passed the compiling tests for all the platforms on github actions. We are fixing it.
$ cat /etc/os-release
NAME="Pop!_OS"
VERSION="22.04 LTS"
ID=pop
ID_LIKE="ubuntu debian"
PRETTY_NAME="Pop!_OS 22.04 LTS"
VERSION_ID="22.04"
HOME_URL="https://pop.system76.com"
SUPPORT_URL="https://support.system76.com"
BUG_REPORT_URL="https://github.com/pop-os/pop/issues"
PRIVACY_POLICY_URL="https://system76.com/privacy"
VERSION_CODENAME=jammy
UBUNTU_CODENAME=jammy
LOGO=distributor-logo-pop-os
$ hostnamectl
Static hostname: pop-os
Icon name: computer-laptop
Chassis: laptop
Machine ID: b82d41eb6ed641d34eabfdf363c14f6c
Boot ID: 32b71e27a6e542da99e80e8d50810d52
Operating System: Pop!_OS 22.04 LTS
Kernel: Linux 6.6.10-76060610-generic
Architecture: x86-64
Hardware Vendor: Dell Inc.
Hardware Model: Latitude 5510
$ uname -r
6.6.10-76060610-generic
$ g++ --version
g++ (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
@leleogere Now the compilation passed on Linux. You could re-clone the resopitory (or git pull
and git submodule update --recursive
) and install it.
Perfect, I managed to build it!
I tried a bit to play with it, and it works well on your example. However, I might have found a bug while doing some to try to compare the output to PrettyMIDI:
path = "1_funk_80_beat_4-4.mid"
import symusic
s = symusic.Score(path)
print("s.end() in ticks", s.end())
s = s.to(symusic.TimeUnit.second) # I was trying to convert it to seconds to compare with PrettyMIDI (as they use seconds)
print("s.end() in seconds", s.end())
end = s.end()
old_times = [0, 10, 30, end]
new_times = [0, 20, 25, end // 2]
s2 = s.adjust_time(old_times, new_times, is_sorted=True)
print("s2.end() in seconds", s2.end())
s2 = s2.to(symusic.TimeUnit.tick)
print("s2.end() in ticks", s2.end())
s2.dump_midi("/tmp/test_symusic.mid")
# Reread score
s3 = symusic.Score("/tmp/test_symusic.mid")
print("s3.end() in ticks", s3.end())
When I run this file multiple times, the printed output is always the same (like the end printed are always the same). However, when opening the produced MIDI file, it sometimes output a 43 second-long file, and sometimes a 116 hour-long file :
When exploring the timing of the events, here is what I got:
It seems that sometimes, some very far events are added to the file.
It is very weird as it happens when running the exact same code, seemingly random.
You can find here the initial file, the "correct" version, and the "corrupted" version: https://we.tl/t-FUpyFstrXE
Quite strange. In MuseScore, both of them get 43 seconds.
@leleogere The bug is fixed now.
I wrote an inequality incorrectly, resulting in two control changes outside of end not being filtered out, which in turn resulted in an out-of-bounds.
While this bug has been fixed, it also hints at another interface semantics issue, which is whether we should consider all events when calculating end or start, or just note as before. @Natooz What's your opinion?
👋
I think it makes sens to also adapt the times (.time
, .duration
) of all the events within the provided ranges.
Btw I can add tests for this feature when it's merged in main
The new version is almost ready. There is still one thing that, should we add an inplace
argument in the adjust_time
methods?
Although not implemented with any inplace operations (no performance benefits to add this argument), this might make the interface more consistent with sort
and filter
(they both get inplace
argument`)
If it's a low effort addition, I see no reason to not add it for consistency reasons :)
Also @leleogere thank you for the figures above, they are very well made and self-explanatory. The docs could greatly benefit to have them included.
@Natooz I have merged the branch into main. Shall I release 0.4.0 directly or do you want to test adjust_time further?
It's best if I add the tests before releasing anything :) I'll do it when I'll have some time in the day (in a few hours)
It seems to be working as it should!
While this bug has been fixed, it also hints at another interface semantics issue, which is whether we should consider all events when calculating end or start, or just note as before. @Natooz What's your opinion?
I agree that considering all events makes more sense.
The new version is almost ready. There is still one thing that, should we add an
inplace
argument in theadjust_time
methods?
The inplace
argument makes sense as it is already in other functions. More generally, a consistent inplace
argument in all methods that modify a score is nice.
Also @leleogere thank you for the figures above, they are very well made and self-explanatory. The docs could greatly benefit to have them included.
Thank you! You can use them in the doc if you want (or use them as inspiration to do something more fancy). I can even provide the SVG file if you want to modify them.
Nice, SVG is better. I'll add them to the document later.
Following #30 , I'll continue the discussion of .adjust_time
's implementation.
From my understanding, the method "shifts" points in time, which effectively shrinks or stretch portions of times.
When giving original_times=[0, 4]
and new_times=[0, 2]
, the method should effectively shrink by two the portion of time from tick 0
to 4
. It should be equivalent to providing original_times=[4]
and new_times=[2]
as it is the only reference time adjusted. It should also be equivalent to providing original_times=[4, 8]
and new_times=[2, 6]
as the time at tick 8
is already "shifted" to tick 6 by the first time portion adjustment (0
to 4
).
Now pretty_midi only covers the time portions within the ranges provided in original_times
. I personally thinks the method should do what its name suggests, that is "shrinking"/"stretching" portions of times given some reference times.
What do you think of this way of treating the time information? I actually find it clearer, especially when visualising what's actually happening on the figures from @leleogere
@leleogere Any suggestions?
I'm not sure that there is a right or wrong answer. I feel like an implementation choice has to be made. If you want ot=[0,4]
/nt=[0,2]
to be equivalent to ot=[4]
/nt=[2]
, then you assume that behind the scenes, a virtual 0 is added at the start of both arrays if ot
does not start with zero. But the question is, should we add a virtual "end" when the last element of ot
isn't the end of the track? Or consider that we only "shift" the remaining elements to the left?
The two possibilities are the following (assuming the track ends at time 8):
In the first case, all those parameters are equivalents :
ot=[4]
/nt=[2]
ot=[0,4]
/nt=[0,2]
ot=[4,8]
/nt=[2,6]
ot=[0,4,8]
/nt=[0,2,6]
(most complete formula)In the second case, we have :
ot=[4]
/nt=[2]
ot=[0,4]
/nt=[0,2]
ot=[4,8]
/nt=[2,8]
ot=[0,4,8]
/nt=[0,2,8]
(most complete formula)In this second case, we consider that the start and the end of the track are fixed if not specified, and only move points in the interval, meaning that moving a point to the left will speed up the part before it, and slow down the part after it (and vice-verse when moved to the right).
I'm not sure about which solution makes the more sense.
I think pretty midi is right, it gives the user a clear idea of the behavior of their code, instead of being added a couple of parameters that implicitly have two possibilities @Natooz
Alright, I'll just adapt the tests accordingly then :)
@leleogere new version has been released with adjust_time
. If there is no other prolems, I'll close this issue.
I quickly found a good usage for this method in MidiTok for a method splitting a MIDI in chunks of n
beats https://github.com/Natooz/MidiTok/pull/148/files#diff-fd4bd5bf18bd048b3958c0bf7a2c8603e60dedec33aea84518aca8d0ab8f74b2R741
The method itself works, the only thing left to handle for a proper usage in this case would be to include all the notes starting in the original_times
range. Here is a figure of what adjust_time
selects when using (0, 12)
beats (2.5 bars):
The original is:
adjust_times
currently discard the last 3 notes as they end outside of the original_times
range.
Do you think an adding an argument allowing to keep such notes could be added?
No big deal if you don't think it's a something worth to be added.
@Natooz You could use the clip
method for Track
and Score
. It will clip all the events in the object according to the given time range, and returns a new one (not an inplace operation).
Here is one of the function signature:
def clip(
self, start: int, end: int, clip_end: bool = False
) -> symusic.core.ScoreTick:
"""
Clip events a given time range
"""
As you can see, there is a clip_end
parameter that meet your needs. Sorry for the lack of documentation.
@Natooz You could use the
clip
method forTrack
andScore
. It will clip all the events in the object according to the given time range, and returns a new one (not an inplace operation).
Not the point, but wouldn't an in_place
parameter for clip
make sense too?
@Yikai-Liao Awesome! That will do, thank you for the advice! :)
@leleogere following #31 , I implemented an order preserving version of adjust_time
using a similar algorithm. This makes the is_sorted
parameter in the original adjust_time
function meaningless, since we don't need to sort the data before adjust_time.
I think I can just remove this parameter now, since the difference in interface this brings is still acceptable. What's your idea?
Not the point, but wouldn't an
in_place
parameter forclip
make sense too?
@leleogere Yeap, I think there is indeed a need to systematize the interfaces that are currently available right now. I'll take the time to categorize and sort out the existing interfaces and open a new issue for discussion. Thanks for your advice.
@leleogere following https://github.com/Yikai-Liao/symusic/issues/31 , I implemented an order preserving version of adjust_time using a similar algorithm. This makes the is_sorted parameter in the original adjust_time function meaningless, since we don't need to sort the data before adjust_time.
I think I can just remove this parameter now, since the difference in interface this brings is still acceptable. What's your idea?
Yeah, I don't see any issue with removing the is_sorted parameter, it seemed a bit odd regardless. It is better to have an order preserving algorithm as any way the adjust_time
shouldn't cause any change in the order of the notes (except if the times are not strictly increasing, do you have a check for that?)
Now the function checks if the original times and bew times are ascending. But i don't check if whether or not there are equal before and after. I will add the check.
Is your feature request related to a problem? Please describe. I would like to be able to speed-up or slow down a MIDI file easily, by modifying note timings (not only the tempo events).
Describe the solution you'd like PrettyMIDI proposes the method
.adjust_times
:This linearly interpolate between the original timing and the new timing by stretching or shrinking time, impacting all the elements of the MIDI file (note starting time, note duration, tempo value, tempo position, time signature position, more generally all possible events). I haven't dig into what they are doing under the hood, to the relation between tempo, ticks and seconds, but this function can be quite useful.
It even allow to pass n-sized lists to allow for more complex mapping than a simple interpolation between the start and the end time.
Describe alternatives you've considered I've considered doing it manually, but it would be handier to have a dedicated function. I don't know if this kind of function have its place in the context of symusic, I would be happy to have your opinion on that.