Files
headphones/lib/mutagen/aiff.py
2022-02-07 09:30:17 +05:30

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