diff --git a/pretty_midi/__init__.py b/pretty_midi/__init__.py index f4dd3f6..2fa348b 100644 --- a/pretty_midi/__init__.py +++ b/pretty_midi/__init__.py @@ -116,6 +116,13 @@ :members: :undoc-members: +``pretty_midi.Text`` +===================== + +.. autoclass:: Text + :members: + :undoc-members: + Utility functions ================= .. autofunction:: key_number_to_key_name diff --git a/pretty_midi/containers.py b/pretty_midi/containers.py index cc51ff0..8e61904 100644 --- a/pretty_midi/containers.py +++ b/pretty_midi/containers.py @@ -199,3 +199,25 @@ def __repr__(self): def __str__(self): return '"{}" at {:.2f} seconds'.format(self.text, self.time) + +class Text(object): + """Timestamped text event. + + Attributes + ---------- + text : str + The text. + time : float + The time it occurs in seconds. + """ + + def __init__(self, text, time): + self.text = text + self.time = time + + def __repr__(self): + return 'Text(text="{}", time={})'.format( + self.text.replace('"', r'\"'), self.time) + + def __str__(self): + return '"{}" at {:.2f} seconds'.format(self.text, self.time) diff --git a/pretty_midi/pretty_midi.py b/pretty_midi/pretty_midi.py index c143594..9d65daa 100644 --- a/pretty_midi/pretty_midi.py +++ b/pretty_midi/pretty_midi.py @@ -12,10 +12,11 @@ import copy import functools import six +from heapq import merge from .instrument import Instrument from .containers import (KeySignature, TimeSignature, Lyric, Note, - PitchBend, ControlChange) + PitchBend, ControlChange, Text) from .utilities import (key_name_to_key_number, qpm_to_bpm) # The largest we'd ever expect a tick to be @@ -46,6 +47,8 @@ class PrettyMIDI(object): List of :class:`pretty_midi.TimeSignature` objects. lyrics : list List of :class:`pretty_midi.Lyric` objects. + text_events : list + List of :class:`pretty_midi.Text` objects. """ def __init__(self, midi_file=None, resolution=220, initial_tempo=120.): @@ -118,6 +121,8 @@ def __init__(self, midi_file=None, resolution=220, initial_tempo=120.): self.time_signature_changes = [] # Empty lyrics list self.lyrics = [] + # Empty text events list + self.text_events = [] def _load_tempo_changes(self, midi_data): """Populates ``self._tick_scales`` with tuples of @@ -157,7 +162,8 @@ def _load_tempo_changes(self, midi_data): def _load_metadata(self, midi_data): """Populates ``self.time_signature_changes`` with ``TimeSignature`` objects, ``self.key_signature_changes`` with ``KeySignature`` objects, - and ``self.lyrics`` with ``Lyric`` objects. + ``self.lyrics`` with ``Lyric`` objects and ``self.text_events`` with + ``Text`` objects. Parameters ---------- @@ -170,6 +176,7 @@ def _load_metadata(self, midi_data): self.key_signature_changes = [] self.time_signature_changes = [] self.lyrics = [] + self.text_events = [] for event in midi_data.tracks[0]: if event.type == 'key_signature': @@ -184,9 +191,31 @@ def _load_metadata(self, midi_data): self.__tick_to_time[event.time]) self.time_signature_changes.append(ts_obj) - elif event.type == 'lyrics': - self.lyrics.append(Lyric( - event.text, self.__tick_to_time[event.time])) + # We search for lyrics and text events on all tracks + # Lists of lyrics and text events lists, for every track + tracks_with_lyrics = [] + tracks_with_text_events = [] + for track in midi_data.tracks: + # Track specific lists that get appended if not empty + lyrics = [] + text_events = [] + for event in track: + if event.type == 'lyrics': + lyrics.append(Lyric( + event.text, self.__tick_to_time[event.time])) + elif event.type == 'text': + text_events.append(Text( + event.text, self.__tick_to_time[event.time])) + + if lyrics: + tracks_with_lyrics.append(lyrics) + if text_events: + tracks_with_text_events.append(text_events) + + # We merge the already sorted lists for every track, based on time + self.lyrics = list(merge(*tracks_with_lyrics, key=lambda x: x.time)) + self.text_events = list(merge(*tracks_with_text_events, key=lambda x: x.time)) + def _update_tick_to_time(self, max_tick): """Creates ``self.__tick_to_time``, a class member array which maps @@ -421,7 +450,7 @@ def get_end_time(self): """ # Get end times from all instruments, and times of all meta-events meta_events = [self.time_signature_changes, self.key_signature_changes, - self.lyrics] + self.lyrics, self.text_events] times = ([i.get_end_time() for i in self.instruments] + [e.time for m in meta_events for e in m] + self.get_tempo_changes()[0].tolist()) @@ -1130,6 +1159,8 @@ def adjust_meta(events): adjust_meta(self.key_signature_changes) # Adjust lyrics adjust_meta(self.lyrics) + # Adjust text events + adjust_meta(self.text_events) # Remove all downbeats which appear before the start of original_times original_downbeats = original_downbeats[ @@ -1276,14 +1307,15 @@ def event_compare(event1, event2): 'time_signature': lambda e: (2 * 256 * 256), 'key_signature': lambda e: (3 * 256 * 256), 'lyrics': lambda e: (4 * 256 * 256), - 'program_change': lambda e: (5 * 256 * 256), - 'pitchwheel': lambda e: ((6 * 256 * 256) + e.pitch), + 'text_events' :lambda e: (5 * 256 * 256), + 'program_change': lambda e: (6 * 256 * 256), + 'pitchwheel': lambda e: ((7 * 256 * 256) + e.pitch), 'control_change': lambda e: ( - (7 * 256 * 256) + (e.control * 256) + e.value), - 'note_off': lambda e: ((8 * 256 * 256) + (e.note * 256)), + (8 * 256 * 256) + (e.control * 256) + e.value), + 'note_off': lambda e: ((9 * 256 * 256) + (e.note * 256)), 'note_on': lambda e: ( - (9 * 256 * 256) + (e.note * 256) + e.velocity), - 'end_of_track': lambda e: (10 * 256 * 256) + (10 * 256 * 256) + (e.note * 256) + e.velocity), + 'end_of_track': lambda e: (11 * 256 * 256) } # If the events have the same tick, and both events have types # which appear in the secondary_sort dictionary, use the dictionary @@ -1335,6 +1367,10 @@ def event_compare(event1, event2): for l in self.lyrics: timing_track.append(mido.MetaMessage( 'lyrics', time=self.time_to_tick(l.time), text=l.text)) + # Add text events + for l in self.text_events: + timing_track.append(mido.MetaMessage( + 'text', time=self.time_to_tick(l.time), text=l.text)) # Sort the (absolute-tick-timed) events. timing_track.sort(key=functools.cmp_to_key(event_compare)) # Add in an end of track event