Files
headphones/lib/mutagen/aiff.py
rembo10 ab4dd18be4 Initial python3 changes
Mostly just updating libraries, removing string encoding/decoding,
fixing some edge cases. No new functionality was added in this
commit.
2022-01-14 10:38:06 +05:30

374 lines
10 KiB
Python
Executable File

# -*- coding: utf-8 -*-
# Copyright (C) 2014 Evan Purkhiser
# 2014 Ben Ockmore
#
# 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 sys
import struct
from struct import pack
from ._compat import endswith, text_type, reraise
from mutagen import StreamInfo, FileType
from mutagen.id3 import ID3
from mutagen.id3._util import ID3NoHeaderError, error as ID3Error
from mutagen._util import resize_bytes, delete_bytes, MutagenError, loadfile, \
convert_error
__all__ = ["AIFF", "Open", "delete"]
class error(MutagenError):
pass
class InvalidChunk(error):
pass
# based on stdlib's aifc
_HUGE_VAL = 1.79769313486231e+308
def is_valid_chunk_id(id):
assert isinstance(id, text_type)
return ((len(id) <= 4) and (min(id) >= ' ') and
(max(id) <= '~'))
def read_float(data): # 10 bytes
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:
f = _HUGE_VAL
else:
expon = expon - 16383
f = (himant * 0x100000000 + lomant) * pow(2.0, expon - 63)
return sign * f
class IFFChunk(object):
"""Representation of a single IFF chunk"""
# Chunk headers are 8 bytes long (4 for ID and 4 for the size)
HEADER_SIZE = 8
def __init__(self, fileobj, parent_chunk=None):
self.__fileobj = fileobj
self.parent_chunk = parent_chunk
self.offset = fileobj.tell()
header = fileobj.read(self.HEADER_SIZE)
if len(header) < self.HEADER_SIZE:
raise InvalidChunk()
self.id, self.data_size = struct.unpack('>4si', header)
try:
self.id = self.id.decode('ascii')
except UnicodeDecodeError:
raise InvalidChunk()
if not is_valid_chunk_id(self.id):
raise InvalidChunk()
self.size = self.HEADER_SIZE + self.data_size
self.data_offset = fileobj.tell()
def read(self):
"""Read the chunks data"""
self.__fileobj.seek(self.data_offset)
return self.__fileobj.read(self.data_size)
def write(self, data):
"""Write the chunk data"""
if len(data) > self.data_size:
raise ValueError
self.__fileobj.seek(self.data_offset)
self.__fileobj.write(data)
def delete(self):
"""Removes the chunk from the file"""
delete_bytes(self.__fileobj, self.size, self.offset)
if self.parent_chunk is not None:
self.parent_chunk._update_size(
self.parent_chunk.data_size - self.size)
def _update_size(self, data_size):
"""Update the size of the chunk"""
self.__fileobj.seek(self.offset + 4)
self.__fileobj.write(pack('>I', data_size))
if self.parent_chunk is not None:
size_diff = self.data_size - data_size
self.parent_chunk._update_size(
self.parent_chunk.data_size - size_diff)
self.data_size = data_size
self.size = data_size + self.HEADER_SIZE
def resize(self, new_data_size):
"""Resize the file and update the chunk sizes"""
resize_bytes(
self.__fileobj, self.data_size, new_data_size, self.data_offset)
self._update_size(new_data_size)
class IFFFile(object):
"""Representation of a IFF file"""
def __init__(self, fileobj):
self.__fileobj = fileobj
self.__chunks = {}
# AIFF Files always start with the FORM chunk which contains a 4 byte
# ID before the start of other chunks
fileobj.seek(0)
self.__chunks['FORM'] = IFFChunk(fileobj)
# Skip past the 4 byte FORM id
fileobj.seek(IFFChunk.HEADER_SIZE + 4)
# Where the next chunk can be located. We need to keep track of this
# since the size indicated in the FORM header may not match up with the
# offset determined from the size of the last chunk in the file
self.__next_offset = fileobj.tell()
# Load all of the chunks
while True:
try:
chunk = IFFChunk(fileobj, self['FORM'])
except InvalidChunk:
break
self.__chunks[chunk.id.strip()] = chunk
# Calculate the location of the next chunk,
# considering the pad byte
self.__next_offset = chunk.offset + chunk.size
self.__next_offset += self.__next_offset % 2
fileobj.seek(self.__next_offset)
def __contains__(self, id_):
"""Check if the IFF file contains a specific chunk"""
assert isinstance(id_, text_type)
if not is_valid_chunk_id(id_):
raise KeyError("AIFF key must be four ASCII characters.")
return id_ in self.__chunks
def __getitem__(self, id_):
"""Get a chunk from the IFF file"""
assert isinstance(id_, text_type)
if not is_valid_chunk_id(id_):
raise KeyError("AIFF key must be four ASCII characters.")
try:
return self.__chunks[id_]
except KeyError:
raise KeyError(
"%r has no %r chunk" % (self.__fileobj, id_))
def __delitem__(self, id_):
"""Remove a chunk from the IFF file"""
assert isinstance(id_, text_type)
if not is_valid_chunk_id(id_):
raise KeyError("AIFF key must be four ASCII characters.")
self.__chunks.pop(id_).delete()
def insert_chunk(self, id_):
"""Insert a new chunk at the end of the IFF file"""
assert isinstance(id_, text_type)
if not is_valid_chunk_id(id_):
raise KeyError("AIFF key must be four ASCII characters.")
self.__fileobj.seek(self.__next_offset)
self.__fileobj.write(pack('>4si', id_.ljust(4).encode('ascii'), 0))
self.__fileobj.seek(self.__next_offset)
chunk = IFFChunk(self.__fileobj, self['FORM'])
self['FORM']._update_size(self['FORM'].data_size + chunk.size)
self.__chunks[id_] = chunk
self.__next_offset = chunk.offset + chunk.size
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
sample_size (`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 = IFFFile(fileobj)
try:
common_chunk = iff['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
self.sample_rate = int(read_float(sample_rate))
self.sample_size = sample_size
self.channels = channels
self.bitrate = channels * sample_size * self.sample_rate
self.length = frame_count / float(self.sample_rate)
def pprint(self):
return "%d channel AIFF @ %d bps, %s Hz, %.2f seconds" % (
self.channels, self.bitrate, self.sample_rate, self.length)
class _IFFID3(ID3):
"""A AIFF file with ID3v2 tags"""
def _pre_load_header(self, fileobj):
try:
fileobj.seek(IFFFile(fileobj)['ID3'].data_offset)
except (InvalidChunk, KeyError):
raise ID3NoHeaderError("No ID3 chunk")
@convert_error(IOError, error)
@loadfile(writable=True)
def save(self, filething, v2_version=4, v23_sep='/', padding=None):
"""Save ID3v2 data to the AIFF file"""
fileobj = filething.fileobj
iff_file = IFFFile(fileobj)
if 'ID3' not in iff_file:
iff_file.insert_chunk('ID3')
chunk = iff_file['ID3']
try:
data = self._prepare_data(
fileobj, chunk.data_offset, chunk.data_size, v2_version,
v23_sep, padding)
except ID3Error as e:
reraise(error, e, sys.exc_info()[2])
new_size = len(data)
new_size += new_size % 2 # pad byte
assert new_size % 2 == 0
chunk.resize(new_size)
data += (new_size - len(data)) * b'\x00'
assert new_size == len(data)
chunk.write(data)
@loadfile(writable=True)
def delete(self, filething):
"""Completely removes the ID3 chunk from the AIFF file"""
delete(filething)
self.clear()
@convert_error(IOError, error)
@loadfile(method=False, writable=True)
def delete(filething):
"""Completely removes the ID3 chunk from the AIFF file"""
try:
del IFFFile(filething.fileobj)['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