mirror of
https://github.com/clinton-hall/nzbToMedia.git
synced 2025-01-26 12:53:04 -08:00
1111074dc3
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
204 lines
5.4 KiB
Python
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"]
|