1024 lines
37 KiB
Python
1024 lines
37 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, ChannelMask, PCMReader,
|
|
Con, BUFFER_SIZE, transfer_data,
|
|
transfer_framelist_data,
|
|
__capped_stream_reader__, FILENAME_FORMAT,
|
|
BIN, open_files, os, subprocess, cStringIO,
|
|
EncodingError, DecodingError, UnsupportedChannelMask,
|
|
WaveContainer, to_pcm_progress)
|
|
import os.path
|
|
import gettext
|
|
from . import pcm
|
|
|
|
gettext.install("audiotools", unicode=True)
|
|
|
|
#######################
|
|
#RIFF WAVE
|
|
#######################
|
|
|
|
|
|
class WaveReader(PCMReader):
|
|
"""A subclass of PCMReader for reading wave file contents."""
|
|
|
|
def __init__(self, wave_file,
|
|
sample_rate, channels, channel_mask, bits_per_sample,
|
|
process=None):
|
|
"""wave_file should be a file-like stream of wave data.
|
|
|
|
sample_rate, channels, channel_mask and bits_per_sample are ints.
|
|
If present, process is waited for when close() is called.
|
|
"""
|
|
|
|
self.file = wave_file
|
|
self.sample_rate = sample_rate
|
|
self.channels = channels
|
|
self.bits_per_sample = bits_per_sample
|
|
self.channel_mask = channel_mask
|
|
|
|
self.process = process
|
|
|
|
#build a capped reader for the data chunk
|
|
try:
|
|
header = WaveAudio.WAVE_HEADER.parse_stream(self.file)
|
|
except Con.ConstError:
|
|
raise InvalidWave(_(u'Invalid WAVE file'))
|
|
except Con.core.FieldError:
|
|
raise InvalidWave(_(u'Invalid WAVE file'))
|
|
|
|
#this won't be pretty for a WAVE file missing a 'data' chunk
|
|
#but those are seriously invalid anyway
|
|
chunk_header = WaveAudio.CHUNK_HEADER.parse_stream(self.file)
|
|
while (chunk_header.chunk_id != 'data'):
|
|
#self.file.seek(chunk_header.chunk_length,1)
|
|
self.file.read(chunk_header.chunk_length)
|
|
chunk_header = WaveAudio.CHUNK_HEADER.parse_stream(self.file)
|
|
|
|
#build a reader which reads no further than the 'data' chunk
|
|
self.wave = __capped_stream_reader__(self.file,
|
|
chunk_header.chunk_length)
|
|
self.data_chunk_length = chunk_header.chunk_length
|
|
|
|
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.wave.read(bytes)
|
|
if ((len(pcm_data) == 0) and (self.data_chunk_length > 0)):
|
|
raise IOError("data chunk ends prematurely")
|
|
else:
|
|
self.data_chunk_length -= len(pcm_data)
|
|
|
|
try:
|
|
return pcm.FrameList(pcm_data,
|
|
self.channels,
|
|
self.bits_per_sample,
|
|
False,
|
|
self.bits_per_sample != 8)
|
|
except ValueError:
|
|
raise IOError("data chunk ends prematurely")
|
|
|
|
def close(self):
|
|
"""Closes the stream for reading.
|
|
|
|
Any subprocess is waited for also so for proper cleanup."""
|
|
|
|
self.wave.close()
|
|
if (self.process is not None):
|
|
if (self.process.wait() != 0):
|
|
raise DecodingError()
|
|
|
|
|
|
class TempWaveReader(WaveReader):
|
|
"""A subclass of WaveReader for reading wave data from temporary files."""
|
|
|
|
def __init__(self, tempfile):
|
|
"""tempfile should be a NamedTemporaryFile.
|
|
|
|
Its contents are used to populate the rest of the fields."""
|
|
|
|
wave = WaveAudio(tempfile.name)
|
|
WaveReader.__init__(self,
|
|
tempfile,
|
|
sample_rate=wave.sample_rate(),
|
|
channels=wave.channels(),
|
|
channel_mask=int(wave.channel_mask()),
|
|
bits_per_sample=wave.bits_per_sample())
|
|
self.tempfile = tempfile
|
|
|
|
def close(self):
|
|
"""Closes the input stream and temporary file."""
|
|
|
|
WaveReader.close(self)
|
|
self.tempfile.close()
|
|
|
|
|
|
class InvalidWave(InvalidFile):
|
|
"""Raises during initialization time if a wave file is invalid."""
|
|
|
|
pass
|
|
|
|
|
|
def __blank_channel_mask__():
|
|
c = Con.Container(undefined=0, undefined2=0)
|
|
|
|
for attr in ('front_right_of_center',
|
|
'front_left_of_center',
|
|
'rear_right',
|
|
'rear_left',
|
|
'LFE',
|
|
'front_center',
|
|
'front_right',
|
|
'front_left',
|
|
'top_back_left',
|
|
'top_front_right',
|
|
'top_front_center',
|
|
'top_front_left',
|
|
'top_center',
|
|
'side_right',
|
|
'side_left',
|
|
'rear_center',
|
|
'top_back_right',
|
|
'top_back_center'):
|
|
setattr(c, attr, False)
|
|
|
|
return c
|
|
|
|
|
|
def __channel_mask__(filename, mask, channel_count):
|
|
mask = ChannelMask(mask)
|
|
c = __blank_channel_mask__()
|
|
|
|
if (mask.defined()):
|
|
attr_map = {"front_left": 'front_left',
|
|
"front_right": 'front_right',
|
|
"front_center": 'front_center',
|
|
"low_frequency": 'LFE',
|
|
"back_left": 'rear_left',
|
|
"back_right": 'rear_right',
|
|
"front_left_of_center": 'front_left_of_center',
|
|
"front_right_of_center": 'front_right_of_center',
|
|
"back_center": 'rear_center',
|
|
"side_left": 'side_left',
|
|
"side_right": 'side_right',
|
|
"top_center": 'top_center',
|
|
"top_front_left": 'top_front_left',
|
|
"top_front_center": 'top_front_center',
|
|
"top_front_right": 'top_front_right',
|
|
"top_back_left": 'top_back_left',
|
|
"top_back_center": 'top_back_center',
|
|
"top_back_right": 'top_back_right'}
|
|
|
|
for channel in mask.channels():
|
|
setattr(c, attr_map[channel], True)
|
|
else:
|
|
attr_map = ['front_left',
|
|
'front_right',
|
|
'front_center',
|
|
'LFE',
|
|
'rear_left',
|
|
'rear_right',
|
|
'front_left_of_center',
|
|
'front_right_of_center',
|
|
'rear_center',
|
|
'side_left',
|
|
'side_right',
|
|
'top_center',
|
|
'top_front_left',
|
|
'top_front_center',
|
|
'top_front_right',
|
|
'top_back_left',
|
|
'top_back_center',
|
|
'top_back_right']
|
|
if (channel_count <= len(attr_map)):
|
|
for channel in attr_map[0:channel_count]:
|
|
setattr(c, channel, True)
|
|
else:
|
|
raise UnsupportedChannelMask(filename, mask)
|
|
|
|
return c
|
|
|
|
|
|
class __ASCII_String__(Con.Validator):
|
|
"""Validates that its data string is printable ASCII."""
|
|
|
|
PRINTABLE_ASCII = set([chr(i) for i in xrange(0x20, 0x7E + 1)])
|
|
|
|
def _validate(self, obj, context):
|
|
return set(obj).issubset(self.PRINTABLE_ASCII)
|
|
|
|
|
|
class WaveAudio(WaveContainer):
|
|
"""A waveform audio file."""
|
|
|
|
SUFFIX = "wav"
|
|
NAME = SUFFIX
|
|
|
|
WAVE_HEADER = Con.Struct("wave_header",
|
|
Con.Const(Con.Bytes("wave_id", 4), 'RIFF'),
|
|
Con.ULInt32("wave_size"),
|
|
Con.Const(Con.Bytes("riff_type", 4), 'WAVE'))
|
|
|
|
CHUNK_HEADER = Con.Struct("chunk_header",
|
|
__ASCII_String__(Con.Bytes("chunk_id", 4)),
|
|
Con.ULInt32("chunk_length"))
|
|
|
|
FMT_CHUNK = Con.Struct("fmt_chunk",
|
|
Con.ULInt16("compression"),
|
|
Con.ULInt16("channels"),
|
|
Con.ULInt32("sample_rate"),
|
|
Con.ULInt32("bytes_per_second"),
|
|
Con.ULInt16("block_align"),
|
|
Con.ULInt16("bits_per_sample"),
|
|
Con.If(lambda ctx: ctx['compression'] == 0xFFFE,
|
|
Con.Embed(
|
|
Con.Struct('extensible',
|
|
Con.ULInt16('cb_size'),
|
|
Con.ULInt16('valid_bits_per_sample'),
|
|
Con.BitStruct('channel_mask',
|
|
#0x80
|
|
Con.Flag('front_right_of_center'),
|
|
|
|
#0x40
|
|
Con.Flag('front_left_of_center'),
|
|
|
|
#0x20
|
|
Con.Flag('rear_right'),
|
|
|
|
#0x10
|
|
Con.Flag('rear_left'),
|
|
|
|
#0x8
|
|
Con.Flag('LFE'),
|
|
|
|
#0x4
|
|
Con.Flag('front_center'),
|
|
|
|
#0x2
|
|
Con.Flag('front_right'),
|
|
|
|
#0x1
|
|
Con.Flag('front_left'),
|
|
|
|
#0x8000
|
|
Con.Flag('top_back_left'),
|
|
|
|
#0x4000
|
|
Con.Flag('top_front_right'),
|
|
|
|
#0x2000
|
|
Con.Flag('top_front_center'),
|
|
|
|
#0x1000
|
|
Con.Flag('top_front_left'),
|
|
|
|
#0x800
|
|
Con.Flag('top_center'),
|
|
|
|
#0x400
|
|
Con.Flag('side_right'),
|
|
|
|
#0x200
|
|
Con.Flag('side_left'),
|
|
|
|
#0x100
|
|
Con.Flag('rear_center'),
|
|
|
|
#0x800000
|
|
#0x400000
|
|
#0x200000
|
|
#0x100000
|
|
#0x80000
|
|
#0x40000
|
|
Con.Bits('undefined', 6),
|
|
|
|
#0x20000
|
|
Con.Flag('top_back_right'),
|
|
|
|
#0x10000
|
|
Con.Flag('top_back_center'),
|
|
|
|
Con.Bits('undefined2', 8)),
|
|
Con.String('sub_format', 16)))))
|
|
|
|
def __init__(self, filename):
|
|
"""filename is a plain string."""
|
|
|
|
AudioFile.__init__(self, filename)
|
|
|
|
self.__wavtype__ = 0
|
|
self.__channels__ = 0
|
|
self.__samplespersec__ = 0
|
|
self.__bytespersec__ = 0
|
|
self.__blockalign__ = 0
|
|
self.__bitspersample__ = 0
|
|
self.__data_size__ = 0
|
|
self.__channel_mask__ = 0
|
|
|
|
self.__chunk_ids__ = []
|
|
|
|
try:
|
|
self.__read_chunks__()
|
|
except Con.ValidationError:
|
|
raise InvalidFile
|
|
except InvalidWave, msg:
|
|
raise InvalidFile(str(msg))
|
|
except IOError, msg:
|
|
raise InvalidFile(str(msg))
|
|
|
|
@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."""
|
|
|
|
header = file.read(12)
|
|
return ((header[0:4] == 'RIFF') and
|
|
(header[8:12] == 'WAVE'))
|
|
|
|
def lossless(self):
|
|
"""Returns True."""
|
|
|
|
return True
|
|
|
|
def has_foreign_riff_chunks(self):
|
|
"""Returns True if the audio file contains non-audio RIFF chunks.
|
|
|
|
During transcoding, if the source audio file has foreign RIFF chunks
|
|
and the target audio format supports foreign RIFF chunks,
|
|
conversion should be routed through .wav conversion
|
|
to avoid losing those chunks."""
|
|
|
|
return set(['fmt ', 'data']) != set(self.__chunk_ids__)
|
|
|
|
def channel_mask(self):
|
|
"""Returns a ChannelMask object of this track's channel layout."""
|
|
|
|
return self.__channel_mask__
|
|
|
|
#Returns the PCMReader object for this WAV's data
|
|
def to_pcm(self):
|
|
"""Returns a PCMReader object containing the track's PCM data."""
|
|
|
|
return WaveReader(file(self.filename, 'rb'),
|
|
sample_rate=self.sample_rate(),
|
|
channels=self.channels(),
|
|
bits_per_sample=self.bits_per_sample(),
|
|
channel_mask=int(self.channel_mask()))
|
|
|
|
#Takes a filename and PCMReader containing WAV data
|
|
#builds a WAV from that data and returns a new WaveAudio object
|
|
@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 WaveAudio object."""
|
|
|
|
try:
|
|
f = file(filename, "wb")
|
|
except IOError, err:
|
|
raise EncodingError(str(err))
|
|
|
|
try:
|
|
header = Con.Container()
|
|
header.wave_id = 'RIFF'
|
|
header.riff_type = 'WAVE'
|
|
header.wave_size = 0
|
|
|
|
fmt_header = Con.Container()
|
|
fmt_header.chunk_id = 'fmt '
|
|
|
|
fmt = Con.Container()
|
|
|
|
if ((pcmreader.channels <= 2) and
|
|
(pcmreader.bits_per_sample <= 16)):
|
|
fmt_header.chunk_length = 16
|
|
fmt.compression = 1
|
|
else:
|
|
fmt_header.chunk_length = 40
|
|
fmt.compression = 0xFFFE
|
|
|
|
fmt.channels = pcmreader.channels
|
|
fmt.sample_rate = pcmreader.sample_rate
|
|
fmt.bytes_per_second = \
|
|
pcmreader.sample_rate * \
|
|
pcmreader.channels * \
|
|
(pcmreader.bits_per_sample / 8)
|
|
fmt.block_align = \
|
|
pcmreader.channels * \
|
|
(pcmreader.bits_per_sample / 8)
|
|
fmt.bits_per_sample = pcmreader.bits_per_sample
|
|
|
|
#these fields only apply to WAVEFORMATEXTENSIBLE Waves
|
|
fmt.cb_size = 22
|
|
fmt.valid_bits_per_sample = pcmreader.bits_per_sample
|
|
fmt.sub_format = "0100000000001000800000aa00389b71".decode('hex')
|
|
if (fmt.compression == 0xFFFE):
|
|
fmt.channel_mask = __channel_mask__(filename,
|
|
pcmreader.channel_mask,
|
|
pcmreader.channels)
|
|
else:
|
|
fmt.channel_mask = __blank_channel_mask__()
|
|
|
|
data_header = Con.Container()
|
|
data_header.chunk_id = 'data'
|
|
data_header.chunk_length = 0
|
|
|
|
#write out the basic headers first
|
|
#we'll be back later to clean up the sizes
|
|
f.write(WaveAudio.WAVE_HEADER.build(header))
|
|
f.write(WaveAudio.CHUNK_HEADER.build(fmt_header))
|
|
f.write(WaveAudio.FMT_CHUNK.build(fmt))
|
|
f.write(WaveAudio.CHUNK_HEADER.build(data_header))
|
|
|
|
#dump pcmreader's FrameLists into the file as little-endian
|
|
try:
|
|
framelist = pcmreader.read(BUFFER_SIZE)
|
|
while (len(framelist) > 0):
|
|
if (framelist.bits_per_sample > 8):
|
|
bytes = framelist.to_bytes(False, True)
|
|
else:
|
|
bytes = framelist.to_bytes(False, False)
|
|
|
|
f.write(bytes)
|
|
data_header.chunk_length += 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
|
|
|
|
#close up the PCM reader and flush our output
|
|
try:
|
|
pcmreader.close()
|
|
except DecodingError, err:
|
|
cls.__unlink__(filename)
|
|
raise EncodingError(err.error_message)
|
|
f.flush()
|
|
|
|
#go back to the beginning the re-write the header
|
|
f.seek(0, 0)
|
|
header.wave_size = 4 + \
|
|
WaveAudio.CHUNK_HEADER.sizeof() + \
|
|
fmt_header.chunk_length + \
|
|
WaveAudio.CHUNK_HEADER.sizeof() + \
|
|
data_header.chunk_length
|
|
|
|
f.write(WaveAudio.WAVE_HEADER.build(header))
|
|
f.write(WaveAudio.CHUNK_HEADER.build(fmt_header))
|
|
f.write(WaveAudio.FMT_CHUNK.build(fmt))
|
|
f.write(WaveAudio.CHUNK_HEADER.build(data_header))
|
|
|
|
finally:
|
|
f.close()
|
|
|
|
return WaveAudio(filename)
|
|
|
|
def to_wave(self, wave_filename, progress=None):
|
|
"""Writes the contents of this file to the given .wav filename string.
|
|
|
|
Raises EncodingError if some error occurs during decoding."""
|
|
|
|
try:
|
|
self.verify()
|
|
except InvalidWave, err:
|
|
raise EncodingError(str(err))
|
|
|
|
try:
|
|
output = file(wave_filename, 'wb')
|
|
input = file(self.filename, 'rb')
|
|
except IOError, msg:
|
|
raise EncodingError(str(msg))
|
|
try:
|
|
transfer_data(input.read, output.write)
|
|
finally:
|
|
input.close()
|
|
output.close()
|
|
|
|
@classmethod
|
|
def from_wave(cls, filename, wave_filename, compression=None,
|
|
progress=None):
|
|
"""Encodes a new AudioFile from an existing .wav file.
|
|
|
|
Takes a filename string, wave_filename string
|
|
of an existing WaveAudio file
|
|
and an optional compression level string.
|
|
Encodes a new audio file from the wave's data
|
|
at the given filename with the specified compression level
|
|
and returns a new WaveAudio object."""
|
|
|
|
try:
|
|
cls(wave_filename).verify()
|
|
except InvalidWave, err:
|
|
raise EncodingError(unicode(err))
|
|
|
|
try:
|
|
input = file(wave_filename, 'rb')
|
|
output = file(filename, 'wb')
|
|
except IOError, err:
|
|
raise EncodingError(str(err))
|
|
try:
|
|
total_bytes = os.path.getsize(wave_filename)
|
|
current_bytes = 0
|
|
s = input.read(4096)
|
|
while (len(s) > 0):
|
|
current_bytes += len(s)
|
|
output.write(s)
|
|
if (progress is not None):
|
|
progress(current_bytes, total_bytes)
|
|
s = input.read(4096)
|
|
output.flush()
|
|
try:
|
|
return WaveAudio(filename)
|
|
except InvalidFile:
|
|
cls.__unlink__(filename)
|
|
raise EncodingError(u"invalid RIFF WAVE source file")
|
|
finally:
|
|
input.close()
|
|
output.close()
|
|
|
|
def convert(self, target_path, target_class, compression=None,
|
|
progress=None):
|
|
"""Encodes a new AudioFile from existing AudioFile.
|
|
|
|
Take a filename string, target class and optional compression string.
|
|
Encodes a new AudioFile in the target class and returns
|
|
the resulting object.
|
|
May raise EncodingError if some problem occurs during encoding."""
|
|
|
|
if (hasattr(target_class, "from_wave")):
|
|
return target_class.from_wave(target_path,
|
|
self.filename,
|
|
compression=compression,
|
|
progress=progress)
|
|
else:
|
|
return target_class.from_pcm(target_path,
|
|
to_pcm_progress(self, progress),
|
|
compression)
|
|
|
|
def total_frames(self):
|
|
"""Returns the total PCM frames of the track as an integer."""
|
|
|
|
return self.__data_size__ / (self.__bitspersample__ / 8) / \
|
|
self.__channels__
|
|
|
|
def sample_rate(self):
|
|
"""Returns the rate of the track's audio as an integer number of Hz."""
|
|
|
|
return self.__samplespersec__
|
|
|
|
def channels(self):
|
|
"""Returns an integer number of channels this track contains."""
|
|
|
|
return self.__channels__
|
|
|
|
def bits_per_sample(self):
|
|
"""Returns an integer number of bits-per-sample this track contains."""
|
|
|
|
return self.__bitspersample__
|
|
|
|
@classmethod
|
|
def can_add_replay_gain(cls):
|
|
"""Returns True if we have the necessary binaries to add ReplayGain."""
|
|
|
|
return True
|
|
|
|
@classmethod
|
|
def lossless_replay_gain(cls):
|
|
"""Returns False."""
|
|
|
|
return False
|
|
|
|
@classmethod
|
|
def add_replay_gain(cls, filenames, progress=None):
|
|
"""Adds ReplayGain values to a list of filename strings.
|
|
|
|
All the filenames must be of this AudioFile type.
|
|
Raises ValueError if some problem occurs during ReplayGain application.
|
|
"""
|
|
|
|
from audiotools.replaygain import ReplayGain, ReplayGainReader
|
|
import tempfile
|
|
|
|
wave_files = [track for track in open_files(filenames) if
|
|
isinstance(track, cls)]
|
|
|
|
track_gains = []
|
|
total_frames = sum([track.total_frames() for track in wave_files]) * 2
|
|
processed_frames = 0
|
|
|
|
#first, calculate the Gain and Peak values from our files
|
|
for original_wave in wave_files:
|
|
try:
|
|
rg = ReplayGain(original_wave.sample_rate())
|
|
except ValueError:
|
|
track_gains.append((None, None))
|
|
pcm = original_wave.to_pcm()
|
|
try:
|
|
try:
|
|
frame = pcm.read(BUFFER_SIZE)
|
|
while (len(frame) > 0):
|
|
processed_frames += frame.frames
|
|
if (progress is not None):
|
|
progress(processed_frames, total_frames)
|
|
rg.update(frame)
|
|
frame = pcm.read(BUFFER_SIZE)
|
|
track_gains.append(rg.title_gain())
|
|
except ValueError:
|
|
track_gains.append((None, None))
|
|
finally:
|
|
pcm.close()
|
|
|
|
#then, apply those Gain and Peak values to our files
|
|
#rewriting the originals in the process
|
|
for (original_wave, (gain, peak)) in zip(wave_files, track_gains):
|
|
if (gain is None):
|
|
continue
|
|
|
|
temp_wav_file = tempfile.NamedTemporaryFile(suffix=".wav")
|
|
try:
|
|
(header, footer) = original_wave.pcm_split()
|
|
temp_wav_file.write(header)
|
|
replaygain_pcm = ReplayGainReader(original_wave.to_pcm(),
|
|
gain, peak)
|
|
frame = replaygain_pcm.read(BUFFER_SIZE)
|
|
while (len(frame) > 0):
|
|
processed_frames += frame.frames
|
|
if (progress is not None):
|
|
progress(processed_frames, total_frames)
|
|
temp_wav_file.write(frame.to_bytes(
|
|
False,
|
|
original_wave.bits_per_sample() > 8))
|
|
frame = replaygain_pcm.read(BUFFER_SIZE)
|
|
|
|
temp_wav_file.write(footer)
|
|
temp_wav_file.seek(0, 0)
|
|
new_wave = open(original_wave.filename, 'wb')
|
|
transfer_data(temp_wav_file.read, new_wave.write)
|
|
new_wave.close()
|
|
finally:
|
|
temp_wav_file.close()
|
|
|
|
@classmethod
|
|
def track_name(cls, file_path, track_metadata=None, format=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.wav"
|
|
return AudioFile.track_name(file_path, track_metadata, format,
|
|
suffix=cls.SUFFIX)
|
|
|
|
def __read_chunks__(self):
|
|
wave_file = file(self.filename, "rb")
|
|
|
|
__chunklist__ = []
|
|
|
|
totalsize = self.__read_wave_header__(wave_file) - 4
|
|
|
|
while (totalsize > 0):
|
|
(chunk_format, chunk_size) = self.__read_chunk_header__(wave_file)
|
|
self.__chunk_ids__.append(chunk_format)
|
|
|
|
__chunklist__.append(chunk_format)
|
|
#Fix odd-sized chunk sizes to be even
|
|
if ((chunk_size & 1) == 1):
|
|
chunk_size += 1
|
|
|
|
if (chunk_format == "fmt "):
|
|
self.__read_format_chunk__(wave_file, chunk_size)
|
|
elif (chunk_format == "data"):
|
|
self.__read_data_chunk__(wave_file, chunk_size)
|
|
else:
|
|
wave_file.seek(chunk_size, 1)
|
|
totalsize -= (chunk_size + 8)
|
|
|
|
def __read_wave_header__(self, wave_file):
|
|
try:
|
|
header = WaveAudio.WAVE_HEADER.parse(wave_file.read(12))
|
|
return header.wave_size
|
|
except Con.ConstError:
|
|
raise InvalidWave(_(u"Not a RIFF WAVE file"))
|
|
except Con.core.FieldError:
|
|
raise InvalidWave(_(u"Invalid RIFF WAVE file"))
|
|
|
|
def __read_chunk_header__(self, wave_file):
|
|
try:
|
|
chunk = WaveAudio.CHUNK_HEADER.parse(wave_file.read(8))
|
|
return (chunk.chunk_id, chunk.chunk_length)
|
|
except Con.core.FieldError:
|
|
raise InvalidWave(_(u"Invalid RIFF WAVE file"))
|
|
|
|
@classmethod
|
|
def fmt_chunk_to_channel_mask(cls, fmt_channel_mask):
|
|
"""Builds a ChannelMask object from Container data.
|
|
|
|
The Container is parsed from fmt_chunk.channel_mask."""
|
|
|
|
channel_mask = ChannelMask(0)
|
|
attr_map = {'front_left': "front_left",
|
|
'front_right': "front_right",
|
|
'front_center': "front_center",
|
|
'LFE': "low_frequency",
|
|
'rear_left': "back_left",
|
|
'rear_right': "back_right",
|
|
'front_left_of_center': "front_left_of_center",
|
|
'front_right_of_center': "front_right_of_center",
|
|
'rear_center': "back_center",
|
|
'side_left': "side_left",
|
|
'side_right': "side_right",
|
|
'top_center': "top_center",
|
|
'top_front_left': "top_front_left",
|
|
'top_front_center': "top_front_center",
|
|
'top_front_right': "top_front_right",
|
|
'top_back_left': "top_back_left",
|
|
'top_back_center': "top_back_center",
|
|
'top_back_right': "top_back_right"}
|
|
for (key, value) in attr_map.items():
|
|
if (getattr(fmt_channel_mask, key)):
|
|
setattr(channel_mask, value, True)
|
|
else:
|
|
setattr(channel_mask, value, False)
|
|
|
|
return channel_mask
|
|
|
|
def __read_format_chunk__(self, wave_file, chunk_size):
|
|
if (chunk_size < 16):
|
|
raise InvalidWave(_(u"fmt chunk is too short"))
|
|
|
|
try:
|
|
fmt = WaveAudio.FMT_CHUNK.parse(wave_file.read(chunk_size))
|
|
except Con.FieldError:
|
|
raise InvalidWave(_(u"fmt chunk is too short"))
|
|
|
|
self.__wavtype__ = fmt.compression
|
|
self.__channels__ = fmt.channels
|
|
self.__samplespersec__ = fmt.sample_rate
|
|
self.__bytespersec__ = fmt.bytes_per_second
|
|
self.__blockalign__ = fmt.block_align
|
|
self.__bitspersample__ = fmt.bits_per_sample
|
|
|
|
if (self.__wavtype__ == 0xFFFE):
|
|
self.__channel_mask__ = WaveAudio.fmt_chunk_to_channel_mask(
|
|
fmt.channel_mask)
|
|
else:
|
|
if (self.__channels__ == 1):
|
|
self.__channel_mask__ = ChannelMask.from_fields(
|
|
front_center=True)
|
|
elif (self.__channels__ == 2):
|
|
self.__channel_mask__ = ChannelMask.from_fields(
|
|
front_left=True, front_right=True)
|
|
#if we have a multi-channel WAVE file
|
|
#that's not WAVEFORMATEXTENSIBLE,
|
|
#assume the channels follow SMPTE/ITU-R recommendations
|
|
#and hope for the best
|
|
elif (self.__channels__ == 3):
|
|
self.__channel_mask__ = ChannelMask.from_fields(
|
|
front_left=True, front_right=True, front_center=True)
|
|
elif (self.__channels__ == 4):
|
|
self.__channel_mask__ = ChannelMask.from_fields(
|
|
front_left=True, front_right=True,
|
|
back_left=True, back_right=True)
|
|
elif (self.__channels__ == 5):
|
|
self.__channel_mask__ = ChannelMask.from_fields(
|
|
front_left=True, front_right=True,
|
|
back_left=True, back_right=True,
|
|
front_center=True)
|
|
elif (self.__channels__ == 6):
|
|
self.__channel_mask__ = ChannelMask.from_fields(
|
|
front_left=True, front_right=True,
|
|
back_left=True, back_right=True,
|
|
front_center=True, low_frequency=True)
|
|
else:
|
|
self.__channel_mask__ = ChannelMask(0)
|
|
|
|
if ((self.__wavtype__ != 1) and (self.__wavtype__ != 0xFFFE)):
|
|
raise InvalidWave(_(u"No support for compressed WAVE files"))
|
|
|
|
def __read_data_chunk__(self, wave_file, chunk_size):
|
|
self.__data_size__ = chunk_size
|
|
wave_file.seek(chunk_size, 1)
|
|
|
|
def chunk_ids(self):
|
|
"""Returns a list of RIFF WAVE chunk ID strings."""
|
|
|
|
return self.__chunk_ids__[:]
|
|
|
|
def chunks(self):
|
|
"""Yields (chunk_id, chunk_data) tuples.
|
|
|
|
chunk_id and chunk_data are both binary strings."""
|
|
|
|
wave_file = file(self.filename, 'rb')
|
|
total_size = self.__read_wave_header__(wave_file) - 4
|
|
|
|
while (total_size > 0):
|
|
(chunk_id, chunk_size) = self.__read_chunk_header__(wave_file)
|
|
|
|
#Fix odd-sized chunks to have 16-bit boundaries
|
|
if ((chunk_size & 1) == 1):
|
|
chunk_size += 1
|
|
|
|
yield (chunk_id, wave_file.read(chunk_size))
|
|
|
|
total_size -= (chunk_size + 8)
|
|
|
|
@classmethod
|
|
def wave_from_chunks(cls, filename, chunk_iter):
|
|
"""Builds a new RIFF WAVE file from a chunk data iterator.
|
|
|
|
filename is the path to the wave file to build.
|
|
chunk_iter should yield (chunk_id, chunk_data) tuples.
|
|
"""
|
|
|
|
f = file(filename, 'wb')
|
|
|
|
header = Con.Container()
|
|
header.wave_id = 'RIFF'
|
|
header.riff_type = 'WAVE'
|
|
header.wave_size = 4
|
|
|
|
#write an unfinished header with an invalid size (for now)
|
|
f.write(cls.WAVE_HEADER.build(header))
|
|
|
|
for (chunk_id, chunk_data) in chunk_iter:
|
|
|
|
#fix odd-sized chunks to fall on 16-bit boundaries
|
|
if ((len(chunk_data) & 1) == 1):
|
|
chunk_data += chr(0)
|
|
|
|
chunk_header = cls.CHUNK_HEADER.build(
|
|
Con.Container(chunk_id=chunk_id,
|
|
chunk_length=len(chunk_data)))
|
|
f.write(chunk_header)
|
|
header.wave_size += len(chunk_header)
|
|
|
|
f.write(chunk_data)
|
|
header.wave_size += len(chunk_data)
|
|
|
|
#now that the chunks are done, go back and re-write the header
|
|
f.seek(0, 0)
|
|
f.write(cls.WAVE_HEADER.build(header))
|
|
f.close()
|
|
|
|
def pcm_split(self):
|
|
"""Returns a pair of data strings before and after PCM data.
|
|
|
|
The first contains all data before the PCM content of the data chunk.
|
|
The second containing all data after the data chunk.
|
|
For example:
|
|
|
|
>>> w = audiotools.open("input.wav")
|
|
>>> (head,tail) = w.pcm_split()
|
|
>>> f = open("output.wav","wb")
|
|
>>> f.write(head)
|
|
>>> audiotools.transfer_framelist_data(w.to_pcm(),f.write)
|
|
>>> f.write(tail)
|
|
>>> f.close()
|
|
|
|
should result in "output.wav" being identical to "input.wav".
|
|
"""
|
|
|
|
head = cStringIO.StringIO()
|
|
tail = cStringIO.StringIO()
|
|
current_block = head
|
|
|
|
wave_file = open(self.filename, 'rb')
|
|
try:
|
|
try:
|
|
#transfer the 12-byte "RIFFsizeWAVE" header to head
|
|
header = WaveAudio.WAVE_HEADER.parse(wave_file.read(12))
|
|
total_size = header.wave_size - 4
|
|
current_block.write(WaveAudio.WAVE_HEADER.build(header))
|
|
except Con.ConstError:
|
|
raise InvalidWave(_(u"Not a RIFF WAVE file"))
|
|
except Con.core.FieldError:
|
|
raise InvalidWave(_(u"Invalid RIFF WAVE file"))
|
|
|
|
while (total_size > 0):
|
|
try:
|
|
#transfer each chunk header
|
|
chunk_header = WaveAudio.CHUNK_HEADER.parse(
|
|
wave_file.read(8))
|
|
current_block.write(WaveAudio.CHUNK_HEADER.build(
|
|
chunk_header))
|
|
total_size -= 8
|
|
except Con.core.FieldError:
|
|
raise InvalidWave(_(u"Invalid RIFF WAVE file"))
|
|
|
|
#and transfer the full content of non-data chunks
|
|
if (chunk_header.chunk_id != "data"):
|
|
current_block.write(
|
|
wave_file.read(chunk_header.chunk_length))
|
|
else:
|
|
wave_file.seek(chunk_header.chunk_length, os.SEEK_CUR)
|
|
current_block = tail
|
|
|
|
total_size -= chunk_header.chunk_length
|
|
|
|
return (head.getvalue(), tail.getvalue())
|
|
finally:
|
|
wave_file.close()
|
|
|
|
def verify(self, progress=None):
|
|
"""Verifies the current file for correctness.
|
|
|
|
Returns True if the file is okay.
|
|
Raises an InvalidFile with an error message if there is
|
|
some problem with the file."""
|
|
|
|
#RIFF WAVE chunk verification is likely to be so fast
|
|
#that individual calls to progress() are
|
|
#a waste of time.
|
|
if (progress is not None):
|
|
progress(0, 1)
|
|
|
|
try:
|
|
f = open(self.filename, 'rb')
|
|
except IOError, msg:
|
|
raise InvalidWave(str(msg))
|
|
|
|
try:
|
|
#check the RIFF WAVE header is correct
|
|
try:
|
|
wave_header = self.WAVE_HEADER.parse_stream(f)
|
|
except (Con.ConstError, Con.FieldError):
|
|
raise InvalidWave(u"error parsing RIFF WAVE header")
|
|
|
|
if (os.path.getsize(self.filename) != (wave_header.wave_size + 8)):
|
|
raise InvalidWave(u"wave file appears truncated")
|
|
|
|
bytes_remaining = wave_header.wave_size - 4
|
|
|
|
fmt_chunk_found = data_chunk_found = False
|
|
|
|
#bounce through all the chunks
|
|
while (bytes_remaining > 0):
|
|
try:
|
|
chunk_header = self.CHUNK_HEADER.parse_stream(f)
|
|
except (Con.FieldError, Con.ValidationError):
|
|
raise InvalidWave(u"error parsing chunk header")
|
|
bytes_remaining -= 8
|
|
|
|
if (chunk_header.chunk_id == 'fmt '):
|
|
#verify the fmt chunk is sane
|
|
try:
|
|
fmt_chunk = self.FMT_CHUNK.parse_stream(f)
|
|
fmt_chunk_found = True
|
|
fmt_chunk_size = len(self.FMT_CHUNK.build(fmt_chunk))
|
|
bytes_remaining -= fmt_chunk_size
|
|
except Con.FieldError:
|
|
raise InvalidWave(u"invalid fmt chunk")
|
|
else:
|
|
if (chunk_header.chunk_id == 'data'):
|
|
data_chunk_found = True
|
|
#verify all other chunks are the correct size
|
|
f.seek(chunk_header.chunk_length, 1)
|
|
bytes_remaining -= chunk_header.chunk_length
|
|
|
|
if (fmt_chunk_found and data_chunk_found):
|
|
if (progress is not None):
|
|
progress(1, 1)
|
|
|
|
return True
|
|
elif (not fmt_chunk_found):
|
|
raise InvalidWave(u"fmt chunk not found")
|
|
else:
|
|
raise InvalidWave(u"data chunk not found")
|
|
finally:
|
|
f.close()
|