Melodia/Melodia/resources/audiotools/__au__.py

257 lines
8.9 KiB
Python

#!/usr/bin/python
#Audio Tools, a module and set of tools for manipulating audio data
#Copyright (C) 2007-2011 Brian Langenberger
#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.
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#GNU General Public License for more details.
#You should have received a copy of the GNU General Public License
#along with this program; if not, write to the Free Software
#Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from audiotools import (AudioFile, InvalidFile, PCMReader, Con,
transfer_data, InvalidFormat,
__capped_stream_reader__, BUFFER_SIZE,
FILENAME_FORMAT, EncodingError, DecodingError,
ChannelMask)
import audiotools.pcm
import gettext
gettext.install("audiotools", unicode=True)
class InvalidAU(InvalidFile):
pass
#######################
#Sun AU
#######################
class AuReader(PCMReader):
"""A subclass of PCMReader for reading Sun AU file contents."""
def __init__(self, au_file, data_size,
sample_rate, channels, channel_mask, bits_per_sample):
"""au_file is a file, data_size is an integer byte count.
sample_rate, channels, channel_mask and bits_per_sample are ints.
"""
PCMReader.__init__(self,
file=au_file,
sample_rate=sample_rate,
channels=channels,
channel_mask=channel_mask,
bits_per_sample=bits_per_sample)
self.data_size = data_size
def read(self, bytes):
"""Try to read a pcm.FrameList of size "bytes"."""
#align bytes downward if an odd number is read in
bytes -= (bytes % (self.channels * self.bits_per_sample / 8))
bytes = max(bytes, self.channels * self.bits_per_sample / 8)
pcm_data = self.file.read(bytes)
if ((len(pcm_data) == 0) and (self.data_size > 0)):
raise IOError("data ends prematurely")
else:
self.data_size -= len(pcm_data)
try:
return audiotools.pcm.FrameList(pcm_data,
self.channels,
self.bits_per_sample,
True,
True)
except ValueError:
raise IOError("data ends prematurely")
class AuAudio(AudioFile):
"""A Sun AU audio file."""
SUFFIX = "au"
NAME = SUFFIX
AU_HEADER = Con.Struct('header',
Con.Const(Con.String('magic_number', 4), '.snd'),
Con.UBInt32('data_offset'),
Con.UBInt32('data_size'),
Con.UBInt32('encoding_format'),
Con.UBInt32('sample_rate'),
Con.UBInt32('channels'))
def __init__(self, filename):
AudioFile.__init__(self, filename)
try:
f = file(filename, 'rb')
except IOError, msg:
raise InvalidAU(str(msg))
try:
header = AuAudio.AU_HEADER.parse_stream(f)
if (header.encoding_format not in (2, 3, 4)):
raise InvalidFile(_(u"Unsupported Sun AU encoding format"))
self.__bits_per_sample__ = {2: 8, 3: 16, 4: 24}[
header.encoding_format]
self.__channels__ = header.channels
self.__sample_rate__ = header.sample_rate
self.__total_frames__ = header.data_size / \
(self.__bits_per_sample__ / 8) / \
self.__channels__
self.__data_offset__ = header.data_offset
self.__data_size__ = header.data_size
except Con.ConstError:
raise InvalidFile(_(u"Invalid Sun AU header"))
except Con.FieldError:
raise InvalidAU(_(u"Invalid Sun AU header"))
@classmethod
def is_type(cls, file):
"""Returns True if the given file object describes this format.
Takes a seekable file pointer rewound to the start of the file."""
return file.read(4) == ".snd"
def lossless(self):
"""Returns True."""
return True
def bits_per_sample(self):
"""Returns an integer number of bits-per-sample this track contains."""
return self.__bits_per_sample__
def channels(self):
"""Returns an integer number of channels this track contains."""
return self.__channels__
def channel_mask(self):
"""Returns a ChannelMask object of this track's channel layout."""
if (self.channels() <= 2):
return ChannelMask.from_channels(self.channels())
else:
return ChannelMask(0)
def sample_rate(self):
"""Returns the rate of the track's audio as an integer number of Hz."""
return self.__sample_rate__
def total_frames(self):
"""Returns the total PCM frames of the track as an integer."""
return self.__total_frames__
def to_pcm(self):
"""Returns a PCMReader object containing the track's PCM data."""
f = file(self.filename, 'rb')
f.seek(self.__data_offset__, 0)
return AuReader(au_file=f,
data_size=self.__data_size__,
sample_rate=self.sample_rate(),
channels=self.channels(),
channel_mask=int(self.channel_mask()),
bits_per_sample=self.bits_per_sample())
@classmethod
def from_pcm(cls, filename, pcmreader, compression=None):
"""Encodes a new file from PCM data.
Takes a filename string, PCMReader object
and optional compression level string.
Encodes a new audio file from pcmreader's data
at the given filename with the specified compression level
and returns a new AuAudio object."""
if (pcmreader.bits_per_sample not in (8, 16, 24)):
raise InvalidFormat(
_(u"Unsupported bits per sample %s") % (
pcmreader.bits_per_sample))
bytes_per_sample = pcmreader.bits_per_sample / 8
header = Con.Container(magic_number='.snd',
data_offset=0,
data_size=0,
encoding_format={8: 2, 16: 3, 24: 4}[
pcmreader.bits_per_sample],
sample_rate=pcmreader.sample_rate,
channels=pcmreader.channels)
try:
f = file(filename, 'wb')
except IOError, err:
raise EncodingError(str(err))
try:
#send out a dummy header
f.write(AuAudio.AU_HEADER.build(header))
header.data_offset = f.tell()
#send our big-endian PCM data
#d will be a list of ints, so we can't use transfer_data
try:
framelist = pcmreader.read(BUFFER_SIZE)
while (len(framelist) > 0):
bytes = framelist.to_bytes(True, True)
f.write(bytes)
header.data_size += len(bytes)
framelist = pcmreader.read(BUFFER_SIZE)
except (IOError, ValueError), err:
cls.__unlink__(filename)
raise EncodingError(str(err))
except Exception, err:
cls.__unlink__(filename)
raise err
#send out a complete header
f.seek(0, 0)
f.write(AuAudio.AU_HEADER.build(header))
finally:
f.close()
try:
pcmreader.close()
except DecodingError, err:
raise EncodingError(err.error_message)
return AuAudio(filename)
@classmethod
def track_name(cls, file_path, track_metadata=None, format=None,
suffix=None):
"""Constructs a new filename string.
Given a plain string to an existing path,
a MetaData-compatible object (or None),
a UTF-8-encoded Python format string
and an ASCII-encoded suffix string (such as "mp3")
returns a plain string of a new filename with format's
fields filled-in and encoded as FS_ENCODING.
Raises UnsupportedTracknameField if the format string
contains invalid template fields."""
if (format is None):
format = "track%(track_number)2.2d.au"
return AudioFile.track_name(file_path, track_metadata, format,
suffix=cls.SUFFIX)