mirror of
https://github.com/rembo10/headphones.git
synced 2026-01-14 09:17:59 -05:00
216 lines
5.5 KiB
Python
216 lines
5.5 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 the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
"""Standard MIDI File (SMF)"""
|
|
|
|
import struct
|
|
|
|
from mutagen import StreamInfo, MutagenError
|
|
from mutagen._file import FileType
|
|
from mutagen._util import loadfile, 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 range(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):
|
|
"""SMFInfo()
|
|
|
|
Attributes:
|
|
length (`float`): Length in seconds
|
|
|
|
"""
|
|
|
|
def __init__(self, fileobj):
|
|
"""Raises SMFError"""
|
|
|
|
self.length = _read_midi_length(fileobj)
|
|
|
|
def pprint(self):
|
|
return u"SMF, %.2f seconds" % self.length
|
|
|
|
|
|
class SMF(FileType):
|
|
"""SMF(filething)
|
|
|
|
Standard MIDI File (SMF)
|
|
|
|
Attributes:
|
|
info (`SMFInfo`)
|
|
tags: `None`
|
|
"""
|
|
|
|
_mimes = ["audio/midi", "audio/x-midi"]
|
|
|
|
@loadfile()
|
|
def load(self, filething):
|
|
try:
|
|
self.info = SMFInfo(filething.fileobj)
|
|
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"]
|