mirror of
https://github.com/rembo10/headphones.git
synced 2026-01-14 01:08:09 -05:00
246 lines
6.2 KiB
Python
246 lines
6.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright (C) 2014 Evan Purkhiser
|
|
# 2014 Ben Ockmore
|
|
# 2019-2020 Philipp Wolfer
|
|
#
|
|
# 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.
|
|
|
|
"""AIFF audio stream information and tags."""
|
|
|
|
import struct
|
|
from struct import pack
|
|
|
|
from mutagen import StreamInfo, FileType
|
|
|
|
from mutagen.id3._util import ID3NoHeaderError, error as ID3Error
|
|
from mutagen._iff import (
|
|
IffChunk,
|
|
IffContainerChunkMixin,
|
|
IffFile,
|
|
IffID3,
|
|
InvalidChunk,
|
|
error as IffError,
|
|
)
|
|
from mutagen._util import (
|
|
convert_error,
|
|
loadfile,
|
|
endswith,
|
|
)
|
|
|
|
__all__ = ["AIFF", "Open", "delete"]
|
|
|
|
|
|
class error(IffError):
|
|
pass
|
|
|
|
|
|
# based on stdlib's aifc
|
|
_HUGE_VAL = 1.79769313486231e+308
|
|
|
|
|
|
def read_float(data):
|
|
"""Raises OverflowError"""
|
|
|
|
assert len(data) == 10
|
|
expon, himant, lomant = struct.unpack('>hLL', data)
|
|
sign = 1
|
|
if expon < 0:
|
|
sign = -1
|
|
expon = expon + 0x8000
|
|
if expon == himant == lomant == 0:
|
|
f = 0.0
|
|
elif expon == 0x7FFF:
|
|
raise OverflowError("inf and nan not supported")
|
|
else:
|
|
expon = expon - 16383
|
|
# this can raise OverflowError too
|
|
f = (himant * 0x100000000 + lomant) * pow(2.0, expon - 63)
|
|
return sign * f
|
|
|
|
|
|
class AIFFChunk(IffChunk):
|
|
"""Representation of a single IFF chunk"""
|
|
|
|
@classmethod
|
|
def parse_header(cls, header):
|
|
return struct.unpack('>4sI', header)
|
|
|
|
@classmethod
|
|
def get_class(cls, id):
|
|
if id == 'FORM':
|
|
return AIFFFormChunk
|
|
else:
|
|
return cls
|
|
|
|
def write_new_header(self, id_, size):
|
|
self._fileobj.write(pack('>4sI', id_, size))
|
|
|
|
def write_size(self):
|
|
self._fileobj.write(pack('>I', self.data_size))
|
|
|
|
|
|
class AIFFFormChunk(AIFFChunk, IffContainerChunkMixin):
|
|
"""The AIFF root chunk."""
|
|
|
|
def parse_next_subchunk(self):
|
|
return AIFFChunk.parse(self._fileobj, self)
|
|
|
|
def __init__(self, fileobj, id, data_size, parent_chunk):
|
|
if id != u'FORM':
|
|
raise InvalidChunk('Expected FORM chunk, got %s' % id)
|
|
|
|
AIFFChunk.__init__(self, fileobj, id, data_size, parent_chunk)
|
|
self.init_container()
|
|
|
|
|
|
class AIFFFile(IffFile):
|
|
"""Representation of a AIFF file"""
|
|
|
|
def __init__(self, fileobj):
|
|
# AIFF Files always start with the FORM chunk which contains a 4 byte
|
|
# ID before the start of other chunks
|
|
super().__init__(AIFFChunk, fileobj)
|
|
|
|
if self.root.id != u'FORM':
|
|
raise InvalidChunk("Root chunk must be a FORM chunk, got %s"
|
|
% self.root.id)
|
|
|
|
def __contains__(self, id_):
|
|
if id_ == 'FORM': # For backwards compatibility
|
|
return True
|
|
return super().__contains__(id_)
|
|
|
|
def __getitem__(self, id_):
|
|
if id_ == 'FORM': # For backwards compatibility
|
|
return self.root
|
|
return super().__getitem__(id_)
|
|
|
|
|
|
class AIFFInfo(StreamInfo):
|
|
"""AIFFInfo()
|
|
|
|
AIFF audio stream information.
|
|
|
|
Information is parsed from the COMM chunk of the AIFF file
|
|
|
|
Attributes:
|
|
length (`float`): audio length, in seconds
|
|
bitrate (`int`): audio bitrate, in bits per second
|
|
channels (`int`): The number of audio channels
|
|
sample_rate (`int`): audio sample rate, in Hz
|
|
bits_per_sample (`int`): The audio sample size
|
|
"""
|
|
|
|
length = 0
|
|
bitrate = 0
|
|
channels = 0
|
|
sample_rate = 0
|
|
|
|
@convert_error(IOError, error)
|
|
def __init__(self, fileobj):
|
|
"""Raises error"""
|
|
|
|
iff = AIFFFile(fileobj)
|
|
try:
|
|
common_chunk = iff[u'COMM']
|
|
except KeyError as e:
|
|
raise error(str(e))
|
|
|
|
data = common_chunk.read()
|
|
if len(data) < 18:
|
|
raise error
|
|
|
|
info = struct.unpack('>hLh10s', data[:18])
|
|
channels, frame_count, sample_size, sample_rate = info
|
|
|
|
try:
|
|
self.sample_rate = int(read_float(sample_rate))
|
|
except OverflowError:
|
|
raise error("Invalid sample rate")
|
|
if self.sample_rate < 0:
|
|
raise error("Invalid sample rate")
|
|
if self.sample_rate != 0:
|
|
self.length = frame_count / float(self.sample_rate)
|
|
|
|
self.bits_per_sample = sample_size
|
|
self.sample_size = sample_size # For backward compatibility
|
|
self.channels = channels
|
|
self.bitrate = channels * sample_size * self.sample_rate
|
|
|
|
def pprint(self):
|
|
return u"%d channel AIFF @ %d bps, %s Hz, %.2f seconds" % (
|
|
self.channels, self.bitrate, self.sample_rate, self.length)
|
|
|
|
|
|
class _IFFID3(IffID3):
|
|
"""A AIFF file with ID3v2 tags"""
|
|
|
|
def _load_file(self, fileobj):
|
|
return AIFFFile(fileobj)
|
|
|
|
|
|
@convert_error(IOError, error)
|
|
@loadfile(method=False, writable=True)
|
|
def delete(filething):
|
|
"""Completely removes the ID3 chunk from the AIFF file"""
|
|
|
|
try:
|
|
del AIFFFile(filething.fileobj)[u'ID3']
|
|
except KeyError:
|
|
pass
|
|
|
|
|
|
class AIFF(FileType):
|
|
"""AIFF(filething)
|
|
|
|
An AIFF audio file.
|
|
|
|
Arguments:
|
|
filething (filething)
|
|
|
|
Attributes:
|
|
tags (`mutagen.id3.ID3`)
|
|
info (`AIFFInfo`)
|
|
"""
|
|
|
|
_mimes = ["audio/aiff", "audio/x-aiff"]
|
|
|
|
@staticmethod
|
|
def score(filename, fileobj, header):
|
|
filename = filename.lower()
|
|
|
|
return (header.startswith(b"FORM") * 2 + endswith(filename, b".aif") +
|
|
endswith(filename, b".aiff") + endswith(filename, b".aifc"))
|
|
|
|
def add_tags(self):
|
|
"""Add an empty ID3 tag to the file."""
|
|
if self.tags is None:
|
|
self.tags = _IFFID3()
|
|
else:
|
|
raise error("an ID3 tag already exists")
|
|
|
|
@convert_error(IOError, error)
|
|
@loadfile()
|
|
def load(self, filething, **kwargs):
|
|
"""Load stream and tag information from a file."""
|
|
|
|
fileobj = filething.fileobj
|
|
|
|
try:
|
|
self.tags = _IFFID3(fileobj, **kwargs)
|
|
except ID3NoHeaderError:
|
|
self.tags = None
|
|
except ID3Error as e:
|
|
raise error(e)
|
|
else:
|
|
self.tags.filename = self.filename
|
|
|
|
fileobj.seek(0, 0)
|
|
self.info = AIFFInfo(fileobj)
|
|
|
|
|
|
Open = AIFF
|