Source code for muspy.outputs.event

"""Event-based representation output interface."""
from math import ceil
from operator import attrgetter, itemgetter
from typing import TYPE_CHECKING, Iterable, List, Tuple

import numpy as np
from bidict import bidict
from numpy import ndarray

if TYPE_CHECKING:
    from ..music import Music


[docs]def to_event_representation( music: "Music", use_single_note_off_event: bool = False, use_end_of_sequence_event: bool = False, encode_velocity: bool = False, force_velocity_event: bool = True, max_time_shift: int = 100, velocity_bins: int = 32, dtype=int, ) -> ndarray: """Encode a Music object into event-based representation. The event-based represetantion represents music as a sequence of events, including note-on, note-off, time-shift and velocity events. The output shape is M x 1, where M is the number of events. The values encode the events. The default configuration uses 0-127 to encode note-on events, 128-255 for note-off events, 256-355 for time-shift events, and 356 to 387 for velocity events. Parameters ---------- music : :class:`muspy.Music` Music object to encode. use_single_note_off_event : bool, default: False Whether to use a single note-off event for all the pitches. If True, the note-off event will close all active notes, which can lead to lossy conversion for polyphonic music. use_end_of_sequence_event : bool, default: False Whether to append an end-of-sequence event to the encoded sequence. encode_velocity : bool, default: False Whether to encode velocities. force_velocity_event : bool, default: True Whether to add a velocity event before every note-on event. If False, velocity events are only used when the note velocity is changed (i.e., different from the previous one). max_time_shift : int, default: 100 Maximum time shift (in ticks) to be encoded as an separate event. Time shifts larger than `max_time_shift` will be decomposed into two or more time-shift events. velocity_bins : int, default: 32 Number of velocity bins to use. dtype : np.dtype, type or str, default: int Data type of the return array. Returns ------- ndarray, shape=(?, 1) Encoded array in event-based representation. """ if dtype is None: dtype = int # Collect notes notes = [] for track in music.tracks: notes.extend(track.notes) # Raise an error if no notes is found if not notes and not use_end_of_sequence_event: raise RuntimeError("No notes found.") # Sort the notes notes.sort(key=attrgetter("time", "pitch", "duration", "velocity")) # Compute offsets offset_note_on = 0 offset_note_off = 128 offset_time_shift = 129 if use_single_note_off_event else 256 offset_velocity = offset_time_shift + max_time_shift if use_end_of_sequence_event: offset_eos = offset_velocity + velocity_bins # Collect note-related events note_events = [] last_velocity = -1 for note in notes: # Velocity event if encode_velocity: quantized_velocity = int(note.velocity * velocity_bins / 128) if force_velocity_event or quantized_velocity != last_velocity: note_events.append( (note.time, offset_velocity + quantized_velocity) ) last_velocity = quantized_velocity # Note on event note_events.append((note.time, offset_note_on + note.pitch)) # Note off event if use_single_note_off_event: note_events.append((note.end, offset_note_off)) else: note_events.append((note.end, offset_note_off + note.pitch)) # Sort events by time note_events.sort(key=itemgetter(0)) # Create a list for all events events = [] # Initialize the time cursor time_cursor = 0 # Iterate over note events for time, code in note_events: # If event time is after the time cursor, append tick shift # events if time > time_cursor: div, mod = divmod(time - time_cursor, max_time_shift) for _ in range(div): events.append(offset_time_shift + max_time_shift - 1) if mod > 0: events.append(offset_time_shift + mod - 1) events.append(code) time_cursor = time else: events.append(code) # Append the end-of-sequence event if use_end_of_sequence_event: events.append(offset_eos) return np.array(events, dtype=dtype).reshape(-1, 1)
class EventSequence: """A class for handling an event sequence. This class serves as a containter for an event sequence. The elements are stored as integer codes, where the corresponding events are defined by the `indexer` attribute. The event sequence can also be accessed as a list of strings by calling `events`. Attributes ---------- codes : list of int List of event codes. indexer : bidict, optional Indexer that defines the mapping between events and their codes. """ def __init__( self, codes: List[int] = None, indexer: bidict[str, int] = None ): self.codes = codes if codes is not None else [] self.indexer = indexer if indexer is not None else bidict() def __len__(self) -> int: return len(self.codes) def __repr__(self) -> str: return f"EventSequence({repr(self.codes)})" def __getitem__(self, key: int) -> int: return self.codes[key] def __setitem__(self, key: int, value: int): self.codes[key] = value def __delitem__(self, key: int): del self.codes[key] def __eq__(self, other) -> bool: if isinstance(other, EventSequence): return self.codes == other.codes return self.codes == other @property def events(self) -> List[str]: """Return a list of all events as strings.""" return [self.indexer.inverse[elem] for elem in self.codes] def get_event(self, idx: int) -> str: """Return the event string at a given index.""" return self.indexer.inverse[self.codes[idx]] def to_event(self, code: int) -> str: """Return an event code as its corresponding event string. This is equivalent to `self.indexer.inverse[code]`. """ return self.indexer.inverse[code] def to_code(self, event: str) -> int: """Return an event code as its corresponding event string. This is equivalent to `self.indexer[event]`. """ return self.indexer[event] def append(self, code: int): """Append an event code to the event sequence.""" self.codes.append(code) def extend(self, codes: Iterable[int]): """Append an event code to the event sequence.""" self.codes.extend(codes) def append_event(self, event: str): """Append an event string to the event sequence.""" self.codes.append(self.to_code(event)) def extend_events(self, events: List[str]): """Extend the event sequence by a list of events.""" self.codes.extend(self.indexer[event] for event in events) def get_default_indexer() -> bidict[str, int]: """Return the default indexer.""" indexer = {} idx = 0 # Note-on events for i in range(128): indexer[f"note_on_{i}"] = idx idx += 1 # Note-off events for i in range(128): indexer[f"note_off_{i}"] = idx idx += 1 # Time-shift events for i in range(1, 101): indexer[f"time_shift_{i}"] = idx idx += 1 return bidict(indexer) class DefaultEventSequence(EventSequence): """A class for handling a MIDI-like event sequence. Attributes ---------- indexer : bidict, optional Indexer that defines the mapping between events and their codes. """ def __init__(self, codes: List[int] = None, indexer: bidict = None): if indexer is not None: super().__init__(codes, indexer) else: super().__init__(codes, get_default_indexer()) @classmethod def to_note_on_event(cls, pitch) -> str: """Return a note-on event for a given pitch.""" return f"note_on_{pitch}" @classmethod def to_note_off_event(cls, pitch) -> str: """Return a note-off event for a given pitch.""" return f"note_off_{pitch}" @classmethod def to_time_shift_events(cls, time_shift) -> List[str]: """Return a list of time-shift events for a given time-shift.""" if time_shift <= 100: return [f"time_shift_{time_shift}"] events = [] div, mod = divmod(time_shift, 100) for _ in range(div): events.append("time_shift_100") if mod > 0: events.append(f"time_shift_{mod}") return events def to_default_event_sequence( music: "Music", resolution: int = None ) -> DefaultEventSequence: """Return a Music object as a DefaultEventSequence object.""" # Adjust resolution if resolution is not None: music.adjust_resolution(resolution) # Collect notes notes = [] for track in music.tracks: notes.extend(track.notes) # Raise an error if no notes is found if not notes: raise RuntimeError("No notes found.") # Create a DefaultEventSequence object seq = DefaultEventSequence() # Collect events events = [] for note in notes: events.append((note.time, seq.to_note_on_event(note.pitch))) events.append((note.end, seq.to_note_off_event(note.pitch))) # Sort the events by time events.sort(key=itemgetter(0)) # Create event sequence last_event_time = 0 for event in events: if event[0] > last_event_time: time_shift = event[0] - last_event_time seq.extend_events(seq.to_time_shift_events(time_shift)) seq.append_event(event[1]) last_event_time = event[0] return seq
[docs]def to_default_event_representation(music: "Music", dtype=int) -> ndarray: """Encode a Music object into the default event representation.""" seq = to_default_event_sequence(music) return np.array(seq, dtype=dtype)
def get_performance_indexer() -> bidict[str, int]: """Return the default indexer.""" indexer = {} idx = 0 # Note-on events for i in range(128): indexer[f"note_on_{i}"] = idx idx += 1 # Note-off events for i in range(128): indexer[f"note_off_{i}"] = idx idx += 1 # Time-shift events for i in range(1, 101): indexer[f"time_shift_{i}"] = idx idx += 1 # Velocity events for i in range(32): indexer[f"velocity_{i}"] = idx idx += 1 return bidict(indexer) class PerformanceEventSequence(EventSequence): """A class for handling a MIDI-like event sequence. Attributes ---------- indexer : bidict, optional Indexer that defines the mapping between events and their codes. """ def __init__(self, codes: List[int] = None, indexer: bidict = None): if indexer is not None: super().__init__(codes, indexer) else: super().__init__(codes, get_performance_indexer()) @classmethod def to_note_on_event(cls, pitch) -> str: """Return a note-on event for a given pitch.""" return f"note_on_{pitch}" @classmethod def to_note_off_event(cls, pitch) -> str: """Return a note-off event for a given pitch.""" return f"note_off_{pitch}" @classmethod def to_velocity_event(cls, velocity) -> str: """Return a velocity event for a given velocity.""" return f"velocity_{velocity//4}" @classmethod def to_time_shift_events(cls, time_shift) -> List[str]: """Return a list of time-shift events for a given time-shift.""" if time_shift <= 100: return [f"time_shift_{time_shift}"] events = [] div, mod = divmod(time_shift, 100) for _ in range(div): events.append("time_shift_100") if mod > 0: events.append(f"time_shift_{mod}") return events def to_performance_event_sequence( music: "Music", resolution: int = None ) -> PerformanceEventSequence: """Return a Music object as a PerformanceEventSequence object.""" # Adjust resolution if resolution is not None: music.adjust_resolution(resolution) # Collect notes notes = [] for track in music.tracks: notes.extend(track.notes) # Raise an error if no notes is found if not notes: raise RuntimeError("No notes found.") # Create a PerformanceEventSequence object seq = PerformanceEventSequence() # Collect events events = [] for note in notes: events.append((note.time, seq.to_velocity_event(note.velocity))) events.append((note.time, seq.to_note_on_event(note.pitch))) events.append((note.end, seq.to_note_off_event(note.pitch))) # Sort the events by time events.sort(key=itemgetter(0)) # Create event sequence last_event_time = 0 for event in events: if event[0] > last_event_time: time_shift = event[0] - last_event_time seq.extend_events(seq.to_time_shift_events(time_shift)) seq.append_event(event[1]) last_event_time = event[0] return seq
[docs]def to_performance_event_representation(music: "Music", dtype=int) -> ndarray: """Encode a Music object to the performance event representation.""" seq = to_performance_event_sequence(music) return np.array(seq, dtype=dtype)
def get_remi_indexer() -> bidict[str, int]: """Return the REMI indexer.""" indexer = {} idx = 0 # Note-on events for i in range(128): indexer[f"note_on_{i}"] = idx idx += 1 # Note-duration events for i in range(1, 65): indexer[f"note_duration_{i}"] = idx idx += 1 # Note-velocity events for i in range(32): indexer[f"note_velocity_{i}"] = idx idx += 1 # Position events for i in range(16): indexer[f"position_{i}"] = idx idx += 1 # Beat event indexer["bar"] = idx idx += 1 # Tempo events for i in range(30, 210): indexer[f"tempo_{i}"] = idx idx += 1 return bidict(indexer) class REMIEventSequence(EventSequence): """A class for handling the REMI event sequence [1]. This by default will adjust the resolution to 16. Attributes ---------- indexer : bidict, optional Indexer that defines the mapping between events and their codes. Warnings -------- Chord events are not supported. References ---------- 1. Yu-Siang Huang and Yi-Hsuan Yang, “Pop Music Transformer: Beat-based Modeling and Generation of Expressive Pop Piano Compositions,” in The 28th ACM International Conference on Multimedia (MMR), 2020. """ def __init__(self, codes: List[int] = None, indexer: bidict = None): if indexer is not None: super().__init__(codes, indexer) else: super().__init__(codes, get_remi_indexer()) @classmethod def to_note_on_event(cls, pitch) -> str: """Return a note-on event for a given pitch.""" return f"note_on_{pitch}" @classmethod def to_note_duration_event(cls, duration) -> str: """Return a note-duration event for a given duration.""" return f"note_duration_{duration}" @classmethod def to_note_velocity_event(cls, velocity) -> str: """Return a note-velocity event for a given velocity.""" return f"note_velocity_{velocity // 4}" @classmethod def to_position_event(cls, position) -> str: """Return a position event for a given position.""" return f"position_{position}" @classmethod def to_bar_event(cls) -> str: """Return a bar event.""" return "bar" @classmethod def to_tempo_event(cls, tempo) -> str: """Return a position event for a given position.""" return f"tempo_{int(tempo)}" def to_remi_event_sequence(music: "Music") -> REMIEventSequence: """Return a Music object as a REMIEventSequence object.""" # Adjust resolution music.adjust_resolution(16) # Collect notes notes = [] for track in music.tracks: notes.extend(track.notes) # Raise an error if no notes is found if not notes: raise RuntimeError("No notes found.") # Create a REMIEventSequence object seq = REMIEventSequence() # Collect measure times barline_times = [barline.time for barline in music.barlines] if barline_times[0] != 0: barline_times.insert(0, 0) measure_times = np.sort(barline_times) assert len(measure_times) > 1 def _get_measure_position(time) -> Tuple[int, int]: measure_idx = np.searchsorted(measure_times, time, "right") - 1 if measure_idx < len(measure_times) - 1: measure_length = ( measure_times[measure_idx + 1] - measure_times[measure_idx] ) else: measure_length = ( measure_times[measure_idx] - measure_times[measure_idx - 1] ) position = ceil( 16 * (time - measure_times[measure_idx]) / measure_length ) return measure_idx, position # Collect events events: List[Tuple[Tuple[int, int], List[str]]] = [] for barline_time in barline_times: events.append( (_get_measure_position(barline_time), [seq.to_bar_event()]) ) for tempo in music.tempos: events.append( ( _get_measure_position(tempo.time), [seq.to_tempo_event(tempo.qpm)], ) ) for note in notes: events.append( ( _get_measure_position(note.time), [ seq.to_note_on_event(note.pitch), seq.to_note_velocity_event(note.velocity), seq.to_note_duration_event(min(note.duration, 32)), ], ) ) # Sort the events by time events.sort(key=itemgetter(0)) # Append the events to the event sequence for event in events: if event[1][0] != "bar": seq.append_event(seq.to_position_event(event[0][1])) seq.extend_events(event[1]) return seq
[docs]def to_remi_event_representation(music: "Music", dtype=int) -> ndarray: """Encode a Music object into the remi event representation.""" seq = to_remi_event_sequence(music) return np.array(seq, dtype=dtype)
def get_indexer(preset=None) -> bidict: """Return a preset indexer.""" if preset is None or preset.lower() == "midi": return get_default_indexer() if preset.lower() == "remi": return get_remi_indexer() if preset.lower() == "performance": return get_performance_indexer() raise ValueError(f"Unknown preset : {preset}")