Source code for madmom.utils.midi

# encoding: utf-8
# pylint: disable=no-member
# pylint: disable=invalid-name
# pylint: disable=too-many-arguments
# pylint: disable=too-few-public-methods
"""
This module contains MIDI functionality, but is deprecated as of version 0.16.
Please use madmom.io.midi instead. This module will be removed in version 0.18.

Almost all code is taken from Giles Hall's python-midi package:
https://github.com/vishnubob/python-midi

It combines the complete package in a single file, to make it easier to
distribute. Most notable changes are `MIDITrack` and `MIDIFile` classes which
handle all data i/o and provide a interface which allows to read/display all
notes as simple numpy arrays. Also, the EventRegistry is handled differently.

The last merged commit is 3053fefe.

Since then the following commits have been added functionality-wise:

- 0964c0b (prevent multiple tick conversions)
- c43bf37 (add pitch and value properties to AfterTouchEvent)
- 40111c6 (add 0x08 MetaEvent: ProgramNameEvent)
- 43de818 (handle unknown MIDI meta events gracefully)

Additionally, the module has been updated to work with Python3.

The MIT License (MIT)
Copyright (c) 2013 Giles F. Hall

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

"""

from __future__ import absolute_import, division, print_function

import sys
import math
import struct
import numpy as np
import warnings


# constants
OCTAVE_MAX_VALUE = 12
OCTAVE_VALUES = list(range(OCTAVE_MAX_VALUE))

NOTE_NAMES = ['C', 'Cs', 'D', 'Ds', 'E', 'F', 'Fs', 'G', 'Gs', 'A', 'As', 'B']
WHITE_KEYS = [0, 2, 4, 5, 7, 9, 11]
BLACK_KEYS = [1, 3, 6, 8, 10]
NOTE_PER_OCTAVE = len(NOTE_NAMES)
NOTE_VALUES = list(range(OCTAVE_MAX_VALUE * NOTE_PER_OCTAVE))
NOTE_NAME_MAP_FLAT = {}
NOTE_VALUE_MAP_FLAT = []
NOTE_NAME_MAP_SHARP = {}
NOTE_VALUE_MAP_SHARP = []

for index in range(128):
    note_idx = index % NOTE_PER_OCTAVE
    oct_idx = index / OCTAVE_MAX_VALUE
    note_name = NOTE_NAMES[note_idx]
    if len(note_name) == 2:
        # sharp note
        flat = NOTE_NAMES[note_idx + 1] + 'b'
        NOTE_NAME_MAP_FLAT['%s_%d' % (flat, oct_idx)] = index
        NOTE_NAME_MAP_SHARP['%s_%d' % (note_name, oct_idx)] = index
        NOTE_VALUE_MAP_FLAT.append('%s_%d' % (flat, oct_idx))
        NOTE_VALUE_MAP_SHARP.append('%s_%d' % (note_name, oct_idx))
        globals()['%s_%d' % (note_name[0] + 's', oct_idx)] = index
        globals()['%s_%d' % (flat, oct_idx)] = index
    else:
        NOTE_NAME_MAP_FLAT['%s_%d' % (note_name, oct_idx)] = index
        NOTE_NAME_MAP_SHARP['%s_%d' % (note_name, oct_idx)] = index
        NOTE_VALUE_MAP_FLAT.append('%s_%d' % (note_name, oct_idx))
        NOTE_VALUE_MAP_SHARP.append('%s_%d' % (note_name, oct_idx))
        globals()['%s_%d' % (note_name, oct_idx)] = index

BEAT_NAMES = ['whole', 'half', 'quarter', 'eighth', 'sixteenth',
              'thirty-second', 'sixty-fourth']
BEAT_VALUES = [4, 2, 1, .5, .25, .125, .0625]
WHOLE = 0
HALF = 1
QUARTER = 2
EIGHTH = 3
SIXTEENTH = 4
THIRTY_SECOND = 5
SIXTY_FOURTH = 6

HEADER_SIZE = 14

RESOLUTION = 480  # ticks per quarter note
TEMPO = 120
TIME_SIGNATURE_NUMERATOR = 4
TIME_SIGNATURE_DENOMINATOR = 4
TIME_SIGNATURE = (TIME_SIGNATURE_NUMERATOR, TIME_SIGNATURE_DENOMINATOR)
SECONDS_PER_QUARTER_NOTE = 60. / TEMPO
SECONDS_PER_TICK = SECONDS_PER_QUARTER_NOTE / RESOLUTION

warnings.warn('Deprecated as of version 0.16. Please use madmom.io.midi '
              'instead. This module will be removed in version 0.18.')

# Ensure Python2/3 compatibility when reading bytes from MIDI files
if sys.version_info[0] == 2:
    int2byte = chr

    def byte2int(byte):
        """Convert a byte-character to an integer."""
        return ord(byte)
else:
    int2byte = struct.Struct(">B").pack

[docs] def byte2int(byte): """Convert a byte-character to an integer.""" return byte
# functions for packing / unpacking variable length data
[docs]def read_variable_length(data): """ Read a variable length variable from the given data. Parameters ---------- data : bytearray Data of variable length. Returns ------- length : int Length in bytes. """ next_byte = 1 value = 0 while next_byte: next_value = byte2int(next(data)) # is the hi-bit set? if not next_value & 0x80: # no next BYTE next_byte = 0 # mask out the 8th bit next_value &= 0x7f # shift last value up 7 bits value <<= 7 # add new value value += next_value return value
[docs]def write_variable_length(value): """ Write a variable length variable. Parameters ---------- value : bytearray Value to be encoded as a variable of variable length. Returns ------- bytearray Variable with variable length. """ result = bytearray() result.insert(0, value & 0x7F) value >>= 7 if value: result.insert(0, (value & 0x7F) | 0x80) value >>= 7 if value: result.insert(0, (value & 0x7F) | 0x80) value >>= 7 if value: result.insert(0, (value & 0x7F) | 0x80) return result
# class for dynamically registering event classes
[docs]class EventRegistry(object): """ Class for registering Events. Event classes should be registered manually by calling EventRegistry.register_event(EventClass) after the class definition. Normal events are registered in the `events` dictionary and use the event's `status_msg` as a key; meta events are registered in the `meta_events` dictionary and use their `meta_command` as key. """ events = {} meta_events = {}
[docs] @classmethod def register_event(cls, event): """ Registers an event in the registry. Parameters ---------- event : :class:`Event` instance Event to be registered. """ # normal events if any(b in (Event, ChannelEvent, NoteEvent) for b in event.__bases__): # raise an error if the event class is registered already if event.status_msg in cls.events: raise AssertionError("Event %s already registered" % event.name) # register the Event cls.events[event.status_msg] = event # meta events elif any(b in (MetaEvent, MetaEventWithText) for b in event.__bases__): # raise an error if the meta event class is registered already if event.meta_command in EventRegistry.meta_events: raise AssertionError("Event %s already registered" % event.name) # register the MetaEvent cls.meta_events[event.meta_command] = event # unknown events else: # raise an error raise AssertionError("Unknown base class in event type: %s" % event.__bases__)
[docs]class Event(object): """ Generic MIDI Event. """ name = "Generic MIDI Event" length = 0 status_msg = 0x0 # sort is a float value used for sorting events occurring at the same tick sort = 0. def __init__(self, **kwargs): if isinstance(self.length, int): data = [0] * self.length else: data = [] self.tick = 0 self.data = data for key in kwargs: setattr(self, key, kwargs[key]) def __eq__(self, other): return ( self.tick == other.tick and self.data == other.data and self.status_msg == other.status_msg) def __ne__(self, other): return not self == other def __lt__(self, other): if self.tick < other.tick: return True elif self.tick == other.tick and self.sort < other.sort: return True return False def __le__(self, other): return NotImplementedError def __gt__(self, other): if self.tick > other.tick: return True elif self.tick == other.tick and self.sort > other.sort: return True return False def __ge__(self, other): return NotImplementedError def __str__(self): return "%s: tick: %s data: %s" % ( self.__class__.__name__, self.tick, self.data)
[docs]class ChannelEvent(Event): """ Event with a channel number. """ name = 'ChannelEvent' def __init__(self, **kwargs): super(ChannelEvent, self).__init__(**kwargs) self.channel = kwargs.get('channel', 0) def __eq__(self, other): return ( self.tick == other.tick and self.channel == other.channel and self.data == other.data and self.status_msg == other.status_msg) def __str__(self): return "%s: tick: %s channel: %s data: %s" % ( self.__class__.__name__, self.tick, self.channel, self.data)
[docs]class NoteEvent(ChannelEvent): """ NoteEvent is a special subclass of Event that is not meant to be used as a concrete class. It defines the generalities of NoteOn and NoteOff events. """ length = 2 def __str__(self): return "%s: tick: %s channel: %s pitch: %s velocity: %s" % ( self.__class__.__name__, self.tick, self.channel, self.pitch, self.velocity) @property def pitch(self): """ Pitch of the note event. """ return self.data[0] @pitch.setter def pitch(self, pitch): """ Set the pitch of the note event. Parameters ---------- pitch : int Pitch of the note. """ self.data[0] = pitch @property def velocity(self): """ Velocity of the note event. """ return self.data[1] @velocity.setter def velocity(self, velocity): """ Set the velocity of the note event. Parameters ---------- velocity : int Velocity of the note. """ self.data[1] = velocity
[docs]class NoteOnEvent(NoteEvent): """ Note On Event. """ status_msg = 0x90 name = 'Note On' sort = .1 # make sure it is sorted before NoteOffEvent
EventRegistry.register_event(NoteOnEvent)
[docs]class NoteOffEvent(NoteEvent): """ Note Off Event. """ status_msg = 0x80 name = 'Note Off' sort = .2 # make sure it is sorted after NoteOnEvent
EventRegistry.register_event(NoteOffEvent)
[docs]class AfterTouchEvent(ChannelEvent): """ After Touch Event. """ status_msg = 0xA0 length = 2 name = 'After Touch' def __str__(self): return "%s: tick: %s channel: %s pitch: %s value: %s" % ( self.__class__.__name__, self.tick, self.channel, self.pitch, self.value) @property def pitch(self): """ Pitch of the after touch event. """ return self.data[0] @pitch.setter def pitch(self, pitch): """ Set the pitch of the after touch event. Parameters ---------- pitch : int Pitch of the after touch event. """ self.data[0] = pitch @property def value(self): """ Value of the after touch event. """ return self.data[1] @value.setter def value(self, value): """ Set the value of the after touch event. Parameters ---------- value : int Value of the after touch event. """ self.data[1] = value
EventRegistry.register_event(AfterTouchEvent)
[docs]class ControlChangeEvent(ChannelEvent): """ Control Change Event. """ status_msg = 0xB0 length = 2 name = 'Control Change' def __str__(self): return "%s: tick: %s channel: %s control: %s value: %s" % ( self.__class__.__name__, self.tick, self.channel, self.control, self.value) @property def control(self): """ Control ID. """ return self.data[0] @control.setter def control(self, control): """ Set control ID. Parameters ---------- control : int Control ID. """ self.data[0] = control @property def value(self): """ Value of the controller. """ return self.data[1] @value.setter def value(self, value): """ Set the value of the controller. Parameters ---------- value : int Value of the controller. """ self.data[1] = value
EventRegistry.register_event(ControlChangeEvent)
[docs]class ProgramChangeEvent(ChannelEvent): """ Program Change Event. """ status_msg = 0xC0 length = 1 name = 'Program Change' def __str__(self): return "%s: tick: %s channel: %s value: %s" % ( self.__class__.__name__, self.tick, self.channel, self.value) @property def value(self): """ Value of the Program Change Event. """ return self.data[0] @value.setter def value(self, value): """ Set the value of the Program Change Event. Parameters ---------- value : int Value of the Program Change Event. """ self.data[0] = value
EventRegistry.register_event(ProgramChangeEvent)
[docs]class ChannelAfterTouchEvent(ChannelEvent): """ Channel After Touch Event. """ status_msg = 0xD0 length = 1 name = 'Channel After Touch' def __str__(self): return "%s: tick: %s channel: %s value: %s" % ( self.__class__.__name__, self.tick, self.channel, self.value) @property def value(self): """ Value of the Channel After Touch Event. """ return self.data[0] @value.setter def value(self, value): """ Set the value of the Channel After Touch Event. Parameters ---------- value : int Value of the Channel After Touch Event. """ self.data[0] = value
EventRegistry.register_event(ChannelAfterTouchEvent)
[docs]class PitchWheelEvent(ChannelEvent): """ Pitch Wheel Event. """ status_msg = 0xE0 length = 2 name = 'Pitch Wheel' @property def pitch(self): """ Pitch of the Pitch Wheel Event. """ return ((self.data[1] << 7) | self.data[0]) - 0x2000 @pitch.setter def pitch(self, pitch): """ Set the pitch of the Pitch Wheel Event. Parameters ---------- pitch : int Pitch of the Pitch Wheel Event. """ value = pitch + 0x2000 self.data[0] = value & 0x7F self.data[1] = (value >> 7) & 0x7F
EventRegistry.register_event(PitchWheelEvent)
[docs]class SysExEvent(Event): """ System Exclusive Event. """ status_msg = 0xF0 length = 'variable' name = 'SysEx'
EventRegistry.register_event(SysExEvent)
[docs]class MetaEvent(Event): """ MetaEvent is a special subclass of Event that is not meant to be used as a concrete class. It defines a subset of Events known as the Meta events. """ status_msg = 0xFF meta_command = 0x0 name = 'Meta Event' def __eq__(self, other): return ( self.tick == other.tick and self.data == other.data and self.status_msg == other.status_msg and self.meta_command == other.meta_command)
[docs]class MetaEventWithText(MetaEvent): """ Meta Event With Text. """ def __init__(self, **kwargs): super(MetaEventWithText, self).__init__(**kwargs) if 'text' not in kwargs: self.text = ''.join(chr(datum) for datum in self.data) def __str__(self): return "%s: %s" % (self.__class__.__name__, self.text)
[docs]class SequenceNumberMetaEvent(MetaEvent): """ Sequence Number Meta Event. """ meta_command = 0x00 length = 2 name = 'Sequence Number'
EventRegistry.register_event(SequenceNumberMetaEvent)
[docs]class TextMetaEvent(MetaEventWithText): """ Text Meta Event. """ meta_command = 0x01 length = 'variable' name = 'Text'
EventRegistry.register_event(TextMetaEvent)
[docs]class CopyrightMetaEvent(MetaEventWithText): """ Copyright Meta Event. """ meta_command = 0x02 length = 'variable' name = 'Copyright Notice'
EventRegistry.register_event(CopyrightMetaEvent)
[docs]class TrackNameEvent(MetaEventWithText): """ Track Name Event. """ meta_command = 0x03 length = 'variable' name = 'Track Name'
EventRegistry.register_event(TrackNameEvent)
[docs]class InstrumentNameEvent(MetaEventWithText): """ Instrument Name Event. """ meta_command = 0x04 length = 'variable' name = 'Instrument Name'
EventRegistry.register_event(InstrumentNameEvent)
[docs]class LyricsEvent(MetaEventWithText): """ Lyrics Event. """ meta_command = 0x05 length = 'variable' name = 'Lyrics'
EventRegistry.register_event(LyricsEvent)
[docs]class MarkerEvent(MetaEventWithText): """ Marker Event. """ meta_command = 0x06 length = 'variable' name = 'Marker'
EventRegistry.register_event(MarkerEvent)
[docs]class CuePointEvent(MetaEventWithText): """ Cue Point Event. """ meta_command = 0x07 length = 'variable' name = 'Cue Point'
EventRegistry.register_event(CuePointEvent)
[docs]class ProgramNameEvent(MetaEventWithText): """ Program Name Event. """ meta_command = 0x08 length = 'variable' name = 'Program Name'
EventRegistry.register_event(ProgramNameEvent)
[docs]class UnknownMetaEvent(MetaEvent): """ Unknown Meta Event. Parameters ---------- meta_command : int Value of the meta command. """ meta_command = None name = 'Unknown' def __init__(self, **kwargs): super(UnknownMetaEvent, self).__init__(**kwargs) # TODO: is this needed, should be handled by Event already self.meta_command = kwargs['meta_command']
EventRegistry.register_event(UnknownMetaEvent)
[docs]class ChannelPrefixEvent(MetaEvent): """ Channel Prefix Event. """ meta_command = 0x20 length = 1 name = 'Channel Prefix'
EventRegistry.register_event(ChannelPrefixEvent)
[docs]class PortEvent(MetaEvent): """ Port Event. """ meta_command = 0x21 name = 'MIDI Port/Cable'
EventRegistry.register_event(PortEvent)
[docs]class TrackLoopEvent(MetaEvent): """ Track Loop Event. """ meta_command = 0x2E name = 'Track Loop'
EventRegistry.register_event(TrackLoopEvent)
[docs]class EndOfTrackEvent(MetaEvent): """ End Of Track Event. """ meta_command = 0x2F name = 'End of Track' sort = .99 # should always come last
EventRegistry.register_event(EndOfTrackEvent)
[docs]class SetTempoEvent(MetaEvent): """ Set Tempo Event. """ meta_command = 0x51 length = 3 name = 'Set Tempo' def __str__(self): return "%s: tick: %s microseconds per quarter note: %s" % ( self.__class__.__name__, self.tick, self.microseconds_per_quarter_note) @property def microseconds_per_quarter_note(self): """ Microseconds per quarter note. """ assert len(self.data) == 3 values = [self.data[x] << (16 - (8 * x)) for x in range(3)] return sum(values) @microseconds_per_quarter_note.setter def microseconds_per_quarter_note(self, microseconds): """ Set microseconds per quarter note. Parameters ---------- microseconds : int Microseconds per quarter note. """ self.data = [(microseconds >> (16 - (8 * x)) & 0xFF) for x in range(3)]
EventRegistry.register_event(SetTempoEvent)
[docs]class SmpteOffsetEvent(MetaEvent): """ SMPTE Offset Event. """ meta_command = 0x54 name = 'SMPTE Offset'
EventRegistry.register_event(SmpteOffsetEvent)
[docs]class TimeSignatureEvent(MetaEvent): """ Time Signature Event. """ meta_command = 0x58 length = 4 name = 'Time Signature' @property def numerator(self): """ Numerator of the time signature. """ return self.data[0] @numerator.setter def numerator(self, numerator): """ Set numerator of the time signature. Parameters ---------- numerator : int Numerator of the time signature. """ self.data[0] = numerator @property def denominator(self): """ Denominator of the time signature. """ return 2 ** self.data[1] @denominator.setter def denominator(self, denominator): """ Set denominator of the time signature. Parameters ---------- denominator : int Denominator of the time signature. """ self.data[1] = int(math.log(denominator, 2)) @property def metronome(self): """ Metronome. """ return self.data[2] @metronome.setter def metronome(self, metronome): """ Set metronome of the time signature. Parameters ---------- metronome : int Metronome of the time signature. """ self.data[2] = metronome @property def thirty_seconds(self): """ Thirty-seconds of the time signature. """ return self.data[3] @thirty_seconds.setter def thirty_seconds(self, thirty_seconds): """ Set thirty-seconds of the time signature. Parameters ---------- thirty_seconds : int Thirty-seconds of the time signature. """ self.data[3] = thirty_seconds
EventRegistry.register_event(TimeSignatureEvent)
[docs]class KeySignatureEvent(MetaEvent): """ Key Signature Event. """ meta_command = 0x59 length = 2 name = 'Key Signature' @property def alternatives(self): """ Alternatives of the key signature. """ return self.data[0] - 256 if self.data[0] > 127 else self.data[0] @alternatives.setter def alternatives(self, alternatives): """ Set alternatives of the key signature. Parameters ---------- alternatives : int Alternatives of the key signature. """ self.data[0] = 256 + alternatives if alternatives < 0 else alternatives @property def minor(self): """ Major / minor. """ return self.data[1] @minor.setter def minor(self, val): """ Set major / minor. Parameters ---------- val : int Major / minor. """ self.data[1] = val
EventRegistry.register_event(KeySignatureEvent)
[docs]class SequencerSpecificEvent(MetaEvent): """ Sequencer Specific Event. """ meta_command = 0x7F name = 'Sequencer Specific'
EventRegistry.register_event(SequencerSpecificEvent) def _add_channel(notes, channel=0): """ Adds a default channel to the notes if missing. Parameters ---------- notes : numpy array, shape (num_notes, 2) Notes, one per row (column definition see notes). channel : int, optional Note channel if not defined by `notes`. Returns ------- numpy array Notes (including note channel). Notes ----- The note columns format must be (channel being optional): 'onset' 'pitch' 'duration' 'velocity' ['channel'] """ if not notes.ndim == 2: raise ValueError('unknown format for `notes`') rows, columns = notes.shape if columns == 5: return notes elif columns == 4: channels = np.ones((rows, 1)) * channel return np.hstack((notes, channels)) raise ValueError('unable to handle `notes` with %d columns' % columns) # MIDI Track
[docs]class MIDITrack(object): """ MIDI Track. Parameters ---------- events : list MIDI events. Notes ----- All events are stored with timing information in absolute ticks. The events must be sorted. Consider using `from_notes()` method. Examples -------- Create a MIDI track from a list of events. Please note that the events must be sorted. >>> e1 = NoteOnEvent(tick=100, pitch=50, velocity=60) >>> e2 = NoteOffEvent(tick=300, pitch=50) >>> e3 = NoteOnEvent(tick=200, pitch=62, velocity=90) >>> e4 = NoteOffEvent(tick=600, pitch=62) >>> t = MIDITrack(sorted([e1, e2, e3, e4])) >>> t # doctest: +ELLIPSIS <madmom.utils.midi.MIDITrack object at 0x...> >>> t.events # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS [<madmom.utils.midi.NoteOnEvent object at 0x...>, <madmom.utils.midi.NoteOnEvent object at 0x...>, <madmom.utils.midi.NoteOffEvent object at 0x...>, <madmom.utils.midi.NoteOffEvent object at 0x...>] It can also be created from an array containing the notes. The `from_notes` method also takes care of creating tempo and time signature events. >>> notes = np.array([[0.1, 50, 0.3, 60], [0.2, 62, 0.4, 90]]) >>> t = MIDITrack.from_notes(notes) >>> t # doctest: +ELLIPSIS <madmom.utils.midi.MIDITrack object at 0x...> >>> t.events # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS [<madmom.utils.midi.SetTempoEvent object at 0x...>, <madmom.utils.midi.TimeSignatureEvent object at 0...>, <madmom.utils.midi.NoteOnEvent object at 0x...>, <madmom.utils.midi.NoteOnEvent object at 0x...>, <madmom.utils.midi.NoteOffEvent object at 0x...>, <madmom.utils.midi.NoteOffEvent object at 0x...>] """ def __init__(self, events=None): if events is None: self.events = [] else: # do not sort the events, since they can have relative timing! self.events = events def _make_ticks_abs(self): """Make the track's events timing information absolute.""" running_tick = 0 for event in self.events: event.tick += running_tick running_tick = event.tick def _make_ticks_rel(self): """Make the track's events timing information relative.""" running_tick = 0 for event in self.events: event.tick -= running_tick running_tick += event.tick @property def data_stream(self): """ MIDI data stream representation of the track. """ # sort the events self.events.sort() # first make sure the timing information is relative self._make_ticks_rel() # and unset the status message status = None # then encode all events of the track track_data = bytearray() for event in self.events: # encode the event data, first the timing information track_data.extend(write_variable_length(event.tick)) # is the event a MetaEvent? if isinstance(event, MetaEvent): track_data.append(event.status_msg) track_data.append(event.meta_command) track_data.extend(write_variable_length(len(event.data))) track_data.extend(event.data) # is this event a SysEx Event? elif isinstance(event, SysExEvent): track_data.append(0xF0) track_data.extend(event.data) track_data.append(0xF7) # not a meta or SysEx event, must be a general message elif isinstance(event, Event): if not status or status.status_msg != event.status_msg or \ status.channel != event.channel: status = event track_data.append(event.status_msg | event.channel) track_data.extend(event.data) else: raise ValueError("Unknown MIDI Event: " + str(event)) # TODO: should we add a EndOfTrackEvent? # convert events back to absolute ticks self._make_ticks_abs() # prepare the data data = bytearray() # generate a MIDI header data.extend(b'MTrk') data.extend(struct.pack(">L", len(track_data))) # append the track data data.extend(track_data) # return the track data return data
[docs] @classmethod def from_stream(cls, midi_stream): """ Create a MIDI track by reading the data from a stream. Parameters ---------- midi_stream : open file handle MIDI file stream (e.g. open MIDI file handle) Returns ------- :class:`MIDITrack` instance :class:`MIDITrack` instance """ events = [] # reset the status status = None # first four bytes are Track header chunk = midi_stream.read(4) if chunk != b'MTrk': raise TypeError("Bad track header in MIDI file: %s" % chunk) # next four bytes are track size track_size = struct.unpack(">L", midi_stream.read(4))[0] track_data = iter(midi_stream.read(track_size)) # read in all events while True: try: # first datum is variable length representing the delta-time tick = read_variable_length(track_data) # next byte is status message status_msg = byte2int(next(track_data)) # is the event a MetaEvent? if MetaEvent.status_msg == status_msg: meta_cmd = byte2int(next(track_data)) if meta_cmd not in EventRegistry.meta_events: import warnings warnings.warn("Unknown Meta MIDI Event: %s" % meta_cmd) event_cls = UnknownMetaEvent else: event_cls = EventRegistry.meta_events[meta_cmd] data_len = read_variable_length(track_data) data = [byte2int(next(track_data)) for _ in range(data_len)] # create an event and append it to the list events.append(event_cls(tick=tick, data=data, meta_command=meta_cmd)) # is this event a SysEx Event? elif SysExEvent.status_msg == status_msg: data = [] while True: datum = byte2int(next(track_data)) if datum == 0xF7: break data.append(datum) # create an event and append it to the list events.append(SysExEvent(tick=tick, data=data)) # not a meta or SysEx event, must be a general MIDI event else: key = status_msg & 0xF0 if key not in EventRegistry.events: assert status, "Bad byte value" data = [] key = status & 0xF0 event_cls = EventRegistry.events[key] channel = status & 0x0F data.append(status_msg) data += [byte2int(next(track_data)) for _ in range(event_cls.length - 1)] # create an event and append it to the list events.append(event_cls(tick=tick, channel=channel, data=data)) else: status = status_msg event_cls = EventRegistry.events[key] channel = status & 0x0F data = [byte2int(next(track_data)) for _ in range(event_cls.length)] # create an event and append it to the list events.append(event_cls(tick=tick, channel=channel, data=data)) # no more events to be processed except StopIteration: break # create a new track track = cls(events) # make the timing of the events (i.e. the ticks) absolute track._make_ticks_abs() # return this track return track
[docs] @classmethod def from_notes(cls, notes, tempo=TEMPO, time_signature=TIME_SIGNATURE, resolution=RESOLUTION): """ Create a MIDI track from the given notes. Parameters ---------- notes : numpy array Array with the notes, one per row. The columns must be: (onset time, pitch, duration, velocity, [channel]). tempo : float, optional Tempo of the MIDI track, given in beats per minute (bpm). time_signature : tuple, optional Time signature of the track, e.g. (4, 4) for 4/4. resolution : int Resolution (i.e. ticks per quarter note) of the MIDI track. Returns ------- :class:`MIDITrack` instance :class:`MIDITrack` instance Notes ----- All events including the generated tempo and time signature events is included in the returned track (i.e. as defined in MIDI format 0). """ # add a default channel if needed notes = _add_channel(notes) # set time signature sig = TimeSignatureEvent(tick=0) sig.numerator, sig.denominator = time_signature # length of a quarter note (seconds) quarter_note_length = 60. / tempo * sig.denominator / 4 # quarter notes per second quarter_notes_per_second = 1 / quarter_note_length # ticks per second ticks_per_second = resolution * quarter_notes_per_second # set tempo tempo = SetTempoEvent(tick=0) tempo.microseconds_per_quarter_note = int(quarter_note_length * 1e6) # list for events (ticks in absolute timing) events = [] # add the notes for note in notes: onset, pitch, duration, velocity, channel = note # add NoteOn e_on = NoteOnEvent() e_on.tick = int(onset * ticks_per_second) e_on.pitch = int(pitch) e_on.velocity = int(velocity) e_on.channel = int(channel) # and NoteOff e_off = NoteOffEvent() e_off.tick = int((onset + duration) * ticks_per_second) e_off.pitch = int(pitch) e_off.channel = int(channel) events.append(e_on) events.append(e_off) # sort the events and prepend the tempo and time signature events events = sorted(events) events.insert(0, sig) events.insert(0, tempo) # create a track from the events return cls(events)
# File I/O classes
[docs]class MIDIFile(object): """ MIDI File. Parameters ---------- tracks : list List of :class:`MIDITrack` instances. resolution : int, optional Resolution (i.e. microseconds per quarter note). file_format : int, optional Format of the MIDI file. Notes ----- Writing a MIDI file assumes a tempo of 120 beats per minute (bpm) and a 4/4 time signature and writes all events into a single track (i.e. MIDI format 0). Examples -------- Create a MIDI file from an array with notes. The format of the note array is: 'onset time', 'pitch', 'duration', 'velocity', 'channel'. The last column can be omitted, assuming channel 0. >>> notes = np.array([[0, 50, 1, 60], [0.5, 62, 0.5, 90]]) >>> m = MIDIFile.from_notes(notes) >>> m # doctest: +ELLIPSIS <madmom.utils.midi.MIDIFile object at 0x...> The notes can be accessed as a numpy array in various formats (default is seconds): >>> m.notes() array([[ 0. , 50. , 1. , 60. , 0. ], [ 0.5, 62. , 0.5, 90. , 0. ]]) >>> m.notes(unit='ticks') array([[ 0., 50., 960., 60., 0.], [480., 62., 480., 90., 0.]]) >>> m.notes(unit='beats') array([[ 0., 50., 2., 60., 0.], [ 1., 62., 1., 90., 0.]]) >>> m = MIDIFile.from_notes(notes, tempo=60) >>> m.notes(unit='ticks') array([[ 0., 50., 480., 60., 0.], [240., 62., 240., 90., 0.]]) >>> m.notes(unit='beats') array([[ 0. , 50. , 1. , 60. , 0. ], [ 0.5, 62. , 0.5, 90. , 0. ]]) >>> m = MIDIFile.from_notes(notes, tempo=60, time_signature=(2, 2)) >>> m.notes(unit='ticks') array([[ 0., 50., 960., 60., 0.], [480., 62., 480., 90., 0.]]) >>> m.notes(unit='beats') array([[ 0. , 50. , 1. , 60. , 0. ], [ 0.5, 62. , 0.5, 90. , 0. ]]) >>> m = MIDIFile.from_notes(notes, tempo=240, time_signature=(3, 8)) >>> m.notes(unit='ticks') array([[ 0., 50., 960., 60., 0.], [480., 62., 480., 90., 0.]]) >>> m.notes(unit='beats') array([[ 0., 50., 4., 60., 0.], [ 2., 62., 2., 90., 0.]]) """ def __init__(self, tracks=None, resolution=RESOLUTION, file_format=0): # init variables if tracks is None: self.tracks = [] elif isinstance(tracks, MIDITrack): self.tracks = [tracks] elif isinstance(tracks, list): # TODO: test if the items of the list are of type MIDITrack self.tracks = tracks else: raise ValueError('file_format of `tracks` not supported.') self.resolution = resolution # i.e. ticks per quarter note # format 0 stores all information in 1 track # format 1 has multiple tracks but plays them back simultaneously # TODO: format 2 has multiple tracks but plays them back one after # another. This type is not supported (yet). if file_format > 1: raise ValueError('Only MIDI file formats 0 and 1 supported.') self.format = file_format @property def ticks_per_quarter_note(self): """ Number of ticks per quarter note. """ return self.resolution
[docs] def tempi(self, suppress_warnings=False): """ Tempi of the MIDI file. Returns ------- tempi : numpy array Array with tempi (tick, seconds per tick, cumulative time). """ if not suppress_warnings: import warnings warnings.warn('this method will be removed soon, do not rely on ' 'its output, rather fix issue #192 ;)') # create an empty tempo list tempo_events = [] for i, track in enumerate(self.tracks): # get a list with tempo events track_tempo_events = [e for e in track.events if isinstance(e, SetTempoEvent)] # tempo events should be only in the first track of a MIDI file if track_tempo_events and i > 0: raise ValueError('SetTempoEvents should be only in the first ' 'track of a MIDI file.') tempo_events.extend(track_tempo_events) # convert to desired format (tick, microseconds per tick) tempi = [(e.tick, e.microseconds_per_quarter_note / (1e6 * self.resolution)) for e in tempo_events] # make sure a tempo is set and the first tempo occurs at tick 0 if not tempi or tempi[0][0] > 0: tempi.insert(0, (0, SECONDS_PER_TICK)) # sort (just to be sure) tempi.sort() # re-iterate over the list to calculate the cumulative time for i, _ in enumerate(tempi): if i == 0: tempi[i] = (tempi[i][0], tempi[i][1], 0) else: ticks = tempi[i][0] - tempi[i - 1][0] cum_time = tempi[i - 1][2] + ticks * tempi[i - 1][1] tempi[i] = (tempi[i][0], tempi[i][1], cum_time) # return tempo return np.asarray(tempi, np.float)
[docs] def time_signatures(self, suppress_warnings=False): """ Time signatures of the MIDI file. Returns ------- time_signatures : numpy array Array with time signatures (tick, numerator, denominator). """ if not suppress_warnings: import warnings warnings.warn('this method will be removed soon, do not rely on ' 'its output, rather fix issue #192 ;)') signatures = None for track in self.tracks: # get a list with time signature events time_signature_events = [e for e in track.events if isinstance(e, TimeSignatureEvent)] if signatures is None and len(time_signature_events) > 0: # convert to desired format signatures = [(e.tick, e.numerator, e.denominator) for e in time_signature_events] elif signatures is not None and len(time_signature_events) > 0: # time signature events should be contained only in the first # track of a MIDI file, thus raise an error raise ValueError('TimeSignatureEvent should be only in the ' 'first track of a MIDI file.') # make sure a time signature is set and the first one occurs at tick 0 if signatures is None: signatures = [(0, TIME_SIGNATURE)] if signatures[0][0] > 0: signatures.insert(0, (0, TIME_SIGNATURE)) # return time signatures return np.asarray(signatures, dtype=np.float)
[docs] def notes(self, unit='s'): """ Notes of the MIDI file. Parameters ---------- unit : {'s', 'seconds', 'b', 'beats', 't', 'ticks'} Time unit for notes, seconds ('s') beats ('b') or ticks ('t') Returns ------- notes : numpy array Array with notes (onset time, pitch, duration, velocity, channel). """ # list for all notes notes = [] # dictionary for storing the last onset and velocity for each # individual note (i.e. same pitch and channel) sounding_notes = {} # as key for the dict use channel * 128 (max number of pitches) + pitch def note_hash(channel, pitch): """Generate a note hash.""" return channel * 128 + pitch for track in self.tracks: # get a list with note events note_events = [e for e in track.events if isinstance(e, NoteEvent)] # process all events tick = 0 for e in note_events: if tick > e.tick: raise AssertionError('note events must be sorted!') n = note_hash(e.channel, e.pitch) is_note_on = isinstance(e, NoteOnEvent) is_note_off = isinstance(e, NoteOffEvent) # if it's a note on event with a velocity > 0, if is_note_on and e.velocity > 0: # save the onset time and velocity sounding_notes[n] = (e.tick, e.velocity) # if it's a note off event or a note on with a velocity of 0, elif is_note_off or (is_note_on and e.velocity == 0): if n not in sounding_notes: import warnings warnings.warn("ignoring %s" % e) continue if sounding_notes[n][0] > e.tick: raise AssertionError('note duration must be positive') if sounding_notes[n][1] <= 0: raise AssertionError('note velocity must be positive') # append the note to the list notes.append((sounding_notes[n][0], e.pitch, e.tick - sounding_notes[n][0], sounding_notes[n][1], e.channel)) # remove hash from dict del sounding_notes[n] else: raise TypeError('unexpected NoteEvent', e) tick = e.tick # sort the notes and convert to numpy array notes = np.asarray(sorted(notes), dtype=np.float) # convert onset times and durations from ticks to the requested unit # and return the notes if unit.lower() in ('t', 'ticks'): return notes elif unit.lower() in ('s', 'seconds'): return self._notes_in_seconds(notes) elif unit.lower() in ('b', 'beats'): return self._notes_in_beats(notes) else: raise ValueError("`unit` must be either 'seconds', 's', 'beats', " "'b', 'ticks', or 't' not %s." % unit)
def _notes_in_beats(self, notes): """ Converts onsets and offsets of notes from ticks to beats. Parameters ---------- notes : numpy array or list of tuples Notes (onset, pitch, offset, velocity). Returns ------- notes : numpy array Notes with onsets and offsets in beats. """ tpq = self.ticks_per_quarter_note time_signatures = self.time_signatures(suppress_warnings=True) # change the second column of time_signatures to beat position of the # signature change, the first column is now the tick position, the # second column the beat position and the third column the new beat # unit after the signature change time_signatures[0, 1] = 0 # quarter notes between time signature changes qnbtsc = np.diff(time_signatures[:, 0]) / tpq # beats between time signature changes bbtsc = qnbtsc * (time_signatures[:-1, 2] / 4.0) # compute beat position of each time signature change time_signatures[1:, 1] = bbtsc.cumsum() # iterate over all notes for note in notes: onset, _, offset, _, _ = note # get info about last time signature change tsc = time_signatures[np.argmax(time_signatures[:, 0] > onset) - 1] # adjust onset onset_ticks_since_tsc = onset - tsc[0] note[0] = tsc[1] + (onset_ticks_since_tsc / tpq) * (tsc[2] / 4.) # adjust offsets offset_ticks_since_tsc = offset - tsc[0] note[2] = tsc[1] + (offset_ticks_since_tsc / tpq) * (tsc[2] / 4.) # return notes return notes def _notes_in_seconds(self, notes): """ Converts onsets and offsets of notes from ticks to seconds. Parameters ---------- notes : numpy array or list of tuples Notes (onset, pitch, offset, velocity). Returns ------- notes : numpy array Notes with onset and offset times in seconds. """ # cache tempo tempi = self.tempi(suppress_warnings=True) # iterate over all notes for note in notes: onset, _, offset, _, _ = note # get last tempo for the onset and offset t_on = tempi[np.argmax(tempi[:, 0] > onset) - 1] t_off = tempi[np.argmax(tempi[:, 0] > offset) - 1] # adjust the note onset and offset note[0] = (onset - t_on[0]) * t_on[1] + t_on[2] note[2] = (offset - t_off[0]) * t_off[1] + t_off[2] # return notes return notes # methods for writing MIDI stuff @property def data_stream(self): """ MIDI data stream representation of the MIDI file. """ # prepare data data = bytearray() # generate a MIDI header data.extend(b'MThd') data.extend(struct.pack(">LHHH", 6, self.format, len(self.tracks), self.resolution)) # append the tracks for track in self.tracks: data.extend(track.data_stream) # return the data return data
[docs] def write(self, midi_file): """ Write a MIDI file. Parameters ---------- midi_file : str The MIDI file name. """ # if we get a filename, open the file if not hasattr(midi_file, 'write'): midi_file = open(midi_file, 'wb') # write the MIDI stream midi_file.write(self.data_stream) # close the file midi_file.close()
[docs] @classmethod def from_file(cls, midi_file): """ Create a MIDI file instance from a .mid file. Parameters ---------- midi_file : str Name of the .mid file to load. Returns ------- :class:`MIDIFile` instance :class:`MIDIFile` instance """ tracks = [] resolution = None midi_format = None with open(midi_file, 'rb') as midi_file: # read in file header # first four bytes are MIDI header chunk = midi_file.read(4) if chunk != b'MThd': raise TypeError("Bad header in MIDI file: %s", chunk) # next four bytes are header size # next two bytes specify the format version # next two bytes specify the number of tracks # next two bytes specify the resolution/PPQ/Parts Per Quarter # (in other words, how many ticks per quarter note) data = struct.unpack(">LHHH", midi_file.read(10)) header_size = data[0] midi_format = data[1] num_tracks = data[2] resolution = data[3] # if the top bit of the resolution word is 0, the following 15 bits # describe the time division in ticks per beat if resolution & 0x8000 == 0: resolution = resolution # otherwise the following 15 bits describe the time division in # frames per second else: # from http://www.sonicspot.com/guide/midifiles.html: # Frames per second is defined by breaking the remaining 15 # bytes into two values. The top 7 bits (bit mask 0x7F00) # define a value for the number of SMPTE frames and can be # 24, 25, 29 (for 29.97 fps) or 30. The remaining byte # (bit mask 0x00FF) defines how many clock ticks or track delta # positions there are per frame. So a time division example of # 0x9978 could be broken down into it's three parts: the top # bit is one, so it is in SMPTE frames per second format, the # following 7 bits have a value of 25 (0x19) and the bottom # byte has a value of 120 (0x78). This means the example plays # at 24(?) frames per second SMPTE time and has 120 ticks per # frame. raise NotImplementedError("SMPTE resolution not implemented.") # skip the remaining part of the header if header_size > HEADER_SIZE: midi_file.read(header_size - HEADER_SIZE) # read in all tracks for _ in range(num_tracks): # read in one track and append it to the tracks list track = MIDITrack.from_stream(midi_file) tracks.append(track) if resolution is None or midi_format is None: raise IOError('unable to read MIDI file %s.' % midi_file) # return a newly created object return cls(tracks=tracks, resolution=resolution, file_format=midi_format)
[docs] @classmethod def from_notes(cls, notes, tempo=TEMPO, time_signature=TIME_SIGNATURE, resolution=RESOLUTION): """ Create a MIDIFile from the given notes. Parameters ---------- notes : numpy array Array with the notes, one per row. The columns must be: (onset time, pitch, duration, velocity, [channel]). tempo : float, optional Tempo of the MIDI track, given in beats per minute (bpm). time_signature : tuple, optional Time signature of the track, e.g. (4, 4) for 4/4. resolution : int Resolution (i.e. ticks per quarter note) of the MIDI track. Returns ------- :class:`MIDIFile` instance :class:`MIDIFile` instance with all notes collected in one track. Notes ----- All note events (including the generated tempo and time signature events) are written into a single track (i.e. MIDI file format 0). """ # create a new track from the notes and then a MIDIFile instance return cls(MIDITrack.from_notes(notes, tempo, time_signature, resolution))
[docs] @staticmethod def add_arguments(parser, length=None, velocity=None, channel=None): """ Add MIDI related arguments to an existing parser object. Parameters ---------- parser : argparse parser instance Existing argparse parser object. length : float, optional Default length of the notes [seconds]. velocity : int, optional Default velocity of the notes. channel : int, optional Default channel of the notes. Returns ------- argparse argument group MIDI argument parser group object. """ # add MIDI related options to the existing parser g = parser.add_argument_group('MIDI arguments') g.add_argument('--midi', action='store_true', help='save as MIDI') if length is not None: g.add_argument('--note_length', action='store', type=float, default=length, help='set the note length [default=%(default).2f]') if velocity is not None: g.add_argument('--note_velocity', action='store', type=int, default=velocity, help='set the note velocity [default=%(default)i]') if channel is not None: g.add_argument('--note_channel', action='store', type=int, default=channel, help='set the note channel [default=%(default)i]') # return the argument group so it can be modified if needed return g
[docs]def process_notes(data, output=None): """ This is a simple processing function. It either loads the notes from a MIDI file and or writes the notes to a file. The behaviour depends on the presence of the `output` argument, if 'None' is given, the notes are read, otherwise the notes are written to file. Parameters ---------- data : str or numpy array MIDI file to be loaded (if `output` is 'None') / notes to be written. output : str, optional Output file name. If set, the notes given by `data` are written. Returns ------- notes : numpy array Notes read/written. """ if output is None: # load the notes return MIDIFile.from_file(data).notes() MIDIFile.from_notes(data).write(output) return data