Files

copied
Last update 6 years 1 month by Olivier Gillet
Filestoolsmidi
..
__init__.py
midifile.py
midifile.py
#!/usr/bin/python2.5 # # Copyright 2009 Olivier Gillet. # # Author: Olivier Gillet (ol.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # # ----------------------------------------------------------------------------- # # Midifile Writer and Reader. """Midifile writer. """ import bisect import math import struct def PackInteger(value, size=4): """Packs a python integer into a n-byte big endian byte sequence.""" return struct.pack('>%s' % {1: 'B', 2: 'H', 4: 'L'}[size], value) def UnpackInteger(value, size=4): """Packs a python integer into a n-byte big endian byte sequence.""" return struct.unpack('>%s' % {1: 'B', 2: 'H', 4: 'L'}[size], value)[0] def PackVariableLengthInteger(value): """Packs a python integer into a variable length byte sequence.""" if value == 0: return '\x00' s = value output = [] while value: to_write = value & 0x7f value = value >> 7 output.insert(0, to_write) for i in xrange(len(output) - 1): output[i] |= 0x80 output = ''.join(map(chr, output)) return output """Classes representing a MIDI event, with a Serialize method which encodes them into a string.""" class Event(object): def __init__(self): pass def Serialize(self, running_status): raise NotImplementedError class MetaEvent(Event): def __init__(self, id, data): assert len(data) < 256 self.id = id self.data = data def Serialize(self, running_status): return ''.join([ '\xff', PackInteger(self.id, size=1), PackInteger(len(self.data), size=1), self.data]), None class TextEvent(MetaEvent): def __init__(self, text): self.text = text super(TextEvent, self).__init__(0x01, text) class CopyrightInfoEvent(MetaEvent): def __init__(self, text): self.text = text super(CopyrightInfoEvent, self).__init__(0x02, text) class TrackNameEvent(MetaEvent): def __init__(self, text): self.text = text super(TrackNameEvent, self).__init__(0x03, text) class TrackInstrumentNameEvent(MetaEvent): def __init__(self, text): self.text = text super(TrackInstrumentNameEvent, self).__init__(0x04, text) class LyricEvent(MetaEvent): def __init__(self, text): self.text = text super(LyricEvent, self).__init__(0x05, text) class MarkerEvent(MetaEvent): def __init__(self, text): self.text = text super(MarkerEvent, self).__init__(0x06, text) class CuePointEvent(MetaEvent): def __init__(self, text): self.text = text super(CuePointEvent, self).__init__(0x07, text) class EndOfTrackEvent(MetaEvent): def __init__(self): super(EndOfTrackEvent, self).__init__(0x2f, '') class TempoEvent(MetaEvent): def __init__(self, bpm): self.bpm = bpm value = 60000000.0 / bpm data = PackInteger(int(value), size=4)[1:] super(TempoEvent, self).__init__(0x51, data) class SMPTEOffsetEvent(MetaEvent): def __init__(self, h, m, s, f, sf): self.smpte_offset = (h, m, s, f, sf) data = ''.join(map(chr, [h, m, s, f, sf])) super(SMPTEOffsetEvent, self).__init__(0x54, data) class TimeSignatureEvent(MetaEvent): def __init__(self, numerator, denominator): self.numerator = numerator self.denominator = denominator data = ''.join([ PackInteger(numerator, size=1), PackInteger(int(math.log(denominator) / math.log(2)), size=1), '\x16\x08']) super(TimeSignatureEvent, self).__init__(0x58, data) class KeyEvent(MetaEvent): def __init__(self, sharp_flats, major_minor): self.sharp_flats = sharp_flats self.major_minor = major_minor data = ''.join([ PackInteger(sharp_flats, size=1), PackInteger(major_minor, size=1)]) super(KeyEvent, self).__init__(0x59, data) class BlobEvent(MetaEvent): def __init__(self, blob): self.data = blob super(BlobEvent, self).__init__(0x7f, blob) """Classic channel-oriented messages.""" class ChannelEvent(Event): def __init__(self, mask, channel, data): self.channel = channel self._status = mask | (channel - 1) self._data = PackInteger(self._status, size=1) + data def Serialize(self, running_status): if self._status == running_status: return self._data[1:], self._status else: return self._data, self._status class NoteOffEvent(ChannelEvent): def __init__(self, channel, note, velocity): data = PackInteger(note, size=1) + PackInteger(velocity, size=1) super(NoteOffEvent, self).__init__(0x80, channel, data) self.note = note self.velocity = velocity class NoteOnEvent(ChannelEvent): def __init__(self, channel, note, velocity): data = PackInteger(note, size=1) + PackInteger(velocity, size=1) super(NoteOnEvent, self).__init__(0x90, channel, data) self.note = note self.velocity = velocity class KeyAftertouchEvent(ChannelEvent): def __init__(self, channel, note, aftertouch): data = PackInteger(note, size=1) + PackInteger(aftertouch, size=1) super(KeyAftertouchEvent, self).__init__(0xa0, channel, data) self.note = note self.aftertouch = aftertouch class ControlChangeEvent(ChannelEvent): def __init__(self, channel, controller, value): data = PackInteger(controller, size=1) + PackInteger(value, size=1) super(ControlChangeEvent, self).__init__(0xb0, channel, data) self.controller = controller self.value = value class ProgramChangeEvent(ChannelEvent): def __init__(self, channel, program_number): data = PackInteger(program_number, size=1) super(ProgramChangeEvent, self).__init__(0xc0, channel, data) self.program_number = program_number class ChannelAftertouchEvent(ChannelEvent): def __init__(self, channel, aftertouch): data = PackInteger(aftertouch, size=1) super(ChannelAftertouchEvent, self).__init__(0xd0, channel, data) self.aftertouch = aftertouch class PitchBendEvent(ChannelEvent): def __init__(self, channel, pitch_bend_14_bits): data = PackInteger(pitch_bend_14_bits >> 7, size=1) + \ PackInteger(pitch_bend_14_bits & 0x7f, size=1) super(PitchBendEvent, self).__init__(0xe0, channel, data) self.pitch_bend = pitch_bend_14_bits class SystemEvent(Event): def __init__(self, id): self._id = id def Serialize(self, running_status): return PackInteger(self._id, size=1), running_status class ClockEvent(SystemEvent): def __init__(self): super(ClockEvent, self).__init__(0xf8) class StartEvent(SystemEvent): def __init__(self): super(StartEvent, self).__init__(0xfa) class ContinueEvent(SystemEvent): def __init__(self): super(ContinueEvent, self).__init__(0xfb) class StopEvent(SystemEvent): def __init__(self): super(StopEvent, self).__init__(0xfc) # TODO(pichenettes): also support pauses within a block transmission (F7) class SysExEvent(Event): def __init__(self, manufacturer_id, device_id, data): self.data = data self.message = ''.join([ manufacturer_id, device_id, data, '\xf7']) self.raw_message = '\xf0' + self.message assert all(ord(x) < 128 for x in self.message[:-1]) self.message = ''.join([ '\xf0', PackVariableLengthInteger(len(self.message)), self.message]) def Serialize(self, running_status): return self.message, None def Nibblize(data, add_checksum=True): """Converts a byte string into a nibble string. Also adds checksum""" output = [] if add_checksum: tail = [chr(sum(ord(char) for char in data) % 256)] else: tail = [] for char in map(ord, list(data) + tail): output.append(chr(char >> 4)) output.append(chr(char & 0x0f)) return ''.join(output) class Track(object): def __init__(self): self._events = [] def AddEvent(self, time, event): self._events.append((time, event)) def Sort(self): self._events = sorted(self._events) def Serialize(self): self.Sort() last_time, last_event = self._events[-1] if type(last_event) != EndOfTrackEvent: self._events.append((last_time + 1, EndOfTrackEvent())) data = [] current_time = 0 running_status = None for time, event in self._events: delta = time - current_time data.append(PackVariableLengthInteger(delta)) event_data, running_status = event.Serialize(running_status) data.append(event_data) current_time = time return ''.join(data) def Write(self, file_object): file_object.write('MTrk') track_data = self.Serialize() file_object.write(PackInteger(len(track_data))) file_object.write(track_data) @property def events(self): return self._events class Writer(object): def __init__(self, ppq=96): self._tracks = [] self._ppq = ppq def AddTrack(self): new_track = Track() self._tracks.append(new_track) return new_track def _MergeTracks(self): new_track = Track() for track in self._tracks: for time_event in track.events: new_track.AddEvent(*time_event) new_track.Sort() return new_track def Write(self, file_object, format=0): tracks = self._tracks if format == 0: tracks = [self._MergeTracks()] # File header. file_object.write('MThd') file_object.write(PackInteger(6)) file_object.write(PackInteger(format, size=2)) if format == 0: file_object.write(PackInteger(1, size=2)) else: file_object.write(PackInteger(len(self._tracks), size=2)) file_object.write(PackInteger(self._ppq, size=2)) # Tracks. for track in tracks: track.Write(file_object) class Reader(object): def __init__(self): self.tracks = [] self.format = 0 self.ppq = 96 self._previous_status = 0 def Read(self, f): assert f.read(4) == 'MThd' assert struct.unpack('>i', f.read(4))[0] == 6 self.format = struct.unpack('>h', f.read(2))[0] assert self.format <= 2 num_tracks = struct.unpack('>h', f.read(2))[0] self.ppq = struct.unpack('>h', f.read(2))[0] self._tempo_map = [] for i in xrange(num_tracks): self.tracks.append(self._ReadTrack(f)) self._CreateCumulativeTempoMap() def _ReadTrack(self, f): assert f.read(4) == 'MTrk' size = struct.unpack('>i', f.read(4))[0] t = 0 events = [] while size > 0: delta_t, e, event_size = self._ReadEvent(f) t += delta_t if e: events.append((t, e)) if type(e) == TempoEvent: self._tempo_map.append((t, e.bpm)) size -= event_size return events def _CreateCumulativeTempoMap(self): t = 0.0 current_tempo = 120.0 previous_beat = 0 cumulative_tempo_map = [(0, 0.0, current_tempo)] for beat, tempo in sorted(self._tempo_map): beats = float(beat - previous_beat) / self.ppq t += beats * 60.0 / current_tempo cumulative_tempo_map.append((beat, t, tempo)) current_tempo = tempo previous_beat = beat self._tempo_map = cumulative_tempo_map def AbsoluteTime(self, t): index = bisect.bisect_left(self._tempo_map, (t, 0, 0)) index = max(index - 1, 0) start_beat, start_seconds, tempo = self._tempo_map[index] return start_seconds + float(t - start_beat) / self.ppq * 60.0 / tempo def _ReadVariableLengthInteger(self, f): v = 0 size = 0 while True: v <<= 7 byte = UnpackInteger(f.read(1), size=1) size += 1 v |= (byte & 0x7f) if not (byte & 0x80): break return v, size def _ReadEvent(self, f): delta_t, size = self._ReadVariableLengthInteger(f) event_byte = ord(f.read(1)) size += 1 if event_byte < 0x80: if self._previous_status: f.seek(f.tell() - 1) size -= 1 event_byte = self._previous_status else: return delta_t, None, size event_type = event_byte & 0xf0 channel = event_byte & 0xf channel += 1 if event_type == 0x80: self._previous_status = event_type note = ord(f.read(1)) velo = ord(f.read(1)) event = NoteOffEvent(channel, note, velo) size += 2 elif event_type == 0x90: self._previous_status = event_type event = NoteOnEvent(channel, ord(f.read(1)), ord(f.read(1))) size += 2 elif event_type == 0xa0: self._previous_status = event_type event = KeyAftertouchEvent(channel, ord(f.read(1)), ord(f.read(1))) size += 2 elif event_type == 0xb0: self._previous_status = event_type event = ControlChangeEvent(channel, ord(f.read(1)), ord(f.read(1))) size += 2 elif event_type == 0xc0: self._previous_status = event_type event = ProgramChangeEvent(channel, ord(f.read(1))) size += 1 elif event_type == 0xd0: self._previous_status = event_type event = ChannelAftertouchEvent(channel, ord(f.read(1))) size += 1 elif event_type == 0xe0: self._previous_status = event_type event = PitchBendEvent(channel, (ord(f.read(1)) << 7) | ord(f.read(1))) size += 2 elif event_byte == 0xff: event_type = ord(f.read(1)) size += 1 event_size, event_size_size = self._ReadVariableLengthInteger(f) size += event_size_size bytes = f.read(event_size) size += event_size if event_type == 0x01: event = TextEvent(bytes) elif event_type == 0x02: event = CopyrightEvent(bytes) elif event_type == 0x03: event = TrackNameEvent(bytes) elif event_type == 0x04: event = TrackInstrumentNameEvent(bytes) elif event_type == 0x05: event = LyricEvent(bytes) elif event_type == 0x06: event = MarkerEvent(bytes) elif event_type == 0x07: event = CuePointEvent(bytes) elif event_type == 0x20: current_channel = ord(bytes[0]) event = None elif event_type == 0x2f: event = EndOfTrackEvent() elif event_type == 0x51: value = UnpackInteger('\x00' + bytes, size=4) event = TempoEvent(60000000.0 / value) elif event_type == 0x54: event = SMPTEOffsetEvent(*map(ord, bytes)) elif event_type == 0x58: event = TimeSignatureEvent(ord(bytes[0]), 2 ** ord(bytes[1])) elif event_type == 0x59: event = KeyEvent(ord(bytes[0]), ord(bytes[1])) elif event_type == 0x7f: event = BlobEvent(bytes) elif event_byte == 0xf0: event_size, event_size_size = self._ReadVariableLengthInteger(f) size += event_size_size bytes = f.read(event_size) size += event_size event = SysExEvent(bytes[0:3], bytes[3:5], bytes[5:-1]) else: print event_byte, '!!' event = None return delta_t, event, size if __name__ == '__main__': m = MidiFile() t = m.AddTrack() t.AddEvent(0, TempoEvent(120.0)) t = m.AddTrack() t.AddEvent(1, SysExEvent( '\x00\x20\x77', '\x00\x01', '\x7f\x7f' + Nibblize('\xff\x00\xcc'))) f = file('output.mid', 'wb') m.Write(f, format=0) f.close()
Report a bug