1# -*- coding: utf-8 -*- 2# Copyright 2015 Christoph Reiter 3# 4# This program is free software; you can redistribute it and/or modify 5# it under the terms of the GNU General Public License as published by 6# the Free Software Foundation; either version 2 of the License, or 7# (at your option) any later version. 8 9"""Standard MIDI File (SMF)""" 10 11import struct 12 13from mutagen import StreamInfo, MutagenError 14from mutagen._file import FileType 15from mutagen._util import loadfile 16from mutagen._compat import xrange, endswith 17 18 19class SMFError(MutagenError): 20 pass 21 22 23def _var_int(data, offset=0): 24 val = 0 25 while 1: 26 try: 27 x = data[offset] 28 except IndexError: 29 raise SMFError("Not enough data") 30 offset += 1 31 val = (val << 7) + (x & 0x7F) 32 if not (x & 0x80): 33 return val, offset 34 35 36def _read_track(chunk): 37 """Retuns a list of midi events and tempo change events""" 38 39 TEMPO, MIDI = range(2) 40 41 # Deviations: The running status should be reset on non midi events, but 42 # some files contain meta events inbetween. 43 # TODO: Offset and time signature are not considered. 44 45 tempos = [] 46 events = [] 47 48 chunk = bytearray(chunk) 49 deltasum = 0 50 status = 0 51 off = 0 52 while off < len(chunk): 53 delta, off = _var_int(chunk, off) 54 deltasum += delta 55 event_type = chunk[off] 56 off += 1 57 if event_type == 0xFF: 58 meta_type = chunk[off] 59 off += 1 60 num, off = _var_int(chunk, off) 61 # TODO: support offset/time signature 62 if meta_type == 0x51: 63 data = chunk[off:off + num] 64 if len(data) != 3: 65 raise SMFError 66 tempo = struct.unpack(">I", b"\x00" + bytes(data))[0] 67 tempos.append((deltasum, TEMPO, tempo)) 68 off += num 69 elif event_type in (0xF0, 0xF7): 70 val, off = _var_int(chunk, off) 71 off += val 72 else: 73 if event_type < 0x80: 74 # if < 0x80 take the type from the previous midi event 75 off += 1 76 event_type = status 77 elif event_type < 0xF0: 78 off += 2 79 status = event_type 80 else: 81 raise SMFError("invalid event") 82 83 if event_type >> 4 in (0xD, 0xC): 84 off -= 1 85 86 events.append((deltasum, MIDI, delta)) 87 88 return events, tempos 89 90 91def _read_midi_length(fileobj): 92 """Returns the duration in seconds. Can raise all kind of errors...""" 93 94 TEMPO, MIDI = range(2) 95 96 def read_chunk(fileobj): 97 info = fileobj.read(8) 98 if len(info) != 8: 99 raise SMFError("truncated") 100 chunklen = struct.unpack(">I", info[4:])[0] 101 data = fileobj.read(chunklen) 102 if len(data) != chunklen: 103 raise SMFError("truncated") 104 return info[:4], data 105 106 identifier, chunk = read_chunk(fileobj) 107 if identifier != b"MThd": 108 raise SMFError("Not a MIDI file") 109 110 if len(chunk) != 6: 111 raise SMFError("truncated") 112 113 format_, ntracks, tickdiv = struct.unpack(">HHH", chunk) 114 if format_ > 1: 115 raise SMFError("Not supported format %d" % format_) 116 117 if tickdiv >> 15: 118 # fps = (-(tickdiv >> 8)) & 0xFF 119 # subres = tickdiv & 0xFF 120 # never saw one of those 121 raise SMFError("Not supported timing interval") 122 123 # get a list of events and tempo changes for each track 124 tracks = [] 125 first_tempos = None 126 for tracknum in xrange(ntracks): 127 identifier, chunk = read_chunk(fileobj) 128 if identifier != b"MTrk": 129 continue 130 events, tempos = _read_track(chunk) 131 132 # In case of format == 1, copy the first tempo list to all tracks 133 first_tempos = first_tempos or tempos 134 if format_ == 1: 135 tempos = list(first_tempos) 136 events += tempos 137 events.sort() 138 tracks.append(events) 139 140 # calculate the duration of each track 141 durations = [] 142 for events in tracks: 143 tempo = 500000 144 parts = [] 145 deltasum = 0 146 for (dummy, type_, data) in events: 147 if type_ == TEMPO: 148 parts.append((deltasum, tempo)) 149 tempo = data 150 deltasum = 0 151 else: 152 deltasum += data 153 parts.append((deltasum, tempo)) 154 155 duration = 0 156 for (deltasum, tempo) in parts: 157 quarter, tpq = deltasum / float(tickdiv), tempo 158 duration += (quarter * tpq) 159 duration /= 10 ** 6 160 161 durations.append(duration) 162 163 # return the longest one 164 return max(durations) 165 166 167class SMFInfo(StreamInfo): 168 """SMFInfo() 169 170 Attributes: 171 length (`float`): Length in seconds 172 173 """ 174 175 def __init__(self, fileobj): 176 """Raises SMFError""" 177 178 self.length = _read_midi_length(fileobj) 179 180 def pprint(self): 181 return u"SMF, %.2f seconds" % self.length 182 183 184class SMF(FileType): 185 """SMF(filething) 186 187 Standard MIDI File (SMF) 188 189 Attributes: 190 info (`SMFInfo`) 191 tags: `None` 192 """ 193 194 _mimes = ["audio/midi", "audio/x-midi"] 195 196 @loadfile() 197 def load(self, filething): 198 try: 199 self.info = SMFInfo(filething.fileobj) 200 except IOError as e: 201 raise SMFError(e) 202 203 def add_tags(self): 204 raise SMFError("doesn't support tags") 205 206 @staticmethod 207 def score(filename, fileobj, header): 208 filename = filename.lower() 209 return header.startswith(b"MThd") and ( 210 endswith(filename, ".mid") or endswith(filename, ".midi")) 211 212 213Open = SMF 214error = SMFError 215 216__all__ = ["SMF"] 217