cuthbertLab / music21

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

problem importing midifile #629

Closed laurentVeliscek closed 3 years ago

laurentVeliscek commented 3 years ago

music21 version

6.1.0

Problem summary

Something seems to be broken when importing multi-part midifiles

When I try to show() the imported score, I always get warnings about meter (?):

meter: WARNING: Found a messed up beam pair <music21.beam.Beams <music21.beam.Beam 1/start>/<music21.beam.Beam 2/partial/right>>, <music21.beam.Beams <music21.beam.Beam 1/stop>/<music21.beam.Beam 2/stop>>, at index 2 of 
[None, None, <music21.beam.Beams <music21.beam.Beam 1/start>/<music21.beam.Beam 2/partial/right>>, <music21.beam.Beams <music21.beam.Beam 1/stop>/<music21.beam.Beam 2/stop>>]

Then, I get a MuseScore error message "the xxx.xml file is not a valid MusicXML file". (here, the error message is in french, but that's the idea...)

If I try to open the file in museScore, it crashes, or if I'm lucky, I can display something, but the score seems to be corrupted.

Steps to reproduce

## Here's my code 

from music21 import *
score = converter.parse("test.mid")
score.show()

Expected vs. actual behavior

I wish I could import and manipulate the score transparently.

More information

Mac OS Mojave, python 3.7, all modules freshly installed and updated

I tried using many midifiles downloaded from different locations, I tried loading and saving with museScore, but the same results.

In fact, I couldn't find any multi-part midifile that can be imported and showed properly.

To illustrate this, I join 2 screen copies of the file before and after import, using museScore , so you can compare (notes timing are just messed up...)

I attached the test.mid file (zipped).

Did I miss something ?

Thanks !

before import after import

test.mid.zip

warnings

jacobtylerwalls commented 3 years ago

heya @laurentVeliscek, thanks for the report.

There are MIDI messages in your file I don't understand. The following is from the second track of music:

>>> mf = midi.MidiFile()
>>> mf.open('../Desktop/test.mid')
>>> mf.read()
>>> mf.close()
>>> mf.tracks[2].events[:8]
[<MidiEvent DeltaTime, t=0, track=2, channel=None>, <MidiEvent SEQUENCE_TRACK_NAME, t=0, track=2, channel=None, data=b'goldberg.mid'>, 
<MidiEvent DeltaTime, t=20, track=2, channel=None>, <MidiEvent NOTE_ON, t=0, track=2, channel=2, pitch=71, velocity=124>, 
<MidiEvent DeltaTime, t=12, track=2, channel=None>, <MidiEvent CONTROLLER_CHANGE, t=0, track=2, channel=2, parameter1=7, parameter2=107>, 
<MidiEvent DeltaTime, t=468, track=2, channel=None>, <MidiEvent NOTE_ON, t=0, track=2, channel=2, pitch=71, velocity=0>]

The pair of events in the second row looks fishy: I don't think music21 is interpreting that correctly. Here's the binary:

>>> mf.tracks[2].data
b'\x00\xff\x03\x0cgoldberg.mid\x14\x91G|\x0c\xb1\x07k\x83T\x91G\x00\x00L|$Jk\x10L\x00,Le\x0cJ ...

The 0x14 after the goldberg.mid I don't understand, and neither does music21 -- music21 gives up and converts 0x14 to an integer to get a value of 20, which becomes your DeltaTime of 20 ticks, which turns out to be a 1/96th rest. Here are your timepoints even after turning quantization off (see the mailing list for some posts on this recently):

>>> s = converter.parse('../Desktop/test.mid', quantizePost=False, forceSource=True)
>>> s.parts[1].makeNotation(inPlace=True)
>>> s.parts[1].measure(1).show('text')
{0.0} <music21.clef.TrebleClef>
{0.0} <music21.meter.TimeSignature 3/4>
{0.0} <music21.stream.Voice 0>
    {0.0} <music21.note.Rest rest>
    {0.0417} <music21.note.Note B>
    {1.1167} <music21.note.Note D>
    {2.55} <music21.note.Note D>
{0.0} <music21.stream.Voice 1>
    {1.0417} <music21.note.Note E>
    {1.2417} <music21.note.Note E>
    {2.775} <music21.note.Note C>

So the B that should start at 0.0 actually starts at 0.0417. That 20-tick 1/96th rest is what's hosing the rest of your score.

The 0x14 has to be signifying something else, but I have no idea. After some poking around this is my hypothesis: -- 0x14 is used in the WAV file format to declare the MIDI unity note for playback on a MIDI sampler -- 0x14 in MIDI is used to specify cues or loop markers, which music21 doesn't support -- a MIDI writer got confused and wrote the MIDI unity note it parsed from a WAV file into the MIDI output -- music21 got confused on account of its lack of support for cue or loop markers and just converted into the fishy events as above, giving you a 1/96th rest.

Notice \x91G| in \x14\x91G|could be a MIDI unity note, since it's exactly that first B4 in the track (x9: note_on prefix, 1G: pitch value (71), |: velocity value (124):

<MidiEvent DeltaTime, t=20, track=2, channel=None>, <MidiEvent NOTE_ON, t=0, track=2, channel=2, pitch=71, velocity=124>

When I load the MIDI into GarageBand, I see ticks very early in the tracks, suggesting markers of some kind, which is bonkers, but at least follows the MIDI spec.

Screen Shot 2020-09-26 at 9 32 25 PM

Finally, may I ask -- where did this file come from? If MuseScore is writing it, we can report it to them, and look into a workaround on this end, but if it's less common than that I doubt we can do much. At the very least, you can possibly get rid of these microscopic rests and try building a new stream from everything else. good luck!

laurentVeliscek commented 3 years ago

Hi, Thank you very much for your answer.

I 've built from scratch a midifile using Ableton Live. If the notes are quantified, it seems to work almost fine (see later the problem with the final whole notes that get truncated.)

But if the're not quantified, I get some errors.

I had to use museScore to merge the 2 parts into one single midifile as Ableton Live can only export single parts as midifiles.

Then I tried:

s = converter.parse('../pathTo/test.mid', quantizePost=False, forceSource=True)

I tried to import and show() my unQuantified midifiles.

The fact that the midifile was exported from sibelius or museScore doesn't change anything. M21 crashes(in module noteStream.show()):

music21.duration.DurationException: Cannot return types smaller than 2048th; qLen was: 1/960
music21.musicxml.m21ToXml.MusicXMLExportException: Cannot convert "2048th" duration to MusicXML (too short).

Notice that there's no "very short notes" in the score. So I guess the problem may occur when rounding delta-time to a valid value for the specified clock.

I join the full traceback error reports as text files:

traceback partsFromLiveFromSibelius.txt traceback partsFromLiveMergedUsingMuseScore.txt

The crashing midiFiles are in midi files.zip, attached at the end of this message.

Then I've done more tests using the default converter.parse() (with no instructions)

I noticed a strange behavior: After importing a very simple 2-parts score made in museScore, and showing it, the last note's duration isn't correct. The ending whole of the score becomes a half dotted dotted dotted.

before: simpleScore before import

After import: simpleScore after import

The whole seems to be shortened before the show() function call.

simpleScore Durations after import in M21

Here's the midiFile: verySimple2partsMidifile.mid.zip

Is it normal ?

Another bug, may be: I noticed that the time signature wasn't imported properly with a chopin waltz (testChopin.mid)). Both museScore and Sibelius see the score as a 3/4 time signature, when M21 see a 4/4 score.

Finally, I've made some experiments with an unquantified midiFile.

importMidiFileMinimal.py.zip

I can import and show each part separatley using m21.

If I merge them into a single score using museScore, and import the score in m21, I can show each part, but when trying to show() the full Score, I get the "not a valid musicXML" alert from museScore, and crash it if try to open it.

I loaded and exported the same 2-parts unquantified midiFile using Sibelius. So now, the midifile is generated by Sibelius.

Then, the first part is showed, the second part triggers a MusicXml Error but can be "showed", and I can show() the full score.

Finally, I tried to open in Sibelius the temp "corrupted" XML files generated by M21 by the show() command. But with no success. (Sibelius complains "UNEXPECTED")

So it seems that those XML files really get corrupted, and that's not specifically a museScore bug.

I attach the midifiles I used for testing... midi files.zip

I guess there's an issue somewhere with how M21 deals with note durations. May be it opens a duration tag, but forgets to close it in some circumstances.

Still digging in...

Thanks again for your help !

:-)

Laurent.

jacobtylerwalls commented 3 years ago

Hi Laurent,

  1. I can't reproduce the 1/960th notes in partsFromLiveFromSibelius.mid:

from .mid straight into Finale:

Screen Shot 2020-09-27 at 8 17 33 AM

from .mid to music21 with default quantization to xml, displayed in Finale [looks correct]:

Screen Shot 2020-09-27 at 8 18 05 AM
  1. I can reproduce the failure to import and .show() the MuseScore midi file. This is a known issue in MuseScore that music21 is tripping over since #589 where the SEQUENCE_TRACK_NAME MuseScore writes is wrongly null-terminated. I have already reported this to MuseScore, so please comment there explaining you're affected:
>>> s.show('text')
{0.0} <music21.stream.Part 0x7fab825cbac8>
    {0.0} <music21.instrument.Piano 'Piano, 1-Kontakt 1\x00'>
  1. I can reproduce the triply-dotted note from your file. Are you sure you've turned off anything like "Human playback"? When I import your .mid into Finale and tell it to not quantize your input (or quantize to 4 EDUs), I get this, so this isn't truly round values:
Screen Shot 2020-09-27 at 8 36 25 AM

I think this is probably also a MuseScore issue. I can't recreate this example inside Finale and get the same result.

  1. The issue with time signatures is likely addressed in #603, which is under review.
laurentVeliscek commented 3 years ago

Hi again,

You're absolutely right. I was so confident in museScore I couldn't imagine that it would create incorrect midiFiles from a score, so I didn't check this. (sorry!.. :-/ )

The "Dotted dotted dotted" midifile was written in museScore using mouse clicks. I didn't check/uncheck anything about "human playback".

It seems that museScore has a problem with durations. Here, I've just written 4 measures of 4 whole notes, using my mouse-clicks.

a minimal score input with mouse in museScore

and after midi export from museScore I get this in Ableton Live: midifile imported by Ableton live

(!)

So I believe I have to use another score editor until those museScore bugs get fixed... (So bad, I love museScore interface...)

I gonna make some more tests using Sibelius and let you know if everything is okay...

Thanks !

L.

laurentVeliscek commented 3 years ago

Hi again,

I've re-done my tests using Sibelius.

Still a crash if I set forceSource=True (But I don't really understand the point of this option)

But works much better. No more XML errors.

\o/

I've just noticed that the note.durations are not displayed as real float values, even if they seem to not be quantized. (I've set parse with quantizePost=False)

monitored durations

Is there a way to display the "real" duration expressed as a float ? (I couldn't find the answer exploring the documentation)

I'd like to check something.

Here's what I import into m21 with quantizePost=False partition2partsNonQuantiseSibelius.mid.zip

partition2partsNonQuantiseSibelius.mid: partition2partsNonQuantiseSibelius before import

After import, I show the stream, and then export it to a midifile with museScore. The upper part seems to be quantized.

exportedByMuseScoreAfterShow

Curiously, if I write the imported stream using midi_out = score.write(‘midi’, fp=’path/to/file/out.mid’), It's the opposite. the lower is quantized, but not the upper.

exportedFromTheStreamUsingScoreWrite

Not a big deal, but I prefer to share...

Thanks again for your help !

L.

jacobtylerwalls commented 3 years ago

You can just take float(note.duration.quarterLength) if you want Fraction(1, 3) -> 0.3333333333333333. Don't know if there's a better way offhand.

jacobtylerwalls commented 3 years ago

And about the different quantization behavior, I don't know quite what's going on there, but in order to demonstrate a bug with music21 you would really need to pare it down to a midi file with just two or three notes, and show exactly how you're parsing it by quoting a block of code and then annotating the result of a function like s.show('text', addEndTimes=True).

Try this -- if you need finer control over the quantization units, use converter.parse(quarterLengthDivisors=(16)) where a value such as 16 represents 1/16 of a quarter (that is, a 64th note). The default is (4, 3), as in, sixteenths or triplet-eighths are both acceptable.

Let us know if you have a suggestion to improve the documentation. I haven't interacted with many users after making some contributions to the quantization functionality this summer. forceSource=True shouldn't be necessary every time after #630 is merged. Good luck!

jacobtylerwalls commented 3 years ago

Unfortunately, you and I have each demonstrated issues with MuseScore's MIDI output--yours with duration, mine with null bytes in instrument names. @mscuthbert this is worth noting for future to discourage users from using MuseScore to write MIDI.

jacobtylerwalls commented 3 years ago

well, maybe some TODOs here for music21 after all...

1 -- should stream.quantize(processDurations=True) enforce a minimum duration of the smallest quantization unit (unless the target is a grace note) to avoid writing out 0-duration elements? that's what caused the crazy display in the first post on this thread, also mentioned in #530, and since quantization is on by default...

2 -- should we implement a workaround for the empty byte that MuseScore is adding to instruments, since that will probably stay wrong for a while? as easy as just .split('\x00')[0]

laurentVeliscek commented 3 years ago

Hi !

Thanks for your feedback. I've posted messages about those 2 issues on museScore forum.

1-- m21 can be fed with human unquantized midifiles, and having very very short notes is some unpredictable. A note with a zero duration doesn't make sense. At least, it must have a minimal duration > 0. So I believe this should be fixed. And a fix would avoid strange behaviors and bugs.

2-- As museScore is the default companion of m21, I agree with you that fixing things that can be fixed would avoid user's complains (like I did ;-). If a workaround is obvious (as you suggest), I thinks it's a good idea to implement it.

The more I dig in m21 documentation, the more I realize how powerful it is.

Thanks again for your amazing work !...

:-)

L.