3

I am using music21 for handling MIDI and mXML files and converting them to a piano roll I am using in my project.

My piano roll is made up of sequence of 88-dimensional vectors where each element in a vector represents one pitch. One vector is one time step that can be 16th, 8th, 4th, and so on. Elements can obtain three values {0, 1, 2}. 0 means note is off. 1 means note is on. 2 means also that note is on but it always follows 1 - that is how I distinguish multiple key presses of same note. E.g., let time step be 8th and these two pitches be C and E:

[0 0 0 ... 1 0 0 0 1 ... 0]
[0 0 0 ... 1 0 0 0 1 ... 0]
[0 0 0 ... 2 0 0 0 2 ... 0]
[0 0 0 ... 2 0 0 0 2 ... 0]
[0 0 0 ... 1 0 0 0 0 ... 0]
[0 0 0 ... 1 0 0 0 0 ... 0]

We see that C and E are simultaneously played for quarter note, then again for quarter note, and we end with a C that lasts quarter note.

Right now, I am creating Stream() for every note and fill it as notes come. That gives me 88 streams and when I convert that to MIDI, and open that MIDI with MuseScore, that leaves me with a mess that is not readable.

My question is, is there some nicer way to transform this kind of piano roll to MIDI? Some algorithm, or idea which I could use would be appreciated.

dosvarog
  • 694
  • 1
  • 6
  • 20
  • If you have 88 streams and the durations are right, put them in a `Score` object and run `.chordify()`. – Michael Scott Asato Cuthbert Jun 21 '17 at 15:07
  • That is something I already tried, but for some reason it doesn't give good output. When I convert my `Stream()`s directly to MIDI, file sounds as it is supposed to. I guess, that means durations are right. But when I `.chordify()` it, I get additional chords as a result. Still correct ones, but for example, instead of one half note, I get two quarter note chords although `addTies=True`. – dosvarog Jun 21 '17 at 16:01

1 Answers1

1

In my opinion music21 is a very good library but too high-level for this job. There is no such thing as streams, quarter notes or chords in MIDI -- only messages. Try the Mido library instead. Here is sample code:

from mido import Message, MidiFile, MidiTrack

def stop_note(note, time):
    return Message('note_off', note = note,
                   velocity = 0, time = time)

def start_note(note, time):
    return Message('note_on', note = note,
                   velocity = 127, time = time)

def roll_to_track(roll):
    delta = 0
    # State of the notes in the roll.
    notes = [False] * len(roll[0])
    # MIDI note for first column.
    midi_base = 60
    for row in roll:
        for i, col in enumerate(row):
            note = midi_base + i
            if col == 1:
                if notes[i]:
                    # First stop the ringing note
                    yield stop_note(note, delta)
                    delta = 0
                yield start_note(note, delta)
                delta = 0
                notes[i] = True
            elif col == 0:
                if notes[i]:
                    # Stop the ringing note
                    yield stop_note(note, delta)
                    delta = 0
                notes[i] = False
        # ms per row
        delta += 500

roll = [[0, 0, 0, 1, 0, 0, 0, 1, 0],
        [0, 0, 0, 1, 0, 0, 0, 1, 0],
        [0, 0, 0, 2, 0, 0, 0, 2, 0],
        [0, 1, 0, 2, 0, 0, 0, 2, 0],
        [0, 0, 0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 0, 0, 0, 0, 0]]

midi = MidiFile(type = 1)
midi.tracks.append(MidiTrack(roll_to_track(roll)))
midi.save('test.mid')
Björn Lindqvist
  • 19,221
  • 20
  • 87
  • 122
  • If `1` means "note is playing" rather than "note just started playing" as in the question, use `if col == 1: if not notes[i]: yield start_note(note, delta)` – root Jan 01 '22 at 22:55
  • Does `delta` keep track of time? Then why does it have to be set `0` after every `start_note` and `stop_note`? Because `mido` counts time relative to the previous event rather than to the beginning? Where is this property of `mido` documented? Thanks! – root Jan 01 '22 at 23:11
  • If the number of milliseconds per row of the piano roll is not an integer, we have to pass `round(delta)` to `start_note` and `stop_note` to prevent "ValueError: message time must be int in MIDI file". And the nontrivial part is that we should probably also do something like `delta -= round(delta)` instead of `delta = 0` to avoid accumulating a time shift. – root Jan 02 '22 at 13:08
  • Consider `roll = np.random.rand(2*3600,10)>0.5` (random notes for 1 hour). The resulting MIDI file should be exactly 1 hour long, but it is 2.5 minutes longer than that. Why? – root Jan 02 '22 at 16:30
  • There seems to be a bug in `mido` that affects all results. For example, `messages = [mido.Message('note_on', note = 100, time = 1000), mido.Message('note_off', note = 100, time = 1000*3600)]; midi.tracks.append(mido.MidiTrack(messages)); midi.save('test.mid')` should create a 1-hour track, but it is 2.5 minutes longer. – root Jan 02 '22 at 16:52