Skip to content

Commit

Permalink
craffel#131 Fix a problem when reading tracks with overlapping notes.
Browse files Browse the repository at this point in the history
In particular when there are two adjacent notes and at the same tick there's
first note-on and then note-off, the library incorrectly closes both notes, even
though the second note should be still open. Such a MIDI is probably a bit
malformed, but the library should not fail on that if it can resolve the situation.

Due to this bug the library incorrectly produces zero-duration events and some
long open notes.

The solution is to keep the note-on event in the list of open notes if it's at the
same tick as the note-off event and we're closing some previous notes.

Note that we changed the format of time within values of the last_note_on dict
- from real time to integer tick. This is to avoid floating point comparison when
we have the original integer values.
  • Loading branch information
bzamecnik committed Jul 18, 2017
1 parent 65b91f4 commit ddad9b3
Showing 1 changed file with 40 additions and 11 deletions.
51 changes: 40 additions & 11 deletions pretty_midi/pretty_midi.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,14 +285,17 @@ def __get_instrument(program, channel, track, create_new):
return instrument

for track_idx, track in enumerate(midi_data.tracks):
print('--track--')
# Keep track of last note on location:
# key = (instrument, note),
# value = (note on time, velocity)
# value = (note-on tick, velocity)
last_note_on = collections.defaultdict(list)
# Keep track of which instrument is playing in each channel
# initialize to program 0 for all channels
current_instrument = np.zeros(16, dtype=np.int)
for event in track:
print()
print('last_note_on', last_note_on)
# Look for track name events
if event.type == 'track_name':
# Set the track name for the current track
Expand All @@ -303,23 +306,42 @@ def __get_instrument(program, channel, track, create_new):
current_instrument[event.channel] = event.program
# Note ons are note on events with velocity > 0
elif event.type == 'note_on' and event.velocity > 0:
print('note-on:', event)
# Store this as the last note-on location
note_on_index = (event.channel, event.note)
last_note_on[note_on_index].append((
self.__tick_to_time[event.time],
event.velocity))
event.time, event.velocity))
# Note offs can also be note on events with 0 velocity
elif event.type == 'note_off' or (event.type == 'note_on' and
event.velocity == 0):
print('note-off:', event)
# Check that a note-on exists (ignore spurious note-offs)
if (event.channel, event.note) in last_note_on:
key = (event.channel, event.note)
if key in last_note_on:
# Get the start/stop times and velocity of every note
# which was turned on with this instrument/drum/pitch
for start, velocity in last_note_on[
(event.channel, event.note)]:
end = self.__tick_to_time[event.time]
# which was turned on with this instrument/drum/pitch.
# One note-off may close multiple note-on events from
# previous ticks. In case there's a note-off and then
# note-on at the same tick we keep the open note from
# this tick.
end_tick = event.time
open_notes = last_note_on[key]

notes_to_close = [
(start_tick, velocity)
for start_tick, velocity in open_notes
if start_tick != end_tick]
notes_to_keep = [
(start_tick, velocity)
for start_tick, velocity in open_notes
if start_tick == end_tick]

for start_tick, velocity in notes_to_close:
print('start_tick', start_tick)
start_time = self.__tick_to_time[start_tick]
end_time = self.__tick_to_time[end_tick]
# Create the note event
note = Note(velocity, event.note, start, end)
note = Note(velocity, event.note, start_time, end_time)
# Get the program and drum type for the current
# instrument
program = current_instrument[event.channel]
Expand All @@ -330,8 +352,14 @@ def __get_instrument(program, channel, track, create_new):
program, event.channel, track_idx, 1)
# Add the note event
instrument.notes.append(note)
# Remove the last note on for this instrument
del last_note_on[(event.channel, event.note)]

if len(notes_to_close) > 0 and len(notes_to_keep) > 0:
# Note-on on the same tick but we already closed
# some previous notes -> it will continue, keep it.
last_note_on[key] = notes_to_keep
else:
# Remove the last note on for this instrument
del last_note_on[key]
# Store pitch bends
elif event.type == 'pitchwheel':
# Create pitch bend class instance
Expand All @@ -358,6 +386,7 @@ def __get_instrument(program, channel, track, create_new):
program, event.channel, track_idx, 0)
# Add the control change event
instrument.control_changes.append(control_change)
print('last_note_on', last_note_on)
# Initialize list of instruments from instrument_map
self.instruments = [i for i in instrument_map.values()]

Expand Down

0 comments on commit ddad9b3

Please sign in to comment.