Labrys 1111074dc3 Update beets to 1.3.18:
Dependencies:
* PyYAML 3.11
* Unidecode 0.4.19
* beets 1.3.18
* colorama 0.3.7
* enum34 1.1.6
* jellyfish 0.5.4
* munkres 1.0.7
* musicbrainzngs 0.6
* mutagen 1.32
2016-06-06 12:08:52 -04:00

204 lines
5.4 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2015 Christoph Reiter
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
"""Standard MIDI File (SMF)"""
import struct
from mutagen import StreamInfo, MutagenError
from mutagen._file import FileType
from mutagen._compat import xrange, endswith
class SMFError(MutagenError):
pass
def _var_int(data, offset=0):
val = 0
while 1:
try:
x = data[offset]
except IndexError:
raise SMFError("Not enough data")
offset += 1
val = (val << 7) + (x & 0x7F)
if not (x & 0x80):
return val, offset
def _read_track(chunk):
"""Retuns a list of midi events and tempo change events"""
TEMPO, MIDI = range(2)
# Deviations: The running status should be reset on non midi events, but
# some files contain meta events inbetween.
# TODO: Offset and time signature are not considered.
tempos = []
events = []
chunk = bytearray(chunk)
deltasum = 0
status = 0
off = 0
while off < len(chunk):
delta, off = _var_int(chunk, off)
deltasum += delta
event_type = chunk[off]
off += 1
if event_type == 0xFF:
meta_type = chunk[off]
off += 1
num, off = _var_int(chunk, off)
# TODO: support offset/time signature
if meta_type == 0x51:
data = chunk[off:off + num]
if len(data) != 3:
raise SMFError
tempo = struct.unpack(">I", b"\x00" + bytes(data))[0]
tempos.append((deltasum, TEMPO, tempo))
off += num
elif event_type in (0xF0, 0xF7):
val, off = _var_int(chunk, off)
off += val
else:
if event_type < 0x80:
# if < 0x80 take the type from the previous midi event
off += 1
event_type = status
elif event_type < 0xF0:
off += 2
status = event_type
else:
raise SMFError("invalid event")
if event_type >> 4 in (0xD, 0xC):
off -= 1
events.append((deltasum, MIDI, delta))
return events, tempos
def _read_midi_length(fileobj):
"""Returns the duration in seconds. Can raise all kind of errors..."""
TEMPO, MIDI = range(2)
def read_chunk(fileobj):
info = fileobj.read(8)
if len(info) != 8:
raise SMFError("truncated")
chunklen = struct.unpack(">I", info[4:])[0]
data = fileobj.read(chunklen)
if len(data) != chunklen:
raise SMFError("truncated")
return info[:4], data
identifier, chunk = read_chunk(fileobj)
if identifier != b"MThd":
raise SMFError("Not a MIDI file")
if len(chunk) != 6:
raise SMFError("truncated")
format_, ntracks, tickdiv = struct.unpack(">HHH", chunk)
if format_ > 1:
raise SMFError("Not supported format %d" % format_)
if tickdiv >> 15:
# fps = (-(tickdiv >> 8)) & 0xFF
# subres = tickdiv & 0xFF
# never saw one of those
raise SMFError("Not supported timing interval")
# get a list of events and tempo changes for each track
tracks = []
first_tempos = None
for tracknum in xrange(ntracks):
identifier, chunk = read_chunk(fileobj)
if identifier != b"MTrk":
continue
events, tempos = _read_track(chunk)
# In case of format == 1, copy the first tempo list to all tracks
first_tempos = first_tempos or tempos
if format_ == 1:
tempos = list(first_tempos)
events += tempos
events.sort()
tracks.append(events)
# calculate the duration of each track
durations = []
for events in tracks:
tempo = 500000
parts = []
deltasum = 0
for (dummy, type_, data) in events:
if type_ == TEMPO:
parts.append((deltasum, tempo))
tempo = data
deltasum = 0
else:
deltasum += data
parts.append((deltasum, tempo))
duration = 0
for (deltasum, tempo) in parts:
quarter, tpq = deltasum / float(tickdiv), tempo
duration += (quarter * tpq)
duration /= 10 ** 6
durations.append(duration)
# return the longest one
return max(durations)
class SMFInfo(StreamInfo):
def __init__(self, fileobj):
"""Raises SMFError"""
self.length = _read_midi_length(fileobj)
"""Length in seconds"""
def pprint(self):
return u"SMF, %.2f seconds" % self.length
class SMF(FileType):
"""Standard MIDI File (SMF)"""
_mimes = ["audio/midi", "audio/x-midi"]
def load(self, filename):
self.filename = filename
try:
with open(filename, "rb") as h:
self.info = SMFInfo(h)
except IOError as e:
raise SMFError(e)
def add_tags(self):
raise SMFError("doesn't support tags")
@staticmethod
def score(filename, fileobj, header):
filename = filename.lower()
return header.startswith(b"MThd") and (
endswith(filename, ".mid") or endswith(filename, ".midi"))
Open = SMF
error = SMFError
__all__ = ["SMF"]