257 lines
8.9 KiB
Python
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)
|