From a9ee5cd58880537d58e677371972c8c42ce880b3 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 18 Jul 2023 12:27:11 +0200 Subject: [PATCH 1/5] new class for performedNotes. --- partitura/performance.py | 98 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 2 deletions(-) diff --git a/partitura/performance.py b/partitura/performance.py index f7ac86dc..aa2c8f15 100644 --- a/partitura/performance.py +++ b/partitura/performance.py @@ -85,7 +85,7 @@ def __init__( super().__init__() self.id = id self.part_name = part_name - self.notes = notes + self.notes = list(map(lambda n: PerformedNote(n), notes)) self.controls = controls or [] self.programs = programs or [] self.ppq = ppq @@ -203,7 +203,11 @@ def from_note_array( if "id" not in note_array.dtype.names: n_ids = ["n{0}".format(i) for i in range(len(note_array))] else: - n_ids = note_array["id"] + # Check if all ids are the same + if np.all(note_array["id"] == note_array["id"][0]): + n_ids = ["n{0}".format(i) for i in range(len(note_array))] + else: + n_ids = note_array["id"] if "track" not in note_array.dtype.names: tracks = np.zeros(len(note_array), dtype=int) @@ -280,6 +284,96 @@ def adjust_offsets_w_sustain( note["sound_off"] = offset +class PerformedNote(dict): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self["id"] = self.get("id", None) + self["pitch"] = self.get("pitch", self["midi_pitch"]) + self["note_on"] = self.get("note_on", -1) + self["note_off"] = self.get("note_off", -1) + self["sound_off"] = self.get("sound_off", self["note_off"]) + self["track"] = self.get("track", 0) + self["channel"] = self.get("channel", 1) + self["velocity"] = self.get("velocity", 60) + self.validate_values() + self._accepted_keys = ["id", "pitch", "note_on", "note_off", "velocity", "track", "channel", "sound_off"] + self.__setitem__ = self._setitem_new + + def __repr__(self): + return f"PerformedNote: {self['id']}" + + def __str__(self): + return f"PerformedNote: {self['id']}" + + def __eq__(self, other): + return self["id"] == other["id"] + + def __hash__(self): + return hash(self["id"]) + + def __lt__(self, other): + return self["note_on"] < other["note_on"] + + def __le__(self, other): + return self["note_on"] <= other["note_on"] + + def __gt__(self, other): + return self["note_on"] > other["note_on"] + + def __ge__(self, other): + return self["note_on"] >= other["note_on"] + + def __getitem__(self, key): + return self.get(key, None) + + def _setitem_new(self, key, value): + if key not in self._accepted_keys: + raise KeyError(f"Key {key} not in PerformedNote") + elif key == "note_off": + # Verify that the note_off is after the note_on + if value < self["note_on"]: + raise ValueError(f"note_off must be after note_on") + self["sound_off"] = value if self["sound_off"] < value else self["sound_off"] + self["note_off"] = value + elif key == "note_on": + # Verify that the note_on is before the note_off + if value > self["note_off"]: + raise ValueError(f"note_on must be before note_off") + + self["duration_sec"] = self["note_off"] - value + self["note_on"] = value + elif key == "sound_off": + # Verify that the sound_off is after the note_on + if value < self["note_off"]: + raise ValueError(f"sound_off must be after note_off") + self["sound_off"] = value + else: + self[key] = value + + def __delitem__(self, key): + raise KeyError("Cannot delete items from PerformedNote") + + def __iter__(self): + return iter(self.keys()) + + def __len__(self): + return len(self.keys()) + + def __contains__(self, key): + return key in self.keys() + + def validate_values(self): + if self["pitch"] > 127 or self["pitch"] < 0: + raise ValueError(f"pitch must be between 0 and 127") + if self["note_on"] < 0: + raise ValueError(f"Note on value provided is invalid, must be greater than or equal to 0") + if self["note_off"] < 0 or self["note_off"] < self["note_on"]: + raise ValueError(f"Note off value provided is invalid, " + f"must be greater than or equal to 0 and greater than note_on") + if self["velocity"] > 127 or self["velocity"] < 0: + raise ValueError(f"velocity must be between 0 and 127") + + class Performance(object): """Main object for representing a performance. From c115949f7398e5e485aaba5633647c5be3633a38 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 10 Aug 2023 16:25:31 +0200 Subject: [PATCH 2/5] Fixed ppart_from_matchfile when first_note_at_zero: Fixed bug when the first note in matchfile is not the first performed note in time. --- partitura/io/importmatch.py | 53 ++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/partitura/io/importmatch.py b/partitura/io/importmatch.py index 04e96f8f..a7202ebd 100644 --- a/partitura/io/importmatch.py +++ b/partitura/io/importmatch.py @@ -371,29 +371,38 @@ def performed_part_from_match( # PerformedNote instances for all MatchNotes notes = [] - first_note = next(mf.iter_notes(), None) - if first_note and first_note_at_zero: - offset = midi_ticks_to_seconds(first_note.Onset, mpq=mpq, ppq=ppq) - offset_tick = first_note.Onset - else: - offset = 0 - offset_tick = 0 - - notes = [ - dict( - id=format_pnote_id(note.Id), - midi_pitch=note.MidiPitch, - note_on=midi_ticks_to_seconds(note.Onset, mpq, ppq) - offset, - note_off=midi_ticks_to_seconds(note.Offset, mpq, ppq) - offset, - note_on_tick=note.Onset - offset_tick, - note_off_tick=note.Offset - offset_tick, - sound_off=midi_ticks_to_seconds(note.Offset, mpq, ppq) - offset, - velocity=note.Velocity, - track=getattr(note, "Track", 0), - channel=getattr(note, "Channel", 1), + notes = list() + note_onsets_in_secs = np.array(np.zeros(len(mf.notes)), dtype=float) + note_onsets_in_tick = np.array(np.zeros(len(mf.notes)), dtype=int) + for i, note in enumerate(mf.notes): + n_onset_sec = midi_ticks_to_seconds(note.Onset, mpq, ppq) + note_onsets_in_secs[i] = n_onset_sec + note_onsets_in_tick[i] = note.Onset + notes.append( + dict( + id=format_pnote_id(note.Id), + midi_pitch=note.MidiPitch, + note_on=n_onset_sec, + note_off=midi_ticks_to_seconds(note.Offset, mpq, ppq), + note_on_tick=note.Onset, + note_off_tick=note.Offset, + sound_off=midi_ticks_to_seconds(note.Offset, mpq, ppq), + velocity=note.Velocity, + track=getattr(note, "Track", 0), + channel=getattr(note, "Channel", 1), + ) ) - for note in mf.notes - ] + # Set first note_on to zero in ticks and seconds if first_note_at_zero + if first_note_at_zero and len(note_onsets_in_secs) > 0: + offset = note_onsets_in_secs.min() + offset_tick = note_onsets_in_tick.min() + if offset > 0 and offset_tick > 0: + for note in notes: + note["note_on"] -= offset + note["note_off"] -= offset + note["sound_off"] -= offset + note["note_on_tick"] -= offset_tick + note["note_off_tick"] -= offset_tick # SustainPedal instances for sustain pedal lines sustain_pedal = [ From d09156374311bd59e374a6da14341471ecf1093b Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 10 Aug 2023 16:47:23 +0200 Subject: [PATCH 3/5] fixes for time_slice from ppart. Changed initialization of ppart towards the end to avoid errors with Performance Notes. --- partitura/utils/music.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index a1469e50..743db632 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -3252,18 +3252,17 @@ def slice_ppart_by_time( # create a new (empty) instance of a PerformedPart # single dummy note added to be able to set sustain_pedal_threshold in __init__ # -> check `adjust_offsets_w_sustain` in partitura.performance - ppart_slice = PerformedPart([{"note_on": 0, "note_off": 0}]) + # ppart_slice = PerformedPart([{"note_on": 0, "note_off": 0, "pitch": 0}]) # get ppq if PerformedPart contains it, # else skip time_tick info when e.g. created with 'load_performance_midi' try: ppq = ppart.ppq - ppart_slice.ppq = ppq except AttributeError: ppq = None + controls_slice = [] if ppart.controls: - controls_slice = [] for cc in ppart.controls: if cc["time"] >= start_time and cc["time"] <= end_time: new_cc = cc.copy() @@ -3271,10 +3270,9 @@ def slice_ppart_by_time( if ppq: new_cc["time_tick"] = int(2 * ppq * cc["time"]) controls_slice.append(new_cc) - ppart_slice.controls = controls_slice + programs_slice = [] if ppart.programs: - programs_slice = [] for pr in ppart.programs: if pr["time"] >= start_time and pr["time"] <= end_time: new_pr = pr.copy() @@ -3282,7 +3280,7 @@ def slice_ppart_by_time( if ppq: new_pr["time_tick"] = int(2 * ppq * pr["time"]) programs_slice.append(new_pr) - ppart_slice.programs = programs_slice + notes_slice = [] note_id = 0 @@ -3326,7 +3324,8 @@ def slice_ppart_by_time( else: break - ppart_slice.notes = notes_slice + # Create slice PerformedPart + ppart_slice = PerformedPart(notes=notes_slice, programs=programs_slice, controls=controls_slice, ppq=ppq) # set threshold property after creating notes list to update 'sound_offset' values ppart_slice.sustain_pedal_threshold = ppart.sustain_pedal_threshold From b7e28059c0b0f2a95672b3e38ff53cee77ad8fac Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 20 Sep 2023 14:50:47 +0200 Subject: [PATCH 4/5] corrections requested by reviewers. --- partitura/performance.py | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/partitura/performance.py b/partitura/performance.py index aa2c8f15..2b04402a 100644 --- a/partitura/performance.py +++ b/partitura/performance.py @@ -285,8 +285,20 @@ def adjust_offsets_w_sustain( class PerformedNote(dict): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + """ + A dictionary-like object representing a performed note. + + Parameters + ---------- + pnote_dict : dict + A dictionary containing performed note information. + This information can contain the following fields: + "id", "pitch", "note_on", "note_off", "velocity", "track", "channel", "sound_off". + If not provided, the default values will be used. + Pitch, note_on, and note_off are required. + """ + def __init__(self, pnote_dict): + super().__init__(pnote_dict) self["id"] = self.get("id", None) self["pitch"] = self.get("pitch", self["midi_pitch"]) self["note_on"] = self.get("note_on", -1) @@ -295,7 +307,7 @@ def __init__(self, *args, **kwargs): self["track"] = self.get("track", 0) self["channel"] = self.get("channel", 1) self["velocity"] = self.get("velocity", 60) - self.validate_values() + self._validate_values() self._accepted_keys = ["id", "pitch", "note_on", "note_off", "velocity", "track", "channel", "sound_off"] self.__setitem__ = self._setitem_new @@ -306,7 +318,11 @@ def __str__(self): return f"PerformedNote: {self['id']}" def __eq__(self, other): - return self["id"] == other["id"] + if not isinstance(PerformedNote): + return False + if not self.keys() == other.keys(): + return False + return np.all(np.array([self[k] == other[k] for k in self.keys() if k in other.keys()])) def __hash__(self): return hash(self["id"]) @@ -332,20 +348,20 @@ def _setitem_new(self, key, value): elif key == "note_off": # Verify that the note_off is after the note_on if value < self["note_on"]: - raise ValueError(f"note_off must be after note_on") + raise ValueError(f"note_off must be after or equal to note_on") self["sound_off"] = value if self["sound_off"] < value else self["sound_off"] self["note_off"] = value elif key == "note_on": # Verify that the note_on is before the note_off if value > self["note_off"]: - raise ValueError(f"note_on must be before note_off") + raise ValueError(f"note_on must be before or equal to note_off") self["duration_sec"] = self["note_off"] - value self["note_on"] = value elif key == "sound_off": - # Verify that the sound_off is after the note_on - if value < self["note_off"]: - raise ValueError(f"sound_off must be after note_off") + # Verify that the sound_off is after the note_off + if value <= self["note_off"]: + raise ValueError(f"sound_off must be after or equal to note_off") self["sound_off"] = value else: self[key] = value @@ -362,14 +378,14 @@ def __len__(self): def __contains__(self, key): return key in self.keys() - def validate_values(self): + def _validate_values(self): if self["pitch"] > 127 or self["pitch"] < 0: raise ValueError(f"pitch must be between 0 and 127") if self["note_on"] < 0: raise ValueError(f"Note on value provided is invalid, must be greater than or equal to 0") if self["note_off"] < 0 or self["note_off"] < self["note_on"]: raise ValueError(f"Note off value provided is invalid, " - f"must be greater than or equal to 0 and greater than note_on") + f"must be greater than or equal to 0 and greater or equal to note_on") if self["velocity"] > 127 or self["velocity"] < 0: raise ValueError(f"velocity must be between 0 and 127") From 6028327b97a44c8f1783a7783f13fdb2c49992a4 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 20 Sep 2023 16:15:07 +0200 Subject: [PATCH 5/5] removed performednote repr method to inherit from parent class. --- partitura/performance.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/partitura/performance.py b/partitura/performance.py index 2b04402a..0b0979bb 100644 --- a/partitura/performance.py +++ b/partitura/performance.py @@ -311,9 +311,6 @@ def __init__(self, pnote_dict): self._accepted_keys = ["id", "pitch", "note_on", "note_off", "velocity", "track", "channel", "sound_off"] self.__setitem__ = self._setitem_new - def __repr__(self): - return f"PerformedNote: {self['id']}" - def __str__(self): return f"PerformedNote: {self['id']}"