Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new class for performed notes #295

Merged
merged 5 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 31 additions & 22 deletions partitura/io/importmatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
98 changes: 96 additions & 2 deletions partitura/performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -280,6 +284,96 @@ def adjust_offsets_w_sustain(
note["sound_off"] = offset


class PerformedNote(dict):
def __init__(self, *args, **kwargs):
manoskary marked this conversation as resolved.
Show resolved Hide resolved
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):
manoskary marked this conversation as resolved.
Show resolved Hide resolved
return f"PerformedNote: {self['id']}"

def __str__(self):
return f"PerformedNote: {self['id']}"

def __eq__(self, other):
manoskary marked this conversation as resolved.
Show resolved Hide resolved
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"]:
fosfrancesco marked this conversation as resolved.
Show resolved Hide resolved
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"]:
fosfrancesco marked this conversation as resolved.
Show resolved Hide resolved
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")
manoskary marked this conversation as resolved.
Show resolved Hide resolved
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):
manoskary marked this conversation as resolved.
Show resolved Hide resolved
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.

Expand Down
13 changes: 6 additions & 7 deletions partitura/utils/music.py
Original file line number Diff line number Diff line change
Expand Up @@ -3252,37 +3252,35 @@ 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()
new_cc["time"] -= start_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()
new_pr["time"] -= start_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
Expand Down Expand Up @@ -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
Expand Down