Projects
Multimedia
audiotools
Sign Up
Log In
Username
Password
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
Expand all
Collapse all
Changes of Revision 2
View file
audiotools.changes
Changed
@@ -1,4 +1,11 @@ ------------------------------------------------------------------- +Fri Mar 1 02:03:02 UTC 2013 - toddrme2178@gmail.com + +- Update to 2.19 +- Fix building on openSUSE 12.2+ +- Clean up spec file formatting + +------------------------------------------------------------------- Thu Apr 19 02:14:10 CET 2012 - pascal.bleser@opensuse.org - initial version (2.18)
View file
audiotools.spec
Changed
@@ -1,6 +1,6 @@ # vim: set sw=4 ts=4 et: -# Copyright (c) 2012 Pascal Bleser <pascal.bleser@opensuse.org> +# Copyright (c) 2013 Pascal Bleser <pascal.bleser@opensuse.org> # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -13,21 +13,19 @@ # Please submit bugfixes or comments via http://bugs.opensuse.org/ -%{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")} -%{!?python_sitearch: %global python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))")} - Name: audiotools -Version: 2.18 +Version: 2.19 Release: 0 Summary: Audio Tool Collection -Source: http://prdownloads.sourceforge.net/audiotools/audiotools-%{version}.tar.gz +Source: http://prdownloads.sourceforge.net/%{name}/%{name}-%{version}.tar.gz URL: http://audiotools.sourceforge.net/ Group: Productivity/Multimedia/Sound/Utilities License: GPL-2.0+ BuildRoot: %{_tmppath}/build-%{name}-%{version} BuildRequires: python-devel BuildRequires: libcdio-devel -BuildRequires: gcc make glibc-devel pkgconfig +BuildRequires: pkgconfig(libcdio_cdda) +BuildRequires: pkg-config %py_requires %description @@ -43,26 +41,27 @@ Lossless, and more. %prep -%setup -q -n "audiotools-%{version}" +%setup -q %build -%__python ./setup.py build +python ./setup.py build %install -%__python ./setup.py install \ - --prefix="%{_prefix}" \ - --root="%{buildroot}" \ - --record-rpm=files.lst +python setup.py install --prefix=%{_prefix} --root=%{buildroot} -%__perl -ni -e 'print unless m,^(%dir\s+)?%{_sysconfdir},' files.lst +chmod a+x %{buildroot}%{python_sitearch}/audiotools/*.py +chmod a+x %{buildroot}%{python_sitearch}/audiotools/*/*.py %clean -%{?buildroot:%__rm -rf "%{buildroot}"} +rm -rf %{buildroot} -%files -f files.lst +%files %defattr(-,root,root) %doc COPYING TODO %config %{_sysconfdir}/audiotools.cfg +%{_bindir}/* +%{python_sitearch}/Python_Audio_Tools-%{version}-*.egg-info +%{python_sitearch}/audiotools/ %changelog
View file
audiotools-2.18.tar.gz/audiotools/__accuraterip__.py
Deleted
@@ -1,178 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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 DiscID - -class AccurateRipDiscID: - def __init__(self, offsets): - """offsets is a list of CD track offsets, in CD sectors - - these offsets *do not* include the 150 sector lead-in""" - - self.__offsets__ = offsets - - def track_count(self): - return len(self.__offsets__) - 1 - - def id1(self): - return sum(self.__offsets__) - - def id2(self): - return sum([max(offset, 1) * (i + 1) - for (i, offset) in enumerate(self.__offsets__)]) - - def id3(self): - return DiscID( - tracks=[y - x for (x,y) in zip(self.__offsets__, - self.__offsets__[1:])], - offsets=[offset + 150 for offset in self.__offsets__[0:-1]], - length=self.__offsets__[-1]) - - @classmethod - def from_cdda(cls, cdda): - return cls([cdda.cdda.track_offsets(i)[0] for i in - xrange(1, len(cdda) + 2)]) - - @classmethod - def from_tracks(cls, tracks): - offsets = [0] - for track in tracks: - offsets.append(offsets[-1] + track.cd_frames()) - - return cls(offsets) - - def db_filename(self): - return ("dBAR-%(tracks)3.3d-%(id1)8.8x-%(id2)8.8x-%(id3)s.bin" % - {"tracks":self.track_count(), - "id1":self.id1(), - "id2":self.id2(), - "id3":self.id3()}) - - def url(self): - id1 = self.id1() - - return ("http://www.accuraterip.com/accuraterip/%.1x/%.1x/%.1x/%s" % - (id1 & 0xF, - (id1 >> 4) & 0xF, - (id1 >> 8) & 0xF, - self.db_filename())) - - def __repr__(self): - return "AccurateRipDiscID(%s)" % (repr(self.__offsets__)) - - -class AccurateRipEntry: - # ACCURATERIP_DB_ENTRY = Con.GreedyRepeater( - # Con.Struct("db_entry", - # Con.ULInt8("track_count"), - # Con.ULInt32("disc_id1"), - # Con.ULInt32("disc_id2"), - # Con.ULInt32("freedb_id"), - # Con.StrictRepeater(lambda ctx: ctx["track_count"], - # Con.Struct("tracks", - # Con.ULInt8("confidence"), - # Con.ULInt32("crc"), - # Con.ULInt32("crc2"))))) - - def __init__(self, disc_id1, disc_id2, freedb_id, track_entries): - """disc_id1, disc_id2 and freedb_id are ints - - track_entries is a list of lists of AccurateRipTrackEntry objects""" - - self.disc_id1 = disc_id1 - self.disc_id2 = disc_id2 - self.freedb_id = freedb_id - self.track_entries = track_entries - - def __repr__(self): - return "AccurateRipEntry(%s, %s, %s, %s)" % \ - (repr(self.disc_id1), - repr(self.disc_id2), - repr(self.freedb_id), - repr(self.track_entries)) - - def __getitem__(self, key): - #returns a list of 0 or more AccurateRipTrackEntry objects - return self.track_entries[key] - - def __len__(self): - return len(self.track_entries) - - @classmethod - def parse_string(cls, string): - """given a string, returns an AccurateRipEntry object""" - - entries = cls.ACCURATERIP_DB_ENTRY.parse(string) - - if (len(entries) == 0): - raise ValueError("no AccurateRip entries found") - - #all disc IDs should be identical - #and zip the track entries together - return cls( - disc_id1=entries[0].disc_id1, - disc_id2=entries[0].disc_id2, - freedb_id=entries[0].freedb_id, - track_entries=[ - [AccurateRipTrackEntry(confidence=track.confidence, - crc=track.crc, - crc2=track.crc2) for track in tracks] - for tracks in zip(*[entry.tracks for entry in entries])]) - - @classmethod - def from_disc_id(cls, disc_id): - """given an AccurateRipDiscID, returns an AccurateRipEntry - or None if the given disc ID has no matches in the database""" - - import urllib - - response = urllib.urlopen(disc_id.url()) - if (response.getcode() == 200): - return cls.parse_string(response.read()) - else: - return None - - -class AccurateRipTrackEntry: - def __init__(self, confidence, crc, crc2): - self.confidence = confidence #heh - self.crc = crc - self.crc2 = crc2 - - def __repr__(self): - return "AccurateRipTrackEntry(%s, %s, %s)" % \ - (self.confidence, - self.crc, - self.crc2) - -class AccurateRipTrackCRC: - def __init__(self): - self.crc = 0 - self.track_index = 1 - from .cdio import accuraterip_crc - self.accuraterip_crc = accuraterip_crc - - def __int__(self): - return self.crc - - def update(self, frame): - (self.crc, self.track_index) = self.accuraterip_crc(self.crc, - self.track_index, - frame)
View file
audiotools-2.18.tar.gz/audiotools/__aiff__.py
Deleted
@@ -1,919 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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, - __capped_stream_reader__, PCMReaderError, - transfer_data, DecodingError, EncodingError, - ID3v22Comment, BUFFER_SIZE, ChannelMask, - ReorderedPCMReader, pcm, - cStringIO, os, AiffContainer, to_pcm_progress, - LimitedFileReader) -import struct -import gettext - -gettext.install("audiotools", unicode=True) - - -def parse_ieee_extended(bitstream): - """returns a parsed 80-bit IEEE extended value from BitstreamReader - this is used to handle AIFF's sample rate field""" - - (signed, exponent, mantissa) = bitstream.parse("1u 15u 64U") - if ((exponent == 0) and (mantissa == 0)): - return 0 - elif (exponent == 0x7FFF): - return 1.79769313486231e+308 - else: - f = mantissa * (2.0 ** (exponent - 16383 - 63)) - return f if not signed else -f - - -def build_ieee_extended(bitstream, value): - """writes an 80-bit IEEE extended value to BitstreamWriter - this is used to handle AIFF's sample rate field""" - - from math import frexp - - if (value < 0): - signed = 1 - value = abs(value) - else: - signed = 0 - - (fmant, exponent) = frexp(value) - if ((exponent > 16384) or (fmant >= 1)): - exponent = 0x7FFF - mantissa = 0 - else: - exponent += 16382 - mantissa = fmant * (2 ** 64) - - bitstream.build("1u 15u 64U", (signed, exponent, mantissa)) - -####################### -#AIFF -####################### - - -class AIFF_Chunk: - """a raw chunk of AIFF data""" - - def __init__(self, chunk_id, chunk_size, chunk_data): - """chunk_id should be a binary string of ASCII - chunk_size is the length of chunk_data - chunk_data should be a binary string of chunk data""" - - #FIXME - check chunk_id's validity - - self.id = chunk_id - self.__size__ = chunk_size - self.__data__ = chunk_data - - def __repr__(self): - return "AIFF_Chunk(%s)" % (repr(self.id)) - - def size(self): - """returns size of chunk in bytes - not including any spacer byte for odd-sized chunks""" - - return self.__size__ - - def total_size(self): - """returns the total size of the chunk - including the 8 byte ID/size and any padding byte""" - - if (self.__size__ % 2): - return 8 + self.__size__ + 1 - else: - return 8 + self.__size__ - - def data(self): - """returns chunk data as file-like object""" - - return cStringIO.StringIO(self.__data__) - - def verify(self): - """returns True if chunk size matches chunk's data""" - - return self.__size__ == len(self.__data__) - - def write(self, f): - """writes the entire chunk to the given output file object - returns size of entire chunk (including header and spacer) - in bytes""" - - f.write(self.id) - f.write(struct.pack(">I", self.__size__)) - f.write(self.__data__) - if (self.__size__ % 2): - f.write(chr(0)) - return self.total_size() - - -class AIFF_File_Chunk(AIFF_Chunk): - """a raw chunk of AIFF data taken from an existing file""" - - def __init__(self, chunk_id, chunk_size, aiff_file, chunk_data_offset): - """chunk_id should be a binary string of ASCII - chunk_size is the size of the chunk in bytes - (not counting any spacer byte) - aiff_file is the file this chunk belongs to - chunk_data_offset is the offset to the chunk's data bytes - (not including the 8 byte header)""" - - self.id = chunk_id - self.__size__ = chunk_size - self.__aiff_file__ = aiff_file - self.__offset__ = chunk_data_offset - - def __repr__(self): - return "AIFF_File_Chunk(%s)" % (repr(self.id)) - - def data(self): - """returns chunk data as file-like object""" - - self.__wav_file__.seek(self.__offset__) - return LimitedFileReader(self.__wav_file__, self.size()) - - def verify(self): - """returns True if chunk size matches chunk's data""" - - self.__aiff_file__.seek(self.__offset__) - to_read = self.__size__ - while (to_read > 0): - s = self.__aiff_file__.read(min(0x100000, to_read)) - if (len(s) == 0): - return False - else: - to_read -= len(s) - return True - - def write(self, f): - """writes the entire chunk to the given output file object - returns size of entire chunk (including header and spacer) - in bytes""" - - f.write(self.id) - f.write(struct.pack(">I", self.__size__)) - self.__aiff_file__.seek(self.__offset__) - to_write = self.__size__ - while (to_write > 0): - s = self.__aiff_file__.read(min(0x100000, to_write)) - f.write(s) - to_write -= len(s) - - if (self.__size__ % 2): - f.write(chr(0)) - return self.total_size() - - -def parse_comm(comm): - """given a COMM chunk (without the 8 byte name/size header) - returns (channels, total_sample_frames, bits_per_sample, - sample_rate, channel_mask) - where channel_mask is a ChannelMask object and the rest are ints - may raise IOError if an error occurs reading the chunk""" - - (channels, - total_sample_frames, - bits_per_sample) = comm.parse("16u 32u 16u") - sample_rate = int(parse_ieee_extended(comm)) - - if (channels <= 2): - channel_mask = ChannelMask.from_channels(channels) - else: - channel_mask = ChannelMask(0) - - return (channels, total_sample_frames, bits_per_sample, - sample_rate, channel_mask) - - -class AiffReader(PCMReader): - """a subclass of PCMReader for reading AIFF file contents""" - - def __init__(self, aiff_file, - sample_rate, channels, channel_mask, bits_per_sample, - total_frames, process=None): - """aiff_file should be a file-like object of aiff data - - sample_rate, channels, channel_mask and bits_per_sample are ints""" - - self.file = aiff_file - self.sample_rate = sample_rate - self.channels = channels - self.channel_mask = channel_mask - self.bits_per_sample = bits_per_sample - self.remaining_frames = total_frames - self.bytes_per_frame = self.channels * self.bits_per_sample / 8 - - self.process = process - - from .bitstream import BitstreamReader - - #build a capped reader for the ssnd chunk - aiff_reader = BitstreamReader(aiff_file, 0) - try: - (form, aiff) = aiff_reader.parse("4b 32p 4b") - if (form != 'FORM'): - raise InvalidAIFF(_(u"Not an AIFF file")) - elif (aiff != 'AIFF'): - raise InvalidAIFF(_(u"Invalid AIFF file")) - - while (True): - (chunk_id, chunk_size) = aiff_reader.parse("4b 32u") - if (chunk_id == 'SSND'): - #adjust for the SSND alignment - aiff_reader.skip(64) - break - else: - aiff_reader.skip_bytes(chunk_size) - if (chunk_size % 2): - aiff_reader.skip(8) - except IOError: - self.read = self.read_error - - def read(self, bytes): - """try to read a pcm.FrameList of size 'bytes'""" - - #convert bytes to a number of PCM frames - frames_read = min(max(bytes / self.bytes_per_frame, 1), - self.remaining_frames) - - if (frames_read > 0): - pcm_data = self.file.read(frames_read * self.bytes_per_frame) - if (len(pcm_data) < frames_read * self.bytes_per_frame): - raise IOError("ssnd chunk ends prematurely") - else: - framelist = pcm.FrameList(pcm_data, - self.channels, - self.bits_per_sample, - True, True) - self.remaining_frames -= framelist.frames - return framelist - - else: - return pcm.FrameList("", - self.channels, - self.bits_per_sample, - True, True) - - def read_error(self, bytes): - """try to read a pcm.FrameList of size 'bytes'""" - - raise IOError() - - -class InvalidAIFF(InvalidFile): - """raised if some problem occurs parsing AIFF chunks""" - - pass - - -class AiffAudio(AiffContainer): - """an AIFF audio file""" - - SUFFIX = "aiff" - NAME = SUFFIX - - PRINTABLE_ASCII = frozenset([chr(i) for i in xrange(0x20, 0x7E + 1)]) - - def __init__(self, filename): - """filename is a plain string""" - - self.filename = filename - - self.__channels__ = 0 - self.__bits_per_sample__ = 0 - self.__sample_rate__ = 0 - self.__channel_mask__ = ChannelMask(0) - self.__total_sample_frames__ = 0 - - from .bitstream import BitstreamReader - - try: - for chunk in self.chunks(): - if (chunk.id == "COMM"): - try: - (self.__channels__, - self.__total_sample_frames__, - self.__bits_per_sample__, - self.__sample_rate__, - self.__channel_mask__) = parse_comm( - BitstreamReader(chunk.data(), 0)) - break - except IOError: - continue - except IOError: - raise InvalidAIFF("I/O error reading wave") - - 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""" - - return self.__channel_mask__ - - def lossless(self): - """returns True""" - - return True - - def total_frames(self): - """returns the total PCM frames of the track as an integer""" - - return self.__total_sample_frames__ - - def sample_rate(self): - """returns the rate of the track's audio as an integer number of Hz""" - - return self.__sample_rate__ - - @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] == 'FORM') and - (header[8:12] == 'AIFF')) - - def chunks(self): - """yields a AIFF_Chunk compatible objects for each chunk in file""" - - aiff_file = file(self.filename, 'rb') - try: - (form, - total_size, - aiff) = struct.unpack(">4sI4s", aiff_file.read(12)) - except struct.error: - raise InvalidAIFF(_(u"Invalid AIFF file")) - - if (form != 'FORM'): - raise InvalidAIFF(_(u"Not an AIFF file")) - elif (aiff != 'AIFF'): - raise InvalidAIFF(_(u"Invalid AIFF file")) - else: - total_size -= 4 - - while (total_size > 0): - #read the chunk header and ensure its validity - try: - data = aiff_file.read(8) - (chunk_id, - chunk_size) = struct.unpack(">4sI", data) - except struct.error: - raise InvalidAIFF(_(u"Invalid AIFF file")) - - if (not frozenset(chunk_id).issubset(self.PRINTABLE_ASCII)): - raise InvalidAIFF(_(u"Invalid AIFF chunk ID")) - else: - total_size -= 8 - - #yield RIFF_Chunk or RIFF_File_Chunk depending on chunk size - if (chunk_size >= 0x100000): - #if chunk is too large, yield a File_Chunk - yield AIFF_File_Chunk(chunk_id, - chunk_size, - file(self.filename, "rb"), - aiff_file.tell()) - aiff_file.seek(chunk_size, 1) - else: - #otherwise, yield a raw data Chunk - yield AIFF_Chunk(chunk_id, chunk_size, - aiff_file.read(chunk_size)) - - if (chunk_size % 2): - if (len(aiff_file.read(1)) < 1): - raise InvalidAIFF(_(u"Invalid AIFF chunk")) - total_size -= (chunk_size + 1) - else: - total_size -= chunk_size - - @classmethod - def aiff_from_chunks(cls, filename, chunk_iter): - """builds a new AIFF file from a chunk data iterator - - filename is the path to the AIFF file to build - chunk_iter should yield AIFF_Chunk-compatible objects - """ - - aiff_file = file(filename, 'wb') - try: - total_size = 4 - - #write an unfinished header with a placeholder size - aiff_file.write(struct.pack(">4sI4s", "FORM", total_size, "AIFF")) - - #write the individual chunks - for chunk in chunk_iter: - total_size += chunk.write(aiff_file) - - #once the chunks are done, go back and re-write the header - aiff_file.seek(0, 0) - aiff_file.write(struct.pack(">4sI4s", "FORM", total_size, "AIFF")) - finally: - aiff_file.close() - - def get_metadata(self): - """returns a MetaData object, or None - - raises IOError if unable to read the file""" - - from .bitstream import BitstreamReader - - for chunk in self.chunks(): - if (chunk.id == 'ID3 '): - return ID3v22Comment.parse(BitstreamReader(chunk.data(), 0)) - else: - return None - - def update_metadata(self, metadata): - """takes this track's current MetaData object - as returned by get_metadata() and sets this track's metadata - with any fields updated in that object - - raises IOError if unable to write the file - """ - - import tempfile - from .bitstream import BitstreamRecorder - - if (metadata is None): - return - elif (not isinstance(metadata, ID3v22Comment)): - raise ValueError(_(u"metadata not from audio file")) - - def chunk_filter(chunks, id3_chunk_data): - for chunk in chunks: - if (chunk.id == "ID3 "): - yield AIFF_Chunk("ID3 ", - len(id3_chunk_data), - id3_chunk_data) - else: - yield chunk - - #turn our ID3v2.2 tag into a raw binary chunk - id3_chunk = BitstreamRecorder(0) - metadata.build(id3_chunk) - id3_chunk = id3_chunk.data() - - #generate a temporary AIFF file in which our new ID3v2.2 chunk - #replaces the existing ID3v2.2 chunk - new_aiff = tempfile.NamedTemporaryFile(suffix=self.SUFFIX) - self.__class__.aiff_from_chunks(new_aiff.name, - chunk_filter(self.chunks(), - id3_chunk)) - - #replace the existing file with data from the temporary file - new_file = open(new_aiff.name, 'rb') - old_file = open(self.filename, 'wb') - transfer_data(new_file.read, old_file.write) - old_file.close() - new_file.close() - - def set_metadata(self, metadata): - """takes a MetaData object and sets this track's metadata - - this metadata includes track name, album name, and so on - raises IOError if unable to write the file""" - - if (metadata is None): - return - elif (self.get_metadata() is not None): - #current file has metadata, so replace it with new metadata - self.update_metadata(ID3v22Comment.converted(metadata)) - else: - #current file has no metadata, so append new ID3 block - - import tempfile - from .bitstream import BitstreamRecorder - - def chunk_filter(chunks, id3_chunk_data): - for chunk in chunks: - yield chunk - - yield AIFF_Chunk("ID3 ", - len(id3_chunk_data), - id3_chunk_data) - - #turn our ID3v2.2 tag into a raw binary chunk - id3_chunk = BitstreamRecorder(0) - ID3v22Comment.converted(metadata).build(id3_chunk) - id3_chunk = id3_chunk.data() - - #generate a temporary AIFF file in which our new ID3v2.2 chunk - #is appended to the file's set of chunks - new_aiff = tempfile.NamedTemporaryFile(suffix=self.SUFFIX) - self.__class__.aiff_from_chunks(new_aiff.name, - chunk_filter(self.chunks(), - id3_chunk)) - - #replace the existing file with data from the temporary file - new_file = open(new_aiff.name, 'rb') - old_file = open(self.filename, 'wb') - transfer_data(new_file.read, old_file.write) - old_file.close() - new_file.close() - - def delete_metadata(self): - """deletes the track's MetaData - - this removes or unsets tags as necessary in order to remove all data - raises IOError if unable to write the file""" - - def chunk_filter(chunks): - for chunk in chunks: - if (chunk.id == 'ID3 '): - continue - else: - yield chunk - - import tempfile - - new_aiff = tempfile.NamedTemporaryFile(suffix=self.SUFFIX) - self.__class__.aiff_from_chunks(new_aiff.name, - chunk_filter(self.chunks())) - - new_file = open(new_aiff.name, 'rb') - old_file = open(self.filename, 'wb') - transfer_data(new_file.read, old_file.write) - old_file.close() - new_file.close() - - def to_pcm(self): - """returns a PCMReader object containing the track's PCM data""" - - return AiffReader(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()), - total_frames=self.__total_sample_frames__) - - @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 AiffAudio object""" - - from .bitstream import BitstreamWriter - - try: - f = open(filename, 'wb') - aiff = BitstreamWriter(f, 0) - except IOError, msg: - raise EncodingError(str(msg)) - - try: - total_size = 0 - data_size = 0 - total_pcm_frames = 0 - - #write out the basic headers first - #we'll be back later to clean up the sizes - aiff.build("4b 32u 4b", ("FORM", total_size, "AIFF")) - total_size += 4 - - aiff.build("4b 32u", ("COMM", 0x12)) - total_size += 8 - - aiff.build("16u 32u 16u", (pcmreader.channels, - total_pcm_frames, - pcmreader.bits_per_sample)) - build_ieee_extended(aiff, pcmreader.sample_rate) - total_size += 0x12 - - aiff.build("4b 32u", ("SSND", data_size)) - total_size += 8 - - aiff.build("32u 32u", (0, 0)) - data_size += 8 - total_size += 8 - - #dump pcmreader's FrameLists into the file as big-endian - try: - framelist = pcmreader.read(BUFFER_SIZE) - while (len(framelist) > 0): - bytes = framelist.to_bytes(True, True) - f.write(bytes) - - total_size += len(bytes) - data_size += len(bytes) - total_pcm_frames += framelist.frames - - framelist = pcmreader.read(BUFFER_SIZE) - except (IOError, ValueError), err: - cls.__unlink__(filename) - raise EncodingError(str(err)) - except Exception, err: - cls.__unlink__(filename) - raise err - - #handle odd-sized data chunks - if (data_size % 2): - aiff.write(8, 0) - total_size += 1 - - #close the PCM reader and flush our output - try: - pcmreader.close() - except DecodingError, err: - cls.__unlink__(filename) - raise EncodingError(err.error_message) - f.flush() - - if (total_size < (2 ** 32)): - #go back to the beginning and rewrite the header - f.seek(0, 0) - aiff.build("4b 32u 4b", ("FORM", total_size, "AIFF")) - aiff.build("4b 32u", ("COMM", 0x12)) - aiff.build("16u 32u 16u", (pcmreader.channels, - total_pcm_frames, - pcmreader.bits_per_sample)) - build_ieee_extended(aiff, pcmreader.sample_rate) - aiff.build("4b 32u", ("SSND", data_size)) - else: - os.unlink(filename) - raise EncodingError("PCM data too large for aiff file") - - finally: - f.close() - - return AiffAudio(filename) - - def to_aiff(self, aiff_filename, progress=None): - """writes the contents of this file to the given .aiff filename string - - raises EncodingError if some error occurs during decoding""" - - try: - self.verify() - except InvalidAIFF, err: - raise EncodingError(str(err)) - - try: - output = file(aiff_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_aiff(cls, filename, aiff_filename, compression=None, - progress=None): - """encodes a new AiffAudio from an existing .aiff file - - takes a filename string, aiff_filename string - of an existing AiffAudio 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 AudioFile compatible object""" - - try: - cls(aiff_filename).verify() - except InvalidAIFF, err: - raise EncodingError(unicode(err)) - - try: - input = file(aiff_filename, 'rb') - output = file(filename, 'wb') - except IOError, err: - raise EncodingError(str(err)) - try: - total_bytes = os.path.getsize(aiff_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 AiffAudio(filename) - except InvalidFile: - cls.__unlink__(filename) - raise EncodingError(u"invalid AIFF source file") - finally: - input.close() - output.close() - - def convert(self, target_path, target_class, compression=None, - progress=None): - """encodes a new AiffAudio 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_aiff")): - return target_class.from_aiff(target_path, - self.filename, - compression=compression, - progress=progress) - else: - return target_class.from_pcm(target_path, - to_pcm_progress(self, progress), - compression) - - 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: - - >>> a = audiotools.open("input.aiff") - >>> (head, tail) = a.pcm_split() - >>> f = open("output.aiff", "wb") - >>> f.write(head) - >>> audiotools.transfer_framelist_data(a.to_pcm(), f.write, True, True) - >>> f.write(tail) - >>> f.close() - - should result in "output.aiff" being identical to "input.aiff" - """ - - from .bitstream import BitstreamReader - from .bitstream import BitstreamRecorder - - head = BitstreamRecorder(0) - tail = BitstreamRecorder(0) - current_block = head - - aiff_file = BitstreamReader(open(self.filename, 'rb'), 0) - try: - #transfer the 12-byte "RIFFsizeWAVE" header to head - (form, size, aiff) = aiff_file.parse("4b 32u 4b") - if (form != 'FORM'): - raise InvalidAIFF(_(u"Not an AIFF file")) - elif (aiff != 'AIFF'): - raise InvalidAIFF(_(u"Invalid AIFF file")) - else: - current_block.build("4b 32u 4b", (form, size, aiff)) - total_size = size - 4 - - while (total_size > 0): - #transfer each chunk header - (chunk_id, chunk_size) = aiff_file.parse("4b 32u") - if (not frozenset(chunk_id).issubset(self.PRINTABLE_ASCII)): - raise InvalidAIFF(_(u"Invalid AIFF chunk ID")) - else: - current_block.build("4b 32u", (chunk_id, chunk_size)) - total_size -= 8 - - #round up chunk size to 16 bits - if (chunk_size % 2): - chunk_size += 1 - - #and transfer the full content of non-data chunks - if (chunk_id != "SSND"): - current_block.write_bytes(aiff_file.read_bytes(chunk_size)) - else: - #transfer alignment as part of SSND's chunk header - align = aiff_file.parse("32u 32u") - current_block.build("32u 32u", align) - aiff_file.skip_bytes(chunk_size - 8) - current_block = tail - - total_size -= chunk_size - - return (head.data(), tail.data()) - finally: - aiff_file.close() - - def has_foreign_aiff_chunks(self): - """returns True if the audio file contains non-audio AIFF chunks""" - - return (set(['COMM', 'SSND']) != set([c.id for c in self.chunks()])) - - 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""" - - #AIFF chunk verification is likely to be so fast - #that individual calls to progress() are - #a waste of time. - - COMM_found = False - SSND_found = False - - for chunk in self.chunks(): - if (chunk.id == "COMM"): - if (not COMM_found): - COMM_found = True - else: - raise InvalidAIFF(_(u"multiple COMM chunks found")) - - elif (chunk.id == "SSND"): - if (not COMM_found): - raise InvalidAIFF(_(u"SSND chunk found before fmt")) - elif (SSND_found): - raise InvalidAIFF(_(u"multiple SSND chunks found")) - else: - SSND_found = True - - if (not chunk.verify()): - raise InvalidAIFF(_(u"truncated %s chunk found") % - (chunk.id.decode('ascii'))) - - if (not COMM_found): - raise InvalidAIFF(_(u"COMM chunk not found")) - if (not SSND_found): - raise InvalidAIFF(_(u"SSND chunk not found")) - - if (progress is not None): - progress(1, 1) - - return True - - def clean(self, fixes_performed, output_filename=None): - """cleans the file of known data and metadata problems - - fixes_performed is a list-like object which is appended - with Unicode strings of fixed problems - - output_filename is an optional filename of the fixed file - if present, a new AudioFile is returned - otherwise, only a dry-run is performed and no new file is written - - raises IOError if unable to write the file or its metadata - raises ValueError if the file has errors of some sort - """ - - chunk_queue = [] - pending_data = None - - for chunk in self.chunks(): - if (chunk.id == "COMM"): - if ("COMM" in [c.id for c in chunk_queue]): - fixes_performed.append( - _(u"multiple COMM chunks found")) - else: - chunk_queue.append(chunk) - if (pending_data is not None): - chunk_queue.append(pending_data) - pending_data = None - elif (chunk.id == "SSND"): - if ("COMM" not in [c.id for c in chunk_queue]): - fixes_performed.append(_(u"SSND chunk found before fmt")) - pending_data = chunk - elif ("SSND" in [c.id for c in chunk_queue]): - fixes_performed.append(_(u"multiple SSND chunks found")) - else: - chunk_queue.append(chunk) - else: - chunk_queue.append(chunk) - - old_metadata = self.get_metadata() - if (old_metadata is not None): - fixed_metadata = old_metadata.clean(fixes_performed) - else: - fixed_metadata = old_metadata - - if (output_filename is not None): - AiffAudio.aiff_from_chunks(output_filename, chunk_queue) - fixed_aiff = AiffAudio(output_filename) - if (fixed_metadata is not None): - fixed_aiff.update_metadata(fixed_metadata) - return fixed_aiff
View file
audiotools-2.18.tar.gz/audiotools/__ape__.py
Deleted
@@ -1,1021 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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, WaveAudio, InvalidFile, PCMReader, - transfer_data, subprocess, BIN, MetaData, - os, re, TempWaveReader, Image, cStringIO) - -import gettext - -gettext.install("audiotools", unicode=True) - - -#takes a pair of integers for the current and total values -#returns a unicode string of their combined pair -#for example, __number_pair__(2,3) returns u"2/3" -#whereas __number_pair__(4,0) returns u"4" -def __number_pair__(current, total): - if (total == 0): - return u"%d" % (current) - else: - return u"%d/%d" % (current, total) - - -def limited_transfer_data(from_function, to_function, - max_bytes): - """transfers up to max_bytes from from_function to to_function - or as many bytes as from_function generates as strings""" - - BUFFER_SIZE = 0x100000 - s = from_function(BUFFER_SIZE) - while ((len(s) > 0) and (max_bytes > 0)): - if (len(s) > max_bytes): - s = s[0:max_bytes] - to_function(s) - max_bytes -= len(s) - s = from_function(BUFFER_SIZE) - - -####################### -#MONKEY'S AUDIO -####################### - - -class ApeTagItem: - """a single item in the ApeTag, typically a unicode value""" - - FORMAT = "32u [ 1u 2u 29p ]" - - def __init__(self, item_type, read_only, key, data): - """fields are as follows: - - item_type is 0 = UTF-8, 1 = binary, 2 = external, 3 = reserved - read_only is 1 if the item is read only - key is an ASCII string - data is a binary string of the data itself - """ - - self.type = item_type - self.read_only = read_only - self.key = key - self.data = data - - def __eq__(self, item): - for attr in ["type", "read_only", "key", "data"]: - if ((not hasattr(item, attr)) or - (getattr(self, attr) != getattr(item, attr))): - return False - else: - return True - - def total_size(self): - """returns total size of item in bytes""" - - return 4 + 4 + len(self.key) + 1 + len(self.data) - - def copy(self): - """returns a duplicate ApeTagItem""" - - return ApeTagItem(self.type, - self.read_only, - self.key, - self.data) - - def __repr__(self): - return "ApeTagItem(%s,%s,%s,%s)" % \ - (repr(self.type), - repr(self.read_only), - repr(self.key), - repr(self.data)) - - def raw_info_pair(self): - """returns a human-readable key/value pair of item data""" - - if (self.type == 0): # text - if (self.read_only): - return (self.key.decode('ascii'), - u"(read only) %s" % (self.data.decode('utf-8'))) - else: - return (self.key.decode('ascii'), self.data.decode('utf-8')) - elif (self.type == 1): # binary - return (self.key.decode('ascii'), - u"(binary) %d bytes" % (len(self.data))) - elif (self.type == 2): # external - return (self.key.decode('ascii'), - u"(external) %d bytes" % (len(self.data))) - else: # reserved - return (self.key.decode('ascii'), - u"(reserved) %d bytes" % (len(self.data))) - - def __str__(self): - return self.data - - def __unicode__(self): - return self.data.rstrip(chr(0)).decode('utf-8', 'replace') - - @classmethod - def parse(cls, reader): - """returns an ApeTagItem parsed from the given BitstreamReader""" - - (item_value_length, - read_only, - encoding) = reader.parse(cls.FORMAT) - - key = [] - c = reader.read(8) - while (c != 0): - key.append(chr(c)) - c = reader.read(8) - - value = reader.read_bytes(item_value_length) - - return cls(encoding, read_only, "".join(key), value) - - def build(self, writer): - """writes the ApeTagItem values to the given BitstreamWriter""" - - writer.build("%s %db 8u %db" % (self.FORMAT, - len(self.key), - len(self.data)), - (len(self.data), - self.read_only, - self.type, - self.key, 0, self.data)) - - @classmethod - def binary(cls, key, data): - """returns an ApeTagItem of binary data - - key is an ASCII string, data is a binary string""" - - return cls(1, 0, key, data) - - @classmethod - def external(cls, key, data): - """returns an ApeTagItem of external data - - key is an ASCII string, data is a binary string""" - - return cls(2, 0, key, data) - - @classmethod - def string(cls, key, data): - """returns an ApeTagItem of text data - - key is an ASCII string, data is a unicode string""" - - return cls(0, 0, key, data.encode('utf-8', 'replace')) - - -class ApeTag(MetaData): - """a complete APEv2 tag""" - - HEADER_FORMAT = "8b 32u 32u 32u [ 1u 2u 26p 1u 1u 1u ] 64p" - - ITEM = ApeTagItem - - ATTRIBUTE_MAP = {'track_name': 'Title', - 'track_number': 'Track', - 'track_total': 'Track', - 'album_number': 'Media', - 'album_total': 'Media', - 'album_name': 'Album', - 'artist_name': 'Artist', - #"Performer" is not a defined APEv2 key - #it would be nice to have, yet would not be standard - 'performer_name': 'Performer', - 'composer_name': 'Composer', - 'conductor_name': 'Conductor', - 'ISRC': 'ISRC', - 'catalog': 'Catalog', - 'copyright': 'Copyright', - 'publisher': 'Publisher', - 'year': 'Year', - 'date': 'Record Date', - 'comment': 'Comment'} - - INTEGER_ITEMS = ('Track', 'Media') - - def __init__(self, tags, contains_header=True, contains_footer=True): - """constructs an ApeTag from a list of ApeTagItem objects""" - - for tag in tags: - if (not isinstance(tag, ApeTagItem)): - raise ValueError("%s is not ApeTag" % (repr(tag))) - self.__dict__["tags"] = list(tags) - self.__dict__["contains_header"] = contains_header - self.__dict__["contains_footer"] = contains_footer - - def __repr__(self): - return "ApeTag(%s, %s, %s)" % (repr(self.tags), - repr(self.contains_header), - repr(self.contains_footer)) - - def total_size(self): - """returns the minimum size of the total ApeTag, in bytes""" - - size = 0 - if (self.contains_header): - size += 32 - for tag in self.tags: - size += tag.total_size() - if (self.contains_footer): - size += 32 - return size - - def __eq__(self, metadata): - if (isinstance(metadata, ApeTag)): - if (set(self.keys()) != set(metadata.keys())): - return False - - for tag in self.tags: - try: - if (tag.data != metadata[tag.key].data): - return False - except KeyError: - return False - else: - return True - elif (isinstance(metadata, MetaData)): - return MetaData.__eq__(self, metadata) - else: - return False - - def keys(self): - return [tag.key for tag in self.tags] - - def __contains__(self, key): - for tag in self.tags: - if (tag.key == key): - return True - else: - return False - - def __getitem__(self, key): - for tag in self.tags: - if (tag.key == key): - return tag - else: - raise KeyError(key) - - def get(self, key, default): - try: - return self[key] - except KeyError: - return default - - def __setitem__(self, key, value): - for i in xrange(len(self.tags)): - if (self.tags[i].key == key): - self.tags[i] = value - return - else: - self.tags.append(value) - - def index(self, key): - for (i, tag) in enumerate(self.tags): - if (tag.key == key): - return i - else: - raise ValueError(key) - - def __delitem__(self, key): - for i in xrange(len(self.tags)): - if (self.tags[i].key == key): - del(self.tags[i]) - return - else: - raise KeyError(key) - - #if an attribute is updated (e.g. self.track_name) - #make sure to update the corresponding dict pair - def __setattr__(self, key, value): - if (key in self.ATTRIBUTE_MAP): - if (key == 'track_number'): - self['Track'] = self.ITEM.string( - 'Track', __number_pair__(value, self.track_total)) - elif (key == 'track_total'): - self['Track'] = self.ITEM.string( - 'Track', __number_pair__(self.track_number, value)) - elif (key == 'album_number'): - self['Media'] = self.ITEM.string( - 'Media', __number_pair__(value, self.album_total)) - elif (key == 'album_total'): - self['Media'] = self.ITEM.string( - 'Media', __number_pair__(self.album_number, value)) - else: - self[self.ATTRIBUTE_MAP[key]] = self.ITEM.string( - self.ATTRIBUTE_MAP[key], value) - else: - self.__dict__[key] = value - - def __getattr__(self, key): - if (key == 'track_number'): - try: - return int(re.findall('\d+', - unicode(self.get("Track", u"0")))[0]) - except IndexError: - return 0 - elif (key == 'track_total'): - try: - return int(re.findall('\d+/(\d+)', - unicode(self.get("Track", u"0")))[0]) - except IndexError: - return 0 - elif (key == 'album_number'): - try: - return int(re.findall('\d+', - unicode(self.get("Media", u"0")))[0]) - except IndexError: - return 0 - elif (key == 'album_total'): - try: - return int(re.findall('\d+/(\d+)', - unicode(self.get("Media", u"0")))[0]) - except IndexError: - return 0 - elif (key in self.ATTRIBUTE_MAP): - return unicode(self.get(self.ATTRIBUTE_MAP[key], u'')) - elif (key in MetaData.FIELDS): - return u'' - else: - try: - return self.__dict__[key] - except KeyError: - raise AttributeError(key) - - def __delattr__(self, key): - if (key == 'track_number'): - setattr(self, 'track_number', 0) - if ((self.track_number == 0) and (self.track_total == 0)): - del(self['Track']) - elif (key == 'track_total'): - setattr(self, 'track_total', 0) - if ((self.track_number == 0) and (self.track_total == 0)): - del(self['Track']) - elif (key == 'album_number'): - setattr(self, 'album_number', 0) - if ((self.album_number == 0) and (self.album_total == 0)): - del(self['Media']) - elif (key == 'album_total'): - setattr(self, 'album_total', 0) - if ((self.album_number == 0) and (self.album_total == 0)): - del(self['Media']) - elif (key in self.ATTRIBUTE_MAP): - try: - del(self[self.ATTRIBUTE_MAP[key]]) - except ValueError: - pass - elif (key in MetaData.FIELDS): - pass - else: - try: - del(self.__dict__[key]) - except KeyError: - raise AttributeError(key) - - @classmethod - def converted(cls, metadata): - """converts a MetaData object to an ApeTag object""" - - if (metadata is None): - return None - elif (isinstance(metadata, ApeTag)): - return ApeTag([tag.copy() for tag in metadata.tags], - contains_header=metadata.contains_header, - contains_footer=metadata.contains_footer) - else: - tags = cls([]) - for (field, key) in cls.ATTRIBUTE_MAP.items(): - if (field not in cls.INTEGER_FIELDS): - field = unicode(getattr(metadata, field)) - if (len(field) > 0): - tags[key] = cls.ITEM.string(key, field) - - if ((metadata.track_number != 0) or - (metadata.track_total != 0)): - tags["Track"] = cls.ITEM.string( - "Track", __number_pair__(metadata.track_number, - metadata.track_total)) - - if ((metadata.album_number != 0) or - (metadata.album_total != 0)): - tags["Media"] = cls.ITEM.string( - "Media", __number_pair__(metadata.album_number, - metadata.album_total)) - - for image in metadata.images(): - tags.add_image(image) - - return tags - - def raw_info(self): - """returns the ApeTag as a human-readable unicode string""" - - from os import linesep - from . import display_unicode - - #align tag values on the "=" sign - if (len(self.tags) > 0): - max_indent = max([len(display_unicode(tag.raw_info_pair()[0])) - for tag in self.tags]) - tag_strings = [u"%s%s = %s" % - (u" " * (max_indent - len(display_unicode(key))), - key, value) for (key, value) in - [tag.raw_info_pair() for tag in self.tags]] - else: - tag_strings = [] - - return linesep.decode('ascii').join([u"APEv2:"] + tag_strings) - - @classmethod - def supports_images(cls): - """returns True""" - - return True - - def __parse_image__(self, key, type): - data = cStringIO.StringIO(self[key].data) - description = [] - c = data.read(1) - while (c != '\x00'): - description.append(c) - c = data.read(1) - - return Image.new(data.read(), - "".join(description).decode('utf-8', 'replace'), - type) - - def add_image(self, image): - """embeds an Image object in this metadata""" - - if (image.type == 0): - self['Cover Art (front)'] = self.ITEM.binary( - 'Cover Art (front)', - image.description.encode('utf-8', 'replace') + - chr(0) + - image.data) - elif (image.type == 1): - self['Cover Art (back)'] = self.ITEM.binary( - 'Cover Art (back)', - image.description.encode('utf-8', 'replace') + - chr(0) + - image.data) - - def delete_image(self, image): - """deletes an Image object from this metadata""" - - if ((image.type == 0) and 'Cover Art (front)' in self.keys()): - del(self['Cover Art (front)']) - elif ((image.type == 1) and 'Cover Art (back)' in self.keys()): - del(self['Cover Art (back)']) - - def images(self): - """returns a list of embedded Image objects""" - - #APEv2 supports only one value per key - #so a single front and back cover are all that is possible - img = [] - if ('Cover Art (front)' in self.keys()): - img.append(self.__parse_image__('Cover Art (front)', 0)) - if ('Cover Art (back)' in self.keys()): - img.append(self.__parse_image__('Cover Art (back)', 1)) - return img - - @classmethod - def read(cls, apefile): - """returns an ApeTag object from an APEv2 tagged file object - - may return None if the file object has no tag""" - - from .bitstream import BitstreamReader - - apefile.seek(-32, 2) - reader = BitstreamReader(apefile, 1) - - (preamble, - version, - tag_size, - item_count, - read_only, - item_encoding, - is_header, - no_footer, - has_header) = reader.parse(cls.HEADER_FORMAT) - - if ((preamble != "APETAGEX") or (version != 2000)): - return None - - apefile.seek(-tag_size, 2) - - return cls([ApeTagItem.parse(reader) - for i in xrange(item_count)], - contains_header=has_header, - contains_footer=True) - - def build(self, writer): - """outputs an APEv2 tag to writer""" - - from .bitstream import BitstreamRecorder - - tags = BitstreamRecorder(1) - - for tag in self.tags: - tag.build(tags) - - if (self.contains_header): - writer.build(ApeTag.HEADER_FORMAT, - ("APETAGEX", # preamble - 2000, # version - tags.bytes() + 32, # tag size - len(self.tags), # item count - 0, # read only - 0, # encoding - 1, # is header - not self.contains_footer, # no footer - self.contains_header)) # has header - - tags.copy(writer) - if (self.contains_footer): - writer.build(ApeTag.HEADER_FORMAT, - ("APETAGEX", # preamble - 2000, # version - tags.bytes() + 32, # tag size - len(self.tags), # item count - 0, # read only - 0, # encoding - 0, # is header - not self.contains_footer, # no footer - self.contains_header)) # has header - - def clean(self, fixes_applied): - tag_items = [] - for tag in self.tags: - if (tag.type == 0): - text = unicode(tag) - - #check trailing whitespace - fix1 = text.rstrip() - if (fix1 != text): - fixes_applied.append( - _(u"removed trailing whitespace from %(field)s") % - {"field": tag.key.decode('ascii')}) - - #check leading whitespace - fix2 = fix1.lstrip() - if (fix2 != fix1): - fixes_applied.append( - _(u"removed leading whitespace from %(field)s") % - {"field": tag.key.decode('ascii')}) - - if (tag.key in self.INTEGER_ITEMS): - try: - current = int(re.findall('\d+', fix2)[0]) - except IndexError: - current = 0 - try: - total = int(re.findall('\d+/(\d+)', fix2)[0]) - except IndexError: - total = 0 - if (total != 0): - fix3 = u"%d/%d" % (current, total) - else: - fix3 = unicode(current) - if (fix3 != fix2): - fixes_applied.append( - _(u"removed leading zeroes from %(field)s") % - {"field": tag.key.decode('ascii')}) - else: - fix3 = fix2 - - if (len(fix3) > 0): - tag_items.append(ApeTagItem.string(tag.key, fix3)) - else: - fixes_applied.append( - _("removed empty field %(field)s") % - {"field": tag.key.decode('ascii')}) - else: - tag_items.append(tag) - - return self.__class__(tag_items, - self.contains_header, - self.contains_footer) - - -class ApeTaggedAudio: - """a class for handling audio formats with APEv2 tags - - this class presumes there will be a filename attribute which - can be opened and checked for tags, or written if necessary""" - - def get_metadata(self): - """returns an ApeTag object, or None - - raises IOError if unable to read the file""" - - f = file(self.filename, 'rb') - try: - return ApeTag.read(f) - finally: - f.close() - - def update_metadata(self, metadata): - """takes this track's current MetaData object - as returned by get_metadata() and sets this track's metadata - with any fields updated in that object - - raises IOError if unable to write the file - """ - - if (metadata is None): - return - elif (not isinstance(metadata, ApeTag)): - raise ValueError(_(u"metadata not from audio file")) - - from .bitstream import BitstreamReader, BitstreamWriter - - f = file(self.filename, "r+b") - f.seek(-32, 2) - - (preamble, - version, - tag_size, - item_count, - read_only, - item_encoding, - is_header, - no_footer, - has_header) = BitstreamReader(f, 1).parse(ApeTag.HEADER_FORMAT) - - if ((preamble == 'APETAGEX') and (version == 2000)): - if (has_header): - old_tag_size = 32 + tag_size - else: - old_tag_size = tag_size - - if (metadata.total_size() >= old_tag_size): - #metadata has grown - #so append it to existing file - f.seek(-old_tag_size, 2) - metadata.build(BitstreamWriter(f, 1)) - else: - #metadata has shrunk - #so rewrite file with smaller metadata - import tempfile - from os.path import getsize - rewritten = tempfile.TemporaryFile() - - #copy everything but the last "old_tag_size" bytes - #from existing file to rewritten file - f = open(self.filename, "rb") - limited_transfer_data(f.read, rewritten.write, - os.path.getsize(self.filename) - - old_tag_size) - f.close() - - #append new tag to rewritten file - metadata.build(BitstreamWriter(rewritten, 1)) - - #finally, overwrite current file with rewritten file - rewritten.seek(0, 0) - f = open(self.filename, "wb") - transfer_data(rewritten.read, f.write) - f.close() - rewritten.close() - else: - #no existing metadata, so simply append a fresh tag - f = file(self.filename, "ab") - metadata.build(BitstreamWriter(f, 1)) - f.close() - - def set_metadata(self, metadata): - """takes a MetaData object and sets this track's metadata - - raises IOError if unable to write the file""" - - if (metadata is None): - return - - from .bitstream import BitstreamWriter - - old_metadata = self.get_metadata() - new_metadata = ApeTag.converted(metadata) - - if (old_metadata is not None): - #transfer ReplayGain tags from old metadata to new metadata - for tag in ["replaygain_track_gain", - "replaygain_track_peak", - "replaygain_album_gain", - "replaygain_album_peak"]: - try: - #if old_metadata has tag, shift it over - new_metadata[tag] = old_metadata[tag] - except KeyError: - try: - #otherwise, if new_metadata has tag, delete it - del(new_metadata[tag]) - except KeyError: - #if neither has tag, ignore it - continue - - #transfer Cuesheet from old metadata to new metadata - if ("Cuesheet" in old_metadata): - new_metadata["Cuesheet"] = old_metadata["Cuesheet"] - elif ("Cuesheet" in new_metadata): - del(new_metadata["Cuesheet"]) - - self.update_metadata(new_metadata) - else: - #delete ReplayGain tags from new metadata - for tag in ["replaygain_track_gain", - "replaygain_track_peak", - "replaygain_album_gain", - "replaygain_album_peak"]: - try: - del(new_metadata[tag]) - except KeyError: - continue - - #delete Cuesheet from new metadata - if ("Cuesheet" in new_metadata): - del(new_metadata["Cuesheet"]) - - #no existing metadata, so simply append a fresh tag - f = file(self.filename, "ab") - new_metadata.build(BitstreamWriter(f, 1)) - f.close() - - def delete_metadata(self): - """deletes the track's MetaData - - raises IOError if unable to write the file""" - - from .bitstream import BitstreamReader, BitstreamWriter - - f = file(self.filename, "r+b") - f.seek(-32, 2) - - (preamble, - version, - tag_size, - item_count, - read_only, - item_encoding, - is_header, - no_footer, - has_header) = BitstreamReader(f, 1).parse(ApeTag.HEADER_FORMAT) - - if ((preamble == 'APETAGEX') and (version == 2000)): - #there's existing metadata to delete - #so rewrite file without trailing metadata tag - if (has_header): - old_tag_size = 32 + tag_size - else: - old_tag_size = tag_size - - import tempfile - from os.path import getsize - rewritten = tempfile.TemporaryFile() - - #copy everything but the last "old_tag_size" bytes - #from existing file to rewritten file - f = open(self.filename, "rb") - limited_transfer_data(f.read, rewritten.write, - os.path.getsize(self.filename) - - old_tag_size) - f.close() - - #finally, overwrite current file with rewritten file - rewritten.seek(0, 0) - f = open(self.filename, "wb") - transfer_data(rewritten.read, f.write) - f.close() - rewritten.close() - - -class ApeAudio(ApeTaggedAudio, AudioFile): - """a Monkey's Audio file""" - - SUFFIX = "ape" - NAME = SUFFIX - DEFAULT_COMPRESSION = "5000" - COMPRESSION_MODES = tuple([str(x * 1000) for x in range(1, 6)]) - BINARIES = ("mac",) - - # FILE_HEAD = Con.Struct("ape_head", - # Con.String('id', 4), - # Con.ULInt16('version')) - - # #version >= 3.98 - # APE_DESCRIPTOR = Con.Struct("ape_descriptor", - # Con.ULInt16('padding'), - # Con.ULInt32('descriptor_bytes'), - # Con.ULInt32('header_bytes'), - # Con.ULInt32('seektable_bytes'), - # Con.ULInt32('header_data_bytes'), - # Con.ULInt32('frame_data_bytes'), - # Con.ULInt32('frame_data_bytes_high'), - # Con.ULInt32('terminating_data_bytes'), - # Con.String('md5', 16)) - - # APE_HEADER = Con.Struct("ape_header", - # Con.ULInt16('compression_level'), - # Con.ULInt16('format_flags'), - # Con.ULInt32('blocks_per_frame'), - # Con.ULInt32('final_frame_blocks'), - # Con.ULInt32('total_frames'), - # Con.ULInt16('bits_per_sample'), - # Con.ULInt16('number_of_channels'), - # Con.ULInt32('sample_rate')) - - # #version <= 3.97 - # APE_HEADER_OLD = Con.Struct("ape_header_old", - # Con.ULInt16('compression_level'), - # Con.ULInt16('format_flags'), - # Con.ULInt16('number_of_channels'), - # Con.ULInt32('sample_rate'), - # Con.ULInt32('header_bytes'), - # Con.ULInt32('terminating_bytes'), - # Con.ULInt32('total_frames'), - # Con.ULInt32('final_frame_blocks')) - - def __init__(self, filename): - """filename is a plain string""" - - AudioFile.__init__(self, filename) - - (self.__samplespersec__, - self.__channels__, - self.__bitspersample__, - self.__totalsamples__) = ApeAudio.__ape_info__(filename) - - @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) == "MAC " - - def lossless(self): - """returns True""" - - return True - - @classmethod - def supports_foreign_riff_chunks(cls): - """returns True""" - - return True - - def has_foreign_riff_chunks(self): - """returns True""" - - #FIXME - this isn't strictly true - #I'll need a way to detect foreign chunks in APE's stream - #without decoding it first, - #but since I'm not supporting APE anyway, I'll take the lazy way out - return True - - def bits_per_sample(self): - """returns an integer number of bits-per-sample this track contains""" - - return self.__bitspersample__ - - def channels(self): - """returns an integer number of channels this track contains""" - - return self.__channels__ - - def total_frames(self): - """returns the total PCM frames of the track as an integer""" - - return self.__totalsamples__ - - def sample_rate(self): - """returns the rate of the track's audio as an integer number of Hz""" - - return self.__samplespersec__ - - @classmethod - def __ape_info__(cls, filename): - f = file(filename, 'rb') - try: - file_head = cls.FILE_HEAD.parse_stream(f) - - if (file_head.id != 'MAC '): - raise InvalidFile(_(u"Invalid Monkey's Audio header")) - - if (file_head.version >= 3980): # the latest APE file type - descriptor = cls.APE_DESCRIPTOR.parse_stream(f) - header = cls.APE_HEADER.parse_stream(f) - - return (header.sample_rate, - header.number_of_channels, - header.bits_per_sample, - ((header.total_frames - 1) * \ - header.blocks_per_frame) + \ - header.final_frame_blocks) - else: # old-style APE file (obsolete) - header = cls.APE_HEADER_OLD.parse_stream(f) - - if (file_head.version >= 3950): - blocks_per_frame = 0x48000 - elif ((file_head.version >= 3900) or - ((file_head.version >= 3800) and - (header.compression_level == 4000))): - blocks_per_frame = 0x12000 - else: - blocks_per_frame = 0x2400 - - if (header.format_flags & 0x01): - bits_per_sample = 8 - elif (header.format_flags & 0x08): - bits_per_sample = 24 - else: - bits_per_sample = 16 - - return (header.sample_rate, - header.number_of_channels, - bits_per_sample, - ((header.total_frames - 1) * \ - blocks_per_frame) + \ - header.final_frame_blocks) - - finally: - f.close() - - def to_wave(self, wave_filename): - """writes the contents of this file to the given .wav filename string - - raises EncodingError if some error occurs during decoding""" - - if (self.filename.endswith(".ape")): - devnull = file(os.devnull, "wb") - sub = subprocess.Popen([BIN['mac'], - self.filename, - wave_filename, - '-d'], - stdout=devnull, - stderr=devnull) - sub.wait() - devnull.close() - else: - devnull = file(os.devnull, 'ab') - import tempfile - ape = tempfile.NamedTemporaryFile(suffix='.ape') - f = file(self.filename, 'rb') - transfer_data(f.read, ape.write) - f.close() - ape.flush() - sub = subprocess.Popen([BIN['mac'], - ape.name, - wave_filename, - '-d'], - stdout=devnull, - stderr=devnull) - sub.wait() - ape.close() - devnull.close() - - @classmethod - def from_wave(cls, filename, wave_filename, compression=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 ApeAudio object""" - - if (str(compression) not in cls.COMPRESSION_MODES): - compression = cls.DEFAULT_COMPRESSION - - devnull = file(os.devnull, "wb") - sub = subprocess.Popen([BIN['mac'], - wave_filename, - filename, - "-c%s" % (compression)], - stdout=devnull, - stderr=devnull) - sub.wait() - devnull.close() - return ApeAudio(filename)
View file
audiotools-2.18.tar.gz/audiotools/__au__.py
Deleted
@@ -1,264 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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, - transfer_data, InvalidFormat, - __capped_stream_reader__, BUFFER_SIZE, - FILENAME_FORMAT, EncodingError, DecodingError, - ChannelMask, os) -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 - - def __init__(self, filename): - AudioFile.__init__(self, filename) - - from .bitstream import BitstreamReader - - try: - f = file(filename, 'rb') - - (magic_number, - self.__data_offset__, - self.__data_size__, - encoding_format, - self.__sample_rate__, - self.__channels__) = BitstreamReader(f, 0).parse( - "4b 32u 32u 32u 32u 32u") - except IOError, msg: - raise InvalidAU(str(msg)) - - if (magic_number != '.snd'): - raise InvalidAU(_(u"Invalid Sun AU header")) - try: - self.__bits_per_sample__ = {2: 8, 3: 16, 4: 24}[encoding_format] - except KeyError: - raise InvalidAU(_(u"Unsupported encoding format")) - - @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.__data_size__ / - (self.__bits_per_sample__ / 8) / - self.__channels__) - - 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()) - - 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""" - - import struct - - f = file(self.filename, 'rb') - (magic_number, data_offset) = struct.unpack(">4sI", f.read(8)) - header = f.read(data_offset - struct.calcsize(">4sI")) - return (struct.pack(">4sI%ds" % (len(header)), - magic_number, data_offset, header), "") - - @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""" - - from .bitstream import BitstreamWriter - - if (pcmreader.bits_per_sample not in (8, 16, 24)): - raise InvalidFormat( - _(u"Unsupported bits per sample %s") % ( - pcmreader.bits_per_sample)) - - data_size = 0 - encoding_format = {8: 2, 16: 3, 24: 4}[pcmreader.bits_per_sample] - - try: - f = file(filename, 'wb') - au = BitstreamWriter(f, 0) - except IOError, err: - raise EncodingError(str(err)) - try: - #write a dummy header - au.build("4b 32u 32u 32u 32u 32u", - (".snd", 24, data_size, encoding_format, - pcmreader.sample_rate, pcmreader.channels)) - - #write our big-endian PCM data - try: - framelist = pcmreader.read(BUFFER_SIZE) - while (len(framelist) > 0): - bytes = framelist.to_bytes(True, True) - f.write(bytes) - 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 - - if (data_size < 2 ** 32): - #rewind and write a complete header - f.seek(0, 0) - au.build("4b 32u 32u 32u 32u 32u", - (".snd", 24, data_size, encoding_format, - pcmreader.sample_rate, pcmreader.channels)) - else: - os.unlink(filename) - raise EncodingError("PCM data too large for Sun AU file") - 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)
View file
audiotools-2.18.tar.gz/audiotools/__dvda__.py
Deleted
@@ -1,565 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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 re, os, pcm, cStringIO, struct -from .bitstream import BitstreamReader - - -class DVDAudio: - """an object representing an entire DVD-Audio disc - - a DVDAudio object contains one or more DVDATitle objects - (accessible via the .titlesets attribute) - typically, only the first DVDTitle is interesting - each DVDATitle then contains one or more DVDATrack objects - """ - - SECTOR_SIZE = 2048 - PTS_PER_SECOND = 90000 - - def __init__(self, audio_ts_path, cdrom_device=None): - """a DVD-A which contains PCMReader-compatible track objects""" - - self.audio_ts_path = audio_ts_path - self.cdrom_device = cdrom_device - - #an inventory of AUDIO_TS files converted to uppercase keys - self.files = dict([(name.upper(), - os.path.join(audio_ts_path, name)) - for name in os.listdir(audio_ts_path)]) - - titleset_numbers = list(self.__titlesets__()) - - #for each titleset, read an ATS_XX_0.IFO file - #each titleset contains one or more DVDATitle objects - #and each DVDATitle object contains one or more DVDATrack objects - self.titlesets = [self.__titles__(titleset) for titleset in - titleset_numbers] - - #for each titleset, calculate the lengths of the corresponding AOBs - #in terms of 2048 byte sectors - self.aob_sectors = [] - for titleset in titleset_numbers: - aob_re = re.compile("ATS_%2.2d_\\d\\.AOB" % (titleset)) - titleset_aobs = dict([(key, value) for (key, value) in - self.files.items() - if (aob_re.match(key))]) - for aob_length in [os.path.getsize(titleset_aobs[key]) / - DVDAudio.SECTOR_SIZE - for key in sorted(titleset_aobs.keys())]: - if (len(self.aob_sectors) == 0): - self.aob_sectors.append( - (0, aob_length)) - else: - self.aob_sectors.append( - (self.aob_sectors[-1][1], - self.aob_sectors[-1][1] + aob_length)) - - def __getitem__(self, key): - return self.titlesets[key] - - def __len__(self): - return len(self.titlesets) - - def __titlesets__(self): - """return valid audio titleset integers from AUDIO_TS.IFO""" - - try: - f = open(self.files['AUDIO_TS.IFO'], 'rb') - except (KeyError, IOError): - raise InvalidDVDA(_(u"unable to open AUDIO_TS.IFO")) - try: - (identifier, - AMG_start_sector, - AMGI_end_sector, - DVD_version, - volume_count, - volume_number, - disc_side, - autoplay, - ts_to_sv, - video_titlesets, - audio_titlesets, - provider_information) = BitstreamReader(f, 0).parse( - "12b 32u 12P 32u 16u 4P 16u 16u 8u 4P 8u 32u 10P 8u 8u 40b") - - if (identifier != 'DVDAUDIO-AMG'): - raise InvalidDVDA(_(u"invalid AUDIO_TS.IFO")) - - for titleset in xrange(1, audio_titlesets + 1): - #ensure there are IFO files and AOBs - #for each valid titleset - if (("ATS_%2.2d_0.IFO" % (titleset) in - self.files.keys()) and - ("ATS_%2.2d_1.AOB" % (titleset) in - self.files.keys())): - yield titleset - finally: - f.close() - - def __titles__(self, titleset): - """returns a list of DVDATitle objects for the given titleset""" - - #this requires bouncing all over the ATS_XX_0.IFO file - - try: - f = open(self.files['ATS_%2.2d_0.IFO' % (titleset)], 'rb') - except (KeyError, IOError): - raise InvalidDVDA( - _(u"unable to open ATS_%2.2d_0.IFO") % (titleset)) - try: - #ensure the file's identifier is correct - #which is all we care about from the first sector - if (f.read(12) != 'DVDAUDIO-ATS'): - raise InvalidDVDA(_(u"invalid ATS_%2.2d_0.IFO") % (titleset)) - - #seek to the second sector and read the title count - #and list of title table offset values - f.seek(DVDAudio.SECTOR_SIZE, os.SEEK_SET) - ats_reader = BitstreamReader(f, 0) - (title_count, last_byte_address) = ats_reader.parse("16u 16p 32u") - title_offsets = [ats_reader.parse("8u 24p 32u")[1] for title in - xrange(title_count)] - - titles = [] - - for (title_number, title_offset) in enumerate(title_offsets): - #for each title, seek to its title table - #and read the title's values and its track timestamps - f.seek(DVDAudio.SECTOR_SIZE + title_offset, os.SEEK_SET) - ats_reader = BitstreamReader(f, 0) - (tracks, - indexes, - track_length, - sector_pointers_table) = ats_reader.parse( - "16p 8u 8u 32u 4P 16u 2P") - timestamps = [ats_reader.parse("32p 8u 8p 32u 32u 48p") - for track in xrange(tracks)] - - #seek to the title's sector pointers table - #and read the first and last sector values for title's tracks - f.seek(DVDAudio.SECTOR_SIZE + - title_offset + - sector_pointers_table, - os.SEEK_SET) - ats_reader = BitstreamReader(f, 0) - sector_pointers = [ats_reader.parse("32u 32u 32u") - for i in xrange(indexes)] - if ((len(sector_pointers) > 1) and - (set([p[0] for p in sector_pointers[1:]]) != - set([0x01000000]))): - raise InvalidDVDA(_(u"invalid sector pointer")) - else: - sector_pointers = [None] + sector_pointers - - #build a preliminary DVDATitle object - #which we'll populate with track data - dvda_title = DVDATitle(dvdaudio=self, - titleset=titleset, - title=title_number + 1, - pts_length=track_length, - tracks=[]) - - #for each track, determine its first and last sector - #based on the sector pointers between the track's - #initial index and the next track's initial index - for (track_number, - (timestamp, - next_timestamp)) in enumerate(zip(timestamps, - timestamps[1:])): - (index_number, first_pts, pts_length) = timestamp - next_timestamp_index = next_timestamp[0] - dvda_title.tracks.append( - DVDATrack( - dvdaudio=self, - titleset=titleset, - title=dvda_title, - track=track_number + 1, - first_pts=first_pts, - pts_length=pts_length, - first_sector=sector_pointers[index_number][1], - last_sector=sector_pointers[ - next_timestamp_index - 1][2])) - - #for the last track, its sector pointers - #simply consume what remains on the list - (index_number, first_pts, pts_length) = timestamps[-1] - dvda_title.tracks.append( - DVDATrack( - dvdaudio=self, - titleset=titleset, - title=dvda_title, - track=len(timestamps), - first_pts=first_pts, - pts_length=pts_length, - first_sector=sector_pointers[index_number][1], - last_sector=sector_pointers[-1][2])) - - #fill in the title's info such as sample_rate, channels, etc. - dvda_title.__parse_info__() - - titles.append(dvda_title) - - return titles - finally: - f.close() - - -class InvalidDVDA(Exception): - pass - - -class DVDATitle: - """an object representing a DVD-Audio title - - contains one or more DVDATrack objects - which may are accessible via __getitem__ - """ - - def __init__(self, dvdaudio, titleset, title, pts_length, tracks): - """length is in PTS ticks, tracks is a list of DVDATrack objects""" - - self.dvdaudio = dvdaudio - self.titleset = titleset - self.title = title - self.pts_length = pts_length - self.tracks = tracks - - self.sample_rate = 0 - self.channels = 0 - self.channel_mask = 0 - self.bits_per_sample = 0 - self.stream_id = 0 - - def __parse_info__(self): - """generates a cache of sample_rate, bits-per-sample, etc""" - - if (len(self.tracks) == 0): - return - - #Why is this post-init processing necessary? - #DVDATrack references DVDATitle - #so a DVDATitle must exist when DVDATrack is initialized. - #But because reading this info requires knowing the sector - #of the first track, we wind up with a circular dependency. - #Doing a "post-process" pass fixes that problem. - - #find the AOB file of the title's first track - track_sector = self[0].first_sector - titleset = re.compile("ATS_%2.2d_\\d\\.AOB" % (self.titleset)) - for aob_path in sorted([self.dvdaudio.files[key] for key in - self.dvdaudio.files.keys() - if (titleset.match(key))]): - aob_sectors = os.path.getsize(aob_path) / DVDAudio.SECTOR_SIZE - if (track_sector > aob_sectors): - track_sector -= aob_sectors - else: - break - else: - raise ValueError(_(u"unable to find track sector in AOB files")) - - #open that AOB file and seek to that track's first sector - aob_file = open(aob_path, 'rb') - try: - aob_file.seek(track_sector * DVDAudio.SECTOR_SIZE) - aob_reader = BitstreamReader(aob_file, 0) - - #read and validate the pack header - #(there's one pack header per sector, at the sector's start) - (sync_bytes, - marker1, - current_pts_high, - marker2, - current_pts_mid, - marker3, - current_pts_low, - marker4, - scr_extension, - marker5, - bit_rate, - marker6, - stuffing_length) = aob_reader.parse( - "32u 2u 3u 1u 15u 1u 15u 1u 9u 1u 22u 2u 5p 3u") - aob_reader.skip_bytes(stuffing_length) - if (sync_bytes != 0x1BA): - raise InvalidDVDA(_(u"invalid AOB sync bytes")) - if ((marker1 != 1) or (marker2 != 1) or (marker3 != 1) or - (marker4 != 1) or (marker5 != 1) or (marker6 != 3)): - raise InvalidDVDA(_(u"invalid AOB marker bits")) - packet_pts = ((current_pts_high << 30) | - (current_pts_mid << 15) | - current_pts_low) - - #skip packets until one with a stream ID of 0xBD is found - (start_code, - stream_id, - packet_length) = aob_reader.parse("24u 8u 16u") - if (start_code != 1): - raise InvalidDVDA(_(u"invalid AOB packet start code")) - while (stream_id != 0xBD): - aob_reader.skip_bytes(packet_length) - (start_code, - stream_id, - packet_length) = aob_reader.parse("24u 8u 16u") - if (start_code != 1): - raise InvalidDVDA(_(u"invalid AOB packet start code")) - - #parse the PCM/MLP header in the packet data - (pad1_size,) = aob_reader.parse("16p 8u") - aob_reader.skip_bytes(pad1_size) - (stream_id, crc) = aob_reader.parse("8u 8u 8p") - if (stream_id == 0xA0): # PCM - #read a PCM reader - (pad2_size, - first_audio_frame, - padding2, - group1_bps, - group2_bps, - group1_sample_rate, - group2_sample_rate, - padding3, - channel_assignment) = aob_reader.parse( - "8u 16u 8u 4u 4u 4u 4u 8u 8u") - else: # MLP - aob_reader.skip_bytes(aob_reader.read(8)) # skip pad2 - #read a total frame size + MLP major sync header - (total_frame_size, - sync_words, - stream_type, - group1_bps, - group2_bps, - group1_sample_rate, - group2_sample_rate, - unknown1, - channel_assignment, - unknown2) = aob_reader.parse( - "4p 12u 16p 24u 8u 4u 4u 4u 4u 11u 5u 48u") - - #return the values indicated by the header - self.sample_rate = DVDATrack.SAMPLE_RATE[group1_sample_rate] - self.channels = DVDATrack.CHANNELS[channel_assignment] - self.channel_mask = DVDATrack.CHANNEL_MASK[channel_assignment] - self.bits_per_sample = DVDATrack.BITS_PER_SAMPLE[group1_bps] - self.stream_id = stream_id - - finally: - aob_file.close() - - def __len__(self): - return len(self.tracks) - - def __getitem__(self, index): - return self.tracks[index] - - def __repr__(self): - return "DVDATitle(%s)" % \ - (",".join(["%s=%s" % (key, getattr(self, key)) - for key in ["titleset", "title", "pts_length", - "tracks"]])) - - def info(self): - """returns a (sample_rate, channels, channel_mask, bps, type) tuple""" - - return (self.sample_rate, - self.channels, - self.channel_mask, - self.bits_per_sample, - self.stream_id) - - def to_pcm(self): - """returns a PCMReader-compatible object of Title data - this PCMReader requires the use of its next_track() method - which indicates the total number of PTS ticks to read - from the next track in the title""" - - from audiotools.decoders import DVDA_Title - - args = {"audio_ts": self.dvdaudio.audio_ts_path, - "titleset": 1, - "start_sector": self.tracks[0].first_sector, - "end_sector": self.tracks[-1].last_sector} - if (self.dvdaudio.cdrom_device is not None): - args["cdrom"] = self.dvdaudio.cdrom_device - return DVDA_Title(**args) - - def total_frames(self): - """returns the title's total PCM frames as an integer""" - - import decimal - - return int((decimal.Decimal(self.pts_length) / - DVDAudio.PTS_PER_SECOND * - self.sample_rate).quantize( - decimal.Decimal(1), - rounding=decimal.ROUND_UP)) - - def metadata_lookup(self, musicbrainz_server="musicbrainz.org", - musicbrainz_port=80, - freedb_server="us.freedb.org", - freedb_port=80, - use_musicbrainz=True, - use_freedb=True): - """generates a set of MetaData objects from DVD title - - returns a metadata[c][t] list of lists - where 'c' is a possible choice - and 't' is the MetaData for a given track (starting from 0) - - this will always return at least one choice, - which may be a list of largely empty MetaData objects - if no match can be found for the title""" - - from audiotools import metadata_lookup - - PTS_PER_FRAME = DVDAudio.PTS_PER_SECOND / 75 - - offsets = [150] - for track in self.tracks[0:-1]: - offsets.append(offsets[-1] + (track.pts_length / PTS_PER_FRAME)) - - return metadata_lookup(first_track_number=1, - last_track_number=len(self), - offsets=offsets, - lead_out_offset=self.pts_length / PTS_PER_FRAME, - total_length=self.pts_length / PTS_PER_FRAME, - musicbrainz_server=musicbrainz_server, - musicbrainz_port=musicbrainz_port, - freedb_server=freedb_server, - freedb_port=freedb_port, - use_musicbrainz=use_musicbrainz, - use_freedb=use_freedb) - - -class DVDATrack: - """an object representing an individual DVD-Audio track""" - - SAMPLE_RATE = [48000, 96000, 192000, 0, 0, 0, 0, 0, - 44100, 88200, 176400, 0, 0, 0, 0, 0] - CHANNELS = [1, 2, 3, 4, 3, 4, 5, 3, 4, 5, 4, 5, 6, 4, 5, 4, 5, 6, 5, 5, 6] - CHANNEL_MASK = [0x4, 0x3, 0x103, 0x33, 0xB, 0x10B, 0x3B, 0x7, - 0x107, 0x37, 0xF, 0x10F, 0x3F, 0x107, 0x37, 0xF, - 0x10F, 0x3F, 0x3B, 0x37, 0x3F] - BITS_PER_SAMPLE = [16, 20, 24] + [0] * 13 - - def __init__(self, dvdaudio, - titleset, title, track, - first_pts, pts_length, - first_sector, last_sector): - self.dvdaudio = dvdaudio - self.titleset = titleset - self.title = title - self.track = track - self.first_pts = first_pts - self.pts_length = pts_length - self.first_sector = first_sector - self.last_sector = last_sector - - def __repr__(self): - return "DVDATrack(%s)" % \ - (", ".join(["%s=%s" % (attr, getattr(self, attr)) - for attr in ["titleset", - "title", - "track", - "first_pts", - "pts_length", - "first_sector", - "last_sector"]])) - - def total_frames(self): - """returns the track's total PCM frames as an integer - - this is based on its PTS ticks and the title's sample rate""" - - import decimal - - return int((decimal.Decimal(self.pts_length) / - DVDAudio.PTS_PER_SECOND * - self.title.sample_rate).quantize( - decimal.Decimal(1), - rounding=decimal.ROUND_UP)) - - def sectors(self): - """iterates (aob_file, start_sector, end_sector) - - for each AOB file necessary to extract the track's data - in the order in which they should be read""" - - track_sectors = Rangeset(self.first_sector, - self.last_sector + 1) - - for (i, (start_sector, - end_sector)) in enumerate(self.dvdaudio.aob_sectors): - aob_sectors = Rangeset(start_sector, end_sector) - intersection = aob_sectors & track_sectors - if (len(intersection)): - yield (self.dvdaudio.files["ATS_%2.2d_%d.AOB" % \ - (self.titleset, i + 1)], - intersection.start - start_sector, - intersection.end - start_sector) - - -class Rangeset: - """an optimized combination of range() and set()""" - - #The purpose of this class is for finding the subset of - #two Rangesets, such as with: - # - # >>> Rangeset(1, 10) & Rangeset(5, 15) - # Rangeset(5, 10) - # - #which returns another Rangeset object. - #This is preferable to performing: - # - # >>> set(range(1, 10)) & set(range(5, 15)) - # set([8, 9, 5, 6, 7]) - # - #which allocates lots of unnecessary values - #when all we're interested in is the min and max. - - def __init__(self, start, end): - self.start = start - self.end = end - - def __repr__(self): - return "Rangeset(%s, %s)" % (repr(self.start), repr(self.end)) - - def __len__(self): - return int(self.end - self.start) - - def __getitem__(self, i): - if (i >= 0): - if (i < len(self)): - return self.start + i - else: - raise IndexError(i) - else: - if (-i - 1 < len(self)): - return self.end + i - else: - raise IndexError(i) - - def __and__(self, rangeset): - min_point = max(self.start, rangeset.start) - max_point = min(self.end, rangeset.end) - - if (min_point <= max_point): - return Rangeset(min_point, max_point) - else: - return Rangeset(0, 0)
View file
audiotools-2.18.tar.gz/audiotools/__flac__.py
Deleted
@@ -1,3000 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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, MetaData, InvalidFile, PCMReader, - transfer_data, transfer_framelist_data, - subprocess, BIN, BUFFER_SIZE, cStringIO, - os, open_files, Image, sys, WaveAudio, AiffAudio, - ReplayGain, ignore_sigint, sheet_to_unicode, - EncodingError, UnsupportedChannelMask, DecodingError, - UnsupportedChannelCount, - Messenger, BufferedPCMReader, calculate_replay_gain, - ChannelMask, PCMReaderError, __default_quality__, - WaveContainer, AiffContainer, to_pcm_progress, - image_metrics, RIFF_Chunk, AIFF_Chunk, - PCMReaderProgress) -from __vorbiscomment__ import * -from __id3__ import skip_id3v2_comment - -import gettext - -gettext.install("audiotools", unicode=True) - - -####################### -#FLAC -####################### - - -class InvalidFLAC(InvalidFile): - pass - - -class FlacMetaDataBlockTooLarge(Exception): - """raised if one attempts to build a FlacMetaDataBlock too large""" - - pass - - -class FlacMetaData(MetaData): - """a class for managing a native FLAC's metadata""" - - def __init__(self, blocks): - self.__dict__["block_list"] = list(blocks) - - def has_block(self, block_id): - """returns True if the given block ID is present""" - - return block_id in [b.BLOCK_ID for b in self.block_list] - - def add_block(self, block): - """adds the given block to our list of blocks""" - - #the specification only requires that STREAMINFO be first - #the rest are largely arbitrary, - #though I like to keep PADDING as the last block for aesthetic reasons - PREFERRED_ORDER = [Flac_STREAMINFO.BLOCK_ID, - Flac_SEEKTABLE.BLOCK_ID, - Flac_CUESHEET.BLOCK_ID, - Flac_VORBISCOMMENT.BLOCK_ID, - Flac_PICTURE.BLOCK_ID, - Flac_APPLICATION.BLOCK_ID, - Flac_PADDING.BLOCK_ID] - - stop_blocks = set( - PREFERRED_ORDER[PREFERRED_ORDER.index(block.BLOCK_ID) + 1:]) - - for (index, old_block) in enumerate(self.block_list): - if (old_block.BLOCK_ID in stop_blocks): - self.block_list.insert(index, block) - break - else: - self.block_list.append(block) - - def get_block(self, block_id): - """returns the first instance of the given block_id - - may raise IndexError if the block is not in our list of blocks""" - - for block in self.block_list: - if (block.BLOCK_ID == block_id): - return block - else: - raise IndexError() - - def get_blocks(self, block_id): - """returns all instances of the given block_id in our list of blocks""" - - return [b for b in self.block_list if (b.BLOCK_ID == block_id)] - - def replace_blocks(self, block_id, blocks): - """replaces all instances of the given block_id with - blocks taken from the given list - - if insufficient matching blocks are present, - this uses add_block() to populate the remainder - - if additional matching blocks are present, - they are removed - """ - - new_blocks = [] - - for block in self.block_list: - if (block.BLOCK_ID == block_id): - if (len(blocks) > 0): - new_blocks.append(blocks.pop(0)) - else: - pass - else: - new_blocks.append(block) - - self.block_list = new_blocks - - while (len(blocks) > 0): - self.add_block(blocks.pop(0)) - - def __setattr__(self, key, value): - if (key in self.FIELDS): - try: - vorbis_comment = self.get_block(Flac_VORBISCOMMENT.BLOCK_ID) - except IndexError: - #add VORBISCOMMENT block if necessary - vorbis_comment = Flac_VORBISCOMMENT( - [], u"Python Audio Tools %s" % (VERSION)) - - self.add_block(vorbis_comment) - - setattr(vorbis_comment, key, value) - else: - self.__dict__[key] = value - - def __getattr__(self, key): - if (key in self.FIELDS): - try: - return getattr(self.get_block(Flac_VORBISCOMMENT.BLOCK_ID), - key) - except IndexError: - if (key in self.INTEGER_FIELDS): - return 0 - else: - return u"" - else: - try: - return self.__dict__[key] - except KeyError: - raise AttributeError(key) - - def __delattr__(self, key): - if (key in self.FIELDS): - try: - delattr(self.get_block(Flac_VORBISCOMMENT.BLOCK_ID), key) - except IndexError: - #no VORBIS comment block - pass - else: - try: - del(self.__dict__[key]) - except KeyError: - raise AttributeError(key) - - @classmethod - def converted(cls, metadata): - """takes a MetaData object and returns a FlacMetaData object""" - - if (metadata is None): - return None - elif (isinstance(metadata, FlacMetaData)): - return cls([block.copy() for block in metadata.block_list]) - else: - return cls([Flac_VORBISCOMMENT.converted(metadata)] + - [Flac_PICTURE.converted(image) - for image in metadata.images()] + - [Flac_PADDING(4096)]) - - def add_image(self, image): - """embeds an Image object in this metadata""" - - self.add_block(Flac_PICTURE.converted(image)) - - def delete_image(self, image): - """deletes an image object from this metadata""" - - self.block_list = [b for b in self.block_list - if not ((b.BLOCK_ID == Flac_PICTURE.BLOCK_ID) and - (b == image))] - - def images(self): - """returns a list of embedded Image objects""" - - return self.get_blocks(Flac_PICTURE.BLOCK_ID) - - @classmethod - def supports_images(cls): - """returns True""" - - return True - - def clean(self, fixes_performed): - """returns a new FlacMetaData object that's been cleaned of problems - - any fixes performed are appended to fixes_performed as unicode""" - - cleaned_blocks = [] - - for block in self.block_list: - if (block.BLOCK_ID == Flac_STREAMINFO.BLOCK_ID): - #reorder STREAMINFO block to be first, if necessary - if (len(cleaned_blocks) == 0): - cleaned_blocks.append(block) - elif (cleaned_blocks[0].BLOCK_ID != block.BLOCK_ID): - fixes_performed.append( - _(u"moved STREAMINFO to first block")) - cleaned_blocks.insert(0, block) - else: - fixes_performed.append( - _(u"removing redundant STREAMINFO block")) - elif (block.BLOCK_ID == Flac_VORBISCOMMENT.BLOCK_ID): - if (block.BLOCK_ID in [b.BLOCK_ID for b in cleaned_blocks]): - #remove redundant VORBIS_COMMENT blocks - fixes_performed.append( - _(u"removing redundant VORBIS_COMMENT block")) - else: - #recursively clean up the text fields in FlacVorbisComment - cleaned_blocks.append(block.clean(fixes_performed)) - elif (block.BLOCK_ID == Flac_PICTURE.BLOCK_ID): - #recursively clean up any image blocks - cleaned_blocks.append(block.clean(fixes_performed)) - elif (block.BLOCK_ID == Flac_APPLICATION.BLOCK_ID): - cleaned_blocks.append(block) - elif (block.BLOCK_ID == Flac_SEEKTABLE.BLOCK_ID): - #remove redundant seektable, if necessary - if (block.BLOCK_ID in [b.BLOCK_ID for b in cleaned_blocks]): - fixes_performed.append( - _(u"removing redundant SEEKTABLE block")) - else: - cleaned_blocks.append(block.clean(fixes_performed)) - elif (block.BLOCK_ID == Flac_CUESHEET.BLOCK_ID): - #remove redundant cuesheet, if necessary - if (block.BLOCK_ID in [b.BLOCK_ID for b in cleaned_blocks]): - fixes_performed.append( - _(u"removing redundant CUESHEET block")) - else: - cleaned_blocks.append(block) - elif (block.BLOCK_ID == Flac_PADDING.BLOCK_ID): - cleaned_blocks.append(block) - else: - #remove undefined blocks - fixes_performed.append(_(u"removing undefined block")) - - return self.__class__(cleaned_blocks) - - def __repr__(self): - return "FlacMetaData(%s)" % (self.block_list) - - @classmethod - def parse(cls, reader): - """returns a FlacMetaData object from the given BitstreamReader - which has already parsed the 4-byte 'fLaC' file ID""" - - block_list = [] - - last = 0 - - while (last != 1): - (last, block_type, block_length) = reader.parse("1u7u24u") - - if (block_type == 0): # STREAMINFO - block_list.append( - Flac_STREAMINFO.parse(reader.substream(block_length))) - elif (block_type == 1): # PADDING - block_list.append(Flac_PADDING.parse( - reader.substream(block_length), block_length)) - elif (block_type == 2): # APPLICATION - block_list.append(Flac_APPLICATION.parse( - reader.substream(block_length), block_length)) - elif (block_type == 3): # SEEKTABLE - block_list.append( - Flac_SEEKTABLE.parse(reader.substream(block_length), - block_length / 18)) - elif (block_type == 4): # VORBIS_COMMENT - block_list.append(Flac_VORBISCOMMENT.parse( - reader.substream(block_length))) - elif (block_type == 5): # CUESHEET - block_list.append( - Flac_CUESHEET.parse(reader.substream(block_length))) - elif (block_type == 6): # PICTURE - block_list.append(Flac_PICTURE.parse( - reader.substream(block_length))) - elif ((block_type >= 7) and (block_type <= 126)): - raise ValueError(_(u"reserved metadata block type %d") % - (block_type)) - else: - raise ValueError(_(u"invalid metadata block type")) - - return cls(block_list) - - def raw_info(self): - """returns human-readable metadata as a unicode string""" - - from os import linesep - - return linesep.decode('ascii').join( - ["FLAC Tags:"] + [block.raw_info() for block in self.blocks()]) - - def blocks(self): - """yields FlacMetaData's individual metadata blocks""" - - for block in self.block_list: - yield block - - def build(self, writer): - """writes the FlacMetaData to the given BitstreamWriter - not including the 4-byte 'fLaC' file ID""" - - from . import iter_last - - for (last_block, - block) in iter_last(iter([b for b in self.blocks() - if (b.size() < (2 ** 24))])): - if (not last_block): - writer.build("1u7u24u", (0, block.BLOCK_ID, block.size())) - else: - writer.build("1u7u24u", (1, block.BLOCK_ID, block.size())) - - block.build(writer) - - def size(self): - """returns the size of all metadata blocks - including the block headers - but not including the 4-byte 'fLaC' file ID""" - - from operator import add - - return reduce(add, [4 + b.size() for b in self.block_list], 0) - - -class Flac_STREAMINFO: - BLOCK_ID = 0 - - def __init__(self, minimum_block_size, maximum_block_size, - minimum_frame_size, maximum_frame_size, - sample_rate, channels, bits_per_sample, - total_samples, md5sum): - """all values are non-negative integers except for md5sum - which is a 16-byte binary string""" - - self.minimum_block_size = minimum_block_size - self.maximum_block_size = maximum_block_size - self.minimum_frame_size = minimum_frame_size - self.maximum_frame_size = maximum_frame_size - self.sample_rate = sample_rate - self.channels = channels - self.bits_per_sample = bits_per_sample - self.total_samples = total_samples - self.md5sum = md5sum - - def copy(self): - """returns a duplicate of this metadata block""" - - return Flac_STREAMINFO(self.minimum_block_size, - self.maximum_block_size, - self.minimum_frame_size, - self.maximum_frame_size, - self.sample_rate, - self.channels, - self.bits_per_sample, - self.total_samples, - self.md5sum) - - def __eq__(self, block): - for attr in ["minimum_block_size", - "maximum_block_size", - "minimum_frame_size", - "maximum_frame_size", - "sample_rate", - "channels", - "bits_per_sample", - "total_samples", - "md5sum"]: - if ((not hasattr(block, attr)) or - (getattr(self, attr) != getattr(block, attr))): - return False - else: - return True - - def __repr__(self): - return ("Flac_STREAMINFO(%s)" % - ",".join(["%s=%s" % (key, repr(getattr(self, key))) - for key in ["minimum_block_size", - "maximum_block_size", - "minimum_frame_size", - "maximum_frame_size", - "sample_rate", - "channels", - "bits_per_sample", - "total_samples", - "md5sum"]])) - - def raw_info(self): - """returns a human-readable version of this metadata block - as unicode""" - - from os import linesep - - return linesep.decode('ascii').join( - [u" STREAMINFO:", - u" minimum block size = %d" % (self.minimum_block_size), - u" maximum block size = %d" % (self.maximum_block_size), - u" minimum frame size = %d" % (self.minimum_frame_size), - u" maximum frame size = %d" % (self.maximum_frame_size), - u" sample rate = %d" % (self.sample_rate), - u" channels = %d" % (self.channels), - u" bits-per-sample = %d" % (self.bits_per_sample), - u" total samples = %d" % (self.total_samples), - u" MD5 sum = %s" % (u"".join( - ["%2.2X" % (ord(b)) for b in self.md5sum]))]) - - @classmethod - def parse(cls, reader): - """returns this metadata block from a BitstreamReader""" - - values = reader.parse("16u16u24u24u20u3u5u36U16b") - values[5] += 1 # channels - values[6] += 1 # bits-per-sample - return cls(*values) - - def build(self, writer): - """writes this metadata block to a BitstreamWriter""" - - writer.build("16u16u24u24u20u3u5u36U16b", - (self.minimum_block_size, - self.maximum_block_size, - self.minimum_frame_size, - self.maximum_frame_size, - self.sample_rate, - self.channels - 1, - self.bits_per_sample - 1, - self.total_samples, - self.md5sum)) - - def size(self): - """the size of this metadata block - not including the 4-byte block header""" - - return 34 - - -class Flac_VORBISCOMMENT(VorbisComment): - BLOCK_ID = 4 - - def copy(self): - """returns a duplicate of this metadata block""" - - return Flac_VORBISCOMMENT(self.comment_strings[:], - self.vendor_string) - - def __repr__(self): - return "Flac_VORBISCOMMENT(%s, %s)" % \ - (repr(self.comment_strings), repr(self.vendor_string)) - - def raw_info(self): - """returns a human-readable version of this metadata block - as unicode""" - - from os import linesep - from . import display_unicode - - #align the text strings on the "=" sign, if any - - if (len(self.comment_strings) > 0): - max_indent = max([len(display_unicode(comment.split(u"=", 1)[0])) - for comment in self.comment_strings - if u"=" in comment]) - else: - max_indent = 0 - - comment_strings = [] - for comment in self.comment_strings: - if (u"=" in comment): - comment_strings.append( - u" " * (4 + max_indent - - len(display_unicode(comment.split(u"=", 1)[0]))) + - comment) - else: - comment_strings.append(u" " * 4 + comment) - - return linesep.decode('ascii').join( - [u" VORBIS_COMMENT:", - u" %s" % (self.vendor_string)] + - comment_strings) - - @classmethod - def converted(cls, metadata): - """converts a MetaData object to a Flac_VORBISCOMMENT object""" - - if ((metadata is None) or (isinstance(metadata, Flac_VORBISCOMMENT))): - return metadata - else: - #make VorbisComment do all the work, - #then lift its data into a new Flac_VORBISCOMMENT - metadata = VorbisComment.converted(metadata) - return cls(metadata.comment_strings, - metadata.vendor_string) - - @classmethod - def parse(cls, reader): - """returns this metadata block from a BitstreamReader""" - - reader.set_endianness(1) - vendor_string = reader.read_bytes(reader.read(32)).decode('utf-8') - return cls([reader.read_bytes(reader.read(32)).decode('utf-8') - for i in xrange(reader.read(32))], vendor_string) - - def build(self, writer): - """writes this metadata block to a BitstreamWriter""" - - writer.set_endianness(1) - vendor_string = self.vendor_string.encode('utf-8') - writer.build("32u%db" % (len(vendor_string)), - (len(vendor_string), vendor_string)) - writer.write(32, len(self.comment_strings)) - for comment_string in self.comment_strings: - comment_string = comment_string.encode('utf-8') - writer.build("32u%db" % (len(comment_string)), - (len(comment_string), comment_string)) - writer.set_endianness(0) - - def size(self): - """the size of this metadata block - not including the 4-byte block header""" - - from operator import add - - return (4 + len(self.vendor_string.encode('utf-8')) + - 4 + - reduce(add, [4 + len(comment.encode('utf-8')) - for comment in self.comment_strings], 0)) - - -class Flac_PICTURE(Image): - BLOCK_ID = 6 - - def __init__(self, picture_type, mime_type, description, - width, height, color_depth, color_count, data): - self.__dict__["data"] = data - self.__dict__["mime_type"] = mime_type - self.__dict__["width"] = width - self.__dict__["height"] = height - self.__dict__["color_depth"] = color_depth - self.__dict__["color_count"] = color_count - self.__dict__["description"] = description - self.__dict__["picture_type"] = picture_type - - def copy(self): - """returns a duplicate of this metadata block""" - - return Flac_PICTURE(self.picture_type, - self.mime_type, - self.description, - self.width, - self.height, - self.color_depth, - self.color_count, - self.data) - - def __getattr__(self, key): - if (key == "type"): - #convert FLAC picture_type to Image type - # - # | Item | FLAC Picture ID | Image type | - # |--------------+-----------------+------------| - # | Other | 0 | 4 | - # | Front Cover | 3 | 0 | - # | Back Cover | 4 | 1 | - # | Leaflet Page | 5 | 2 | - # | Media | 6 | 3 | - - return {0: 4, 3: 0, 4: 1, 5: 2, 6: 3}.get(self.picture_type, 4) - else: - try: - return self.__dict__[key] - except KeyError: - raise AttributeError(key) - - def __setattr__(self, key, value): - if (key == "type"): - #convert Image type to FLAC picture_type - # - # | Item | Image type | FLAC Picture ID | - # |--------------+------------+-----------------| - # | Other | 4 | 0 | - # | Front Cover | 0 | 3 | - # | Back Cover | 1 | 4 | - # | Leaflet Page | 2 | 5 | - # | Media | 3 | 6 | - - self.picture_type = {4: 0, 0: 3, 1: 4, 2: 5, 3: 6}.get(value, 0) - else: - self.__dict__[key] = value - - def __repr__(self): - return ("Flac_PICTURE(%s)" % - ",".join(["%s=%s" % (key, repr(getattr(self, key))) - for key in ["picture_type", - "mime_type", - "description", - "width", - "height", - "color_depth", - "color_count"]])) - - def raw_info(self): - """returns a human-readable version of this metadata block - as unicode""" - - from os import linesep - - return linesep.decode('ascii').join( - [u" PICTURE:", - u" picture type = %d" % (self.picture_type), - u" MIME type = %s" % (self.mime_type), - u" description = %s" % (self.description), - u" width = %d" % (self.width), - u" height = %d" % (self.height), - u" color depth = %d" % (self.color_depth), - u" color count = %d" % (self.color_count), - u" bytes = %d" % (len(self.data))]) - - @classmethod - def parse(cls, reader): - """returns this metadata block from a BitstreamReader""" - - return cls( - picture_type=reader.read(32), - mime_type=reader.read_bytes(reader.read(32)).decode('ascii'), - description=reader.read_bytes(reader.read(32)).decode('utf-8'), - width=reader.read(32), - height=reader.read(32), - color_depth=reader.read(32), - color_count=reader.read(32), - data=reader.read_bytes(reader.read(32))) - - def build(self, writer): - """writes this metadata block to a BitstreamWriter""" - - writer.build("32u [ 32u%db ] [32u%db ] 32u 32u 32u 32u [ 32u%db ]" % - (len(self.mime_type.encode('ascii')), - len(self.description.encode('utf-8')), - len(self.data)), - (self.picture_type, - len(self.mime_type.encode('ascii')), - self.mime_type.encode('ascii'), - len(self.description.encode('utf-8')), - self.description.encode('utf-8'), - self.width, - self.height, - self.color_depth, - self.color_count, - len(self.data), - self.data)) - - def size(self): - """the size of this metadata block - not including the 4-byte block header""" - - from .bitstream import format_size - - return format_size( - "32u [ 32u%db ] [32u%db ] 32u 32u 32u 32u [ 32u%db ]" % - (len(self.mime_type.encode('ascii')), - len(self.description.encode('utf-8')), - len(self.data))) / 8 - - @classmethod - def converted(cls, image): - """converts an Image object to a FlacPictureComment""" - - return cls( - picture_type={4: 0, 0: 3, 1: 4, 2: 5, 3: 6}.get(image.type, 0), - mime_type=image.mime_type, - description=image.description, - width=image.width, - height=image.height, - color_depth=image.color_depth, - color_count=image.color_count, - data=image.data) - - def type_string(self): - """returns the image's type as a human readable plain string - - for example, an image of type 0 returns "Front Cover" - """ - - return {0: "Other", - 1: "File icon", - 2: "Other file icon", - 3: "Cover (front)", - 4: "Cover (back)", - 5: "Leaflet page", - 6: "Media", - 7: "Lead artist / lead performer / soloist", - 8: "Artist / Performer", - 9: "Conductor", - 10: "Band / Orchestra", - 11: "Composer", - 12: "Lyricist / Text writer", - 13: "Recording Location", - 14: "During recording", - 15: "During performance", - 16: "Movie / Video screen capture", - 17: "A bright colored fish", - 18: "Illustration", - 19: "Band/Artist logotype", - 20: "Publisher / Studio logotype"}.get(self.picture_type, - "Other") - - def clean(self, fixes_performed): - img = image_metrics(self.data) - - if ((self.mime_type != img.mime_type) or - (self.width != img.width) or - (self.height != img.height) or - (self.color_depth != img.bits_per_pixel) or - (self.color_count != img.color_count)): - fixes_performed.append(_(u"fixed embedded image metadata fields")) - return self.__class__.converted(Image( - type=self.type, - mime_type=img.mime_type, - description=self.description, - width=img.width, - height=img.height, - color_depth=img.bits_per_pixel, - color_count=img.color_count, - data=self.data)) - else: - return self - - -class Flac_APPLICATION: - BLOCK_ID = 2 - - def __init__(self, application_id, data): - self.application_id = application_id - self.data = data - - def __eq__(self, block): - for attr in ["application_id", "data"]: - if ((not hasattr(block, attr)) or - (getattr(self, attr) != getattr(block, attr))): - return False - else: - return True - - def copy(self): - """returns a duplicate of this metadata block""" - - return Flac_APPLICATION(self.application_id, - self.data) - - def __repr__(self): - return "Flac_APPLICATION(%s, %s)" % (repr(self.application_id), - repr(self.data)) - - def raw_info(self): - """returns a human-readable version of this metadata block - as unicode""" - - from os import linesep - - return u" APPLICATION:%s %s (%d bytes)" % \ - (linesep.decode('ascii'), - self.application_id.decode('ascii'), - len(self.data)) - - @classmethod - def parse(cls, reader, block_length): - """returns this metadata block from a BitstreamReader""" - - return cls(application_id=reader.read_bytes(4), - data=reader.read_bytes(block_length - 4)) - - def build(self, writer): - """writes this metadata block to a BitstreamWriter""" - - writer.write_bytes(self.application_id) - writer.write_bytes(self.data) - - def size(self): - """the size of this metadata block - not including the 4-byte block header""" - - return len(self.application_id) + len(self.data) - - -class Flac_SEEKTABLE: - BLOCK_ID = 3 - - def __init__(self, seekpoints): - """seekpoints is a list of - (PCM frame offset, byte offset, PCM frame count) tuples""" - self.seekpoints = seekpoints - - def __eq__(self, block): - if (hasattr(block, "seekpoints")): - return self.seekpoints == block.seekpoints - else: - return False - - def copy(self): - """returns a duplicate of this metadata block""" - - return Flac_SEEKTABLE(self.seekpoints[:]) - - def __repr__(self): - return "Flac_SEEKTABLE(%s)" % (repr(self.seekpoints)) - - def raw_info(self): - """returns a human-readable version of this metadata block - as unicode""" - - from os import linesep - - return linesep.decode('ascii').join( - [u" SEEKTABLE:", - u" first sample file offset frame samples"] + - [u" %14.1d %13.1X %15.d" % seekpoint - for seekpoint in self.seekpoints]) - - @classmethod - def parse(cls, reader, total_seekpoints): - """returns this metadata block from a BitstreamReader""" - - return cls([tuple(reader.parse("64U64U16u")) - for i in xrange(total_seekpoints)]) - - def build(self, writer): - """writes this metadata block to a BitstreamWriter""" - - for seekpoint in self.seekpoints: - writer.build("64U64U16u", seekpoint) - - def size(self): - """the size of this metadata block - not including the 4-byte block header""" - - from .bitstream import format_size - - return (format_size("64U64U16u") / 8) * len(self.seekpoints) - - def clean(self, fixes_performed): - """removes any empty seek points - and ensures PCM frame offset and byte offset - are both incrementing""" - - nonempty_points = [seekpoint for seekpoint in self.seekpoints - if (seekpoint[2] != 0)] - if (len(nonempty_points) != len(self.seekpoints)): - fixes_performed.append( - _(u"removed empty seekpoints from seektable")) - - ascending_order = list(set(nonempty_points)) - ascending_order.sort() - - if (ascending_order != nonempty_points): - fixes_performed.append( - _(u"reordered seektable to be in ascending order")) - - return Flac_SEEKTABLE(ascending_order) - - -class Flac_CUESHEET: - BLOCK_ID = 5 - - def __init__(self, catalog_number, lead_in_samples, is_cdda, tracks): - self.catalog_number = catalog_number - self.lead_in_samples = lead_in_samples - self.is_cdda = is_cdda - self.tracks = tracks - - def copy(self): - """returns a duplicate of this metadata block""" - - return Flac_CUESHEET(self.catalog_number, - self.lead_in_samples, - self.is_cdda, - [track.copy() for track in self.tracks]) - - def __eq__(self, cuesheet): - for attr in ["catalog_number", - "lead_in_samples", - "is_cdda", - "tracks"]: - if ((not hasattr(cuesheet, attr)) or - (getattr(self, attr) != getattr(cuesheet, attr))): - return False - else: - return True - - def __repr__(self): - return ("Flac_CUESHEET(%s)" % - ",".join(["%s=%s" % (key, repr(getattr(self, key))) - for key in ["catalog_number", - "lead_in_samples", - "is_cdda", - "tracks"]])) - - def raw_info(self): - """returns a human-readable version of this metadata block - as unicode""" - - from os import linesep - - return linesep.decode('ascii').join( - [u" CUESHEET:", - u" catalog number = %s" % \ - (self.catalog_number.decode('ascii', 'replace')), - u" lead-in samples = %d" % (self.lead_in_samples), - u" is CDDA = %d" % (self.is_cdda), - u" track offset ISRC"] + - [track.raw_info() for track in self.tracks]) - - @classmethod - def parse(cls, reader): - """returns this metadata block from a BitstreamReader""" - - (catalog_number, - lead_in_samples, - is_cdda, - track_count) = reader.parse("128b64U1u2071p8u") - return cls(catalog_number, - lead_in_samples, - is_cdda, - [Flac_CUESHEET_track.parse(reader) - for i in xrange(track_count)]) - - def build(self, writer): - """writes this metadata block to a BitstreamWriter""" - - writer.build("128b64U1u2071p8u", - (self.catalog_number, - self.lead_in_samples, - self.is_cdda, - len(self.tracks))) - for track in self.tracks: - track.build(writer) - - def size(self): - """the size of this metadata block - not including the 4-byte block header""" - - from .bitstream import BitstreamAccumulator - - a = BitstreamAccumulator(0) - self.build(a) - return a.bytes() - - @classmethod - def converted(cls, sheet, total_frames, sample_rate=44100): - """converts a cuesheet compatible object to Flac_CUESHEET objects - - a total_frames integer (in PCM frames) is also required - """ - - if (sheet.catalog() is None): - catalog_number = chr(0) * 128 - else: - catalog_number = sheet.catalog() + (chr(0) * - (128 - len(sheet.catalog()))) - - ISRCs = sheet.ISRCs() - - return cls( - catalog_number=catalog_number, - lead_in_samples=sample_rate * 2, - is_cdda=1 if sample_rate == 44100 else 0, - tracks=[Flac_CUESHEET_track( - offset=indexes[0] * sample_rate / 75, - number=i + 1, - ISRC=ISRCs.get(i + 1, chr(0) * 12), - track_type=0, - pre_emphasis=0, - index_points=[Flac_CUESHEET_index( - offset=(index - indexes[0]) * sample_rate / 75, - number=point_number + (1 if len(indexes) == 1 - else 0)) - for (point_number, index) - in enumerate(indexes)]) - for (i, indexes) in enumerate(sheet.indexes())] + - # lead-out track - [Flac_CUESHEET_track(offset=total_frames, - number=170, - ISRC=chr(0) * 12, - track_type=0, - pre_emphasis=0, - index_points=[])]) - - def catalog(self): - """returns the cuesheet's catalog number as a plain string""" - - catalog_number = self.catalog_number.rstrip(chr(0)) - - if (len(catalog_number) > 0): - return catalog_number - else: - return None - - def ISRCs(self): - """returns a dict of ISRC values as plain strings""" - - return dict([(track.number, track.ISRC) for track in - self.tracks - if ((track.number != 170) and - (len(track.ISRC.strip(chr(0))) > 0))]) - - def indexes(self, sample_rate=44100): - """returns a list of (start, end) integer tuples""" - - return [tuple([(index.offset + track.offset) * 75 / sample_rate - for index in - sorted(track.index_points, - lambda i1, i2: cmp(i1.number, i2.number))]) - for track in - sorted(self.tracks, lambda t1, t2: cmp(t1.number, t2.number)) - if (track.number != 170)] - - def pcm_lengths(self, total_length): - """returns a list of PCM lengths for all cuesheet audio tracks - - note that the total length variable is only for compatibility - it is not necessary for FlacCueSheets - """ - - if (len(self.tracks) > 0): - return [(current.offset + - max([i.offset for i in current.index_points] + [0])) - - ((previous.offset + - max([i.offset for i in previous.index_points] + [0]))) - for (previous, current) in - zip(self.tracks, self.tracks[1:])] - else: - return [] - - def __unicode__(self): - return sheet_to_unicode(self, None) - - -class Flac_CUESHEET_track: - def __init__(self, offset, number, ISRC, track_type, pre_emphasis, - index_points): - self.offset = offset - self.number = number - self.ISRC = ISRC - self.track_type = track_type - self.pre_emphasis = pre_emphasis - self.index_points = index_points - - def copy(self): - """returns a duplicate of this metadata block""" - - return Flac_CUESHEET_track(self.offset, - self.number, - self.ISRC, - self.track_type, - self.pre_emphasis, - [index.copy() for index in - self.index_points]) - - def __repr__(self): - return ("Flac_CUESHEET_track(%s)" % - ",".join(["%s=%s" % (key, repr(getattr(self, key))) - for key in ["offset", - "number", - "ISRC", - "track_type", - "pre_emphasis", - "index_points"]])) - - def raw_info(self): - """returns a human-readable version of this track as unicode""" - - if (len(self.ISRC.strip(chr(0))) > 0): - return u"%9.d %13.d %s" % \ - (self.number, self.offset, self.ISRC) - else: - return u"%9.d %13.d" % \ - (self.number, self.offset) - - def __eq__(self, track): - for attr in ["offset", - "number", - "ISRC", - "track_type", - "pre_emphasis", - "index_points"]: - if ((not hasattr(track, attr)) or - (getattr(self, attr) != getattr(track, attr))): - return False - else: - return True - - @classmethod - def parse(cls, reader): - """returns this cuesheet track from a BitstreamReader""" - - (offset, - number, - ISRC, - track_type, - pre_emphasis, - index_points) = reader.parse("64U8u12b1u1u110p8u") - return cls(offset, number, ISRC, track_type, pre_emphasis, - [Flac_CUESHEET_index.parse(reader) - for i in xrange(index_points)]) - - def build(self, writer): - """writes this cuesheet track to a BitstreamWriter""" - - writer.build("64U8u12b1u1u110p8u", - (self.offset, - self.number, - self.ISRC, - self.track_type, - self.pre_emphasis, - len(self.index_points))) - for index_point in self.index_points: - index_point.build(writer) - - -class Flac_CUESHEET_index: - def __init__(self, offset, number): - self.offset = offset - self.number = number - - def copy(self): - """returns a duplicate of this metadata block""" - - return Flac_CUESHEET_index(self.offset, self.number) - - def __repr__(self): - return "Flac_CUESHEET_index(%s, %s)" % (repr(self.offset), - repr(self.number)) - - def __eq__(self, index): - try: - return ((self.offset == index.offset) and - (self.number == index.number)) - except AttributeError: - return False - - @classmethod - def parse(cls, reader): - """returns this cuesheet index from a BitstreamReader""" - - (offset, number) = reader.parse("64U8u24p") - - return cls(offset, number) - - def build(self, writer): - """writes this cuesheet index to a BitstreamWriter""" - - writer.build("64U8u24p", (self.offset, self.number)) - - -class Flac_PADDING: - BLOCK_ID = 1 - - def __init__(self, length): - self.length = length - - def copy(self): - """returns a duplicate of this metadata block""" - - return Flac_PADDING(self.length) - - def __repr__(self): - return "Flac_PADDING(%d)" % (self.length) - - def raw_info(self): - """returns a human-readable version of this metadata block - as unicode""" - - from os import linesep - - return linesep.decode('ascii').join( - [u" PADDING:", - u" length = %d" % (self.length)]) - - @classmethod - def parse(cls, reader, block_length): - """returns this metadata block from a BitstreamReader""" - - reader.skip_bytes(block_length) - return cls(length=block_length) - - def build(self, writer): - """writes this metadata block to a BitstreamWriter""" - - writer.write_bytes(chr(0) * self.length) - - def size(self): - """the size of this metadata block - not including the 4-byte block header""" - - return self.length - - -class FlacAudio(WaveContainer, AiffContainer): - """a Free Lossless Audio Codec file""" - - SUFFIX = "flac" - NAME = SUFFIX - DEFAULT_COMPRESSION = "8" - COMPRESSION_MODES = tuple(map(str, range(0, 9))) - COMPRESSION_DESCRIPTIONS = {"0": _(u"least amount of compresson, " + - u"fastest compression speed"), - "8": _(u"most amount of compression, " + - u"slowest compression speed")} - - METADATA_CLASS = FlacMetaData - - def __init__(self, filename): - """filename is a plain string""" - - AudioFile.__init__(self, filename) - self.__samplerate__ = 0 - self.__channels__ = 0 - self.__bitspersample__ = 0 - self.__total_frames__ = 0 - self.__stream_offset__ = 0 - self.__md5__ = chr(0) * 16 - - try: - self.__read_streaminfo__() - except IOError, msg: - raise InvalidFLAC(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""" - - if (file.read(4) == 'fLaC'): - #proper FLAC file with no junk at the beginning - try: - block_ids = list(cls.__block_ids__(file)) - except (ValueError, IOError): - return False - if ((len(block_ids) == 0) or (0 not in block_ids)): - return False - else: - return True - else: - #messed-up FLAC file with ID3v2 tags at the beginning - #which can be fixed using clean() - file.seek(0, 0) - if (file.read(3) == 'ID3'): - file.seek(-3, 1) - skip_id3v2_comment(file) - if (file.read(4) == 'fLaC'): - try: - block_ids = list(cls.__block_ids__(file)) - except (ValueError, IOError): - return False - if ((len(block_ids) == 0) or (0 not in block_ids)): - return False - else: - return True - else: - return False - else: - return False - - def channel_mask(self): - """returns a ChannelMask object of this track's channel layout""" - - if (self.channels() <= 2): - return ChannelMask.from_channels(self.channels()) - - try: - return ChannelMask( - int(self.get_metadata().get_block( - Flac_VORBISCOMMENT.BLOCK_ID)[ - u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"][0], 16)) - except (IndexError, KeyError, ValueError): - #if there is no VORBIS_COMMENT block - #or no WAVEFORMATEXTENSIBLE_CHANNEL_MASK in that block - #or it's not an integer, - #use FLAC's default mask based on channels - if (self.channels() == 3): - return ChannelMask.from_fields( - front_left=True, front_right=True, front_center=True) - elif (self.channels() == 4): - return ChannelMask.from_fields( - front_left=True, front_right=True, - back_left=True, back_right=True) - elif (self.channels() == 5): - return ChannelMask.from_fields( - front_left=True, front_right=True, front_center=True, - back_left=True, back_right=True) - elif (self.channels() == 6): - return ChannelMask.from_fields( - front_left=True, front_right=True, front_center=True, - back_left=True, back_right=True, - low_frequency=True) - else: - return ChannelMask(0) - - def lossless(self): - """returns True""" - - return True - - def get_metadata(self): - """returns a MetaData object - - raises IOError if unable to read the file""" - - #FlacAudio *always* returns a FlacMetaData object - #even if the blocks aren't present - #so there's no need to test for None - - f = file(self.filename, 'rb') - try: - f.seek(self.__stream_offset__, 0) - if (f.read(4) != 'fLaC'): - raise InvalidFLAC(_(u'Invalid FLAC file')) - - from .bitstream import BitstreamReader - - return FlacMetaData.parse(BitstreamReader(f, 0)) - finally: - f.close() - - def update_metadata(self, metadata): - """takes this track's current MetaData object - as returned by get_metadata() and sets this track's metadata - with any fields updated in that object - - raises IOError if unable to write the file - """ - - from .bitstream import BitstreamWriter - from .bitstream import BitstreamAccumulator - from .bitstream import BitstreamReader - from operator import add - - if (metadata is None): - return - - if (not isinstance(metadata, FlacMetaData)): - raise ValueError(_(u"metadata not from audio file")) - - has_padding = len(metadata.get_blocks(Flac_PADDING.BLOCK_ID)) > 0 - - if (has_padding): - total_padding_size = sum( - [b.size() for b in metadata.get_blocks(Flac_PADDING.BLOCK_ID)]) - else: - total_padding_size = 0 - - metadata_delta = metadata.size() - self.metadata_length() - - if (has_padding and (metadata_delta <= total_padding_size)): - #if padding size is larger than change in metadata - #shrink padding blocks so that new size matches old size - #(if metadata_delta is negative, - # this will enlarge padding blocks as necessary) - - for padding in metadata.get_blocks(Flac_PADDING.BLOCK_ID): - if (metadata_delta > 0): - #extract bytes from PADDING blocks - #until the metadata_delta is exhausted - if (metadata_delta <= padding.length): - padding.length -= metadata_delta - metadata_delta = 0 - else: - metadata_delta -= padding.length - padding.length = 0 - elif (metadata_delta < 0): - #dump all our new bytes into the first PADDING block found - padding.length -= metadata_delta - metadata_delta = 0 - else: - break - - #then overwrite the beginning of the file - stream = file(self.filename, 'r+b') - stream.write('fLaC') - metadata.build(BitstreamWriter(stream, 0)) - stream.close() - else: - #if padding is smaller than change in metadata, - #or file has no padding, - #rewrite entire file to fit new metadata - - import tempfile - - stream = file(self.filename, 'rb') - stream.seek(self.__stream_offset__, 0) - - if (stream.read(4) != 'fLaC'): - raise InvalidFLAC(_(u'Invalid FLAC file')) - - #skip the existing metadata blocks - stop = 0 - reader = BitstreamReader(stream, 0) - while (stop == 0): - (stop, length) = reader.parse("1u 7p 24u") - reader.skip_bytes(length) - - #write the remaining data stream to a temp file - file_data = tempfile.TemporaryFile() - transfer_data(stream.read, file_data.write) - file_data.seek(0, 0) - - #finally, rebuild our file using new metadata and old stream - stream = file(self.filename, 'wb') - stream.write('fLaC') - writer = BitstreamWriter(stream, 0) - metadata.build(writer) - writer.flush() - transfer_data(file_data.read, stream.write) - file_data.close() - stream.close() - - def set_metadata(self, metadata): - """takes a MetaData object and sets this track's metadata - - this metadata includes track name, album name, and so on - raises IOError if unable to write the file""" - - new_metadata = self.METADATA_CLASS.converted(metadata) - - if (new_metadata is None): - return - - old_metadata = self.get_metadata() - - #replace old metadata's VORBIS_COMMENT with one from new metadata - #(if any) - if (new_metadata.has_block(Flac_VORBISCOMMENT.BLOCK_ID)): - new_vorbiscomment = new_metadata.get_block( - Flac_VORBISCOMMENT.BLOCK_ID) - - if (old_metadata.has_block(Flac_VORBISCOMMENT.BLOCK_ID)): - #both new and old metadata has a VORBIS_COMMENT block - - old_vorbiscomment = old_metadata.get_block( - Flac_VORBISCOMMENT.BLOCK_ID) - - #update vendor string from our current VORBIS_COMMENT block - new_vorbiscomment.vendor_string = \ - old_vorbiscomment.vendor_string - - #update REPLAYGAIN_* tags from our current VORBIS_COMMENT block - for key in [u"REPLAYGAIN_TRACK_GAIN", - u"REPLAYGAIN_TRACK_PEAK", - u"REPLAYGAIN_ALBUM_GAIN", - u"REPLAYGAIN_ALBUM_PEAK", - u"REPLAYGAIN_REFERENCE_LOUDNESS"]: - try: - new_vorbiscomment[key] = old_vorbiscomment[key] - except KeyError: - new_vorbiscomment[key] = [] - - #update WAVEFORMATEXTENSIBLE_CHANNEL_MASK - #from our current VORBIS_COMMENT block, if any - if (((self.channels() > 2) or - (self.bits_per_sample() > 16)) and - (u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK" in - old_vorbiscomment.keys())): - new_vorbiscomment[u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"] = \ - old_vorbiscomment[u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"] - elif (u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK" in - new_vorbiscomment.keys()): - new_vorbiscomment[u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"] = [] - - old_metadata.replace_blocks(Flac_VORBISCOMMENT.BLOCK_ID, - [new_vorbiscomment]) - else: - #new metadata has VORBIS_COMMENT block, - #but old metadata does not - - #remove REPLAYGAIN_* tags from new VORBIS_COMMENT block - for key in [u"REPLAYGAIN_TRACK_GAIN", - u"REPLAYGAIN_TRACK_PEAK", - u"REPLAYGAIN_ALBUM_GAIN", - u"REPLAYGAIN_ALBUM_PEAK", - u"REPLAYGAIN_REFERENCE_LOUDNESS"]: - new_vorbiscomment[key] = [] - - #update WAVEFORMATEXTENSIBLE_CHANNEL_MASK - #from our actual mask if necessary - if ((self.channels() > 2) or (self.bits_per_sample() > 16)): - new_vorbiscomment[u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"] = [ - u"0x%.4X" % (self.channel_mask())] - - old_metadata.add_block(new_vorbiscomment) - else: - #new metadata has no VORBIS_COMMENT block - pass - - #replace old metadata's PICTURE blocks with those from new metadata - old_metadata.replace_blocks( - Flac_PICTURE.BLOCK_ID, - new_metadata.get_blocks(Flac_PICTURE.BLOCK_ID)) - - #everything else remains as-is - - self.update_metadata(old_metadata) - - def metadata_length(self): - """returns the length of all FLAC metadata blocks as an integer - - not including the 4 byte "fLaC" file header""" - - from .bitstream import BitstreamReader - - counter = 0 - f = file(self.filename, 'rb') - try: - f.seek(self.__stream_offset__, 0) - reader = BitstreamReader(f, 0) - - if (reader.read_bytes(4) != 'fLaC'): - raise InvalidFLAC(_(u'Invalid FLAC file')) - - stop = 0 - while (stop == 0): - (stop, block_id, length) = reader.parse("1u 7u 24u") - counter += 4 - - reader.skip_bytes(length) - counter += length - - return counter - finally: - f.close() - - def delete_metadata(self): - """deletes the track's MetaData - - this removes or unsets tags as necessary in order to remove all data - raises IOError if unable to write the file""" - - self.set_metadata(MetaData()) - - @classmethod - def __block_ids__(cls, flacfile): - """yields a block_id int per metadata block - - raises ValueError if a block_id is invalid - """ - - valid_block_ids = frozenset(range(0, 6 + 1)) - from .bitstream import BitstreamReader - reader = BitstreamReader(flacfile, 0) - stop = 0 - while (stop == 0): - (stop, block_id, length) = reader.parse("1u 7u 24u") - if (block_id in valid_block_ids): - yield block_id - else: - raise ValueError(_(u"invalid FLAC block ID")) - reader.skip_bytes(length) - - def set_cuesheet(self, cuesheet): - """imports cuesheet data from a Cuesheet-compatible object - - this are objects with catalog(), ISRCs(), indexes(), and pcm_lengths() - methods. Raises IOError if an error occurs setting the cuesheet""" - - if (cuesheet is not None): - metadata = self.get_metadata() - metadata.add_block(Flac_CUESHEET.converted( - cuesheet, self.total_frames(), self.sample_rate())) - self.update_metadata(metadata) - - def get_cuesheet(self): - """returns the embedded Cuesheet-compatible object, or None - - raises IOError if a problem occurs when reading the file""" - - try: - return self.get_metadata().get_block(Flac_CUESHEET.BLOCK_ID) - except IndexError: - return None - - def to_pcm(self): - """returns a PCMReader object containing the track's PCM data""" - - from . import decoders - - try: - return decoders.FlacDecoder(self.filename, - self.channel_mask(), - self.__stream_offset__) - except (IOError, ValueError), msg: - #The only time this is likely to occur is - #if the FLAC is modified between when FlacAudio - #is initialized and when to_pcm() is called. - return PCMReaderError(error_message=str(msg), - 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 FlacAudio object""" - - from . import encoders - - if ((compression is None) or - (compression not in cls.COMPRESSION_MODES)): - compression = __default_quality__(cls.NAME) - - encoding_options = {"0": {"block_size": 1152, - "max_lpc_order": 0, - "min_residual_partition_order": 0, - "max_residual_partition_order": 3}, - "1": {"block_size": 1152, - "max_lpc_order": 0, - "adaptive_mid_side": True, - "min_residual_partition_order": 0, - "max_residual_partition_order": 3}, - "2": {"block_size": 1152, - "max_lpc_order": 0, - "exhaustive_model_search": True, - "min_residual_partition_order": 0, - "max_residual_partition_order": 3}, - "3": {"block_size": 4096, - "max_lpc_order": 6, - "min_residual_partition_order": 0, - "max_residual_partition_order": 4}, - "4": {"block_size": 4096, - "max_lpc_order": 8, - "adaptive_mid_side": True, - "min_residual_partition_order": 0, - "max_residual_partition_order": 4}, - "5": {"block_size": 4096, - "max_lpc_order": 8, - "mid_side": True, - "min_residual_partition_order": 0, - "max_residual_partition_order": 5}, - "6": {"block_size": 4096, - "max_lpc_order": 8, - "mid_side": True, - "min_residual_partition_order": 0, - "max_residual_partition_order": 6}, - "7": {"block_size": 4096, - "max_lpc_order": 8, - "mid_side": True, - "exhaustive_model_search": True, - "min_residual_partition_order": 0, - "max_residual_partition_order": 6}, - "8": {"block_size": 4096, - "max_lpc_order": 12, - "mid_side": True, - "exhaustive_model_search": True, - "min_residual_partition_order": 0, - "max_residual_partition_order": 6}}[ - compression] - - if (pcmreader.channels > 8): - raise UnsupportedChannelCount(filename, pcmreader.channels) - - if (int(pcmreader.channel_mask) == 0): - if (pcmreader.channels <= 6): - channel_mask = {1: 0x0004, - 2: 0x0003, - 3: 0x0007, - 4: 0x0033, - 5: 0x0037, - 6: 0x003F}[pcmreader.channels] - else: - channel_mask = 0 - - elif (int(pcmreader.channel_mask) not in - (0x0001, # 1ch - mono - 0x0004, # 1ch - mono - 0x0003, # 2ch - left, right - 0x0007, # 3ch - left, right, center - 0x0033, # 4ch - left, right, back left, back right - 0x0603, # 4ch - left, right, side left, side right - 0x0037, # 5ch - L, R, C, back left, back right - 0x0607, # 5ch - L, R, C, side left, side right - 0x003F, # 6ch - L, R, C, LFE, back left, back right - 0x060F)): # 6ch - L, R, C, LFE, side left, side right - raise UnsupportedChannelMask(filename, - int(pcmreader.channel_mask)) - else: - channel_mask = int(pcmreader.channel_mask) - - try: - offsets = encoders.encode_flac( - filename, - pcmreader=BufferedPCMReader(pcmreader), - **encoding_options) - flac = FlacAudio(filename) - metadata = flac.get_metadata() - - #generate SEEKTABLE from encoder offsets and add it to metadata - metadata_length = flac.metadata_length() + 4 - seekpoint_interval = pcmreader.sample_rate * 10 - - metadata.add_block(flac.seektable( - [(byte_offset - metadata_length, - pcm_frames) for byte_offset, pcm_frames in offsets], - seekpoint_interval)) - - #if channels or bps is too high, - #automatically generate and add channel mask - if (((pcmreader.channels > 2) or - (pcmreader.bits_per_sample > 16)) and - (channel_mask != 0)): - metadata.get_block(Flac_VORBISCOMMENT.BLOCK_ID)[ - u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"] = [ - u"0x%.4X" % (channel_mask)] - - flac.update_metadata(metadata) - - return flac - except (IOError, ValueError), err: - cls.__unlink__(filename) - raise EncodingError(str(err)) - except Exception, err: - cls.__unlink__(filename) - raise err - - def seektable(self, offsets=None, seekpoint_interval=None): - """returns a new Flac_SEEKTABLE object - created from parsing the FLAC file itself""" - - from bisect import bisect_right - - if (offsets is None): - metadata_length = (self.__stream_offset__ + - 4 + self.metadata_length()) - offsets = [(byte_offset - metadata_length, - pcm_frames) for byte_offset, pcm_frames in - self.to_pcm().offsets()] - - if (seekpoint_interval is None): - seekpoint_interval = self.sample_rate() * 10 - - total_samples = 0 - all_frames = {} - sample_offsets = [] - for (byte_offset, pcm_frames) in offsets: - all_frames[total_samples] = (byte_offset, pcm_frames) - sample_offsets.append(total_samples) - total_samples += pcm_frames - - seekpoints = [] - for pcm_frame in xrange(0, - self.total_frames(), - seekpoint_interval): - flac_frame = bisect_right(sample_offsets, pcm_frame) - 1 - seekpoints.append((sample_offsets[flac_frame], - all_frames[sample_offsets[flac_frame]][0], - all_frames[sample_offsets[flac_frame]][1])) - - return Flac_SEEKTABLE(seekpoints) - - 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""" - - try: - return 'riff' in [ - block.application_id for block in - self.get_metadata().get_blocks(Flac_APPLICATION.BLOCK_ID)] - except IOError: - return False - - def riff_wave_chunks(self, progress=None): - """yields a set of RIFF_Chunk or RIFF_File_Chunk objects""" - - from struct import unpack - - for application_block in [ - block.data for block in - self.get_metadata().get_blocks(Flac_APPLICATION.BLOCK_ID) - if (block.application_id == "riff")]: - - (chunk_id, chunk_size, chunk_data) = (application_block[0:4], - application_block[4:8], - application_block[8:]) - if (chunk_id == 'RIFF'): - #skip 12 byte RIFF<size>WAVE header - continue - elif (chunk_id == 'data'): - if (progress is not None): - yield FLAC_Data_Chunk( - self.__total_frames__, - PCMReaderProgress(self.to_pcm(), - self.__total_frames__, - progress)) - else: - yield FLAC_Data_Chunk(self.__total_frames__, self.to_pcm()) - else: - chunk_size = unpack("<I", chunk_size)[0] - yield RIFF_Chunk(chunk_id, chunk_size, chunk_data[0:chunk_size]) - - 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""" - - if (self.has_foreign_riff_chunks()): - WaveAudio.wave_from_chunks(wave_filename, - self.riff_wave_chunks(progress)) - else: - WaveAudio.from_pcm(wave_filename, to_pcm_progress(self, progress)) - - @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 FlacAudio object""" - - if ((compression is None) or - (compression not in cls.COMPRESSION_MODES)): - compression = __default_quality__(cls.NAME) - - wave = WaveAudio(wave_filename) - - flac = cls.from_pcm(filename, - to_pcm_progress(wave, progress), - compression=compression) - - if (wave.has_foreign_riff_chunks()): - from struct import pack - - metadata = flac.get_metadata() - - #add block containing RIFF WAVE header - metadata.add_block(Flac_APPLICATION( - application_id="riff", - data=pack("<4sI4s", - "RIFF", - 4 + sum([c.total_size() for c in wave.chunks()]), - "WAVE"))) - - #add remaining chunks as APPLICATION blocks - for chunk in wave.chunks(): - if (chunk.id == "data"): - metadata.add_block(Flac_APPLICATION( - application_id="riff", - data=pack("<4sI", "data", chunk.size()))) - else: - chunk_data = cStringIO.StringIO() - chunk.write(chunk_data) - metadata.add_block(Flac_APPLICATION( - application_id="riff", - data=chunk_data.getvalue())) - - flac.update_metadata(metadata) - - return flac - - def has_foreign_aiff_chunks(self): - """returns True if the audio file contains non-audio AIFF chunks""" - - return 'aiff' in [ - block.application_id for block in - self.get_metadata().get_blocks(Flac_APPLICATION.BLOCK_ID)] - - @classmethod - def from_aiff(cls, filename, aiff_filename, compression=None, - progress=None): - """encodes a new AudioFile from an existing .aiff file - - takes a filename string, aiff_filename string - of an existing AiffAudio 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 FlacAudio object""" - - if ((compression is None) or - (compression not in cls.COMPRESSION_MODES)): - compression = __default_quality__(cls.NAME) - - aiff = AiffAudio(aiff_filename) - - flac = cls.from_pcm(filename, - to_pcm_progress(aiff, progress), - compression=compression) - - if (AiffAudio(aiff_filename).has_foreign_aiff_chunks()): - from struct import pack - - metadata = flac.get_metadata() - - #add block containing FORM AIFF header - metadata.add_block(Flac_APPLICATION( - application_id="aiff", - data=pack(">4sI4s", - "FORM", - 4 + sum([c.total_size() for c in aiff.chunks()]), - "AIFF"))) - - #add remaining chunks as APPLICATION blocks - for chunk in aiff.chunks(): - if (chunk.id == "SSND"): - metadata.add_block(Flac_APPLICATION( - application_id="aiff", - data=pack(">4sIII", "SSND", chunk.size(), 0, 0))) - else: - chunk_data = cStringIO.StringIO() - chunk.write(chunk_data) - metadata.add_block(Flac_APPLICATION( - application_id="aiff", - data=chunk_data.getvalue())) - - flac.update_metadata(metadata) - - return flac - - def to_aiff(self, aiff_filename, progress=None): - """writes the contents of this file to the given .aiff filename string - - raises EncodingError if some error occurs during decoding""" - - if (self.has_foreign_aiff_chunks()): - AiffAudio.aiff_from_chunks(aiff_filename, - self.aiff_chunks(progress)) - else: - AiffAudio.from_pcm(aiff_filename, to_pcm_progress(self, progress)) - - def aiff_chunks(self, progress=None): - """yields a set of AIFF_Chunk or AIFF_File_Chunk objects""" - - from struct import unpack - - for application_block in [ - block.data for block in - self.get_metadata().get_blocks(Flac_APPLICATION.BLOCK_ID) - if (block.application_id == "aiff")]: - (chunk_id, chunk_size, chunk_data) = (application_block[0:4], - application_block[4:8], - application_block[8:]) - if (chunk_id == 'FORM'): - #skip 12 byte FORM<size>AIFF header - continue - elif (chunk_id == 'SSND'): - if (progress is not None): - yield FLAC_SSND_Chunk( - self.__total_frames__, - PCMReaderProgress(self.to_pcm(), - self.__total_frames__, - progress)) - else: - yield FLAC_SSND_Chunk(self.__total_frames__, self.to_pcm()) - else: - chunk_size = unpack(">I", chunk_size)[0] - yield AIFF_Chunk(chunk_id, chunk_size, chunk_data[0:chunk_size]) - - 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 a FLAC has embedded RIFF *and* embedded AIFF chunks, - #RIFF takes precedence if the target format supports both. - #It's hard to envision a scenario in which that would happen. - - import tempfile - - if (target_class == WaveAudio): - self.to_wave(target_path, progress=progress) - return WaveAudio(target_path) - elif (target_class == AiffAudio): - self.to_aiff(target_path, progress=progress) - return AiffAudio(target_path) - elif (self.has_foreign_riff_chunks() and - hasattr(target_class, "from_wave")): - temp_wave = tempfile.NamedTemporaryFile(suffix=".wav") - try: - #we'll only log the second leg of conversion, - #since that's likely to be the slower portion - self.to_wave(temp_wave.name) - return target_class.from_wave(target_path, - temp_wave.name, - compression, - progress=progress) - finally: - temp_wave.close() - elif (self.has_foreign_aiff_chunks() and - hasattr(target_class, "from_aiff")): - temp_aiff = tempfile.NamedTemporaryFile(suffix=".aiff") - try: - self.to_aiff(temp_aiff.name) - return target_class.from_aiff(target_path, - temp_aiff.name, - compression, - progress=progress) - finally: - temp_aiff.close() - else: - return target_class.from_pcm(target_path, - to_pcm_progress(self, progress), - compression) - - def bits_per_sample(self): - """returns an integer number of bits-per-sample this track contains""" - - return self.__bitspersample__ - - def channels(self): - """returns an integer number of channels this track contains""" - - return self.__channels__ - - def total_frames(self): - """returns the total PCM frames of the track as an integer""" - - return self.__total_frames__ - - def sample_rate(self): - """returns the rate of the track's audio as an integer number of Hz""" - - return self.__samplerate__ - - def __read_streaminfo__(self): - valid_header_types = frozenset(range(0, 6 + 1)) - f = file(self.filename, "rb") - try: - self.__stream_offset__ = skip_id3v2_comment(f) - f.read(4) - - from .bitstream import BitstreamReader - - reader = BitstreamReader(f, 0) - - stop = 0 - - while (stop == 0): - (stop, header_type, length) = reader.parse("1u 7u 24u") - if (header_type not in valid_header_types): - raise InvalidFLAC(_("invalid header type")) - elif (header_type == 0): - (self.__samplerate__, - self.__channels__, - self.__bitspersample__, - self.__total_frames__, - self.__md5__) = reader.parse("80p 20u 3u 5u 36U 16b") - self.__channels__ += 1 - self.__bitspersample__ += 1 - break - else: - #though the STREAMINFO should always be first, - #we'll be permissive and check them all if necessary - reader.skip_bytes(length) - finally: - f.close() - - @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 - """ - - tracks = [track for track in open_files(filenames) if - isinstance(track, cls)] - - if (len(tracks) > 0): - for (track, - track_gain, - track_peak, - album_gain, - album_peak) in calculate_replay_gain(tracks, progress): - metadata = track.get_metadata() - try: - comment = metadata.get_block( - Flac_VORBISCOMMENT.BLOCK_ID) - except IndexError: - comment = Flac_VORBISCOMMENT( - [], u"Python Audio Tools %s" % (VERSION)) - metadata.add_block(comment) - - comment["REPLAYGAIN_TRACK_GAIN"] = [ - "%1.2f dB" % (track_gain)] - comment["REPLAYGAIN_TRACK_PEAK"] = [ - "%1.8f" % (track_peak)] - comment["REPLAYGAIN_ALBUM_GAIN"] = [ - "%1.2f dB" % (album_gain)] - comment["REPLAYGAIN_ALBUM_PEAK"] = ["%1.8f" % (album_peak)] - comment["REPLAYGAIN_REFERENCE_LOUDNESS"] = [u"89.0 dB"] - track.update_metadata(metadata) - - @classmethod - def can_add_replay_gain(cls): - """returns True""" - - return True - - @classmethod - def lossless_replay_gain(cls): - """returns True""" - - return True - - def replay_gain(self): - """returns a ReplayGain object of our ReplayGain values - - returns None if we have no values""" - - try: - vorbis_metadata = self.get_metadata().get_block( - Flac_VORBISCOMMENT.BLOCK_ID) - except IndexError: - return None - - if (set(['REPLAYGAIN_TRACK_PEAK', 'REPLAYGAIN_TRACK_GAIN', - 'REPLAYGAIN_ALBUM_PEAK', 'REPLAYGAIN_ALBUM_GAIN']).issubset( - [key.upper() for key in vorbis_metadata.keys()])): - # we have ReplayGain data - try: - return ReplayGain( - vorbis_metadata['REPLAYGAIN_TRACK_GAIN'][0][0:-len(" dB")], - vorbis_metadata['REPLAYGAIN_TRACK_PEAK'][0], - vorbis_metadata['REPLAYGAIN_ALBUM_GAIN'][0][0:-len(" dB")], - vorbis_metadata['REPLAYGAIN_ALBUM_PEAK'][0]) - except ValueError: - return None - else: - return None - - def __eq__(self, audiofile): - if (isinstance(audiofile, FlacAudio)): - return self.__md5__ == audiofile.__md5__ - elif (isinstance(audiofile, AudioFile)): - try: - from hashlib import md5 - except ImportError: - from md5 import new as md5 - - p = audiofile.to_pcm() - m = md5() - s = p.read(BUFFER_SIZE) - while (len(s) > 0): - m.update(s.to_bytes(False, True)) - s = p.read(BUFFER_SIZE) - p.close() - return m.digest() == self.__md5__ - else: - return False - - def clean(self, fixes_performed, output_filename=None): - """cleans the file of known data and metadata problems - - fixes_performed is a list-like object which is appended - with Unicode strings of fixed problems - - output_filename is an optional filename of the fixed file - if present, a new AudioFile is returned - otherwise, only a dry-run is performed and no new file is written - - raises IOError if unable to write the file or its metadata - """ - - def seektable_valid(seektable, metadata_offset, input_file): - from .bitstream import BitstreamReader - reader = BitstreamReader(input_file, 0) - - for (pcm_frame_offset, - seekpoint_offset, - pcm_frame_count) in seektable.seekpoints: - input_file.seek(seekpoint_offset + metadata_offset) - try: - (sync_code, - reserved1, - reserved2) = reader.parse( - "14u 1u 1p 4p 4p 4p 3p 1u") - if ((sync_code != 0x3FFE) or - (reserved1 != 0) or - (reserved2 != 0)): - return False - except IOError: - return False - else: - return True - - if (output_filename is None): - #dry run only - - input_f = open(self.filename, "rb") - try: - #remove ID3 tags from before and after FLAC stream - stream_offset = skip_id3v2_comment(input_f) - if (stream_offset > 0): - fixes_performed.append(_(u"removed ID3v2 tag")) - input_f.seek(-128, 2) - if (input_f.read(3) == 'TAG'): - fixes_performed.append(_(u"removed ID3v1 tag")) - - #fix empty MD5SUM - if (self.__md5__ == chr(0) * 16): - fixes_performed.append(_(u"populated empty MD5SUM")) - - #FLAC should always have metadata - metadata = self.get_metadata() - - #fix missing WAVEFORMATEXTENSIBLE_CHANNEL_MASK - if ((self.channels() > 2) or (self.bits_per_sample() > 16)): - try: - if (u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK" not in - metadata.get_block( - Flac_VORBISCOMMENT.BLOCK_ID).keys()): - fixes_performed.append( - _(u"added WAVEFORMATEXTENSIBLE_CHANNEL_MASK")) - except IndexError: - fixes_performed.append( - _(u"added WAVEFORMATEXTENSIBLE_CHANNEL_MASK")) - - #fix an invalid SEEKTABLE, if present - try: - if (not seektable_valid( - metadata.get_block(Flac_SEEKTABLE.BLOCK_ID), - stream_offset + 4 + self.metadata_length(), - input_f)): - fixes_performed.append( - _(u"fixed invalid SEEKTABLE")) - except IndexError: - pass - - #fix any remaining metadata problems - metadata.clean(fixes_performed) - - finally: - input_f.close() - else: - #perform complete fix - - input_f = open(self.filename, "rb") - try: - #remove ID3 tags from before and after FLAC stream - stream_size = os.path.getsize(self.filename) - - stream_offset = skip_id3v2_comment(input_f) - if (stream_offset > 0): - fixes_performed.append(_(u"removed ID3v2 tag")) - stream_size -= stream_offset - - input_f.seek(-128, 2) - if (input_f.read(3) == 'TAG'): - fixes_performed.append(_(u"removed ID3v1 tag")) - stream_size -= 128 - - output_f = open(output_filename, "wb") - try: - input_f.seek(stream_offset, 0) - while (stream_size > 0): - s = input_f.read(4096) - if (len(s) > stream_size): - s = s[0:stream_size] - output_f.write(s) - stream_size -= len(s) - finally: - output_f.close() - - output_track = self.__class__(output_filename) - - metadata = self.get_metadata() - - #fix empty MD5SUM - if (self.__md5__ == chr(0) * 16): - from hashlib import md5 - md5sum = md5() - transfer_framelist_data( - self.to_pcm(), - md5sum.update, - signed=True, - big_endian=False) - metadata.get_block( - Flac_STREAMINFO.BLOCK_ID).md5sum = md5sum.digest() - fixes_performed.append(_(u"populated empty MD5SUM")) - - #fix missing WAVEFORMATEXTENSIBLE_CHANNEL_MASK - if ((self.channels() > 2) or (self.bits_per_sample() > 16)): - try: - vorbis_comment = metadata.get_block( - Flac_VORBISCOMMENT.BLOCK_ID) - except IndexError: - vorbis_comment = Flac_VORBISCOMMENT( - [], u"Python Audio Tools %s" % (VERSION)) - - if (u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK" not in - vorbis_comment.keys()): - fixes_performed.append( - _(u"added WAVEFORMATEXTENSIBLE_CHANNEL_MASK")) - vorbis_comment[ - u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"] = \ - [u"0x%.4X" % (self.channel_mask())] - - metadata.replace_blocks(Flac_VORBISCOMMENT.BLOCK_ID, - [vorbis_comment]) - - #fix an invalid SEEKTABLE, if present - try: - if (not seektable_valid( - metadata.get_block(Flac_SEEKTABLE.BLOCK_ID), - stream_offset + 4 + self.metadata_length(), - input_f)): - fixes_performed.append( - _(u"fixed invalid SEEKTABLE")) - - metadata.replace_blocks(Flac_SEEKTABLE.BLOCK_ID, - [self.seektable()]) - except IndexError: - pass - - #fix remaining metadata problems - #which automatically shifts STREAMINFO to the right place - #(the message indicating the fix has already been output) - output_track.update_metadata(metadata.clean(fixes_performed)) - - return output_track - finally: - input_f.close() - - -class FLAC_Data_Chunk: - def __init__(self, total_frames, pcmreader): - self.id = "data" - self.__total_frames__ = total_frames - self.__pcmreader__ = pcmreader - - def __repr__(self): - return "FLAC_Data_Chunk()" - - def size(self): - """returns size of chunk in bytes - not including any spacer byte for odd-sized chunks""" - - return (self.__total_frames__ * - self.__pcmreader__.channels * - (self.__pcmreader__.bits_per_sample / 8)) - - def verify(self): - "returns True" - - return True - - def write(self, f): - """writes the entire chunk to the given output file object - returns size of entire chunk (including header and spacer) - in bytes""" - - from struct import pack - - f.write(self.id) - f.write(pack("<I", self.size())) - bytes_written = 8 - signed = (self.__pcmreader__.bits_per_sample > 8) - s = self.__pcmreader__.read(0x10000) - while (len(s) > 0): - b = s.to_bytes(False, signed) - f.write(b) - bytes_written += len(b) - s = self.__pcmreader__.read(0x10000) - - if (bytes_written % 2): - f.write(chr(0)) - bytes_written += 1 - - return bytes_written - - -class FLAC_SSND_Chunk(FLAC_Data_Chunk): - def __init__(self, total_frames, pcmreader): - self.id = "SSND" - self.__total_frames__ = total_frames - self.__pcmreader__ = pcmreader - - def __repr__(self): - return "FLAC_SSND_Chunk()" - - def size(self): - """returns size of chunk in bytes - not including any spacer byte for odd-sized chunks""" - - return 8 + (self.__total_frames__ * - self.__pcmreader__.channels * - (self.__pcmreader__.bits_per_sample / 8)) - - def write(self, f): - """writes the entire chunk to the given output file object - returns size of entire chunk (including header and spacer) - in bytes""" - - from struct import pack - - f.write(self.id) - f.write(pack(">I", self.size())) - bytes_written = 8 - f.write(pack(">II", 0, 0)) - bytes_written += 8 - s = self.__pcmreader__.read(0x10000) - while (len(s) > 0): - b = s.to_bytes(True, True) - f.write(b) - bytes_written += len(b) - s = self.__pcmreader__.read(0x10000) - - if (bytes_written % 2): - f.write(chr(0)) - bytes_written += 1 - - return bytes_written - - -####################### -#Ogg FLAC -####################### - - -class OggFlacMetaData(FlacMetaData): - @classmethod - def converted(cls, metadata): - """takes a MetaData object and returns an OggFlacMetaData object""" - - if (metadata is None): - return None - elif (isinstance(metadata, FlacMetaData)): - return cls([block.copy() for block in metadata.block_list]) - else: - return cls([Flac_VORBISCOMMENT.converted(metadata)] + - [Flac_PICTURE.converted(image) - for image in metadata.images()]) - - def __repr__(self): - return ("OggFlacMetaData(%s)" % (repr(self.block_list))) - - @classmethod - def parse(cls, reader): - """returns an OggFlacMetaData object from the given BitstreamReader""" - - from . import read_ogg_packets - - streaminfo = None - applications = [] - seektable = None - vorbis_comment = None - cuesheet = None - pictures = [] - - packets = read_ogg_packets(reader) - - streaminfo_packet = packets.next() - streaminfo_packet.set_endianness(0) - - (packet_byte, - ogg_signature, - major_version, - minor_version, - header_packets, - flac_signature, - block_type, - block_length, - minimum_block_size, - maximum_block_size, - minimum_frame_size, - maximum_frame_size, - sample_rate, - channels, - bits_per_sample, - total_samples, - md5sum) = streaminfo_packet.parse( - "8u 4b 8u 8u 16u 4b 8u 24u 16u 16u 24u 24u 20u 3u 5u 36U 16b") - - block_list = [Flac_STREAMINFO(minimum_block_size=minimum_block_size, - maximum_block_size=maximum_block_size, - minimum_frame_size=minimum_frame_size, - maximum_frame_size=maximum_frame_size, - sample_rate=sample_rate, - channels=channels + 1, - bits_per_sample=bits_per_sample + 1, - total_samples=total_samples, - md5sum=md5sum)] - - for (i, packet) in zip(range(header_packets), packets): - packet.set_endianness(0) - (block_type, length) = packet.parse("1p 7u 24u") - if (block_type == 1): # PADDING - block_list.append(Flac_PADDING.parse(packet, length)) - if (block_type == 2): # APPLICATION - block_list.append(Flac_APPLICATION.parse(packet, length)) - elif (block_type == 3): # SEEKTABLE - block_list.append(Flac_SEEKTABLE.parse(packet, length / 18)) - elif (block_type == 4): # VORBIS_COMMENT - block_list.append(Flac_VORBISCOMMENT.parse(packet)) - elif (block_type == 5): # CUESHEET - block_list.append(Flac_CUESHEET.parse(packet)) - elif (block_type == 6): # PICTURE - block_list.append(Flac_PICTURE.parse(packet)) - elif ((block_type >= 7) and (block_type <= 126)): - raise ValueError(_(u"reserved metadata block type %d") % - (block_type)) - elif (block_type == 127): - raise ValueError(_(u"invalid metadata block type")) - - return cls(block_list) - - def build(self, oggwriter): - """oggwriter is an OggStreamWriter-compatible object""" - - from .bitstream import BitstreamRecorder - from .bitstream import format_size - from . import iter_first, iter_last - - packet = BitstreamRecorder(0) - - #build extended Ogg FLAC STREAMINFO block - #which will always occupy its own page - streaminfo = self.get_block(Flac_STREAMINFO.BLOCK_ID) - - #all our non-STREAMINFO blocks that are small enough - #to fit in the output stream - valid_blocks = [b for b in self.blocks() - if ((b.BLOCK_ID != Flac_STREAMINFO.BLOCK_ID) and - (b.size() < (2 ** 24)))] - - packet.build( - "8u 4b 8u 8u 16u 4b 8u 24u 16u 16u 24u 24u 20u 3u 5u 36U 16b", - (0x7F, "FLAC", 1, 0, len(valid_blocks), "fLaC", 0, - format_size("16u 16u 24u 24u 20u 3u 5u 36U 16b") / 8, - streaminfo.minimum_block_size, - streaminfo.maximum_block_size, - streaminfo.minimum_frame_size, - streaminfo.maximum_frame_size, - streaminfo.sample_rate, - streaminfo.channels - 1, - streaminfo.bits_per_sample - 1, - streaminfo.total_samples, - streaminfo.md5sum)) - oggwriter.write_page(0, [packet.data()], 0, 1, 0) - - #FIXME - adjust non-STREAMINFO blocks to use fewer pages - - #pack remaining metadata blocks into as few pages as possible - for (last_block, block) in iter_last(iter(valid_blocks)): - - packet.reset() - if (not last_block): - packet.build("1u 7u 24u", (0, block.BLOCK_ID, block.size())) - else: - packet.build("1u 7u 24u", (1, block.BLOCK_ID, block.size())) - - block.build(packet) - for (first_page, page_segments) in iter_first( - oggwriter.segments_to_pages( - oggwriter.packet_to_segments(packet.data()))): - oggwriter.write_page(0 if first_page else -1, - page_segments, - 0 if first_page else 1, 0, 0) - - -class __Counter__: - def __init__(self): - self.value = 0 - - def count_byte(self, i): - self.value += 1 - - def __int__(self): - return self.value - - -class OggFlacAudio(FlacAudio): - """a Free Lossless Audio Codec file inside an Ogg container""" - - SUFFIX = "oga" - NAME = SUFFIX - DEFAULT_COMPRESSION = "8" - COMPRESSION_MODES = tuple(map(str, range(0, 9))) - COMPRESSION_DESCRIPTIONS = {"0": _(u"least amount of compresson, " + - u"fastest compression speed"), - "8": _(u"most amount of compression, " + - u"slowest compression speed")} - BINARIES = ("flac",) - - METADATA_CLASS = OggFlacMetaData - - def __init__(self, filename): - """filename is a plain string""" - - AudioFile.__init__(self, filename) - self.__samplerate__ = 0 - self.__channels__ = 0 - self.__bitspersample__ = 0 - self.__total_frames__ = 0 - - try: - self.__read_streaminfo__() - except IOError, msg: - raise InvalidFLAC(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(0x23) - - return (header.startswith('OggS') and - header[0x1C:0x21] == '\x7FFLAC') - - def bits_per_sample(self): - """returns an integer number of bits-per-sample this track contains""" - - return self.__bitspersample__ - - def channels(self): - """returns an integer number of channels this track contains""" - - return self.__channels__ - - def total_frames(self): - """returns the total PCM frames of the track as an integer""" - - return self.__total_frames__ - - def sample_rate(self): - """returns the rate of the track's audio as an integer number of Hz""" - - return self.__samplerate__ - - def get_metadata(self): - """returns a MetaData object, or None - - raises IOError if unable to read the file""" - - f = open(self.filename, "rb") - try: - from .bitstream import BitstreamReader - - return OggFlacMetaData.parse(BitstreamReader(f, 1)) - finally: - f.close() - - def update_metadata(self, metadata): - """takes this track's current MetaData object - as returned by get_metadata() and sets this track's metadata - with any fields updated in that object - - raises IOError if unable to write the file - """ - - if (metadata is None): - return None - - if (not isinstance(metadata, OggFlacMetaData)): - raise ValueError(_(u"metadata not from audio file")) - - #always overwrite Ogg FLAC with fresh metadata - # - #The trouble with Ogg FLAC padding is that Ogg header overhead - #requires a variable amount of overhead bytes per Ogg page - #which makes it very difficult to calculate how many - #bytes to allocate to the PADDING packet. - #We'd have to build a bunch of empty pages for padding - #then go back and fill-in the initial padding page's length - #field before re-checksumming it. - - import tempfile - - from .bitstream import BitstreamWriter - from .bitstream import BitstreamRecorder - from .bitstream import BitstreamAccumulator - from .bitstream import BitstreamReader - from . import OggStreamReader, OggStreamWriter - - new_file = tempfile.TemporaryFile() - try: - original_file = file(self.filename, 'rb') - try: - original_reader = BitstreamReader(original_file, 1) - original_ogg = OggStreamReader(original_reader) - - new_writer = BitstreamWriter(new_file, 1) - new_ogg = OggStreamWriter(new_writer, - self.__serial_number__) - - #write our new comment blocks to the new file - metadata.build(new_ogg) - - #skip the metadata packets in the original file - OggFlacMetaData.parse(original_reader) - - #transfer the remaining pages from the original file - #(which are re-sequenced and re-checksummed automatically) - for (granule_position, - segments, - continuation, - first_page, - last_page) in original_ogg.pages(): - new_ogg.write_page(granule_position, - segments, - continuation, - first_page, - last_page) - finally: - original_file.close() - - #copy temporary file data over our original file - original_file = file(self.filename, "wb") - try: - new_file.seek(0, 0) - transfer_data(new_file.read, original_file.write) - new_file.close() - finally: - original_file.close() - finally: - new_file.close() - - def metadata_length(self): - """returns the length of all Ogg FLAC metadata blocks as an integer - - this includes all Ogg page headers""" - - from .bitstream import BitstreamReader - - f = file(self.filename, 'rb') - try: - byte_count = __Counter__() - ogg_stream = BitstreamReader(f, 1) - ogg_stream.add_callback(byte_count.count_byte) - - OggFlacMetaData.parse(ogg_stream) - - return int(byte_count) - finally: - f.close() - - def __read_streaminfo__(self): - from .bitstream import BitstreamReader - - f = open(self.filename, "rb") - try: - ogg_reader = BitstreamReader(f, 1) - (magic_number, - version, - header_type, - granule_position, - self.__serial_number__, - page_sequence_number, - checksum, - segment_count) = ogg_reader.parse("4b 8u 8u 64S 32u 32u 32u 8u") - - if (magic_number != 'OggS'): - raise InvalidFLAC(_(u"invalid Ogg magic number")) - if (version != 0): - raise InvalidFLAC(_(u"invalid Ogg version")) - - segment_length = ogg_reader.read(8) - - ogg_reader.set_endianness(0) - - (packet_byte, - ogg_signature, - major_version, - minor_version, - self.__header_packets__, - flac_signature, - block_type, - block_length, - minimum_block_size, - maximum_block_size, - minimum_frame_size, - maximum_frame_size, - self.__samplerate__, - self.__channels__, - self.__bitspersample__, - self.__total_frames__, - self.__md5__) = ogg_reader.parse( - "8u 4b 8u 8u 16u 4b 8u 24u 16u 16u 24u 24u 20u 3u 5u 36U 16b") - - if (packet_byte != 0x7F): - raise InvalidFLAC(_(u"invalid packet byte")) - if (ogg_signature != 'FLAC'): - raise InvalidFLAC(_(u"invalid Ogg signature")) - if (major_version != 1): - raise InvalidFLAC(_(u"invalid major version")) - if (minor_version != 0): - raise InvalidFLAC(_(u"invalid minor version")) - if (flac_signature != 'fLaC'): - raise InvalidFLAC(_(u"invalid FLAC signature")) - - self.__channels__ += 1 - self.__bitspersample__ += 1 - finally: - f.close() - - def to_pcm(self): - """returns a PCMReader object containing the track's PCM data""" - - from . import decoders - - try: - return decoders.OggFlacDecoder(self.filename, - self.channel_mask()) - except (IOError, ValueError), msg: - #The only time this is likely to occur is - #if the Ogg FLAC is modified between when OggFlacAudio - #is initialized and when to_pcm() is called. - return PCMReaderError(error_message=str(msg), - 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 OggFlacAudio object""" - - SUBSTREAM_SAMPLE_RATES = frozenset([ - 8000, 16000, 22050, 24000, 32000, - 44100, 48000, 96000]) - SUBSTREAM_BITS = frozenset([8, 12, 16, 20, 24]) - - if ((compression is None) or - (compression not in cls.COMPRESSION_MODES)): - compression = __default_quality__(cls.NAME) - - if ((pcmreader.sample_rate in SUBSTREAM_SAMPLE_RATES) and - (pcmreader.bits_per_sample in SUBSTREAM_BITS)): - lax = [] - else: - lax = ["--lax"] - - if (pcmreader.channels > 8): - raise UnsupportedChannelCount(filename, pcmreader.channels) - - if (int(pcmreader.channel_mask) == 0): - if (pcmreader.channels <= 6): - channel_mask = {1: 0x0004, - 2: 0x0003, - 3: 0x0007, - 4: 0x0033, - 5: 0x0037, - 6: 0x003F}[pcmreader.channels] - else: - channel_mask = 0 - - elif (int(pcmreader.channel_mask) not in - (0x0001, # 1ch - mono - 0x0004, # 1ch - mono - 0x0003, # 2ch - left, right - 0x0007, # 3ch - left, right, center - 0x0033, # 4ch - left, right, back left, back right - 0x0603, # 4ch - left, right, side left, side right - 0x0037, # 5ch - L, R, C, back left, back right - 0x0607, # 5ch - L, R, C, side left, side right - 0x003F, # 6ch - L, R, C, LFE, back left, back right - 0x060F)): # 6ch - L, R, C, LFE, side left, side right - raise UnsupportedChannelMask(filename, - int(pcmreader.channel_mask)) - else: - channel_mask = int(pcmreader.channel_mask) - - devnull = file(os.devnull, 'ab') - - sub = subprocess.Popen([BIN['flac']] + lax + \ - ["-s", "-f", "-%s" % (compression), - "-V", "--ogg", - "--endian=little", - "--channels=%d" % (pcmreader.channels), - "--bps=%d" % (pcmreader.bits_per_sample), - "--sample-rate=%d" % (pcmreader.sample_rate), - "--sign=signed", - "--force-raw-format", - "-o", filename, "-"], - stdin=subprocess.PIPE, - stdout=devnull, - stderr=devnull, - preexec_fn=ignore_sigint) - - try: - transfer_framelist_data(pcmreader, sub.stdin.write) - except (ValueError, IOError), err: - sub.stdin.close() - sub.wait() - cls.__unlink__(filename) - raise EncodingError(str(err)) - except Exception, err: - sub.stdin.close() - sub.wait() - cls.__unlink__(filename) - raise err - - try: - pcmreader.close() - except DecodingError, err: - raise EncodingError(err.error_message) - sub.stdin.close() - devnull.close() - - if (sub.wait() == 0): - oggflac = OggFlacAudio(filename) - if (((pcmreader.channels > 2) or - (pcmreader.bits_per_sample > 16)) and - (channel_mask != 0)): - metadata = oggflac.get_metadata() - metadata.get_block(Flac_VORBISCOMMENT.BLOCK_ID)[ - u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"] = [ - u"0x%.4X" % (channel_mask)] - oggflac.update_metadata(metadata) - return oggflac - else: - raise EncodingError(u"error encoding file with flac") - - def sub_pcm_tracks(self): - """yields a PCMReader object per cuesheet track - - this currently does nothing since the FLAC reference - decoder has limited support for Ogg FLAC - """ - - return iter([]) - - 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""" - - from audiotools import verify_ogg_stream - - #Ogg stream 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, err: - raise InvalidFLAC(str(err)) - try: - try: - result = verify_ogg_stream(f) - if (progress is not None): - progress(1, 1) - return result - except (IOError, ValueError), err: - raise InvalidFLAC(str(err)) - finally: - f.close()
View file
audiotools-2.18.tar.gz/audiotools/__freedb__.py
Deleted
@@ -1,739 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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 - - -##### DEPRECATION WARNING ##### -#This whole module will go away very soon. -# -#Use the audiotools.freedb.perform_lookup() function -#which takes a CD's table-of-contents -#and performs the entire FreeDB lookup and parsing process -#rather than use these functions and classes -#to parse and edit those files directly. - - -from audiotools import (VERSION, cStringIO, sys, re, MetaData, - AlbumMetaData, AlbumMetaDataFile, __most_numerous__, - DummyAudioFile, MetaDataFileException) -import StringIO -import gettext - -gettext.install("audiotools", unicode=True) - -####################### -#XMCD -####################### - - -#DEPRECATED - this class will soon be removed -class XMCDException(MetaDataFileException): - """raised if some error occurs parsing an XMCD file""" - - def __unicode__(self): - return _(u"Invalid XMCD file") - - -#DEPRECATED - this class will soon be removed -class XMCD(AlbumMetaDataFile): - LINE_LENGTH = 78 - - def __init__(self, fields, comments): - """fields a dict of key->values. comment is a list of comments - - keys are plain strings. values and comments are unicode""" - - self.fields = fields - self.comments = comments - - def __getattr__(self, key): - if (key == 'album_name'): - dtitle = self.fields.get('DTITLE', u"") - if (u" / " in dtitle): - return dtitle.split(u" / ", 1)[1] - else: - return dtitle - elif (key == 'artist_name'): - dtitle = self.fields.get('DTITLE', u"") - if (u" / " in dtitle): - return dtitle.split(u" / ", 1)[0] - else: - return u"" - elif (key == 'year'): - return self.fields.get('DYEAR', u"") - elif (key == 'catalog'): - return u"" - elif (key == 'extra'): - return self.fields.get('EXTD', u"") - else: - try: - return self.__dict__[key] - except KeyError: - raise AttributeError(key) - - def __setattr__(self, key, value): - if (key == 'album_name'): - dtitle = self.fields.get('DTITLE', u"") - if (u" / " in dtitle): - artist = dtitle.split(u" / ", 1)[0] - self.fields['DTITLE'] = u"%s / %s" % (artist, value) - else: - self.fields['DTITLE'] = value - elif (key == 'artist_name'): - dtitle = self.fields.get('DTITLE', u"") - if (u" / " in dtitle): - album = dtitle.split(u" / ", 1)[1] - else: - album = dtitle - self.fields['DTITLE'] = u"%s / %s" % (value, album) - elif (key == 'year'): - self.fields['DYEAR'] = value - elif (key == 'catalog'): - pass - elif (key == 'extra'): - self.fields['EXTD'] = value - else: - self.__dict__[key] = value - - def __len__(self): - track_field = re.compile(r'(TTITLE|EXTT)(\d+)') - - return max(set([int(m.group(2)) for m in - [track_field.match(key) for key in self.fields.keys()] - if m is not None])) + 1 - - def to_string(self): - def write_field(f, key, value): - chars = list(value) - encoded_value = "%s=" % (key) - - while ((len(chars) > 0) and - (len(encoded_value + - chars[0].encode('utf-8', 'replace')) < - XMCD.LINE_LENGTH)): - encoded_value += chars.pop(0).encode('utf-8', 'replace') - - f.write("%s\r\n" % (encoded_value)) - if (len(chars) > 0): - write_field(f, key, u"".join(chars)) - - output = cStringIO.StringIO() - - for comment in self.comments: - output.write(comment.encode('utf-8')) - output.write('\r\n') - - fields = set(self.fields.keys()) - for field in ['DISCID', 'DTITLE', 'DYEAR', 'DGENRE']: - if (field in fields): - write_field(output, field, self.fields[field]) - fields.remove(field) - - for i in xrange(len(self)): - field = 'TTITLE%d' % (i) - if (field in fields): - write_field(output, field, self.fields[field]) - fields.remove(field) - - if ('EXTD' in fields): - write_field(output, 'EXTD', self.fields['EXTD']) - fields.remove('EXTD') - - for i in xrange(len(self)): - field = 'EXTT%d' % (i) - if (field in fields): - write_field(output, field, self.fields[field]) - fields.remove(field) - - for field in fields: - write_field(output, field, self.fields[field]) - - return output.getvalue() - - @classmethod - def from_string(cls, string): - # try: - # data = string.decode('latin-1') - # except UnicodeDecodeError: - # data = string.decode('utf-8','replace') - #FIXME - handle latin-1 files? - data = string.decode('utf-8', 'replace') - - if (not data.startswith(u"# xmcd")): - raise XMCDException() - - fields = {} - comments = [] - field_line = re.compile(r'([A-Z0-9]+?)=(.*)') - - for line in StringIO.StringIO(data): - if (line.startswith(u'#')): - comments.append(line.rstrip('\r\n')) - else: - match = field_line.match(line.rstrip('\r\n')) - if (match is not None): - key = match.group(1).encode('ascii') - value = match.group(2) - if (key in fields): - fields[key] += value - else: - fields[key] = value - - return cls(fields, comments) - - def get_track(self, index): - try: - ttitle = self.fields['TTITLE%d' % (index)] - track_extra = self.fields['EXTT%d' % (index)] - except KeyError: - return (u"", u"", u"") - - if (u' / ' in ttitle): - (track_artist, track_title) = ttitle.split(u' / ', 1) - else: - track_title = ttitle - track_artist = u"" - - return (track_title, track_artist, track_extra) - - def set_track(self, index, name, artist, extra): - if ((index < 0) or (index >= len(self))): - raise IndexError(index) - - if (len(artist) > 0): - self.fields["TTITLE%d" % (index)] = u"%s / %s" % (artist, name) - else: - self.fields["TTITLE%d" % (index)] = name - - if (len(extra) > 0): - self.fields["EXTT%d" % (index)] = extra - - @classmethod - def from_tracks(cls, tracks): - def track_string(track, album_artist, metadata): - if (track.track_number() in metadata.keys()): - metadata = metadata[track.track_number()] - if (metadata.artist_name == album_artist): - return metadata.track_name - else: - return u"%s / %s" % (metadata.artist_name, - metadata.track_name) - else: - return u"" - - audiofiles = [f for f in tracks if f.track_number() != 0] - audiofiles.sort(lambda t1, t2: cmp(t1.track_number(), - t2.track_number())) - - discid = DiscID([track.cd_frames() for track in audiofiles]) - - metadata = dict([(t.track_number(), t.get_metadata()) - for t in audiofiles - if (t.get_metadata() is not None)]) - - artist_names = [m.artist_name for m in metadata.values()] - if (len(artist_names) == 0): - album_artist = u"" - elif ((len(artist_names) > 1) and - (len(set(artist_names)) == len(artist_names))): - #if all track artists are different, don't pick one - album_artist = u"Various" - else: - album_artist = __most_numerous__(artist_names) - - return cls(dict([("DISCID", str(discid).decode('ascii')), - ("DTITLE", u"%s / %s" % \ - (album_artist, - __most_numerous__([m.album_name for m in - metadata.values()]))), - ("DYEAR", __most_numerous__([m.year for m in - metadata.values()])), - ("EXTDD", u""), - ("PLAYORDER", u"")] + \ - [("TTITLE%d" % (track.track_number() - 1), - track_string(track, album_artist, metadata)) - for track in audiofiles] + \ - [("EXTT%d" % (track.track_number() - 1), - u"") - for track in audiofiles]), - [u"# xmcd", - u"#", - u"# Track frame offsets:"] + - [u"#\t%d" % (offset) for offset in discid.offsets()] + - [u"#", - u"# Disc length: %d seconds" % ( - (discid.length() / 75) + 2), - u"#"]) - - -####################### -#FREEDB -####################### - -#DEPRECATED - this class will soon be removed -class DiscID: - """an object representing a 32 bit FreeDB disc ID value""" - - def __init__(self, tracks=[], offsets=None, length=None, lead_in=150): - """fields are as follows: - - tracks - a list of track lengths in CD frames - offsets - a list of track offsets in CD frames - length - the length of the entire disc in CD frames - lead_in - the location of the first track on the CD, in frames - - these fields are all optional - one will presumably fill them with data later in that event - """ - - self.tracks = tracks - self.__offsets__ = offsets - self.__length__ = length - self.__lead_in__ = lead_in - - @classmethod - def from_cdda(cls, cdda): - """given a CDDA object, returns a populated DiscID - - may raise ValueError if there are no audio tracks on the CD""" - - tracks = list(cdda) - if (len(tracks) < 1): - raise ValueError(_(u"no audio tracks in CDDA object")) - - return cls(tracks=[t.length() for t in tracks], - offsets=[t.offset() for t in tracks], - length=cdda.last_sector(), - lead_in=tracks[0].offset()) - - def add(self, track): - """adds a new track length, in CD frames""" - - self.tracks.append(track) - - def offsets(self): - """returns a list of calculated offset integers, from track lengths""" - - if (self.__offsets__ is None): - offsets = [self.__lead_in__] - - for track in self.tracks[0:-1]: - offsets.append(track + offsets[-1]) - - return offsets - else: - return self.__offsets__ - - def length(self): - """returns the total length of the disc, in seconds""" - - if (self.__length__ is None): - return sum(self.tracks) - else: - return self.__length__ - - def idsuffix(self): - """returns a FreeDB disc ID suffix string - - this is for making server queries""" - - return str(len(self.tracks)) + " " + \ - " ".join([str(offset) for offset in self.offsets()]) + \ - " " + str((self.length() + self.__lead_in__) / 75) - - def __str__(self): - def __count_digits__(i): - if (i == 0): - return 0 - else: - return (i % 10) + __count_digits__(i / 10) - - track_count = len(self.tracks) - length = self.length() / 75 - digit_sum = sum([__count_digits__(o / 75) - for o in self.offsets()]) % 0xFF - - return "%8.8X" % (((digit_sum & 0xFF) << 24) | - ((length & 0xFFFF) << 8) | - (track_count & 0xFF)) - - def freedb_id(self): - """returns the entire FreeDB disc ID, including suffix""" - - return str(self) + " " + self.idsuffix() - - def toxmcd(self, output): - """writes a newly created XMCD file to output - - its values are populated from this DiscID's fields""" - - output.write(XMCD.from_tracks( - [DummyAudioFile(length, None, i + 1) - for (i, length) in enumerate(self.tracks)]).to_string()) - - -#DEPRECATED - this class will soon be removed -class FreeDBException(Exception): - """raised if some problem occurs during FreeDB querying""" - - pass - - -#DEPRECATED - this class will soon be removed -class FreeDB: - """a class for performing queries on a FreeDB or compatible server - - this operates using the original FreeDB client-server protocol""" - - LINE = re.compile(r'\d\d\d\s.+') - - def __init__(self, server, port, messenger): - """server is a string, port is an int, messenger is a Messenger - - queries are sent to the server, and output to the messenger""" - - self.server = server - self.port = port - self.socket = None - self.r = None - self.w = None - self.messenger = messenger - - def connect(self): - """performs the initial connection""" - - import socket - - try: - self.messenger.info(_(u"Connecting to \"%s\"") % (self.server)) - - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.socket.connect((self.server, self.port)) - - self.r = self.socket.makefile("rb") - self.w = self.socket.makefile("wb") - - (code, msg) = self.read() # the welcome message - if (code == 201): - self.messenger.info(_(u"Connected ... attempting to login")) - else: - self.r.close() - self.w.close() - self.socket.close() - raise FreeDBException(_(u"Invalid hello message")) - - self.write("cddb hello user %s %s %s" % \ - (socket.getfqdn(), "audiotools", VERSION)) - - (code, msg) = self.read() # the handshake successful message - if (code != 200): - self.r.close() - self.w.close() - self.socket.close() - raise FreeDBException(_(u"Handshake unsuccessful")) - - self.write("proto 6") - - (code, msg) = self.read() # the protocol successful message - if ((code != 200) and (code != 201)): - self.r.close() - self.w.close() - self.socket.close() - raise FreeDBException(_(u"Protocol change unsuccessful")) - - except socket.error, err: - raise FreeDBException(err[1]) - - def close(self): - """closes an open connection""" - - self.messenger.info(_(u"Closing connection")) - - self.write("quit") - (code, msg) = self.read() # the quit successful message - - self.r.close() - self.w.close() - self.socket.close() - - def write(self, line): - """writes a single command line to the server""" - - if (self.socket is not None): - self.w.write(line) - self.w.write("\r\n") - self.w.flush() - - def read(self): - """reads a result line from the server""" - - line = self.r.readline() - if (FreeDB.LINE.match(line)): - return (int(line[0:3]), line[4:].rstrip("\r\n")) - else: - return (None, line.rstrip("\r\n")) - - def query(self, disc_id): - """given a DiscID, performs an album query and returns matches - - each match is a (category, id) pair, which the user may - need to decide between""" - - matches = [] - - self.messenger.info( - _(u"Sending Disc ID \"%(disc_id)s\" to server \"%(server)s\"") % \ - {"disc_id": str(disc_id).decode('ascii'), - "server": self.server.decode('ascii', 'replace')}) - - self.write("cddb query " + disc_id.freedb_id()) - (code, msg) = self.read() - if (code == 200): - matches.append(msg) - elif ((code == 211) or (code == 210)): - while (msg != "."): - (code, msg) = self.read() - if (msg != "."): - matches.append(msg) - - if (len(matches) == 1): - self.messenger.info(_(u"1 match found")) - else: - self.messenger.info(_(u"%s matches found") % (len(matches))) - - return map(lambda m: m.split(" ", 2), matches) - - def read_data(self, category, id, output): - """reads the FreeDB entry matching category and id to output - - category and id are raw strings, as returned by query() - output is an open file object - """ - - self.write("cddb read " + category + " " + id) - (code, msg) = self.read() - if (code == 210): - line = self.r.readline() - while (line.strip() != "."): - output.write(line) - line = self.r.readline() - else: - print >> sys.stderr, (code, msg) - - -#DEPRECATED - this class will soon be removed -class FreeDBWeb(FreeDB): - """a class for performing queries on a FreeDB or compatible server - - this operates using the FreeDB web-based protocol""" - - def __init__(self, server, port, messenger): - """server is a string, port is an int, messenger is a Messenger - - queries are sent to the server, and output to the messenger""" - - self.server = server - self.port = port - self.connection = None - self.messenger = messenger - - def connect(self): - """performs the initial connection""" - - import httplib - - self.connection = httplib.HTTPConnection(self.server, self.port, - timeout=10) - - def close(self): - """closes an open connection""" - - if (self.connection is not None): - self.connection.close() - - def write(self, line): - """writes a single command line to the server""" - - import urllib - import socket - - u = urllib.urlencode({"hello": "user %s %s %s" % \ - (socket.getfqdn(), - "audiotools", - VERSION), - "proto": str(6), - "cmd": line}) - - try: - self.connection.request( - "POST", - "/~cddb/cddb.cgi", - u, - {"Content-type": "application/x-www-form-urlencoded", - "Accept": "text/plain"}) - except socket.error, msg: - raise FreeDBException(str(msg)) - - def read(self): - """reads a result line from the server""" - - response = self.connection.getresponse() - return response.read() - - def __parse_line__(self, line): - if (FreeDB.LINE.match(line)): - return (int(line[0:3]), line[4:].rstrip("\r\n")) - else: - return (None, line.rstrip("\r\n")) - - def query(self, disc_id): - """given a DiscID, performs an album query and returns matches - - each match is a (category, id) pair, which the user may - need to decide between""" - - matches = [] - - self.messenger.info( - _(u"Sending Disc ID \"%(disc_id)s\" to server \"%(server)s\"") % \ - {"disc_id": str(disc_id).decode('ascii'), - "server": self.server.decode('ascii', 'replace')}) - - self.write("cddb query " + disc_id.freedb_id()) - data = cStringIO.StringIO(self.read()) - (code, msg) = self.__parse_line__(data.readline()) - if (code == 200): - matches.append(msg) - elif ((code == 211) or (code == 210)): - while (msg != "."): - (code, msg) = self.__parse_line__(data.readline()) - if (msg != "."): - matches.append(msg) - - if (len(matches) == 1): - self.messenger.info(_(u"1 match found")) - else: - self.messenger.info(_(u"%s matches found") % (len(matches))) - - return map(lambda m: m.split(" ", 2), matches) - - def read_data(self, category, id, output): - """reads the FreeDB entry matching category and id to output - - category and id are raw strings, as returned by query() - output is an open file object - """ - - self.write("cddb read " + category + " " + id) - data = cStringIO.StringIO(self.read()) - (code, msg) = self.__parse_line__(data.readline()) - if (code == 210): - line = data.readline() - while (line.strip() != "."): - output.write(line) - line = data.readline() - else: - print >> sys.stderr, (code, msg) - - -#matches is a list of (category,disc_id,title) tuples returned from -#FreeDB.query(). If the length of that list is 1, return the first -#item. If the length is greater than one, present the user a list of -#choices and force him/her to pick the closest match for the CD. -#That data can then be sent to FreeDB.read_data() - -#DEPRECATED - this function will soon be removed -def __select_match__(matches, messenger): - if (len(matches) == 1): - return matches[0] - elif (len(matches) < 1): - return None - else: - messenger.info(_(u"Please Select the Closest Match:")) - selected = 0 - while ((selected < 1) or (selected > len(matches))): - for i in range(len(matches)): - messenger.info(_(u"%(choice)s) [%(genre)s] %(name)s") % \ - {"choice": i + 1, - "genre": matches[i][0], - "name": matches[i][2].decode('utf-8', - 'replace')}) - try: - messenger.partial_info(_(u"Your Selection [1-%s]:") % \ - (len(matches))) - selected = int(sys.stdin.readline().strip()) - except ValueError: - selected = 0 - - return matches[selected - 1] - - -#DEPRECATED - this function will soon be removed -def __select_default_match__(matches, selection): - if (len(matches) < 1): - return None - else: - try: - return matches[selection] - except IndexError: - return matches[0] - - -#DEPRECATED - this function will soon be removed -def get_xmcd(disc_id, output, freedb_server, freedb_server_port, - messenger, default_selection=None): - """runs through the entire FreeDB querying sequence - - fields are as follows: - disc_id - a DiscID object - output - an open file object for writing - freedb_server - a server name string - freedb_port - a server port int - messenger - a Messenger object - default_selection - if given, the default match to choose - """ - - try: - freedb = FreeDBWeb(freedb_server, freedb_server_port, messenger) - freedb.connect() - except FreeDBException, msg: - #if an exception occurs during the opening, - #freedb will auto-close its sockets - raise IOError(str(msg)) - - try: - matches = freedb.query(disc_id) - #HANDLE MULTIPLE MATCHES, or NO MATCHES - if (len(matches) > 0): - if (default_selection is None): - (category, idstring, title) = __select_match__( - matches, messenger) - else: - (category, idstring, title) = __select_default_match__( - matches, default_selection) - - freedb.read_data(category, idstring, output) - output.flush() - - freedb.close() - except FreeDBException, msg: - #otherwise, close the sockets manually - freedb.close() - raise IOError(str(msg)) - - return len(matches)
View file
audiotools-2.18.tar.gz/audiotools/__id3__.py
Deleted
@@ -1,2185 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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 (MetaData, re, os, cStringIO, - Image, InvalidImage, config) -import codecs -import gettext - - -def is_latin_1(unicode_string): - """returns True if the given unicode string is a subset of latin-1""" - - return frozenset(unicode_string).issubset( - frozenset(map(unichr, range(32, 127) + range(160, 256)))) - - -class UCS2Codec(codecs.Codec): - """a special unicode codec for UCS-2 - - this is a subset of UTF-16 with no support for surrogate pairs, - limiting it to U+0000-U+FFFF""" - - @classmethod - def fix_char(cls, c): - """a filter which changes overly large c values to 'unknown'""" - - if (ord(c) <= 0xFFFF): - return c - else: - return u"\ufffd" - - def encode(self, input, errors='strict'): - """encodes unicode input to plain UCS-2 strings""" - - return codecs.utf_16_encode(u"".join(map(self.fix_char, input)), - errors) - - def decode(self, input, errors='strict'): - """decodes plain UCS-2 strings to unicode""" - - (chars, size) = codecs.utf_16_decode(input, errors, True) - return (u"".join(map(self.fix_char, chars)), size) - - -class UCS2CodecStreamWriter(UCS2Codec, codecs.StreamWriter): - pass - - -class UCS2CodecStreamReader(UCS2Codec, codecs.StreamReader): - pass - - -def __reg_ucs2__(name): - if (name == 'ucs2'): - return (UCS2Codec().encode, - UCS2Codec().decode, - UCS2CodecStreamReader, - UCS2CodecStreamWriter) - else: - return None - -codecs.register(__reg_ucs2__) - - -def decode_syncsafe32(reader): - """returns a syncsafe32 integer from a BitstreamReader""" - - from operator import or_ - return reduce(or_, - [size << (7 * (3 - i)) - for (i, size) in - enumerate(reader.parse("1p 7u 1p 7u 1p 7u 1p 7u"))]) - - -def encode_syncsafe32(writer, value): - """writes a syncsafe32 integer to a BitstreamWriter""" - - writer.build("1p 7u 1p 7u 1p 7u 1p 7u", - [(value >> (7 * i)) & 0x7F for i in [3, 2, 1, 0]]) - - -class C_string: - TERMINATOR = {'ascii': chr(0), - 'latin_1': chr(0), - 'latin-1': chr(0), - 'ucs2': chr(0) * 2, - 'utf_16': chr(0) * 2, - 'utf-16': chr(0) * 2, - 'utf_16be': chr(0) * 2, - 'utf-16be': chr(0) * 2, - 'utf_8': chr(0), - 'utf-8': chr(0)} - - def __init__(self, encoding, unicode_string): - """encoding is a string such as 'utf-8', 'latin-1', etc""" - - self.encoding = encoding - self.unicode_string = unicode_string - - def __repr__(self): - return "C_string(%s, %s)" % (repr(self.encoding), - repr(self.unicode_string)) - - def __unicode__(self): - return self.unicode_string - - def __getitem__(self, char): - return self.unicode_string[char] - - def __len__(self): - return len(self.unicode_string) - - def __cmp__(self, c_string): - return cmp(self.unicode_string, c_string.unicode_string) - - @classmethod - def parse(cls, encoding, reader): - """returns a C_string with the given encoding string - from the given BitstreamReader - raises LookupError if encoding is unknown - raises IOError if a problem occurs reading the stream - """ - - try: - terminator = cls.TERMINATOR[encoding] - terminator_size = len(terminator) - except KeyError: - raise LookupError(encoding) - - s = [] - char = reader.read_bytes(terminator_size) - while (char != terminator): - s.append(char) - char = reader.read_bytes(terminator_size) - - return cls(encoding, "".join(s).decode(encoding, 'replace')) - - def build(self, writer): - """writes our C_string data to the given BitstreamWriter - with the appropriate terminator""" - - writer.write_bytes(self.unicode_string.encode(self.encoding, - 'replace')) - writer.write_bytes(self.TERMINATOR[self.encoding]) - - def size(self): - """returns the length of our C string in bytes""" - - return (len(self.unicode_string.encode(self.encoding, 'replace')) + - len(self.TERMINATOR[self.encoding])) - - -def __attrib_equals__(attributes, o1, o2): - for attrib in attributes: - if ((not hasattr(o1, attrib)) or - (not hasattr(o2, attrib)) or - (getattr(o1, attrib) != getattr(o2, attrib))): - return False - else: - return True - - -#takes a pair of integers for the current and total values -#returns a unicode string of their combined pair -#for example, __number_pair__(2,3) returns u"2/3" -#whereas __number_pair__(4,0) returns u"4" -def __number_pair__(current, total): - if (config.getboolean_default("ID3", "pad", False)): - if (total == 0): - return u"%2.2d" % (current) - else: - return u"%2.2d/%2.2d" % (current, total) - else: - if (total == 0): - return u"%d" % (current) - else: - return u"%d/%d" % (current, total) - - -def read_id3v2_comment(filename): - """given a filename, returns an ID3v22Comment or a subclass - - for example, if the file is ID3v2.3 tagged, - this returns an ID3v23Comment - """ - - from .bitstream import BitstreamReader - - reader = BitstreamReader(file(filename, "rb"), 0) - reader.mark() - try: - (tag, version_major, version_minor) = reader.parse("3b 8u 8u") - if (tag != 'ID3'): - raise ValueError("invalid ID3 header") - elif (version_major == 0x2): - reader.rewind() - return ID3v22Comment.parse(reader) - elif (version_major == 0x3): - reader.rewind() - return ID3v23Comment.parse(reader) - elif (version_major == 0x4): - reader.rewind() - return ID3v24Comment.parse(reader) - else: - raise ValueError("unsupported ID3 version") - finally: - reader.unmark() - reader.close() - - -def skip_id3v2_comment(file): - """seeks past an ID3v2 comment if found in the file stream - returns the number of bytes skipped - - the stream must be seekable, obviously""" - - from .bitstream import BitstreamReader - - bytes_skipped = 0 - reader = BitstreamReader(file, 0) - reader.mark() - try: - (tag_id, version_major, version_minor) = reader.parse("3b 8u 8u 8p") - except IOError, err: - reader.unmark() - raise err - - if ((tag_id == 'ID3') and (version_major in (2, 3, 4))): - reader.unmark() - - #parse the header - bytes_skipped += 6 - tag_size = decode_syncsafe32(reader) - bytes_skipped += 4 - - #skip to the end of its length - reader.skip_bytes(tag_size) - bytes_skipped += tag_size - - #skip any null bytes after the IDv2 tag - reader.mark() - try: - byte = reader.read(8) - while (byte == 0): - reader.unmark() - bytes_skipped += 1 - reader.mark() - byte = reader.read(8) - - reader.rewind() - reader.unmark() - - return bytes_skipped - except IOError, err: - reader.unmark() - raise err - else: - reader.rewind() - reader.unmark() - return 0 - - -############################################################ -# ID3v2.2 Comment -############################################################ - - -class ID3v22_Frame: - def __init__(self, frame_id, data): - self.id = frame_id - self.data = data - - def copy(self): - return self.__class__(self.id, self.data) - - def __repr__(self): - return "ID3v22_Frame(%s, %s)" % (repr(self.id), repr(self.data)) - - def raw_info(self): - if (len(self.data) > 20): - return u"%s = %s\u2026" % \ - (self.id.decode('ascii', 'replace'), - u"".join([u"%2.2X" % (ord(b)) for b in self.data[0:20]])) - else: - return u"%s = %s" % \ - (self.id.decode('ascii', 'replace'), - u"".join([u"%2.2X" % (ord(b)) for b in self.data])) - - def __eq__(self, frame): - return __attrib_equals__(["id", "data"], self, frame) - - @classmethod - def parse(cls, frame_id, frame_size, reader): - """given a frame_id string, frame_size int and BitstreamReader - of the remaining frame data, returns a parsed ID3v2?_Frame""" - - return cls(frame_id, reader.read_bytes(frame_size)) - - def build(self, writer): - """writes this frame to the given BitstreamWriter - not including its frame header""" - - writer.write_bytes(self.data) - - def size(self): - """returns the size of this frame in bytes - not including the frame header""" - - return len(self.data) - - @classmethod - def converted(cls, frame_id, o): - """given foreign data, returns an ID3v22_Frame""" - - raise NotImplementedError() - - def clean(self, fixes_applied): - """returns a cleaned ID3v22_Frame, - or None if the frame should be removed entirely - any fixes are appended to fixes_applied as unicode string""" - - return self.__class__(self.id, self.data) - - -class ID3v22_T__Frame: - NUMERICAL_IDS = ('TRK', 'TPA') - - def __init__(self, frame_id, encoding, data): - """fields are as follows: - | frame_id | 3 byte frame ID string | - | encoding | 1 byte encoding int | - | data | text data as raw string | - """ - - assert((encoding == 0) or (encoding == 1)) - - self.id = frame_id - self.encoding = encoding - self.data = data - - def copy(self): - return self.__class__(self.id, self.encoding, self.data) - - def __repr__(self): - return "ID3v22_T__Frame(%s, %s, %s)" % \ - (repr(self.id), repr(self.encoding), repr(self.data)) - - def raw_info(self): - """returns a human-readable version of this frame as unicode""" - - return u"%s = (%s) %s" % \ - (self.id.decode('ascii'), - {0: u"Latin-1", 1: u"UCS-2"}[self.encoding], - unicode(self)) - - def __eq__(self, frame): - return __attrib_equals__(["id", "encoding", "data"], self, frame) - - def __unicode__(self): - return self.data.decode( - {0: 'latin-1', 1: 'ucs2'}[self.encoding], - 'replace').split(unichr(0), 1)[0] - - def number(self): - """if the frame is numerical, returns the track/album_number portion - raises TypeError if not""" - - if (self.id in self.NUMERICAL_IDS): - try: - return int(re.findall(r'\d+', unicode(self))[0]) - except IndexError: - return 0 - else: - raise TypeError() - - def total(self): - """if the frame is numerical, returns the track/album_total portion - raises TypeError if not""" - - if (self.id in self.NUMERICAL_IDS): - try: - return int(re.findall(r'\d+/(\d+)', unicode(self))[0]) - except IndexError: - return 0 - else: - raise TypeError() - - @classmethod - def parse(cls, frame_id, frame_size, reader): - """given a frame_id string, frame_size int and BitstreamReader - of the remaining frame data, returns a parsed text frame""" - - encoding = reader.read(8) - return cls(frame_id, encoding, reader.read_bytes(frame_size - 1)) - - def build(self, writer): - """writes the frame's data to the BitstreamWriter - not including its frame header""" - - writer.build("8u %db" % (len(self.data)), (self.encoding, self.data)) - - def size(self): - """returns the frame's total size - not including its frame header""" - - return 1 + len(self.data) - - @classmethod - def converted(cls, frame_id, unicode_string): - """given a unicode string, returns a text frame""" - - if (is_latin_1(unicode_string)): - return cls(frame_id, 0, unicode_string.encode('latin-1')) - else: - return cls(frame_id, 1, unicode_string.encode('ucs2')) - - def clean(self, fixes_performed): - """returns a cleaned frame, - or None if the frame should be removed entirely - any fixes are appended to fixes_applied as unicode string""" - - field = self.id.decode('ascii') - value = unicode(self) - - #check for an empty tag - if (len(value.strip()) == 0): - fixes_performed.append( - u"removed empty field %(field)s" % - {"field": field}) - return None - - #check trailing whitespace - fix1 = value.rstrip() - if (fix1 != value): - fixes_performed.append( - u"removed trailing whitespace from %(field)s" % - {"field": field}) - - #check leading whitespace - fix2 = fix1.lstrip() - if (fix2 != fix1): - fixes_performed.append( - u"removed leading whitespace from %(field)s" % - {"field": field}) - - #check leading zeroes for a numerical tag - if (self.id in self.NUMERICAL_IDS): - fix3 = __number_pair__(self.number(), self.total()) - if (fix3 != fix2): - if (config.getboolean_default("ID3", "pad", False)): - fixes_performed.append( - u"added leading zeroes to %(field)s" % - {"field": field}) - else: - fixes_performed.append( - u"removed leading zeroes from %(field)s" % - {"field": field}) - else: - fix3 = fix2 - - return self.__class__.converted(self.id, fix3) - - -class ID3v22_TXX_Frame: - def __init__(self, encoding, description, data): - self.id = 'TXX' - - self.encoding = encoding - self.description = description - self.data = data - - def copy(self): - return self.__class__(self.encoding, - self.description, - self.data) - - def __repr__(self): - return "ID3v22_TXX_Frame(%s, %s, %s)" % \ - (repr(self.encoding), repr(self.description), repr(self.data)) - - def raw_info(self): - """returns a human-readable version of this frame as unicode""" - - return u"%s = (%s, \"%s\") %s" % \ - (self.id, - {0: u"Latin-1", 1: u"UCS-2"}[self.encoding], - self.description, - unicode(self)) - - def __eq__(self, frame): - return __attrib_equals__(["id", "encoding", "description", "data"]) - - def __unicode__(self): - return self.data.decode( - {0: 'latin-1', 1: 'ucs2'}[self.encoding], - 'replace').split(unichr(0), 1)[0] - - @classmethod - def parse(cls, frame_id, frame_size, reader): - """given a frame_id string, frame_size int and BitstreamReader - of the remaining frame data, returns a parsed text frame""" - - encoding = reader.read(8) - description = C_string.parse({0: "latin-1", 1: "ucs2"}[encoding], - reader) - data = reader.read_bytes(frame_size - 1 - description.size()) - - return cls(encoding, description, data) - - def build(self, writer): - """writes this frame to the given BitstreamWriter - not including its frame header""" - - writer.write(8, self.encoding) - self.description.build(writer) - writer.write_bytes(self.data) - - def size(self): - """returns the size of this frame in bytes - not including the frame header""" - - return 1 + self.description.size() + len(self.data) - - def clean(self, fixes_performed): - """returns a cleaned frame, - or None if the frame should be removed entirely - any fixes are appended to fixes_applied as unicode string""" - - field = self.id.decode('ascii') - value = unicode(self) - - #check for an empty tag - if (len(value.strip()) == 0): - fixes_performed.append( - u"removed empty field %(field)s" % - {"field": field}) - return None - - #check trailing whitespace - fix1 = value.rstrip() - if (fix1 != value): - fixes_performed.append( - u"removed trailing whitespace from %(field)s" % - {"field": field}) - - #check leading whitespace - fix2 = fix1.lstrip() - if (fix2 != fix1): - fixes_performed.append( - u"removed leading whitespace from %(field)s" % - {"field": field}) - - return self.__class__(self.encoding, self.description, fix2) - - -class ID3v22_W__Frame: - def __init__(self, frame_id, data): - self.id = frame_id - self.data = data - - def copy(self): - return self.__class__(self.frame_id, self.data) - - def __repr__(self): - return "ID3v22_W__Frame(%s, %s)" % \ - (repr(self.frame_id), repr(self.data)) - - def raw_info(self): - """returns a human-readable version of this frame as unicode""" - - return u"%s = %s" % (self.id.decode('ascii'), - self.data.decode('ascii', 'replace')) - - def __eq__(self, frame): - return __attrib_equals__(["id", "data"], self, frame) - - @classmethod - def parse(cls, frame_id, frame_size, reader): - return cls(frame_id, reader.read_bytes(frame_size)) - - def build(self, writer): - """writes this frame to the given BitstreamWriter - not including its frame header""" - - writer.write_bytes(self.data) - - def size(self): - """returns the size of this frame in bytes - not including the frame header""" - - return len(self.data) - - def clean(self, fixes_applied): - """returns a cleaned frame, - or None if the frame should be removed entirely - any fixes are appended to fixes_applied as unicode string""" - - return self.__class__(self.frame_id, self.data) - - -class ID3v22_WXX_Frame: - def __init__(self, encoding, description, data): - self.id = 'WXX' - - self.encoding = encoding - self.description = description - self.data = data - - def copy(self): - return self.__class__(self.encoding, - self.description, - self.data) - - def __repr__(self): - return "ID3v22_WXX_Frame(%s, %s, %s)" % \ - (repr(self.encoding), repr(self.description), repr(self.data)) - - def raw_info(self): - """returns a human-readable version of this frame as unicode""" - - return u"%s = (%s, \"%s\") %s" % \ - (self.id, - {0: u"Latin-1", 1: u"UCS-2"}[self.encoding], - self.description, - self.data.decode('ascii', 'replace')) - - def __eq__(self, frame): - return __attrib_equals__(["id", "encoding", "description", "data"]) - - @classmethod - def parse(cls, frame_id, frame_size, reader): - """given a frame_id string, frame_size int and BitstreamReader - of the remaining frame data, returns a parsed text frame""" - - encoding = reader.read(8) - description = C_string.parse({0: "latin-1", 1: "ucs2"}[encoding], - reader) - data = reader.read_bytes(frame_size - 1 - description.size()) - - return cls(encoding, description, data) - - def build(self, writer): - """writes this frame to the given BitstreamWriter - not including its frame header""" - - writer.write(8, self.encoding) - self.description.build(writer) - writer.write_bytes(self.data) - - def size(self): - """returns the size of this frame in bytes - not including the frame header""" - - return 1 + self.description.size() + len(self.data) - - def clean(self, fixes_performed): - """returns a cleaned frame, - or None if the frame should be removed entirely - any fixes are appended to fixes_applied as unicode string""" - - return self.__class__(self.encoding, - self.description, - self.data) - - -class ID3v22_COM_Frame: - def __init__(self, encoding, language, short_description, data): - """fields are as follows: - | encoding | 1 byte int of the comment's text encoding | - | language | 3 byte string of the comment's language | - | short_description | C_string of a short description | - | data | plain string of the comment data itself | - """ - - self.id = "COM" - self.encoding = encoding - self.language = language - self.short_description = short_description - self.data = data - - def copy(self): - return self.__class__(self.encoding, - self.language, - self.short_description, - self.data) - - def __repr__(self): - return "ID3v22_COM_Frame(%s, %s, %s, %s)" % \ - (repr(self.encoding), repr(self.language), - repr(self.short_description), repr(self.data)) - - def raw_info(self): - """returns a human-readable version of this frame as unicode""" - - return u"COM = (%s, %s, \"%s\") %s" % \ - ({0: u'Latin-1', 1: 'UCS-2'}[self.encoding], - self.language.decode('ascii', 'replace'), - self.short_description, - self.data.decode({0: 'latin-1', 1: 'ucs2'}[self.encoding])) - - def __eq__(self, frame): - return __attrib_equals__(["encoding", - "language", - "short_description", - "data"], self, frame) - - def __unicode__(self): - return self.data.decode({0: 'latin-1', 1: 'ucs2'}[self.encoding], - 'replace') - - @classmethod - def parse(cls, frame_id, frame_size, reader): - """given a frame_id string, frame_size int and BitstreamReader - of the remaining frame data, returns a parsed ID3v22_COM_Frame""" - - (encoding, language) = reader.parse("8u 3b") - short_description = C_string.parse({0: 'latin-1', 1: 'ucs2'}[encoding], - reader) - data = reader.read_bytes(frame_size - (4 + short_description.size())) - - return cls(encoding, language, short_description, data) - - def build(self, writer): - """writes this frame to the given BitstreamWriter - not including its frame header""" - - writer.build("8u 3b", (self.encoding, self.language)) - self.short_description.build(writer) - writer.write_bytes(self.data) - - def size(self): - """returns the size of this frame in bytes - not including the frame header""" - - return 4 + self.short_description.size() + len(self.data) - - @classmethod - def converted(cls, frame_id, unicode_string): - if (is_latin_1(unicode_string)): - return cls(0, "eng", C_string("latin-1", u""), - unicode_string.encode('latin-1')) - else: - return cls(1, "eng", C_string("ucs2", u""), - unicode_string.encode('ucs2')) - - def clean(self, fixes_performed): - """returns a cleaned frame of the same class - or None if the frame should be omitted - fix text will be appended to fixes_performed, if necessary""" - - field = self.id.decode('ascii') - text_encoding = {0: 'latin-1', 1: 'ucs2'} - - value = self.data.decode(text_encoding[self.encoding], 'replace') - - #check for an empty tag - if (len(value.strip()) == 0): - fixes_performed.append( - u"removed empty field %(field)s" % - {"field": field}) - return None - - #check trailing whitespace - fix1 = value.rstrip() - if (fix1 != value): - fixes_performed.append( - u"removed trailing whitespace from %(field)s" % - {"field": field}) - - #check leading whitespace - fix2 = fix1.lstrip() - if (fix2 != fix1): - fixes_performed.append( - u"removed leading whitespace from %(field)s" % - {"field": field}) - - #stripping whitespace shouldn't alter text/description encoding - - return self.__class__(self.encoding, - self.language, - self.short_description, - fix2.encode(text_encoding[self.encoding])) - - -class ID3v22_PIC_Frame(Image): - def __init__(self, image_format, picture_type, description, data): - """fields are as follows: - | image_format | a 3 byte image format, such as 'JPG' | - | picture_type | a 1 byte field indicating front cover, etc. | - | description | a description of the image as a C_string | - | data | image data itself as a raw string | - """ - - self.id = 'PIC' - - #add PIC-specific fields - self.pic_format = image_format - self.pic_type = picture_type - self.pic_description = description - - #figure out image metrics from raw data - try: - metrics = Image.new(data, u'', 0) - except InvalidImage: - metrics = Image(data=data, mime_type=u'', - width=0, height=0, color_depth=0, color_count=0, - description=u'', type=0) - - #then initialize Image parent fields from metrics - self.mime_type = metrics.mime_type - self.width = metrics.width - self.height = metrics.height - self.color_depth = metrics.color_depth - self.color_count = metrics.color_count - self.data = data - - def copy(self): - return ID3v22_PIC_Frame(self.pic_format, - self.pic_type, - self.pic_description, - self.data) - - def __repr__(self): - return "ID3v22_PIC_Frame(%s, %s, %s, ...)" % \ - (repr(self.pic_format), repr(self.pic_type), - repr(self.pic_description)) - - def raw_info(self): - """returns a human-readable version of this frame as unicode""" - - return u"PIC = (%s, %d\u00D7%d, %s, \"%s\") %d bytes" % \ - (self.type_string(), - self.width, - self.height, - self.mime_type, - self.pic_description, - len(self.data)) - - def type_string(self): - return {0: "Other", - 1: "32x32 pixels 'file icon' (PNG only)", - 2: "Other file icon", - 3: "Cover (front)", - 4: "Cover (back)", - 5: "Leaflet page", - 6: "Media (e.g. label side of CD)", - 7: "Lead artist/lead performer/soloist", - 8: "Artist / Performer", - 9: "Conductor", - 10: "Band / Orchestra", - 11: "Composer", - 12: "Lyricist / Text writer", - 13: "Recording Location", - 14: "During recording", - 15: "During performance", - 16: "Movie/Video screen capture", - 17: "A bright coloured fish", - 18: "Illustration", - 19: "Band/Artist logotype", - 20: "Publisher/Studio logotype"}.get(self.pic_type, "Other") - - def __getattr__(self, attr): - if (attr == 'type'): - return {3: 0, # front cover - 4: 1, # back cover - 5: 2, # leaflet page - 6: 3 # media - }.get(self.pic_type, 4) # other - elif (attr == 'description'): - return unicode(self.pic_description) - else: - raise AttributeError(attr) - - def __setattr__(self, attr, value): - if (attr == 'type'): - self.__dict__["pic_type"] = {0: 3, # front cover - 1: 4, # back cover - 2: 5, # leaflet page - 3: 6, # media - }.get(value, 0) # other - elif (attr == 'description'): - if (is_latin_1(value)): - self.__dict__["pic_description"] = C_string('latin-1', value) - else: - self.__dict__["pic_description"] = C_string('ucs2', value) - else: - self.__dict__[attr] = value - - @classmethod - def parse(cls, frame_id, frame_size, reader): - (encoding, image_format, picture_type) = reader.parse("8u 3b 8u") - description = C_string.parse({0: 'latin-1', - 1: 'ucs2'}[encoding], reader) - data = reader.read_bytes(frame_size - (5 + description.size())) - return cls(image_format, - picture_type, - description, - data) - - def build(self, writer): - """writes this frame to the given BitstreamWriter - not including its frame header""" - - writer.build("8u 3b 8u", ({'latin-1': 0, - 'ucs2': 1}[self.pic_description.encoding], - self.pic_format, - self.pic_type)) - self.pic_description.build(writer) - writer.write_bytes(self.data) - - def size(self): - """returns the size of this frame in bytes - not including the frame header""" - - return (5 + self.pic_description.size() + len(self.data)) - - @classmethod - def converted(cls, frame_id, image): - if (is_latin_1(image.description)): - description = C_string('latin-1', image.description) - else: - description = C_string('ucs2', image.description) - - return cls(image_format={u"image/png": u"PNG", - u"image/jpeg": u"JPG", - u"image/jpg": u"JPG", - u"image/x-ms-bmp": u"BMP", - u"image/gif": u"GIF", - u"image/tiff": u"TIF"}.get(image.mime_type, - 'UNK'), - picture_type={0: 3, # front cover - 1: 4, # back cover - 2: 5, # leaflet page - 3: 6, # media - }.get(image.type, 0), # other - description=description, - data=image.data) - - def clean(self, fixes_performed): - """returns a cleaned ID3v22_PIC_Frame, - or None if the frame should be removed entirely - any fixes are appended to fixes_applied as unicode string""" - - #all the fields are derived from the image data - #so there's no need to test for a mismatch - - #not sure if it's worth testing for bugs in the description - #or format fields - - return ID3v22_PIC_Frame(self.pic_format, - self.pic_type, - self.pic_description, - self.data) - - -class ID3v22Comment(MetaData): - NAME = u'ID3v2.2' - - ATTRIBUTE_MAP = {'track_name': 'TT2', - 'track_number': 'TRK', - 'track_total': 'TRK', - 'album_name': 'TAL', - 'artist_name': 'TP1', - 'performer_name': 'TP2', - 'conductor_name': 'TP3', - 'composer_name': 'TCM', - 'media': 'TMT', - 'ISRC': 'TRC', - 'copyright': 'TCR', - 'publisher': 'TPB', - 'year': 'TYE', - 'date': 'TRD', - 'album_number': 'TPA', - 'album_total': 'TPA', - 'comment': 'COM'} - - RAW_FRAME = ID3v22_Frame - TEXT_FRAME = ID3v22_T__Frame - USER_TEXT_FRAME = ID3v22_TXX_Frame - WEB_FRAME = ID3v22_W__Frame - USER_WEB_FRAME = ID3v22_WXX_Frame - COMMENT_FRAME = ID3v22_COM_Frame - IMAGE_FRAME = ID3v22_PIC_Frame - IMAGE_FRAME_ID = 'PIC' - - def __init__(self, frames): - self.__dict__["frames"] = frames[:] - - def copy(self): - return self.__class__([frame.copy() for frame in self]) - - def __repr__(self): - return "ID3v22Comment(%s)" % (repr(self.frames)) - - def __iter__(self): - return iter(self.frames) - - def raw_info(self): - """returns a human-readable version of this frame as unicode""" - - from os import linesep - - return linesep.decode('ascii').join( - ["%s:" % (self.NAME)] + - [frame.raw_info() for frame in self]) - - @classmethod - def parse(cls, reader): - """given a BitstreamReader, returns a parsed ID3v22Comment""" - - (id3, - major_version, - minor_version, - flags) = reader.parse("3b 8u 8u 8u") - if (id3 != 'ID3'): - raise ValueError("invalid ID3 header") - elif (major_version != 0x02): - raise ValueError("invalid major version") - elif (minor_version != 0x00): - raise ValueError("invalid minor version") - total_size = decode_syncsafe32(reader) - - frames = [] - - while (total_size > 0): - (frame_id, frame_size) = reader.parse("3b 24u") - - if (frame_id == chr(0) * 3): - break - elif (frame_id == 'TXX'): - frames.append(cls.USER_TEXT_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - elif (frame_id == 'WXX'): - frames.append(cls.USER_WEB_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - elif (frame_id == 'COM'): - frames.append(cls.COMMENT_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - elif (frame_id == 'PIC'): - frames.append(cls.IMAGE_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - elif (frame_id.startswith('T')): - frames.append(cls.TEXT_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - elif (frame_id.startswith('W')): - frames.append(cls.WEB_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - else: - frames.append(cls.RAW_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - - total_size -= (6 + frame_size) - - return cls(frames) - - def build(self, writer): - """writes the complete ID3v22Comment data - to the given BitstreamWriter""" - - from operator import add - - writer.build("3b 8u 8u 8u", ("ID3", 0x02, 0x00, 0x00)) - encode_syncsafe32(writer, - reduce(add, [6 + frame.size() for frame in self], 0)) - - for frame in self: - writer.build("3b 24u", (frame.id, frame.size())) - frame.build(writer) - - def size(self): - """returns the total size of the ID3v22Comment, including its header""" - - from operator import add - - return reduce(add, [6 + frame.size() for frame in self], 10) - - def __len__(self): - return len(self.frames) - - def __getitem__(self, frame_id): - frames = [frame for frame in self if (frame.id == frame_id)] - if (len(frames) > 0): - return frames - else: - raise KeyError(frame_id) - - def __setitem__(self, frame_id, frames): - new_frames = frames[:] - updated_frames = [] - - for old_frame in self: - if (old_frame.id == frame_id): - try: - #replace current frame with newly set frame - updated_frames.append(new_frames.pop(0)) - except IndexError: - #no more newly set frames, so remove current frame - continue - else: - #passthrough unmatched frames - updated_frames.append(old_frame) - else: - #append any leftover frames - for new_frame in new_frames: - updated_frames.append(new_frame) - - self.__dict__["frames"] = updated_frames - - def __delitem__(self, frame_id): - updated_frames = [frame for frame in self if frame.id != frame_id] - if (len(updated_frames) < len(self)): - self.__dict__["frames"] = updated_frames - else: - raise KeyError(frame_id) - - def keys(self): - return list(set([frame.id for frame in self])) - - def values(self): - return [self[key] for key in self.keys()] - - def items(self): - return [(key, self[key]) for key in self.keys()] - - def __getattr__(self, attr): - if (attr in self.ATTRIBUTE_MAP): - try: - frame = self[self.ATTRIBUTE_MAP[attr]][0] - if (attr in ('track_number', 'album_number')): - return frame.number() - elif (attr in ('track_total', 'album_total')): - return frame.total() - else: - return unicode(frame) - except KeyError: - if (attr in self.INTEGER_FIELDS): - return 0 - else: - return u"" - elif (attr in self.FIELDS): - return u"" - else: - raise AttributeError(attr) - - def __setattr__(self, attr, value): - if (attr in self.ATTRIBUTE_MAP): - frame_id = self.ATTRIBUTE_MAP[attr] - if (attr == 'track_number'): - self[frame_id] = [self.TEXT_FRAME.converted( - frame_id, __number_pair__(value, self.track_total))] - elif (attr == 'track_total'): - self[frame_id] = [self.TEXT_FRAME.converted( - frame_id, __number_pair__(self.track_number, value))] - elif (attr == 'album_number'): - self[frame_id] = [self.TEXT_FRAME.converted( - frame_id, __number_pair__(value, self.album_total))] - elif (attr == 'album_total'): - self[frame_id] = [self.TEXT_FRAME.converted( - frame_id, __number_pair__(self.album_number, value))] - elif (attr == 'comment'): - self[frame_id] = [self.COMMENT_FRAME.converted( - frame_id, value)] - else: - self[frame_id] = [ - self.TEXT_FRAME.converted(frame_id, unicode(value))] - elif (attr in MetaData.FIELDS): - pass - else: - self.__dict__[attr] = value - - def __delattr__(self, attr): - if (attr in self.ATTRIBUTE_MAP): - updated_frames = [] - delete_frame_id = self.ATTRIBUTE_MAP[attr] - for frame in self: - if (frame.id == delete_frame_id): - if ((attr == 'track_number') or (attr == 'album_number')): - #handle the *_number numerical fields - if (frame.total() != 0): - #if *_number is deleted, but *_total is present - #build new frame with only the *_total field - updated_frames.append( - self.TEXT_FRAME( - frame.id, - frame.encoding, - __number_pair__(0, frame.total()))) - else: - #if both *_number and *_total are deleted, - #delete frame entirely - continue - elif ((attr == 'track_total') or (attr == 'album_total')): - #handle the *_total numerical fields - if (frame.number() != 0): - #if *_total is deleted, but *_number is present - #build a new frame with only the *_number field - updated_frames.append( - self.TEXT_FRAME( - frame.id, - frame.encoding, - __number_pair__(frame.number(), 0))) - else: - #if both *_number and *_total are deleted, - #delete frame entirely - continue - else: - #handle the textual fields - #which are simply deleted outright - continue - else: - updated_frames.append(frame) - - self.__dict__["frames"] = updated_frames - - elif (attr in MetaData.FIELDS): - #ignore deleted attributes which are in MetaData - #but we don't support - pass - else: - try: - del(self.__dict__[attr]) - except KeyError: - raise AttributeError(attr) - - def images(self): - return [frame for frame in self if (frame.id == self.IMAGE_FRAME_ID)] - - def add_image(self, image): - self.frames.append( - self.IMAGE_FRAME.converted(self.IMAGE_FRAME_ID, image)) - - def delete_image(self, image): - self.__dict__["frames"] = [frame for frame in self if - ((frame.id != self.IMAGE_FRAME_ID) or - (frame != image))] - - @classmethod - def converted(cls, metadata): - """converts a MetaData object to an ID3v2*Comment object""" - - if (metadata is None): - return None - elif (cls is metadata.__class__): - return cls([frame.copy() for frame in metadata]) - - frames = [] - - for (attr, key) in cls.ATTRIBUTE_MAP.items(): - value = getattr(metadata, attr) - if ((attr not in cls.INTEGER_FIELDS) and - (len(value) > 0)): - if (attr == 'comment'): - frames.append(cls.COMMENT_FRAME.converted(key, value)) - else: - frames.append(cls.TEXT_FRAME.converted(key, value)) - - if ((metadata.track_number != 0) or - (metadata.track_total != 0)): - frames.append(cls.TEXT_FRAME.converted( - cls.ATTRIBUTE_MAP["track_number"], - __number_pair__(metadata.track_number, - metadata.track_total))) - - if ((metadata.album_number != 0) or - (metadata.album_total != 0)): - frames.append(cls.TEXT_FRAME.converted( - cls.ATTRIBUTE_MAP["album_number"], - __number_pair__(metadata.album_number, - metadata.album_total))) - - for image in metadata.images(): - frames.append(cls.IMAGE_FRAME.converted(cls.IMAGE_FRAME_ID, image)) - - if (hasattr(cls, 'ITUNES_COMPILATION_ID')): - frames.append(cls.TEXT_FRAME.converted( - cls.ITUNES_COMPILATION_ID, u'1')) - - return cls(frames) - - def clean(self, fixes_performed): - """returns a new MetaData object that's been cleaned of problems""" - - return self.__class__([filtered_frame for filtered_frame in - [frame.clean(fixes_performed) for frame in self] - if filtered_frame is not None]) - - -############################################################ -# ID3v2.3 Comment -############################################################ - - -class ID3v23_Frame(ID3v22_Frame): - def __repr__(self): - return "ID3v23_Frame(%s, %s)" % (repr(self.id), repr(self.data)) - - -class ID3v23_T___Frame(ID3v22_T__Frame): - NUMERICAL_IDS = ('TRCK', 'TPOS') - - def __repr__(self): - return "ID3v23_T___Frame(%s, %s, %s)" % \ - (repr(self.id), repr(self.encoding), repr(self.data)) - - -class ID3v23_TXXX_Frame(ID3v22_TXX_Frame): - def __init__(self, encoding, description, data): - self.id = 'TXXX' - - self.encoding = encoding - self.description = description - self.data = data - - def __repr__(self): - return "ID3v23_TXXX_Frame(%s, %s, %s)" % \ - (repr(self.encoding), repr(self.description), repr(self.data)) - - -class ID3v23_W___Frame(ID3v22_W__Frame): - def __repr__(self): - return "ID3v23_W___Frame(%s, %s)" % \ - (repr(self.frame_id), repr(self.data)) - - -class ID3v23_WXXX_Frame(ID3v22_WXX_Frame): - def __init__(self, encoding, description, data): - self.id = 'WXXX' - - self.encoding = encoding - self.description = description - self.data = data - - def __repr__(self): - return "ID3v23_WXXX_Frame(%s, %s, %s)" % \ - (repr(self.encoding), repr(self.description), repr(self.data)) - - -class ID3v23_APIC_Frame(ID3v22_PIC_Frame): - def __init__(self, mime_type, picture_type, description, data): - """fields are as follows: - | mime_type | a C_string of the image's MIME type | - | picture_type | a 1 byte field indicating front cover, etc. | - | description | a description of the image as a C_string | - | data | image data itself as a raw string | - """ - - self.id = 'APIC' - - #add APIC-specific fields - self.pic_type = picture_type - self.pic_description = description - self.pic_mime_type = mime_type - - #figure out image metrics from raw data - try: - metrics = Image.new(data, u'', 0) - except InvalidImage: - metrics = Image(data=data, mime_type=u'', - width=0, height=0, color_depth=0, color_count=0, - description=u'', type=0) - - #then initialize Image parent fields from metrics - self.width = metrics.width - self.height = metrics.height - self.color_depth = metrics.color_depth - self.color_count = metrics.color_count - self.data = data - - def copy(self): - return self.__class__(self.pic_mime_type, - self.pic_type, - self.pic_description, - self.data) - - def __repr__(self): - return "ID3v23_APIC_Frame(%s, %s, %s, ...)" % \ - (repr(self.pic_mime_type), repr(self.pic_type), - repr(self.pic_description)) - - def raw_info(self): - """returns a human-readable version of this frame as unicode""" - - return u"APIC = (%s, %d\u00D7%d, %s, \"%s\") %d bytes" % \ - (self.type_string(), - self.width, - self.height, - self.pic_mime_type, - self.pic_description, - len(self.data)) - - def __getattr__(self, attr): - if (attr == 'type'): - return {3: 0, # front cover - 4: 1, # back cover - 5: 2, # leaflet page - 6: 3 # media - }.get(self.pic_type, 4) # other - elif (attr == 'description'): - return unicode(self.pic_description) - elif (attr == 'mime_type'): - return unicode(self.pic_mime_type) - else: - raise AttributeError(attr) - - def __setattr__(self, attr, value): - if (attr == 'type'): - self.__dict__["pic_type"] = {0: 3, # front cover - 1: 4, # back cover - 2: 5, # leaflet page - 3: 6, # media - }.get(value, 0) # other - elif (attr == 'description'): - if (is_latin_1(value)): - self.__dict__["pic_description"] = C_string('latin-1', value) - else: - self.__dict__["pic_description"] = C_string('ucs2', value) - elif (attr == 'mime_type'): - self.__dict__["pic_mime_type"] = C_string('ascii', value) - else: - self.__dict__[attr] = value - - @classmethod - def parse(cls, frame_id, frame_size, reader): - """parses this frame from the given BitstreamReader""" - - encoding = reader.read(8) - mime_type = C_string.parse('ascii', reader) - picture_type = reader.read(8) - description = C_string.parse({0: 'latin-1', - 1: 'ucs2'}[encoding], reader) - data = reader.read_bytes(frame_size - (1 + - mime_type.size() + - 1 + - description.size())) - - return cls(mime_type, picture_type, description, data) - - def build(self, writer): - """writes this frame to the given BitstreamWriter - not including its frame header""" - - writer.write(8, {'latin-1': 0, - 'ucs2': 1}[self.pic_description.encoding]) - self.pic_mime_type.build(writer) - writer.write(8, self.pic_type) - self.pic_description.build(writer) - writer.write_bytes(self.data) - - def size(self): - """returns the size of this frame in bytes - not including the frame header""" - - return (1 + - self.pic_mime_type.size() + - 1 + - self.pic_description.size() + - len(self.data)) - - @classmethod - def converted(cls, frame_id, image): - if (is_latin_1(image.description)): - description = C_string('latin-1', image.description) - else: - description = C_string('ucs2', image.description) - - return cls(mime_type=C_string('ascii', image.mime_type), - picture_type={0: 3, # front cover - 1: 4, # back cover - 2: 5, # leaflet page - 3: 6, # media - }.get(image.type, 0), # other - description=description, - data=image.data) - - def clean(self, fixes_performed): - """returns a cleaned ID3v23_APIC_Frame, - or None if the frame should be removed entirely - any fixes are appended to fixes_applied as unicode string""" - - actual_mime_type = Image.new(self.data, u"", 0).mime_type - if (unicode(self.pic_mime_type) != actual_mime_type): - fixes_performed.append(u"fixed embedded image's MIME type") - return ID3v23_APIC_Frame(C_string('ascii', - actual_mime_type.encode('ascii')), - self.pic_type, - self.pic_description, - self.data) - else: - return ID3v23_APIC_Frame(self.pic_mime_type, - self.pic_type, - self.pic_description, - self.data) - - -class ID3v23_COMM_Frame(ID3v22_COM_Frame): - def __init__(self, encoding, language, short_description, data): - """fields are as follows: - | encoding | 1 byte int of the comment's text encoding | - | language | 3 byte string of the comment's language | - | short_description | C_string of a short description | - | data | plain string of the comment data itself | - """ - - self.id = "COMM" - self.encoding = encoding - self.language = language - self.short_description = short_description - self.data = data - - def __repr__(self): - return "ID3v23_COMM_Frame(%s, %s, %s, %s)" % \ - (repr(self.encoding), repr(self.language), - repr(self.short_description), repr(self.data)) - - def raw_info(self): - """returns a human-readable version of this frame as unicode""" - - return u"COMM = (%s, %s, \"%s\") %s" % \ - ({0: u'Latin-1', 1: 'UCS-2'}[self.encoding], - self.language.decode('ascii', 'replace'), - self.short_description, - self.data.decode({0: 'latin-1', 1: 'ucs2'}[self.encoding])) - - -class ID3v23Comment(ID3v22Comment): - NAME = u'ID3v2.3' - - ATTRIBUTE_MAP = {'track_name': 'TIT2', - 'track_number': 'TRCK', - 'track_total': 'TRCK', - 'album_name': 'TALB', - 'artist_name': 'TPE1', - 'performer_name': 'TPE2', - 'composer_name': 'TCOM', - 'conductor_name': 'TPE3', - 'media': 'TMED', - 'ISRC': 'TSRC', - 'copyright': 'TCOP', - 'publisher': 'TPUB', - 'year': 'TYER', - 'date': 'TRDA', - 'album_number': 'TPOS', - 'album_total': 'TPOS', - 'comment': 'COMM'} - - RAW_FRAME = ID3v23_Frame - TEXT_FRAME = ID3v23_T___Frame - WEB_FRAME = ID3v23_W___Frame - USER_TEXT_FRAME = ID3v23_TXXX_Frame - USER_WEB_FRAME = ID3v23_WXXX_Frame - COMMENT_FRAME = ID3v23_COMM_Frame - IMAGE_FRAME = ID3v23_APIC_Frame - IMAGE_FRAME_ID = 'APIC' - ITUNES_COMPILATION_ID = 'TCMP' - - def __repr__(self): - return "ID3v23Comment(%s)" % (repr(self.frames)) - - @classmethod - def parse(cls, reader): - """given a BitstreamReader, returns a parsed ID3v23Comment""" - - (id3, - major_version, - minor_version, - flags) = reader.parse("3b 8u 8u 8u") - if (id3 != 'ID3'): - raise ValueError("invalid ID3 header") - elif (major_version != 0x03): - raise ValueError("invalid major version") - elif (minor_version != 0x00): - raise ValueError("invalid minor version") - total_size = decode_syncsafe32(reader) - - frames = [] - - while (total_size > 0): - (frame_id, frame_size, frame_flags) = reader.parse("4b 32u 16u") - - if (frame_id == chr(0) * 4): - break - elif (frame_id == 'TXXX'): - frames.append(cls.USER_TEXT_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - elif (frame_id == 'WXXX'): - frames.append(cls.USER_WEB_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - elif (frame_id == 'COMM'): - frames.append(cls.COMMENT_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - elif (frame_id == 'APIC'): - frames.append(cls.IMAGE_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - elif (frame_id.startswith('T')): - frames.append(cls.TEXT_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - elif (frame_id.startswith('W')): - frames.append(cls.WEB_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - else: - frames.append(cls.RAW_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - - total_size -= (10 + frame_size) - - return cls(frames) - - def build(self, writer): - """writes the complete ID3v23Comment data - to the given BitstreamWriter""" - - from operator import add - - writer.build("3b 8u 8u 8u", ("ID3", 0x03, 0x00, 0x00)) - encode_syncsafe32(writer, - reduce(add, - [10 + frame.size() for frame in self], 0)) - - for frame in self: - writer.build("4b 32u 16u", (frame.id, frame.size(), 0)) - frame.build(writer) - - def size(self): - """returns the total size of the ID3v23Comment, including its header""" - - from operator import add - - return reduce(add, [10 + frame.size() for frame in self], 10) - - -############################################################ -# ID3v2.4 Comment -############################################################ - - -class ID3v24_Frame(ID3v23_Frame): - def __repr__(self): - return "ID3v24_Frame(%s, %s)" % (repr(self.id), repr(self.data)) - - -class ID3v24_T___Frame(ID3v23_T___Frame): - def __init__(self, frame_id, encoding, data): - assert((encoding == 0) or (encoding == 1) or - (encoding == 2) or (encoding == 3)) - - self.id = frame_id - self.encoding = encoding - self.data = data - - def __repr__(self): - return "ID3v24_T___Frame(%s, %s, %s)" % \ - (repr(self.id), repr(self.encoding), repr(self.data)) - - def __unicode__(self): - return self.data.decode( - {0: u"latin-1", - 1: u"utf-16", - 2: u"utf-16BE", - 3: u"utf-8"}[self.encoding], 'replace').split(unichr(0), 1)[0] - - def raw_info(self): - """returns a human-readable version of this frame as unicode""" - - return u"%s = (%s) %s" % (self.id.decode('ascii'), - {0: u"Latin-1", - 1: u"UTF-16", - 2: u"UTF-16BE", - 3: u"UTF-8"}[self.encoding], - unicode(self)) - - @classmethod - def converted(cls, frame_id, unicode_string): - """given a unicode string, returns a text frame""" - - if (is_latin_1(unicode_string)): - return cls(frame_id, 0, unicode_string.encode('latin-1')) - else: - return cls(frame_id, 3, unicode_string.encode('utf-8')) - - -class ID3v24_TXXX_Frame(ID3v23_TXXX_Frame): - def __repr__(self): - return "ID3v24_TXXX_Frame(%s, %s, %s)" % \ - (repr(self.encoding), repr(self.description), repr(self.data)) - - def raw_info(self): - """returns a human-readable version of this frame as unicode""" - - return u"%s = (%s, \"%s\") %s" % \ - (self.id, - {0: u"Latin-1", - 1: u"UTF-16", - 2: u"UTF-16BE", - 3: u"UTF-8"}[self.encoding], - self.description, - unicode(self)) - - def __unicode__(self): - return self.data.decode( - {0: u"latin-1", - 1: u"utf-16", - 2: u"utf-16BE", - 3: u"utf-8"}[self.encoding], 'replace').split(unichr(0), 1)[0] - - @classmethod - def parse(cls, frame_id, frame_size, reader): - """given a frame_id string, frame_size int and BitstreamReader - of the remaining frame data, returns a parsed text frame""" - - encoding = reader.read(8) - description = C_string.parse({0: "latin-1", - 1: "utf-16", - 2: "utf-16be", - 3: "utf-8"}[encoding], - reader) - data = reader.read_bytes(frame_size - 1 - description.size()) - - return cls(encoding, description, data) - - -class ID3v24_APIC_Frame(ID3v23_APIC_Frame): - def __repr__(self): - return "ID3v24_APIC_Frame(%s, %s, %s, ...)" % \ - (repr(self.pic_mime_type), repr(self.pic_type), - repr(self.pic_description)) - - def __setattr__(self, attr, value): - if (attr == 'type'): - self.__dict__["pic_type"] = {0: 3, # front cover - 1: 4, # back cover - 2: 5, # leaflet page - 3: 6, # media - }.get(value, 0) # other - elif (attr == 'description'): - if (is_latin_1(value)): - self.__dict__["pic_description"] = C_string('latin-1', value) - else: - self.__dict__["pic_description"] = C_string('utf-8', value) - elif (attr == 'mime_type'): - self.__dict__["pic_mime_type"] = C_string('ascii', value) - else: - self.__dict__[attr] = value - - @classmethod - def parse(cls, frame_id, frame_size, reader): - """parses this frame from the given BitstreamReader""" - - encoding = reader.read(8) - mime_type = C_string.parse('ascii', reader) - picture_type = reader.read(8) - description = C_string.parse({0: 'latin-1', - 1: 'utf-16', - 2: 'utf-16be', - 3: 'utf-8'}[encoding], reader) - data = reader.read_bytes(frame_size - (1 + - mime_type.size() + - 1 + - description.size())) - - return cls(mime_type, picture_type, description, data) - - def build(self, writer): - """writes this frame to the given BitstreamWriter - not including its frame header""" - - writer.write(8, {'latin-1': 0, - 'utf-16': 1, - 'utf-16be': 2, - 'utf-8': 3}[self.pic_description.encoding]) - self.pic_mime_type.build(writer) - writer.write(8, self.pic_type) - self.pic_description.build(writer) - writer.write_bytes(self.data) - - @classmethod - def converted(cls, frame_id, image): - if (is_latin_1(image.description)): - description = C_string('latin-1', image.description) - else: - description = C_string('utf-8', image.description) - - return cls(mime_type=C_string('ascii', image.mime_type), - picture_type={0: 3, # front cover - 1: 4, # back cover - 2: 5, # leaflet page - 3: 6, # media - }.get(image.type, 0), # other - description=description, - data=image.data) - - def clean(self, fixes_performed): - """returns a cleaned ID3v24_APIC_Frame, - or None if the frame should be removed entirely - any fixes are appended to fixes_applied as unicode string""" - - actual_mime_type = Image.new(self.data, u"", 0).mime_type - if (unicode(self.pic_mime_type) != actual_mime_type): - fixes_performed.append(u"fixed embedded image's MIME type") - return ID3v24_APIC_Frame(C_string('ascii', - actual_mime_type.encode('ascii')), - self.pic_type, - self.pic_description, - self.data) - else: - return ID3v24_APIC_Frame(self.pic_mime_type, - self.pic_type, - self.pic_description, - self.data) - - -class ID3v24_W___Frame(ID3v23_W___Frame): - def __repr__(self): - return "ID3v24_W___Frame(%s, %s)" % \ - (repr(self.frame_id), repr(self.data)) - - -class ID3v24_WXXX_Frame(ID3v23_WXXX_Frame): - def __repr__(self): - return "ID3v24_WXXX_Frame(%s, %s, %s)" % \ - (repr(self.encoding), repr(self.description), repr(self.data)) - - def raw_info(self): - """returns a human-readable version of this frame as unicode""" - - return u"%s = (%s, \"%s\") %s" % \ - (self.id, - {0: u'Latin-1', - 1: u'UTF-16', - 2: u'UTF-16BE', - 3: u'UTF-8'}[self.encoding], - self.description, - self.data.decode('ascii', 'replace')) - - @classmethod - def parse(cls, frame_id, frame_size, reader): - """given a frame_id string, frame_size int and BitstreamReader - of the remaining frame data, returns a parsed text frame""" - - encoding = reader.read(8) - description = C_string.parse({0: 'latin-1', - 1: 'utf-16', - 2: 'utf-16be', - 3: 'utf-8'}[encoding], - reader) - data = reader.read_bytes(frame_size - 1 - description.size()) - - return cls(encoding, description, data) - - -class ID3v24_COMM_Frame(ID3v23_COMM_Frame): - def __repr__(self): - return "ID3v24_COMM_Frame(%s, %s, %s, %s)" % \ - (repr(self.encoding), repr(self.language), - repr(self.short_description), repr(self.data)) - - def __unicode__(self): - return self.data.decode({0: 'latin-1', - 1: 'utf-16', - 2: 'utf-16be', - 3: 'utf-8'}[self.encoding], 'replace') - - def raw_info(self): - """returns a human-readable version of this frame as unicode""" - - return u"COMM = (%s, %s, \"%s\") %s" % \ - ({0: u'Latin-1', - 1: u'UTF-16', - 2: u'UTF-16BE', - 3: u'UTF-8'}[self.encoding], - self.language.decode('ascii', 'replace'), - self.short_description, - self.data.decode({0: 'latin-1', - 1: 'utf-16', - 2: 'utf-16be', - 3: 'utf-8'}[self.encoding])) - - @classmethod - def parse(cls, frame_id, frame_size, reader): - """given a frame_id string, frame_size int and BitstreamReader - of the remaining frame data, returns a parsed ID3v22_COM_Frame""" - - (encoding, language) = reader.parse("8u 3b") - short_description = C_string.parse({0: 'latin-1', - 1: 'utf-16', - 2: 'utf-16be', - 3: 'utf-8'}[encoding], - reader) - data = reader.read_bytes(frame_size - (4 + short_description.size())) - - return cls(encoding, language, short_description, data) - - @classmethod - def converted(cls, frame_id, unicode_string): - if (is_latin_1(unicode_string)): - return cls(0, "eng", C_string("latin-1", u""), - unicode_string.encode('latin-1')) - else: - return cls(3, "eng", C_string("utf-8", u""), - unicode_string.encode('utf-8')) - - def clean(self, fixes_performed): - """returns a cleaned frame of the same class - or None if the frame should be omitted - fix text will be appended to fixes_performed, if necessary""" - - field = self.id.decode('ascii') - text_encoding = {0: 'latin-1', - 1: 'utf-16', - 2: 'utf-16be', - 3: 'utf-8'} - - value = self.data.decode(text_encoding[self.encoding], 'replace') - - #check for an empty tag - if (len(value.strip()) == 0): - fixes_performed.append( - u"removed empty field %(field)s" % - {"field": field}) - return None - - #check trailing whitespace - fix1 = value.rstrip() - if (fix1 != value): - fixes_performed.append( - u"removed trailing whitespace from %(field)s" % - {"field": field}) - - #check leading whitespace - fix2 = fix1.lstrip() - if (fix2 != fix1): - fixes_performed.append( - u"removed leading whitespace from %(field)s" % - {"field": field}) - - #stripping whitespace shouldn't alter text/description encoding - - return self.__class__(self.encoding, - self.language, - self.short_description, - fix2.encode(text_encoding[self.encoding])) - - -class ID3v24Comment(ID3v23Comment): - NAME = u'ID3v2.4' - - RAW_FRAME = ID3v24_Frame - TEXT_FRAME = ID3v24_T___Frame - USER_TEXT_FRAME = ID3v24_TXXX_Frame - WEB_FRAME = ID3v24_W___Frame - USER_WEB_FRAME = ID3v24_WXXX_Frame - COMMENT_FRAME = ID3v24_COMM_Frame - IMAGE_FRAME = ID3v24_APIC_Frame - IMAGE_FRAME_ID = 'APIC' - ITUNES_COMPILATION_ID = 'TCMP' - - def __repr__(self): - return "ID3v24Comment(%s)" % (repr(self.frames)) - - @classmethod - def parse(cls, reader): - """given a BitstreamReader, returns a parsed ID3v24Comment""" - - (id3, - major_version, - minor_version, - flags) = reader.parse("3b 8u 8u 8u") - if (id3 != 'ID3'): - raise ValueError("invalid ID3 header") - elif (major_version != 0x04): - raise ValueError("invalid major version") - elif (minor_version != 0x00): - raise ValueError("invalid minor version") - total_size = decode_syncsafe32(reader) - - frames = [] - - while (total_size > 0): - frame_id = reader.read_bytes(4) - frame_size = decode_syncsafe32(reader) - flags = reader.read(16) - - if (frame_id == chr(0) * 4): - break - elif (frame_id == 'TXXX'): - frames.append(cls.USER_TEXT_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - elif (frame_id == 'WXXX'): - frames.append(cls.USER_WEB_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - elif (frame_id == 'COMM'): - frames.append(cls.COMMENT_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - elif (frame_id == 'APIC'): - frames.append(cls.IMAGE_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - elif (frame_id.startswith('T')): - frames.append(cls.TEXT_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - elif (frame_id.startswith('W')): - frames.append(cls.WEB_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - else: - frames.append(cls.RAW_FRAME.parse( - frame_id, frame_size, reader.substream(frame_size))) - - total_size -= (10 + frame_size) - - return cls(frames) - - def build(self, writer): - """writes the complete ID3v24Comment data - to the given BitstreamWriter""" - - from operator import add - - writer.build("3b 8u 8u 8u", ("ID3", 0x04, 0x00, 0x00)) - encode_syncsafe32(writer, - reduce(add, [10 + frame.size() for frame in self], - 0)) - - for frame in self: - writer.write_bytes(frame.id) - encode_syncsafe32(writer, frame.size()) - writer.write(16, 0) - frame.build(writer) - - def size(self): - """returns the total size of the ID3v24Comment, including its header""" - - from operator import add - - return reduce(add, [10 + frame.size() for frame in self], 10) - - -ID3v2Comment = ID3v22Comment - -from __id3v1__ import * - - -class ID3CommentPair(MetaData): - """a pair of ID3v2/ID3v1 comments - - these can be manipulated as a set""" - - def __init__(self, id3v2_comment, id3v1_comment): - """id3v2 and id3v1 are ID3v2Comment and ID3v1Comment objects or None - - values in ID3v2 take precendence over ID3v1, if present""" - - self.__dict__['id3v2'] = id3v2_comment - self.__dict__['id3v1'] = id3v1_comment - - if (self.id3v2 is not None): - base_comment = self.id3v2 - elif (self.id3v1 is not None): - base_comment = self.id3v1 - else: - raise ValueError("ID3v2 and ID3v1 cannot both be blank") - - def __getattr__(self, key): - if (key in self.INTEGER_FIELDS): - if ((self.id3v2 is not None) and - (getattr(self.id3v2, key) != 0)): - return getattr(self.id3v2, key) - if (self.id3v1 is not None): - return getattr(self.id3v1, key) - else: - raise ValueError("ID3v2 and ID3v1 cannot both be blank") - elif (key in self.FIELDS): - if ((self.id3v2 is not None) and - (getattr(self.id3v2, key) != u'')): - return getattr(self.id3v2, key) - if (self.id3v1 is not None): - return getattr(self.id3v1, key) - else: - raise ValueError("ID3v2 and ID3v1 cannot both be blank") - else: - raise AttributeError(key) - - def __setattr__(self, key, value): - self.__dict__[key] = value - - if (self.id3v2 is not None): - setattr(self.id3v2, key, value) - if (self.id3v1 is not None): - setattr(self.id3v1, key, value) - - def __delattr__(self, key): - if (self.id3v2 is not None): - delattr(self.id3v2, key) - if (self.id3v1 is not None): - delattr(self.id3v1, key) - - @classmethod - def converted(cls, metadata, - id3v2_class=ID3v23Comment, - id3v1_class=ID3v1Comment): - """takes a MetaData object and returns an ID3CommentPair object""" - - if (metadata is None): - return None - elif (isinstance(metadata, ID3CommentPair)): - return ID3CommentPair( - metadata.id3v2.__class__.converted(metadata.id3v2), - metadata.id3v1.__class__.converted(metadata.id3v1)) - elif (isinstance(metadata, ID3v2Comment)): - return ID3CommentPair(metadata, - id3v1_class.converted(metadata)) - else: - return ID3CommentPair( - id3v2_class.converted(metadata), - id3v1_class.converted(metadata)) - - def raw_info(self): - """returns a human-readable version of this metadata pair - as a unicode string""" - - if ((self.id3v2 is not None) and (self.id3v1 is not None)): - #both comments present - return (self.id3v2.raw_info() + - os.linesep.decode('ascii') * 2 + - self.id3v1.raw_info()) - elif (self.id3v2 is not None): - #only ID3v2 - return self.id3v2.raw_info() - elif (self.id3v1 is not None): - #only ID3v1 - return self.id3v1.raw_info() - else: - return u'' - - #ImageMetaData passthroughs - def images(self): - """returns a list of embedded Image objects""" - - if (self.id3v2 is not None): - return self.id3v2.images() - else: - return [] - - def add_image(self, image): - """embeds an Image object in this metadata""" - - if (self.id3v2 is not None): - self.id3v2.add_image(image) - - def delete_image(self, image): - """deletes an Image object from this metadata""" - - if (self.id3v2 is not None): - self.id3v2.delete_image(image) - - @classmethod - def supports_images(cls): - """returns True""" - - return True - - def clean(self, fixes_performed): - if (self.id3v2 is not None): - new_id3v2 = self.id3v2.clean(fixes_performed) - else: - new_id3v2 = None - - if (self.id3v1 is not None): - new_id3v1 = self.id3v1.clean(fixes_performed) - else: - new_id3v1 = None - - return ID3CommentPair(new_id3v2, new_id3v1)
View file
audiotools-2.18.tar.gz/audiotools/__id3v1__.py
Deleted
@@ -1,172 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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 MetaData - - -class ID3v1Comment(MetaData): - """a complete ID3v1 tag""" - - def __init__(self, track_name, artist_name, album_name, - year, comment, track_number, genre): - """all fields except track_number are binary strings""" - - #pre-emptively cut down overlong fields - MetaData.__init__(self, - track_name=track_name[0:30], - artist_name=artist_name[0:30], - album_name=album_name[0:30], - year=year[0:4], - comment=comment[0:28], - track_number=track_number) - self.__dict__['genre'] = genre - - def raw_info(self): - """returns a human-readable version of this metadata - as a unicode string""" - - from os import linesep - - return linesep.decode('ascii').join( - [u"ID3v1:", - u" track name = %s" % (self.track_name), - u" artist name = %s" % (self.artist_name), - u" album name = %s" % (self.album_name), - u" year = %s" % (self.year), - u" comment = %s" % (self.comment), - u"track number = %d" % (self.track_number), - u" genre = %d" % (self.genre)]) - - @classmethod - def parse(cls, mp3_file): - """given an MP3 file, returns an ID3v1Comment - - raises ValueError if the comment is invalid""" - - from .bitstream import BitstreamReader - - mp3_file.seek(-128, 2) - reader = BitstreamReader(mp3_file, 0) - (tag, - track_name, - artist_name, - album_name, - year, - comment) = reader.parse("3b 30b 30b 30b 4b 28b") - if (tag != 'TAG'): - raise ValueError(u"invalid ID3v1 tag") - separator = reader.read(8) - if (separator == 0): - track_number = reader.read(8) - else: - track_number = 0 - comment = chr(separator) + reader.read_bytes(1) - genre = reader.read(8) - - return cls( - track_name=track_name.rstrip(chr(0)).decode('ascii', 'replace'), - artist_name=artist_name.rstrip(chr(0)).decode('ascii', 'replace'), - album_name=album_name.rstrip(chr(0)).decode('ascii', 'replace'), - year=year.rstrip(chr(0)).decode('ascii', 'replace'), - comment=comment.rstrip(chr(0)).decode('ascii', 'replace'), - track_number=track_number, - genre=genre) - - def build(self, mp3_file): - """given an MP3 file positioned at the file's end, generate a tag""" - - def __s_pad__(s, length): - if (len(s) < length): - return s + chr(0) * (length - len(s)) - else: - s = s[0:length].rstrip() - return s + chr(0) * (length - len(s)) - - from .bitstream import BitstreamWriter - - BitstreamWriter(mp3_file, 0).build( - "3b 30b 30b 30b 4b 28b 8p 8u 8u", - ("TAG", - __s_pad__(self.track_name.encode('ascii', 'replace'), 30), - __s_pad__(self.artist_name.encode('ascii', 'replace'), 30), - __s_pad__(self.album_name.encode('ascii', 'replace'), 30), - __s_pad__(self.year.encode('ascii', 'replace'), 4), - __s_pad__(self.comment.encode('ascii', 'replace'), 28), - self.track_number, - self.genre)) - - @classmethod - def supports_images(cls): - """returns False""" - - return False - - @classmethod - def converted(cls, metadata): - """converts a MetaData object to an ID3v1Comment object""" - - if (metadata is None): - return None - elif (isinstance(metadata, ID3v1Comment)): - return ID3v1Comment(track_name=metadata.track_name, - artist_name=metadata.artist_name, - album_name=metadata.album_name, - year=metadata.year, - comment=metadata.comment, - track_number=metadata.track_number, - genre=metadata.genre) - else: - return ID3v1Comment(track_name=metadata.track_name, - artist_name=metadata.artist_name, - album_name=metadata.album_name, - year=metadata.year, - comment=metadata.comment, - track_number=metadata.track_number, - genre=0) - - def images(self): - """returns an empty list of Image objects""" - - return [] - - def clean(self, fixes_performed): - """returns a new ID3v1Comment object that's been cleaned of problems""" - - fields = {} - for (attr, name) in [("track_name", u"title"), - ("artist_name", u"artist"), - ("album_name", u"album"), - ("year", u"year"), - ("comment", u"comment")]: - fix1 = getattr(self, attr).rstrip() - if (fix1 != getattr(self, attr)): - fixes_performed.append( - u"removed trailing whitespace from %(field)s" % - {"field": name}) - fix2 = fix1.lstrip() - if (fix2 != fix1): - fixes_performed.append( - u"removed leading whitespace from %(field)s" % - {"field": name}) - fields[attr] = fix2 - - for attr in ["track_number", "genre"]: - fields[attr] = getattr(self, attr) - - return ID3v1Comment(**fields)
View file
audiotools-2.18.tar.gz/audiotools/__image__.py
Deleted
@@ -1,444 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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 - -import imghdr -import cStringIO -import gettext -from .bitstream import BitstreamReader, format_size - -gettext.install("audiotools", unicode=True) - - -def __jpeg__(h, f): - if (h[0:3] == "FFD8FF".decode('hex')): - return 'jpeg' - else: - return None - - -imghdr.tests.append(__jpeg__) - - -def image_metrics(file_data): - """returns an ImageMetrics subclass from a string of file data - - raises InvalidImage if there is an error parsing the file - or its type is unknown""" - - header = imghdr.what(None, file_data) - - file = cStringIO.StringIO(file_data) - try: - if (header == 'jpeg'): - return __JPEG__.parse(file) - elif (header == 'png'): - return __PNG__.parse(file) - elif (header == 'gif'): - return __GIF__.parse(file) - elif (header == 'bmp'): - return __BMP__.parse(file) - elif (header == 'tiff'): - return __TIFF__.parse(file) - else: - raise InvalidImage(_(u'Unknown image type')) - finally: - file.close() - - -####################### -#JPEG -####################### - - -class ImageMetrics: - """a container for image data""" - - def __init__(self, width, height, bits_per_pixel, color_count, mime_type): - """fields are as follows: - - width - image width as an integer number of pixels - height - image height as an integer number of pixels - bits_per_pixel - the number of bits per pixel as an integer - color_count - for palette-based images, the total number of colors - mime_type - the image's MIME type, as a string - - all of the ImageMetrics subclasses implement these fields - in addition, they all implement a parse() classmethod - used to parse binary string data and return something - imageMetrics compatible - """ - - self.width = width - self.height = height - self.bits_per_pixel = bits_per_pixel - self.color_count = color_count - self.mime_type = mime_type - - def __repr__(self): - return "ImageMetrics(%s,%s,%s,%s,%s)" % \ - (repr(self.width), - repr(self.height), - repr(self.bits_per_pixel), - repr(self.color_count), - repr(self.mime_type)) - - -class InvalidImage(Exception): - """raised if an image cannot be parsed correctly""" - - def __init__(self, err): - self.err = unicode(err) - - def __unicode__(self): - return self.err - - -class InvalidJPEG(InvalidImage): - """raised if a JPEG cannot be parsed correctly""" - - pass - - -class __JPEG__(ImageMetrics): - def __init__(self, width, height, bits_per_pixel): - ImageMetrics.__init__(self, width, height, bits_per_pixel, - 0, u'image/jpeg') - - @classmethod - def parse(cls, file): - def segments(reader): - if (reader.read(8) != 0xFF): - raise InvalidJPEG(_(u"Invalid JPEG segment marker")) - segment_type = reader.read(8) - - while (segment_type != 0xDA): - if (segment_type not in (0xD8, 0xD9)): - yield (segment_type, reader.substream(reader.read(16) - 2)) - else: - yield (segment_type, None) - - if (reader.read(8) != 0xFF): - raise InvalidJPEG(_(u"Invalid JPEG segment marker")) - segment_type = reader.read(8) - - for (segment_type, segment_data) in segments(BitstreamReader(file, 0)): - if (segment_type in (0xC0, 0xC1, 0xC2, 0xC3, - 0xC5, 0XC5, 0xC6, 0xC7, - 0xC9, 0xCA, 0xCB, 0xCD, - 0xCE, 0xCF)): # start of frame - (data_precision, - image_height, - image_width, - components) = segment_data.parse("8u 16u 16u 8u") - return __JPEG__(width=image_width, - height=image_height, - bits_per_pixel=data_precision * components) - - -####################### -#PNG -####################### - - -class InvalidPNG(InvalidImage): - """raised if a PNG cannot be parsed correctly""" - - pass - - -class __PNG__(ImageMetrics): - def __init__(self, width, height, bits_per_pixel, color_count): - ImageMetrics.__init__(self, width, height, bits_per_pixel, color_count, - u'image/png') - - @classmethod - def parse(cls, file): - def chunks(reader): - if (reader.read_bytes(8) != '\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'): - raise InvalidPNG(_(u'Invalid PNG')) - (chunk_length, chunk_type) = reader.parse("32u 4b") - while (chunk_type != 'IEND'): - yield (chunk_type, - chunk_length, - reader.substream(chunk_length)) - chunk_crc = reader.read(32) - (chunk_length, chunk_type) = reader.parse("32u 4b") - - ihdr = None - plte_length = 0 - - for (chunk_type, - chunk_length, - chunk_data) in chunks(BitstreamReader(file, 0)): - if (chunk_type == 'IHDR'): - ihdr = chunk_data - elif (chunk_type == 'PLTE'): - plte_length = chunk_length - - if (ihdr is None): - raise InvalidPNG(_(u"Invalid PNG")) - - (width, - height, - bit_depth, - color_type, - compression_method, - filter_method, - interlace_method) = ihdr.parse("32u 32u 8u 8u 8u 8u 8u") - - if (color_type == 0): # grayscale - return cls(width=width, - height=height, - bits_per_pixel=bit_depth, - color_count=0) - elif (color_type == 2): # RGB - return cls(width=width, - height=height, - bits_per_pixel=bit_depth * 3, - color_count=0) - elif (color_type == 3): # palette - if ((plte_length % 3) != 0): - raise InvalidPNG(_(u'Invalid PLTE chunk length')) - else: - return cls(width=width, - height=height, - bits_per_pixel=8, - color_count=plte_length / 3) - elif (color_type == 4): # grayscale + alpha - return cls(width=width, - height=height, - bits_per_pixel=bit_depth * 2, - color_count=0) - elif (color_type == 6): # RGB + alpha - return cls(width=width, - height=height, - bits_per_pixel=bit_depth * 4, - color_count=0) - -####################### -#BMP -####################### - - -class InvalidBMP(InvalidImage): - """raised if a BMP cannot be parsed correctly""" - - pass - - -class __BMP__(ImageMetrics): - def __init__(self, width, height, bits_per_pixel, color_count): - ImageMetrics.__init__(self, width, height, bits_per_pixel, color_count, - u'image/x-ms-bmp') - - @classmethod - def parse(cls, file): - (magic_number, - file_size, - data_offset, - header_size, - width, - height, - color_planes, - bits_per_pixel, - compression_method, - image_size, - horizontal_resolution, - vertical_resolution, - colors_used, - important_colors_used) = BitstreamReader(file, 1).parse( - "2b 32u 16p 16p 32u " + - "32u 32u 32u 16u 16u 32u 32u 32u 32u 32u 32u") - if (magic_number != 'BM'): - raise InvalidBMP(_(u'Invalid BMP')) - else: - return cls(width=width, - height=height, - bits_per_pixel=bits_per_pixel, - color_count=colors_used) - - -####################### -#GIF -####################### - - -class InvalidGIF(InvalidImage): - """raised if a GIF cannot be parsed correctly""" - - pass - - -class __GIF__(ImageMetrics): - def __init__(self, width, height, color_count): - ImageMetrics.__init__(self, width, height, 8, color_count, - u'image/gif') - - @classmethod - def parse(cls, file): - (gif, - version, - width, - height, - color_table_size) = BitstreamReader(file, 1).parse( - "3b 3b 16u 16u 3u 5p") - if (gif != 'GIF'): - raise InvalidGIF(u'Invalid GIF') - else: - return cls(width=width, - height=height, - color_count=2 ** (color_table_size + 1)) - - -####################### -#TIFF -####################### - - -class InvalidTIFF(InvalidImage): - """raised if a TIFF cannot be parsed correctly""" - - pass - - -class __TIFF__(ImageMetrics): - def __init__(self, width, height, bits_per_pixel, color_count): - ImageMetrics.__init__(self, width, height, - bits_per_pixel, color_count, - u'image/tiff') - - @classmethod - def parse(cls, file): - def tags(file, order): - while (True): - reader = BitstreamReader(file, order) - #read all the tags in an IFD - tag_count = reader.read(16) - sub_reader = reader.substream(tag_count * 12) - next_ifd = reader.read(32) - - for i in xrange(tag_count): - (tag_code, - tag_datatype, - tag_value_count) = sub_reader.parse("16u 16u 32u") - if (tag_datatype == 1): # BYTE type - tag_struct = "8u" * tag_value_count - elif (tag_datatype == 3): # SHORT type - tag_struct = "16u" * tag_value_count - elif (tag_datatype == 4): # LONG type - tag_struct = "32u" * tag_value_count - else: # all other types - tag_struct = "4b" - if (format_size(tag_struct) <= 32): - yield (tag_code, sub_reader.parse(tag_struct)) - sub_reader.skip(32 - format_size(tag_struct)) - else: - offset = sub_reader.read(32) - file.seek(offset, 0) - yield (tag_code, - BitstreamReader(file, order).parse(tag_struct)) - - if (next_ifd != 0): - file.seek(next_ifd, 0) - else: - break - - byte_order = file.read(2) - if (byte_order == 'II'): - order = 1 - elif (byte_order == 'MM'): - order = 0 - else: - raise InvalidTIFF(_(u"Invalid TIFF")) - reader = BitstreamReader(file, order) - if (reader.read(16) != 42): - raise InvalidTIFF(_(u"Invalid TIFF")) - - initial_ifd = reader.read(32) - file.seek(initial_ifd, 0) - - width = 0 - height = 0 - bits_per_pixel = 0 - color_count = 0 - for (tag_id, tag_values) in tags(file, order): - if (tag_id == 0x0100): - width = tag_values[0] - elif (tag_id == 0x0101): - height = tag_values[0] - elif (tag_id == 0x0102): - bits_per_pixel = sum(tag_values) - elif (tag_id == 0x0140): - color_count = len(tag_values) / 3 - - return cls(width=width, - height=height, - bits_per_pixel=bits_per_pixel, - color_count=color_count) - - -def can_thumbnail(): - """returns True if we have the capability to thumbnail images""" - - try: - import Image as PIL_Image - return True - except ImportError: - return False - - -def thumbnail_formats(): - """returns a list of available thumbnail image formats""" - - import Image as PIL_Image - import cStringIO - - #performing a dummy save seeds PIL_Image.SAVE with possible save types - PIL_Image.new("RGB", (1, 1)).save(cStringIO.StringIO(), "bmp") - - return PIL_Image.SAVE.keys() - - -def thumbnail_image(image_data, width, height, format): - """generates a new, smaller image from a larger one - - image_data is a binary string - width and height are the requested maximum values - format as a binary string, such as 'JPEG' - """ - - import cStringIO - import Image as PIL_Image - import ImageFile as PIL_ImageFile - - PIL_ImageFile.MAXBLOCK = 0x100000 - - img = PIL_Image.open(cStringIO.StringIO(image_data)).convert('RGB') - img.thumbnail((width, height), PIL_Image.ANTIALIAS) - output = cStringIO.StringIO() - - if (format.upper() == 'JPEG'): - #PIL's default JPEG save quality isn't too great - #so it's best to add a couple of optimizing parameters - #since this is a common case - img.save(output, 'JPEG', quality=90, optimize=True) - else: - img.save(output, format) - - return output.getvalue()
View file
audiotools-2.18.tar.gz/audiotools/__m4a__.py
Deleted
@@ -1,1328 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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, PCMConverter, - transfer_data, transfer_framelist_data, - subprocess, BIN, cStringIO, MetaData, os, - Image, image_metrics, InvalidImage, - ignore_sigint, InvalidFormat, - open, open_files, EncodingError, DecodingError, - WaveAudio, WaveReader, - ChannelMask, UnsupportedBitsPerSample, - UnsupportedChannelMask, - UnsupportedChannelCount, - BufferedPCMReader, to_pcm_progress, - at_a_time, VERSION, PCMReaderError, - __default_quality__, iter_last) -from __m4a_atoms__ import * -import gettext - -gettext.install("audiotools", unicode=True) - - -####################### -#M4A File -####################### - - -class InvalidM4A(InvalidFile): - pass - - -def get_m4a_atom(reader, *atoms): - """given a BitstreamReader and atom name strings - returns a (size, substream) of the final atom data - (not including its 64-bit size/name header) - after traversing the parent atoms - """ - - for (last, next_atom) in iter_last(iter(atoms)): - try: - (length, stream_atom) = reader.parse("32u 4b") - while (stream_atom != next_atom): - reader.skip_bytes(length - 8) - (length, stream_atom) = reader.parse("32u 4b") - if (last): - return (length - 8, reader.substream(length - 8)) - else: - reader = reader.substream(length - 8) - except IOError: - raise KeyError(next_atom) - - -def get_m4a_atom_offset(reader, *atoms): - """given a BitstreamReader and atom name strings - returns a (size, offset) of the final atom data - (including its 64-bit size/name header) - after traversing the parent atoms""" - - offset = 0 - - for (last, next_atom) in iter_last(iter(atoms)): - try: - (length, stream_atom) = reader.parse("32u 4b") - offset += 8 - while (stream_atom != next_atom): - reader.skip_bytes(length - 8) - offset += (length - 8) - (length, stream_atom) = reader.parse("32u 4b") - offset += 8 - if (last): - return (length, offset - 8) - else: - reader = reader.substream(length - 8) - except IOError: - raise KeyError(next_atom) - - -class M4ATaggedAudio: - def __init__(self, filename): - self.filename = filename - - def get_metadata(self): - """returns a MetaData object, or None - - raises IOError if unable to read the file""" - - from .bitstream import BitstreamReader - - reader = BitstreamReader(file(self.filename, 'rb'), 0) - try: - try: - (meta_size, - meta_reader) = get_m4a_atom(reader, "moov", "udta", "meta") - except KeyError: - return None - - return M4A_META_Atom.parse("meta", meta_size, meta_reader, - {"hdlr": M4A_HDLR_Atom, - "ilst": M4A_Tree_Atom, - "free": M4A_FREE_Atom, - "\xa9alb": M4A_ILST_Leaf_Atom, - "\xa9ART": M4A_ILST_Leaf_Atom, - 'aART': M4A_ILST_Leaf_Atom, - "\xa9cmt": M4A_ILST_Leaf_Atom, - "covr": M4A_ILST_Leaf_Atom, - "cpil": M4A_ILST_Leaf_Atom, - "cprt": M4A_ILST_Leaf_Atom, - "\xa9day": M4A_ILST_Leaf_Atom, - "disk": M4A_ILST_Leaf_Atom, - "gnre": M4A_ILST_Leaf_Atom, - "----": M4A_ILST_Leaf_Atom, - "pgap": M4A_ILST_Leaf_Atom, - "rtng": M4A_ILST_Leaf_Atom, - "tmpo": M4A_ILST_Leaf_Atom, - "\xa9grp": M4A_ILST_Leaf_Atom, - "\xa9nam": M4A_ILST_Leaf_Atom, - "\xa9too": M4A_ILST_Leaf_Atom, - "trkn": M4A_ILST_Leaf_Atom, - "\xa9wrt": M4A_ILST_Leaf_Atom}) - finally: - reader.close() - - def update_metadata(self, metadata, old_metadata=None): - """takes this track's updated MetaData object - as returned by get_metadata() and sets this track's metadata - with any fields updated in that object - - old_metadata is the unmodifed metadata returned by get_metadata() - - raises IOError if unable to write the file - """ - - from .bitstream import BitstreamWriter - from .bitstream import BitstreamReader - - if (metadata is None): - return - - if (not isinstance(metadata, M4A_META_Atom)): - raise ValueError(_(u"metadata not from audio file")) - - if (old_metadata is None): - #get_metadata() result may still be None, and that's okay - old_metadata = self.get_metadata() - - #M4A streams often have *two* "free" atoms we can attempt to resize - - #first, attempt to resize the one inside the "meta" atom - if ((old_metadata is not None) and - metadata.has_child("free") and - ((metadata.size() - metadata["free"].size()) <= - old_metadata.size())): - - metadata.replace_child( - M4A_FREE_Atom(old_metadata.size() - - (metadata.size() - - metadata["free"].size()))) - - f = file(self.filename, 'r+b') - (meta_size, meta_offset) = get_m4a_atom_offset( - BitstreamReader(f, 0), "moov", "udta", "meta") - f.seek(meta_offset + 8, 0) - metadata.build(BitstreamWriter(f, 0)) - f.close() - return - else: - #if there's insufficient room, - #attempt to resize the outermost "free" also - - #this is only possible if the file is laid out correctly, - #with "free" coming after "moov" but before "mdat" - #FIXME - - #if neither fix is possible, the whole file must be rewritten - #which also requires adjusting the "stco" atom offsets - m4a_tree = M4A_Tree_Atom.parse( - None, - os.path.getsize(self.filename), - BitstreamReader(file(self.filename, "rb"), 0), - {"moov": M4A_Tree_Atom, - "trak": M4A_Tree_Atom, - "mdia": M4A_Tree_Atom, - "minf": M4A_Tree_Atom, - "stbl": M4A_Tree_Atom, - "stco": M4A_STCO_Atom, - "udta": M4A_Tree_Atom}) - - #find initial mdat offset - initial_mdat_offset = m4a_tree.child_offset("mdat") - - #adjust moov -> udta -> meta atom - #(generating sub-atoms as necessary) - if (not m4a_tree.has_child("moov")): - return - else: - moov = m4a_tree["moov"] - if (not moov.has_child("udta")): - moov.append_child(M4A_Tree_Atom("udta", [])) - udta = moov["udta"] - if (not udta.has_child("meta")): - udta.append_child(metadata) - else: - udta.replace_child(metadata) - - #find new mdat offset - new_mdat_offset = m4a_tree.child_offset("mdat") - - #adjust moov -> trak -> mdia -> minf -> stbl -> stco offsets - #based on the difference between the new mdat position and the old - try: - delta_offset = new_mdat_offset - initial_mdat_offset - stco = m4a_tree["moov"]["trak"]["mdia"]["minf"]["stbl"]["stco"] - stco.offsets = [offset + delta_offset for offset in - stco.offsets] - except KeyError: - #if there is no stco atom, don't worry about it - pass - - #then write entire tree back to disk - writer = BitstreamWriter(file(self.filename, "wb"), 0) - m4a_tree.build(writer) - - def set_metadata(self, metadata): - """takes a MetaData object and sets this track's metadata - - this metadata includes track name, album name, and so on - raises IOError if unable to write the file""" - - if (metadata is None): - return - - old_metadata = self.get_metadata() - metadata = M4A_META_Atom.converted(metadata) - - #replace file-specific atoms in new metadata - #with ones from old metadata (if any) - #which can happen if we're shifting metadata - #from one M4A file to another - file_specific_atoms = frozenset(['\xa9too', '----', 'pgap', 'tmpo']) - - if (metadata.has_ilst_atom()): - metadata.ilst_atom().leaf_atoms = filter( - lambda atom: atom.name not in file_specific_atoms, - metadata.ilst_atom()) - - if (old_metadata.has_ilst_atom()): - metadata.ilst_atom().leaf_atoms.extend( - filter(lambda atom: atom.name in file_specific_atoms, - old_metadata.ilst_atom())) - - self.update_metadata(metadata, old_metadata) - - def delete_metadata(self): - """deletes the track's MetaData - - this removes or unsets tags as necessary in order to remove all data - raises IOError if unable to write the file""" - - self.set_metadata(MetaData()) - - -class M4AAudio_faac(M4ATaggedAudio, AudioFile): - """an M4A audio file using faac/faad binaries for I/O""" - - SUFFIX = "m4a" - NAME = SUFFIX - DEFAULT_COMPRESSION = "100" - COMPRESSION_MODES = tuple(["10"] + map(str, range(50, 500, 25)) + ["500"]) - BINARIES = ("faac", "faad") - - def __init__(self, filename): - """filename is a plain string""" - - from .bitstream import BitstreamReader - - self.filename = filename - - #first, fetch the mdia atom - #which is the parent of both the mp4a and mdhd atoms - try: - mdia = get_m4a_atom(BitstreamReader(file(filename, 'rb'), 0), - "moov", "trak", "mdia")[1] - except IOError: - raise InvalidM4A(_(u"I/O error opening M4A file")) - except KeyError: - raise InvalidM4A(_(u"Required mdia atom not found")) - mdia.mark() - try: - try: - stsd = get_m4a_atom(mdia, "minf", "stbl", "stsd")[1] - except KeyError: - raise InvalidM4A(_(u"Required stsd atom not found")) - - #then, fetch the mp4a atom for bps, channels and sample rate - try: - (stsd_version, descriptions) = stsd.parse("8u 24p 32u") - (mp4a, - self.__channels__, - self.__bits_per_sample__) = stsd.parse( - "32p 4b 48p 16p 16p 16p 4P 16u 16u 16p 16p 32p") - except IOError: - raise InvalidM4A(_(u"Invalid mp4a atom")) - - #finally, fetch the mdhd atom for total track length - mdia.rewind() - try: - mdhd = get_m4a_atom(mdia, "mdhd")[1] - except KeyError: - raise InvalidM4A(_(u"Required mdhd atom not found")) - try: - (version, ) = mdhd.parse("8u 24p") - if (version == 0): - (self.__sample_rate__, - self.__length__,) = mdhd.parse("32p 32p 32u 32u 2P 16p") - elif (version == 1): - (self.__sample_rate__, - self.__length__,) = mdhd.parse("64p 64p 32u 64U 2P 16p") - else: - raise InvalidM4A(_(u"Unsupported mdhd version")) - except IOError: - raise InvalidM4A(_(u"Invalid mdhd atom")) - finally: - mdia.unmark() - - def channel_mask(self): - """returns a ChannelMask object of this track's channel layout""" - - #M4A seems to use the same channel assignment - #as old-style RIFF WAVE/FLAC - if (self.channels() == 1): - return ChannelMask.from_fields( - front_center=True) - elif (self.channels() == 2): - return ChannelMask.from_fields( - front_left=True, front_right=True) - elif (self.channels() == 3): - return ChannelMask.from_fields( - front_left=True, front_right=True, front_center=True) - elif (self.channels() == 4): - return ChannelMask.from_fields( - front_left=True, front_right=True, - back_left=True, back_right=True) - elif (self.channels() == 5): - return ChannelMask.from_fields( - front_left=True, front_right=True, front_center=True, - back_left=True, back_right=True) - elif (self.channels() == 6): - return ChannelMask.from_fields( - front_left=True, front_right=True, front_center=True, - back_left=True, back_right=True, - low_frequency=True) - else: - return ChannelMask(0) - - @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""" - - from .bitstream import BitstreamReader - - reader = BitstreamReader(file, 0) - reader.mark() - try: - (ftyp, major_brand) = reader.parse("32p 4b 4b") - except IOError: - reader.unmark() - return False - - if ((ftyp == 'ftyp') and - (major_brand in ('mp41', 'mp42', 'M4A ', 'M4B '))): - reader.rewind() - reader.unmark() - try: - stsd = get_m4a_atom(reader, "moov", "trak", "mdia", - "minf", "stbl", "stsd")[1] - try: - (stsd_version, descriptions) = stsd.parse("8u 24p 32u") - (mp4a_size, mp4a_type) = stsd.parse("32u 4b") - return (mp4a_type == 'mp4a') - except IOError: - return False - except KeyError: - return False - else: - reader.unmark() - return False - - def lossless(self): - """returns False""" - - return False - - 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.__bits_per_sample__ - - def sample_rate(self): - """returns the rate of the track's audio as an integer number of Hz""" - - return self.__sample_rate__ - - def cd_frames(self): - """returns the total length of the track in CD frames - - each CD frame is 1/75th of a second""" - - return (self.__length__ - 1024) / self.__sample_rate__ * 75 - - def total_frames(self): - """returns the total PCM frames of the track as an integer""" - - return self.__length__ - 1024 - - def to_pcm(self): - """returns a PCMReader object containing the track's PCM data""" - - devnull = file(os.devnull, "ab") - - sub = subprocess.Popen([BIN['faad'], "-f", str(2), "-w", - self.filename], - stdout=subprocess.PIPE, - stderr=devnull) - return PCMReader( - sub.stdout, - sample_rate=self.sample_rate(), - channels=self.channels(), - channel_mask=int(self.channel_mask()), - bits_per_sample=self.bits_per_sample(), - process=sub) - - @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 M4AAudio object""" - - if ((compression is None) or - (compression not in cls.COMPRESSION_MODES)): - compression = __default_quality__(cls.NAME) - - if (pcmreader.channels > 2): - pcmreader = PCMConverter(pcmreader, - sample_rate=pcmreader.sample_rate, - channels=2, - channel_mask=ChannelMask.from_channels(2), - bits_per_sample=pcmreader.bits_per_sample) - - #faac requires files to end with .m4a for some reason - if (not filename.endswith(".m4a")): - import tempfile - actual_filename = filename - tempfile = tempfile.NamedTemporaryFile(suffix=".m4a") - filename = tempfile.name - else: - actual_filename = tempfile = None - - devnull = file(os.devnull, "ab") - - sub = subprocess.Popen([BIN['faac'], - "-q", compression, - "-P", - "-R", str(pcmreader.sample_rate), - "-B", str(pcmreader.bits_per_sample), - "-C", str(pcmreader.channels), - "-X", - "-o", filename, - "-"], - stdin=subprocess.PIPE, - stderr=devnull, - stdout=devnull, - preexec_fn=ignore_sigint) - #Note: faac handles SIGINT on its own, - #so trying to ignore it doesn't work like on most other encoders. - - try: - transfer_framelist_data(pcmreader, sub.stdin.write) - except (ValueError, IOError), err: - sub.stdin.close() - sub.wait() - cls.__unlink__(filename) - raise EncodingError(str(err)) - except Exception, err: - sub.stdin.close() - sub.wait() - cls.__unlink__(filename) - raise err - - try: - pcmreader.close() - except DecodingError, err: - raise EncodingError(err.error_message) - sub.stdin.close() - - if (sub.wait() == 0): - if (tempfile is not None): - filename = actual_filename - f = file(filename, 'wb') - tempfile.seek(0, 0) - transfer_data(tempfile.read, f.write) - f.close() - tempfile.close() - - return M4AAudio(filename) - else: - if (tempfile is not None): - tempfile.close() - raise EncodingError(u"unable to write file with faac") - - @classmethod - def can_add_replay_gain(cls): - """returns False""" - - return False - - @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 - """ - - track_names = [track.filename for track in - open_files(filenames) if - isinstance(track, cls)] - - if (progress is not None): - progress(0, 1) - - #helpfully, aacgain is flag-for-flag compatible with mp3gain - if ((len(track_names) > 0) and (BIN.can_execute(BIN['aacgain']))): - devnull = file(os.devnull, 'ab') - sub = subprocess.Popen([BIN['aacgain'], '-k', '-q', '-r'] + \ - track_names, - stdout=devnull, - stderr=devnull) - sub.wait() - - devnull.close() - - if (progress is not None): - progress(1, 1) - - -class M4AAudio_nero(M4AAudio_faac): - """an M4A audio file using neroAacEnc/neroAacDec binaries for I/O""" - - DEFAULT_COMPRESSION = "0.5" - COMPRESSION_MODES = ("0.4", "0.5", - "0.6", "0.7", "0.8", "0.9", "1.0") - COMPRESSION_DESCRIPTIONS = {"0.4": _(u"lowest quality, " + - u"corresponds to neroAacEnc -q 0.4"), - "1.0": _(u"highest quality, " + - u"corresponds to neroAacEnc -q 1")} - BINARIES = ("neroAacDec", "neroAacEnc") - - @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 M4AAudio object""" - - if ((compression is None) or - (compression not in cls.COMPRESSION_MODES)): - compression = __default_quality__(cls.NAME) - - import tempfile - tempwavefile = tempfile.NamedTemporaryFile(suffix=".wav") - try: - if (pcmreader.sample_rate > 96000): - tempwave = WaveAudio.from_pcm( - tempwavefile.name, - PCMConverter(pcmreader, - sample_rate=96000, - channels=pcmreader.channels, - channel_mask=pcmreader.channel_mask, - bits_per_sample=pcmreader.bits_per_sample)) - else: - tempwave = WaveAudio.from_pcm( - tempwavefile.name, - pcmreader) - - cls.__from_wave__(filename, tempwave.filename, compression) - return cls(filename) - finally: - if (os.path.isfile(tempwavefile.name)): - tempwavefile.close() - else: - tempwavefile.close_called = True - - def to_pcm(self): - import tempfile - f = tempfile.NamedTemporaryFile(suffix=".wav") - try: - self.to_wave(f.name) - return WaveReader(wave_file=file(f.name, "rb"), - sample_rate=self.sample_rate(), - channels=self.channels(), - channel_mask=int(self.channel_mask()), - bits_per_sample=self.bits_per_sample()) - except EncodingError, err: - return PCMReaderError(error_message=err.error_message, - sample_rate=self.sample_rate(), - channels=self.channels(), - channel_mask=int(self.channel_mask()), - bits_per_sample=self.bits_per_sample()) - - def to_wave(self, wave_file, progress=None): - """writes the contents of this file to the given .wav filename string - - raises EncodingError if some error occurs during decoding""" - - devnull = file(os.devnull, "w") - try: - sub = subprocess.Popen([BIN["neroAacDec"], - "-if", self.filename, - "-of", wave_file], - stdout=devnull, - stderr=devnull) - if (sub.wait() != 0): - raise EncodingError(u"unable to write file with neroAacDec") - finally: - devnull.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 M4AAudio object""" - - if ((compression is None) or - (compression not in cls.COMPRESSION_MODES)): - compression = __default_quality__(cls.NAME) - - try: - wave = WaveAudio(wave_filename) - wave.verify() - except InvalidFile: - raise EncodingError(u"invalid wave file") - - if (wave.sample_rate > 96000): - #convert through PCMConverter if sample rate is too high - import tempfile - tempwavefile = tempfile.NamedTemporaryFile(suffix=".wav") - try: - tempwave = WaveAudio.from_pcm( - tempwavefile.name, - PCMConverter(to_pcm_progress(wave, progress), - sample_rate=96000, - channels=wave.channels(), - channel_mask=wave.channel_mask(), - bits_per_sample=wave.bits_per_sample())) - return cls.__from_wave__(filename, tempwave.filename, - compression) - finally: - if (os.path.isfile(tempwavefile.name)): - tempwavefile.close() - else: - tempwavefile.close_called = True - else: - return cls.__from_wave__(filename, wave_filename, compression) - - @classmethod - def __from_wave__(cls, filename, wave_filename, compression): - devnull = file(os.devnull, "w") - try: - sub = subprocess.Popen([BIN["neroAacEnc"], - "-q", compression, - "-if", wave_filename, - "-of", filename], - stdout=devnull, - stderr=devnull) - - if (sub.wait() != 0): - raise EncodingError(u"neroAacEnc unable to write file") - else: - return cls(filename) - finally: - devnull.close() - -if (BIN.can_execute(BIN["neroAacEnc"]) and - BIN.can_execute(BIN["neroAacDec"])): - M4AAudio = M4AAudio_nero -else: - M4AAudio = M4AAudio_faac - - -class M4ACovr(Image): - """a subclass of Image to store M4A 'covr' atoms""" - - def __init__(self, image_data): - self.image_data = image_data - - img = Image.new(image_data, u'', 0) - - Image.__init__(self, - data=image_data, - mime_type=img.mime_type, - width=img.width, - height=img.height, - color_depth=img.color_depth, - color_count=img.color_count, - description=img.description, - type=img.type) - - @classmethod - def converted(cls, image): - """given an Image object, returns an M4ACovr object""" - - return M4ACovr(image.data) - - -class __counter__: - def __init__(self): - self.val = 0 - - def incr(self): - self.val += 1 - - def __int__(self): - return self.val - - -class InvalidALAC(InvalidFile): - pass - - -class ALACAudio(M4ATaggedAudio, AudioFile): - """an Apple Lossless audio file""" - - SUFFIX = "m4a" - NAME = "alac" - DEFAULT_COMPRESSION = "" - COMPRESSION_MODES = ("",) - BINARIES = tuple() - - BLOCK_SIZE = 4096 - INITIAL_HISTORY = 10 - HISTORY_MULTIPLIER = 40 - MAXIMUM_K = 14 - - def __init__(self, filename): - """filename is a plain string""" - - from .bitstream import BitstreamReader - - self.filename = filename - - #first, fetch the mdia atom - #which is the parent of both the alac and mdhd atoms - try: - mdia = get_m4a_atom(BitstreamReader(file(filename, 'rb'), 0), - "moov", "trak", "mdia")[1] - except IOError: - raise InvalidALAC(_(u"I/O error opening ALAC file")) - except KeyError: - raise InvalidALAC(_(u"Required mdia atom not found")) - mdia.mark() - try: - try: - stsd = get_m4a_atom(mdia, "minf", "stbl", "stsd")[1] - except KeyError: - raise InvalidALAC(_(u"Required stsd atom not found")) - - #then, fetch the alac atom for bps, channels and sample rate - try: - #though some of these fields are parsed redundantly - #in .to_pcm(), we still need to parse them here - #to fetch values for .bits_per_sample(), etc. - (stsd_version, descriptions) = stsd.parse("8u 24p 32u") - (alac1, - alac2, - self.__max_samples_per_frame__, - self.__bits_per_sample__, - self.__history_multiplier__, - self.__initial_history__, - self.__maximum_k__, - self.__channels__, - self.__sample_rate__) = stsd.parse( - #ignore much of the stuff in the "high" ALAC atom - "32p 4b 6P 16p 16p 16p 4P 16p 16p 16p 16p 4P" + - #and use the attributes in the "low" ALAC atom instead - "32p 4b 4P 32u 8p 8u 8u 8u 8u 8u 16p 32p 32p 32u") - except IOError: - raise InvalidALAC(_(u"Invalid alac atom")) - - if ((alac1 != 'alac') or (alac2 != 'alac')): - mdia.unmark() - raise InvalidALAC(_(u"Invalid alac atom")) - - #finally, fetch the mdhd atom for total track length - mdia.rewind() - try: - mdhd = get_m4a_atom(mdia, "mdhd")[1] - except KeyError: - raise InvalidALAC(_(u"Required mdhd atom not found")) - try: - (version, ) = mdhd.parse("8u 24p") - if (version == 0): - (self.__length__,) = mdhd.parse("32p 32p 32p 32u 2P 16p") - elif (version == 1): - (self.__length__,) = mdhd.parse("64p 64p 32p 64U 2P 16p") - else: - raise InvalidALAC(_(u"Unsupported mdhd version")) - except IOError: - raise InvalidALAC(_(u"Invalid mdhd atom")) - finally: - mdia.unmark() - - @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""" - - from .bitstream import BitstreamReader - - reader = BitstreamReader(file, 0) - reader.mark() - try: - (ftyp, major_brand) = reader.parse("32p 4b 4b") - except IOError: - reader.unmark() - return False - - if ((ftyp == 'ftyp') and - (major_brand in ('mp41', 'mp42', 'M4A ', 'M4B '))): - reader.rewind() - reader.unmark() - try: - stsd = get_m4a_atom(reader, "moov", "trak", "mdia", - "minf", "stbl", "stsd")[1] - try: - (stsd_version, descriptions) = stsd.parse("8u 24p 32u") - (alac_size, alac_type) = stsd.parse("32u 4b") - return (alac_type == 'alac') - except IOError: - return False - except KeyError: - return False - else: - reader.unmark() - return False - - 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.__bits_per_sample__ - - 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.__length__ - - def channel_mask(self): - """returns a ChannelMask object of this track's channel layout""" - - return {1: ChannelMask.from_fields(front_center=True), - 2: ChannelMask.from_fields(front_left=True, - front_right=True), - 3: ChannelMask.from_fields(front_center=True, - front_left=True, - front_right=True), - 4: ChannelMask.from_fields(front_center=True, - front_left=True, - front_right=True, - back_center=True), - 5: ChannelMask.from_fields(front_center=True, - front_left=True, - front_right=True, - back_left=True, - back_right=True), - 6: ChannelMask.from_fields(front_center=True, - front_left=True, - front_right=True, - back_left=True, - back_right=True, - low_frequency=True), - 7: ChannelMask.from_fields(front_center=True, - front_left=True, - front_right=True, - back_left=True, - back_right=True, - back_center=True, - low_frequency=True), - 8: ChannelMask.from_fields(front_center=True, - front_left_of_center=True, - front_right_of_center=True, - front_left=True, - front_right=True, - back_left=True, - back_right=True, - low_frequency=True)}.get( - self.channels(), ChannelMask(0)) - - def cd_frames(self): - """returns the total length of the track in CD frames - - each CD frame is 1/75th of a second""" - - try: - return (self.total_frames() * 75) / self.sample_rate() - except ZeroDivisionError: - return 0 - - def lossless(self): - """returns True""" - - return True - - def to_pcm(self): - """returns a PCMReader object containing the track's PCM data""" - - import audiotools.decoders - - try: - return audiotools.decoders.ALACDecoder(self.filename) - except (IOError, ValueError), msg: - return PCMReaderError(error_message=str(msg), - 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, - block_size=4096): - """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 ALACAudio object""" - - if (pcmreader.bits_per_sample not in (16, 24)): - raise UnsupportedBitsPerSample(filename, pcmreader.bits_per_sample) - - if (int(pcmreader.channel_mask) not in - (0x0001, # 1ch - mono - 0x0004, # 1ch - mono - 0x0003, # 2ch - left, right - 0x0007, # 3ch - center, left, right - 0x0107, # 4ch - center, left, right, back center - 0x0037, # 5ch - center, left, right, back left, back right - 0x003F, # 6ch - C, L, R, back left, back right, LFE - 0x013F, # 7ch - C, L, R, bL, bR, back center, LFE - 0x00FF, # 8ch - C, cL, cR, L, R, bL, bR, LFE - 0x0000)): # undefined - raise UnsupportedChannelMask(filename, - int(pcmreader.channel_mask)) - - from . import encoders - from .bitstream import BitstreamWriter - import time - import tempfile - - mdat_file = tempfile.TemporaryFile() - - #perform encode_alac() on pcmreader to our output file - #which returns a tuple of output values - #which are various fields for the "alac" atom - try: - (frame_sample_sizes, - frame_byte_sizes, - frame_file_offsets, - mdat_size) = encoders.encode_alac( - file=mdat_file, - pcmreader=BufferedPCMReader(pcmreader), - block_size=block_size, - initial_history=cls.INITIAL_HISTORY, - history_multiplier=cls.HISTORY_MULTIPLIER, - maximum_k=cls.MAXIMUM_K) - except (IOError, ValueError), err: - raise EncodingError(str(err)) - - #use the fields from encode_alac() to populate our ALAC atoms - create_date = long(time.time()) + 2082844800 - total_pcm_frames = sum(frame_sample_sizes) - - stts_frame_counts = {} - for sample_size in frame_sample_sizes: - stts_frame_counts.setdefault(sample_size, __counter__()).incr() - stts_frame_counts = dict([(k, int(v)) for (k, v) - in stts_frame_counts.items()]) - - offsets = frame_file_offsets[:] - chunks = [] - for frames in at_a_time(len(frame_file_offsets), 5): - if (frames > 0): - chunks.append(offsets[0:frames]) - offsets = offsets[frames:] - del(offsets) - - #add the size of ftyp + moov + free to our absolute file offsets - pre_mdat_size = (8 + cls.__ftyp_atom__().size() + - 8 + cls.__moov_atom__(pcmreader, - create_date, - mdat_size, - total_pcm_frames, - frame_sample_sizes, - stts_frame_counts, - chunks, - frame_byte_sizes).size() + - 8 + cls.__free_atom__(0x1000).size()) - - chunks = [[chunk + pre_mdat_size for chunk in chunk_list] - for chunk_list in chunks] - - #then regenerate our live ftyp, moov and free atoms - #with actual data - ftyp = cls.__ftyp_atom__() - - moov = cls.__moov_atom__(pcmreader, - create_date, - mdat_size, - total_pcm_frames, - frame_sample_sizes, - stts_frame_counts, - chunks, - frame_byte_sizes) - - free = cls.__free_atom__(0x1000) - - #build our complete output file - try: - f = file(filename, 'wb') - m4a_writer = BitstreamWriter(f, 0) - m4a_writer.build("32u 4b", (ftyp.size() + 8, ftyp.name)) - ftyp.build(m4a_writer) - m4a_writer.build("32u 4b", (moov.size() + 8, moov.name)) - moov.build(m4a_writer) - m4a_writer.build("32u 4b", (free.size() + 8, free.name)) - free.build(m4a_writer) - mdat_file.seek(0, 0) - transfer_data(mdat_file.read, f.write) - mdat_file.close() - except (IOError), err: - mdat_file.close() - raise EncodingError(str(err)) - - return cls(filename) - - @classmethod - def __ftyp_atom__(cls): - return M4A_FTYP_Atom(major_brand='M4A ', - major_brand_version=0, - compatible_brands=['M4A ', - 'mp42', - 'isom', - chr(0) * 4]) - - @classmethod - def __moov_atom__(cls, pcmreader, - create_date, - mdat_size, - total_pcm_frames, - frame_sample_sizes, - stts_frame_counts, - chunks, - frame_byte_sizes): - return M4A_Tree_Atom( - "moov", - [cls.__mvhd_atom__(pcmreader, create_date, total_pcm_frames), - M4A_Tree_Atom( - "trak", - [cls.__tkhd_atom__(create_date, total_pcm_frames), - M4A_Tree_Atom( - "mdia", - [cls.__mdhd_atom__(pcmreader, - create_date, - total_pcm_frames), - cls.__hdlr_atom__(), - M4A_Tree_Atom( - "minf", - [cls.__smhd_atom__(), - M4A_Tree_Atom( - "dinf", - [cls.__dref_atom__()]), - M4A_Tree_Atom( - "stbl", - [cls.__stsd_atom__( - pcmreader, - mdat_size, - frame_sample_sizes, - frame_byte_sizes), - cls.__stts_atom__( - stts_frame_counts), - cls.__stsc_atom__( - chunks), - cls.__stsz_atom__( - frame_byte_sizes), - cls.__stco_atom__( - chunks)])])])]), - M4A_Tree_Atom( - "udta", - [cls.__meta_atom__()])]) - - @classmethod - def __mvhd_atom__(cls, pcmreader, create_date, total_pcm_frames): - return M4A_MVHD_Atom(version=0, - flags=0, - created_utc_date=create_date, - modified_utc_date=create_date, - time_scale=pcmreader.sample_rate, - duration=total_pcm_frames, - playback_speed=0x10000, - user_volume=0x100, - geometry_matrices=[0x10000, - 0, - 0, - 0, - 0x10000, - 0, - 0, - 0, - 0x40000000], - qt_preview=0, - qt_still_poster=0, - qt_selection_time=0, - qt_current_time=0, - next_track_id=2) - - @classmethod - def __tkhd_atom__(cls, create_date, total_pcm_frames): - return M4A_TKHD_Atom(version=0, - track_in_poster=0, - track_in_preview=1, - track_in_movie=1, - track_enabled=1, - created_utc_date=create_date, - modified_utc_date=create_date, - track_id=1, - duration=total_pcm_frames, - video_layer=0, - qt_alternate=0, - volume=0x100, - geometry_matrices=[0x10000, - 0, - 0, - 0, - 0x10000, - 0, - 0, - 0, - 0x40000000], - video_width=0, - video_height=0) - - @classmethod - def __mdhd_atom__(cls, pcmreader, create_date, total_pcm_frames): - return M4A_MDHD_Atom(version=0, - flags=0, - created_utc_date=create_date, - modified_utc_date=create_date, - sample_rate=pcmreader.sample_rate, - track_length=total_pcm_frames, - language=[ord(c) - 0x60 for c in "und"], - quality=0) - - @classmethod - def __hdlr_atom__(cls): - return M4A_HDLR_Atom(version=0, - flags=0, - qt_type=chr(0) * 4, - qt_subtype='soun', - qt_manufacturer=chr(0) * 4, - qt_reserved_flags=0, - qt_reserved_flags_mask=0, - component_name="", - padding_size=1) - - @classmethod - def __smhd_atom__(cls): - return M4A_SMHD_Atom(version=0, - flags=0, - audio_balance=0) - - @classmethod - def __dref_atom__(cls): - return M4A_DREF_Atom(version=0, - flags=0, - references=[M4A_Leaf_Atom("url ", - "\x00\x00\x00\x01")]) - - @classmethod - def __stsd_atom__(cls, pcmreader, mdat_size, frame_sample_sizes, - frame_byte_sizes): - return M4A_STSD_Atom( - version=0, - flags=0, - descriptions=[M4A_ALAC_Atom( - reference_index=1, - qt_version=0, - qt_revision_level=0, - qt_vendor=chr(0) * 4, - channels=pcmreader.channels, - bits_per_sample=pcmreader.bits_per_sample, - qt_compression_id=0, - audio_packet_size=0, - sample_rate=0xAC440000, # regardless of actual sample rate - sub_alac=M4A_SUB_ALAC_Atom( - max_samples_per_frame=max(frame_sample_sizes), - bits_per_sample=pcmreader.bits_per_sample, - history_multiplier=cls.HISTORY_MULTIPLIER, - initial_history=cls.INITIAL_HISTORY, - maximum_k=cls.MAXIMUM_K, - channels=pcmreader.channels, - unknown=0x00FF, - max_coded_frame_size=max(frame_byte_sizes), - bitrate=((mdat_size * 8 * pcmreader.sample_rate) / - sum(frame_sample_sizes)), - sample_rate=pcmreader.sample_rate))]) - - @classmethod - def __stts_atom__(cls, stts_frame_counts): - return M4A_STTS_Atom( - version=0, - flags=0, - times=[(int(stts_frame_counts[samples]), samples) - for samples in reversed(sorted(stts_frame_counts.keys()))]) - - @classmethod - def __stsc_atom__(cls, chunks): - return M4A_STSC_Atom( - version=0, - flags=0, - blocks=[(i + 1, current, 1) for (i, (current, previous)) - in enumerate(zip(map(len, chunks), [0] + map(len, chunks))) - if (current != previous)]) - - @classmethod - def __stsz_atom__(cls, frame_byte_sizes): - return M4A_STSZ_Atom( - version=0, - flags=0, - byte_size=0, - block_sizes=frame_byte_sizes) - - @classmethod - def __stco_atom__(cls, chunks): - return M4A_STCO_Atom( - version=0, - flags=0, - offsets=[chunk[0] for chunk in chunks]) - - @classmethod - def __meta_atom__(cls): - return M4A_META_Atom( - version=0, - flags=0, - leaf_atoms=[ - M4A_HDLR_Atom(version=0, - flags=0, - qt_type=chr(0) * 4, - qt_subtype='mdir', - qt_manufacturer='appl', - qt_reserved_flags=0, - qt_reserved_flags_mask=0, - component_name="", - padding_size=1), - M4A_Tree_Atom( - "ilst", - [M4A_ILST_Leaf_Atom( - '\xa9too', - [M4A_ILST_Unicode_Data_Atom( - 0, 1, - "Python Audio Tools %s" % (VERSION))])]), - M4A_FREE_Atom(1024)]) - - @classmethod - def __free_atom__(cls, size): - return M4A_FREE_Atom(size)
View file
audiotools-2.18.tar.gz/audiotools/__m4a_atoms__.py
Deleted
@@ -1,1892 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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 MetaData, Image, image_metrics - -#M4A atoms are typically laid on in the file as follows: -# ftyp -# mdat -# moov/ -# +mvhd -# +iods -# +trak/ -# +-tkhd -# +-mdia/ -# +--mdhd -# +--hdlr -# +--minf/ -# +---smhd -# +---dinf/ -# +----dref -# +---stbl/ -# +----stsd -# +----stts -# +----stsz -# +----stsc -# +----stco -# +----ctts -# +udta/ -# +-meta -# -#Where atoms ending in / are container atoms and the rest are leaf atoms. -#'mdat' is where the file's audio stream is stored -#the rest are various bits of metadata - - -def parse_sub_atoms(data_size, reader, parsers): - """data size is the length of the parent atom's data - reader is a BitstreamReader - parsers is a dict of leaf_name->parser() - where parser is defined as: - parser(leaf_name, leaf_data_size, BitstreamReader, parsers) - as a sort of recursive parsing handler - """ - - leaf_atoms = [] - - while (data_size > 0): - (leaf_size, leaf_name) = reader.parse("32u 4b") - leaf_atoms.append( - parsers.get(leaf_name, M4A_Leaf_Atom).parse( - leaf_name, - leaf_size - 8, - reader.substream(leaf_size - 8), - parsers)) - data_size -= leaf_size - - return leaf_atoms - -#build(), parse() and size() work on atom data -#but not the atom's size and name values - - -class M4A_Tree_Atom: - def __init__(self, name, leaf_atoms): - """name should be a 4 byte string - - children should be a list of M4A_Tree_Atoms or M4A_Leaf_Atoms""" - - self.name = name - try: - iter(leaf_atoms) - except TypeError: - raise TypeError(_(u"leaf atoms must be a list")) - self.leaf_atoms = leaf_atoms - - def copy(self): - """returns a newly copied instance of this atom - and new instances of any sub-atoms it contains""" - - return M4A_Tree_Atom(self.name, [leaf.copy() for leaf in self]) - - def __repr__(self): - return "M4A_Tree_Atom(%s, %s)" % \ - (repr(self.name), repr(self.leaf_atoms)) - - def __eq__(self, atom): - for attr in ["name", "leaf_atoms"]: - if ((not hasattr(atom, attr)) or - (getattr(self, attr) != getattr(atom, attr))): - return False - else: - return True - - def __iter__(self): - for leaf in self.leaf_atoms: - yield leaf - - def __getitem__(self, atom_name): - return self.get_child(atom_name) - - def get_child(self, atom_name): - """returns the first instance of the given child atom - raises KeyError if the child is not found""" - - for leaf in self: - if (leaf.name == atom_name): - return leaf - else: - raise KeyError(atom_name) - - def has_child(self, atom_name): - """returns True if the given atom name - is an immediate child of this atom""" - - for leaf in self: - if (leaf.name == atom_name): - return True - else: - return False - - def add_child(self, atom_obj): - """adds the given child atom to this container""" - - self.leaf_atoms.append(atom_obj) - - def remove_child(self, atom_name): - """removes the first instance of the given atom from this container""" - - new_leaf_atoms = [] - data_deleted = False - for leaf_atom in self: - if ((leaf_atom.name == atom_obj.name) and (not data_deleted)): - data_deleted = True - else: - new_leaf_atoms.append(leaf_atom) - - self.leaf_atoms = new_leaf_atoms - - def replace_child(self, atom_obj): - """replaces the first instance of the given atom's name - with the given atom""" - - new_leaf_atoms = [] - data_replaced = False - for leaf_atom in self: - if ((leaf_atom.name == atom_obj.name) and (not data_replaced)): - new_leaf_atoms.append(atom_obj) - data_replaced = True - else: - new_leaf_atoms.append(leaf_atom) - - self.leaf_atoms = new_leaf_atoms - - def child_offset(self, *child_path): - """given a path to the given child atom - returns its offset within this parent - - raises KeyError if the child cannot be found""" - - offset = 0 - next_child = child_path[0] - for leaf_atom in self: - if (leaf_atom.name == next_child): - if (len(child_path) > 1): - return (offset + 8 + - leaf_atom.child_offset(*(child_path[1:]))) - else: - return offset - else: - offset += (8 + leaf_atom.size()) - else: - raise KeyError(next_child) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - return cls(name, parse_sub_atoms(data_size, reader, parsers)) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - for sub_atom in self: - writer.build("32u 4b", (sub_atom.size() + 8, sub_atom.name)) - sub_atom.build(writer) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return sum([8 + sub_atom.size() for sub_atom in self]) - - -class M4A_Leaf_Atom: - def __init__(self, name, data): - """name should be a 4 byte string - - data should be a binary string of atom data""" - - self.name = name - self.data = data - - def copy(self): - """returns a newly copied instance of this atom - and new instances of any sub-atoms it contains""" - - return M4A_Leaf_Atom(self.name, self.data) - - def __repr__(self): - return "M4A_Leaf_Atom(%s, %s)" % \ - (repr(self.name), repr(self.data)) - - def __eq__(self, atom): - for attr in ["name", "data"]: - if ((not hasattr(atom, attr)) or - (getattr(self, attr) != getattr(atom, attr))): - return False - else: - return True - - - def __unicode__(self): - #FIXME - should make this more informative, if possible - return self.data.encode('hex')[0:40].decode('ascii') - - def raw_info(self): - """returns a line of human-readable information about the atom""" - - if (len(self.data) > 20): - return u"%s : %s\u2026" % \ - (self.name.decode('ascii', 'replace'), - u"".join([u"%2.2X" % (ord(b)) for b in self.data[0:20]])) - else: - return u"%s : %s" % \ - (self.name.decode('ascii', 'replace'), - u"".join([u"%2.2X" % (ord(b)) for b in self.data])) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - return cls(name, reader.read_bytes(data_size)) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.write_bytes(self.data) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return len(self.data) - - -class M4A_FTYP_Atom(M4A_Leaf_Atom): - def __init__(self, major_brand, major_brand_version, compatible_brands): - self.name = 'ftyp' - self.major_brand = major_brand - self.major_brand_version = major_brand_version - self.compatible_brands = compatible_brands - - def __repr__(self): - return "M4A_FTYP_Atom(%s, %s, %s)" % \ - (repr(self.major_brand), - repr(self.major_brand_version), - repr(self.compatible_brands)) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - assert(name == 'ftyp') - return cls(reader.read_bytes(4), - reader.read(32), - [reader.read_bytes(4) - for i in xrange((data_size - 8) / 4)]) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("4b 32u %s" % ("4b" * len(self.compatible_brands)), - [self.major_brand, - self.major_brand_version] + - self.compatible_brands) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return 4 + 4 + (4 * len(self.compatible_brands)) - - -class M4A_MVHD_Atom(M4A_Leaf_Atom): - def __init__(self, version, flags, created_utc_date, modified_utc_date, - time_scale, duration, playback_speed, user_volume, - geometry_matrices, qt_preview, qt_still_poster, - qt_selection_time, qt_current_time, next_track_id): - self.name = 'mvhd' - self.version = version - self.flags = flags - self.created_utc_date = created_utc_date - self.modified_utc_date = modified_utc_date - self.time_scale = time_scale - self.duration = duration - self.playback_speed = playback_speed - self.user_volume = user_volume - self.geometry_matrices = geometry_matrices - self.qt_preview = qt_preview - self.qt_still_poster = qt_still_poster - self.qt_selection_time = qt_selection_time - self.qt_current_time = qt_current_time - self.next_track_id = next_track_id - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - assert(name == 'mvhd') - (version, flags) = reader.parse("8u 24u") - - if (version == 0): - atom_format = "32u 32u 32u 32u 32u 16u 10P" - else: - atom_format = "64U 64U 32u 64U 32u 16u 10P" - (created_utc_date, - modified_utc_date, - time_scale, - duration, - playback_speed, - user_volume) = reader.parse(atom_format) - - geometry_matrices = reader.parse("32u" * 9) - - (qt_preview, - qt_still_poster, - qt_selection_time, - qt_current_time, - next_track_id) = reader.parse("64U 32u 64U 32u 32u") - - return cls(version=version, - flags=flags, - created_utc_date=created_utc_date, - modified_utc_date=modified_utc_date, - time_scale=time_scale, - duration=duration, - playback_speed=playback_speed, - user_volume=user_volume, - geometry_matrices=geometry_matrices, - qt_preview=qt_preview, - qt_still_poster=qt_still_poster, - qt_selection_time=qt_selection_time, - qt_current_time=qt_current_time, - next_track_id=next_track_id) - - def __repr__(self): - return "MVHD_Atom(%s)" % ( - ",".join(map(repr, - [self.version, self.flags, - self.created_utc_date, self.modified_utc_date, - self.time_scale, self.duration, self.playback_speed, - self.user_volume, self.geometry_matrices, - self.qt_preview, self.qt_still_poster, - self.qt_selection_time, self.qt_current_time, - self.next_track_id]))) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("8u 24u", (self.version, self.flags)) - - if (self.version == 0): - atom_format = "32u 32u 32u 32u 32u 16u 10P" - else: - atom_format = "64U 64U 32u 64U 32u 16u 10P" - - writer.build(atom_format, - (self.created_utc_date, self.modified_utc_date, - self.time_scale, self.duration, - self.playback_speed, self.user_volume)) - - writer.build("32u" * 9, self.geometry_matrices) - - writer.build("64U 32u 64U 32u 32u", - (self.qt_preview, self.qt_still_poster, - self.qt_selection_time, self.qt_current_time, - self.next_track_id)) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - if (self.version == 0): - return 100 - else: - return 112 - - -class M4A_TKHD_Atom(M4A_Leaf_Atom): - def __init__(self, version, track_in_poster, track_in_preview, - track_in_movie, track_enabled, created_utc_date, - modified_utc_date, track_id, duration, video_layer, - qt_alternate, volume, geometry_matrices, - video_width, video_height): - self.name = 'tkhd' - self.version = version - self.track_in_poster = track_in_poster - self.track_in_preview = track_in_preview - self.track_in_movie = track_in_movie - self.track_enabled = track_enabled - self.created_utc_date = created_utc_date - self.modified_utc_date = modified_utc_date - self.track_id = track_id - self.duration = duration - self.video_layer = video_layer - self.qt_alternate = qt_alternate - self.volume = volume - self.geometry_matrices = geometry_matrices - self.video_width = video_width - self.video_height = video_height - - def __repr__(self): - return "M4A_TKHD_Atom(%s)" % ( - ",".join(map(repr, - [self.version, self.track_in_poster, - self.track_in_preview, self.track_in_movie, - self.track_enabled, self.created_utc_date, - self.modified_utc_date, self.track_id, - self.duration, self.video_layer, self.qt_alternate, - self.volume, self.geometry_matrices, - self.video_width, self.video_height]))) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - (version, - track_in_poster, - track_in_preview, - track_in_movie, - track_enabled) = reader.parse("8u 20p 1u 1u 1u 1u") - - if (version == 0): - atom_format = "32u 32u 32u 4P 32u 8P 16u 16u 16u 2P" - else: - atom_format = "64U 64U 32u 4P 64U 8P 16u 16u 16u 2P" - (created_utc_date, - modified_utc_date, - track_id, - duration, - video_layer, - qt_alternate, - volume) = reader.parse(atom_format) - - geometry_matrices = reader.parse("32u" * 9) - (video_width, video_height) = reader.parse("32u 32u") - - return cls(version=version, - track_in_poster=track_in_poster, - track_in_preview=track_in_preview, - track_in_movie=track_in_movie, - track_enabled=track_enabled, - created_utc_date=created_utc_date, - modified_utc_date=modified_utc_date, - track_id=track_id, - duration=duration, - video_layer=video_layer, - qt_alternate=qt_alternate, - volume=volume, - geometry_matrices=geometry_matrices, - video_width=video_width, - video_height=video_height) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("8u 20p 1u 1u 1u 1u", - (self.version, self.track_in_poster, - self.track_in_preview, self.track_in_movie, - self.track_enabled)) - if (self.version == 0): - atom_format = "32u 32u 32u 4P 32u 8P 16u 16u 16u 2P" - else: - atom_format = "64U 64U 32u 4P 64U 8P 16u 16u 16u 2P" - writer.build(atom_format, - (self.created_utc_date, self.modified_utc_date, - self.track_id, self.duration, self.video_layer, - self.qt_alternate, self.volume)) - writer.build("32u" * 9, self.geometry_matrices) - writer.build("32u 32u", (self.video_width, self.video_height)) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - if (self.version == 0): - return 84 - else: - return 96 - - -class M4A_MDHD_Atom(M4A_Leaf_Atom): - def __init__(self, version, flags, created_utc_date, modified_utc_date, - sample_rate, track_length, language, quality): - self.name = 'mdhd' - self.version = version - self.flags = flags - self.created_utc_date = created_utc_date - self.modified_utc_date = modified_utc_date - self.sample_rate = sample_rate - self.track_length = track_length - self.language = language - self.quality = quality - - def __repr__(self): - return "M4A_MDHD_Atom(%s)" % \ - (",".join(map(repr, - [self.version, self.flags, self.created_utc_date, - self.modified_utc_date, self.sample_rate, - self.track_length, self.language, self.quality]))) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - assert(name == 'mdhd') - (version, flags) = reader.parse("8u 24u") - if (version == 0): - atom_format = "32u 32u 32u 32u" - else: - atom_format = "64U 64U 32u 64U" - (created_utc_date, - modified_utc_date, - sample_rate, - track_length) = reader.parse(atom_format) - language = reader.parse("1p 5u 5u 5u") - quality = reader.read(16) - - return cls(version=version, - flags=flags, - created_utc_date=created_utc_date, - modified_utc_date=modified_utc_date, - sample_rate=sample_rate, - track_length=track_length, - language=language, - quality=quality) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("8u 24u", (self.version, self.flags)) - if (self.version == 0): - atom_format = "32u 32u 32u 32u" - else: - atom_format = "64U 64U 32u 64U" - writer.build(atom_format, - (self.created_utc_date, self.modified_utc_date, - self.sample_rate, self.track_length)) - writer.build("1p 5u 5u 5u", self.language) - writer.write(16, self.quality) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - if (self.version == 0): - return 24 - else: - return 36 - - -class M4A_SMHD_Atom(M4A_Leaf_Atom): - def __init__(self, version, flags, audio_balance): - self.name = 'smhd' - self.version = version - self.flags = flags - self.audio_balance = audio_balance - - def __repr__(self): - return "M4A_SMHD_Atom(%s)" % \ - (",".join(map(repr, (self.version, - self.flags, - self.audio_balance)))) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - return cls(*reader.parse("8u 24u 16u 16p")) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("8u 24u 16u 16p", - (self.version, self.flags, self.audio_balance)) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return 8 - - -class M4A_DREF_Atom(M4A_Leaf_Atom): - def __init__(self, version, flags, references): - self.name = 'dref' - self.version = version - self.flags = flags - self.references = references - - def __repr__(self): - return "M4A_DREF_Atom(%s)" % \ - (",".join(map(repr, (self.version, - self.flags, - self.references)))) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - (version, flags, reference_count) = reader.parse("8u 24u 32u") - references = [] - for i in xrange(reference_count): - (leaf_size, leaf_name) = reader.parse("32u 4b") - references.append(M4A_Leaf_Atom.parse( - leaf_name, leaf_size - 8, - reader.substream(leaf_size - 8), {})) - return cls(version, flags, references) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("8u 24u 32u", (self.version, - self.flags, - len(self.references))) - - for reference_atom in self.references: - writer.build("32u 4b", (reference_atom.size() + 8, - reference_atom.name)) - reference_atom.build(writer) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return 8 + sum([reference_atom.size() + 8 - for reference_atom in self.references]) - - -class M4A_STSD_Atom(M4A_Leaf_Atom): - def __init__(self, version, flags, descriptions): - self.name = 'stsd' - self.version = version - self.flags = flags - self.descriptions = descriptions - - def __repr__(self): - return "M4A_STSD_Atom(%s, %s, %s)" % \ - (repr(self.version), repr(self.flags), repr(self.descriptions)) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - (version, flags, description_count) = reader.parse("8u 24u 32u") - descriptions = [] - for i in xrange(description_count): - (leaf_size, leaf_name) = reader.parse("32u 4b") - descriptions.append( - parsers.get(leaf_name, M4A_Leaf_Atom).parse( - leaf_name, - leaf_size - 8, - reader.substream(leaf_size - 8), - parsers)) - return cls(version=version, - flags=flags, - descriptions=descriptions) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("8u 24u 32u", (self.version, - self.flags, - len(self.descriptions))) - - for description_atom in self.descriptions: - writer.build("32u 4b", (description_atom.size() + 8, - description_atom.name)) - description_atom.build(writer) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return 8 + sum([8 + description_atom.size() - for description_atom in self.descriptions]) - - -class M4A_STTS_Atom(M4A_Leaf_Atom): - def __init__(self, version, flags, times): - self.name = 'stts' - self.version = version - self.flags = flags - self.times = times - - def __repr__(self): - return "M4A_STTS_Atom(%s, %s, %s)" % \ - (repr(self.version), repr(self.flags), repr(self.times)) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - (version, flags) = reader.parse("8u 24u") - return cls(version=version, - flags=flags, - times=[tuple(reader.parse("32u 32u")) - for i in xrange(reader.read(32))]) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("8u 24u 32u", (self.version, self.flags, len(self.times))) - for time in self.times: - writer.build("32u 32u", time) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return 8 + (8 * len(self.times)) - - -class M4A_STSC_Atom(M4A_Leaf_Atom): - def __init__(self, version, flags, blocks): - self.name = 'stsc' - self.version = version - self.flags = flags - self.blocks = blocks - - def __repr__(self): - return "M4A_STSC_Atom(%s, %s, %s)" % \ - (repr(self.version), repr(self.flags), repr(self.blocks)) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - (version, flags) = reader.parse("8u 24u") - return cls(version=version, - flags=flags, - blocks=[tuple(reader.parse("32u 32u 32u")) - for i in xrange(reader.read(32))]) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("8u 24u 32u", - (self.version, self.flags, len(self.blocks))) - for block in self.blocks: - writer.build("32u 32u 32u", block) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return 8 + (12 * len(self.blocks)) - - -class M4A_STSZ_Atom(M4A_Leaf_Atom): - def __init__(self, version, flags, byte_size, block_sizes): - self.name = 'stsz' - self.version = version - self.flags = flags - self.byte_size = byte_size - self.block_sizes = block_sizes - - def __repr__(self): - return "M4A_STSZ_Atom(%s, %s, %s, %s)" % \ - (repr(self.version), repr(self.flags), repr(self.byte_size), - repr(self.block_sizes)) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - (version, flags, byte_size) = reader.parse("8u 24u 32u") - return cls(version=version, - flags=flags, - byte_size=byte_size, - block_sizes=[reader.read(32) for i in - xrange(reader.read(32))]) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("8u 24u 32u 32u", (self.version, - self.flags, - self.byte_size, - len(self.block_sizes))) - for size in self.block_sizes: - writer.write(32, size) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return 12 + (4 * len(self.block_sizes)) - - -class M4A_STCO_Atom(M4A_Leaf_Atom): - def __init__(self, version, flags, offsets): - self.name = 'stco' - self.version = version - self.flags = flags - self.offsets = offsets - - def __repr__(self): - return "M4A_STCO_Atom(%s, %s, %s)" % \ - (self.version, self.flags, self.offsets) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - assert(name == "stco") - (version, flags, offset_count) = reader.parse("8u 24u 32u") - return cls(version, flags, - [reader.read(32) for i in xrange(offset_count)]) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("8u 24u 32u", (self.version, self.flags, - len(self.offsets))) - for offset in self.offsets: - writer.write(32, offset) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return 8 + (4 * len(self.offsets)) - - -class M4A_ALAC_Atom(M4A_Leaf_Atom): - def __init__(self, reference_index, qt_version, qt_revision_level, - qt_vendor, channels, bits_per_sample, qt_compression_id, - audio_packet_size, sample_rate, sub_alac): - self.name = 'alac' - self.reference_index = reference_index - self.qt_version = qt_version - self.qt_revision_level = qt_revision_level - self.qt_vendor = qt_vendor - self.channels = channels - self.bits_per_sample = bits_per_sample - self.qt_compression_id = qt_compression_id - self.audio_packet_size = audio_packet_size - self.sample_rate = sample_rate - self.sub_alac = sub_alac - - def __repr__(self): - return "M4A_ALAC_Atom(%s)" % \ - ",".join(map(repr, [self.reference_index, - self.qt_version, - self.qt_revision_level, - self.qt_vendor, - self.channels, - self.bits_per_sample, - self.qt_compression_id, - self.audio_packet_size, - self.sample_rate, - self.sub_alac])) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - (reference_index, - qt_version, - qt_revision_level, - qt_vendor, - channels, - bits_per_sample, - qt_compression_id, - audio_packet_size, - sample_rate) = reader.parse( - "6P 16u 16u 16u 4b 16u 16u 16u 16u 32u") - (sub_alac_size, sub_alac_name) = reader.parse("32u 4b") - sub_alac = M4A_SUB_ALAC_Atom.parse(sub_alac_name, - sub_alac_size - 8, - reader.substream(sub_alac_size - 8), - {}) - return cls(reference_index=reference_index, - qt_version=qt_version, - qt_revision_level=qt_revision_level, - qt_vendor=qt_vendor, - channels=channels, - bits_per_sample=bits_per_sample, - qt_compression_id=qt_compression_id, - audio_packet_size=audio_packet_size, - sample_rate=sample_rate, - sub_alac=sub_alac) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("6P 16u 16u 16u 4b 16u 16u 16u 16u 32u", - (self.reference_index, - self.qt_version, - self.qt_revision_level, - self.qt_vendor, - self.channels, - self.bits_per_sample, - self.qt_compression_id, - self.audio_packet_size, - self.sample_rate)) - writer.build("32u 4b", (self.sub_alac.size() + 8, - self.sub_alac.name)) - self.sub_alac.build(writer) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return 28 + 8 + self.sub_alac.size() - - -class M4A_SUB_ALAC_Atom(M4A_Leaf_Atom): - def __init__(self, max_samples_per_frame, bits_per_sample, - history_multiplier, initial_history, maximum_k, - channels, unknown, max_coded_frame_size, bitrate, - sample_rate): - self.name = 'alac' - self.max_samples_per_frame = max_samples_per_frame - self.bits_per_sample = bits_per_sample - self.history_multiplier = history_multiplier - self.initial_history = initial_history - self.maximum_k = maximum_k - self.channels = channels - self.unknown = unknown - self.max_coded_frame_size = max_coded_frame_size - self.bitrate = bitrate - self.sample_rate = sample_rate - - def __repr__(self): - return "M4A_SUB_ALAC_Atom(%s)" % \ - (",".join(map(repr, [self.max_samples_per_frame, - self.bits_per_sample, - self.history_multiplier, - self.initial_history, - self.maximum_k, - self.channels, - self.unknown, - self.max_coded_frame_size, - self.bitrate, - self.sample_rate]))) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - return cls(*reader.parse( - "4P 32u 8p 8u 8u 8u 8u 8u 16u 32u 32u 32u")) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("4P 32u 8p 8u 8u 8u 8u 8u 16u 32u 32u 32u", - (self.max_samples_per_frame, - self.bits_per_sample, - self.history_multiplier, - self.initial_history, - self.maximum_k, - self.channels, - self.unknown, - self.max_coded_frame_size, - self.bitrate, - self.sample_rate)) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return 28 - - -class M4A_META_Atom(MetaData, M4A_Tree_Atom): - UNICODE_ATTRIB_TO_ILST = {"track_name": "\xa9nam", - "album_name": "\xa9alb", - "artist_name": "\xa9ART", - "composer_name": "\xa9wrt", - "copyright": "cprt", - "year": "\xa9day", - "comment": "\xa9cmt"} - - INT_ATTRIB_TO_ILST = {"track_number": "trkn", - "album_number": "disk"} - - TOTAL_ATTRIB_TO_ILST = {"track_total": "trkn", - "album_total": "disk"} - - def __init__(self, version, flags, leaf_atoms): - M4A_Tree_Atom.__init__(self, "meta", leaf_atoms) - self.__dict__["version"] = version - self.__dict__["flags"] = flags - - def __repr__(self): - return "M4A_META_Atom(%s, %s, %s)" % \ - (repr(self.version), repr(self.flags), repr(self.leaf_atoms)) - - def has_ilst_atom(self): - """returns True if this atom contains an ILST sub-atom""" - - for a in self.leaf_atoms: - if (a.name == 'ilst'): - return True - else: - return False - - def ilst_atom(self): - """returns the first ILST sub-atom, or None""" - - for a in self.leaf_atoms: - if (a.name == 'ilst'): - return a - else: - return None - - def add_ilst_atom(self): - """place new ILST atom after the first HDLR atom, if any""" - - for (index, atom) in enumerate(self.leaf_atoms): - if (atom.name == 'hdlr'): - self.leaf_atoms.insert(index, M4A_Tree_Atom('ilst', [])) - break - else: - self.leaf_atoms.append(M4A_Tree_Atom('ilst', [])) - - def raw_info(self): - """returns a Unicode string of low-level MetaData information - - whereas __unicode__ is meant to contain complete information - at a very high level - raw_info() should be more developer-specific and with - very little adjustment or reordering to the data itself - """ - - from os import linesep - from . import display_unicode - - if (self.has_ilst_atom()): - comment_lines = [u"M4A:"] - - for atom in self.ilst_atom(): - if (hasattr(atom, "raw_info_lines")): - comment_lines.extend(atom.raw_info_lines()) - else: - comment_lines.append(u"%s : (%d bytes)" % - (atom.name.decode('ascii', 'replace'), - atom.size())) - - return linesep.decode('ascii').join(comment_lines) - else: - return u"" - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - assert(name == "meta") - (version, flags) = reader.parse("8u 24u") - return cls(version, flags, - parse_sub_atoms(data_size - 4, reader, parsers)) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("8u 24u", (self.version, self.flags)) - for sub_atom in self: - writer.build("32u 4b", (sub_atom.size() + 8, sub_atom.name)) - sub_atom.build(writer) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return 4 + sum([8 + sub_atom.size() for sub_atom in self]) - - def __getattr__(self, key): - if (key in self.UNICODE_ATTRIB_TO_ILST): - if (self.has_ilst_atom()): - try: - return unicode([a for a in self.ilst_atom() - if (a.name == - self.UNICODE_ATTRIB_TO_ILST[key])][0]) - except IndexError: - return u"" - else: - return u"" - elif (key in self.INT_ATTRIB_TO_ILST): - if (self.has_ilst_atom()): - try: - return int([a for a in self.ilst_atom() - if (a.name == - self.INT_ATTRIB_TO_ILST[key])][0]) - except IndexError: - return 0 - else: - return 0 - elif (key in self.TOTAL_ATTRIB_TO_ILST): - if (self.has_ilst_atom()): - try: - return [a for a in self.ilst_atom() - if (a.name == - self.TOTAL_ATTRIB_TO_ILST[key])][0].total() - except IndexError: - return 0 - else: - return 0 - elif (key in self.FIELDS): - return u"" - else: - raise AttributeError(key) - - def __setattr__(self, key, value): - def new_data_atom(attribute, value): - if (attribute in self.UNICODE_ATTRIB_TO_ILST): - return M4A_ILST_Unicode_Data_Atom(0, 1, value.encode('utf-8')) - elif (attribute == "track_number"): - return M4A_ILST_TRKN_Data_Atom(int(value), 0) - elif (attribute == "track_total"): - return M4A_ILST_TRKN_Data_Atom(0, int(value)) - elif (attribute == "album_number"): - return M4A_ILST_DISK_Data_Atom(int(value), 0) - elif (attribute == "album_total"): - return M4A_ILST_DISK_Data_Atom(0, int(value)) - else: - raise ValueError(value) - - def replace_data_atom(attribute, parent_atom, value): - new_leaf_atoms = [] - data_replaced = False - for leaf_atom in parent_atom.leaf_atoms: - if ((leaf_atom.name == 'data') and (not data_replaced)): - if (attribute == "track_number"): - new_leaf_atoms.append( - M4A_ILST_TRKN_Data_Atom(int(value), - leaf_atom.track_total)) - elif (attribute == "track_total"): - new_leaf_atoms.append( - M4A_ILST_TRKN_Data_Atom(leaf_atom.track_number, - int(value))) - elif (attribute == "album_number"): - new_leaf_atoms.append( - M4A_ILST_DISK_Data_Atom(int(value), - leaf_atom.disk_total)) - elif (attribute == "album_total"): - new_leaf_atoms.append( - M4A_ILST_DISK_Data_Atom(leaf_atom.disk_number, - int(value))) - else: - new_leaf_atoms.append(new_data_atom(attribute, value)) - - data_replaced = True - else: - new_leaf_atoms.append(leaf_atom) - - parent_atom.leaf_atoms = new_leaf_atoms - - ilst_leaf = self.UNICODE_ATTRIB_TO_ILST.get( - key, - self.INT_ATTRIB_TO_ILST.get( - key, - self.TOTAL_ATTRIB_TO_ILST.get( - key, - None))) - - if (ilst_leaf is None): - self.__dict__[key] = value - return - - if (not self.has_ilst_atom()): - self.add_ilst_atom() - - #an ilst atom is present, so check its sub-atoms - for ilst_atom in self.ilst_atom(): - if (ilst_atom.name == ilst_leaf): - #atom already present, so adjust its data sub-atom - replace_data_atom(key, ilst_atom, value) - break - else: - #atom not present, so append new parent and data sub-atom - self.ilst_atom().add_child( - M4A_ILST_Leaf_Atom(ilst_leaf, [new_data_atom(key, value)])) - - def __delattr__(self, key): - if (self.has_ilst_atom()): - ilst_atom = self.ilst_atom() - - if (key in self.UNICODE_ATTRIB_TO_ILST): - ilst_atom.leaf_atoms = filter( - lambda atom: atom.name != self.UNICODE_ATTRIB_TO_ILST[key], - ilst_atom) - elif (key == "track_number"): - if (self.track_total == 0): - ilst_atom.leaf_atoms = filter( - lambda atom: atom.name != "trkn", ilst_atom) - else: - self.track_number = 0 - elif (key == "track_total"): - if (self.track_number == 0): - ilst_atom.leaf_atoms = filter( - lambda atom: atom.name != "trkn", ilst_atom) - else: - self.track_total = 0 - elif (key == "album_number"): - if (self.album_total == 0): - ilst_atom.leaf_atoms = filter( - lambda atom: atom.name != "disk", ilst_atom) - else: - self.album_number = 0 - elif (key == "album_total"): - if (self.album_number == 0): - ilst_atom.leaf_atoms = filter( - lambda atom: atom.name != "disk", ilst_atom) - else: - self.album_total = 0 - else: - try: - del(self.__dict__[key]) - except KeyError: - raise AttributeError(key) - - def images(self): - """returns a list of embedded Image objects""" - - if (self.has_ilst_atom()): - return [atom['data'] for atom in self.ilst_atom() - if ((atom.name == 'covr') and (atom.has_child('data')))] - else: - return [] - - def add_image(self, image): - """embeds an Image object in this metadata""" - - if (not self.has_ilst_atom()): - self.add_ilst_atom() - - ilst_atom = self.ilst_atom() - - #filter out old cover image before adding new one - ilst_atom.leaf_atoms = filter( - lambda atom: not ((atom.name == 'covr') and - (atom.has_child('data'))), - ilst_atom) + [M4A_ILST_Leaf_Atom( - 'covr', - [M4A_ILST_COVR_Data_Atom.converted(image)])] - - def delete_image(self, image): - """deletes an Image object from this metadata""" - - if (self.has_ilst_atom()): - ilst_atom = self.ilst_atom() - - ilst_atom.leaf_atoms = filter( - lambda atom: not ((atom.name == 'covr') and - (atom.has_child('data')) and - (atom['data'].data == image.data)), - ilst_atom) - - @classmethod - def converted(cls, metadata): - """converts metadata from another class to this one, if necessary - - takes a MetaData-compatible object (or None) - and returns a new MetaData subclass with the data fields converted""" - - if (metadata is None): - return None - elif (isinstance(metadata, cls)): - return cls(metadata.version, - metadata.flags, - [leaf.copy() for leaf in metadata]) - - ilst_atoms = [M4A_ILST_Leaf_Atom( - cls.UNICODE_ATTRIB_TO_ILST[attrib], - [M4A_ILST_Unicode_Data_Atom( - 0, 1, value.encode('utf-8'))]) - for (attrib, value) in metadata.filled_fields() - if (attrib in cls.UNICODE_ATTRIB_TO_ILST)] - - if ((metadata.track_number != 0) or - (metadata.track_total != 0)): - ilst_atoms.append(M4A_ILST_Leaf_Atom( - 'trkn', - [M4A_ILST_TRKN_Data_Atom( - metadata.track_number, - metadata.track_total)])) - - if ((metadata.album_number != 0) or - (metadata.album_total != 0)): - ilst_atoms.append(M4A_ILST_Leaf_Atom( - 'disk', - [M4A_ILST_DISK_Data_Atom( - metadata.album_number, - metadata.album_total)])) - - if (len(metadata.front_covers()) > 0): - ilst_atoms.append(M4A_ILST_Leaf_Atom( - 'covr', - [M4A_ILST_COVR_Data_Atom.converted( - metadata.front_covers()[0])])) - - ilst_atoms.append(M4A_ILST_Leaf_Atom( - 'cpil', - [M4A_Leaf_Atom('data', - '\x00\x00\x00\x15\x00\x00\x00\x00\x01')])) - - return cls(0, 0, [M4A_HDLR_Atom(0, 0, '\x00\x00\x00\x00', - 'mdir', 'appl', 0, 0, '', 0), - M4A_Tree_Atom('ilst', ilst_atoms), - M4A_FREE_Atom(1024)]) - - @classmethod - def supports_images(self): - """returns True""" - - return True - - def clean(self, fixes_performed): - """returns a new MetaData object that's been cleaned of problems - - any fixes performed are appended to fixes_performed as Unicode""" - - def cleaned_atom(atom): - #numerical fields are stored in bytes, - #so no leading zeroes are possible - - #image fields don't store metadata, - #so no field problems are possible there either - - if (atom.name in self.UNICODE_ATTRIB_TO_ILST.values()): - text = atom['data'].data.decode('utf-8') - fix1 = text.rstrip() - if (fix1 != text): - fixes_performed.append( - _(u"removed trailing whitespace from %(field)s") % - {"field": atom.name.lstrip('\xa9').decode('ascii')}) - fix2 = fix1.lstrip() - if (fix2 != fix1): - fixes_performed.append( - _(u"removed leading whitespace from %(field)s") % - {"field": atom.name.lstrip('\xa9').decode('ascii')}) - if (len(fix2) > 0): - return M4A_ILST_Leaf_Atom( - atom.name, - [M4A_ILST_Unicode_Data_Atom(0, 1, - fix2.encode('utf-8'))]) - else: - fixes_performed.append( - _(u"removed empty field %(field)s") % - {"field": atom.name.lstrip('\xa9').decode('ascii')}) - return None - else: - return atom - - if (self.has_ilst_atom()): - return M4A_META_Atom( - self.version, - self.flags, - [M4A_Tree_Atom('ilst', - filter(lambda atom: atom is not None, - map(cleaned_atom, self.ilst_atom())))]) - else: - #if no ilst atom, return a copy of the meta atom as-is - return M4A_META_Atom( - self.version, - self.flags, - [M4A_Tree_Atom('ilst', - [atom.copy() for atom in self.ilst_atom()])]) - - -class M4A_ILST_Leaf_Atom(M4A_Tree_Atom): - def copy(self): - """returns a newly copied instance of this atom - and new instances of any sub-atoms it contains""" - - return M4A_ILST_Leaf_Atom(self.name, [leaf.copy() for leaf in self]) - - def __repr__(self): - return "M4A_ILST_Leaf_Atom(%s, %s)" % \ - (repr(self.name), repr(self.leaf_atoms)) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - return cls(name, - parse_sub_atoms( - data_size, reader, - {"data": {"\xa9alb": M4A_ILST_Unicode_Data_Atom, - "\xa9ART": M4A_ILST_Unicode_Data_Atom, - "\xa9cmt": M4A_ILST_Unicode_Data_Atom, - "cprt": M4A_ILST_Unicode_Data_Atom, - "\xa9day": M4A_ILST_Unicode_Data_Atom, - "\xa9grp": M4A_ILST_Unicode_Data_Atom, - "\xa9nam": M4A_ILST_Unicode_Data_Atom, - "\xa9too": M4A_ILST_Unicode_Data_Atom, - "\xa9wrt": M4A_ILST_Unicode_Data_Atom, - 'aART': M4A_ILST_Unicode_Data_Atom, - "covr": M4A_ILST_COVR_Data_Atom, - "trkn": M4A_ILST_TRKN_Data_Atom, - "disk": M4A_ILST_DISK_Data_Atom}.get( - name, M4A_Leaf_Atom)})) - - def __unicode__(self): - try: - return unicode(filter(lambda f: f.name == 'data', - self.leaf_atoms)[0]) - except IndexError: - return u"" - - def raw_info_lines(self): - """yields lines of human-readable information about the atom""" - - for leaf_atom in self.leaf_atoms: - name = self.name.replace("\xa9", " ").decode('ascii') - if (hasattr(leaf_atom, "raw_info")): - yield u"%s : %s" % (name, leaf_atom.raw_info()) - else: - yield u"%s : %s" % (name, repr(leaf_atom)) # FIXME - - def __int__(self): - try: - return int(filter(lambda f: f.name == 'data', - self.leaf_atoms)[0]) - except IndexError: - return 0 - - def total(self): - """returns the track/album total field - of this atom's "data" sub atom, if any - otherwise returns 0""" - - try: - return filter(lambda f: f.name == 'data', - self.leaf_atoms)[0].total() - except IndexError: - return 0 - - -class M4A_ILST_Unicode_Data_Atom(M4A_Leaf_Atom): - def __init__(self, type, flags, data): - self.name = "data" - self.type = type - self.flags = flags - self.data = data - - def copy(self): - """returns a newly copied instance of this atom - and new instances of any sub-atoms it contains""" - - return M4A_ILST_Unicode_Data_Atom(self.type, self.flags, self.data) - - def __repr__(self): - return "M4A_ILST_Unicode_Data_Atom(%s, %s, %s)" % \ - (repr(self.type), repr(self.flags), repr(self.data)) - - def __eq__(self, atom): - for attr in ["type", "flags", "data"]: - if ((not hasattr(atom, attr)) or - (getattr(self, attr) != getattr(atom, attr))): - return False - else: - return True - - def raw_info(self): - """returns a line of human-readable information about the atom""" - - return self.data.decode('utf-8') - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - assert(name == "data") - (type, flags) = reader.parse("8u 24u 32p") - return cls(type, flags, reader.read_bytes(data_size - 8)) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("8u 24u 32p %db" % (len(self.data)), - (self.type, self.flags, self.data)) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return 8 + len(self.data) - - def __unicode__(self): - return self.data.decode('utf-8') - - -class M4A_ILST_TRKN_Data_Atom(M4A_Leaf_Atom): - def __init__(self, track_number, track_total): - self.name = "data" - self.track_number = track_number - self.track_total = track_total - - def copy(self): - """returns a newly copied instance of this atom - and new instances of any sub-atoms it contains""" - - return M4A_ILST_TRKN_Data_Atom(self.track_number, self.track_total) - - def __repr__(self): - return "M4A_ILST_TRKN_Data_Atom(%d, %d)" % \ - (self.track_number, self.track_total) - - def __eq__(self, atom): - for attr in ["track_number", "track_total"]: - if ((not hasattr(atom, attr)) or - (getattr(self, attr) != getattr(atom, attr))): - return False - else: - return True - - def __unicode__(self): - if (self.track_total > 0): - return u"%d/%d" % (self.track_number, self.track_total) - else: - return unicode(self.track_number) - - def raw_info(self): - """returns a line of human-readable information about the atom""" - - return u"%d/%d" % (self.track_number, self.track_total) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - assert(name == "data") - return cls(*reader.parse("64p 16p 16u 16u 16p")) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("64p 16p 16u 16u 16p", - (self.track_number, self.track_total)) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return 16 - - def __int__(self): - return self.track_number - - def total(self): - """returns this atom's track_total field""" - - return self.track_total - - -class M4A_ILST_DISK_Data_Atom(M4A_Leaf_Atom): - def __init__(self, disk_number, disk_total): - self.name = "data" - self.disk_number = disk_number - self.disk_total = disk_total - - def copy(self): - """returns a newly copied instance of this atom - and new instances of any sub-atoms it contains""" - - return M4A_ILST_DISK_Data_Atom(self.disk_number, self.disk_total) - - def __repr__(self): - return "M4A_ILST_DISK_Data_Atom(%d, %d)" % \ - (self.disk_number, self.disk_total) - - def __eq__(self, atom): - for attr in ["disk_number", "disk_total"]: - if ((not hasattr(atom, attr)) or - (getattr(self, attr) != getattr(atom, attr))): - return False - else: - return True - - def __unicode__(self): - if (self.disk_total > 0): - return u"%d/%d" % (self.disk_number, self.disk_total) - else: - return unicode(self.disk_number) - - def raw_info(self): - """returns a line of human-readable information about the atom""" - - return u"%d/%d" % (self.disk_number, self.disk_total) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - assert(name == "data") - return cls(*reader.parse("64p 16p 16u 16u")) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("64p 16p 16u 16u", - (self.disk_number, self.disk_total)) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return 14 - - def __int__(self): - return self.disk_number - - def total(self): - """returns this atom's disk_total field""" - - return self.disk_total - - -class M4A_ILST_COVR_Data_Atom(Image, M4A_Leaf_Atom): - def __init__(self, version, flags, image_data): - self.version = version - self.flags = flags - self.name = "data" - - img = image_metrics(image_data) - Image.__init__(self, - data=image_data, - mime_type=img.mime_type, - width=img.width, - height=img.height, - color_depth=img.bits_per_pixel, - color_count=img.color_count, - description=u"", - type=0) - - def copy(self): - """returns a newly copied instance of this atom - and new instances of any sub-atoms it contains""" - - return M4A_ILST_COVR_Data_Atom(self.version, self.flags, self.data) - - def __repr__(self): - return "M4A_ILST_COVR_Data_Atom(%s, %s, ...)" % \ - (self.version, self.flags) - - def raw_info(self): - """returns a line of human-readable information about the atom""" - - if (len(self.data) > 20): - return (u"(%d bytes) " % (len(self.data)) + - u"".join([u"%2.2X" % (ord(b)) for b in self.data[0:20]]) + - u"\u2026") - else: - return (u"(%d bytes) " % (len(self.data)) + - u"".join([u"%2.2X" % (ord(b)) for b in self.data[0:20]])) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - assert(name == "data") - (version, flags) = reader.parse("8u 24u 32p") - return cls(version, flags, reader.read_bytes(data_size - 8)) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("8u 24u 32p %db" % (len(self.data)), - (self.version, self.flags, self.data)) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return 8 + len(self.data) - - @classmethod - def converted(cls, image): - """given an Image-compatible object, - returns a new M4A_ILST_COVR_Data_Atom object""" - - return cls(0, 0, image.data) - - -class M4A_HDLR_Atom(M4A_Leaf_Atom): - def __init__(self, version, flags, qt_type, qt_subtype, - qt_manufacturer, qt_reserved_flags, qt_reserved_flags_mask, - component_name, padding_size): - self.name = 'hdlr' - self.version = version - self.flags = flags - self.qt_type = qt_type - self.qt_subtype = qt_subtype - self.qt_manufacturer = qt_manufacturer - self.qt_reserved_flags = qt_reserved_flags - self.qt_reserved_flags_mask = qt_reserved_flags_mask - self.component_name = component_name - self.padding_size = padding_size - - def copy(self): - """returns a newly copied instance of this atom - and new instances of any sub-atoms it contains""" - - return M4A_HDLR_Atom(self.version, - self.flags, - self.qt_type, - self.qt_subtype, - self.qt_manufacturer, - self.qt_reserved_flags, - self.qt_reserved_flags_mask, - self.component_name, - self.padding_size) - - def __repr__(self): - return "M4A_HDLR_Atom(%s, %s, %s, %s, %s, %s, %s, %s, %d)" % \ - (self.version, self.flags, repr(self.qt_type), - repr(self.qt_subtype), repr(self.qt_manufacturer), - self.qt_reserved_flags, self.qt_reserved_flags_mask, - repr(self.component_name), self.padding_size) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - assert(name == 'hdlr') - (version, - flags, - qt_type, - qt_subtype, - qt_manufacturer, - qt_reserved_flags, - qt_reserved_flags_mask) = reader.parse( - "8u 24u 4b 4b 4b 32u 32u") - component_name = reader.read_bytes(reader.read(8)) - return cls(version, flags, qt_type, qt_subtype, - qt_manufacturer, qt_reserved_flags, - qt_reserved_flags_mask, component_name, - data_size - len(component_name) - 25) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.build("8u 24u 4b 4b 4b 32u 32u 8u %db %dP" % \ - (len(self.component_name), - self.padding_size), - (self.version, - self.flags, - self.qt_type, - self.qt_subtype, - self.qt_manufacturer, - self.qt_reserved_flags, - self.qt_reserved_flags_mask, - len(self.component_name), - self.component_name)) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return 25 + len(self.component_name) + self.padding_size - - -class M4A_FREE_Atom(M4A_Leaf_Atom): - def __init__(self, bytes): - self.name = "free" - self.bytes = bytes - - def copy(self): - """returns a newly copied instance of this atom - and new instances of any sub-atoms it contains""" - - return M4A_FREE_Atom(self.bytes) - - def __repr__(self): - return "M4A_FREE_Atom(%d)" % (self.bytes) - - @classmethod - def parse(cls, name, data_size, reader, parsers): - """given a 4 byte name, data_size int, BitstreamReader - and dict of {"atom":handler} sub-parsers, - returns an atom of this class""" - - assert(name == "free") - reader.skip_bytes(data_size) - return cls(data_size) - - def build(self, writer): - """writes the atom to the given BitstreamWriter - not including its 64-bit size / name header""" - - writer.write_bytes(chr(0) * self.bytes) - - def size(self): - """returns the atom's size - not including its 64-bit size / name header""" - - return self.bytes
View file
audiotools-2.18.tar.gz/audiotools/__mp3__.py
Deleted
@@ -1,798 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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, PCMConverter, - transfer_data, transfer_framelist_data, - subprocess, BIN, ApeTag, ReplayGain, - ignore_sigint, open_files, EncodingError, - DecodingError, PCMReaderError, ChannelMask, - __default_quality__, config, sys) -from __id3__ import * -import gettext - -gettext.install("audiotools", unicode=True) - - -####################### -#MP3 -####################### - - -class InvalidMP3(InvalidFile): - """raised by invalid files during MP3 initialization""" - - pass - - -class MP3Audio(AudioFile): - """an MP3 audio file""" - - SUFFIX = "mp3" - NAME = SUFFIX - DEFAULT_COMPRESSION = "2" - #0 is better quality/lower compression - #9 is worse quality/higher compression - COMPRESSION_MODES = ("0", "1", "2", "3", "4", "5", "6", - "medium", "standard", "extreme", "insane") - COMPRESSION_DESCRIPTIONS = {"0": _(u"high quality, larger files, " + - u"corresponds to lame's -V0"), - "6": _(u"lower quality, smaller files, " + - u"corresponds to lame's -V6"), - "medium": _(u"corresponds to lame's " + - u"--preset medium"), - "standard": _(u"corresponds to lame's " + - u"--preset standard"), - "extreme": _(u"corresponds to lame's " + - u"--preset extreme"), - "insane": _(u"corresponds to lame's " + - u"--preset insane")} - BINARIES = ("lame", "mpg123") - REPLAYGAIN_BINARIES = ("mp3gain", ) - - SAMPLE_RATE = ((11025, 12000, 8000, None), # MPEG-2.5 - (None, None, None, None), # reserved - (22050, 24000, 16000, None), # MPEG-2 - (44100, 48000, 32000, None)) # MPEG-1 - - BIT_RATE = ( - #MPEG-2.5 - ( - #reserved - (None,) * 16, - #layer III - (None, 8000, 16000, 24000, 32000, 40000, 48000, 56000, - 64000, 80000, 96000, 112000, 128000, 144000, 160000, None), - #layer II - (None, 8000, 16000, 24000, 32000, 40000, 48000, 56000, - 64000, 80000, 96000, 112000, 128000, 144000, 160000, None), - #layer I - (None, 32000, 48000, 56000, 64000, 80000, 96000, 112000, - 128000, 144000, 160000, 176000, 192000, 224000, 256000, None), - ), - #reserved - ((None,) * 16, ) * 4, - #MPEG-2 - ( - #reserved - (None,) * 16, - #layer III - (None, 8000, 16000, 24000, 32000, 40000, 48000, 56000, - 64000, 80000, 96000, 112000, 128000, 144000, 160000, None), - #layer II - (None, 8000, 16000, 24000, 32000, 40000, 48000, 56000, - 64000, 80000, 96000, 112000, 128000, 144000, 160000, None), - #layer I - (None, 32000, 48000, 56000, 64000, 80000, 96000, 112000, - 128000, 144000, 160000, 176000, 192000, 224000, 256000, None), - ), - #MPEG-1 - ( - #reserved - (None,) * 16, - #layer III - (None, 32000, 40000, 48000, 56000, 64000, 80000, 96000, - 112000, 128000, 160000, 192000, 224000, 256000, 320000, None), - #layer II - (None, 32000, 48000, 56000, 64000, 80000, 96000, 112000, - 128000, 160000, 192000, 224000, 256000, 320000, 384000, None), - #layer I - (None, 32000, 64000, 96000, 128000, 160000, 192000, 224000, - 256000, 288000, 320000, 352000, 384000, 416000, 448000, None) - ) - ) - - PCM_FRAMES_PER_MPEG_FRAME = (None, 1152, 1152, 384) - - def __init__(self, filename): - """filename is a plain string""" - - AudioFile.__init__(self, filename) - - from .bitstream import BitstreamReader - - try: - mp3file = open(filename, "rb") - except IOError, msg: - raise InvalidMP3(str(msg)) - - try: - try: - header_bytes = MP3Audio.__find_next_mp3_frame__(mp3file) - except IOError: - raise InvalidMP3(_(u"MP3 frame not found")) - - (frame_sync, - mpeg_id, - layer, - bit_rate, - sample_rate, - pad, - channels) = BitstreamReader(mp3file, 0).parse( - "11u 2u 2u 1p 4u 2u 1u 1p 2u 6p") - - self.__samplerate__ = self.SAMPLE_RATE[mpeg_id][sample_rate] - if (self.__samplerate__ is None): - raise InvalidMP3(_(u"Invalid sample rate")) - if (channels in (0, 1, 2)): - self.__channels__ = 2 - else: - self.__channels__ = 1 - - first_frame = mp3file.read(self.frame_length(mpeg_id, - layer, - bit_rate, - sample_rate, - pad) - 4) - - if ("Xing" in first_frame): - #pull length from Xing header, if present - self.__pcm_frames__ = ( - BitstreamReader( - cStringIO.StringIO( - first_frame[first_frame.index("Xing"): - first_frame.index("Xing") + 160]), - 0).parse("32p 32p 32u 32p 832p")[0] * - self.PCM_FRAMES_PER_MPEG_FRAME[layer]) - else: - #otherwise, bounce through file frames - reader = BitstreamReader(mp3file, 0) - self.__pcm_frames__ = 0 - - try: - (frame_sync, - mpeg_id, - layer, - bit_rate, - sample_rate, - pad) = reader.parse("11u 2u 2u 1p 4u 2u 1u 9p") - - while (frame_sync == 0x7FF): - self.__pcm_frames__ += \ - self.PCM_FRAMES_PER_MPEG_FRAME[layer] - - reader.skip_bytes(self.frame_length(mpeg_id, - layer, - bit_rate, - sample_rate, - pad) - 4) - - (frame_sync, - mpeg_id, - layer, - bit_rate, - sample_rate, - pad) = reader.parse("11u 2u 2u 1p 4u 2u 1u 9p") - except IOError: - pass - except ValueError, err: - raise InvalidMP3(unicode(err)) - finally: - mp3file.close() - - @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""" - - from .bitstream import BitstreamReader - - try: - skip_id3v2_comment(file) - - (frame_sync, - mpeg_id, - layer) = BitstreamReader(file, 0).parse("11u 2u 2u 1p") - - return ((frame_sync == 0x7FF) and - (mpeg_id in (0, 2, 3)) and - (layer in (1, 3))) - except IOError: - return False - - def lossless(self): - """returns False""" - - return False - - def to_pcm(self): - """returns a PCMReader object containing the track's PCM data""" - - BIG_ENDIAN = sys.byteorder == 'big' - - sub = subprocess.Popen([BIN["mpg123"], "-qs", self.filename], - stdout=subprocess.PIPE, - stderr=file(os.devnull, "a")) - - return PCMReader(sub.stdout, - sample_rate=self.sample_rate(), - channels=self.channels(), - bits_per_sample=16, - channel_mask=int(ChannelMask.from_channels( - self.channels())), - process=sub, - big_endian=BIG_ENDIAN) - - @classmethod - def __help_output__(cls): - import cStringIO - help_data = cStringIO.StringIO() - sub = subprocess.Popen([BIN['lame'], '--help'], - stdout=subprocess.PIPE) - transfer_data(sub.stdout.read, help_data.write) - sub.wait() - return help_data.getvalue() - - @classmethod - def __lame_version__(cls): - try: - version = re.findall(r'version \d+\.\d+', - cls.__help_output__())[0] - return tuple(map(int, version[len('version '):].split("."))) - except IndexError: - return (0, 0) - - @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 MP3Audio object""" - - import decimal - import bisect - - if ((compression is None) or - (compression not in cls.COMPRESSION_MODES)): - compression = __default_quality__(cls.NAME) - - if ((pcmreader.channels > 2) or - (pcmreader.sample_rate not in (32000, 48000, 44100))): - pcmreader = PCMConverter( - pcmreader, - sample_rate=[32000, 32000, 44100, 48000][bisect.bisect( - [32000, 44100, 48000], pcmreader.sample_rate)], - channels=min(pcmreader.channels, 2), - channel_mask=ChannelMask.from_channels( - min(pcmreader.channels, 2)), - bits_per_sample=16) - - if (pcmreader.channels > 1): - mode = "j" - else: - mode = "m" - - #FIXME - not sure if all LAME versions support "--little-endian" - # #LAME 3.98 (and up, presumably) handle the byteswap correctly - # #LAME 3.97 always uses -x - # if (BIG_ENDIAN or (cls.__lame_version__() < (3,98))): - # endian = ['-x'] - # else: - # endian = [] - - devnull = file(os.devnull, 'ab') - - if (str(compression) in map(str, range(0, 10))): - compression = ["-V" + str(compression)] - else: - compression = ["--preset", str(compression)] - - sub = subprocess.Popen([ - BIN['lame'], "--quiet", - "-r", - "-s", str(decimal.Decimal(pcmreader.sample_rate) / 1000), - "--bitwidth", str(pcmreader.bits_per_sample), - "--signed", "--little-endian", - "-m", mode] + compression + ["-", filename], - stdin=subprocess.PIPE, - stdout=devnull, - stderr=devnull, - preexec_fn=ignore_sigint) - - try: - transfer_framelist_data(pcmreader, sub.stdin.write) - except (IOError, ValueError), err: - sub.stdin.close() - sub.wait() - cls.__unlink__(filename) - raise EncodingError(str(err)) - except Exception, err: - sub.stdin.close() - sub.wait() - cls.__unlink__(filename) - raise err - - try: - pcmreader.close() - except DecodingError, err: - cls.__unlink__(filename) - raise EncodingError(err.error_message) - sub.stdin.close() - - devnull.close() - - if (sub.wait() == 0): - return MP3Audio(filename) - else: - cls.__unlink__(filename) - raise EncodingError(u"error encoding file with lame") - - def bits_per_sample(self): - """returns an integer number of bits-per-sample this track contains""" - - return 16 - - def channels(self): - """returns an integer number of channels this track contains""" - - return self.__channels__ - - def sample_rate(self): - """returns the rate of the track's audio as an integer number of Hz""" - - return self.__samplerate__ - - def get_metadata(self): - """returns a MetaData object, or None - - raises IOError if unable to read the file""" - - f = file(self.filename, "rb") - try: - if (f.read(3) != "ID3"): # no ID3v2 tag, try ID3v1 - try: - # no ID3v2, yes ID3v1 - return ID3v1Comment.parse(f) - except ValueError: - # no ID3v2, no ID3v1 - return None - else: - id3v2 = read_id3v2_comment(self.filename) - - try: - # yes IDv2, yes ID3v1 - return ID3CommentPair(id3v2, - ID3v1Comment.parse(f)) - except ValueError: - # yes ID3v2, no ID3v1 - return id3v2 - finally: - f.close() - - def update_metadata(self, metadata): - """takes this track's current MetaData object - as returned by get_metadata() and sets this track's metadata - with any fields updated in that object - - raises IOError if unable to write the file - """ - - if (metadata is None): - return - elif (not (isinstance(metadata, ID3v2Comment) or - isinstance(metadata, ID3CommentPair) or - isinstance(metadata, ID3v1Comment))): - raise ValueError(_(u"metadata not from audio file")) - - #get the original MP3 data - f = file(self.filename, "rb") - MP3Audio.__find_mp3_start__(f) - data_start = f.tell() - MP3Audio.__find_last_mp3_frame__(f) - data_end = f.tell() - f.seek(data_start, 0) - mp3_data = f.read(data_end - data_start) - f.close() - - from .bitstream import BitstreamWriter - - #write id3v2 + data + id3v1 to file - f = file(self.filename, "wb") - if (isinstance(metadata, ID3CommentPair)): - metadata.id3v2.build(BitstreamWriter(f, 0)) - f.write(mp3_data) - metadata.id3v1.build(f) - elif (isinstance(metadata, ID3v2Comment)): - metadata.build(BitstreamWriter(f, 0)) - f.write(mp3_data) - elif (isinstance(metadata, ID3v1Comment)): - f.write(mp3_data) - metadata.build(f) - f.close() - - def set_metadata(self, metadata): - """takes a MetaData object and sets this track's metadata - - this metadata includes track name, album name, and so on - raises IOError if unable to write the file""" - - if (metadata is None): - return - - if (not (isinstance(metadata, ID3v2Comment) or - isinstance(metadata, ID3CommentPair) or - isinstance(metadata, ID3v1Comment))): - DEFAULT_ID3V2 = "id3v2.3" - DEFAULT_ID3V1 = "id3v1.1" - - id3v2_class = {"id3v2.2": ID3v22Comment, - "id3v2.3": ID3v23Comment, - "id3v2.4": ID3v24Comment, - "none": None}.get(config.get_default("ID3", - "id3v2", - DEFAULT_ID3V2), - DEFAULT_ID3V2) - id3v1_class = {"id3v1.1": ID3v1Comment, - "none": None}.get(config.get_default("ID3", - "id3v1", - DEFAULT_ID3V1)) - if ((id3v2_class is not None) and (id3v1_class is not None)): - self.update_metadata( - ID3CommentPair.converted(metadata, - id3v2_class=id3v2_class, - id3v1_class=id3v1_class)) - elif (id3v2_class is not None): - self.update_metadata(id3v2_class.converted(metadata)) - elif (id3v1_class is not None): - self.update_metadata(id3v1_class.converted(metadata)) - else: - return - else: - self.update_metadata(metadata) - - def delete_metadata(self): - """deletes the track's MetaData - - this removes or unsets tags as necessary in order to remove all data - raises IOError if unable to write the file""" - - #get the original MP3 data - f = file(self.filename, "rb") - MP3Audio.__find_mp3_start__(f) - data_start = f.tell() - MP3Audio.__find_last_mp3_frame__(f) - data_end = f.tell() - f.seek(data_start, 0) - mp3_data = f.read(data_end - data_start) - f.close() - - #write data to file - f = file(self.filename, "wb") - f.write(mp3_data) - f.close() - - #places mp3file at the position of the next MP3 frame's start - @classmethod - def __find_next_mp3_frame__(cls, mp3file): - #if we're starting at an ID3v2 header, skip it to save a bunch of time - bytes_skipped = skip_id3v2_comment(mp3file) - - #then find the next mp3 frame - from .bitstream import BitstreamReader - - reader = BitstreamReader(mp3file, 0) - reader.mark() - try: - (sync, - mpeg_id, - layer_description) = reader.parse("11u 2u 2u 1p") - except IOError, err: - reader.unmark() - raise err - - while (not ((sync == 0x7FF) and - (mpeg_id in (0, 2, 3)) and - (layer_description in (1, 2, 3)))): - reader.rewind() - reader.unmark() - reader.skip(8) - bytes_skipped += 1 - reader.mark() - try: - (sync, - mpeg_id, - layer_description) = reader.parse("11u 2u 2u 1p") - except IOError, err: - reader.unmark() - raise err - else: - reader.rewind() - reader.unmark() - return bytes_skipped - - @classmethod - def __find_mp3_start__(cls, mp3file): - """places mp3file at the position of the MP3 file's start""" - - #if we're starting at an ID3v2 header, skip it to save a bunch of time - skip_id3v2_comment(mp3file) - - from .bitstream import BitstreamReader - - reader = BitstreamReader(mp3file, 0) - - #skip over any bytes that aren't a valid MPEG header - reader.mark() - (frame_sync, mpeg_id, layer) = reader.parse("11u 2u 2u 1p") - while (not ((frame_sync == 0x7FF) and - (mpeg_id in (0, 2, 3)) and - (layer in (1, 2, 3)))): - reader.rewind() - reader.unmark() - reader.skip(8) - reader.mark() - reader.rewind() - reader.unmark() - - @classmethod - def __find_last_mp3_frame__(cls, mp3file): - """places mp3file at the position of the last MP3 frame's end - - (either the last byte in the file or just before the ID3v1 tag) - this may not be strictly accurate if ReplayGain data is present, - since APEv2 tags came before the ID3v1 tag, - but we're not planning to change that tag anyway - """ - - mp3file.seek(-128, 2) - if (mp3file.read(3) == 'TAG'): - mp3file.seek(-128, 2) - return - else: - mp3file.seek(0, 2) - return - - def frame_length(self, mpeg_id, layer, bit_rate, sample_rate, pad): - """returns the total MP3 frame length in bytes - - the given arguments are the header's bit values - mpeg_id = 2 bits - layer = 2 bits - bit_rate = 4 bits - sample_rate = 2 bits - pad = 1 bit - """ - - sample_rate = self.SAMPLE_RATE[mpeg_id][sample_rate] - if (sample_rate is None): - raise ValueError(_(u"Invalid sample rate")) - bit_rate = self.BIT_RATE[mpeg_id][layer][bit_rate] - if (bit_rate is None): - raise ValueError(_(u"Invalid bit rate")) - if (layer == 3): # layer I - return (((12 * bit_rate) / sample_rate) + pad) * 4 - else: # layer II/III - return ((144 * bit_rate) / sample_rate) + pad - - def total_frames(self): - """returns the total PCM frames of the track as an integer""" - - return self.__pcm_frames__ - - @classmethod - def can_add_replay_gain(cls): - """returns True if we have the necessary binaries to add ReplayGain""" - - return BIN.can_execute(BIN['mp3gain']) - - @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 - """ - - track_names = [track.filename for track in - open_files(filenames) if - isinstance(track, cls)] - - if (progress is not None): - progress(0, 1) - - if ((len(track_names) > 0) and (BIN.can_execute(BIN['mp3gain']))): - devnull = file(os.devnull, 'ab') - sub = subprocess.Popen([BIN['mp3gain'], '-f', '-k', '-q', '-r'] + \ - track_names, - stdout=devnull, - stderr=devnull) - sub.wait() - - devnull.close() - - if (progress is not None): - progress(1, 1) - - 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""" - - from . import verify - try: - f = open(self.filename, 'rb') - except IOError, err: - raise InvalidMP3(str(err)) - - #MP3 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: - try: - #skip ID3v2/ID3v1 tags during verification - self.__find_mp3_start__(f) - start = f.tell() - self.__find_last_mp3_frame__(f) - end = f.tell() - f.seek(start, 0) - - verify.mpeg(f, start, end) - if (progress is not None): - progress(1, 1) - - return True - except (IOError, ValueError), err: - raise InvalidMP3(str(err)) - finally: - f.close() - - -####################### -#MP2 AUDIO -####################### - -class MP2Audio(MP3Audio): - """an MP2 audio file""" - - SUFFIX = "mp2" - NAME = SUFFIX - DEFAULT_COMPRESSION = str(192) - COMPRESSION_MODES = tuple(map(str, (64, 96, 112, 128, 160, 192, - 224, 256, 320, 384))) - COMPRESSION_DESCRIPTIONS = {"64": _(u"total bitrate of 64kbps"), - "384": _(u"total bitrate of 384kbps")} - BINARIES = ("lame", "twolame") - - @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""" - - from .bitstream import BitstreamReader - - try: - skip_id3v2_comment(file) - - (frame_sync, - mpeg_id, - layer) = BitstreamReader(file, 0).parse("11u 2u 2u 1p") - - return ((frame_sync == 0x7FF) and - (mpeg_id in (0, 2, 3)) and - (layer == 2)) - except IOError: - return False - - @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 MP2Audio object""" - - import decimal - import bisect - - if ((compression is None) or - (compression not in cls.COMPRESSION_MODES)): - compression = __default_quality__(cls.NAME) - - if ((pcmreader.channels > 2) or - (pcmreader.sample_rate not in (32000, 48000, 44100)) or - (pcmreader.bits_per_sample != 16)): - pcmreader = PCMConverter( - pcmreader, - sample_rate=[32000, 32000, 44100, 48000][bisect.bisect( - [32000, 44100, 48000], pcmreader.sample_rate)], - channels=min(pcmreader.channels, 2), - channel_mask=pcmreader.channel_mask, - bits_per_sample=16) - - devnull = file(os.devnull, 'ab') - - sub = subprocess.Popen([BIN['twolame'], "--quiet", - "-r", - "-s", str(pcmreader.sample_rate), - "--samplesize", str(pcmreader.bits_per_sample), - "-N", str(pcmreader.channels), - "-m", "a", - "-b", compression, - "-", - filename], - stdin=subprocess.PIPE, - stdout=devnull, - stderr=devnull, - preexec_fn=ignore_sigint) - - try: - transfer_framelist_data(pcmreader, sub.stdin.write) - except (ValueError, IOError), err: - sub.stdin.close() - sub.wait() - cls.__unlink__(filename) - raise EncodingError(str(err)) - except Exception, err: - sub.stdin.close() - sub.wait() - cls.__unlink__(filename) - raise err - - try: - pcmreader.close() - except DecodingError, err: - cls.__unlink__(filename) - raise EncodingError(err.error_message) - - sub.stdin.close() - devnull.close() - - if (sub.wait() == 0): - return MP2Audio(filename) - else: - cls.__unlink__(filename) - raise EncodingError(u"twolame exited with error")
View file
audiotools-2.18.tar.gz/audiotools/__musepack__.py
Deleted
@@ -1,332 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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, InvalidFormat, PCMReader, - PCMConverter, Con, subprocess, BIN, ApeTaggedAudio, - os, TempWaveReader, ignore_sigint, transfer_data, - EncodingError, DecodingError) -from __wav__ import WaveAudio -import gettext - -gettext.install("audiotools", unicode=True) - -####################### -#Musepack Audio -####################### - - -class NutValue(Con.Adapter): - """a construct for Musepack Nut-encoded integer fields""" - - def __init__(self, name): - Con.Adapter.__init__( - self, - Con.RepeatUntil(lambda obj, ctx: (obj & 0x80) == 0x00, - Con.UBInt8(name))) - - def _encode(self, value, context): - data = [value & 0x7F] - value = value >> 7 - - while (value != 0): - data.append(0x80 | (value & 0x7F)) - value = value >> 7 - - data.reverse() - return data - - def _decode(self, obj, context): - i = 0 - for x in obj: - i = (i << 7) | (x & 0x7F) - return i - - -class Musepack8StreamReader: - """an object for parsing Musepack SV8 streams""" - - NUT_HEADER = Con.Struct('nut_header', - Con.String('key', 2), - NutValue('length')) - - def __init__(self, stream): - """initialized with a file object""" - - self.stream = stream - - def packets(self): - """yields a set of (key, data) tuples""" - - import string - - UPPERCASE = frozenset(string.ascii_uppercase) - - while (True): - try: - frame_header = self.NUT_HEADER.parse_stream(self.stream) - except Con.core.FieldError: - break - - if (not frozenset(frame_header.key).issubset(UPPERCASE)): - break - - yield (frame_header.key, - self.stream.read(frame_header.length - - len(self.NUT_HEADER.build(frame_header)))) - - -class MusepackAudio(ApeTaggedAudio, AudioFile): - """a Musepack audio file""" - - SUFFIX = "mpc" - NAME = SUFFIX - DEFAULT_COMPRESSION = "standard" - COMPRESSION_MODES = ("thumb", "radio", "standard", "extreme", "insane") - - ###Musepack SV7### - #BINARIES = ('mppdec','mppenc') - - ###Musepack SV8### - BINARIES = ('mpcdec', 'mpcenc') - - MUSEPACK8_HEADER = Con.Struct('musepack8_header', - Con.UBInt32('crc32'), - Con.Byte('bitstream_version'), - NutValue('sample_count'), - NutValue('beginning_silence'), - Con.Embed(Con.BitStruct( - 'flags', - Con.Bits('sample_frequency', 3), - Con.Bits('max_used_bands', 5), - Con.Bits('channel_count', 4), - Con.Flag('mid_side_used'), - Con.Bits('audio_block_frames', 3)))) - - #not sure about some of the flag locations - #Musepack 7's header is very unusual - MUSEPACK7_HEADER = Con.Struct('musepack7_header', - Con.Const(Con.String('signature', 3), 'MP+'), - Con.Byte('version'), - Con.ULInt32('frame_count'), - Con.ULInt16('max_level'), - Con.Embed( - Con.BitStruct('flags', - Con.Bits('profile', 4), - Con.Bits('link', 2), - Con.Bits('sample_frequency', 2), - Con.Flag('intensity_stereo'), - Con.Flag('midside_stereo'), - Con.Bits('maxband', 6))), - Con.ULInt16('title_gain'), - Con.ULInt16('title_peak'), - Con.ULInt16('album_gain'), - Con.ULInt16('album_peak'), - Con.Embed( - Con.BitStruct('more_flags', - Con.Bits('unused1', 16), - Con.Bits('last_frame_length_low', 4), - Con.Flag('true_gapless'), - Con.Bits('unused2', 3), - Con.Flag('fast_seeking'), - Con.Bits('last_frame_length_high', 7))), - Con.Bytes('unknown', 3), - Con.Byte('encoder_version')) - - def __init__(self, filename): - """filename is a plain string""" - - AudioFile.__init__(self, filename) - f = file(filename, 'rb') - try: - if (f.read(4) == 'MPCK'): # a Musepack 8 stream - for (key, packet) in Musepack8StreamReader(f).packets(): - if (key == 'SH'): - header = MusepackAudio.MUSEPACK8_HEADER.parse(packet) - - self.__sample_rate__ = (44100, 48000, - 37800, 32000)[ - header.sample_frequency] - - self.__total_frames__ = header.sample_count - self.__channels__ = header.channel_count + 1 - - break - elif (key == 'SE'): - raise InvalidFile(_(u'No Musepack header found')) - - else: # a Musepack 7 stream - f.seek(0, 0) - - try: - header = MusepackAudio.MUSEPACK7_HEADER.parse_stream(f) - except Con.ConstError: - raise InvalidFile(_(u'Musepack signature incorrect')) - - header.last_frame_length = \ - (header.last_frame_length_high << 4) | \ - header.last_frame_length_low - - self.__sample_rate__ = (44100, 48000, - 37800, 32000)[header.sample_frequency] - self.__total_frames__ = (((header.frame_count - 1) * 1152) + - header.last_frame_length) - - self.__channels__ = 2 - finally: - f.close() - - @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 MusepackAudio object""" - - import tempfile - import bisect - - if (str(compression) not in cls.COMPRESSION_MODES): - compression = cls.DEFAULT_COMPRESSION - - if ((pcmreader.channels > 2) or - (pcmreader.sample_rate not in (44100, 48000, 37800, 32000)) or - (pcmreader.bits_per_sample != 16)): - pcmreader = PCMConverter( - pcmreader, - sample_rate=[32000, 32000, 37800, 44100, 48000][bisect.bisect( - [32000, 37800, 44100, 48000], pcmreader.sample_rate)], - channels=min(pcmreader.channels, 2), - bits_per_sample=16) - - f = tempfile.NamedTemporaryFile(suffix=".wav") - w = WaveAudio.from_pcm(f.name, pcmreader) - try: - return cls.__from_wave__(filename, f.name, compression) - finally: - del(w) - f.close() - - #While Musepack needs to pipe things through WAVE, - #not all WAVEs are acceptable. - #Use the *_pcm() methods first. - def __to_wave__(self, wave_filename): - devnull = file(os.devnull, "wb") - try: - sub = subprocess.Popen([BIN['mpcdec'], - self.filename, - wave_filename], - stdout=devnull, - stderr=devnull) - - #FIXME - small files (~5 seconds) result in an error by mpcdec, - #even if they decode correctly. - #Not much we can do except try to workaround its bugs. - if (sub.wait() not in [0, 250]): - raise DecodingError() - finally: - devnull.close() - - @classmethod - def __from_wave__(cls, filename, wave_filename, compression=None): - if (str(compression) not in cls.COMPRESSION_MODES): - compression = cls.DEFAULT_COMPRESSION - - #mppenc requires files to end with .mpc for some reason - if (not filename.endswith(".mpc")): - import tempfile - actual_filename = filename - tempfile = tempfile.NamedTemporaryFile(suffix=".mpc") - filename = tempfile.name - else: - actual_filename = tempfile = None - - ###Musepack SV7### - #sub = subprocess.Popen([BIN['mppenc'], - # "--silent", - # "--overwrite", - # "--%s" % (compression), - # wave_filename, - # filename], - # preexec_fn=ignore_sigint) - - ###Musepack SV8### - sub = subprocess.Popen([BIN['mpcenc'], - "--silent", - "--overwrite", - "--%s" % (compression), - wave_filename, - filename]) - - if (sub.wait() == 0): - if (tempfile is not None): - filename = actual_filename - f = file(filename, 'wb') - tempfile.seek(0, 0) - transfer_data(tempfile.read, f.write) - f.close() - tempfile.close() - - return MusepackAudio(filename) - else: - if (tempfile is not None): - tempfile.close() - raise EncodingError(u"error encoding file with mpcenc") - - @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(4) - - ###Musepack SV7### - #return header == 'MP+\x07' - - ###Musepack SV8### - return (header == 'MP+\x07') or (header == 'MPCK') - - 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 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 16 - - def lossless(self): - """returns False""" - - return False
View file
audiotools-2.18.tar.gz/audiotools/__musicbrainz__.py
Deleted
@@ -1,662 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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 - - -##### DEPRECATION WARNING ##### -#This whole module will go away very soon. -# -#Use the audiotools.musicbrainz.perform_lookup() function -#which takes a CD's table-of-contents -#and performs the entire MusicBrainz lookup and parsing process -#rather than use these functions and classes -#to parse and edit those files directly. - - -from audiotools import (MetaData, AlbumMetaData, AlbumMetaDataFile, - MetaDataFileException, - __most_numerous__, DummyAudioFile, sys) -import urllib -import gettext - -gettext.install("audiotools", unicode=True) - - -#DEPRECATED - this function will soon be removed -def get_xml_nodes(parent, child_tag): - """a helper routine for returning all children with the given XML tag""" - - return [node for node in parent.childNodes - if (hasattr(node, "tagName") and - (node.tagName == child_tag))] - - -#DEPRECATED - this function will soon be removed -def walk_xml_tree(parent, *child_tags): - """a helper routine for walking through several children""" - - if (len(child_tags) == 0): - return parent - else: - base_tag = child_tags[0] - remaining_tags = child_tags[1:] - for node in parent.childNodes: - if (hasattr(node, "tagName") and - (node.tagName == base_tag)): - return walk_xml_tree(node, *remaining_tags) - else: - return None - - -#DEPRECATED - this function will soon be removed -def walk_xml_tree_build(dom, parent, *child_tags): - - if (len(child_tags) == 0): - return parent - else: - base_tag = child_tags[0] - remaining_tags = child_tags[1:] - for node in parent.childNodes: - if (hasattr(node, "tagName") and - (node.tagName == base_tag)): - return walk_xml_tree_build(dom, node, *remaining_tags) - else: - new_child = dom.createElement(base_tag) - parent.appendChild(new_child) - return walk_xml_tree_build(dom, new_child, *remaining_tags) - - -#DEPRECATED - this function will soon be removed -def get_xml_text_node(parent, child_tag): - """a helper routine for returning the first text child XML node""" - - try: - return get_xml_nodes(parent, child_tag)[0].childNodes[0].data.strip() - except IndexError: - return u'' - - -#DEPRECATED - this function will soon be removed -def reorder_xml_children(parent, child_order): - """given an XML element with childNodes, reorders them to child_order - - child_order should be a list of unicode tag strings - """ - - if (parent.childNodes is None): - return - - child_tags = {} - leftovers = [] - for child in parent.childNodes: - if (hasattr(child, "tagName")): - child_tags.setdefault(child.tagName, []).append(child) - else: - leftovers.append(child) - - #remove all the old childen from parent - for child in parent.childNodes: - parent.removeChild(child) - - #re-add the childen in child_order - for tagName in child_order: - if (tagName in child_tags): - for child in child_tags[tagName]: - parent.appendChild(child) - del(child_tags[tagName]) - - #re-add any leftover children tags or non-tags - for child_tags in child_tags.values(): - for child in child_tags: - parent.appendChild(child) - - for child in leftovers: - parent.appendChild(child) - - -#DEPRECATED - this class will soon be removed -class MBDiscID: - """a MusicBrainz disc ID""" - - def __init__(self, tracks=[], offsets=None, length=None, lead_in=150, - first_track_number=None, last_track_number=None, - lead_out_track_offset=None): - """fields are as follows: - - tracks - a list of track lengths in CD frames - offsets - a list of track offsets in CD frames - length - the length of the entire disc in CD frames - lead_in - the location of the first track on the CD, in frames - - first_track_number, last_track_number and lead_out_track_offset - are integer values - - all fields are optional - one will presumably fill them with data later in that event - """ - - self.tracks = tracks - self.__offsets__ = offsets - self.__length__ = length - self.__lead_in__ = lead_in - self.first_track_number = first_track_number - self.last_track_number = last_track_number - self.lead_out_track_offset = lead_out_track_offset - - @classmethod - def from_cdda(cls, cdda): - """given a CDDA object, returns a populated MBDiscID - - may raise ValueError if there are no audio tracks on the CD""" - - tracks = list(cdda) - if (len(tracks) < 1): - raise ValueError(_(u"no audio tracks in CDDA object")) - - return cls( - tracks=[t.length() for t in tracks], - offsets=[t.offset() for t in tracks], - length=cdda.length(), - lead_in=tracks[0].offset(), - lead_out_track_offset=cdda.last_sector() + 150 + 1) - - def offsets(self): - """returns a list of calculated offset integers, from track lengths""" - - if (self.__offsets__ is None): - offsets = [self.__lead_in__] - - for track in self.tracks[0:-1]: - offsets.append(track + offsets[-1]) - - return offsets - else: - return self.__offsets__ - - def __repr__(self): - return ("MBDiscID(tracks=%s,offsets=%s,length=%s,lead_in=%s," + - "first_track_number=%s,last_track_number=%s," + - "lead_out_track_offset=%s)") % \ - (repr(self.tracks), - repr(self.__offsets__), - repr(self.__length__), - repr(self.__lead_in__), - repr(self.first_track_number), - repr(self.last_track_number), - repr(self.lead_out_track_offset)) - - #returns a MusicBrainz DiscID value as a string - def __str__(self): - from hashlib import sha1 - - if (self.lead_out_track_offset is None): - if (self.__length__ is None): - lead_out_track_offset = sum(self.tracks) + self.__lead_in__ - else: - lead_out_track_offset = self.__length__ + self.__lead_in__ - else: - lead_out_track_offset = self.lead_out_track_offset - - if (self.first_track_number is None): - first_track_number = 1 - else: - first_track_number = self.first_track_number - - if (self.last_track_number is None): - last_track_number = len(self.tracks) - else: - last_track_number = self.last_track_number - - digest = sha1("%02X%02X%s" % \ - (first_track_number, - last_track_number, - "".join(["%08X" % (i) for i in - [lead_out_track_offset] + - self.offsets() + - ([0] * (99 - len(self.offsets())))]))) - - return "".join([{'=': '-', '+': '.', '/': '_'}.get(c, c) for c in - digest.digest().encode('base64').rstrip('\n')]) - - def toxml(self, output): - """writes an XML file to the output file object""" - - output.write(MusicBrainzReleaseXML.from_tracks( - [DummyAudioFile(length, None, i + 1) - for (i, length) in enumerate(self.tracks)]).to_string()) - - -#DEPRECATED - this class will soon be removed -class MusicBrainz: - """a class for performing queries on a MusicBrainz or compatible server""" - - def __init__(self, server, port, messenger): - self.server = server - self.port = port - self.connection = None - self.messenger = messenger - - def connect(self): - """performs the initial connection""" - - import httplib - - self.connection = httplib.HTTPConnection(self.server, self.port) - - def close(self): - """closes an open connection""" - - if (self.connection is not None): - self.connection.close() - - def read_data(self, disc_id, output): - """returns a (matches,dom) tuple from a MBDiscID object - - matches is an integer - and dom is a minidom Document object or None""" - - from xml.dom.minidom import parseString - from xml.parsers.expat import ExpatError - - self.connection.request( - "GET", - "%s?%s" % ("/ws/1/release", - urllib.urlencode({"type": "xml", - "discid": str(disc_id)}))) - - response = self.connection.getresponse() - #FIXME - check for errors in the HTTP response - - data = response.read() - - try: - dom = parseString(data) - return (len(dom.getElementsByTagName(u'release')), dom) - except ExpatError: - return (0, None) - - -#DEPRECATED - this class will soon be removed -class MBXMLException(MetaDataFileException): - """raised if MusicBrainzReleaseXML.read() encounters an error""" - - def __unicode__(self): - return _(u"Invalid MusicBrainz XML file") - - -#DEPRECATED - this class will soon be removed -class MusicBrainzReleaseXML(AlbumMetaDataFile): - """an XML file as returned by MusicBrainz""" - - TAG_ORDER = {u"release": [u"title", - u"text-representation", - u"asin", - u"artist", - u"release-group", - u"release-event-list", - u"disc-list", - u"puid-list", - u"track-list", - u"relation-list", - u"tag-list", - u"user-tag-list", - u"rating", - u"user-rating"], - u"artist": [u"name", - u"sort-name", - u"disambiguation", - u"life-span", - u"alias-list", - u"release-list", - u"release-group-list", - u"relation-list", - u"tag-list", - u"user-tag-list", - u"rating"], - u"track": [u"title", - u"duration", - u"isrc-list", - u"artist", - u"release-list", - u"puid-list", - u"relation-list", - u"tag-list", - u"user-tag-list", - u"rating", - u"user-rating"]} - - def __init__(self, dom): - self.dom = dom - - def __getattr__(self, key): - if (key == 'album_name'): - try: - return get_xml_text_node( - walk_xml_tree(self.dom, - u'metadata', u'release-list', u'release'), - u'title') - except AttributeError: - return u"" - elif (key == 'artist_name'): - try: - return get_xml_text_node( - walk_xml_tree(self.dom, - u'metadata', u'release-list', u'release', - u'artist'), - u'name') - except AttributeError: - return u"" - elif (key == 'year'): - try: - return walk_xml_tree( - self.dom, u'metadata', u'release-list', u'release', - u'release-event-list', - u'event').getAttribute('date')[0:4] - except (IndexError, AttributeError): - return u"" - elif (key == 'catalog'): - try: - return walk_xml_tree( - self.dom, u'metadata', u'release-list', u'release', - u'release-event-list', - u'event').getAttribute('catalog-number') - except (IndexError, AttributeError): - return u"" - elif (key == 'extra'): - return u"" - else: - try: - return self.__dict__[key] - except KeyError: - raise AttributeError(key) - - def __setattr__(self, key, value): - #FIXME - create nodes if they don't exist - if (key == 'album_name'): - title = walk_xml_tree(self.dom, u'metadata', u'release-list', - u'release', u'title') - if (len(title.childNodes) > 0): - title.replaceChild(self.dom.createTextNode(value), - title.firstChild) - else: - title.appendChild(self.dom.createTextNode(value)) - elif (key == 'artist_name'): - name = walk_xml_tree(self.dom, u'metadata', u'release-list', - u'release', u'artist', u'name') - if (len(name.childNodes) > 0): - name.replaceChild(self.dom.createTextNode(value), - name.firstChild) - else: - name.appendChild(self.dom.createTextNode(value)) - elif (key == 'year'): - walk_xml_tree_build(self.dom, self.dom, - u'metadata', u'release-list', - u'release', u'release-event-list', - u'event').setAttribute(u"date", value) - elif (key == 'catalog'): - walk_xml_tree_build(self.dom, self.dom, - u'metadata', u'release-list', - u'release', u'release-event-list', - u'event').setAttribute(u"catalog-number", - value) - elif (key == 'extra'): - pass - else: - self.__dict__[key] = value - - def __len__(self): - return len(self.dom.getElementsByTagName(u'track')) - - def to_string(self): - for (tag, order) in MusicBrainzReleaseXML.TAG_ORDER.items(): - for parent in self.dom.getElementsByTagName(tag): - reorder_xml_children(parent, order) - - return self.dom.toxml(encoding='utf-8') - - @classmethod - def from_string(cls, string): - from xml.dom.minidom import parseString - from xml.parsers.expat import ExpatError - - try: - return cls(parseString(string)) - except ExpatError: - raise MBXMLException("") - - def get_track(self, index): - track_node = self.dom.getElementsByTagName(u'track')[index] - track_name = get_xml_text_node(track_node, u'title') - artist_node = walk_xml_tree(track_node, u'artist') - if (artist_node is not None): - artist_name = get_xml_text_node(artist_node, u'name') - if (len(artist_name) == 0): - artist_name = u"" - else: - artist_name = u"" - return (track_name, artist_name, u"") - - def set_track(self, index, name, artist, extra): - track_node = self.dom.getElementsByTagName(u'track')[index] - title = walk_xml_tree(track_node, 'title') - if (len(title.childNodes) > 0): - title.replaceChild(self.dom.createTextNode(name), - title.firstChild) - else: - title.appendChild(self.dom.createTextNode(name)) - if (len(artist) > 0): - artist_node = walk_xml_tree_build(self.dom, - track_node, - u'artist', u'name') - if (artist_node.hasChildNodes()): - artist_node.replaceChild(self.dom.createTextNode(artist), - artist_node.firstChild) - else: - artist_node.appendChild(self.dom.createTextNode(artist)) - - @classmethod - def from_tracks(cls, tracks): - """returns a MusicBrainzReleaseXML from a list of AudioFile objects - - these objects are presumably from the same album - if not, these heuristics may generate something unexpected - """ - - from xml.dom.minidom import parseString - - def make_text_node(document, tagname, text): - node = document.createElement(tagname) - node.appendChild(document.createTextNode(text)) - return node - - tracks.sort(lambda x, y: cmp(x.track_number(), y.track_number())) - - #our base DOM to start with - dom = parseString('<?xml version="1.0" encoding="UTF-8"?>' + - '<metadata xmlns="http://musicbrainz.org/' + - 'ns/mmd-1.0#" xmlns:ext="http://musicbrainz.org/' + - 'ns/ext-1.0#"></metadata>') - - release = dom.createElement(u'release') - - track_metadata = [t.get_metadata() for t in tracks - if (t.get_metadata() is not None)] - - #add album title - release.appendChild(make_text_node( - dom, u'title', unicode(__most_numerous__( - [m.album_name for m in track_metadata])))) - - #add album artist - if (len(set([m.artist_name for m in track_metadata])) < - len(track_metadata)): - artist = dom.createElement(u'artist') - album_artist = unicode(__most_numerous__( - [m.artist_name for m in track_metadata])) - artist.appendChild(make_text_node(dom, u'name', album_artist)) - release.appendChild(artist) - else: - album_artist = u'' # all track artist names differ - artist = dom.createElement(u'artist') - artist.appendChild(make_text_node(dom, u'name', album_artist)) - release.appendChild(artist) - - #add release info (catalog number, release date, media, etc.) - event_list = dom.createElement(u'release-event-list') - event = dom.createElement(u'event') - - year = unicode(__most_numerous__( - [m.year for m in track_metadata])) - if (year != u""): - event.setAttribute(u'date', year) - - catalog_number = unicode(__most_numerous__( - [m.catalog for m in track_metadata])) - if (catalog_number != u""): - event.setAttribute(u'catalog-number', catalog_number) - - media = unicode(__most_numerous__( - [m.media for m in track_metadata])) - if (media != u""): - event.setAttribute(u'format', media) - - event_list.appendChild(event) - release.appendChild(event_list) - - #add tracks - track_list = dom.createElement(u'track-list') - - for track in tracks: - node = dom.createElement(u'track') - track_metadata = track.get_metadata() - if (track_metadata is not None): - node.appendChild(make_text_node( - dom, u'title', track_metadata.track_name)) - else: - node.appendChild(make_text_node( - dom, u'title', u'')) - - node.appendChild(make_text_node( - dom, u'duration', - unicode((track.total_frames() * 1000) / - track.sample_rate()))) - - if (track_metadata is not None): - #add track artist, if different from album artist - if (track_metadata.artist_name != album_artist): - artist = dom.createElement(u'artist') - artist.appendChild(make_text_node( - dom, u'name', track_metadata.artist_name)) - node.appendChild(artist) - - track_list.appendChild(node) - - release.appendChild(track_list) - - release_list = dom.createElement(u'release-list') - release_list.appendChild(release) - dom.getElementsByTagName(u'metadata')[0].appendChild(release_list) - - return cls(dom) - - -#takes a Document containing multiple <release> tags -#and a Messenger object to query for output -#returns a modified Document containing only one <release> - -#DEPRECATED - this function will soon be removed -def __select_match__(dom, messenger): - messenger.info(_(u"Please Select the Closest Match:")) - matches = dom.getElementsByTagName(u'release') - selected = 0 - while ((selected < 1) or (selected > len(matches))): - for i in range(len(matches)): - messenger.info(_(u"%(choice)s) %(name)s") % \ - {"choice": i + 1, - "name": get_xml_text_node(matches[i], - u'title')}) - try: - messenger.partial_info(_(u"Your Selection [1-%s]:") % \ - (len(matches))) - selected = int(sys.stdin.readline().strip()) - except ValueError: - selected = 0 - - for (i, release) in enumerate(dom.getElementsByTagName(u'release')): - if (i != (selected - 1)): - release.parentNode.removeChild(release) - - return dom - - -#takes a Document containing multiple <release> tags -#and a default selection integer -#returns a modified Document containing only one <release> -#DEPRECATED - this function will soon be removed -def __select_default_match__(dom, selection): - for (i, release) in enumerate(dom.getElementsByTagName(u'release')): - if (i != selection): - release.parentNode.removeChild(release) - - return dom - - -#DEPRECATED - this function will soon be removed -def get_mbxml(disc_id, output, musicbrainz_server, musicbrainz_port, - messenger, default_selection=None): - """runs through the entire MusicBrainz querying sequence - - fields are as follows: - disc_id - an MBDiscID object - output - an open file object for writing - musicbrainz_server - a server name string - musicbrainz_port - a server port int - messenger - a Messenger object - default_selection - if given, the default match to choose - """ - - mb = MusicBrainz(musicbrainz_server, musicbrainz_port, messenger) - - mb.connect() - messenger.info( - _(u"Sending Disc ID \"%(disc_id)s\" to server \"%(server)s\"") % \ - {"disc_id": str(disc_id).decode('ascii'), - "server": musicbrainz_server.decode('ascii', 'replace')}) - - (matches, dom) = mb.read_data(disc_id, output) - mb.close() - - if (matches == 1): - messenger.info(_(u"1 match found")) - else: - messenger.info(_(u"%s matches found") % (matches)) - - if (matches > 1): - if (default_selection is None): - output.write(__select_match__( - dom, messenger).toxml(encoding='utf-8')) - else: - output.write(__select_default_match__( - dom, default_selection).toxml(encoding='utf-8')) - - output.flush() - elif (matches == 1): - output.write(dom.toxml(encoding='utf-8')) - output.flush() - else: - return matches
View file
audiotools-2.18.tar.gz/audiotools/__ogg__.py
Deleted
@@ -1,334 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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 - - -def read_ogg_packets(reader): - """given a BitstreamReader - yields BitstreamReader substream objects - for each packet within the Ogg stream""" - - from .bitstream import Substream - - header_type = 0 - packet = Substream(1) - - while (not (header_type & 0x4)): - (magic_number, - version, - header_type, - granule_position, - serial_number, - page_sequence_number, - checksum, - segment_count) = reader.parse("4b 8u 8u 64S 32u 32u 32u 8u") - for segment_length in [reader.read(8) for i in xrange(segment_count)]: - reader.substream_append(packet, segment_length) - if (segment_length != 255): - yield packet - packet = Substream(1) - - -def read_ogg_packets_data(reader): - """given a BitstreamReader - yields binary strings - for each packet within the Ogg stream""" - - header_type = 0 - packet = [] - - while (not (header_type & 0x4)): - (magic_number, - version, - header_type, - granule_position, - serial_number, - page_sequence_number, - checksum, - segment_count) = reader.parse("4b 8u 8u 64S 32u 32u 32u 8u") - for segment_length in [reader.read(8) for i in xrange(segment_count)]: - packet.append(reader.read_bytes(segment_length)) - if (segment_length != 255): - yield "".join(packet) - packet = [] - - -class OggChecksum: - """calculates the checksum of Ogg pages - the final checksum may be determined by int(ogg_checksum_instance)""" - - CRC_LOOKUP = (0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, - 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005, - 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, - 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, - 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, - 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75, - 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, - 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd, - 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, - 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, - 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81, - 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d, - 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, - 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95, - 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, - 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, - 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae, - 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072, - 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, - 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca, - 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, - 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, - 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066, - 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba, - 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, - 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692, - 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, - 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, - 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e, - 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, - 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, - 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a, - 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, - 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, - 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f, - 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53, - 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, - 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b, - 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, - 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, - 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7, - 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b, - 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, - 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3, - 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, - 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, - 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f, - 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3, - 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, - 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c, - 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, - 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, - 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30, - 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec, - 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, - 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654, - 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, - 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, - 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18, - 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4, - 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, - 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c, - 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, - 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4) - - def __init__(self): - self.checksum = 0 - - def reset(self): - """clears the accumulated checksum so the object may be reused""" - - self.checksum = 0 - - def update(self, byte): - """given a byte integer, updates the running checksum""" - - self.checksum = (((self.checksum << 8) ^ - self.CRC_LOOKUP[((self.checksum >> 24) & 0xFF) ^ - byte]) & 0xFFFFFFFF) - - def __int__(self): - return self.checksum - - -class OggStreamReader: - def __init__(self, reader): - """reader is a BitstreamReader object""" - - self.reader = reader - - #try to grab a few useful bits of info - self.reader.mark() - (magic_number, - version, - self.serial_number) = self.reader.parse("4b 8u 8p 64p 32u") - if (magic_number != "OggS"): - raise ValueError(_(u"invalid Ogg serial number")) - elif (version != 0): - raise ValueError(_(u"invalid Ogg version")) - - self.reader.rewind() - self.reader.unmark() - self.checksum = OggChecksum() - self.reader.add_callback(self.checksum.update) - - def read_page(self): - """returns a tuple of (granule_position, - segments, - continuation, - first_page, - last_page) - - raises ValueError if the page checksum is invalid""" - - self.checksum.reset() - - #grab all the header data up to page checksum - (magic_number, - version, - continuation, - first_page, - last_page, - granule_position, - serial_number, - page_sequence_number) = self.reader.parse( - "4b 8u 1u 1u 1u 5p 64S 32u 32u") - - #update checksum with placeholder value - old_callback = self.reader.pop_callback() - old_callback(0) - old_callback(0) - old_callback(0) - old_callback(0) - checksum = self.reader.read(32) - self.reader.add_callback(old_callback) - - #grab all the segment data - segments = self.reader.parse( - "".join(["%db" % (segment_length) for segment_length in - self.reader.parse("8u" * self.reader.read(8))])) - - #verify calculated checksum against found checksum - if (int(self.checksum) != checksum): - raise ValueError(_(u"Ogg page checksum mismatch")) - else: - return (granule_position, segments, - continuation, first_page, last_page) - - def pages(self): - """yields a tuple of (granule_position, - segments, - continuation, - first_page, - last_page) - - for each page in the stream - raises ValueError if a page checksum is invalid""" - - while (True): - page = self.read_page() - yield page - if (page[-1]): - break - - -class OggStreamWriter: - def __init__(self, writer, serial_number): - """writer is a BitstreamWriter - - serial_number is a signed integer""" - - from .bitstream import BitstreamRecorder - - self.writer = writer - self.serial_number = serial_number - self.sequence_number = 0 - self.temp = BitstreamRecorder(1) - self.checksum = OggChecksum() - self.temp.add_callback(self.checksum.update) - - def packet_to_segments(self, packet): - """yields a segment string per segment of the packet string - - where each segment is a string up to 255 bytes long - - the final segment will be 0 bytes if the packet - is equally divisible by 255 bytes""" - - use_pad = len(packet) % 255 == 0 - - while (len(packet) > 0): - yield packet[0:255] - packet = packet[255:] - - if (use_pad): - yield "" - - def segments_to_pages(self, segments): - """given an iterator of segment strings, - - yields a list of strings where each list is up to 255 segments - """ - - page = [] - for segment in segments: - page.append(segment) - if (len(page) == 255): - yield page - page = [] - - if (len(page) > 0): - yield page - - def write_page(self, granule_position, segments, - continuation, first_page, last_page): - """granule_position is a signed long - - segments is a list of strings containing binary data - - continuation, first_page and last_page indicate this page's - position in the Ogg stream - """ - - from .bitstream import format_size - - assert(len(segments) < 0x100) - assert(max(map(len, segments)) < 0x100) - - self.temp.reset() - self.checksum.reset() - - #write a checksummed header to temp - self.temp.build("4b 8u 1u 1u 1u 5p 64S 32u 32u", - ("OggS", 0, continuation, first_page, last_page, - granule_position, self.serial_number, - self.sequence_number)) - self.checksum.update(0) - self.checksum.update(0) - self.checksum.update(0) - self.checksum.update(0) - - #write the segment lengths and segment data to temp - self.temp.write(8, len(segments)) - self.temp.build("8u" * len(segments), map(len, segments)) - self.temp.build("".join(["%db" % (len(segment)) - for segment in segments]), segments) - - #transfer everything from the page start to the page checksum - #from temp to the final stream - self.temp.split(self.writer, self.temp, - format_size("4b 8u 8u 64S 32u 32u") / 8) - - #write the calculated page checksum to the final stream - self.writer.write(32, int(self.checksum)) - - #transfer everything past the page checksum from temp to final - self.temp.copy(self.writer) - - #increment sequence number - self.sequence_number += 1
View file
audiotools-2.18.tar.gz/audiotools/__shn__.py
Deleted
@@ -1,542 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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, ChannelMask, PCMReader, - transfer_framelist_data, WaveAudio, - AiffAudio, cStringIO, EncodingError, - UnsupportedBitsPerSample, InvalidFile, - PCMReaderError, - WaveContainer, AiffContainer, to_pcm_progress, - parse_fmt, parse_comm) - -import os.path - - -class InvalidShorten(InvalidFile): - pass - - -class ShortenAudio(WaveContainer, AiffContainer): - """a Shorten audio file""" - - SUFFIX = "shn" - NAME = SUFFIX - - def __init__(self, filename): - """filename is a plain string""" - - from audiotools.bitstream import BitstreamReader - - AudioFile.__init__(self, filename) - try: - f = open(filename, 'rb') - except IOError, msg: - raise InvalidShorten(str(msg)) - - reader = BitstreamReader(f, 0) - try: - if (reader.parse("4b 8u") != ["ajkg", 2]): - raise InvalidShorten("invalid Shorten header") - except IOError: - raise InvalidShorten("invalid Shorten header") - - def read_unsigned(r, c): - MSB = r.unary(1) - LSB = r.read(c) - return MSB * 2 ** c + LSB - - def read_long(r): - return read_unsigned(r, read_unsigned(r, 2)) - - #populate channels and bits_per_sample from Shorten header - (file_type, - self.__channels__, - block_length, - max_LPC, - number_of_means, - bytes_to_skip) = [read_long(reader) for i in xrange(6)] - - if ((1 <= file_type) and (file_type <= 2)): - self.__bits_per_sample__ = 8 - elif ((3 <= file_type) and (file_type <= 6)): - self.__bits_per_sample__ = 16 - else: - raise InvalidShorten("unsupported Shorten file type") - - #setup some default dummy metadata - self.__sample_rate__ = 44100 - if (self.__channels__ == 1): - self.__channel_mask__ = ChannelMask(0x4) - elif (self.__channels__ == 2): - self.__channel_mask__ = ChannelMask(0x3) - else: - self.__channel_mask__ = ChannelMask(0) - self.__total_frames__ = 0 - - #populate sample_rate and total_frames from first VERBATIM command - command = read_unsigned(reader, 2) - if (command == 9): - verbatim_bytes = "".join([chr(read_unsigned(reader, 8) & 0xFF) - for i in xrange( - read_unsigned(reader, 5))]) - try: - wave = BitstreamReader(cStringIO.StringIO(verbatim_bytes), 1) - header = wave.read_bytes(12) - if (header.startswith("RIFF") and header.endswith("WAVE")): - #got RIFF/WAVE header, so parse wave blocks as needed - total_size = len(verbatim_bytes) - 12 - while (total_size >= 8): - (chunk_id, chunk_size) = wave.parse("4b 32u") - total_size -= 8 - if (chunk_id == 'fmt '): - (channels, - self.__sample_rate__, - bits_per_sample, - self.__channel_mask__) = parse_fmt( - wave.substream(chunk_size)) - elif (chunk_id == 'data'): - self.__total_frames__ = \ - (chunk_size / - (self.__channels__ * - (self.__bits_per_sample__ / 8))) - else: - if (chunk_size % 2): - wave.read_bytes(chunk_size + 1) - total_size -= (chunk_size + 1) - else: - wave.read_bytes(chunk_size) - total_size -= chunk_size - except (IOError, ValueError): - pass - - try: - aiff = BitstreamReader(cStringIO.StringIO(verbatim_bytes), 0) - header = aiff.read_bytes(12) - if (header.startswith("FORM") and header.endswith("AIFF")): - #got FORM/AIFF header, so parse aiff blocks as needed - total_size = len(verbatim_bytes) - 12 - while (total_size >= 8): - (chunk_id, chunk_size) = aiff.parse("4b 32u") - total_size -= 8 - if (chunk_id == 'COMM'): - (channels, - total_sample_frames, - bits_per_sample, - self.__sample_rate__, - self.__channel_mask__) = parse_comm( - aiff.substream(chunk_size)) - elif (chunk_id == 'SSND'): - #subtract 8 bytes for "offset" and "block size" - self.__total_frames__ = \ - ((chunk_size - 8) / - (self.__channels__ * - (self.__bits_per_sample__ / 8))) - else: - if (chunk_size % 2): - aiff.read_bytes(chunk_size + 1) - total_size -= (chunk_size + 1) - else: - aiff.read_bytes(chunk_size) - total_size -= chunk_size - except IOError: - pass - - @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) == 'ajkg') and (ord(file.read(1)) == 2) - - 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""" - - return self.__channel_mask__ - - def lossless(self): - """returns True""" - - return True - - def total_frames(self): - """returns the total PCM frames of the track as an integer""" - - return self.__total_frames__ - - def sample_rate(self): - """returns the rate of the track's audio as an integer number of Hz""" - - return self.__sample_rate__ - - def to_pcm(self): - """returns a PCMReader object containing the track's PCM data""" - - try: - from . import decoders - - return decoders.SHNDecoder(self.filename) - except (IOError, ValueError), msg: - #these may not be accurate if the Shorten file is broken - #but if it is broken, there'll be no way to - #cross-check the results anyway - return PCMReaderError(error_message=str(msg), - sample_rate=44100, - channels=2, - channel_mask=0x3, - bits_per_sample=16) - - @classmethod - def from_pcm(cls, filename, pcmreader, compression=None, - block_size=256): - """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 ShortenAudio object""" - - if (pcmreader.bits_per_sample not in (8, 16)): - raise UnsupportedBitsPerSample(filename, pcmreader.bits_per_sample) - - import tempfile - - f = tempfile.NamedTemporaryFile(suffix=".wav") - try: - w = WaveAudio.from_pcm(f.name, pcmreader) - return cls.from_wave(filename, f.name, compression, block_size) - finally: - if (os.path.isfile(f.name)): - f.close() - else: - f.close_called = True - - 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""" - - from . import decoders - - try: - (head, tail) = decoders.SHNDecoder(self.filename).pcm_split() - except IOError, err: - raise EncodingError(str(err)) - - if ((head[0:4] == 'RIFF') and (head[8:12] == 'WAVE')): - try: - f = open(wave_filename, 'wb') - except IOError, msg: - raise EncodingError(str(msg)) - - try: - f.write(head) - total_frames = self.total_frames() - current_frames = 0 - decoder = decoders.SHNDecoder(self.filename) - frame = decoder.read(4096) - while (len(frame) > 0): - f.write(frame.to_bytes(False, self.bits_per_sample() > 8)) - current_frames += frame.frames - if (progress is not None): - progress(current_frames, total_frames) - frame = decoder.read(4096) - f.write(tail) - f.close() - except IOError, msg: - self.__unlink__(wave_filename) - raise EncodingError(str(msg)) - else: - WaveAudio.from_pcm(wave_filename, to_pcm_progress(self, progress)) - - def to_aiff(self, aiff_filename, progress=None): - """writes the contents of this file to the given .aiff filename string - - raises EncodingError if some error occurs during decoding""" - - from . import decoders - - try: - (head, tail) = decoders.SHNDecoder(self.filename).pcm_split() - except IOError: - raise EncodingError(str(msg)) - - if ((head[0:4] == 'FORM') and (head[8:12] == 'AIFF')): - try: - f = open(aiff_filename, 'wb') - except IOError, msg: - raise EncodingError(str(msg)) - - try: - f.write(head) - total_frames = self.total_frames() - current_frames = 0 - decoder = decoders.SHNDecoder(self.filename) - frame = decoder.read(4096) - while (len(frame) > 0): - f.write(frame.to_bytes(True, True)) - current_frames += frame.frames - if (progress is not None): - progress(current_frames, total_frames) - frame = decoder.read(4096) - f.write(tail) - f.close() - except IOError, msg: - self.__unlink__(aiff_filename) - raise EncodingError(str(msg)) - else: - AiffAudio.from_pcm(aiff_filename, to_pcm_progress(self, progress)) - - @classmethod - def from_wave(cls, filename, wave_filename, compression=None, - block_size=256, 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 ShortenAudio object""" - - wave = WaveAudio(wave_filename) - - if (wave.bits_per_sample() not in (8, 16)): - raise UnsupportedBitsPerSample(filename, wave.bits_per_sample()) - - (head, tail) = wave.pcm_split() - - from .encoders import encode_shn - - try: - if (len(tail) == 0): - encode_shn(filename=filename, - pcmreader=to_pcm_progress(wave, progress), - is_big_endian=False, - signed_samples=wave.bits_per_sample() == 16, - header_data=head, - block_size=block_size) - else: - encode_shn(filename=filename, - pcmreader=to_pcm_progress(wave, progress), - is_big_endian=False, - signed_samples=wave.bits_per_sample() == 16, - header_data=head, - footer_data=tail, - block_size=block_size) - - return cls(filename) - except IOError, err: - cls.__unlink__(filename) - raise EncodingError(str(err)) - except Exception, err: - cls.__unlink__(filename) - raise err - - @classmethod - def from_aiff(cls, filename, aiff_filename, compression=None, - block_size=256, progress=None): - """encodes a new AudioFile from an existing .aiff file - - takes a filename string, aiff_filename string - of an existing WaveAudio file - and an optional compression level string - encodes a new audio file from the aiff's data - at the given filename with the specified compression level - and returns a new ShortenAudio object""" - - aiff = AiffAudio(aiff_filename) - - if (aiff.bits_per_sample() not in (8, 16)): - raise UnsupportedBitsPerSample(filename, aiff.bits_per_sample()) - - (head, tail) = aiff.pcm_split() - - from .encoders import encode_shn - - try: - if (len(tail) == 0): - encode_shn(filename=filename, - pcmreader=to_pcm_progress(aiff, progress), - is_big_endian=True, - signed_samples=True, - header_data=head, - block_size=block_size) - else: - encode_shn(filename=filename, - pcmreader=to_pcm_progress(aiff, progress), - is_big_endian=True, - signed_samples=True, - header_data=head, - footer_data=tail, - block_size=block_size) - - return cls(filename) - except IOError, err: - cls.__unlink__(filename) - raise EncodingError(str(err)) - except Exception, err: - cls.__unlink__(filename) - raise err - - 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 - metadata is not copied during conversion, but embedded - RIFF chunks are (if any) - may raise EncodingError if some problem occurs during encoding""" - - #Note that a Shorten file cannot contain - #both RIFF chunks and AIFF chunks at the same time. - - import tempfile - - if (target_class == WaveAudio): - self.to_wave(target_path, progress=progress) - return WaveAudio(target_path) - elif (target_class == AiffAudio): - self.to_aiff(target_path, progress=progress) - return AiffAudio(target_path) - elif (self.has_foreign_riff_chunks() and - hasattr(target_class, "from_wave")): - temp_wave = tempfile.NamedTemporaryFile(suffix=".wav") - try: - #we'll only log the second leg of conversion, - #since that's likely to be the slower portion - self.to_wave(temp_wave.name) - return target_class.from_wave(target_path, - temp_wave.name, - compression, - progress=progress) - finally: - temp_wave.close() - elif (self.has_foreign_aiff_chunks() and - hasattr(target_class, "from_aiff")): - temp_aiff = tempfile.NamedTemporaryFile(suffix=".aiff") - try: - self.to_aiff(temp_aiff.name) - return target_class.from_aiff(target_path, - temp_aiff.name, - compression, - progress=progress) - finally: - temp_aiff.close() - else: - return target_class.from_pcm(target_path, - to_pcm_progress(self, progress), - compression) - - 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""" - - from . import decoders - from . import bitstream - - try: - (head, tail) = decoders.SHNDecoder(self.filename).pcm_split() - header = bitstream.BitstreamReader(cStringIO.StringIO(head), 1) - (RIFF, SIZE, WAVE) = header.parse("4b 32u 4b") - if ((RIFF != 'RIFF') or (WAVE != 'WAVE')): - return False - - #if the tail has room for chunks, there must be some foreign ones - if (len(tail) >= 8): - return True - - #otherwise, check the header for foreign chunks - total_size = len(head) - bitstream.format_byte_size("4b 32u 4b") - while (total_size >= 8): - (chunk_id, chunk_size) = header.parse("4b 32u") - total_size -= bitstream.format_byte_size("4b 32u") - if (chunk_id not in ('fmt ', 'data')): - return True - else: - if (chunk_size % 2): - header.skip_bytes(chunk_size + 1) - total_size -= chunk_size + 1 - else: - header.skip_bytes(chunk_size) - total_size -= chunk_size - else: - #no foreign chunks found - return False - except IOError: - return False - - def has_foreign_aiff_chunks(self): - """returns True if the audio file contains non-audio AIFF chunks - - during transcoding, if the source audio file has foreign AIFF chunks - and the target audio format supports foreign AIFF chunks, - conversion should be routed through .aiff conversion - to avoid losing those chunks""" - - from . import decoders - from . import bitstream - - try: - (head, tail) = decoders.SHNDecoder(self.filename).pcm_split() - header = bitstream.BitstreamReader(cStringIO.StringIO(head), 0) - (FORM, SIZE, AIFF) = header.parse("4b 32u 4b") - if ((FORM != 'FORM') or (AIFF != 'AIFF')): - return False - - #if the tail has room for chunks, there must be some foreign ones - if (len(tail) >= 8): - return True - - #otherwise, check the header for foreign chunks - total_size = len(head) - bitstream.format_byte_size("4b 32u 4b") - while (total_size >= 8): - (chunk_id, chunk_size) = header.parse("4b 32u") - total_size -= bitstream.format_byte_size("4b 32u") - if (chunk_id not in ('COMM', 'SSND')): - return True - else: - if (chunk_size % 2): - header.skip_bytes(chunk_size + 1) - total_size -= chunk_size + 1 - else: - header.skip_bytes(chunk_size) - total_size -= chunk_size - else: - #no foreign chunks found - return False - except IOError: - return False
View file
audiotools-2.18.tar.gz/audiotools/__vorbis__.py
Deleted
@@ -1,611 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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, - ReorderedPCMReader, transfer_data, - transfer_framelist_data, subprocess, BIN, - cStringIO, open_files, os, ReplayGain, - ignore_sigint, EncodingError, DecodingError, - ChannelMask, UnsupportedChannelMask, - __default_quality__) -from __vorbiscomment__ import * -import gettext - -gettext.install("audiotools", unicode=True) - - -class InvalidVorbis(InvalidFile): - pass - - -def verify_ogg_stream(stream): - """verifies an Ogg stream file object - - this file must be rewound to the start of a page - returns True if the file is valid - raises IOError or ValueError if there is some problem with the file - """ - - from . import verify - verify.ogg(stream) - return True - - -####################### -#Vorbis File -####################### - -class VorbisAudio(AudioFile): - """an Ogg Vorbis file""" - - SUFFIX = "ogg" - NAME = SUFFIX - DEFAULT_COMPRESSION = "3" - COMPRESSION_MODES = tuple([str(i) for i in range(0, 11)]) - COMPRESSION_DESCRIPTIONS = {"0": _(u"very low quality, " + - u"corresponds to oggenc -q 0"), - "10": _(u"very high quality, " + - u"corresponds to oggenc -q 10")} - BINARIES = ("oggenc", "oggdec") - REPLAYGAIN_BINARIES = ("vorbisgain", ) - - def __init__(self, filename): - """filename is a plain string""" - - AudioFile.__init__(self, filename) - self.__sample_rate__ = 0 - self.__channels__ = 0 - try: - self.__read_identification__() - except IOError, msg: - raise InvalidVorbis(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(0x23) - - return (header.startswith('OggS') and - header[0x1C:0x23] == '\x01vorbis') - - def __read_identification__(self): - from .bitstream import BitstreamReader - - f = open(self.filename, "rb") - try: - ogg_reader = BitstreamReader(f, 1) - (magic_number, - version, - header_type, - granule_position, - self.__serial_number__, - page_sequence_number, - checksum, - segment_count) = ogg_reader.parse("4b 8u 8u 64S 32u 32u 32u 8u") - - if (magic_number != 'OggS'): - raise InvalidFLAC(_(u"invalid Ogg magic number")) - if (version != 0): - raise InvalidFLAC(_(u"invalid Ogg version")) - - segment_length = ogg_reader.read(8) - - (vorbis_type, - header, - version, - self.__channels__, - self.__sample_rate__, - maximum_bitrate, - nominal_bitrate, - minimum_bitrate, - blocksize0, - blocksize1, - framing) = ogg_reader.parse( - "8u 6b 32u 8u 32u 32u 32u 32u 4u 4u 1u") - - if (vorbis_type != 1): - raise InvalidVorbis(_(u"invalid Vorbis type")) - if (header != 'vorbis'): - raise InvalidVorbis(_(u"invalid Vorbis header")) - if (version != 0): - raise InvalidVorbis(_(u"invalid Vorbis version")) - if (framing != 1): - raise InvalidVorbis(_(u"invalid framing bit")) - finally: - f.close() - - def lossless(self): - """returns False""" - - return False - - def bits_per_sample(self): - """returns an integer number of bits-per-sample this track contains""" - - return 16 - - 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() == 1): - return ChannelMask.from_fields( - front_center=True) - elif (self.channels() == 2): - return ChannelMask.from_fields( - front_left=True, front_right=True) - elif (self.channels() == 3): - return ChannelMask.from_fields( - front_left=True, front_right=True, - front_center=True) - elif (self.channels() == 4): - return ChannelMask.from_fields( - front_left=True, front_right=True, - back_left=True, back_right=True) - elif (self.channels() == 5): - return ChannelMask.from_fields( - front_left=True, front_right=True, - front_center=True, - back_left=True, back_right=True) - elif (self.channels() == 6): - return ChannelMask.from_fields( - front_left=True, front_right=True, - front_center=True, - back_left=True, back_right=True, - low_frequency=True) - elif (self.channels() == 7): - return ChannelMask.from_fields( - front_left=True, front_right=True, - front_center=True, - side_left=True, side_right=True, - back_center=True, low_frequency=True) - elif (self.channels() == 8): - return ChannelMask.from_fields( - front_left=True, front_right=True, - side_left=True, side_right=True, - back_left=True, back_right=True, - front_center=True, low_frequency=True) - else: - return ChannelMask(0) - - def total_frames(self): - """returns the total PCM frames of the track as an integer""" - - from .bitstream import BitstreamReader - from . import OggStreamReader - - pcm_samples = 0 - for (granule_position, - segments, - continuation, - first_page, - last_page) in OggStreamReader( - BitstreamReader(file(self.filename, "rb"), 1)).pages(): - if (granule_position >= 0): - pcm_samples = granule_position - return pcm_samples - - def sample_rate(self): - """returns the rate of the track's audio as an integer number of Hz""" - - return self.__sample_rate__ - - def to_pcm(self): - """returns a PCMReader object containing the track's PCM data""" - - sub = subprocess.Popen([BIN['oggdec'], '-Q', - '-b', str(16), - '-e', str(0), - '-s', str(1), - '-R', - '-o', '-', - self.filename], - stdout=subprocess.PIPE, - stderr=file(os.devnull, "a")) - - pcmreader = PCMReader(sub.stdout, - sample_rate=self.sample_rate(), - channels=self.channels(), - channel_mask=int(self.channel_mask()), - bits_per_sample=self.bits_per_sample(), - process=sub) - - if (self.channels() <= 2): - return pcmreader - elif (self.channels() <= 8): - #these mappings transform Vorbis order into ChannelMask order - standard_channel_mask = self.channel_mask() - vorbis_channel_mask = VorbisChannelMask(self.channel_mask()) - return ReorderedPCMReader( - pcmreader, - [vorbis_channel_mask.channels().index(channel) for channel in - standard_channel_mask.channels()]) - else: - return pcmreader - - @classmethod - def from_pcm(cls, filename, pcmreader, compression=None): - """returns a PCMReader object containing the track's PCM data""" - - if ((compression is None) or - (compression not in cls.COMPRESSION_MODES)): - compression = __default_quality__(cls.NAME) - - devnull = file(os.devnull, 'ab') - - sub = subprocess.Popen([BIN['oggenc'], '-Q', - '-r', - '-B', str(pcmreader.bits_per_sample), - '-C', str(pcmreader.channels), - '-R', str(pcmreader.sample_rate), - '--raw-endianness', str(0), - '-q', compression, - '-o', filename, '-'], - stdin=subprocess.PIPE, - stdout=devnull, - stderr=devnull, - preexec_fn=ignore_sigint) - - if ((pcmreader.channels <= 2) or (int(pcmreader.channel_mask) == 0)): - try: - transfer_framelist_data(pcmreader, sub.stdin.write) - except (IOError, ValueError), err: - sub.stdin.close() - sub.wait() - cls.__unlink__(filename) - raise EncodingError(str(err)) - except Exception, err: - sub.stdin.close() - sub.wait() - cls.__unlink__(filename) - raise err - - elif (pcmreader.channels <= 8): - if (int(pcmreader.channel_mask) in - (0x7, # FR, FC, FL - 0x33, # FR, FL, BR, BL - 0x37, # FR, FC, FL, BL, BR - 0x3f, # FR, FC, FL, BL, BR, LFE - 0x70f, # FL, FC, FR, SL, SR, BC, LFE - 0x63f)): # FL, FC, FR, SL, SR, BL, BR, LFE - - standard_channel_mask = ChannelMask(pcmreader.channel_mask) - vorbis_channel_mask = VorbisChannelMask(standard_channel_mask) - else: - raise UnsupportedChannelMask(filename, - int(pcmreader.channel_mask)) - - try: - transfer_framelist_data(ReorderedPCMReader( - pcmreader, - [standard_channel_mask.channels().index(channel) - for channel in vorbis_channel_mask.channels()]), - sub.stdin.write) - except (IOError, ValueError), err: - sub.stdin.close() - sub.wait() - cls.__unlink__(filename) - raise EncodingError(str(err)) - except Exception, err: - sub.stdin.close() - sub.wait() - cls.__unlink__(filename) - raise err - - else: - raise UnsupportedChannelMask(filename, - int(pcmreader.channel_mask)) - - try: - pcmreader.close() - except DecodingError, err: - raise EncodingError(err.error_message) - - sub.stdin.close() - devnull.close() - - if (sub.wait() == 0): - return VorbisAudio(filename) - else: - raise EncodingError(u"unable to encode file with oggenc") - - def update_metadata(self, metadata): - """takes this track's current MetaData object - as returned by get_metadata() and sets this track's metadata - with any fields updated in that object - - raises IOError if unable to write the file - """ - - if (metadata is None): - return - - if (not isinstance(metadata, VorbisComment)): - raise ValueError(_(u"metadata not from audio file")) - - from .bitstream import BitstreamReader - from .bitstream import BitstreamRecorder - from .bitstream import BitstreamWriter - from . import OggStreamWriter - from . import OggStreamReader - from . import read_ogg_packets_data - from . import iter_first - - original_reader = BitstreamReader(open(self.filename, "rb"), 1) - original_ogg = OggStreamReader(original_reader) - original_serial_number = original_ogg.serial_number - original_packets = read_ogg_packets_data(original_reader) - - #save the current file's identification page/packet - #(the ID packet is always fixed size, and fits in one page) - identification_page = original_ogg.read_page() - - #discard the current file's comment packet - original_packets.next() - - #save the current file's setup packet - setup_packet = original_packets.next() - - #save all the subsequent Ogg pages - data_pages = list(original_ogg.pages()) - - del(original_ogg) - del(original_packets) - original_reader.close() - - updated_writer = BitstreamWriter(open(self.filename, "wb"), 1) - updated_ogg = OggStreamWriter(updated_writer, original_serial_number) - - #write the identification packet in its own page - updated_ogg.write_page(*identification_page) - - #write the new comment packet in its own page(s) - comment_writer = BitstreamRecorder(1) - comment_writer.build("8u 6b", (3, "vorbis")) - vendor_string = metadata.vendor_string.encode('utf-8') - comment_writer.build("32u %db" % (len(vendor_string)), - (len(vendor_string), vendor_string)) - comment_writer.write(32, len(metadata.comment_strings)) - for comment_string in metadata.comment_strings: - comment_string = comment_string.encode('utf-8') - comment_writer.build("32u %db" % (len(comment_string)), - (len(comment_string), comment_string)) - - comment_writer.build("1u a", (1,)) - - for (first_page, segments) in iter_first( - updated_ogg.segments_to_pages( - updated_ogg.packet_to_segments(comment_writer.data()))): - updated_ogg.write_page(0, segments, 0 if first_page else 1, 0, 0) - - #write the setup packet in its own page(s) - for (first_page, segments) in iter_first( - updated_ogg.segments_to_pages( - updated_ogg.packet_to_segments(setup_packet))): - updated_ogg.write_page(0, segments, 0 if first_page else 1, 0, 0) - - #write the subsequent Ogg pages - for page in data_pages: - updated_ogg.write_page(*page) - - def set_metadata(self, metadata): - """takes a MetaData object and sets this track's metadata - - this metadata includes track name, album name, and so on - raises IOError if unable to write the file""" - - if (metadata is not None): - metadata = VorbisComment.converted(metadata) - - old_metadata = self.get_metadata() - - metadata.vendor_string = old_metadata.vendor_string - - #remove REPLAYGAIN_* tags from new metadata (if any) - for key in [u"REPLAYGAIN_TRACK_GAIN", - u"REPLAYGAIN_TRACK_PEAK", - u"REPLAYGAIN_ALBUM_GAIN", - u"REPLAYGAIN_ALBUM_PEAK", - u"REPLAYGAIN_REFERENCE_LOUDNESS"]: - try: - metadata[key] = old_metadata[key] - except KeyError: - metadata[key] = [] - - self.update_metadata(metadata) - - def get_metadata(self): - """returns a MetaData object, or None - - raises IOError if unable to read the file""" - - from .bitstream import BitstreamReader - from . import read_ogg_packets - - packets = read_ogg_packets( - BitstreamReader(open(self.filename, "rb"), 1)) - - identification = packets.next() - comment = packets.next() - - (packet_type, packet_header) = comment.parse("8u 6b") - if ((packet_type != 3) or (packet_header != 'vorbis')): - return None - else: - vendor_string = \ - comment.read_bytes(comment.read(32)).decode('utf-8') - comment_strings = [ - comment.read_bytes(comment.read(32)).decode('utf-8') - for i in xrange(comment.read(32))] - if (comment.read(1) == 1): # framing bit - return VorbisComment(comment_strings, vendor_string) - else: - return None - - def delete_metadata(self): - """deletes the track's MetaData - - this removes or unsets tags as necessary in order to remove all data - raises IOError if unable to write the file""" - - #the vorbis comment packet is required, - #so simply zero out its contents - self.set_metadata(MetaData()) - - @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 - """ - - track_names = [track.filename for track in - open_files(filenames) if - isinstance(track, cls)] - - if (progress is not None): - progress(0, 1) - - if ((len(track_names) > 0) and - BIN.can_execute(BIN['vorbisgain'])): - devnull = file(os.devnull, 'ab') - - sub = subprocess.Popen([BIN['vorbisgain'], - '-q', '-a'] + track_names, - stdout=devnull, - stderr=devnull) - sub.wait() - devnull.close() - - if (progress is not None): - progress(1, 1) - - @classmethod - def can_add_replay_gain(cls): - """returns True if we have the necessary binaries to add ReplayGain""" - - return BIN.can_execute(BIN['vorbisgain']) - - @classmethod - def lossless_replay_gain(cls): - """returns True""" - - return True - - def replay_gain(self): - """returns a ReplayGain object of our ReplayGain values - - returns None if we have no values""" - - vorbis_metadata = self.get_metadata() - - if ((vorbis_metadata is not None) and - (set(['REPLAYGAIN_TRACK_PEAK', 'REPLAYGAIN_TRACK_GAIN', - 'REPLAYGAIN_ALBUM_PEAK', 'REPLAYGAIN_ALBUM_GAIN']).issubset( - vorbis_metadata.keys()))): # we have ReplayGain data - try: - return ReplayGain( - vorbis_metadata['REPLAYGAIN_TRACK_GAIN'][0][0:-len(" dB")], - vorbis_metadata['REPLAYGAIN_TRACK_PEAK'][0], - vorbis_metadata['REPLAYGAIN_ALBUM_GAIN'][0][0:-len(" dB")], - vorbis_metadata['REPLAYGAIN_ALBUM_PEAK'][0]) - except (IndexError, ValueError): - return None - else: - return None - - 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""" - - #Ogg stream 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, err: - raise InvalidVorbis(str(err)) - try: - try: - result = verify_ogg_stream(f) - if (progress is not None): - progress(1, 1) - return result - except (IOError, ValueError), err: - raise InvalidVorbis(str(err)) - finally: - f.close() - - -class VorbisChannelMask(ChannelMask): - """the Vorbis-specific channel mapping""" - - def __repr__(self): - return "VorbisChannelMask(%s)" % \ - ",".join(["%s=%s" % (field, getattr(self, field)) - for field in self.SPEAKER_TO_MASK.keys() - if (getattr(self, field))]) - - def channels(self): - """returns a list of speaker strings this mask contains - - returned in the order in which they should appear - in the PCM stream - """ - - count = len(self) - if (count == 1): - return ["front_center"] - elif (count == 2): - return ["front_left", "front_right"] - elif (count == 3): - return ["front_left", "front_center", "front_right"] - elif (count == 4): - return ["front_left", "front_right", - "back_left", "back_right"] - elif (count == 5): - return ["front_left", "front_center", "front_right", - "back_left", "back_right"] - elif (count == 6): - return ["front_left", "front_center", "front_right", - "back_left", "back_right", "low_frequency"] - elif (count == 7): - return ["front_left", "front_center", "front_right", - "side_left", "side_right", "back_center", - "low_frequency"] - elif (count == 8): - return ["front_left", "front_center", "front_right", - "side_left", "side_right", - "back_left", "back_right", "low_frequency"] - else: - return []
View file
audiotools-2.18.tar.gz/audiotools/__vorbiscomment__.py
Deleted
@@ -1,420 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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 MetaData, VERSION, re - - -class VorbisComment(MetaData): - ATTRIBUTE_MAP = {'track_name': u'TITLE', - 'track_number': u'TRACKNUMBER', - 'track_total': u'TRACKTOTAL', - 'album_name': u'ALBUM', - 'artist_name': u'ARTIST', - 'performer_name': u'PERFORMER', - 'composer_name': u'COMPOSER', - 'conductor_name': u'CONDUCTOR', - 'media': u'SOURCE MEDIUM', - 'ISRC': u'ISRC', - 'catalog': u'CATALOG', - 'copyright': u'COPYRIGHT', - 'publisher': u'PUBLISHER', - 'year': u'DATE', - 'album_number': u'DISCNUMBER', - 'album_total': u'DISCTOTAL', - 'comment': u'COMMENT'} - - ALIASES = {} - - for aliases in [frozenset([u'TRACKTOTAL', u'TOTALTRACKS']), - frozenset([u'DISCTOTAL', u'TOTALDISCS'])]: - for alias in aliases: - ALIASES[alias] = aliases - - SLASHED_FIELD = re.compile(r'(\d+)\s*/\s*(\d+)') - - SLASHED_FIELDS = {'track_number': (u'TRACKNUMBER', 0), - 'track_total': (u'TRACKNUMBER', 1), - 'album_number': (u'DISCNUMBER', 0), - 'album_total': (u'DISCNUMBER', 1)} - - def __init__(self, comment_strings, vendor_string): - """comment_strings is a list of unicode strings - - vendor_string is a unicode string""" - - self.__dict__["comment_strings"] = comment_strings - self.__dict__["vendor_string"] = vendor_string - - def keys(self): - return list(set([comment.split(u"=", 1)[0] - for comment in self.comment_strings - if (u"=" in comment)])) - - def values(self): - return [self[key] for key in self.keys()] - - def items(self): - return [(key, self[key]) for key in self.keys()] - - def __getitem__(self, key): - matching_keys = self.ALIASES.get(key.upper(), frozenset([key.upper()])) - - values = [item_value for (item_key, item_value) in - [comment.split(u"=", 1) for comment in self.comment_strings - if (u"=" in comment)] - if (item_key.upper() in matching_keys)] - - if (len(values) > 0): - return values - else: - raise KeyError(key) - - def __setitem__(self, key, values): - new_values = values[:] - new_comment_strings = [] - matching_keys = self.ALIASES.get(key.upper(), frozenset([key.upper()])) - - for comment in self.comment_strings: - if (u"=" in comment): - (c_key, c_value) = comment.split(u"=", 1) - if (c_key.upper() in matching_keys): - try: - #replace current value with newly set value - new_comment_strings.append( - u"%s=%s" % (c_key, new_values.pop(0))) - except IndexError: - #no more newly set values, so remove current value - continue - else: - #passthrough unmatching values - new_comment_strings.append(comment) - else: - #passthrough values with no "=" sign - new_comment_strings.append(comment) - - #append any leftover values - for new_value in new_values: - new_comment_strings.append(u"%s=%s" % (key.upper(), new_value)) - - self.__dict__["comment_strings"] = new_comment_strings - - def __repr__(self): - return "VorbisComment(%s, %s)" % \ - (repr(self.comment_strings), repr(self.vendor_string)) - - def raw_info(self): - """returns a Unicode string of low-level MetaData information - - whereas __unicode__ is meant to contain complete information - at a very high level - raw_info() should be more developer-specific and with - very little adjustment or reordering to the data itself - """ - - from os import linesep - from . import display_unicode - - #align text strings on the "=" sign, if any - - if (len(self.comment_strings) > 0): - max_indent = max([len(display_unicode(comment.split(u"=", 1)[0])) - for comment in self.comment_strings - if u"=" in comment]) - - comment_strings = [] - for comment in self.comment_strings: - if (u"=" in comment): - comment_strings.append( - u" " * (max_indent - - len( - display_unicode(comment.split(u"=", 1)[0]))) + - comment) - else: - comment_strings.append(comment) - else: - comment_strings = 0 - - return linesep.decode('ascii').join( - [u"Vorbis Comment: %s" % (self.vendor_string)] + - comment_strings) - - def __getattr__(self, attr): - #returns the first matching key for the given attribute - #in our list of comment strings - - if (attr in self.SLASHED_FIELDS): - #handle rare u'TRACKNUMBER=1/2' cases - #which must always be numerical fields - - (slashed_field, slash_side) = self.SLASHED_FIELDS[attr] - - try: - return int([match.group(slash_side + 1) - for match in - [self.SLASHED_FIELD.search(field) - for field in self[slashed_field]] - if (match is not None)][0]) - except (KeyError, IndexError): - pass - - if (attr in self.INTEGER_FIELDS): - #all integer fields are present in attribute map - try: - return int(self[self.ATTRIBUTE_MAP[attr]][0]) - except (KeyError, ValueError): - return 0 - elif (attr in self.ATTRIBUTE_MAP): - try: - return self[self.ATTRIBUTE_MAP[attr]][0] - except KeyError: - return u"" - elif (attr in self.FIELDS): - return u"" - else: - try: - return self.__dict__[attr] - except KeyError: - raise AttributeError(attr) - - def __setattr__(self, attr, value): - #updates the first matching key for the given attribute - #in our list of comment strings - - if (attr in self.SLASHED_FIELDS): - #setting numerical fields to 0 - #is equivilent to deleting them - #in our high level implementation - if (value == 0): - self.__delattr__(attr) - return - - #handle rare u'TRACKNUMBER=1/2' cases - #which must always be numerical fields - (slashed_field, slash_side) = self.SLASHED_FIELDS[attr] - - try: - slashed_matches = [match for match in - [self.SLASHED_FIELD.search(field) - for field in self[slashed_field]] - if (match is not None)] - except KeyError: - slashed_matches = [] - - if (len(slashed_matches) > 0): - if (slash_side == 0): - #retain the number on the right side - self[slashed_field] = ( - [u"%d/%s" % (int(value), - slashed_matches[0].group(2))] + - [m.group(0) for m in slashed_matches][1:]) - else: - #retain the number on the left side - self[slashed_field] = ( - [u"%s/%d" % (slashed_matches[0].group(1), - int(value))] + - [m.group(0) for m in slashed_matches][1:]) - - else: - try: - current_values = self[self.ATTRIBUTE_MAP[attr]] - except KeyError: - current_values = [] - self[self.ATTRIBUTE_MAP[attr]] = ([unicode(value)] + - current_values[1:]) - - #plain text fields are easier - elif (attr in self.ATTRIBUTE_MAP): - #try to leave subsequent fields as-is - try: - current_values = self[self.ATTRIBUTE_MAP[attr]] - except KeyError: - current_values = [] - self[self.ATTRIBUTE_MAP[attr]] = [value] + current_values[1:] - else: - self.__dict__[attr] = value - - def __delattr__(self, attr): - #deletes all matching keys for the given attribute - #in our list of comment strings - - if (attr in self.SLASHED_FIELDS): - #handle rare u'TRACKNUMBER=1/2' cases - #which must always be numerical fields - (slashed_field, slash_side) = self.SLASHED_FIELDS[attr] - - try: - slashed_matches = [match for match in - [self.SLASHED_FIELD.search(field) - for field in self[slashed_field]] - if (match is not None)] - except KeyError: - slashed_matches = [] - - if (len(slashed_matches) > 0): - if (slash_side == 0): - #retain the number on the right side - self[slashed_field] = \ - [u"0/%s" % (m.group(2)) for m in slashed_matches - if (int(m.group(2)) != 0)] - else: - #retain the number on the left side - self[slashed_field] = \ - [m.group(1) for m in slashed_matches - if (int(m.group(1)) != 0)] - #FIXME - also wipe non-slashed field? - - else: - self[self.ATTRIBUTE_MAP[attr]] = [] - - elif (attr in self.ATTRIBUTE_MAP): - #unlike __setattr_, which tries to preserve multiple instances - #of fields, __delattr__ wipes them all - #so that orphaned fields don't show up after deletion - self[self.ATTRIBUTE_MAP[attr]] = [] - else: - try: - del(self.__dict__[attr]) - except KeyError: - raise AttributeError(attr) - - def __eq__(self, metadata): - if (isinstance(metadata, self.__class__)): - return self.comment_strings == metadata.comment_strings - else: - return MetaData.__eq__(self, metadata) - - @classmethod - def converted(cls, metadata): - """converts metadata from another class to VorbisComment""" - - if (metadata is None): - return None - elif (isinstance(metadata, VorbisComment)): - return cls(metadata.comment_strings[:], - metadata.vendor_string) - elif (metadata.__class__.__name__ == 'FlacMetaData'): - if (metadata.has_block(4)): - vorbis_comment = metadata.get_block(4) - return cls(vorbis_comment.comment_strings[:], - vorbis_comment.vendor_string) - else: - return cls([], u"Python Audio Tools %s" % (VERSION)) - elif (metadata.__class__.__name__ == 'Flac_VORBISCOMMENT'): - return cls(metadata.comment_strings[:], - metadata.vendor_string) - else: - comment_strings = [] - - for (attr, keys) in cls.ATTRIBUTE_MAP.items(): - if (attr not in cls.INTEGER_FIELDS): - if (len(getattr(metadata, attr)) > 0): - comment_strings.append( - "%s=%s" % (cls.ATTRIBUTE_MAP[attr], - getattr(metadata, attr))) - else: - if (getattr(metadata, attr) > 0): - comment_strings.append( - "%s=%s" % (cls.ATTRIBUTE_MAP[attr], - getattr(metadata, attr))) - - return cls(comment_strings, u"Python Audio Tools %s" % (VERSION)) - - @classmethod - def supports_images(cls): - """returns False""" - - #There's actually a (proposed?) standard to add embedded covers - #to Vorbis Comments by base64 encoding them. - #This strikes me as messy and convoluted. - #In addition, I'd have to perform a special case of - #image extraction and re-insertion whenever converting - #to FlacMetaData. The whole thought gives me a headache. - - return False - - def images(self): - """returns a list of embedded Image objects""" - - return [] - - def clean(self, fixes_performed): - """returns a new MetaData object that's been cleaned of problems""" - - reverse_attr_map = {} - for (attr, key) in self.ATTRIBUTE_MAP.items(): - reverse_attr_map[key] = attr - if (key in self.ALIASES): - for alias in self.ALIASES[key]: - reverse_attr_map[alias] = attr - - cleaned_fields = [] - - for comment_string in self.comment_strings: - if (u"=" in comment_string): - (key, value) = comment_string.split(u"=", 1) - if (key.upper() in reverse_attr_map): - attr = reverse_attr_map[key.upper()] - #handle all text fields by stripping whitespace - if (len(value.strip()) == 0): - fixes_performed.append( - _(u"removed empty field %(field)s") % - {"field": key}) - else: - fix1 = value.rstrip() - if (fix1 != value): - fixes_performed.append( - _(u"removed trailing whitespace " + - u"from %(field)s") % - {"field": key}) - - fix2 = fix1.lstrip() - if (fix2 != fix1): - fixes_performed.append( - _(u"removed leading whitespace " + - "from %(field)s") % - {"field": key}) - - #integer fields also strip leading zeroes - if ((attr in self.SLASHED_FIELDS) and - (self.SLASHED_FIELD.search(fix2) is not None)): - match = self.SLASHED_FIELD.search(value) - fix3 = u"%d/%d" % (int(match.group(1)), - int(match.group(2))) - if (fix3 != fix2): - fixes_performed.append( - _(u"removed whitespace/zeroes " + - u"from %(field)s") % - {"field": key}) - elif (attr in self.INTEGER_FIELDS): - fix3 = fix2.lstrip(u"0") - if (fix3 != fix2): - fixes_performed.append( - _(u"removed leading zeroes from %(field)s") % - {"field": key}) - else: - fix3 = fix2 - - cleaned_fields.append(u"%s=%s" % (key, fix3)) - else: - cleaned_fields.append(comment_string) - else: - cleaned_fields.append(comment_string) - - return self.__class__(cleaned_fields, self.vendor_string)
View file
audiotools-2.18.tar.gz/audiotools/__wav__.py
Deleted
@@ -1,971 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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, - BUFFER_SIZE, transfer_data, - transfer_framelist_data, - __capped_stream_reader__, FILENAME_FORMAT, - BIN, open_files, os, subprocess, cStringIO, - EncodingError, DecodingError, - UnsupportedChannelMask, - UnsupportedChannelCount, - WaveContainer, to_pcm_progress, - LimitedFileReader) -import os.path -import struct -import gettext -from . import pcm - -gettext.install("audiotools", unicode=True) - -####################### -#RIFF WAVE -####################### - - -class RIFF_Chunk: - """a raw chunk of RIFF WAVE data""" - - def __init__(self, chunk_id, chunk_size, chunk_data): - """chunk_id should be a binary string of ASCII - chunk_data should be a binary string of chunk data""" - - #FIXME - check chunk_id's validity - - self.id = chunk_id - self.__size__ = chunk_size - self.__data__ = chunk_data - - def __repr__(self): - return "RIFF_Chunk(%s)" % (repr(self.id)) - - def size(self): - """returns size of chunk in bytes - not including any spacer byte for odd-sized chunks""" - - return self.__size__ - - def total_size(self): - """returns the total size of the chunk - including the 8 byte ID/size and any padding byte""" - - if (self.__size__ % 2): - return 8 + self.__size__ + 1 - else: - return 8 + self.__size__ - - def data(self): - """returns chunk data as file-like object""" - - return cStringIO.StringIO(self.__data__) - - def verify(self): - """returns True if the chunk is sized properly""" - - return self.__size__ == len(self.__data__) - - def write(self, f): - """writes the entire chunk to the given output file object - returns size of entire chunk (including header and spacer) - in bytes""" - - f.write(self.id) - f.write(struct.pack("<I", self.__size__)) - f.write(self.__data__) - if (self.__size__ % 2): - f.write(chr(0)) - return self.total_size() - - -class RIFF_File_Chunk(RIFF_Chunk): - """a raw chunk of RIFF WAVE data taken from an existing file""" - - def __init__(self, chunk_id, chunk_size, wav_file, chunk_data_offset): - """chunk_id should be a binary string of ASCII - chunk_size is the size of the chunk in bytes - (not counting any spacer byte) - wav_file is the file this chunk belongs to - chunk_data_offset is the offset to the chunk's data bytes - (not including the 8 byte header)""" - - self.id = chunk_id - self.__size__ = chunk_size - self.__wav_file__ = wav_file - self.__offset__ = chunk_data_offset - - def __repr__(self): - return "RIFF_File_Chunk(%s)" % (repr(self.id)) - - def data(self): - """returns chunk data as file-like object""" - - self.__wav_file__.seek(self.__offset__) - return LimitedFileReader(self.__wav_file__, self.size()) - - def verify(self): - """returns True if the chunk is sized properly""" - - self.__wav_file__.seek(self.__offset__) - to_read = self.__size__ - while (to_read > 0): - s = self.__wav_file__.read(min(0x100000, to_read)) - if (len(s) == 0): - return False - else: - to_read -= len(s) - return True - - def write(self, f): - """writes the entire chunk to the given output file object - returns size of entire chunk (including header and spacer) - in bytes""" - - f.write(self.id) - f.write(struct.pack("<I", self.__size__)) - self.__wav_file__.seek(self.__offset__) - to_write = self.__size__ - while (to_write > 0): - s = self.__wav_file__.read(min(0x100000, to_write)) - f.write(s) - to_write -= len(s) - - if (self.__size__ % 2): - f.write(chr(0)) - return self.total_size() - - -def parse_fmt(fmt): - """given a fmt block BitstreamReader (without the 8 byte header) - returns (channels, sample_rate, bits_per_sample, channel_mask) - where channel_mask is a ChannelMask object and the rest are ints - may raise ValueError if the fmt chunk is invalid - or IOError if an error occurs parsing the chunk""" - - (compression, - channels, - sample_rate, - bytes_per_second, - block_align, - bits_per_sample) = fmt.parse("16u 16u 32u 32u 16u 16u") - - if (compression == 1): - #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 - if (channels == 1): - channel_mask = ChannelMask.from_fields( - front_center=True) - elif (channels == 2): - channel_mask = ChannelMask.from_fields( - front_left=True, front_right=True) - elif (channels == 3): - channel_mask = ChannelMask.from_fields( - front_left=True, front_right=True, - front_center=True) - elif (channels == 4): - channel_mask = ChannelMask.from_fields( - front_left=True, front_right=True, - back_left=True, back_right=True) - elif (channels == 5): - channel_mask = ChannelMask.from_fields( - front_left=True, front_right=True, - back_left=True, back_right=True, - front_center=True) - elif (channels == 6): - channel_mask = ChannelMask.from_fields( - front_left=True, front_right=True, - back_left=True, back_right=True, - front_center=True, low_frequency=True) - else: - channel_mask = ChannelMask(0) - - return (channels, sample_rate, bits_per_sample, channel_mask) - elif (compression == 0xFFFE): - (cb_size, - valid_bits_per_sample, - channel_mask, - sub_format) = fmt.parse("16u 16u 32u 16b") - if (sub_format != - ('\x01\x00\x00\x00\x00\x00\x10\x00' + - '\x80\x00\x00\xaa\x00\x38\x9b\x71')): - raise ValueError("invalid WAVE sub-format") - else: - channel_mask = ChannelMask(channel_mask) - - return (channels, sample_rate, bits_per_sample, channel_mask) - else: - raise ValueError("unsupported WAVE compression") - - -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 - - from .bitstream import BitstreamReader - - #build a capped reader for the data chunk - wave_reader = BitstreamReader(wave_file, 1) - try: - (riff, wave) = wave_reader.parse("4b 32p 4b") - if (riff != 'RIFF'): - raise InvalidWave(_(u"Not a RIFF WAVE file")) - elif (wave != 'WAVE'): - raise InvalidWave(_(u"Invalid RIFF WAVE file")) - - while (True): - (chunk_id, chunk_size) = wave_reader.parse("4b 32u") - if (chunk_id == 'data'): - self.wave = __capped_stream_reader__(self.file, - chunk_size) - self.data_chunk_length = chunk_size - break - else: - wave_reader.skip_bytes(chunk_size) - if (chunk_size % 2): - wave_reader.skip(8) - - except IOError: - raise InvalidWave(_(u"data chunk not found")) - - def read(self, bytes): - """try to read a pcm.FrameList of size 'bytes'""" - - #align bytes downward if an odd number is read in - bytes_per_frame = self.channels * (self.bits_per_sample / 8) - requested_frames = max(1, bytes / bytes_per_frame) - pcm_data = self.wave.read(requested_frames * bytes_per_frame) - 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 - - -class WaveAudio(WaveContainer): - """a waveform audio file""" - - SUFFIX = "wav" - NAME = SUFFIX - - PRINTABLE_ASCII = frozenset([chr(i) for i in xrange(0x20, 0x7E + 1)]) - - def __init__(self, filename): - """filename is a plain string""" - - AudioFile.__init__(self, filename) - - self.__channels__ = 0 - self.__sample_rate__ = 0 - self.__bits_per_sample__ = 0 - self.__data_size__ = 0 - self.__channel_mask__ = ChannelMask(0) - - from .bitstream import BitstreamReader - - fmt_read = data_read = False - - try: - for chunk in self.chunks(): - if (chunk.id == "fmt "): - try: - (self.__channels__, - self.__sample_rate__, - self.__bits_per_sample__, - self.__channel_mask__) = parse_fmt( - BitstreamReader(chunk.data(), 1)) - fmt_read = True - if (fmt_read and data_read): - break - except IOError: - continue - elif (chunk.id == "data"): - self.__data_size__ = chunk.size() - data_read = True - if (fmt_read and data_read): - break - except IOError: - raise InvalidWave("I/O error reading wave") - - @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([c.id for c in self.chunks()]) - - 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""" - - if (pcmreader.channels > 18): - raise UnsupportedChannelCount(filename, pcmreader.channels) - - from .bitstream import BitstreamWriter, format_size - - try: - f = file(filename, "wb") - wave = BitstreamWriter(f, 1) - except IOError, err: - raise EncodingError(str(err)) - - try: - total_size = 0 - data_size = 0 - - avg_bytes_per_second = (pcmreader.sample_rate * - pcmreader.channels * - (pcmreader.bits_per_sample / 8)) - block_align = (pcmreader.channels * - (pcmreader.bits_per_sample / 8)) - - #build a regular or extended fmt chunk - #based on the reader's attributes - if ((pcmreader.channels <= 2) and - (pcmreader.bits_per_sample <= 16)): - fmt = "16u 16u 32u 32u 16u 16u" - fmt_fields = (1, # compression code - pcmreader.channels, - pcmreader.sample_rate, - avg_bytes_per_second, - block_align, - pcmreader.bits_per_sample) - else: - if (pcmreader.channel_mask != 0): - channel_mask = pcmreader.channel_mask - else: - channel_mask = {1:0x4, - 2:0x3, - 3:0x7, - 4:0x33, - 5:0x37, - 6:0x3F}.get(pcmreader.channels, 0) - - fmt = "16u 16u 32u 32u 16u 16u" + "16u 16u 32u 16b" - fmt_fields = (0xFFFE, # compression code - pcmreader.channels, - pcmreader.sample_rate, - avg_bytes_per_second, - block_align, - pcmreader.bits_per_sample, - 22, # CB size - pcmreader.bits_per_sample, - channel_mask, - '\x01\x00\x00\x00\x00\x00\x10\x00' + - '\x80\x00\x00\xaa\x00\x38\x9b\x71' # sub format - ) - - #write out the basic headers first - #we'll be back later to clean up the sizes - wave.build("4b 32u 4b", ("RIFF", total_size, "WAVE")) - total_size += 4 - - wave.build("4b 32u", ('fmt ', format_size(fmt) / 8)) - total_size += format_size("4b 32u") / 8 - - wave.build(fmt, fmt_fields) - total_size += format_size(fmt) / 8 - - wave.build("4b 32u", ('data', data_size)) - total_size += format_size("4b 32u") / 8 - - #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) - total_size += len(bytes) - 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 - - #handle odd-sized data chunks - if (data_size % 2): - wave.write(8, 0) - total_size += 1 - - #close the PCM reader and flush our output - try: - pcmreader.close() - except DecodingError, err: - cls.__unlink__(filename) - raise EncodingError(err.error_message) - f.flush() - - if (total_size < (2 ** 32)): - #go back to the beginning the rewrite the header - f.seek(0, 0) - wave.build("4b 32u 4b", ("RIFF", total_size, "WAVE")) - wave.build("4b 32u", ('fmt ', format_size(fmt) / 8)) - wave.build(fmt, fmt_fields) - wave.build("4b 32u", ('data', data_size)) - else: - os.unlink(filename) - raise EncodingError("PCM data too large for wave file") - - 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.__bits_per_sample__ / 8) / \ - self.__channels__ - - def sample_rate(self): - """returns the rate of the track's audio as an integer number of Hz""" - - return self.__sample_rate__ - - 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.__bits_per_sample__ - - @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 chunks(self): - """yields a set of RIFF_Chunk or RIFF_File_Chunk objects""" - - wave_file = file(self.filename, "rb") - try: - (riff, - total_size, - wave) = struct.unpack("<4sI4s", wave_file.read(12)) - except struct.error: - raise InvalidWave(_(u"Invalid RIFF WAVE file")) - - if (riff != 'RIFF'): - raise InvalidWave(_(u"Not a RIFF WAVE file")) - elif (wave != 'WAVE'): - raise InvalidWave(_(u"Invalid RIFF WAVE file")) - else: - total_size -= 4 - - while (total_size > 0): - #read the chunk header and ensure its validity - try: - (chunk_id, - chunk_size) = struct.unpack("<4sI", wave_file.read(8)) - except struct.error: - raise InvalidWave(_(u"Invalid RIFF WAVE file")) - if (not frozenset(chunk_id).issubset(self.PRINTABLE_ASCII)): - raise InvalidWave(_(u"Invalid RIFF WAVE chunk ID")) - else: - total_size -= 8 - - #yield RIFF_Chunk or RIFF_File_Chunk depending on chunk size - if (chunk_size >= 0x100000): - #if chunk is too large, yield a File_Chunk - yield RIFF_File_Chunk(chunk_id, - chunk_size, - file(self.filename, "rb"), - wave_file.tell()) - wave_file.seek(chunk_size, 1) - else: - #otherwise, yield a raw data Chunk - yield RIFF_Chunk(chunk_id, chunk_size, - wave_file.read(chunk_size)) - - if (chunk_size % 2): - if (len(wave_file.read(1)) < 1): - raise InvalidWave(_(u"Invalid RIFF WAVE chunk")) - total_size -= (chunk_size + 1) - else: - total_size -= chunk_size - - @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 RIFF_Chunk-compatible objects - """ - - wave_file = file(filename, 'wb') - try: - total_size = 4 - - #write an unfinished header with a placeholder size - wave_file.write(struct.pack("<4sI4s", "RIFF", total_size, "WAVE")) - - #write the individual chunks - for chunk in chunk_iter: - total_size += chunk.write(wave_file) - - #once the chunks are done, go back and re-write the header - wave_file.seek(0, 0) - wave_file.write(struct.pack("<4sI4s", "RIFF", total_size, "WAVE")) - finally: - wave_file.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" - """ - - from .bitstream import BitstreamReader - from .bitstream import BitstreamRecorder - - head = BitstreamRecorder(1) - tail = BitstreamRecorder(1) - current_block = head - - wave_file = BitstreamReader(open(self.filename, 'rb'), 1) - try: - #transfer the 12-byte "RIFFsizeWAVE" header to head - (riff, size, wave) = wave_file.parse("4b 32u 4b") - if (riff != 'RIFF'): - raise InvalidWave(_(u"Not a RIFF WAVE file")) - elif (wave != 'WAVE'): - raise InvalidWave(_(u"Invalid RIFF WAVE file")) - else: - current_block.build("4b 32u 4b", (riff, size, wave)) - total_size = size - 4 - - while (total_size > 0): - #transfer each chunk header - (chunk_id, chunk_size) = wave_file.parse("4b 32u") - if (not frozenset(chunk_id).issubset(self.PRINTABLE_ASCII)): - raise InvalidWave(_(u"Invalid RIFF WAVE chunk ID")) - else: - current_block.build("4b 32u", (chunk_id, chunk_size)) - total_size -= 8 - - #round up chunk size to 16 bits - if (chunk_size % 2): - chunk_size += 1 - - #and transfer the full content of non-data chunks - if (chunk_id != "data"): - current_block.write_bytes(wave_file.read_bytes(chunk_size)) - else: - wave_file.skip_bytes(chunk_size) - current_block = tail - - total_size -= chunk_size - - return (head.data(), tail.data()) - 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) - - fmt_found = False - data_found = False - - for chunk in self.chunks(): - if (chunk.id == "fmt "): - if (not fmt_found): - fmt_found = True - else: - raise InvalidWave(_(u"multiple fmt chunks found")) - - elif (chunk.id == "data"): - if (not fmt_found): - raise InvalidWave(_(u"data chunk found before fmt")) - elif (data_found): - raise InvalidWave(_(u"multiple data chunks found")) - else: - data_found = True - - if (not chunk.verify()): - raise InvalidWave(_(u"truncated %s chunk found") % - (chunk.id.decode('ascii'))) - - if (not fmt_found): - raise InvalidWave(_(u"fmt chunk not found")) - if (not data_found): - raise InvalidWave(_(u"data chunk not found")) - - if (progress is not None): - progress(1, 1) - - return True - - def clean(self, fixes_performed, output_filename=None): - """cleans the file of known data and metadata problems - - fixes_performed is a list-like object which is appended - with Unicode strings of fixed problems - - output_filename is an optional filename of the fixed file - if present, a new AudioFile is returned - otherwise, only a dry-run is performed and no new file is written - - raises IOError if unable to write the file or its metadata - raises ValueError if the file has errors of some sort - """ - - chunk_queue = [] - pending_data = None - - for chunk in self.chunks(): - if (chunk.id == "fmt "): - if ("fmt " in [c.id for c in chunk_queue]): - fixes_performed.append( - _(u"multiple fmt chunks found")) - else: - chunk_queue.append(chunk) - if (pending_data is not None): - chunk_queue.append(pending_data) - pending_data = None - elif (chunk.id == "data"): - if ("fmt " not in [c.id for c in chunk_queue]): - fixes_performed.append(_(u"data chunk found before fmt")) - pending_data = chunk - elif ("data" in [c.id for c in chunk_queue]): - fixes_performed.append(_(u"multiple data chunks found")) - else: - chunk_queue.append(chunk) - else: - chunk_queue.append(chunk) - - if (output_filename is not None): - WaveAudio.wave_from_chunks(output_filename, chunk_queue) - return WaveAudio(output_filename)
View file
audiotools-2.18.tar.gz/audiotools/__wavpack__.py
Deleted
@@ -1,690 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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, subprocess, BIN, - open_files, os, ReplayGain, ignore_sigint, - transfer_data, transfer_framelist_data, - BufferedPCMReader, Image, MetaData, sheet_to_unicode, - calculate_replay_gain, ApeTagItem, - EncodingError, DecodingError, PCMReaderError, - PCMReader, ChannelMask, - InvalidWave, __default_quality__, - WaveContainer, to_pcm_progress) -from __wav__ import WaveAudio -from __ape__ import ApeTaggedAudio, ApeTag, __number_pair__ -import gettext - -gettext.install("audiotools", unicode=True) - - -class InvalidWavPack(InvalidFile): - pass - - -def __riff_chunk_ids__(data_size, data): - data_size = __Counter__(data_size) - data.add_callback(data_size.callback) - (riff, size, wave) = data.parse("4b 32u 4b") - if (riff != "RIFF"): - return - elif (wave != "WAVE"): - return - - while (int(data_size) > 0): - (chunk_id, chunk_size) = data.parse("4b 32u") - if ((chunk_size % 2) == 1): - chunk_size += 1 - yield chunk_id - if (chunk_id != 'data'): - data.skip_bytes(chunk_size) - - -class __Counter__: - def __init__(self, value): - self.value = value - - def callback(self, byte): - self.value -= 1 - - def __int__(self): - return self.value - - -####################### -#WavPack -####################### - - -class WavPackAudio(ApeTaggedAudio, WaveContainer): - """a WavPack audio file""" - - SUFFIX = "wv" - NAME = SUFFIX - DEFAULT_COMPRESSION = "standard" - COMPRESSION_MODES = ("veryfast", "fast", "standard", "high", "veryhigh") - COMPRESSION_DESCRIPTIONS = {"veryfast": _(u"fastest encode/decode, " + - u"worst compression"), - "veryhigh": _(u"slowest encode/decode, " + - u"best compression")} - - BITS_PER_SAMPLE = (8, 16, 24, 32) - SAMPLING_RATE = (6000, 8000, 9600, 11025, - 12000, 16000, 22050, 24000, - 32000, 44100, 48000, 64000, - 88200, 96000, 192000, 0) - - __options__ = {"veryfast": {"block_size": 44100, - "joint_stereo": True, - "false_stereo": True, - "wasted_bits": True, - "decorrelation_passes": 1}, - "fast": {"block_size": 44100, - "joint_stereo": True, - "false_stereo": True, - "wasted_bits": True, - "decorrelation_passes": 2}, - "standard": {"block_size": 44100, - "joint_stereo": True, - "false_stereo": True, - "wasted_bits": True, - "decorrelation_passes": 5}, - "high": {"block_size": 44100, - "joint_stereo": True, - "false_stereo": True, - "wasted_bits": True, - "decorrelation_passes": 10}, - "veryhigh": {"block_size": 44100, - "joint_stereo": True, - "false_stereo": True, - "wasted_bits": True, - "decorrelation_passes": 16}} - - def __init__(self, filename): - """filename is a plain string""" - - self.filename = filename - self.__samplerate__ = 0 - self.__channels__ = 0 - self.__bitspersample__ = 0 - self.__total_frames__ = 0 - - try: - self.__read_info__() - except IOError, msg: - raise InvalidWavPack(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""" - - return file.read(4) == 'wvpk' - - def lossless(self): - """returns True""" - - return True - - def channel_mask(self): - """returns a ChannelMask object of this track's channel layout""" - - return self.__channel_mask__ - - def get_metadata(self): - """returns a MetaData object, or None - - raises IOError if unable to read the file""" - - metadata = ApeTaggedAudio.get_metadata(self) - if (metadata is not None): - metadata.frame_count = self.total_frames() - return metadata - - 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""" - - for (sub_header, nondecoder, data_size, data) in self.sub_blocks(): - if ((sub_header == 1) and nondecoder): - if (set(__riff_chunk_ids__(data_size, data)) != - set(['fmt ', 'data'])): - return True - elif ((sub_header == 2) and nondecoder): - return True - else: - return False - - def blocks(self, reader=None): - """yields (length, reader) tuples of WavPack frames - - length is the total length of all the substreams - reader is a BitstreamReader which can be parsed - """ - - def blocks_iter(reader): - try: - while (True): - (wvpk, block_size) = reader.parse("4b 32u 192p") - if (wvpk == 'wvpk'): - yield (block_size - 24, - reader.substream(block_size - 24)) - else: - return - except IOError: - return - - if (reader is None): - from .bitstream import BitstreamReader - - reader = BitstreamReader(file(self.filename), 1) - try: - for block in blocks_iter(reader): - yield block - finally: - reader.close() - else: - for block in blocks_iter(reader): - yield block - - def sub_blocks(self, reader=None): - """yields (function, nondecoder, data_size, data) tuples - - function is an integer - nondecoder is a boolean indicating non-decoder data - data is a BitstreamReader which can be parsed - """ - - for (block_size, block_data) in self.blocks(reader): - while (block_size > 0): - (metadata_function, - nondecoder_data, - actual_size_1_less, - large_block) = block_data.parse("5u 1u 1u 1u") - - if (large_block): - sub_block_size = block_data.read(24) - block_size -= 4 - else: - sub_block_size = block_data.read(8) - block_size -= 2 - - if (actual_size_1_less): - yield (metadata_function, - nondecoder_data, - sub_block_size * 2 - 1, - block_data.substream(sub_block_size * 2 - 1)) - block_data.skip(8) - else: - yield (metadata_function, - nondecoder_data, - sub_block_size * 2, - block_data.substream(sub_block_size * 2)) - - block_size -= sub_block_size * 2 - - def __read_info__(self): - from .bitstream import BitstreamReader - - reader = BitstreamReader(file(self.filename, "rb"), 1) - reader.mark() - try: - (block_id, - total_samples, - bits_per_sample, - mono_output, - initial_block, - final_block, - sample_rate) = reader.parse( - "4b 64p 32u 64p 2u 1u 8p 1u 1u 5p 5p 4u 37p") - - if (block_id != 'wvpk'): - raise InvalidWavPack(_(u'WavPack header ID invalid')) - - if (sample_rate != 0xF): - self.__samplerate__ = WavPackAudio.SAMPLING_RATE[sample_rate] - else: - #if unknown, pull from SAMPLE_RATE sub-block - for (block_id, - nondecoder, - data_size, - data) in self.sub_blocks(reader): - if ((block_id == 0x7) and nondecoder): - self.__samplerate__ = data.read(data_size * 8) - break - else: - #no SAMPLE RATE sub-block found - #so pull info from FMT chunk - reader.rewind() - (self.__samplerate__,) = self.fmt_chunk(reader).parse( - "32p 32u") - - self.__bitspersample__ = [8, 16, 24, 32][bits_per_sample] - self.__total_frames__ = total_samples - - if (initial_block and final_block): - if (mono_output): - self.__channels__ = 1 - self.__channel_mask__ = ChannelMask(0x4) - else: - self.__channels__ = 2 - self.__channel_mask__ = ChannelMask(0x3) - else: - #if not mono or stereo, pull from CHANNEL INFO sub-block - reader.rewind() - for (block_id, - nondecoder, - data_size, - data) in self.sub_blocks(reader): - if ((block_id == 0xD) and not nondecoder): - self.__channels__ = data.read(8) - self.__channel_mask__ = ChannelMask( - data.read((data_size - 1) * 8)) - break - else: - #no CHANNEL INFO sub-block found - #so pull info from FMT chunk - reader.rewind() - fmt = self.fmt_chunk(reader) - compression_code = fmt.read(16) - self.__channels__ = fmt.read(16) - if (compression_code == 1): - #this is theoretically possible - #with very old .wav files, - #but shouldn't happen in practice - self.__channel_mask__ = { - 1: ChannelMask.from_fields( - front_center=True), - 2: ChannelMask.from_fields( - front_left=True, front_right=True), - 3: ChannelMask.from_fields( - front_left=True, front_right=True, - front_center=True), - 4: ChannelMask.from_fields( - front_left=True, front_right=True, - back_left=True, back_right=True), - 5: ChannelMask.from_fields( - front_left=True, front_right=True, - back_left=True, back_right=True, - front_center=True), - 6: ChannelMask.from_fields( - front_left=True, front_right=True, - back_left=True, back_right=True, - front_center=True, low_frequency=True) - }.get(self.__channels__, ChannelMask(0)) - elif (compression_code == 0xFFFE): - fmt.skip(128) - mask = fmt.read(32) - self.__channel_mask__ = ChannelMask(mask) - else: - raise InvalidWavPack(_(u"unsupported FMT compression")) - - finally: - reader.unmark() - reader.close() - - def bits_per_sample(self): - """returns an integer number of bits-per-sample this track contains""" - - return self.__bitspersample__ - - def channels(self): - """returns an integer number of channels this track contains""" - - return self.__channels__ - - def total_frames(self): - """returns the total PCM frames of the track as an integer""" - - return self.__total_frames__ - - def sample_rate(self): - """returns the rate of the track's audio as an integer number of Hz""" - - return self.__samplerate__ - - @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 WavPackAudio object""" - - from . import encoders - - if ((compression is None) or - (compression not in cls.COMPRESSION_MODES)): - compression = __default_quality__(cls.NAME) - - try: - encoders.encode_wavpack(filename, - BufferedPCMReader(pcmreader), - **cls.__options__[compression]) - - return cls(filename) - except (ValueError, IOError), msg: - cls.__unlink__(filename) - raise EncodingError(str(msg)) - except Exception, err: - cls.__unlink__(filename) - raise err - - 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""" - - from . import decoders - - try: - f = open(wave_filename, 'wb') - except IOError, msg: - raise EncodingError(str(msg)) - - (head, tail) = self.pcm_split() - - try: - f.write(head) - total_frames = self.total_frames() - current_frames = 0 - decoder = decoders.WavPackDecoder(self.filename) - frame = decoder.read(4096) - while (len(frame) > 0): - f.write(frame.to_bytes(False, self.bits_per_sample() > 8)) - current_frames += frame.frames - if (progress is not None): - progress(current_frames, total_frames) - frame = decoder.read(4096) - f.write(tail) - f.close() - except IOError, msg: - self.__unlink__(wave_filename) - raise EncodingError(str(msg)) - - def to_pcm(self): - """returns a PCMReader object containing the track's PCM data""" - - from . import decoders - - try: - return decoders.WavPackDecoder(self.filename) - except (IOError, ValueError), msg: - return PCMReaderError(error_message=str(msg), - sample_rate=self.__samplerate__, - channels=self.__channels__, - channel_mask=int(self.channel_mask()), - bits_per_sample=self.__bitspersample__) - - @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 WavPackAudio object""" - - from . import encoders - - if ((compression is None) or - (compression not in cls.COMPRESSION_MODES)): - compression = __default_quality__(cls.NAME) - - wave = WaveAudio(wave_filename) - - (head, tail) = wave.pcm_split() - - try: - encoders.encode_wavpack(filename, - to_pcm_progress(wave, progress), - wave_header=head, - wave_footer=tail, - **cls.__options__[compression]) - - return cls(filename) - except (ValueError, IOError), msg: - cls.__unlink__(filename) - raise EncodingError(str(msg)) - except Exception, err: - cls.__unlink__(filename) - raise err - - def pcm_split(self): - """returns a pair of data strings before and after PCM data""" - - head = None - tail = "" - - for (sub_block_id, nondecoder, data_size, data) in self.sub_blocks(): - if ((sub_block_id == 1) and nondecoder): - head = data.read_bytes(data_size) - elif ((sub_block_id == 2) and nondecoder): - tail = data.read_bytes(data_size) - - #build dummy head if none found in file - if (head is None): - import audiotools.bitstream as bitstream - - riff_head = bitstream.BitstreamRecorder(1) - - avg_bytes_per_second = (self.sample_rate() * - self.channels() * - (self.bits_per_sample() / 8)) - block_align = (self.channels() * - (self.bits_per_sample() / 8)) - total_size = 4 * 3 # "RIFF" + size + "WAVE" - - total_size += 4 * 2 # "fmt " + size - if ((self.channels() <= 2) or - (self.bits_per_sample() <= 16)): - # classic fmt chunk - fmt = "16u 16u 32u 32u 16u 16u" - else: - # extended fmt chunk - fmt = "16u 16u 32u 32u 16u 16u 16u 16u 32u 16b" - - total_size += bitstream.format_size(fmt) / 8 - - total_size += 4 * 2 # "data" + size - data_size = (self.total_frames() * - self.channels() * - (self.bits_per_sample() / 8)) - total_size += data_size - - total_size += len(tail) - - riff_head.build("4b 32u 4b 4b 32u", - ("RIFF", total_size - 8, "WAVE", - "fmt ", bitstream.format_size(fmt) / 8)) - if ((self.channels() <= 2) or - (self.bits_per_sample() <= 16)): - riff_head.build(fmt, - (1, # compression code - self.channels(), - self.sample_rate(), - avg_bytes_per_second, - block_align, - self.bits_per_sample())) - else: - riff_head.build(fmt, - (0xFFFE, # compression code - self.channels(), - self.sample_rate(), - avg_bytes_per_second, - block_align, - self.bits_per_sample(), - 22, # CB size - self.bits_per_sample(), - self.channel_mask(), - "\x01\x00\x00\x00\x00\x00\x10\x00" + - "\x80\x00\x00\xaa\x00\x38\x9b\x71")) - - riff_head.build("4b 32u", ("data", data_size)) - - head = riff_head.data() - - return (head, tail) - - def fmt_chunk(self, reader=None): - """returns the 'fmt' chunk as a BitstreamReader""" - - for (block_id, - nondecoder, - data_size, - data) in self.sub_blocks(reader): - if ((block_id == 1) and nondecoder): - (riff, wave) = data.parse("4b 32p 4b") - if ((riff != 'RIFF') or (wave != 'WAVE')): - raise InvalidWavPack(_(u'invalid FMT chunk')) - else: - while (True): - (chunk_id, chunk_size) = data.parse("4b 32u") - if (chunk_id == 'fmt '): - return data.substream(chunk_size) - elif (chunk_id == 'data'): - raise InvalidWavPack(_(u'invalid FMT chunk')) - else: - data.skip_bytes(chunk_size) - else: - raise InvalidWavPack(_(u'FMT chunk not found in WavPack')) - - @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 - """ - - tracks = [track for track in open_files(filenames) if - isinstance(track, cls)] - - if (len(tracks) > 0): - for (track, - track_gain, - track_peak, - album_gain, - album_peak) in calculate_replay_gain(tracks, progress): - metadata = track.get_metadata() - if (metadata is None): - metadata = ApeTag([]) - - metadata["replaygain_track_gain"] = ApeTagItem.string( - "replaygain_track_gain", - u"%+1.2f dB" % (track_gain)) - metadata["replaygain_track_peak"] = ApeTagItem.string( - "replaygain_track_peak", - u"%1.6f" % (track_peak)) - metadata["replaygain_album_gain"] = ApeTagItem.string( - "replaygain_album_gain", - u"%+1.2f dB" % (album_gain)) - metadata["replaygain_album_peak"] = ApeTagItem.string( - "replaygain_album_peak", - u"%1.6f" % (album_peak)) - - track.update_metadata(metadata) - - @classmethod - def can_add_replay_gain(cls): - """returns True""" - - return True - - @classmethod - def lossless_replay_gain(cls): - """returns True""" - - return True - - def replay_gain(self): - """returns a ReplayGain object of our ReplayGain values - - returns None if we have no values""" - - metadata = self.get_metadata() - if (metadata is None): - return None - - if (set(['replaygain_track_gain', 'replaygain_track_peak', - 'replaygain_album_gain', 'replaygain_album_peak']).issubset( - metadata.keys())): # we have ReplayGain data - try: - return ReplayGain( - unicode(metadata['replaygain_track_gain'])[0:-len(" dB")], - unicode(metadata['replaygain_track_peak']), - unicode(metadata['replaygain_album_gain'])[0:-len(" dB")], - unicode(metadata['replaygain_album_peak'])) - except ValueError: - return None - else: - return None - - def get_cuesheet(self): - """returns the embedded Cuesheet-compatible object, or None - - raises IOError if a problem occurs when reading the file""" - - import cue - - metadata = self.get_metadata() - - if ((metadata is not None) and ('Cuesheet' in metadata.keys())): - try: - return cue.parse(cue.tokens( - unicode(metadata['Cuesheet']).encode('utf-8', - 'replace'))) - except cue.CueException: - #unlike FLAC, just because a cuesheet is embedded - #does not mean it is compliant - return None - else: - return None - - def set_cuesheet(self, cuesheet): - """imports cuesheet data from a Cuesheet-compatible object - - this are objects with catalog(), ISRCs(), indexes(), and pcm_lengths() - methods. Raises IOError if an error occurs setting the cuesheet""" - - import os.path - import cue - - if (cuesheet is None): - return - - metadata = self.get_metadata() - if (metadata is None): - metadata = ApeTag.converted(MetaData()) - - metadata['Cuesheet'] = ApeTag.ITEM.string('Cuesheet', - cue.Cuesheet.file( - cuesheet, - os.path.basename(self.filename)).decode('ascii', 'replace')) - self.update_metadata(metadata)
View file
audiotools-2.18.tar.gz/audiotools/replaygain_old.py
Deleted
@@ -1,259 +0,0 @@ -#!/usr/bin/python - -#Audio Tools, a module and set of tools for manipulating audio data -#Copyright (C) 2007-2012 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 - - -#This is a module for ReplayGain calculation of a given PCM stream. -#It is included as a reference implementation and not as a substitute -#for external ReplayGain calculators. - -#The first problem with it is that the results are not identical -#to those of external calculators, by about a 100th of a dB or so. -#This is probably because the C-based implementations use floats -#while Python uses doubles. Thus the difference in rounding errors. - -#The second problem with it is it's very, very slow. -#Python is ill-suited to these kinds of rolling loop calculations -#involving thousands of samples per second, so the Python-based -#approach is several times slower than real-time. - - -import audiotools -import audiotools.pcmstream -from itertools import izip - -AYule = ((1.0, -3.8466461711806699, 7.81501653005538, -11.341703551320419, 13.055042193275449, -12.28759895145294, 9.4829380631978992, -5.8725786177599897, 2.7546586187461299, -0.86984376593551005, 0.13919314567432001), - (1.0, -3.4784594855007098, 6.3631777756614802, -8.5475152747187408, 9.4769360780128, -8.8149868137015499, 6.8540154093699801, -4.3947099607955904, 2.1961168489077401, -0.75104302451432003, 0.13149317958807999), - (1.0, -2.3789883497308399, 2.84868151156327, -2.6457717022982501, 2.2369765745171302, -1.67148153367602, 1.0059595480854699, -0.45953458054982999, 0.16378164858596, -0.050320777171309998, 0.023478974070199998), - (1.0, -1.6127316513724701, 1.0797749225997, -0.2565625775407, -0.1627671912044, -0.22638893773905999, 0.39120800788283999, -0.22138138954924999, 0.045002353873520001, 0.020058518065010002, 0.0030243909574099999), - (1.0, -1.4985897936779899, 0.87350271418187997, 0.12205022308084, -0.80774944671437998, 0.47854794562325997, -0.12453458140019, -0.040675101970140001, 0.083337552841070001, -0.042373480257460003, 0.029772073199250002), - (1.0, -0.62820619233671005, 0.29661783706366002, -0.37256372942400001, 0.0021376785712399998, -0.42029820170917997, 0.22199650564824, 0.0061342435068200002, 0.06747620744683, 0.057848203758010003, 0.032227540721730001), - (1.0, -1.0480033512634901, 0.29156311971248999, -0.26806001042946997, 0.0081999964585799997, 0.45054734505007998, -0.33032403314005998, 0.067393683331100004, -0.047842542290329998, 0.016399078361890002, 0.018073643235729998), - (1.0, -0.51035327095184002, -0.31863563325244998, -0.20256413484477001, 0.14728154134329999, 0.38952639978998999, -0.23313271880868, -0.052460190244630001, -0.025059617240530001, 0.02442357316099, 0.01818801111503), - (1.0, -0.25049871956019998, -0.43193942311113998, -0.034246810176749999, -0.046783287842420002, 0.26408300200954998, 0.15113130533215999, -0.17556493366449, -0.18823009262115001, 0.054777204286740003, 0.047044096881200002) - ) - -BYule = ((0.038575994352000001, -0.021603671841850001, -0.0012339531685100001, -9.2916779589999993e-05, -0.016552603416190002, 0.02161526843274, -0.02074045215285, 0.0059429806512499997, 0.0030642802319099998, 0.00012025322027, 0.0028846368391600001), - (0.054186564064300002, -0.029110078089480001, -0.0084870937985100006, -0.0085116564546900003, -0.0083499090493599996, 0.022452932533390001, -0.025963385129149998, 0.016248649629749999, -0.0024087905158400001, 0.0067461368224699999, -0.00187763777362), - (0.15457299681924, -0.093310490563149995, -0.062478801536530001, 0.021635418887979999, -0.05588393329856, 0.047814766749210001, 0.0022231259774300001, 0.031740925400489998, -0.013905894218979999, 0.00651420667831, -0.0088136273383899993), - (0.30296907319326999, -0.22613988682123001, -0.085873237307719993, 0.032829301726640003, -0.0091570293343400007, -0.02364141202522, -0.0058445603991300003, 0.062761013217490003, -8.2808674800000004e-06, 0.0020586188556400002, -0.029501349832869998), - (0.33642304856131999, -0.25572241425570003, -0.11828570177555001, 0.11921148675203, -0.078344896094790006, -0.0046997791438, -0.0058950022444000001, 0.057242281403510002, 0.0083204398077299999, -0.016353813845399998, -0.017601765681500001), - (0.44915256608449999, -0.14351757464546999, -0.22784394429749, -0.01419140100551, 0.040782627971389998, -0.12398163381747999, 0.04097565135648, 0.10478503600251, -0.01863887810927, -0.031934284389149997, 0.0054190774870700002), - (0.56619470757640999, -0.75464456939302005, 0.16242137742230001, 0.16744243493672001, -0.18901604199609001, 0.30931782841830002, -0.27562961986223999, 0.0064731067724599998, 0.086475037803509999, -0.037889845548399997, -0.0058821544342100001), - (0.58100494960552995, -0.53174909058578002, -0.14289799034253001, 0.17520704835522, 0.02377945217615, 0.15558449135572999, -0.25344790059353001, 0.016284624063329999, 0.069204677639589998, -0.03721611395801, -0.0074961879717200001), - (0.53648789255105001, -0.42163034350695999, -0.0027595361192900001, 0.042678422194150002, -0.10214864179676, 0.14590772289387999, -0.024598648593450002, -0.11202315195388, -0.04060034127, 0.047886655481800003, -0.02217936801134) - ) - -AButter = ((1.0, -1.9722337291952701, 0.97261396931305999), - (1.0, -1.96977855582618, 0.97022847566350001), - (1.0, -1.9583538097539801, 0.95920349965458995), - (1.0, -1.9500275914987799, 0.95124613669835001), - (1.0, -1.94561023566527, 0.94705070426117999), - (1.0, -1.9278328697703599, 0.93034775234267997), - (1.0, -1.91858953033784, 0.92177618768380998), - (1.0, -1.9154210807478, 0.91885558323625005), - (1.0, -1.88903307939452, 0.89487434461663995)) - -BButter = ((0.98621192462707996, -1.9724238492541599, 0.98621192462707996), - (0.98500175787241995, -1.9700035157448399, 0.98500175787241995), - (0.97938932735214002, -1.95877865470428, 0.97938932735214002), - (0.97531843204928004, -1.9506368640985701, 0.97531843204928004), - (0.97316523498161001, -1.94633046996323, 0.97316523498161001), - (0.96454515552826003, -1.9290903110565201, 0.96454515552826003), - (0.96009142950541004, -1.9201828590108201, 0.96009142950541004), - (0.95856916599601005, -1.9171383319920301, 0.95856916599601005), - (0.94597685600279002, -1.89195371200558, 0.94597685600279002)) - -SAMPLE_RATE_MAP = {48000:0,44100:1,32000:2,24000:3,22050:4, - 16000:5,12000:6,11025:7,8000:8} - - -PINK_REF = 64.82 - -class Filter: - def __init__(self, input_kernel, output_kernel): - self.input_kernel = input_kernel - self.output_kernel = output_kernel - - self.unfiltered_samples = [0.0] * len(self.input_kernel) - self.filtered_samples = [0.0] * len(self.output_kernel) - - #takes a list of floating point samples - #returns a list of filtered floating point samples - def filter(self, samples): - toreturn = [] - - input_kernel = tuple(reversed(self.input_kernel)) - output_kernel = tuple(reversed(self.output_kernel[1:])) - - for s in samples: - self.unfiltered_samples.append(s) - - filtered = sum([i * k for i,k in zip( - self.unfiltered_samples[-len(input_kernel):], - input_kernel)]) - \ - sum([i * k for i,k in zip( - self.filtered_samples[-len(output_kernel):], - output_kernel)]) - - self.filtered_samples.append(filtered) - toreturn.append(filtered) - - - #if we have more filtered and unfiltered samples than we'll need, - #chop off the excess at the beginning - if (len(self.unfiltered_samples) > (len(self.input_kernel))): - self.unfiltered_samples = self.unfiltered_samples[-len(self.input_kernel):] - - if (len(self.filtered_samples) > (len(self.output_kernel))): - self.filtered_samples = self.filtered_samples[-len(self.output_kernel):] - - return toreturn - - -MAX_ORDER = 10 - -class EqualLoudnessFilter(audiotools.PCMReader): - def __init__(self, pcmreader): - if (pcmreader.channels != 2): - raise ValueError("channels must equal 2") - if (pcmreader.sample_rate not in SAMPLE_RATE_MAP.keys()): - raise ValueError("unsupported sample rate") - - self.stream = audiotools.pcmstream.PCMStreamReader( - pcmreader, - pcmreader.bits_per_sample / 8, - False,True) - - audiotools.PCMReader.__init__( - self, - self.stream, - pcmreader.sample_rate, - 2, - pcmreader.bits_per_sample) - - self.leftover_samples = [] - - self.yule_filter_l = Filter( - BYule[SAMPLE_RATE_MAP[self.sample_rate]], - AYule[SAMPLE_RATE_MAP[self.sample_rate]]) - - self.yule_filter_r = Filter( - BYule[SAMPLE_RATE_MAP[self.sample_rate]], - AYule[SAMPLE_RATE_MAP[self.sample_rate]]) - - self.butter_filter_l = Filter( - BButter[SAMPLE_RATE_MAP[self.sample_rate]], - AButter[SAMPLE_RATE_MAP[self.sample_rate]]) - - self.butter_filter_r = Filter( - BButter[SAMPLE_RATE_MAP[self.sample_rate]], - AButter[SAMPLE_RATE_MAP[self.sample_rate]]) - - def read(self, bytes): - #read in a bunch of floating point samples - (frame_list,self.leftover_samples) = audiotools.FrameList.from_samples( - self.leftover_samples + self.stream.read(bytes), - self.channels) - - #convert them to a pair of floating-point channel lists - l_channel = frame_list.channel(0) - r_channel = frame_list.channel(1) - - #run our channel lists through the Yule and Butter filters - l_channel = self.butter_filter_l.filter( - self.yule_filter_l.filter(l_channel)) - - r_channel = self.butter_filter_r.filter( - self.yule_filter_r.filter(r_channel)) - - #convert our channel lists back to integer samples - multiplier = 1 << (self.bits_per_sample - 1) - - return audiotools.pcmstream.pcm_to_string( - audiotools.FrameList.from_channels( - ([int(round(s * multiplier)) for s in l_channel], - [int(round(s * multiplier)) for s in r_channel])), - self.bits_per_sample / 8, - False) - - -#this takes a PCMReader-compatible object -#it yields FrameLists, each 50ms long (1/20th of a second) -#how many PCM frames that is varies depending on the sample rate -def replay_gain_blocks(pcmreader): - unhandled_samples = [] #partial PCM frames - frame_pool = audiotools.FrameList([],pcmreader.channels) - - reader = audiotools.pcmstream.PCMStreamReader(pcmreader, - pcmreader.bits_per_sample / 8, - False,False) - - (framelist,unhandled_samples) = audiotools.FrameList.from_samples( - unhandled_samples + reader.read(audiotools.BUFFER_SIZE), - pcmreader.channels) - - while ((len(framelist) > 0) or (len(unhandled_samples) > 0)): - frame_pool.extend(framelist) - - while (frame_pool.total_frames() >= (pcmreader.sample_rate / 20)): - yield audiotools.FrameList( - frame_pool[0: - ((pcmreader.sample_rate / 20) * pcmreader.channels)], - pcmreader.channels) - frame_pool = audiotools.FrameList( - frame_pool[((pcmreader.sample_rate / 20) * pcmreader.channels):], - pcmreader.channels) - - (framelist,unhandled_samples) = audiotools.FrameList.from_samples( - unhandled_samples + reader.read(audiotools.BUFFER_SIZE), - pcmreader.channels) - - reader.close() - #this drops the last block that's not 50ms long - #that's probably the right thing to do - - -#takes a PCMReader-compatible object with 2 channels and a -#supported sample rate -#returns the stream's ReplayGain value in dB -def calculate_replay_gain(pcmstream): - import math - - def __mean__(l): - return sum(l) / len(l) - - pcmstream = EqualLoudnessFilter(pcmstream) - - db_blocks = [] - - for block in replay_gain_blocks(pcmstream): - left = __mean__([s ** 2 for s in block.channel(0)]) - right = __mean__([s ** 2 for s in block.channel(1)]) - db_blocks.append((left + right) / 2) - - db_blocks = [10 * math.log10(b + 10 ** -10) for b in db_blocks] - db_blocks.sort() - replay_gain = db_blocks[int(round(len(db_blocks) * 0.95))] - - return PINK_REF - replay_gain - - -if (__name__ == '__main__'): - pass -
View file
audiotools-2.18.tar.gz/docs/programming/source/audiotools_resample.rst
Deleted
@@ -1,41 +0,0 @@ -:mod:`audiotools.resample` --- the Resampler Module -=================================================== - -.. module:: audiotools.resample - :synopsis: a Module for Resampling PCM Data - - - -The :mod:`audiotools.resample` module contains a resampler for -modifying the sample rate of PCM data. -This class is not usually instantiated directly; -instead, one can use :class:`audiotools.PCMConverter` -which calculates the resampling ratio and handles unprocessed -samples automatically. - -Resampler Objects ------------------ - -.. class:: Resampler(channels, ratio, quality) - - This class performs the actual resampling and maintains the - resampler's state. - ``channels`` is the number of channels in the stream being resampled. - ``ratio`` is the new sample rate divided by the current sample rate. - ``quality`` is an integer value between 0 and 4, where 0 is the best - quality. - - For example, to convert a 2 channel, 88200Hz audio stream to - 44100Hz, one starts by building a resampler as follows: - - >>> resampler = Resampler(2, float(44100) / float(88200), 0) - -.. method:: Resampler.process(float_frame_list, last) - - Given a :class:`FloatFrameList` object and whether this - is the last chunk of PCM data from the stream, - returns a pair of new :class:`FloatFrameList` objects. - The first is the processed samples at the new rate. - The second is a set of unprocessed samples - which must be pushed through again on the next call to - :meth:`process`.
View file
audiotools-2.18.tar.gz/docs/reference/alac-codec.m4
Deleted
@@ -1,3 +0,0 @@ -include(header.m4) -\include{alac} -include(footer.m4)
View file
audiotools-2.18.tar.gz/docs/reference/audioformats.m4
Deleted
@@ -1,23 +0,0 @@ -include(header.m4) -\include{introduction} -\include{basics} -\include{wav} -\include{aiff} -\include{au} -\include{shorten} -\include{flac} -\include{wavpack} -\include{ape} -\include{mp3} -\include{m4a} -\include{alac} -\include{vorbis} -\include{oggflac} -\include{speex} -\include{musepack} -\include{dvda2} -\include{freedb} -\include{musicbrainz} -\include{musicbrainz_mmd} -\include{replaygain} -include(footer.m4)
View file
audiotools-2.18.tar.gz/docs/reference/dvda-codec.m4
Deleted
@@ -1,3 +0,0 @@ -include(header.m4) -\include{dvda2} -include(footer.m4)
View file
audiotools-2.18.tar.gz/docs/reference/figures/alac
Deleted
-(directory)
View file
audiotools-2.18.tar.gz/docs/reference/figures/ape/apev2_flags.bdx
Deleted
@@ -1,17 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<diagram> - <row> - <col start="0" end="4" width=".55">Undefined (0x00)</col> - <col start="5" end="6" width=".30">Encoding</col> - <col start="7" end="7" width=".15">Read-Only</col> - </row> - <row> - <col start="8" end="23">Undefined (0x00)</col> - </row> - <row> - <col start="24" end="24" width=".25">Container Header</col> - <col start="25" end="25" width=".25">Contains no Footer</col> - <col start="26" end="26" width=".25">Is Header</col> - <col start="27" end="31" width=".25">Undefined (0x00)</col> - </row> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/ape/apev2_item.bdx
Deleted
@@ -1,45 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<diagram> - <row> - <col style="dashed" end="255" start="0" width=".20">Header</col> - <col width=".20" id="item">Item₀</col> - <col width=".20">Item₁</col> - <col style="dashed" width=".20">...</col> - <col end="255" start="0" width=".20">Footer</col> - </row> - <spacer/> - <row> - <col start="0" end="31" width=".20" id="item_s">value size</col> - <col start="32" end="63" width=".20" id="flags">item flags</col> - <col start="64" width=".20">item key</col> - <col start="0" end="7" width=".20">terminator (0)</col> - <col width=".20" id="item_e">item value</col> - </row> - <spacer/> - <row> - <col start="0" end="0" width=".333333" id="flags_s">read only</col> - <col start="1" end="2" width=".333333">item encoding</col> - <col start="3" end="28" width=".333333" id="flags_e">undefined</col> - </row> - <row> - <col start="29" end="29" width=".333333">is header</col> - <col start="30" end="30" width=".333333">contains no footer</col> - <col start="31" end="31" width=".333333">contains a header</col> - </row> - <line style="dotted"> - <start id="item" corner="sw"/> - <end id="item_s" corner="nw"/> - </line> - <line style="dotted"> - <start id="item" corner="se"/> - <end id="item_e" corner="ne"/> - </line> - <line style="dotted"> - <start id="flags" corner="sw"/> - <end id="flags_s" corner="nw"/> - </line> - <line style="dotted"> - <start id="flags" corner="se"/> - <end id="flags_e" corner="ne"/> - </line> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/ape/apev2_tag.bdx
Deleted
@@ -1,9 +0,0 @@ -<?xml version="1.0" ?><diagram> - <row> - <col style="dashed" end="255" start="0" width=".20">Header</col> - <col width=".20">Item₀</col> - <col width=".20">Item₁</col> - <col style="dashed" width=".20">...</col> - <col end="255" start="0" width=".20">Footer</col> - </row> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/ape/apev2_tagheader.bdx
Deleted
@@ -1,59 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<diagram> - <row> - <col style="dashed" end="255" start="0" width=".20" id="header">Header</col> - <col width=".20">Item₀</col> - <col width=".20">Item₁</col> - <col style="dashed" width=".20">...</col> - <col end="255" start="0" width=".20" id="footer">Footer</col> - </row> - <spacer/> - <row> - <col start="0" end="63" - id="header_footer">preamble (`APETAGEX')</col> - </row> - <row> - <col start="64" end="95" width=".5">version (2000)</col> - <col start="96" end="127" width=".5">tag size</col> - </row> - <row> - <col start="128" end="159" width=".333333">item count</col> - <col start="160" end="191" width=".333333" id="flags">flags</col> - <col start="192" end="255" width=".333333">reserved</col> - </row> - <spacer/> - <row> - <col start="0" end="0" width=".333333" id="flags_s">read only</col> - <col start="1" end="2" width=".333333">item encoding</col> - <col start="3" end="28" width=".333333" id="flags_e">undefined</col> - </row> - <row> - <col start="29" end="29" width=".333333">is header</col> - <col start="30" end="30" width=".333333">contains no footer</col> - <col start="31" end="31" width=".333333">contains a header</col> - </row> - <line style="dotted" color="blue"> - <start id="header" corner="sw"/> - <end id="header_footer" corner="nw"/> - </line> - <line style="dotted" color="blue"> - <start id="header" corner="se"/> - <end id="header_footer" corner="ne"/> - </line> - <line style="dotted" color="green"> - <start id="footer" corner="sw"/> - <end id="header_footer" corner="nw"/> - </line> - <line style="dotted" color="green"> - <start id="footer" corner="se"/> - <end id="header_footer" corner="ne"/> - </line> - <line style="dotted"> - <start id="flags" corner="sw"/> - <end id="flags_s" corner="nw"/> - </line> - <line style="dotted"> - <start id="flags" corner="se"/> - <end id="flags_e" corner="ne"/> - </line> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/flac
Deleted
-(directory)
View file
audiotools-2.18.tar.gz/docs/reference/figures/flac/application.bdx
Deleted
@@ -1,7 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<diagram> - <row> - <col start="0" end="31" width=".25">application ID</col> - <col start="32" width=".75">application data</col> - </row> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/flac/constant.bdx
Deleted
@@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<diagram> - <row> - <col width=".333333" id="subframe">Subframe₀</col> - <col width=".333333">Subframe₁</col> - <col style="dashed" width=".333333">...</col> - </row> - <spacer/> - <row> - <col width=".1" start="0" end="0" id="subframe_s">pad</col> - <col width=".60" start="1" end="6">subframe type (0) </col> - <col width=".30" start="7" end="7" id="subframe_e">has wasted BPS (0)</col> - </row> - <row> - <col start="0" end="subframe bps - 1">constant</col> - </row> - <line style="dotted"> - <start id="subframe" corner="sw"/> - <end id="subframe_s" corner="nw"/> - </line> - <line style="dotted"> - <start id="subframe" corner="se"/> - <end id="subframe_e" corner="ne"/> - </line> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/flac/cuesheet.bdx
Deleted
@@ -1,34 +0,0 @@ -<?xml version="1.0" ?><diagram> - <row> - <col end="1023" start="0" width=".21">catalog number</col> - <col end="1087" start="1024" width=".22">lead-in samples</col> - <col end="1088" start="1088" width=".12">is cdda</col> - <col end="3159" start="1089" width=".10">NULL</col> - <col end="3167" start="3160" width=".15">track count</col> - <col id="track" start="3168" width=".10">Track₀</col> - <col style="dashed" width=".10">...</col> - </row> - <row><blank/></row> - <row> - <col end="63" id="track_offset" start="0" width=".14">offset</col> - <col end="71" start="64" width=".14">number</col> - <col end="167" start="72" width=".07">ISRC</col> - <col end="168" start="168" width=".07">type</col> - <col end="169" start="169" width=".14">pre-emph.</col> - <col end="279" start="170" width=".08">NULL</col> - <col end="287" start="280" width=".16">index points</col> - <col id="index" start="288" width=".10">Index₀</col> - <col id="track_end" style="dashed" width=".10">...</col> - </row> - <row><blank/></row> - <row> - <blank width=".54"/> - <col end="63" id="index_offset" start="0" width=".18">index offset</col> - <col end="71" start="64" width=".18">index number</col> - <col end="95" id="index_end" start="72" width=".10">NULL</col> - </row> - <line style="dotted"><start corner="sw" id="track"/><end corner="nw" id="track_offset"/></line> - <line style="dotted"><start corner="se" id="track"/><end corner="ne" id="track_end"/></line> - <line style="dotted"><start corner="sw" id="index"/><end corner="nw" id="index_offset"/></line> - <line style="dotted"><start corner="se" id="index"/><end corner="ne" id="index_end"/></line> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/flac/fixed.bdx
Deleted
@@ -1,35 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<diagram> - <row> - <col width=".333333" id="subframe">Subframe₀</col> - <col width=".333333">Subframe₁</col> - <col style="dashed" width=".333333">...</col> - </row> - <spacer/> - <row> - <col width=".1" start="0" end="0" id="subframe_s">pad</col> - <col width=".225" start="1" end="3">type (1)</col> - <col width=".225" start="4" end="6">order</col> - <col width=".2" start="7" end="7">has wasted BPS</col> - <col style="dashed" width=".25" id="subframe_e">wasted BPS (+1)</col> </row> - <row> - <col width=".283" start="0" - end="subframe bps - 1">warm-up sample₀</col> - <col width=".283" start="0" - end="subframe bps - 1">warm-up sample₁</col> - <col width=".283" start="0" - end="subframe bps - 1">warm-up sample₂</col> - <col width=".15" style="dashed">...</col> - </row> - <row> - <col>residual block</col> - </row> - <line style="dotted"> - <start id="subframe" corner="sw"/> - <end id="subframe_s" corner="nw"/> - </line> - <line style="dotted"> - <start id="subframe" corner="se"/> - <end id="subframe_e" corner="ne"/> - </line> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/flac/frames.bdx
Deleted
@@ -1,51 +0,0 @@ -<?xml version="1.0" ?> -<diagram> - <row> - <col width=".333333" id="frame">Frame₀</col> - <col width=".333333">Frame₁</col> - <col style="dashed" width=".333333">...</col> - </row> - <spacer/> - <row> - <col width=".25" id="frame_s">Frame Header</col> - <col width=".25">Subframe₀</col> - <col width=".25">Subframe₁</col> - <col width=".1" style="dashed">...</col> - <col width=".15" style="dashed" id="frame_e">byte-align</col> - </row> - <spacer/> - <row> - <col end="13" start="0" width=".55" id="frame_h_s">sync code (0x3FFE)</col> - <col end="14" start="14" width=".20">reserved (0)</col> - <col end="15" start="15" width=".25" id="frame_h_e">blocking strategy</col> - </row> - <row> - <col end="19" start="16" width=".20">block size</col> - <col end="23" start="20" width=".20">sample rate</col> - <col end="27" start="24" width=".25">channel assignment</col> - <col end="30" start="28" width=".20">bits per sample</col> - <col end="31" start="31" width=".15">padding</col> - </row> - <row> - <col end="39-87" start="32" width=".30">sample/frame number</col> - <col end="0/7/15" start="0" style="dashed" width=".25">block size</col> - <col end="0/7/15" start="0" style="dashed" width=".25">sample rate</col> - <col end="7" start="0" width=".20">CRC-8</col> - </row> - <line style="dotted"> - <start id="frame" corner="sw"/> - <end id="frame_s" corner="nw"/> - </line> - <line style="dotted"> - <start id="frame" corner="se"/> - <end id="frame_e" corner="ne"/> - </line> - <line style="dotted"> - <start id="frame_s" corner="sw"/> - <end id="frame_h_s" corner="nw"/> - </line> - <line style="dotted"> - <start id="frame_s" corner="se"/> - <end id="frame_h_e" corner="ne"/> - </line> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/flac/lpc-parse.bpx
Deleted
@@ -1,15 +0,0 @@ -<?xml version="1.0" ?> -<struct endianness="big"> - <field size="1" value="0">pad</field> - <field size="6" value="100010b">type and order</field> - <field size="1" value="0">wasted</field> - <field size="16" value="43">warm-up sample₀</field> - <field size="16" value="48">warm-up sample₁</field> - <field size="16" value="50">warm-up sample₂</field> - <field size="4" value="11">QLP precision (+1)</field> - <field size="5" value="10">QLP shift needed</field> - <field size="12" value="1451">QLP coefficient₀</field> - <field size="12" value="0xebd">QLP coefficient₁</field> - <field size="12" value="0xf92">QLP coefficient₂</field> - <field size="28" value="0000010010001001001101110101b">residual block</field> -</struct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/flac/lpc.bdx
Deleted
@@ -1,43 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<diagram> - <row> - <col width=".333333" id="subframe">Subframe₀</col> - <col width=".333333">Subframe₁</col> - <col style="dashed" width=".333333">...</col> - </row> - <spacer/> - <row> - <col width=".1" start="0" end="0" id="subframe_s">pad</col> - <col width=".225" start="1" end="1">type (1)</col> - <col width=".225" start="2" end="6">order</col> - <col width=".2" start="7" end="7">has wasted BPS</col> - <col style="dashed" width=".25" id="subframe_e">wasted BPS (+1)</col> - </row> - <row> - <col start="0" end="subframe bps - 1" width=".25">warm-up sample₀</col> - <col start="0" end="subframe bps - 1" width=".25">warm-up sample₁</col> - <col start="0" end="subframe bps - 1" width=".25">warm-up sample₂</col> - <col width=".25" style="dashed">...</col> - </row> - <row> - <col start="0" end="3" width=".40">QLP precision (+1)</col> - <col start="4" end="8" width=".60">QLP shift needed</col> - </row> - <row> - <col start="0" end ="QLP precision - 1" width=".25">QLP coefficient₀</col> - <col start="0" end ="QLP precision - 1" width=".25">QLP coefficient₁</col> - <col start="0" end ="QLP precision - 1" width=".25">QLP coefficient₂</col> - <col style="dashed" width=".25">...</col> - </row> - <row> - <col>residual block</col> - </row> - <line style="dotted"> - <start id="subframe" corner="sw"/> - <end id="subframe_s" corner="nw"/> - </line> - <line style="dotted"> - <start id="subframe" corner="se"/> - <end id="subframe_e" corner="ne"/> - </line> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/flac/picture.bdx
Deleted
@@ -1,30 +0,0 @@ -<?xml version="1.0" ?><diagram> - <row> - <col end="31" start="0" width=".15">picture type</col> - <col id="mime" start="32" width=".10">MIME</col> - <col id="desc" width=".15">dDescription</col> - <col end="31" start="0" width=".12">width</col> - <col end="63" start="32" width=".12">height</col> - <col end="95" start="64" width=".12">depth</col> - <col end="127" start="96" width=".12">count</col> - <col id="data" start="128" width=".12">Data</col> - </row> - <row><blank/></row> - <row> - <blank width=".025"/> - <col end="31" id="mime_l" start="0" width=".10">length</col> - <col id="mime_s" start="32" width=".10">string</col> - <blank width=".05"/> - <col end="31" id="desc_l" start="0" width=".10">length</col> - <col id="desc_s" start="32" width=".10">string</col> - <blank width=".325"/> - <col end="31" id="data_l" start="0" width=".10">length</col> - <col id="data_s" start="32" width=".10">data</col> - </row> - <line style="dotted"><start corner="sw" id="mime"/><end corner="nw" id="mime_l"/></line> - <line style="dotted"><start corner="se" id="mime"/><end corner="ne" id="mime_s"/></line> - <line style="dotted"><start corner="sw" id="desc"/><end corner="nw" id="desc_l"/></line> - <line style="dotted"><start corner="se" id="desc"/><end corner="ne" id="desc_s"/></line> - <line style="dotted"><start corner="sw" id="data"/><end corner="nw" id="data_l"/></line> - <line style="dotted"><start corner="se" id="data"/><end corner="ne" id="data_s"/></line> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/flac/residual-parse.bpx
Deleted
@@ -1,26 +0,0 @@ -<?xml version="1.0" ?> -<struct endianness="big"> - <field size="2" value="00b">coding</field> - <field size="4" value="0000b">partition order</field> - <field size="4" value="0010b">Rice parameter</field> - <field size="1" value="1b">MSB₀</field> - <field size="2" value="11b">LSB₀</field> - <field size="2" value="01b">MSB₁</field> - <field size="2" value="10b">LSB₁</field> - <field size="1" value="1b">MSB₂</field> - <field size="2" value="01b">LSB₂</field> - <field size="3" value="001b">MSB₃</field> - <field size="2" value="01b">LSB₃</field> - <field size="1" value="1b">MSB₄</field> - <field size="2" value="10b">LSB₄</field> - <field size="3" value="001b">MSB₅</field> - <field size="2" value="01b">LSB₅</field> - <field size="3" value="001b">MSB₆</field> - <field size="2" value="00b">LSB₆</field> - <field size="1" value="1b">MSB₇</field> - <field size="2" value="11b">LSB₇</field> - <field size="2" value="01b">MSB₈</field> - <field size="2" value="01b">LSB₈</field> - <field size="1" value="1b">MSB₉</field> - <field size="2" value="10b">LSB₉</field> -</struct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/flac/seektable.bdx
Deleted
@@ -1,15 +0,0 @@ -<?xml version="1.0" ?><diagram> - <row> - <col end="143" id="seekpoint" name="seekpoint" start="0" width=".333333">Seekpoint₀</col> - <col end="287" start="144" width=".333333">Seekpoint₁</col> - <col style="dashed" width=".333333">...</col> - </row> - <row><blank/></row> - <row> - <col end="63" id="sample_number" start="0" width=".40">sample number in target frame</col> - <col end="127" start="64" width=".36">byte offset to frame header</col> - <col end="143" id="samples_in_frame" start="128" width=".24">samples in frame</col> - </row> - <line style="dotted"><start corner="sw" id="seekpoint"/><end corner="nw" id="sample_number"/></line> - <line style="dotted"><start corner="se" id="seekpoint"/><end corner="ne" id="samples_in_frame"/></line> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/flac/stream.bdx
Deleted
@@ -1,21 +0,0 @@ -<?xml version="1.0" ?><diagram> - <row> - <col end="31" start="0" width=".20">header (‘fLaC’)</col> - <col id="metadata" start="32" width=".133333">Metadata₀</col> - <col width=".133333">Metadata₁</col> - <col style="dashed" width=".133333">...</col> - <col width=".133333">Frame₀</col> - <col width=".133333">Frame₁</col> - <col style="dashed" width=".133333">...</col> - </row> - <row><blank/></row> - <row> - <blank width=".05"/> - <col end="0" id="last" start="0" width=".10">last</col> - <col end="7" start="1" width=".18">block type</col> - <col end="31" start="8" width=".18">block length</col> - <col id="data" start="32" style="dashed" width=".40">block data</col> - </row> - <line style="dotted"><start corner="sw" id="metadata"/><end corner="nw" id="last"/></line> - <line style="dotted"><start corner="se" id="metadata"/><end corner="ne" id="data"/></line> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/flac/streaminfo.bdx
Deleted
@@ -1,22 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<diagram> - <row> - <col start="0" end="15" width=".5">minimum block size (in samples)</col> - <col start="16" end="31" width=".5">maximum block size (in samples)</col> - </row> - <row> - <col start="32" end="55" width=".5">minimum frame size (in bytes)</col> - <col start="56" end="79" width=".5">maximum frame size (in bytes)</col> - </row> - <row> - <col start="80" end="99" width=".60">sample rate</col> - <col start="100" end="102" width=".20">channels</col> - <col start="103" end="107" width=".20">bits per sample</col> - </row> - <row> - <col start="108" end="143">total samples</col> - </row> - <row> - <col start="144" end="271">MD5SUM of PCM data</col> - </row> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/flac/subframes.bdx
Deleted
@@ -1,26 +0,0 @@ -<?xml version="1.0" ?> -<diagram> - <row> - <col width=".333333" id="subframe">Subframe₀</col> - <col width=".333333">Subframe₁</col> - <col style="dashed" width=".333333">...</col> - </row> - <spacer/> - <row> - <col width=".1" start="0" end="0" id="subframe_s">pad</col> - <col width=".45" start="1" end="6">subframe type and order</col> - <col width=".2" start="7" end="7">has wasted BPS</col> - <col style="dashed" width=".25" id="subframe_e">wasted BPS (+1)</col> - </row> - <row> - <col id="subframe_data">subframe data</col> - </row> - <line style="dotted"> - <start id="subframe" corner="sw"/> - <end id="subframe_s" corner="nw"/> - </line> - <line style="dotted"> - <start id="subframe" corner="se"/> - <end id="subframe_e" corner="ne"/> - </line> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/flac/verbatim.bdx
Deleted
@@ -1,31 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<diagram> - <row> - <col width=".333333" id="subframe">Subframe₀</col> - <col width=".333333">Subframe₁</col> - <col style="dashed" width=".333333">...</col> - </row> - <spacer/> - <row> - <col width=".1" start="0" end="0" id="subframe_s">pad</col> - <col width=".60" start="1" end="6">subframe type (1) </col> - <col width=".30" start="7" end="7" id="subframe_e">has wasted BPS (0)</col> - </row> - <row> - <col width=".283" start="0" - end="subframe bps - 1">uncompressed sample₀</col> - <col width=".283" start="0" - end="subframe bps - 1">uncompressed sample₁</col> - <col width=".283" start="0" - end="subframe bps - 1">uncompressed sample₂</col> - <col width=".15" style="dashed">...</col> - </row> - <line style="dotted"> - <start id="subframe" corner="sw"/> - <end id="subframe_s" corner="nw"/> - </line> - <line style="dotted"> - <start id="subframe" corner="se"/> - <end id="subframe_e" corner="ne"/> - </line> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/flac/vorbiscomment.bdx
Deleted
@@ -1,21 +0,0 @@ -<?xml version="1.0" ?><diagram> - <row> - <col id="vendorstring" width=".17">Vendor String</col> - <col end="31" start="0" width=".20">total comments</col> - <col width=".21">Comment String₀</col> - <col id="comment" width=".21">Comment String₁</col> - <col style="dashed" width=".21">...</col> - </row> - <row><blank/></row> - <row> - <col end="31" id="v_len" start="0" width=".25">vendor string length</col> - <col id="v_str" start="32" width=".20">vendor string</col> - <blank width=".05"/> - <col end="31" id="c_len" start="0" width=".30">comment string length</col> - <col id="c_str" start="0" width=".20">comment string</col> - </row> - <line style="dotted"><start corner="sw" id="vendorstring"/><end corner="nw" id="v_len"/></line> - <line style="dotted"><start corner="se" id="vendorstring"/><end corner="ne" id="v_str"/></line> - <line style="dotted"><start corner="sw" id="comment"/><end corner="nw" id="c_len"/></line> - <line style="dotted"><start corner="se" id="comment"/><end corner="ne" id="c_str"/></line> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/ogg_stream.bdx
Deleted
@@ -1,35 +0,0 @@ -<?xml version="1.0" ?><diagram> - <row> - <col id="oggpage" width=".25">Ogg Page₀</col> - <col width=".25">Ogg Page₁</col> - <col width=".25">Ogg Page₂</col> - <col style="dashed" width=".25">...</col> - </row> - <row><blank/></row> - <row> - <col end="31" id="magic" start="0">magic number `OggS' (0x4F676753)</col> - </row> - <row> - <col end="39" start="32" width=".333333">version (0x00)</col> - <col end="47" start="40" width=".333333">header type</col> - <col end="111" start="48" width=".333333">granule position</col> - </row> - <row> - <col end="143" start="112" width=".333333">bitstream serial number</col> - <col end="175" start="144" width=".333333">page sequence number</col> - <col end="207" start="176" width=".333333">checksum</col> - </row> - <row> - <col end="215" start="208" width=".25">page segments</col> - <col end="223" start="216" width=".25">segment length₀</col> - <col style="dashed" width=".25">...</col> - <col width=".25">segment lengthₓ</col> - </row> - <row> - <col width=".333333">segment₀</col> - <col style="dashed" width=".333333">...</col> - <col width=".333333">segmentₓ</col> - </row> - <line style="dotted"><start corner="sw" id="oggpage"/><end corner="nw" id="magic"/></line> - <line style="dotted"><start corner="se" id="oggpage"/><end corner="ne" id="magic"/></line> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/shorten
Deleted
-(directory)
View file
audiotools-2.18.tar.gz/docs/reference/figures/wavpack
Deleted
-(directory)
View file
audiotools-2.18.tar.gz/docs/reference/figures/wavpack/decorrelation0.plot
Deleted
@@ -1,11 +0,0 @@ -#!/usr/bin/gnuplot - -set terminal fig color -set title "Bitstream Values" -set xlabel "i" -set ylabel "value" -set border 3 -set xtics nomirror -set ytics nomirror - -plot "figures/wavpack/decorr_pass0.dat" title 'residual' with lines;
View file
audiotools-2.18.tar.gz/docs/reference/figures/wavpack/decorrelation1.plot
Deleted
@@ -1,11 +0,0 @@ -#!/usr/bin/gnuplot - -set terminal fig color -set title "Decorrelation Pass 1" -set xlabel "i" -set ylabel "value" -set border 3 -set xtics nomirror -set ytics nomirror - -plot "figures/wavpack/decorr_pass1.dat" title 'pass 1' with lines;
View file
audiotools-2.18.tar.gz/docs/reference/figures/wavpack/decorrelation2.plot
Deleted
@@ -1,11 +0,0 @@ -#!/usr/bin/gnuplot - -set terminal fig color -set title "Decorrelation Pass 2" -set xlabel "i" -set ylabel "value" -set border 3 -set xtics nomirror -set ytics nomirror - -plot "figures/wavpack/decorr_pass2.dat" title 'pass 2' with lines;
View file
audiotools-2.18.tar.gz/docs/reference/figures/wavpack/decorrelation3.plot
Deleted
@@ -1,11 +0,0 @@ -#!/usr/bin/gnuplot - -set terminal fig color -set title "Decorrelation Pass 3" -set xlabel "i" -set ylabel "value" -set border 3 -set xtics nomirror -set ytics nomirror - -plot "figures/wavpack/decorr_pass3.dat" title 'pass 3' with lines;
View file
audiotools-2.18.tar.gz/docs/reference/figures/wavpack/decorrelation4.plot
Deleted
@@ -1,11 +0,0 @@ -#!/usr/bin/gnuplot - -set terminal fig color -set title "Decorrelation Pass 4" -set xlabel "i" -set ylabel "value" -set border 3 -set xtics nomirror -set ytics nomirror - -plot "figures/wavpack/decorr_pass4.dat" title 'pass 4' with lines;
View file
audiotools-2.18.tar.gz/docs/reference/figures/wavpack/decorrelation5.plot
Deleted
@@ -1,11 +0,0 @@ -#!/usr/bin/gnuplot - -set terminal fig color -set title "Decorrelation Pass 5" -set xlabel "i" -set ylabel "value" -set border 3 -set xtics nomirror -set ytics nomirror - -plot "figures/wavpack/decorr_pass5.dat" title 'pass 5' with lines;
View file
audiotools-2.18.tar.gz/docs/reference/figures/wavpack/decorrelation_passes.plot
Deleted
@@ -1,11 +0,0 @@ -#!/usr/bin/gnuplot - -set terminal fig color -set title "decorrelation passes" -set xlabel "time" -set ylabel "intensity" -set border 3 -set xtics nomirror -set ytics nomirror - -plot "figures/wavpack/decorrelation0.dat" title 'residuals' with steps,"figures/wavpack/decorrelation1.dat" title 'pass 1' with steps, "figures/wavpack/decorrelation2.dat" title 'pass 2' with steps, "figures/wavpack/decorrelation3.dat" title 'pass 3' with steps, "figures/wavpack/decorrelation4.dat" title 'pass 4' with steps;
View file
audiotools-2.18.tar.gz/docs/reference/figures/wavpack/extended_integers.bdx
Deleted
@@ -1,36 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<diagram> - <row> - <col end="255" start="0" width=".20">Block Header</col> - <col start="256" width=".16">Sub Block₀</col> - <col width=".16">Sub Block₁</col> - <col width=".16">Sub Block₂</col> - <col width=".16">Sub Block₃</col> - <col width=".16" id="subblock">Sub Block₄</col> - </row> - <spacer/> - <row> - <col start="0" end="4" width=".34" - id="subblock_s">metadata function (9)</col> - <col start="5" end="5" width=".22">nondecoder (0)</col> - <col start="6" end="6" width=".22">actual size 1 less</col> - <col start="7" end="7" width=".22" id="subblock_e">large sub block</col> - </row> - <row> - <col start="8" end="15">sub block size (16)</col> - </row> - <row> - <col start="16" end="23" width=".25">Sent Bits</col> - <col start="24" end="31" width=".25">Zero Bits</col> - <col start="32" end="39" width=".25">One Bits</col> - <col start="40" end="47" width=".25">Duplicate Bits</col> - </row> - <line style="dotted"> - <start id="subblock" corner="sw"/> - <end id="subblock_s" corner="nw"/> - </line> - <line style="dotted"> - <start id="subblock" corner="se"/> - <end id="subblock_e" corner="ne"/> - </line> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/wavpack/md5sum.bdx
Deleted
@@ -1,32 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<diagram> - <row> - <col end="255" start="0" width=".20">Block Header</col> - <col start="256" width=".16">Sub Block₀</col> - <col width=".16">Sub Block₁</col> - <col width=".16">Sub Block₂</col> - <col width=".16">Sub Block₃</col> - <col width=".16" id="subblock">Sub Block₄</col> - </row> - <spacer/> - <row> - <col start="0" end="4" width=".28" - id="subblock_s">metadata function (6)</col> - <col start="5" end="5" width=".25">nondecoder data (1)</col> - <col start="6" end="6" width=".25">actual size 1 less (0)</col> - <col start="7" end="7" width=".22" - id="subblock_e">large block (0)</col> - </row> - <row> - <col start="8" end="15" width=".25">block size (16)</col> - <col width=".75" start="16" end="271">MD5 sum</col> - </row> - <line style="dotted"> - <start id="subblock" corner="sw"/> - <end id="subblock_s" corner="nw"/> - </line> - <line style="dotted"> - <start id="subblock" corner="se"/> - <end id="subblock_e" corner="ne"/> - </line> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/wavpack/subblock.bdx
Deleted
@@ -1,30 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<diagram> - <row> - <col end="255" start="0" width=".22">Block Header</col> - <col start="256" width=".22" id="subblock">Sub Block₀</col> - <col width=".22">Sub Block₁</col> - <col style="dashed" width=".22">...</col> - </row> - <spacer/> - <row> - <col start="0" end="4" width=".34" id="subblock_s">metadata function</col> - <col start="5" end="5" width=".22">nondecoder data</col> - <col start="6" end="6" width=".22">actual size 1 less</col> - <col start="7" end="7" width=".22" id="subblock_e">large sub block</col> - </row> - <row> - <col start="8" end="15/31" width=".5">sub block size</col> - <col width=".5" style="dashed" - start="(sub block size × 2) bytes" - end="(sub block size × 2) bytes">sub block data</col> - </row> - <line style="dotted"> - <start id="subblock" corner="sw"/> - <end id="subblock_s" corner="nw"/> - </line> - <line style="dotted"> - <start id="subblock" corner="se"/> - <end id="subblock_e" corner="ne"/> - </line> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/wavpack/typical_block.bdx
Deleted
@@ -1,61 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<diagram> - <row> - <col width=".20">Block₀</col> - <col width=".20" id="block">Block₁</col> - <col width=".20">Block₂</col> - <col width=".20">Block₃</col> - <col width=".20" style="dashed"/> - </row> - <spacer/> - <row> - <col width=".25" start="0" end="255" id="block_header">Block Header</col> - <col width=".15" id="subblock_0">Sub Block₀</col> - <col width=".15">Sub Block₁</col> - <col width=".15">Sub Block₂</col> - <col width=".15">Sub Block₃</col> - <col width=".15" id="subblock_4">Sub Block₄</col> - </row> - <spacer height=".5"/> - <row> - <col width=".2" style="blank"/> - <col width=".3" id="subblocks_s">Sub Block Header</col> - <col width=".5" id="subblocks_e">Decorrelation Terms and Deltas</col> - </row> - <row> - <col width=".2" style="blank"/> - <col width=".3">Sub Block Header</col> - <col width=".5">Decorrelation Weights</col> - </row> - <row> - <col width=".2" style="blank"/> - <col width=".3">Sub Block Header</col> - <col width=".5">Decorrelation Samples</col> - </row> - <row> - <col width=".2" style="blank"/> - <col width=".3">Sub Block Header</col> - <col width=".5">Entropy Variables</col> - </row> - <row> - <col width=".2" style="blank"/> - <col width=".3">Sub Block Header</col> - <col width=".5">Bitstream</col> - </row> - <line style="dotted"> - <start id="block" corner="sw"/> - <end id="block_header" corner="nw"/> - </line> - <line style="dotted"> - <start id="block" corner="se"/> - <end id="subblock_4" corner="ne"/> - </line> - <line style="dotted"> - <start id="subblock_0" corner="sw"/> - <end id="subblocks_s" corner="nw"/> - </line> - <line style="dotted"> - <start id="subblock_0" corner="se"/> - <end id="subblocks_e" corner="ne"/> - </line> -</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/flac-codec.m4
Deleted
@@ -1,3 +0,0 @@ -include(header.m4) -\include{flac} -include(footer.m4)
View file
audiotools-2.18.tar.gz/docs/reference/header.m4
Deleted
@@ -1,77 +0,0 @@ -%This work is licensed under the -%Creative Commons Attribution-Share Alike 3.0 United States License. -%To view a copy of this license, visit -%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to -%Creative Commons, -%171 Second Street, Suite 300, -%San Francisco, California, 94105, USA. - -\documentclass[PAPERSIZE]{scrbook} -\setlength{\pdfpagewidth}{\paperwidth} -\setlength{\pdfpageheight}{\paperheight} -\setlength{\textwidth}{6in} -\usepackage{amsmath} -\usepackage{graphicx} -\usepackage{picins} -\usepackage{fancyvrb} -\usepackage{relsize} -\usepackage{array} -\usepackage{wrapfig} -\usepackage{subfig} -\usepackage{multicol} -\usepackage{paralist} -\usepackage{textcomp} -\usepackage{fancyvrb} -\usepackage{multirow} -\usepackage{rotating} -\usepackage[toc,page]{appendix} -\usepackage{hyperref} -\usepackage{units} -\usepackage{color} -\definecolor{gray}{rgb}{0.5,0.5,0.5} -\definecolor{red}{rgb}{1.0,0.0,0.0} -\definecolor{orange}{rgb}{1.0,0.4,0.0} -\definecolor{fuchsia}{rgb}{1.0,0.0,1.0} -\definecolor{blue}{rgb}{0.0,0.0,1.0} -\definecolor{green}{rgb}{0.0,1.0,0.0} -\definecolor{darkgreen}{rgb}{0.0,0.75,0.0} -\usepackage[vlined,lined,commentsnumbered]{algorithm2e} -\usepackage{lscape} -\newcommand{\xor}{\textbf{ xor }} -%#1 = i -%#2 = byte -%#3 = previous checksum -%#4 = shift results -%#5 = new xor -%#6 = new CRC-16 -\newcommand{\CRCSIXTEEN}[6]{\text{checksum}_{#1} &= \text{CRC16}(\texttt{#2}\xor(\texttt{#3} \gg \texttt{8}))\xor(\texttt{#3} \ll \texttt{8}) = \text{CRC16}(\texttt{#4})\xor \texttt{#5} = \texttt{#6}} -\newcommand{\LINK}[1]{\href{#1}{\texttt{#1}}} -\newcommand{\REFERENCE}[2]{\item #1 \\ \LINK{#2}} -\newcommand{\VAR}[1]{``{#1}''} -\newcommand{\ATOM}[1]{\texttt{#1}} -\newcommand{\IDOTS}{\mathrel{\ldotp\ldotp}} -\newcommand{\ALGORITHM}[2]{\begin{algorithm}[H] - \DontPrintSemicolon - \SetKw{READ}{read} - \SetKw{WRITE}{write} - \SetKw{UNARY}{read unary} - \SetKw{WUNARY}{write unary} - \SetKw{SKIP}{skip} - \SetKw{ASSERT}{assert} - \SetKw{IN}{in} - \KwIn{#1} - \KwOut{#2} - \BlankLine -} -\newcommand{\EALGORITHM}{\end{algorithm}} -\long\def\symbolfootnote[#1]#2{\begingroup% - \def\thefootnote{\fnsymbol{footnote}}\footnote[#1]{#2}\endgroup} -\long\def\symbolfootnotemark[#1]{\begingroup% - \def\thefootnote{\fnsymbol{footnote}}\footnotemark[#1]\endgroup} -\long\def\symbolfootnotetext[#1]#2{\begingroup% - \def\thefootnote{\fnsymbol{footnote}}\footnotetext[#1]{#2}\endgroup} -\title{Audio Formats Reference} -\author{Brian Langenberger} -\begin{document} -\maketitle -\tableofcontents
View file
audiotools-2.18.tar.gz/docs/reference/shorten-codec.m4
Deleted
@@ -1,3 +0,0 @@ -include(header.m4) -\include{shorten} -include(footer.m4)
View file
audiotools-2.18.tar.gz/docs/reference/wavpack-codec.m4
Deleted
@@ -1,3 +0,0 @@ -include(header.m4) -\include{wavpack} -include(footer.m4)
View file
audiotools-2.18.tar.gz/src/Makefile.sa
Deleted
@@ -1,55 +0,0 @@ -#This makefile is for generating debug standalone executables - -VERSION = "2.18" -OBJS = array.o pcm.o bitstream.o -FLAGS = -Wall -g - -all: flacenc alacenc wvenc mlpdec huffman bitstream - -flacenc: $(OBJS) encoders/flac.c encoders/flac.h flac_crc.o md5.o pcmconv.o - $(CC) $(FLAGS) -DVERSION=$(VERSION) -o flacenc encoders/flac.c $(OBJS) md5.o flac_crc.o pcmconv.o -DSTANDALONE -lm - -alacenc: $(OBJS) encoders/alac.c encoders/alac.h pcmconv.o - $(CC) $(FLAGS) -DVERSION=$(VERSION) -o alacenc encoders/alac.c pcmconv.o $(OBJS) -DSTANDALONE -lm - -shnenc: $(OBJS) encoders/shn.c encoders/shn.h - $(CC) $(FLAGS) -DVERSION=$(VERSION) -o shnenc encoders/shn.c $(OBJS) -DSTANDALONE -lm - -wvenc: $(OBJS) encoders/wavpack.c encoders/wavpack.h md5.o pcmconv.o - $(CC) $(FLAGS) -o wvenc encoders/wavpack.c md5.o pcmconv.o $(OBJS) -DSTANDALONE - -mlpdec: decoders/mlp.c decoders/mlp.h array.o bitstream.o - $(CC) $(FLAGS) -o mlpdec decoders/mlp.c array.o bitstream.o -DSTANDALONE - -dvdadec: dvda/dvda.c dvda/dvda.h bitstream.o - $(CC) $(FLAGS) -o dvdadec dvda/dvda.c bitstream.o -DSTANDALONE - -huffman: huffman.c huffman.h - $(CC) $(FLAGS) -o huffman huffman.c -DSTANDALONE -DEXECUTABLE -ljansson - -clean: - rm -f flacenc alacenc shnenc huffman *.o - -array.o: array.c array.h - $(CC) $(FLAGS) -c array.c -DSTANDALONE - -pcm.o: pcm.c pcm.h - $(CC) $(FLAGS) -c pcm.c -DSTANDALONE - -pcmconv.o: pcmconv.c pcmconv.h - $(CC) $(FLAGS) -c pcmconv.c -DSTANDALONE - -bitstream.o: bitstream.c bitstream.h - $(CC) $(FLAGS) -c bitstream.c - -md5.o: common/md5.c common/md5.h - $(CC) $(FLAGS) -c common/md5.c -DSTANDALONE - -flac_crc.o: common/flac_crc.c common/flac_crc.h - $(CC) $(FLAGS) -c common/flac_crc.c -DSTANDALONE - -huffman.o: huffman.c huffman.h - $(CC) $(FLAGS) -c huffman.c -DSTANDALONE - -bitstream: bitstream.c bitstream.h huffman.o - $(CC) $(FLAGS) bitstream.c huffman.o -DEXECUTABLE -o $@
View file
audiotools-2.18.tar.gz/src/resample.c
Deleted
@@ -1,280 +0,0 @@ -#include <Python.h> -#include "samplerate/samplerate.h" - -/******************************************************** - Audio Tools, a module and set of tools for manipulating audio data - Copyright (C) 2007-2012 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 -*******************************************************/ - -#include "resample.h" -#include "pcm.h" -#include "samplerate/samplerate.c" - -#ifdef IS_PY3K - -static PyModuleDef resamplemodule = { - PyModuleDef_HEAD_INIT, - "resample", - "A PCM resampling module.", - -1, - NULL, - NULL, NULL, NULL, NULL -}; - -PyMODINIT_FUNC -PyInit_resample(void) -{ - PyObject* m; - - resample_ResamplerType.tp_new = PyType_GenericNew; - if (PyType_Ready(&resample_ResamplerType) < 0) - return NULL; - - m = PyModule_Create(&resamplemodule); - if (m == NULL) - return NULL; - - Py_INCREF(&resample_ResamplerType); - PyModule_AddObject(m, "Resampler", - (PyObject *)&resample_ResamplerType); - return m; -} - -#else - -PyMODINIT_FUNC -initresample(void) -{ - PyObject* m; - - resample_ResamplerType.tp_new = PyType_GenericNew; - if (PyType_Ready(&resample_ResamplerType) < 0) - return; - - m = Py_InitModule3("resample", module_methods, - "A PCM stream reading, writing and editing module."); - - Py_INCREF(&resample_ResamplerType); - PyModule_AddObject(m, "Resampler", - (PyObject *)&resample_ResamplerType); -} - -#endif - -#ifdef IS_PY3K - -void -Resampler_dealloc(resample_Resampler* self) -{ - src_delete(self->src_state); - Py_TYPE(self)->tp_free((PyObject*)self); -} - -#else - -void -Resampler_dealloc(resample_Resampler* self) -{ - src_delete(self->src_state); - Py_XDECREF(self->pcm_module); - self->ob_type->tp_free((PyObject*)self); -} - -#endif - -PyObject* -Resampler_new(PyTypeObject *type, - PyObject *args, PyObject *kwds) -{ - resample_Resampler *self; - - self = (resample_Resampler *)type->tp_alloc(type, 0); - self->pcm_module = NULL; - - return (PyObject *)self; -} - -int -Resampler_init(resample_Resampler *self, - PyObject *args, PyObject *kwds) -{ - int error; - int channels; - int quality; - double ratio; - - if (!PyArg_ParseTuple(args, "idi", &channels, &ratio, &quality)) - return -1; - - if ((self->pcm_module = PyImport_ImportModule("audiotools.pcm")) == NULL) - return -1; - - if (channels < 1) { - PyErr_SetString(PyExc_ValueError, - "channel count must be greater than 1"); - return -1; - } - if ((quality < 0) || (quality > 4)) { - PyErr_SetString(PyExc_ValueError, - "quality must be between 0 and 4"); - return -1; - } - - self->src_state = src_new(0, channels, &error); - self->channels = channels; - self->ratio = ratio; - - return 0; -} - -/**************************/ -/*Resampler implementation*/ -/**************************/ - -#define OUTPUT_SAMPLES_LENGTH 0x100000 - -PyObject* -Resampler_process(resample_Resampler* self, PyObject *args) -{ - PyObject *framelist_obj; - int last; - - SRC_DATA src_data; - int processing_error; - - static float data_out[OUTPUT_SAMPLES_LENGTH]; - - Py_ssize_t i, j; - - PyObject *framelist_type_obj = NULL; - pcm_FloatFrameList *framelist; - pcm_FloatFrameList *processed_samples = NULL; - pcm_FloatFrameList *unprocessed_samples = NULL; - PyObject *toreturn; - - src_data.data_in = NULL; - - /*grab (framelist,last) passed in from the method call*/ - if (!PyArg_ParseTuple(args, "Oi", &framelist_obj, &last)) - goto error; - - /*ensure input is a FloatFrameList*/ - if ((framelist_type_obj = PyObject_GetAttrString( - self->pcm_module, - "FloatFrameList")) == NULL) - goto error; - if (framelist_obj->ob_type == (PyTypeObject*)framelist_type_obj) { - framelist = (pcm_FloatFrameList*)framelist_obj; - } else { - PyErr_SetString(PyExc_TypeError, - "first argument must be a FloatFrameList"); - goto error; - } - - if (framelist->channels != self->channels) { - PyErr_SetString(PyExc_ValueError, - "FrameList's channel count differs from Resampler's"); - goto error; - } - - /*build SRC_DATA from our inputs*/ - if ((src_data.data_in = malloc(framelist->samples_length * - sizeof(float))) == NULL) { - PyErr_SetString(PyExc_MemoryError, "out of memory"); - goto error; - } - - src_data.data_out = data_out; - src_data.input_frames = framelist->frames; - src_data.output_frames = OUTPUT_SAMPLES_LENGTH / self->channels; - src_data.end_of_input = last; - src_data.src_ratio = self->ratio; - - for (i = 0; i < framelist->samples_length; i++) { - src_data.data_in[i] = (float)framelist->samples[i]; - } - - /*run src_process() on our self->SRC_STATE and SRC_DATA*/ - if ((processing_error = src_process(self->src_state, &src_data)) != 0) { - /*some sort of processing error raises ValueError*/ - PyErr_SetString(PyExc_ValueError, - src_strerror(processing_error)); - goto error; - } - - - /*turn our processed and unprocessed data into two new FloatFrameLists*/ - if ((processed_samples = (pcm_FloatFrameList*)PyObject_CallMethod( - self->pcm_module, "__blank_float__", NULL)) == NULL) - goto error; - processed_samples->channels = - self->channels; - processed_samples->frames = - (unsigned int)src_data.output_frames_gen; - processed_samples->samples_length = - (unsigned int)processed_samples->frames * processed_samples->channels; - processed_samples->samples = - realloc(processed_samples->samples, - sizeof(double) * processed_samples->samples_length); - - if ((unprocessed_samples = (pcm_FloatFrameList*)PyObject_CallMethod( - self->pcm_module, "__blank_float__", NULL)) == NULL) - goto error; - unprocessed_samples->channels = - self->channels; - unprocessed_samples->frames = - (unsigned int)(src_data.input_frames - src_data.input_frames_used); - unprocessed_samples->samples_length = - unprocessed_samples->frames * unprocessed_samples->channels; - unprocessed_samples->samples = - realloc(unprocessed_samples->samples, - sizeof(double) * unprocessed_samples->samples_length); - - - /*successfully processed samples*/ - for (i = 0; i < src_data.output_frames_gen * self->channels; i++) { - processed_samples->samples[i] = src_data.data_out[i]; - } - - /*not-yet-successfully processed samples*/ - for (i = src_data.input_frames_used * self->channels, j=0; - i < (src_data.input_frames * self->channels); - i++, j++) { - unprocessed_samples->samples[j] = src_data.data_in[i]; - } - - - /*return those two arrays as a tuple*/ - toreturn = Py_BuildValue("(O,O)", processed_samples, unprocessed_samples); - - /*cleanup anything allocated*/ - free(src_data.data_in); - Py_DECREF(framelist_type_obj); - Py_DECREF(processed_samples); - Py_DECREF(unprocessed_samples); - - return toreturn; - - error: - if (src_data.data_in != NULL) - free(src_data.data_in); - Py_XDECREF(framelist_type_obj); - Py_XDECREF(processed_samples); - Py_XDECREF(unprocessed_samples); - - return NULL; -}
View file
audiotools-2.18.tar.gz/src/resample.h
Deleted
@@ -1,154 +0,0 @@ -#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN) -typedef int Py_ssize_t; -#define PY_SSIZE_T_MAX INT_MAX -#define PY_SSIZE_T_MIN INT_MIN -#endif - -/******************************************************** - Audio Tools, a module and set of tools for manipulating audio data - Copyright (C) 2007-2012 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 -*******************************************************/ - -#if PY_MAJOR_VERSION >= 3 -#define IS_PY3K -#endif - -PyMethodDef module_methods[] = { - {NULL} -}; - -/***********************/ -/*Resampler definitions*/ -/***********************/ - -typedef struct { - PyObject_HEAD - SRC_STATE *src_state; - int channels; - double ratio; - PyObject *pcm_module; -} resample_Resampler; - -void -Resampler_dealloc(resample_Resampler* self); - -PyObject* -Resampler_new(PyTypeObject *type, - PyObject *args, PyObject *kwds); - -int -Resampler_init(resample_Resampler *self, - PyObject *args, PyObject *kwds); - -PyObject* -Resampler_process(resample_Resampler* self, PyObject *args); - -PyMethodDef Resampler_methods[] = { - {"process", (PyCFunction)Resampler_process, - METH_VARARGS, "Processes PCM samples into the new sample rate"}, - {NULL} -}; - -#ifdef IS_PY3K - -static PyTypeObject resample_ResamplerType = { - PyVarObject_HEAD_INIT(NULL, 0) - "resample.Resampler", /* tp_name */ - sizeof(resample_Resampler), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)Resampler_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_reserved */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | - Py_TPFLAGS_BASETYPE, /* tp_flags */ - "Resampler objects", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - Resampler_methods, /* tp_methods */ - 0, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc)Resampler_init, /* tp_init */ - 0, /* tp_alloc */ - Resampler_new, /* tp_new */ -}; - -#else - -PyTypeObject resample_ResamplerType = { - PyObject_HEAD_INIT(NULL) - 0, /*ob_size*/ - "resample.Resampler", /*tp_name*/ - sizeof(resample_Resampler), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - (destructor)Resampler_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash */ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ - "Resampler objects", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - Resampler_methods, /* tp_methods */ - 0, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc)Resampler_init, /* tp_init */ - 0, /* tp_alloc */ - Resampler_new, /* tp_new */ -}; - -#endif
View file
audiotools-2.18.tar.gz/TODO -> audiotools-2.19.tar.gz/TODO
Changed
@@ -4375,7 +4375,7 @@ If a disc is not found on FreeDB or MusicBrainz, there should be some mechanism to submit user-defined data to those services. -* TODO build track2cover/xmcd2cover utilities +* TODO build track2cover utility It would be helpful to have tools that could query online cover databases and retrieve images for tagging purposes. track2cover can replace coverdump, functioning like trac2xmcd @@ -4404,230 +4404,9 @@ ** TODO add trackverify unit tests for all known verifiable problems ** DONE add trackverify man page ** DONE link trackverify man page to others -* TODO Integrate WavPack -** DONE update WavPack documentation to little-endian reading order -** TODO Add WavPack decoder -*** DONE Add audiotools.decoders.WavPackDecoder object - Must include the following attributes: - - [X] sample_rate - - [X] bits_per_sample - - [X] channels - - [X] channel_mask - And the following methods: - - [X] __init__ - - [X] read() - - [X] analyze_frame() - - [X] close() -*** DONE Add block header parsing -*** DONE Add sub-block header parsing -*** DONE Parse sub-blocks - - [X] decorr_terms - - [X] decorr_weights - - [X] decorr_samples - - [X] entropy_vars - - [X] wv_bitstream -*** DONE Decode 1 or more blocks in read() -*** DONE Check for EOFs during decoding -*** DONE Return pcm.FrameList objects on calls to read() -*** DONE Return dict objects on calls to analyze_frame() -*** DONE Ensure different bits-per-sample work correctly - - [X] 8 bps - - [X] 16 bps - - [X] 24 bps -*** DONE Ensure different number of channels work correctly - - [X] 1 channel - - [X] 2 channels - - [X] 3 channels - - [X] 4 channels - - [X] 5 channels - - [X] 6 channels - - [X] 7 channels - - [X] 8 channels -*** DONE Ensure different sample rates work correctly - - [X] 44100Hz - - [X] 48000Hz - - [X] 96000Hz - - [X] 192000Hz -*** TODO Ensure test files decode correctly -**** TODO bit_depths - - [X] 8bit.wv - - [ ] 12bit.wv - - [X] 16bit.wv - - [ ] 20bit.wv - - [X] 24bit.wv - - [ ] 32bit_int.wv - - [ ] 32bit_int_p.wv - - [ ] 32bit_float_p.wv - - [ ] 32bit_float.wv - (I may omit the odd bit depths in the short term) -**** TODO hybrid_bitrates - - [ ] 24kbps.wv - - [ ] 32kbps.wv - - [ ] 48kbps.wv - - [ ] 64kbps.wv - - [ ] 128kbps.wv - - [ ] 160kbps.wv - - [ ] 256kbps.wv - - [ ] 320kbps.wv - - [ ] 384kbps.wv - - [ ] 512kbps.wv - - [ ] 1024kbps.wv -**** TODO hybrid_bitrates/wvc_files - - [ ] 24kbps.wvc - - [ ] 32kbps.wvc - - [ ] 48kbps.wvc - - [ ] 64kbps.wvc - - [ ] 128kbps.wvc - - [ ] 160kbps.wvc - - [ ] 256kbps.wvc - - [ ] 320kbps.wvc - - [ ] 384kbps.wvc - - [ ] 512kbps.wvc - - [ ] 1024kbps.wvc -**** DONE num_channels - - [X] mono-1.wv - - [X] stereo-2.wv -**** TODO special_cases - - [ ] cue_sheet.wv - - [ ] cue_sheet.wvc - - [X] false_stereo.wv - - [ ] win_executable.wv - - [X] zero_lsbs.wv -**** DONE speed_modes - - [X] default.wv - - [X] fast.wv - - [X] high.wv - - [X] vhigh.wv -*** DONE Ensure block CRC is checked -*** DONE Ensure trailing MD5 is checked, if present -** DONE Add WavPack encoder -*** DONE Add audiotools.encoders.encode_wavpack function -*** DONE Split samples into blocks based on channel count -*** DONE Write one block per block-size number of channels, per channel pair -**** DONE Calculate block CRC -**** DONE Write placeholder block header -**** DONE Determine tunable data per block - - [X] decorrelation terms - - [X] decorrelation deltas - - [X] decorrelation weights - - [X] decorrelation samples - - [X] entropy variables (medians) -**** DONE Write sub-blocks containing tunables data -***** DONE decorrelation terms/deltas -***** DONE decorrelation weights -***** DONE decorrelation samples -***** DONE entropy variables -**** DONE Calculate bitstream values from entropy variables and PCM data -**** DONE Write bitstream sub-block -**** DONE Calculate joint-stereo -**** DONE Add channel mask sub-block for multi-channel audio -*** DONE Rewrite block headers with final "total samples" data - Using from_pcm(), this data won't be known in advance -*** DONE Allow external RIFF WAVE header/footer - As with Shorten, these may be passed in from outside. -*** DONE Rewrite RIFF WAVE data block header with final size - If we're building the file with from_pcm(), this header - will need to be built and then filled-in at finalize-time. -*** DONE Update for standalone operation - Not necessarily to generate fully compliant files, - but to ensure there are no memory leaks and prep - for gprof-style optimizing. -*** DONE Generate trailing MD5 -*** DONE Unify the Python and standalone encode_wavpack functions - Much like FLAC's, making them more similar should reduce potential bugs. -*** DONE Identify/Optimize false stereo files -*** DONE Identify/Optimize wasted left bits -*** DONE Optimize memory usage - Perform less memory allocation/freeing over the course of encoding. -*** DONE Optimize for speed -** DONE Add WavPack-specific unit tests - - [X] test_small_files - - [X] test_full_scale_deflection - - [X] test_sines - - [X] test_wasted_bps - - [X] test_blocksizes - - [X] test_option_variations - - [X] test_silence - - [X] test_noise - - [X] test_fractional - - [X] test_multichannel - A combination of converted FLAC unit tests - and stuff that's bitten me during initial implementation. -** DONE Document WavPack -*** DONE the WavPack file stream -*** DONE WavPack decoding -**** DONE WavPack sub-blocks - - [X] decorr_terms - - [X] decorr_weights - - [X] decorr_samples - - [X] entropy_vars - - [X] wv_bitstream -**** DONE decorrelation passes -**** DONE joint stereo -**** DONE int32 info - - [X] zeroes - - [X] ones - - [X] dupes - - [X] shift -**** DONE channel info -**** DONE checksum -**** DONE RIFF sub-blocks -**** DONE MD5 sub-block -**** DONE false stereo -*** DONE WavPack encoding -**** DONE false stereo -**** DONE int32_info - for left shifting zeroes, in particular -**** DONE checksum -**** DONE joint stereo -**** DONE WavPack block header -**** DONE decorrelation passes -**** DONE WavPack sub-blocks - - [X] decorr_terms - - [X] decorr_weights - - [X] decorr_samples - - [X] entropy_vars - - [X] extended integers - - [X] channel info - - [X] wv_bitstream - - [X] RIFF sub-blocks - - [X] MD5 sub-block -**** DONE bitstream generation -*** DONE Ensure section capitalization is consistent -*** DONE Spell-check -*** DONE Verify A4 layout is correct -** DONE Test encoder/decoder against reference - Ensure a wide range of lossless audio round-trips correctly -** DONE Remove WavePackAudio.BINARIES -** DONE Add more WavPack INVALIDFILE .verify() tests -** DONE Remove wvgain for ReplayGain application * TODO Add a -r/--no-results flag to trackcmp/trackverify Listing only the final results and not the running list ** TODO Update man page -* TODO Add optional interactive modes to utilities - Now that Urwid is being used for editxmcd, it might be helpful - to add optional console-based interactive modes to the other tools. - This may improve ease-of-use (particularly discoverability - in the case of format and quality options) without sacrificing - scriptability or command-line power. - - Just as the command-line options are kept as consistent as possible, - all interactive modes will also need a consistent interface. -** DONE cd2track -** TODO cdinfo -** TODO track2track -** TODO trackrename -** TODO trackinfo -** TODO tracklength -** TODO track2cd -** TODO trackcmp -** DONE trackplay -** DONE tracktag -** TODO trackcat -** DONE tracksplit -** TODO tracklint -** TODO trackverify -** TODO coverdump * TODO Add DVD-A support ** DONE Build AOB parser/extractor *** DONE Optimize AOB extractor for better offset handling @@ -4767,7 +4546,7 @@ ** DONE Add -V/--verbose option ** DONE Add --shuffle option ** DONE Add -x/--xmcd file support -** TODO Avoid spinning down CDs between tracks +** DONE Avoid spinning down CDs between tracks This makes playback less seamless than I'd like ** TODO Improve stability ** DONE Add man page for cdplay.1 @@ -5021,77 +4800,6 @@ **** TODO test_options **** TODO test_errors ** DONE Split tests into multiple files -* TODO Add True Audio support - This compresses well and does seem to be used in some circles, - so it would be a good idea to add. - The tricky part is that its reference implementation doesn't compile well. -* TODO Assimilate Vorbis support -** TODO Add Vorbis decoder -*** TODO Add VorbisDecoder Python object to decoders -*** TODO Document Vorbis decoding -**** DONE identification header packet -**** DONE comment header packet -**** TODO setup header packet -***** TODO codebooks -***** TODO time domain transforms -***** TODO floors -***** TODO residues -***** TODO mappings -***** TODO modes -**** TODO audio packet decoding -***** TODO decode packet type flag -***** TODO decode mode number -***** TODO decode window shape -***** TODO decode floor -***** TODO decode residue into residue vectors -***** TODO inverse channel coupling of reside vectors -***** TODO generate floor curve from decoded floor data -***** TODO compute dot product of floor and residue - producing audio spectrum vector -***** TODO inverse monolithic transform of audio spectrum vector -***** TODO overlap / add left-hand output with right-hand output -***** TODO store right-hand data from transform of current frame - for future lapping -***** TODO if not first frame, return results of overlap/add as audio -*** TODO Add VorbisDecoder to VorbisAudio's to_pcm() method -**** DONE handle identification header packet -**** DONE skip comment header packet -**** TODO handle setup header packet -***** TODO codebooks - store result of codebooks decoding somewhere -***** TODO time domain transforms - store result of time domain transforms somewhere? -***** TODO floors -***** TODO residues -***** TODO mappings -***** TODO modes -**** TODO perform data decoding -***** TODO decode packet type flag -***** TODO decode mode number -***** TODO decode window shape -***** TODO decode floor -***** TODO decode residue into residue vectors -***** TODO inverse channel coupling of reside vectors -***** TODO generate floor curve from decoded floor data -***** TODO compute dot product of floor and residue - producing audio spectrum vector -***** TODO inverse monolithic transform of audio spectrum vector -***** TODO overlap / add left-hand output with right-hand output -***** TODO store right-hand data from transform of current frame - for future lapping -***** TODO if not first frame, return results of overlap/add as audio -**** TODO return FloatFrameList via read_float() method -**** TODO return FrameList via read() method -*** TODO Add unit tests for VorbisDecoder -** TODO Add Vorbis encoder -* TODO Restore Musepack support - This was removed because compiling it was a pain - and I'd never seen Musepack files in the wild. - But it could provide a good secondary assimilated lossy format - in addition to Vorbis. -* TODO Add interative mode to audiotools-config - This is one instance where seeing all the options "up-front" - is likely to make the process easier. * TODO Add AccurateRip support This might make give cd2track a little added respectability and post-rip tools could be built also to verify tracks/disc images. @@ -5160,39 +4868,6 @@ Unfortunately, Chrome doesn't do MathML and IE doesn't do SVG which makes it non-viable right now. -* TODO Shift to Cython? - Rather than push toward Python 3, which is slower than 2 - and not yet as widely supported, it may be better to shift toward - Cython for handling lots of low-level file processing. - It would add a dependency, but has the potential to make - chunks of the code faster and smaller. -* TODO Fix --number=0 argument to tracktag - I'd like to make track_number=0 a valid value - and have track_number=None indicate a missing field. - The problem is maintaining consistency between - metadata formats that store track numbers as text (like ID3v2) - and those that store track numbers as integers (like M4A). -* TODO Split off tracktag's functionality -** TODO Move the image options into a specialized tool (covertag?) - Its interactive mode should use PyGTK/Tkinter - since it's helpful to see the images one is selecting to add/remove. - - [ ] --remove-images - - [ ] --front-cover - - [ ] --back-cover - - [ ] --leaflet - - [ ] --media - - [ ] --other-image - - [ ] -T, --thumbnail -** TODO Move CD options into a specialized tool - Something for tagging a group of tracks as if they're an entire CD - - [ ] --cue - - [ ] -l, --lookup - - [ ] --musicbrainz-server - - [ ] --musicbrainz-port - - [ ] --no-musicbrainz - - [ ] --freedb-server - - [ ] --freedb-port - - [ ] --no-freedb * TODO Add a dvdaplay utility ** TODO Unify trackplay/cdplay/dvdaplay widgets into audiotools.ui ** TODO Unify non-Urwid player into audiotools.ui @@ -5218,108 +4893,525 @@ - [ ] src/encoders/shn.c - [X] src/encoders/wavpack.c * TODO handle AIFF offset/block size in SSND chunk -* TODO Finish version 2.19 -** TODO Assimilate MP3 support - This is probably one of the most used codecs - yet it requires the maximum number of external utilities. - I'd prefer it to work out-of-the-box instead. -*** TODO Add MP3 decoding documentation +* TODO Assimilate external codecs + More of a long-term plan than anything else. + It would be best to have as few external program + dependencies as possible +** TODO Add internal MP3 codec + This is likely the codec people are most interested in + and so it should be folded in so it can be used + without any external dependencies. *** TODO Add C-based MP3 decoder -*** TODO Add Python-based MP3 decoder -*** TODO Add MP3 encoding documentation +*** TODO Add MP3 decoding reference documentation *** TODO Add C-based MP3 encoder -*** TODO Add codec unit tests -** TODO Update PCMReader.read() to take a PCM frame count instead of bytes + Unlike lossless codecs where output can be verified automatically, + lossy codecs offer no similar guarantees. + So like libsamplerate, it would be better to import + a known correct implementation like LAME + than to reimplement the wheel. +*** TODO Add MP3 encoding reference documentation + A rundown of how LAME operates, in the hope of demystifying + the format. +** TODO Add internal MP2 encoder +** TODO Add internal Vorbis codec +*** TODO Add Vorbis decoder +**** TODO Add VorbisDecoder Python object to decoders +**** TODO Document Vorbis decoding +***** DONE identification header packet +***** DONE comment header packet +***** TODO setup header packet +****** TODO codebooks +****** TODO time domain transforms +****** TODO floors +****** TODO residues +****** TODO mappings +****** TODO modes +***** TODO audio packet decoding +****** TODO decode packet type flag +****** TODO decode mode number +****** TODO decode window shape +****** TODO decode floor +****** TODO decode residue into residue vectors +****** TODO inverse channel coupling of reside vectors +****** TODO generate floor curve from decoded floor data +****** TODO compute dot product of floor and residue + producing audio spectrum vector +****** TODO inverse monolithic transform of audio spectrum vector +****** TODO overlap / add left-hand output with right-hand output +****** TODO store right-hand data from transform of current frame + for future lapping +****** TODO if not first frame, return results of overlap/add as audio +**** TODO Add VorbisDecoder to VorbisAudio's to_pcm() method +***** DONE handle identification header packet +***** DONE skip comment header packet +***** TODO handle setup header packet +****** TODO codebooks + store result of codebooks decoding somewhere +****** TODO time domain transforms + store result of time domain transforms somewhere? +****** TODO floors +****** TODO residues +****** TODO mappings +****** TODO modes +***** TODO perform data decoding +****** TODO decode packet type flag +****** TODO decode mode number +****** TODO decode window shape +****** TODO decode floor +****** TODO decode residue into residue vectors +****** TODO inverse channel coupling of reside vectors +****** TODO generate floor curve from decoded floor data +****** TODO compute dot product of floor and residue + producing audio spectrum vector +****** TODO inverse monolithic transform of audio spectrum vector +****** TODO overlap / add left-hand output with right-hand output +****** TODO store right-hand data from transform of current frame + for future lapping +****** TODO if not first frame, return results of overlap/add as audio +***** TODO return FloatFrameList via read_float() method +***** TODO return FrameList via read() method +**** TODO Add unit tests for VorbisDecoder +*** TODO Add Vorbis encoder +** TODO Add internal Opus codec +** TODO Add internal True Audio codec + This compresses well and does seem to be used in some circles, + so it would be a good idea to add. + The tricky part is that its reference implementation doesn't compile well. +* TODO Finish version 2.19 +** DONE Update PCMReader.read() to take a PCM frame count instead of bytes Taking a byte count as an argument is a relic from the days when most conversion happened through external programs. It's time to update this function to work on PCM frames instead which is much more natural fit and makes many calculations much easier. -** TODO Update .convert() method to use fewer temporary files - .wav and .aiff containers with embedded chunks - currently route data through actual .wav/.aiff files - in order to pass those chunks to another format. - It would be better to avoid creating an intermediate - file whenever possible. -** TODO Remove the big pile of imports in audiotools/__init__.py - only import the stuff we need -** TODO Add internal Ogg FLAC encoder - This shouldn't be too hard since native FLAC encoding is already in place, - as is Ogg stream encoding. -** TODO Add Python-based file decoders - These would be low-performance, Python-based PCMReader-style objects - demonstrating how the decoding process works in a relatively simple - manner through the use of the BitstreamReader objects. - They would also provide reference implementations. -*** DONE py_decoders/FlacDecoder -*** TODO py_decoders/OggFlacDecoder -*** DONE py_decoders/SHNDecoder -*** DONE py_decoders/ALACDecoder -*** DONE py_decoders/WavPackDecoder -** TODO Restore Monkey's Audio support - This needs to be assimilated with a native decoder/encoder. - I've seen these out in the wild, but only rarely. -** TODO Deprecate little-used / poorly-tested bits - Although there's a risk of annoying people by taking out - stuff they might be using, some utilities and formats - were added simply because it was easy and aren't tested - enough for my liking. -*** DONE record2track - This needs to be rewritten to support multiple audio sources - and constructed with a threaded GUI, or dropped. - The current implementation is entirely too half-assed. -*** DONE AACAudio -*** DONE SpeexAudio - I don't believe this format was ever meant for standalone use, - and I doubt anybody is using it that way either. - I just threw it in because it was easy and because - I already had a lot of Ogg stream parsing infrastructure in place. -*** TODO M4AAudio_faac - The authors of faad/faac have moved on to neroAacEnc/neroAacDec. - Even though M4AAudio_nero is also a hack involving intermediate - wav files, at least it gets updated. - Having two different, slightly incompatible M4A implementations - needlessly complicates things. -** TODO Update trackplay for interactivity - Urwid-based display should have various standard features, - analagous to those in cdplay - - [X] display track info - - [X] progress monitor - - [X] start - - [X] pause - - [X] next track - - [X] previous track -*** DONE Handle tracks that trigger a DecodeError in to_pcm() -*** DONE Update Player object to handle ReplayGain - - [X] track gain - - [X] album gain -*** TODO Add different audio output sinks -**** DONE PulseAudioOutput -**** DONE OSSAudioOutput -**** DONE NULLAudioOutput -**** TODO CoreAudioOutput -*** DONE Support different sinks from trackplay(1) -*** DONE Update trackplay man page -*** DONE Add audiotools.player programming documentation -**** DONE Player -**** DONE AudioOutput -**** DONE AUDIO_OUTPUT -*** DONE Add docstrings -**** DONE Player -**** DONE AudioPutput -**** DONE ThreadedPCMConverter -**** DONE PortAudioOutput -**** DONE PulseAudioOutput -**** DONE OSSAudioOutput -**** DONE NULLAudioOutput -*** DONE Add --shuffle option -*** DONE Work better with piped arguments - Don't generate an ugly exception when arguments are piped in via xargs. -*** TODO Add basic unit tests - - [ ] Ensure valid and invalid files generate the proper return values - when run in "quiet" mode to the "NULL" decoder - - [ ] Check command-line arguments - -** TODO Remove Construct module +*** DONE Update __init__.py + - [X] PCMReader.read + - [X] PCMReaderError.read + - [X] PCMReaderProgress.read + - [X] ReorderedPCMReader.read + - [X] transfer_framelist_data + - [X] threaded_transfer_framelist_data + - [X] pcm_cmp + - [X] pcm_frame_cmp + - [X] PCMCat.read + - [X] __buffer__.__init__ + - [X] __buffer__.__len__ + - [X] BufferedPCMReader.read + - [X] BufferedPCMReader.__fill__ + - [X] LimitedPCMReader.read + - [X] pcm_split + - [X] PCMConverter.read + - [X] ReplayGainReader.read + - [X] calculate_replay_gain + - [X] AudioFile.verify + - [X] PCMReaderWindow.read + - [X] CDTrackReader.read + - [X] CDTrackReaderAccurateRipCRC.read +*** DONE Update __aiff__.py + - [X] AiffReader.read + - [X] AiffAudio.from_pcm +*** DONE Update __au__.py + - [X] AuReader.read + - [X] AuReader.from_pcm +*** DONE Update __flac__.py + - [X] FlacAudio.__eq__ + - [X] FLAC_Data_Chunk.write + - [X] FLAC_SSND_Chunk.write +*** DONE Update __shn__.py + - [X] ShortenAudio.to_wave + - [X] ShortenAudio.to_aiff +*** DONE Update __wav__.py + - [X] WaveReader.read + - [X] WaveAudio.from_pcm + - [X] WaveAudio.add_replay_gain +*** DONE Update __wavpack__.py + - [X] WavPackAudio.to_wave +*** DONE Update py_decoders/alac.py + - [X] ALACDecoder.read +*** DONE Update py_decoders/flac.py + - [X] FlacDecoder.read +*** DONE Update py_decoders/shn.py + - [X] SHNDecoder.read +*** DONE Update py_decoders/wavpack.py + - [X] WavPackDecoder.read +*** DONE Update py_encoders/alac.py + - [X] encode_mdat +*** DONE Update py_encoders/flac.py + - [X] encode_flac +*** DONE Update py_encoders/shn.py + - [X] encode_shn +*** DONE Update py_encoders/wavpack.py + - [X] encode_wavpack +*** DONE Update src/pcmconv.c + - [X] pcmreader_read + - [X] pcmreader_read (alt version) +*** DONE Update src/replaygain.c + - [X] ReplayGainReader_read +*** DONE Update src/decoders/sine.c + - [X] Sine_Mono_read + - [X] Sine_Stereo_read + - [X] Sine_Simple_read +*** DONE Update test/test.py + - [X] BLANK_PCM_Reader + - [X] RANDOM_PCM_Reader + - [X] EXACT_BLANK_PCM_Reader + - [X] EXACT_SILENCE_PCM_Reader + - [X] EXACT_RANDOM_PCM_Reader + - [X] MD5_Reader + - [X] Variable_Reader + - [X] Join_Reader + - [X] MiniFrameReader +*** DONE Update test/test_core.py + - [X] BufferedPCMReader.test_pcm + - [X] LimitedPCMReader.test_read + - [X] PCMReaderWindow.__test_reader__ + - [X] PCM_Reader_Multiplexer.read + - [X] TestMultiChannel.__test_assignment__ +*** DONE Update test/test_formats.py + - [X] ERROR_PCM_Reader.read + - [X] ALACFileTest.__test_reader__ + - [X] ALACFileTest.__test_reader_nonalac__ + - [X] ALACFileTest.test_streams + - [X] FlacFileTest.test_streams + - [X] FlacFileTest.__test_reader__ + - [X] ShortenFileTest.test_streams + - [X] ShortenFileTest.__test_reader__ + - [X] WavPackFileTest.__test_reader__ +*** DONE Update test/test_streams.py + - [X] FrameListReader.read + - [X] MD5Reader.read + - [X] Sine8_Mono.read + - [X] Sine8_Stereo.read + - [X] Simple_Sine.read + - [X] WastedBPS16.read +*** DONE Ensure all unit tests pass +**** DONE [Lib] + - [X] core + - [X] cuesheet + - [X] freedb + - [X] image + - [X] musicbrainz + - [X] pcm + - [X] bitstream + - [X] replaygain + - [X] resample + - [X] tocfile + - [X] verify + - [X] player +**** DONE [Format] + - [X] audiofile + - [X] lossless + - [X] lossy + - [X] aiff + - [X] alac + - [X] au + - [X] dvda + - [X] flac + - [X] m4a + - [X] mp2 + - [X] mp3 + - [X] oggflac + - [X] shorten + - [X] sines + - [X] vorbis + - [X] wave + - [X] wavpack +**** DONE [Metadata] + - [X] metadata + - [X] flac + - [X] wavpack + - [X] id3v1 + - [X] id3v2 + - [X] vorbis + - [X] m4a +**** DONE [Util] + - [X] audiotools_config + - [X] cd2track + - [X] cdinfo + - [X] cdplay + - [X] coverdump + - [X] coverview + - [X] dvda2track + - [X] dvdainfo + - [X] track2cd + - [X] track2track + - [X] trackcat + - [X] trackcmp + - [X] trackinfo + - [X] tracklength + - [X] tracklint + - [X] trackplay + - [X] trackrename + - [X] tracksplit + - [X] tracktag + - [X] trackverify +*** DONE Update reference documentation +** DONE Remove the big pile of imports from various modules + Only import the stuff we need, when we need it. +*** DONE __init__.py +*** DONE accuraterip.py +*** DONE aiff.py +*** DONE ape.py +*** DONE au.py +*** DONE dvda.py +*** DONE flac.py +*** DONE id3.py +*** DONE id3v1.py +*** DONE image.py +*** DONE m4a.py +*** DONE m4a_atoms.py +*** DONE mp3.py +*** DONE ogg.py +*** DONE shn.py +*** DONE vorbis.py +*** DONE vorbiscomment.py +*** DONE wav.py +*** DONE wavpack.py +*** DONE cue.py +*** DONE delta.py +*** DONE freedb.py +*** DONE musicbrainz.py +*** DONE player.py +*** DONE toc.py +*** DONE ui.py +*** DONE ensure unit tests pass +**** DONE [Lib] + - [X] core + - [X] cuesheet + - [X] freedb + - [X] image + - [X] musicbrainz + - [X] pcm + - [X] bitstream + - [X] replaygain + - [X] resample + - [X] tocfile + - [X] verify + - [X] player +**** DONE [Format] + - [X] audiofile + - [X] lossless + - [X] lossy + - [X] aiff + - [X] alac + - [X] au + - [X] dvda + - [X] flac + - [X] m4a + - [X] mp2 + - [X] mp3 + - [X] oggflac + - [X] shorten + - [X] sines + - [X] vorbis + - [X] wave + - [X] wavpack +**** DONE [Metadata] + - [X] metadata + - [X] flac + - [X] wavpack + - [X] id3v1 + - [X] id3v2 + - [X] vorbis + - [X] m4a +**** DONE [Util] + - [X] audiotools_config + - [X] cd2track + - [X] cdinfo + - [X] cdplay + - [X] coverdump + - [X] coverview + - [X] dvda2track + - [X] dvdainfo + - [X] track2cd + - [X] track2track + - [X] trackcat + - [X] trackcmp + - [X] trackinfo + - [X] tracklength + - [X] tracklint + - [X] trackplay + - [X] trackrename + - [X] tracksplit + - [X] tracktag + - [X] trackverify +** DONE Integrate Filename object into utilities + This object should replace the Messenger.filename classmethod. + It automatically performs filename -> unicode conversion + and, when files are on disk, compares for equality by device/inode. +*** DONE Update utilities +**** DONE audiotools-config +**** DONE cd2track +**** DONE coverdump +**** DONE coverview +**** DONE dvda2track +**** DONE dvdainfo +**** DONE track2cd +**** DONE track2track +**** DONE trackcat +**** DONE trackcmp +**** DONE trackinfo +**** DONE tracklint +**** DONE trackrename +**** DONE tracksplit +**** DONE tracktag +**** DONE trackverify +*** DONE Add documentation for Filename +*** DONE Add unit tests for Filename +*** DONE Remove Messenger.filename classmethod +**** DONE remove mention in documentation +*** DONE Ensure all unit tests pass +** DONE Sanity check tool inputs/outputs +*** DONE Ensure input files are included only once + - [X] track2cd - generate warning + - [X] track2track - generate error + - [X] trackcat - generate warning + - [X] trackcmp - short-circuit same file comparison + - [X] tracklength - generate warning + - [X] tracklint - generate error + - [X] trackrename - generate error + - [X] tracktag - generate error + - [X] trackverify - skip duplicates +**** DONE Unit test new behavior + - [X] track2cd + - [X] track2track + - [X] trackcat + - [X] trackcmp + - [X] tracklint + - [X] trackrename + - [X] tracktag + - [X] trackverify +*** DONE Ensure input file(s) are different from output file(s) + Overwriting files is okay by default (in the Unix tradition) + but input and output as same file is not and should generate error. + - [X] coverdump + - [X] track2track + - [X] trackcat + - [X] tracksplit +**** DONE Unit test new behavior + - [X] coverdump + - [X] track2track + - [X] trackcat + - [X] tracksplit +*** DONE Ensure same file isn't written twice by the same utility + This would typically be the result of a misused "--format" argument. + - [X] cd2track + - [X] dvda2track + - [X] track2track + - [X] trackrename + - [X] tracksplit +**** DONE unit test new behavior + - [X] cd2track + - [X] track2track + - [X] trackrename + - [X] tracksplit +** DONE Ensure progress display doesn't overload terminal with rows + As more cores become commonplace, it's important not to + overload the screen with too many progress rows + in case the number of simultaneous jobs + exceeds the number of terminal rows. +** DONE Improve metadata tagging widget + I'm not convinced the current opened/closed bottom window + is the ideal design for editing extended album/track metadata. + Something more similar to a spreadsheet would be ideal + but that's complicated by the serious lack of space + in terminal windows. +** DONE Fix ReplayGain to work on files with different sample rates + This should handle a wider array of cases than it does now. +*** DONE Update can_add_replay_gain classmethod + It should take a list of tracks and return True + if ReplayGain can be added to them, False if not + which takes the place of applicable_replay_gain + - [X] AudioFile.can_add_replay_gain + - [X] FlacAudio.can_add_replay_gain + - [X] M4AAudio_faac.can_add_replay_gain + - [X] MP3Audio.can_add_replay_gain + - [X] VorbisAudio.can_add_replay_gain + - [X] WaveAudio.can_add_replay_gain + - [X] WavPackAudio.can_add_replay_gain +**** DONE update reference documentation +**** DONE update unit tests + - [X] test_formats.py AudioFileTest.test_replay_gain + - [X] test_utils.py track2track.test_options + - [X] test_utils.py tracktag.test_options +*** DONE Add supports_replay_gain() classmethod + Returns True if the class supports ReplayGain of any kind. + - [X] AudioFile + - [X] FlacAudio + - [X] M4AAudio + - [X] MP2Audio + - [X] MP3Audio + - [X] OggFlacAudio + - [X] VorbisAudio + - [X] WavPackAudio + - [X] WaveAudio +**** DONE Add documentation +*** DONE Remove audiotools.applicable_replay_gain function + this functionality is shifted to can_add_replay_gain +**** DONE remove from utilities + - [X] dvda2track + - [X] track2track + - [X] tracksplit +**** DONE update reference documentation +**** DONE update unit tests +*** DONE Update calculate_replay_gain function +**** DONE tracks with unsupported sample rates should be resampled + according to the nearest supported sample rate available +**** DONE tracks with different sample rates should be resampled + according to the most common sample rate available +**** DONE tracks with more than two channels should be culled + remove any channels above the first two during calculation +**** DONE update unit tests + - [X] test_core.py TestReplayGain.test_basics +*** DONE Update add_replay_gain classmethods as needed + - [X] AudioFile.add_replay_gain + - [X] FlacAudio.add_replay_gain + - [X] M4AAudio_faac.add_replay_gain + - [X] MP3Audio.add_replay_gain + - [X] VorbisAudio.add_replay_gain + - [X] WaveAudio.add_replay_gain + - [X] WavPackAudio.add_replay_gain +*** DONE Update utilities to use new ReplayGain application procedure + Given a list of tracks for a given album, + if all are the same format and can_add_replay_gain returns True, + queue a call to add_replay_gain on those tracks. +**** DONE audiotools-config +**** DONE cd2track +**** DONE dvda2track +**** DONE track2track +**** DONE tracksplit +**** DONE tracktag +** DONE Display X/Y progress during operations + When a track is finished transcoding, for instance, output: + [ 2 / 15 ] input.wav -> output.mp3 + or something similar. +*** DONE Update utilities + - [X] cd2track + - [X] dvda2track + - [X] track2track + - [X] trackcmp + - [X] trackrename + - [X] tracksplit + - [X] tracktag + - [X] trackverify +*** DONE Update unit tests + - [X] cd2track + - [X] dvda2track + - [X] track2track + - [X] trackcmp + - [X] trackrename + - [X] tracksplit + - [X] tracktag + - [X] trackverify +** DONE Update metadata documentation with examples +*** DONE FLAC +*** DONE MP3 + - [X] ID3v1/ID3v1.1 + - [X] ID3v2.2 + - [X] ID3v2.3 + - [X] ID3v2.4 +*** DONE APEv2 +*** DONE M4A +** DONE Remove Construct module Convert all usage of Construct to BitstreamReader/BitstreamWriter. Since this is unlikely to be updated for Python 3, removing it should smooth that inevitable transition. @@ -5349,14 +5441,14 @@ *** DONE __freedb__.py *** DONE __id3__.py *** DONE __id3v1__.py -*** TODO __image__.py +*** DONE __image__.py - [X] __JPEG__ - [X] __PNG__ - [X] __GIF__ - [X] __BMP__ - [X] __TIFF__ -**** TODO check these against PIL's output -**** TODO check against truncated images +**** DONE check these against PIL's output +**** DONE check against truncated images *** DONE __init__.py *** DONE __m4a__.py **** DONE M4ATaggedAudio @@ -5457,3 +5549,923 @@ - [X] test_blocksizes **** DONE WavpackFileTest - [X] test_blocksizes +** DONE Replace gettext with string constants + Transforming _(u"some text") to SOME_TEXT + where SOME_TEXT is a predefined unicode string + in an audiotools sub-module. + This makes text more consistent *and* easier to modify. +*** DONE Update utilities + - [X] audiotools-config + - [X] cd2track + - [X] cdinfo + - [X] cdplay + - [X] coverdump + - [X] coverview + - [X] dvda2track + - [X] dvdainfo + - [X] track2cd + - [X] track2track + - [X] trackcat + - [X] trackcmp + - [X] trackinfo + - [X] tracklength + - [X] tracklint + - [X] trackplay + - [X] trackrename + - [X] tracksplit + - [X] tracktag + - [X] trackverify +*** DONE Update modules + - [X] __init__.py + - [X] aiff.py + - [X] ape.py + - [X] au.py + - [X] cue.py + - [X] dvda.py + - [X] flac.py + - [X] image.py + - [X] m4a.py + - [X] m4a_atoms.py + - [X] mp3.py + - [X] ogg.py + - [X] toc.py + - [X] ui.py + - [X] vorbis.py + - [X] vorbiscomment.py + - [X] wav.py + - [X] wavpack.py +*** DONE Remove gettext + - [X] audiotools-config + - [X] cd2track + - [X] cdinfo + - [X] cdplay + - [X] coverdump + - [X] coverview + - [X] dvda2track + - [X] dvdainfo + - [X] track2cd + - [X] track2track + - [X] trackcat + - [X] trackcmp + - [X] trackinfo + - [X] tracklength + - [X] tracklint + - [X] trackplay + - [X] trackrename + - [X] tracksplit + - [X] tracktag + - [X] trackverify + - [X] audiotools/__init__.py + - [X] audiotools/aiff.py + - [X] audiotools/ape.py + - [X] audiotools/au.py + - [X] audiotools/cue.py + - [X] audiotools/flac.py + - [X] audiotools/id3.py + - [X] audiotools/image.py + - [X] audiotools/m4a.py + - [X] audiotools/mp3.py + - [X] audiotools/toc.py + - [X] audiotools/vorbis.py + - [X] audiotools/wav.py + - [X] audiotools/wavpack.py +*** DONE Update unit tests + - [X] test_formats.py + - [X] test_metadata.py + - [X] test_utils.py +*** DONE Reduce big import chunks + Convert "from audiotools.text import (CONSTANT, ...)" + to "import audiotools.text as t" and "t.CONSTANT" + to avoid having huge import blocks at the start of utilities. + - [X] audiotools-config + - [X] cd2track + - [X] cdinfo + - [X] cdplay + - [X] coverdump + - [X] coverview + - [X] dvda2track + - [X] dvdainfo + - [X] track2cd + - [X] track2track + - [X] trackcat + - [X] trackcmp + - [X] trackinfo + - [X] tracklength + - [X] tracklint + - [X] trackplay + - [X] trackrename + - [X] tracksplit + - [X] tracktag + - [X] trackverify +*** DONE Ensure unit tests pass +** DONE Update C-based ReplayGainReader + Use a lot of the new C-based facilities to make it simpler. +** DONE Fix --number=0 argument to tracktag + I'd like to make track_number=0 a valid value + and have track_number=None indicate a missing field. + The problem is maintaining consistency between + metadata formats that store track numbers as text (like ID3v2) + and those that store track numbers as integers (like M4A). + - [X] MetaData + - [X] ApeTag + - [X] FlacMetaData + - [X] ID3CommentPair + - [X] ID3v1Comment + - [X] ID3v22Comment + - [X] ID3v23Comment + - [X] ID3v24Comment + - [X] M4A_META_Atom + - [X] VorbisComment +*** DONE Update utilities +*** DONE Update documentation +*** DONE Update unit tests +**** DONE Add getitem/setitem/getattr/setattr/delattr tests to metadata + - [X] flac + - [X] wavpack + - [X] id3v1 + - [X] id3v2 + - [X] vorbis + - [X] m4a +** DONE Remove Python Imaging Library requirement + This is only used for thumbnailing + and for TKinter-based image display. + Since a Python3 version doesn't seem to be pending, + it would be better to remove the requirement. +*** DONE Remove thumbnail functions from audiotools.image + - [X] can_thumbnail + - [X] thumbnail_formats + - [X] thumbnail_image +*** DONE Remove thumbnail method from audiotools.Image +*** DONE Remove thumbnail config options + - [X] audiotools.THUMBNAIL_FORMAT + - [X] audiotools.THUMBNAIL_SIZE +*** DONE Update utilities + - [X] audiotools-config + - [X] track2track + - [X] tracktag +*** DONE Update utility man pages + - [X] audiotools-config + - [X] track2track + - [X] tracktag +*** DONE Update programming documentation +*** DONE Update unit tests +** DONE Update cuesheet interface to handle non-CD sheets + Though rare, non-CD audio is sometimes combined + with cuesheets and should be handled properly + in those instances. +*** DONE Add sample_rate field to cuesheet pcm_lengths() method + - [X] Cuesheet.pcm_lengths + - [X] Flac_CUESHEET.pcm_lengths + - [X] TOCFile.pcm_lengths +**** DONE Update documentation +**** DONE Update unit tests +*** DONE Remove sheet_to_unicode function +*** DONE Update utilities to use sample_rate field with pcm_lengths + - [X] trackinfo + - [X] tracksplit + - [X] tracktag +*** DONE Ensure unit tests pass +** DONE Add faster and more accurate audio type identifier + Instead of checking open files on a format-by-format basis + to determine its type via looping, it's more effecient + to check for all possible file types from the same stream simultaneously + via a sort of finite automata. +*** DONE Add file_type function + - [X] ALACAudio + - [X] AiffAudio + - [X] AuAudio + - [X] FlacAudio - without ID3v2 tag + - [X] M4AAudio + - [X] MP2Audio - without ID3v2 tag + - [X] MP3Audio - without ID3v2 tag + - [X] OggFlacAudio + - [X] ShortenAudio + - [X] VorbisAudio + - [X] WavPackAudio + - [X] WaveAudio + - [X] FlacAudio - with ID3v2 tag + - [X] MP2Audio - with ID3v2 tag + - [X] MP3Audio - with ID3v2 tag +*** DONE Update functions which use is_type method to use file_type function + - [X] trackverify + - [X] audiotools.open +*** DONE Remove is_type method from AudioFile classes + - [X] ALACAudio + - [X] AiffAudio + - [X] AuAudio + - [X] FlacAudio + - [X] M4AAudio + - [X] MP2Audio + - [X] MP3Audio + - [X] OggFlacAudio + - [X] ShortenAudio + - [X] VorbisAudio + - [X] WavPackAudio + - [X] WaveAudio +*** DONE Update programming documentation +*** DONE Update unit tests + - [X] test_formats.py +** DONE Update utilities to handle broken track_name template fields + - [X] cd2track + - [X] dvda2track + - [X] track2track + - [X] trackrename + - [X] tracksplit +*** DONE Add unit tests for broken track_name template fields +** DONE Split off tracktag's functionality +*** DONE Move the image options into a specialized tool (covertag?) + Its interactive mode should use PyGTK/Tkinter + since it's helpful to see the images one is selecting to add/remove. + - [X] --remove-images + - [X] --front-cover + - [X] --back-cover + - [X] --leaflet + - [X] --media + - [X] --other-image + - [X] -T, --thumbnail +*** DONE Move CD options into a specialized tool + Something for tagging a group of tracks as if they're an entire CD + - [X] --cue + - [X] -l, --lookup + - [X] --musicbrainz-server + - [X] --musicbrainz-port + - [X] --no-musicbrainz + - [X] --freedb-server + - [X] --freedb-port + - [X] --no-freedb +** DONE Add interative mode to audiotools-config + This is one instance where seeing all the options "up-front" + is likely to make the process easier. +** DONE Add Python-based file decoders + These would be low-performance, Python-based PCMReader-style objects + demonstrating how the decoding process works in a relatively simple + manner through the use of the BitstreamReader objects. + They would also provide reference implementations. +*** DONE py_decoders/FlacDecoder +*** DONE py_decoders/SHNDecoder +*** DONE py_decoders/ALACDecoder +*** DONE py_decoders/WavPackDecoder +** DONE Update .convert() method to use fewer temporary files + .wav and .aiff containers with embedded chunks + currently route data through actual .wav/.aiff files + in order to pass those chunks to another format. + It would be better to avoid creating an intermediate + file whenever possible. +*** DONE Update wav containers to use new interface + WaveContainer.has_foreign_wav_chunks() + returns True if the instance has chunks to convert + WaveContainer.header_footer() + returns (header, footer) binary strings + WaveContainer.from_wave(header, pcmreader, footer) + returns new instance built from heaeder, PCM data and footer + + Once interface is in place, .convert() can pass header/footer + and wrap progress monitor around pcmreader in order to avoid + temporary wav files. +**** DONE Update FlacAudio + - [X] has_foreign_wave_chunks() + - [X] wave_header_footer() + - [X] from_wave(filename, header, pcmreader, footer, compression) + - [X] convert(target_page, target_class, compression, progress) +**** DONE Update OggFlacAudio + - [X] has_foreign_wave_chunks() + - [X] wave_header_footer() + - [X] from_wave(filename, header, pcmreader, footer, compression) + - [X] convert(target_page, target_class, compression, progress) +**** DONE Update ShortenAudio + - [X] has_foreign_wave_chunks() + - [X] wave_header_footer() + - [X] from_wave(filename, header, pcmreader, footer, compression) + - [X] convert(target_page, target_class, compression, progress) +**** DONE Update WavPackAudio + - [X] has_foreign_wave_chunks() + - [X] wave_header_footer() + - [X] from_wave(filename, header, pcmreader, footer, compression) + - [X] convert(target_page, target_class, compression, progress) +**** DONE Update WaveAudio + - [X] has_foreign_wave_chunks() + - [X] wave_header_footer() + - [X] from_wave(filename, header, pcmreader, footer, compression) + - [X] convert(target_page, target_class, compression, progress) +**** DONE Update programming documentation +**** DONE Update unit tests +*** DONE Update aiff containers to use new interface + AiffContainer.has_foreign_aiff_chunks() + returns True if the instance has chunks to convert + + AiffContainer.header_footer() + returns (header, footer) binary strings + + AiffContainer.from_aiff(header, pcmreader, footer) + returns new instance built from heaeder, PCM data and footer + + Once interface is in place, .convert() can pass header/footer + and wrap progress monitor around pcmreader in order to avoid + temporary aiff files. +**** DONE Update AiffAudio + - [X] has_foreign_aiff_chunks() + - [X] aiff_header_footer() + - [X] from_aiff(filename, header, pcmreader, footer, compression) + - [X] convert(target_page, target_class, compression, progress) +**** DONE Update FlacAudio + - [X] has_foreign_aiff_chunks() + - [X] aiff_header_footer() + - [X] from_aiff(filename, header, pcmreader, footer, compression) + - [X] convert(target_page, target_class, compression, progress) +**** DONE Update OggFlacAudio + - [X] has_foreign_aiff_chunks() + - [X] aiff_header_footer() + - [X] from_aiff(filename, header, pcmreader, footer, compression) + - [X] convert(target_page, target_class, compression, progress) +**** DONE Update ShortenAudio + - [X] has_foreign_aiff_chunks() + - [X] aiff_header_footer() + - [X] from_aiff(filename, header, pcmreader, footer, compression) + - [X] convert(target_page, target_class, compression, progress) +**** DONE Update programming documentation +**** DONE Update unit tests +** DONE Add pause()/resume() methods to low-level output players + This will hopefully make pausing more responsive + and work better than simply emptying the output buffer. +** DONE Ensure unicode command-line arguments are parsed properly +*** DONE cd2track + - [X] --format + - [X] --dir +*** DONE coverdump + - [X] filename arguments + - [X] --dir + - [X] --prefix +*** DONE covertag + - [X] filename arguments + - [X] --front-cover + - [X] --back-cover + - [X] --leaflet + - [X] --media + - [X] --other-image +*** DONE track2track + - [X] filename arguments + - [X] --dir + - [X] --format + - [X] --output +*** DONE trackcat + - [X] filename arguments + - [X] --output + - [X] --cue +*** DONE trackcmp + - [X] filename arguments +*** DONE trackinfo + - [X] filename arguments +*** DONE tracklength + - [X] filename arguments +*** DONE tracklint + - [X] filename arguments + - [X] --db +*** DONE trackrename + - [X] filename arguments + - [X] --format +*** DONE tracksplit + - [X] filename arguments + - [X] --cue + - [X] --dir + - [X] --format +*** DONE tracktag + - [X] filename arguments + - [X] --name + - [X] --artist + - [X] --album + - [X] --performer + - [X] --composer + - [X] --conductor + - [X] --catalog + - [X] --ISRC + - [X] --publisher + - [X] --media-type + - [X] --year + - [X] --date + - [X] --copyright + - [X] --comment + - [X] --comment-file +** DONE Add unit tests for Python-based decoders + Since Python-based codecs are so much slower + it's impossible to make these as comprehensive as the C-based tests. + So we'll only be able to test the basics using very small streams + against the output of the C-based decoders. +*** DONE FlacDecoder +*** DONE ALACDecoder +*** DONE WavPackDecoder +*** DONE SHNDecoder +** DONE Add unit tests for Python-based encoders +*** DONE encode_flac +*** DONE encode_mdat +*** DONE encode_wavpack +*** DONE encode_shn +** DONE Ensure Python files are PEP8-compliant +*** DONE executables + - [X] audiotools-config + - [X] cd2track + - [X] cdinfo + - [X] cdplay + - [X] coverdump + - [X] covertag + - [X] coverview + - [X] dvda2track + - [X] dvdainfo + - [X] track2cd + - [X] track2track + - [X] trackcat + - [X] trackcmp + - [X] trackinfo + - [X] tracklength + - [X] tracklint + - [X] trackplay + - [X] trackrename + - [X] tracksplit + - [X] tracktag + - [X] trackverify +*** DONE libraries + - [X] __init__.py + - [X] accuraterip.py + - [X] aiff.py + - [X] ape.py + - [X] au.py + - [X] cue.py + - [X] delta.py + - [X] dvda.py + - [X] flac.py + - [X] freedb.py + - [X] id3.py + - [X] id3v1.py + - [X] image.py + - [X] m4a.py + - [X] m4a_atoms.py + - [X] mp3.py + - [X] musicbrainz.py + - [X] ogg.py + - [X] opus.py + - [X] player.py + - [X] shn.py + - [X] text.py + - [X] toc.py + - [X] ui.py + - [X] vorbis.py + - [X] vorbiscomment.py + - [X] wav.py + - [X] wavpack.py +**** DONE py_decoders + - [X] __init__.py + - [X] alac.py + - [X] flac.py + - [X] shn.py + - [X] wavpack.py +**** DONE py_encoders + - [X] __init__.py + - [X] alac.py + - [X] flac.py + - [X] shn.py + - [X] wavpack.py +** DONE Have interactive modes clear screen after finishing + Some systems leave the Urwid-built screens half-cleared + before resuming regular output. + It's preferable to erase the whole thing in that case. + - [X] audiotools-config + - [X] cd2track + - [X] cdplay + - [X] dvda2track + - [X] track2track + - [X] trackcat + - [X] trackplay + - [X] trackrename + - [X] tracksplit + - [X] tracktag +** DONE Have interactive modes detect termios.error at launch + This seems to be caused mostly by opening interactive modes + with arguments piped from xargs + - [X] audiotools-config + - [X] cd2track + - [X] cdplay + - [X] dvda2track + - [X] track2track + - [X] trackcat + - [X] trackplay + - [X] trackrename + - [X] tracksplit + - [X] tracktag +** DONE Spellcheck programming documentation + - [X] audiotools.rst + - [X] audiotools_bitstream.rst + - [X] audiotools_cdio.rst + - [X] audiotools_cue.rst + - [X] audiotools_pcm.rst + - [X] audiotools_pcmconverter.rst + - [X] audiotools_player.rst + - [X] audiotools_replaygain.rst + - [X] audiotools_toc.rst + - [X] index.rst + - [X] metadata.rst +** DONE Spellcheck reference documentation + - [X] aiff.tex + - [X] alac.tex + - [X] ape.tex + - [X] apev2.tex + - [X] au.tex + - [X] basics.tex + - [X] dvda2.tex + - [X] flac.tex + - [X] freedb.tex + - [X] introduction.tex + - [X] license.tex + - [X] m4a.tex + - [X] mp3.tex + - [X] musepack.tex + - [X] musicbrainz.tex + - [X] musicbrainz_mmd.tex + - [X] ogg.tex + - [X] oggflac.tex + - [X] references.tex + - [X] replaygain.tex + - [X] shorten.tex + - [X] speex.tex + - [X] vorbis.tex + - [X] wav.tex + - [X] wavpack.tex +** DONE Spellcheck reference figures +** DONE Spellcheck manual pages +** DONE Split reference documentation into sections by file + That is, massive codecs should be split into individual + .tex files by decode/encode sections and then subdivided + further as necessary to keep them from getting too fragile/unweildy +*** DONE Shorten + - [X] decode + - [X] encode +*** DONE FLAC +**** DONE metadata +**** DONE decode +**** DONE encode + - [X] fixed + - [X] residual + - [X] lpc +*** DONE WavPack +**** DONE decode + - [X] terms + - [X] weights + - [X] samples + - [X] entropy + - [X] bitstream + - [X] decorrelation +**** DONE encode + - [X] correlation + - [X] terms + - [X] weights + - [X] samples + - [X] entropy + - [X] bitstream +*** DONE ALAC +**** DONE decode +**** DONE encode + - [X] atoms + - [X] lpc + - [X] residual +*** DONE Remove m4 requirement + This is massive overkill for what little we need it for, + which is generating audioformats-_.tex and _-codec.tex files + with header/footers included and paper size populated. + + It's better to use Python for that trivial templating instead. +** DONE Tweak reference documentation layout + Try harder to get pseudocode, file diagram, + bit diagram and example on same pair of pages. +*** DONE Shorten +*** DONE FLAC +*** DONE WavPack +*** DONE ALAC +*** DONE DVD-A +** DONE Ensure documentation flows correctly + Pages should start/end properly at letter and A4 paper sizes. + - [X] letter + - [X] A4 +** DONE Handle multi-channel .opus files properly +*** DONE Add unit tests +** DONE Add optional interactive modes to utilities + Now that Urwid is being used for editxmcd, it might be helpful + to add optional console-based interactive modes to the other tools. + This may improve ease-of-use (particularly discoverability + in the case of format and quality options) without sacrificing + scriptability or command-line power. + + Just as the command-line options are kept as consistent as possible, + all interactive modes will also need a consistent interface. +*** DONE audiotools-config +*** DONE cd2track +*** DONE cdplay +**** DONE make player widget generic +*** DONE dvda2track +*** DONE track2track +*** DONE trackcat +*** DONE trackplay +**** DONE make player widget generic +*** DONE trackrename +*** DONE tracksplit +*** DONE tracktag +** DONE run codecs through valgrind + Try to ensure there's no hidden bugs in the low-level C code. +*** DONE decoders + Will need to assemble standalone decoders for this. + - [X] alac + - [X] flac + - [X] mlp + - [X] oggflac + - [X] shorten + - [X] wavpack +*** DONE encoders + - [X] alac + - [X] flac + - [X] shorten + - [X] wavpack +*** DONE Update standalone encoders to take command-line arguments + This would make them easier to memory test. + - [X] alac + - [X] flac + - [X] shorten + - [X] wavpack +** DONE Have cdplay play properly +** TODO Update version number to 2.19 final +* TODO Finish version 2.20 +** TODO Make decoder streams seekable + PCMReader.seek(pcm_frames) -> pcm_frames skipped + could seek to the closest position in the stream to the given frames + without going over, and return the number of frames actually skipped. + + This could dramatically improve the performance of + PCMReaderWindow and enable more stream features down the line. +*** TODO decoders.ALACDecoder +*** TODO decoders.FlacDecoder +*** TODO decoders.SHNDecoder +*** TODO decoders.WavPackDecoder +** TODO Update track2track to use better option filler widget + cd2track, dvda2track and tracksplit always work with one album + at a time, pretty much by definition. + track2track may handle multiple albums simultaneously + and currently makes mutiple calls to urwid to work. + A better approach is to have all the metadata lookups + performed first, multiple metadata edit screens, + and a single output options/preview screen. +** TODO Deprecate little-used / poorly-tested bits + Although there's a risk of annoying people by taking out + stuff they might be using, some utilities and formats + were added simply because it was easy and aren't tested + enough for my liking. +*** DONE record2track + This needs to be rewritten to support multiple audio sources + and constructed with a threaded GUI, or dropped. + The current implementation is entirely too half-assed. +*** DONE AACAudio +*** DONE SpeexAudio + I don't believe this format was ever meant for standalone use, + and I doubt anybody is using it that way either. + I just threw it in because it was easy and because + I already had a lot of Ogg stream parsing infrastructure in place. +*** TODO Ogg FLAC + I've kept this around for parity with the reference implementation + and for something to point my existing Ogg routines at. + In practice, nobody really uses it and there's no reason + to use it since it just makes your files slightly larger + than native FLAC and harder to tag for no real benefit. +*** TODO M4AAudio_faac + The authors of faad/faac have moved on to neroAacEnc/neroAacDec. + Even though M4AAudio_nero is also a hack involving intermediate + wav files, at least it gets updated. + Having two different, slightly incompatible M4A implementations + needlessly complicates things. +** TODO Remove track number guessing heuristics + Don't try to guess number from filename. + If a track number is needed, build it from the order + in which the files are submitted on the command line. +** TODO Update coverview widget set + Although wxPython is available lots of places, + it doesn't behave the same everywhere. + It's like a better Tkinter. + + PyGTK works well on X11 platforms, but is a non-starter on Mac OS. + + Using the built-in PyObjC bridge on Mac OS is the most compatible + solution, if it can be implemented without making a huge mess. +** TODO Add internal Monkey's Audio codec + This needs to be assimilated with a native decoder/encoder. + I've seen these out in the wild, but only rarely. +*** TODO Add Python-based Monkey's Audio decoder +*** TODO Document Monkey's Audio decoding +*** TODO Add C-based Monkey's Audio decoder +*** TODO Add Python-based Monkey's Audio encoder +*** TODO Document Monkey's Audio encoding +*** TODO Add C-based Monkey's Audio encoder +*** TODO Add Monkey's Audio-specific unit tests +*** TODO Test encoder/decoder against reference +*** TODO Restore AudioFile-compatible Python interface +** TODO Add internal WavPack codec +*** DONE update WavPack documentation to little-endian reading order +*** TODO Add WavPack decoder +**** DONE Add audiotools.decoders.WavPackDecoder object + Must include the following attributes: + - [X] sample_rate + - [X] bits_per_sample + - [X] channels + - [X] channel_mask + And the following methods: + - [X] __init__ + - [X] read() + - [X] analyze_frame() + - [X] close() +**** DONE Add block header parsing +**** DONE Add sub-block header parsing +**** DONE Parse sub-blocks + - [X] decorr_terms + - [X] decorr_weights + - [X] decorr_samples + - [X] entropy_vars + - [X] wv_bitstream +**** DONE Decode 1 or more blocks in read() +**** DONE Check for EOFs during decoding +**** DONE Return pcm.FrameList objects on calls to read() +**** DONE Return dict objects on calls to analyze_frame() +**** DONE Ensure different bits-per-sample work correctly + - [X] 8 bps + - [X] 16 bps + - [X] 24 bps +**** DONE Ensure different number of channels work correctly + - [X] 1 channel + - [X] 2 channels + - [X] 3 channels + - [X] 4 channels + - [X] 5 channels + - [X] 6 channels + - [X] 7 channels + - [X] 8 channels +**** DONE Ensure different sample rates work correctly + - [X] 44100Hz + - [X] 48000Hz + - [X] 96000Hz + - [X] 192000Hz +**** TODO Ensure test files decode correctly +***** TODO bit_depths + - [X] 8bit.wv + - [ ] 12bit.wv + - [X] 16bit.wv + - [ ] 20bit.wv + - [X] 24bit.wv + - [ ] 32bit_int.wv + - [ ] 32bit_int_p.wv + - [ ] 32bit_float_p.wv + - [ ] 32bit_float.wv + (I may omit the odd bit depths in the short term) +***** TODO hybrid_bitrates + - [ ] 24kbps.wv + - [ ] 32kbps.wv + - [ ] 48kbps.wv + - [ ] 64kbps.wv + - [ ] 128kbps.wv + - [ ] 160kbps.wv + - [ ] 256kbps.wv + - [ ] 320kbps.wv + - [ ] 384kbps.wv + - [ ] 512kbps.wv + - [ ] 1024kbps.wv +***** TODO hybrid_bitrates/wvc_files + - [ ] 24kbps.wvc + - [ ] 32kbps.wvc + - [ ] 48kbps.wvc + - [ ] 64kbps.wvc + - [ ] 128kbps.wvc + - [ ] 160kbps.wvc + - [ ] 256kbps.wvc + - [ ] 320kbps.wvc + - [ ] 384kbps.wvc + - [ ] 512kbps.wvc + - [ ] 1024kbps.wvc +***** DONE num_channels + - [X] mono-1.wv + - [X] stereo-2.wv +***** TODO special_cases + - [ ] cue_sheet.wv + - [ ] cue_sheet.wvc + - [X] false_stereo.wv + - [ ] win_executable.wv + - [X] zero_lsbs.wv +***** DONE speed_modes + - [X] default.wv + - [X] fast.wv + - [X] high.wv + - [X] vhigh.wv +**** DONE Ensure block CRC is checked +**** DONE Ensure trailing MD5 is checked, if present +*** DONE Add WavPack encoder +**** DONE Add audiotools.encoders.encode_wavpack function +**** DONE Split samples into blocks based on channel count +**** DONE Write one block per block-size number of channels, per channel pair +***** DONE Calculate block CRC +***** DONE Write placeholder block header +***** DONE Determine tunable data per block + - [X] decorrelation terms + - [X] decorrelation deltas + - [X] decorrelation weights + - [X] decorrelation samples + - [X] entropy variables (medians) +***** DONE Write sub-blocks containing tunables data +****** DONE decorrelation terms/deltas +****** DONE decorrelation weights +****** DONE decorrelation samples +****** DONE entropy variables +***** DONE Calculate bitstream values from entropy variables and PCM data +***** DONE Write bitstream sub-block +***** DONE Calculate joint-stereo +***** DONE Add channel mask sub-block for multi-channel audio +**** DONE Rewrite block headers with final "total samples" data + Using from_pcm(), this data won't be known in advance +**** DONE Allow external RIFF WAVE header/footer + As with Shorten, these may be passed in from outside. +**** DONE Rewrite RIFF WAVE data block header with final size + If we're building the file with from_pcm(), this header + will need to be built and then filled-in at finalize-time. +**** DONE Update for standalone operation + Not necessarily to generate fully compliant files, + but to ensure there are no memory leaks and prep + for gprof-style optimizing. +**** DONE Generate trailing MD5 +**** DONE Unify the Python and standalone encode_wavpack functions + Much like FLAC's, making them more similar should reduce potential bugs. +**** DONE Identify/Optimize false stereo files +**** DONE Identify/Optimize wasted left bits +**** DONE Optimize memory usage + Perform less memory allocation/freeing over the course of encoding. +**** DONE Optimize for speed +*** DONE Add WavPack-specific unit tests + - [X] test_small_files + - [X] test_full_scale_deflection + - [X] test_sines + - [X] test_wasted_bps + - [X] test_blocksizes + - [X] test_option_variations + - [X] test_silence + - [X] test_noise + - [X] test_fractional + - [X] test_multichannel + A combination of converted FLAC unit tests + and stuff that's bitten me during initial implementation. +*** DONE Document WavPack +**** DONE the WavPack file stream +**** DONE WavPack decoding +***** DONE WavPack sub-blocks + - [X] decorr_terms + - [X] decorr_weights + - [X] decorr_samples + - [X] entropy_vars + - [X] wv_bitstream +***** DONE decorrelation passes +***** DONE joint stereo +***** DONE int32 info + - [X] zeroes + - [X] ones + - [X] dupes + - [X] shift +***** DONE channel info +***** DONE checksum +***** DONE RIFF sub-blocks +***** DONE MD5 sub-block +***** DONE false stereo +**** DONE WavPack encoding +***** DONE false stereo +***** DONE int32_info + for left shifting zeroes, in particular +***** DONE checksum +***** DONE joint stereo +***** DONE WavPack block header +***** DONE decorrelation passes +***** DONE WavPack sub-blocks + - [X] decorr_terms + - [X] decorr_weights + - [X] decorr_samples + - [X] entropy_vars + - [X] extended integers + - [X] channel info + - [X] wv_bitstream + - [X] RIFF sub-blocks + - [X] MD5 sub-block +***** DONE bitstream generation +**** DONE Ensure section capitalization is consistent +**** DONE Spell-check +**** DONE Verify A4 layout is correct +*** DONE Test encoder/decoder against reference + Ensure a wide range of lossless audio round-trips correctly +*** DONE Remove WavePackAudio.BINARIES +*** DONE Add more WavPack INVALIDFILE .verify() tests +*** DONE Remove wvgain for ReplayGain application +** TODO Add volume control to low-level output players + It should be possible to adjust the volume from trackplay/cdplay + without adjusting the output data itself. +** TODO Add raop support + It'd be useful to have support for AirPlay devices + using the output routines from raop_play as a base. +* TODO Finish version 3.0 +** TODO Shift to Python 3 + This lengthy transition will have to wait + until Python 3 is installed by default on + common Linux distros and Mac OS.
View file
audiotools-2.18.tar.gz/audiotools-config -> audiotools-2.19.tar.gz/audiotools-config
Changed
@@ -19,232 +19,924 @@ import sys -import gettext +import os.path -gettext.install("audiotools", unicode=True) +def display_defaults(config, msg): + def display_option(msg, label, value, indent=2): + """label and value should be unicode objects""" -def check_binaries(msg, binary_list): - for binary in binary_list: + msg.output(u"%s%s%s" % (u" " * indent, + u"%s : " % (msg.ansi(label, [msg.BOLD])), + value)) + + def display_boolean_option(msg, label, value, indent=2): + """value should be a boolean""" + + display_option(msg, label, u"yes" if value else u"no", indent) + + display_option(msg, + _.LAB_AT_CONFIG_VERBOSITY, + config.get_default("Defaults", + "verbosity", + "normal").decode('utf-8'), + indent=0) + + msg.output(u"") + msg.output(_.OPT_CAT_EXTRACTION) + + msg.new_row() + msg.output_column(u" ") + msg.output_column(_.LAB_AT_CONFIG_TYPE) + msg.output_column(u" ") + msg.output_column(_.LAB_AT_CONFIG_BINARIES, True) + msg.output_column(u" ") + msg.output_column(_.LAB_AT_CONFIG_DEFAULT_QUALITY) + + msg.divider_row([u" ", u"\u2500", u" ", u"\u2500", u" ", u"\u2500"]) + + for audio_type in sorted(audiotools.AVAILABLE_TYPES, + lambda x, y: cmp(x.NAME, y.NAME)): msg.new_row() - if (audiotools.BIN.can_execute(audiotools.BIN[binary])): - msg.output_column(u"+ ") - msg.output_column(_(u"found"), True) - msg.output_column(u" : ") - msg.output_column(binary.decode('ascii')) + msg.output_column(u" ") + options = [] + if (config.get_default("System", + "default_type", + "wav") == audio_type.NAME): + options.append(msg.UNDERLINE) + if (audio_type.has_binaries(audiotools.BIN)): + options.append(msg.FG_GREEN) + else: + options.append(msg.FG_RED) + msg.output_column(msg.ansi(audio_type.NAME.decode('ascii'), + options), True) + msg.output_column(u" ") + if (len(audio_type.BINARIES) < 2): + msg.output_column(u"") else: - msg.output_column(u"- ") - msg.output_column(_(u"not found"), True) - msg.output_column(u" : ") - msg.output_column(binary.decode('ascii')) + msg.output_column( + u", ".join( + [msg.ansi(b.decode('ascii'), + [msg.FG_GREEN if + audiotools.BIN.can_execute(audiotools.BIN[b]) + else msg.FG_RED]) + for b in audio_type.BINARIES]), + True) + + msg.output_column(u" ") + if (len(audio_type.COMPRESSION_MODES) < 2): + msg.output_column(u" ") + else: + quality = config.get_default("Quality", + audio_type.NAME, + audio_type.DEFAULT_COMPRESSION) + if (quality in audio_type.COMPRESSION_DESCRIPTIONS): + msg.output_column( + u"%s - %s" % + (quality.decode('ascii'), + audio_type.COMPRESSION_DESCRIPTIONS[quality])) + else: + msg.output_column( + quality.decode('ascii')) + msg.output_rows() + msg.output(u"") + + display_option( + msg, + _.LAB_OPTIONS_FILENAME_FORMAT, + config.get_default("Filenames", + "format", + audiotools.DEFAULT_FILENAME_FORMAT).decode('utf-8')) + + display_option(msg, + _.LAB_AT_CONFIG_JOBS, + unicode(config.get_default("System", + "maximum_jobs", + audiotools.MAX_JOBS))) + + display_boolean_option(msg, + _.LAB_AT_CONFIG_ADD_REPLAY_GAIN, + config.getboolean_default("ReplayGain", + "add_by_default", + True)) + + msg.output(u"") + msg.output(_.OPT_CAT_ID3) + + display_option( + msg, + _.LAB_AT_CONFIG_ID3V2_VERSION, + {"id3v2.2": _.LAB_AT_CONFIG_ID3V2_ID3V22, + "id3v2.3": _.LAB_AT_CONFIG_ID3V2_ID3V23, + "id3v2.4": _.LAB_AT_CONFIG_ID3V2_ID3V24, + "none": _.LAB_AT_CONFIG_ID3V2_NONE}.get( + config.get_default("ID3", "id3v2", "id3v2.3"))) + + display_option( + msg, + _.LAB_AT_CONFIG_ID3V2_PADDING, + {True: _.LAB_AT_CONFIG_ID3V2_PADDING_YES, + False: _.LAB_AT_CONFIG_ID3V2_PADDING_NO}.get( + config.getboolean_default("ID3", "pad", False))) + + display_option( + msg, + _.LAB_AT_CONFIG_ID3V1_VERSION, + {"id3v1.1": _.LAB_AT_CONFIG_ID3V1_ID3V11, + "none": _.LAB_AT_CONFIG_ID3V1_NONE}.get( + config.get_default("ID3", + "id3v1", + "id3v1.1"))) + + msg.output(u"") + msg.output(_.OPT_CAT_CD_LOOKUP) + + display_boolean_option(msg, + _.LAB_AT_CONFIG_USE_MUSICBRAINZ, + config.getboolean_default("MusicBrainz", + "service", + True)) + display_option(msg, + _.LAB_AT_CONFIG_MUSICBRAINZ_SERVER, + config.get_default("MusicBrainz", + "server", + "musicbrainz.org").decode('utf-8')) + + display_option(msg, + _.LAB_AT_CONFIG_MUSICBRAINZ_PORT, + unicode(config.getint_default("MusicBrainz", + "port", + 80))) + + msg.output(u"") + + display_boolean_option(msg, + _.LAB_AT_CONFIG_USE_FREEDB, + config.getboolean_default("FreeDB", + "service", + True)) + display_option(msg, + _.LAB_AT_CONFIG_FREEDB_SERVER, + config.get_default("FreeDB", + "server", + "us.freedb.org").decode('utf-8')) + + display_option(msg, + _.LAB_AT_CONFIG_FREEDB_PORT, + unicode(config.getint_default("FreeDB", + "port", + 80))) + + msg.output(u"") + msg.output(_.OPT_CAT_SYSTEM) + + display_option( + msg, + _.LAB_AT_CONFIG_DEFAULT_CDROM, + config.get_default("System", + "cdrom", + "/dev/cdrom").decode(audiotools.FS_ENCODING)) + + display_option( + msg, + _.LAB_AT_CONFIG_CDROM_READ_OFFSET, + unicode(config.getint_default("System", + "cdrom_read_offset", + 0))) + + display_option( + msg, + _.LAB_AT_CONFIG_CDROM_WRITE_OFFSET, + unicode(config.getint_default("System", + "cdrom_write_offset", + 0))) + + display_option( + msg, + _.LAB_AT_CONFIG_FS_ENCODING, + config.get_default("System", + "fs_encoding", + sys.getfilesystemencoding()).decode('ascii')) + + display_option( + msg, + _.LAB_AT_CONFIG_IO_ENCODING, + config.get_default("System", "io_encoding", "UTF-8").decode('ascii')) + + display_option( + msg, + _.LAB_AT_CONFIG_AUDIO_OUTPUT, + u", ".join([player.NAME.decode('ascii') + for player in audiotools.player.AUDIO_OUTPUT + if player.available()])) + + +def apply_options(options, config): + """given an OptionParser's options dict + and audiotools.RawConfigParser object + applies the options to the config""" + + #apply --verbose option + if (options.verbosity is not None): + config.set_default("Defaults", "verbosity", options.verbosity) + + #apply transcoding options + if (options.filename_format is not None): + config.set_default("Filenames", "format", options.filename_format) + + if (options.quality is None): + #not setting no --quality value + if (options.type is None): + #do nothing + pass + else: + #set new default output type + config.set_default("System", "default_type", options.type) + else: + #setting new --quality value + if (options.type is None): + #set new quality value for current default type + AudioType = audiotools.TYPE_MAP[audiotools.DEFAULT_TYPE] + else: + #set new quality value for given type + AudioType = audiotools.TYPE_MAP[options.type] + config.set_default("Quality", AudioType.NAME, options.quality) + if (options.system_maximum_jobs is not None): + config.set_default("System", "maximum_jobs", + str(options.system_maximum_jobs)) -class FormatSummary: - def __init__(self, format, quality, binaries, replaygain): - """format and quality are unicode. The rest are list of unicode. - - All should be formatted for output.""" - - self.format = format - self.quality = quality - self.binaries = binaries - self.replaygain = replaygain - - @classmethod - def headers(cls, messenger): - messenger.new_row() - messenger.output_column(u" ") - messenger.output_column(u" ") - messenger.output_column(u" ") - messenger.output_column(u" ") - messenger.output_column(u" default ") - messenger.output_column(u" ") - messenger.output_column(u" ") - - messenger.new_row() - messenger.output_column(_(u" type ")) - messenger.output_column(u" ") - messenger.output_column(_(u" binaries ")) - messenger.output_column(u" ") - messenger.output_column(_(u" quality ")) - messenger.output_column(u" ") - messenger.output_column(_(u" ReplayGain ")) - messenger.divider_row([u"-", u"+", u"-", - u"+", u"-", u"+", u"-"]) - - def output_rows(self, messenger): - #first, put our rows of data into lists of columns - - format_col = [self.format] - quality_col = [self.quality] - binaries_col = self.binaries[:] - replaygain_col = self.replaygain[:] - - #then, make our columns consistently-sized - total_rows = max(map(len, [format_col, - quality_col, - binaries_col, - replaygain_col])) - - for col in [format_col, quality_col, binaries_col, replaygain_col]: - col.extend([u""] * (total_rows - len(col))) - - #finally, output our rows to the Messenger - for (format, quality, binary, replaygain) in zip(format_col, - quality_col, - binaries_col, - replaygain_col): - messenger.new_row() - messenger.output_column(u" %s " % (format), True) - messenger.output_column(u"|") - messenger.output_column(u" %s " % (binary)) - messenger.output_column(u"|") - messenger.output_column(u" %s " % (quality)) - messenger.output_column(u"|") - messenger.output_column(u" %s " % (replaygain)) - messenger.divider_row([u"-", u"+", u"-", - u"+", u"-", u"+", u"-"]) - - @classmethod - def from_audiofile(cls, messenger, audiofile, binaries): - if (audiofile.has_binaries(binaries)): - code = [messenger.FG_GREEN] - else: - code = [messenger.FG_RED] + #apply CD lookup options + if (options.use_musicbrainz is not None): + config.set_default("MusicBrainz", "service", + True if (options.use_musicbrainz == "yes") + else False) - if (audiofile.NAME == audiotools.DEFAULT_TYPE): - code.append(messenger.BOLD) - code.append(messenger.UNDERLINE) + if (options.musicbrainz_server is not None): + config.set_default("MusicBrainz", "server", options.musicbrainz_server) - format = messenger.ansi(audiofile.NAME.decode('ascii'), code) + if (options.musicbrainz_port is not None): + config.set_default("MusicBrainz", "port", options.musicbrainz_port) - binaries_list = [] - for binary in audiofile.BINARIES: - if (binaries.can_execute(binaries[binary])): - code = [messenger.FG_GREEN] - else: - code = [messenger.FG_RED] - binaries_list.append(messenger.ansi(binaries[binary], code)) - - replaygain = [] - if (len(audiofile.REPLAYGAIN_BINARIES) > 0): - for binary in audiofile.REPLAYGAIN_BINARIES: - if (binaries.can_execute(binaries[binary])): - code = [messenger.FG_GREEN] - else: - code = [messenger.FG_RED] - replaygain.append(messenger.ansi(binaries[binary], code)) - elif (audiofile.can_add_replay_gain()): - replaygain.append(messenger.ansi("YES", [messenger.FG_GREEN])) - else: - replaygain.append("N/A") + if (options.use_freedb is not None): + config.set_default("FreeDB", "service", + True if (options.use_freedb == "yes") + else False) - return cls(format, - audiotools.__default_quality__( - audiofile.NAME).decode('ascii'), - binaries_list, - replaygain) + if (options.freedb_server is not None): + config.set_default("FreeDB", "server", options.freedb_server) + if (options.freedb_port is not None): + config.set_default("FreeDB", "port", options.freedb_port) -if (__name__ == '__main__'): - try: - import audiotools - except ImportError: - print _(u"* audiotools Python module not found!") - print _(u"Perhaps you should re-install the Python Audio Tools") - sys.exit(1) - try: - import audiotools.player - except ImportError: - print _(u"* audiotools.player Python module not found!") - print _(u"Perhaps you should re-install the Python Audio Tools") - sys.exit(1) + #apply ID3 options + if (options.id3v2_version is not None): + config.set_default("ID3", "id3v2", options.id3v2_version) - parser = audiotools.OptionParser( - usage=_(u'%prog [options]'), - version="Python Audio Tools %s" % (audiotools.VERSION)) + if (options.id3v1_version is not None): + config.set_default("ID3", "id3v1", options.id3v1_version) - opt_map = {"system_cdrom": ("System", "cdrom"), - "system_cdrom_read_offset": ("System", "cdrom_read_offset"), - "system_cdrom_write_offset": ("System", "cdrom_write_offset"), - "system_fs_encoding": ("System", "fs_encoding"), - "system_io_encoding": ("System", "io_encoding"), - "system_maximum_jobs": ("System", "maximum_jobs"), + if (options.id3_digit_padding is not None): + config.set_default("ID3", "pad", + True if (options.id3_digit_padding == "yes") + else False) - "thumbnail_format": ("Thumbnail", "format"), - "thumbnail_size": ("Thumbnail", "size"), + #apply ReplayGain options + if (options.add_replaygain is not None): + config.set_default("ReplayGain", "add_by_default", + True if (options.add_replaygain == "yes") + else False) - "freedb_server": ("FreeDB", "server"), - "freedb_port": ("FreeDB", "port"), - "use_freedb": ("FreeDB", "service"), + #apply system options + if (options.system_cdrom is not None): + config.set_default("System", "cdrom", options.system_cdrom) - "musicbrainz_server": ("MusicBrainz", "server"), - "musicbrainz_port": ("MusicBrainz", "port"), - "use_musicbrainz": ("MusicBrainz", "service"), + if (options.system_cdrom_read_offset is not None): + config.set_default("System", "cdrom_read_offset", + options.system_cdrom_read_offset) - "filename_format": ("Filenames", "format"), - "verbosity": ("Defaults", "verbosity"), + if (options.system_cdrom_write_offset is not None): + config.set_default("System", "cdrom_write_offset", + options.system_cdrom_write_offset) - "id3_digit_padding": ("ID3", "pad"), - "id3v2_version": ("ID3", "id3v2"), - "id3v1_version": ("ID3", "id3v1"), - "add_replaygain": ("ReplayGain", "add_by_default")} + if (options.system_fs_encoding is not None): + config.set_default("System", "fs_encoding", options.system_fs_encoding) - system = audiotools.OptionGroup(parser, _(u"System Options")) + if (options.system_io_encoding is not None): + config.set_default("System", "io_encoding", options.system_io_encoding) - system.add_option('-c', '--cdrom', - action='store', - dest='system_cdrom', - metavar='PATH', - help=_(u'the CD-ROM drive to use')) + #apply binaries options + bins = set([]) + for audioclass in audiotools.AVAILABLE_TYPES: + for binary in audioclass.BINARIES: + bins.add(binary) - system.add_option('--cdrom-read-offset', - action='store', - type='int', - metavar='INT', - dest='system_cdrom_read_offset', - help=_(u'the CD-ROM read offset to use')) + for binary in bins: + setting = getattr(options, "binary_" + binary) + if (setting is not None): + config.set_default("Binaries", binary, setting) - system.add_option('--cdrom-write-offset', - action='store', - type='int', - metavar='INT', - dest='system_cdrom_write_offset', - help=_(u'the CD-ROM write offset to use')) - system.add_option( - '--fs-encoding', - action='store', - metavar='ENCODING', - dest='system_fs_encoding', - help=_(u"the filesystem's text encoding")) +if (__name__ == '__main__'): + #There's no good way to make these dynamic + #since the configurable text comes from the audiotools.text module + #which we can't load because the module isn't installed correctly. + try: + import audiotools + import audiotools.ui + except ImportError: + print "* audiotools Python module not found!" + print "Perhaps you should re-install the Python Audio Tools" + sys.exit(1) + try: + import audiotools.player + except ImportError: + print "* audiotools.player Python module not found!" + print "Perhaps you should re-install the Python Audio Tools" + sys.exit(1) - system.add_option( - '--io-encoding', - action='store', - metavar='ENCODING', - dest='system_io_encoding', - help=_(u"the system's text encoding")) + import audiotools.text as _ + + if (audiotools.ui.AVAILABLE): + #setup widgets for interactive mode + + urwid = audiotools.ui.urwid + import termios + + class TranscodingOptions(urwid.ListBox): + def __init__(self, config): + #get defaults from current config file + DEFAULT_TYPE = config.get_default("System", + "default_type", + "wav") + if (not audiotools.TYPE_MAP[DEFAULT_TYPE].has_binaries( + audiotools.BIN)): + DEFAULT_TYPE = "wav" + + audio_types = list(sorted(audiotools.TYPE_MAP.values(), + lambda x, y: cmp(x.NAME, y.NAME))) + name_size = max(len(t.NAME) for t in audio_types) + default_format = [] + + format_rows = [] + format_rows.append( + urwid.Columns( + [("fixed", + len(audiotools.display_unicode( + _.LAB_AT_CONFIG_DEFAULT)), + urwid.Text(("label", _.LAB_AT_CONFIG_DEFAULT))), + ("fixed", + len(audiotools.display_unicode( + _.LAB_AT_CONFIG_TYPE)), + urwid.Text(("label", _.LAB_AT_CONFIG_TYPE))), + ("fixed", 1, urwid.Text(u" ")), + ("weight", 1, + urwid.Text(("label", + _.LAB_AT_CONFIG_DEFAULT_QUALITY)))], + dividechars=1)) + format_rows.append( + urwid.Columns( + [("fixed", + len(audiotools.display_unicode( + _.LAB_AT_CONFIG_DEFAULT)), + urwid.Divider(u"\u2500")), + ("fixed", + len(audiotools.display_unicode( + _.LAB_AT_CONFIG_TYPE)), + urwid.Divider(u"\u2500")), + ("fixed", 1, urwid.Text(u" ")), + ("weight", 1, urwid.Divider(u"\u2500"))], + dividechars=1)) + + for audio_type in audio_types: + if (len(audio_type.COMPRESSION_MODES) < 2): + qualities = urwid.Text(u"") + no_modes = True + else: + DEFAULT_QUALITY = config.get_default( + "Quality", + audio_type.NAME, + audio_type.DEFAULT_COMPRESSION) + + qualities = \ + audiotools.ui.SelectOne( + [(q.decode('ascii') if + q not in + audio_type.COMPRESSION_DESCRIPTIONS else + u"%s - %s" % + (q.decode('ascii'), + audio_type.COMPRESSION_DESCRIPTIONS[q]), + q) for q in + audio_type.COMPRESSION_MODES], + DEFAULT_QUALITY, + on_change=self.change_quality, + user_data=(config, + "Quality", + audio_type.NAME), + label=_.LAB_OPTIONS_AUDIO_QUALITY) + no_modes = False + + format_rows.append( + urwid.Columns( + [("fixed", + len(audiotools.display_unicode( + _.LAB_AT_CONFIG_DEFAULT)), + urwid.RadioButton( + default_format, + label=u"", + state=(audio_type.NAME == DEFAULT_TYPE), + on_state_change=self.change_choice, + user_data=(config, + "System", + "default_type", + audio_type.NAME))), + ("fixed", + len(audiotools.display_unicode( + _.LAB_AT_CONFIG_TYPE)), + urwid.Text( + markup=audio_type.NAME.decode('ascii'), + align="right")), + ("fixed", 1, + urwid.Text(u" " if no_modes else u"-")), + ("weight", 1, qualities) + ], + dividechars=1)) + + format = urwid.Edit( + caption=("label", + u"%s : " % (_.LAB_OPTIONS_FILENAME_FORMAT)), + edit_text=config.get_default( + "Filenames", + "format", + audiotools.DEFAULT_FILENAME_FORMAT).decode('utf-8'), + wrap='clip') + urwid.connect_signal(format, + "change", + self.change_text, + (config, + "Filenames", + "format")) + + format_rows.append(urwid.Divider(u" ")) + format_rows.append( + urwid.Columns( + [('weight', 1, format), + ('fixed', 10, audiotools.ui.BrowseFields(format))])) + + joint = urwid.IntEdit( + caption=("label", u"%s : " % (_.LAB_AT_CONFIG_JOBS,)), + default=config.getint_default("System", "maximum_jobs", 1)) + urwid.connect_signal(joint, + "change", + self.change_int, + (config, + "System", + "maximum_jobs")) + + format_rows.append(joint) + + replay_gain_row = urwid.CheckBox( + label=("label", _.LAB_AT_CONFIG_ADD_REPLAY_GAIN), + state=config.getboolean_default( + "ReplayGain", + "add_by_default", + True), + on_state_change=self.change_boolean, + user_data=(config, + "ReplayGain", + "add_by_default")) + + format_rows.append(replay_gain_row) + + urwid.ListBox.__init__( + self, + format_rows) + + def change_text(self, widget, new_value, user_data): + (config, section, option) = user_data + config.set_default(section, option, new_value) + + def change_int(self, widget, new_value, user_data): + (config, section, option) = user_data + try: + config.set_default(section, option, int(new_value)) + except ValueError: + config.set_default(section, option, 0) + + def change_choice(self, radiobutton, new_state, user_data): + if (new_state): + (config, section, option, value) = user_data + config.set_default(section, option, value) + + def change_quality(self, new_value, user_data): + (config, section, option) = user_data + config.set_default(section, option, new_value) + + def change_boolean(self, checkbox, new_state, user_data): + (config, section, option) = user_data + config.set_default(section, option, new_state) + + class CDLookup(urwid.ListBox): + def __init__(self, config): + #get defaults from current config file + + use_musicbrainz = urwid.CheckBox( + label=("label", _.LAB_AT_CONFIG_USE_MUSICBRAINZ), + state=config.getboolean_default( + "MusicBrainz", + "service", + True), + on_state_change=self.change_boolean, + user_data=(config, + "MusicBrainz", + "service")) + + musicbrainz_server = urwid.Edit( + caption=("label", + u"%s : " % (_.LAB_AT_CONFIG_MUSICBRAINZ_SERVER)), + edit_text=config.get_default( + "MusicBrainz", + "server", + "musicbrainz.org").decode('utf-8')) + urwid.connect_signal(musicbrainz_server, + "change", + self.change_text, + (config, + "MusicBrainz", + "server")) + + musicbrainz_port = urwid.IntEdit( + caption=("label", + u"%s : " % (_.LAB_AT_CONFIG_MUSICBRAINZ_PORT)), + default=config.getint_default( + "MusicBrainz", + "port", + 80)) + urwid.connect_signal(musicbrainz_port, + "change", + self.change_int, + (config, + "MusicBrainz", + "port")) + + use_freedb = urwid.CheckBox( + label=("label", _.LAB_AT_CONFIG_USE_FREEDB), + state=config.getboolean_default( + "FreeDB", + "service", + True), + on_state_change=self.change_boolean, + user_data=(config, + "FreeDB", + "service")) + + freedb_server = urwid.Edit( + caption=("label", + u"%s : " % (_.LAB_AT_CONFIG_FREEDB_SERVER)), + edit_text=config.get_default( + "FreeDB", + "server", + "us.freedb.org").decode('utf-8')) + urwid.connect_signal(freedb_server, + "change", + self.change_text, + (config, + "FreeDB", + "server")) + + freedb_port = urwid.IntEdit( + caption=("label", + u"%s : " % (_.LAB_AT_CONFIG_FREEDB_PORT)), + default=config.getint_default( + "FreeDB", + "port", + 80)) + urwid.connect_signal(freedb_port, + "change", + self.change_int, + (config, + "FreeDB", + "port")) + + urwid.ListBox.__init__(self, + [use_musicbrainz, + musicbrainz_server, + musicbrainz_port, + urwid.Divider(u" "), + use_freedb, + freedb_server, + freedb_port]) + + def change_boolean(self, checkbox, new_state, user_data): + (config, section, option) = user_data + config.set_default(section, option, new_state) + + def change_text(self, widget, new_value, user_data): + (config, section, option) = user_data + config.set_default(section, option, new_value) + + def change_int(self, widget, new_value, user_data): + (config, section, option) = user_data + try: + config.set_default(section, option, int(new_value)) + except ValueError: + config.set_default(section, option, 0) + + class ID3(urwid.ListBox): + def __init__(self, config): + default_id3v2_version = config.get_default("ID3", + "id3v2", + "id3v2.3") + default_id3v1_version = config.get_default("ID3", + "id3v1", + "id3v1.1") + default_id3v2_padding = config.getboolean_default( + "ID3", "pad", False) + + id3v2_version = [] + id3v2_version_row = urwid.Columns( + [("fixed", + len(audiotools.display_unicode( + _.LAB_AT_CONFIG_ID3V2_VERSION)) + 3, + urwid.Text(("label", u"%s : " % + (_.LAB_AT_CONFIG_ID3V2_VERSION))))] + + [("weight", + 1, + urwid.RadioButton( + group=id3v2_version, + label=radio_label, + state=radio_value == default_id3v2_version, + on_state_change=self.change_choice, + user_data=(config, + "ID3", + "id3v2", + radio_value))) + for (radio_value, radio_label) in + [("id3v2.4", u"ID3v2.4"), + ("id3v2.3", u"ID3v2.3"), + ("id3v2.2", u"ID3v2.2"), + ("none", u"No ID3v2")]]) + + id3v1_version = [] + id3v1_version_row = urwid.Columns( + [("fixed", + len(audiotools.display_unicode( + _.LAB_AT_CONFIG_ID3V1_VERSION)) + 3, + urwid.Text(("label", u"%s : " % + (_.LAB_AT_CONFIG_ID3V1_VERSION))))] + + [("weight", + 1, + urwid.RadioButton( + group=id3v1_version, + label=radio_label, + state=radio_value == default_id3v1_version, + on_state_change=self.change_choice, + user_data=(config, + "ID3", + "id3v1", + radio_value))) + for (radio_value, radio_label) in + [("id3v1.1", u"ID3v1.1"), + ("none", u"No ID3v1")]]) + + id3_padding = [] + id3_pad_row = urwid.Columns( + [("fixed", + len(audiotools.display_unicode( + _.LAB_AT_CONFIG_ID3V2_PADDING)) + 3, + urwid.Text(("label", u"%s : " % + (_.LAB_AT_CONFIG_ID3V2_PADDING))))] + + [("weight", + 1, + urwid.RadioButton( + group=id3_padding, + label=radio_label, + state=radio_value == default_id3v2_padding, + on_state_change=self.change_choice, + user_data=(config, + "ID3", + "pad", + radio_value))) + for (radio_value, radio_label) in + [(True, _.LAB_AT_CONFIG_ID3V2_PADDING_YES), + (False, _.LAB_AT_CONFIG_ID3V2_PADDING_NO)]]) + + urwid.ListBox.__init__(self, + [id3v2_version_row, + id3_pad_row, + id3v1_version_row]) + + def change_choice(self, radiobutton, new_state, user_data): + if (new_state): + (config, section, option, value) = user_data + config.set_default(section, option, value) + + class CDROM(urwid.ListBox): + def __init__(self, config): + cdrom_device = urwid.Edit( + caption=("label", + u"%s : " % (_.LAB_AT_CONFIG_DEFAULT_CDROM)), + edit_text=config.get_default( + "System", + "cdrom", + "/dev/cdrom").decode(audiotools.FS_ENCODING)) + urwid.connect_signal(cdrom_device, + "change", + self.change_text, + (config, + "System", + "cdrom")) + + read_offset = urwid.IntEdit( + caption=("label", + u"%s : " % (_.LAB_AT_CONFIG_CDROM_READ_OFFSET)), + default=config.getint_default("System", + "cdrom_read_offset", + 0)) + urwid.connect_signal(read_offset, + "change", + self.change_int, + (config, + "System", + "cdrom_read_offset")) + + write_offset = urwid.IntEdit( + caption=("label", + u"%s : " % (_.LAB_AT_CONFIG_CDROM_WRITE_OFFSET)), + default=config.getint_default("System", + "cdrom_write_offset", + 0)) + urwid.connect_signal(write_offset, + "change", + self.change_int, + (config, + "System", + "cdrom_write_offset")) + + urwid.ListBox.__init__(self, + [cdrom_device, + read_offset, + write_offset]) + + def change_text(self, widget, new_value, user_data): + (config, section, option) = user_data + config.set_default(section, option, new_value) + + def change_int(self, widget, new_value, user_data): + (config, section, option) = user_data + try: + config.set_default(section, option, int(new_value)) + except ValueError: + config.set_default(section, option, 0) + + class AudiotoolsConfig(urwid.Frame): + def __init__(self, config): + self.config = config + self.__cancelled__ = True + self.status = urwid.Text(u"") + + transcode_box = urwid.LineBox( + TranscodingOptions(config), + title=_.OPT_CAT_EXTRACTION) + + cd_lookup_box = urwid.LineBox(CDLookup(config), + title=_.OPT_CAT_CD_LOOKUP) + + id3_box = urwid.LineBox(ID3(config), + title=_.OPT_CAT_ID3) + + verbosity = [] + verbosity_level = config.get_default("Defaults", + "verbosity", + "normal") + verbosity_row = urwid.Columns( + [("fixed", + len(audiotools.display_unicode( + _.LAB_AT_CONFIG_VERBOSITY)) + 3, + urwid.Text(("label", u"%s : " % + (_.LAB_AT_CONFIG_VERBOSITY))))] + + [("weight", 1, + urwid.RadioButton( + group=verbosity, + label=level.decode('ascii'), + state=level == verbosity_level, + on_state_change=self.change_choice, + user_data=(config, + "Defaults", + "verbosity", + level))) + for level in audiotools.VERBOSITY_LEVELS]) + + cdrom_box = urwid.LineBox(CDROM(config), + title=_.OPT_CAT_SYSTEM) + + completion_buttons = urwid.Filler( + urwid.Columns( + widget_list=[('weight', 1, + urwid.Button(_.LAB_CANCEL_BUTTON, + on_press=self.cancel)), + ('weight', 2, + urwid.Button(_.LAB_APPLY_BUTTON, + on_press=self.apply))], + dividechars=3, + focus_column=1)) + + option_widgets = urwid.ListBox( + [urwid.LineBox(verbosity_row), + urwid.BoxAdapter(transcode_box, + len(audiotools.TYPE_MAP) + 8), + urwid.BoxAdapter(id3_box, 5), + urwid.BoxAdapter(cd_lookup_box, 9), + urwid.BoxAdapter(cdrom_box, 5)]) + + urwid.Frame.__init__( + self, + body=urwid.Pile( + [("weight", 1, option_widgets), + ("fixed", 1, + urwid.Filler(urwid.Divider(div_char=u"\u2500"))), + ("fixed", 1, completion_buttons)]), + footer=self.status) + + def change_choice(self, radiobutton, new_state, user_data): + if (new_state): + (config, section, option, value) = user_data + config.set_default(section, option, value) + + def change_boolean(self, checkbox, new_state, user_data): + (config, section, option) = user_data + config.set_default(section, option, new_state) + + def change_text(self, widget, new_value, user_data): + (config, section, option) = user_data + config.set_default(section, option, new_value) + + def change_int(self, widget, new_value, user_data): + (config, section, option) = user_data + try: + config.set_default(section, option, int(new_value)) + except ValueError: + config.set_default(section, option, 0) + + def handle_text(self, i): + if (i == 'esc'): + self.__cancelled__ = True + raise urwid.ExitMainLoop() + + def apply(self, button): + #ensure --format is valid before returning + try: + audiotools.AudioFile.track_name( + file_path="", + track_metadata=audiotools.MetaData(), + format=self.config.get_default( + "Filenames", + "format", + audiotools.DEFAULT_FILENAME_FORMAT)) + + self.__cancelled__ = False + raise urwid.ExitMainLoop() + except audiotools.UnsupportedTracknameField, err: + self.status.set_text(("error", + _.ERR_INVALID_FILENAME_FORMAT)) + except audiotools.InvalidFilenameFormat, err: + self.status.set_text(("error", + _.ERR_INVALID_FILENAME_FORMAT)) + + def cancel(self, button): + self.__cancelled__ = True + raise urwid.ExitMainLoop() + + def cancelled(self): + return self.__cancelled__ - parser.add_option_group(system) + parser = audiotools.OptionParser( + usage=_.USAGE_AT_CONFIG, + version="Python Audio Tools %s" % (audiotools.VERSION)) - transcoding = audiotools.OptionGroup(parser, _(u"Transcoding Options")) + transcoding = audiotools.OptionGroup(parser, _.OPT_CAT_TRANSCODING) transcoding.add_option( '-t', '--type', action='store', dest='type', - choices=audiotools.TYPE_MAP.keys(), - help=_(u'the default audio type to use, ' + - 'or the type for a given default quality level')) + choices=sorted(audiotools.TYPE_MAP.keys() + ['help']), + help=_.OPT_TYPE_AT_CONFIG) transcoding.add_option( '-q', '--quality', action='store', type='string', dest='quality', - help=_(u'the default quality level for a given audio type')) + help=_.OPT_QUALITY_AT_CONFIG) + + transcoding.add_option( + '--format', + action='store', + metavar='FORMAT', + dest='filename_format', + help=_.OPT_FORMAT) transcoding.add_option( '-j', '--joint', @@ -252,125 +944,130 @@ type='int', metavar='MAX_PROCESSES', dest='system_maximum_jobs', - help=_(u'the maximum number of processes to run at a time')) + help=_.OPT_JOINT) + + transcoding.add_option( + '--replay-gain', + action='store', + type='choice', + choices=('yes', 'no'), + dest='add_replaygain', + metavar='yes / no', + help=_.OPT_REPLAY_GAIN) parser.add_option_group(transcoding) - thumbnails = audiotools.OptionGroup(parser, _(u"Thumbnail Options")) + id3 = audiotools.OptionGroup(parser, _.OPT_CAT_ID3) - thumbnails.add_option( - '--thumbnail-format', + id3.add_option( + '--id3v2-version', action='store', - metavar='TYPE', - dest='thumbnail_format', - help=_(u'the image format to use for thumbnails ' + - u'such as "jpeg" or "png"')) + type='choice', + choices=('id3v2.2', 'id3v2.3', 'id3v2.4', 'none'), + dest='id3v2_version', + metavar='VERSION', + help=_.OPT_AT_CONFIG_ID3V2_VERSION) - thumbnails.add_option( - '--thumbnail-size', + id3.add_option( + '--id3v2-pad', action='store', - type='int', - metavar='INT', - dest='thumbnail_size', - help=_(u'the maximum size of each thumbnail, in pixels squared')) + type='choice', + choices=('yes', 'no'), + dest='id3_digit_padding', + metavar='yes / no', + help=_.OPT_AT_CONFIG_ID3V2_PAD) + + id3.add_option( + '--id3v1-version', + action='store', + type='choice', + choices=('id3v1.1', 'none'), + dest='id3v1_version', + metavar='VERSION', + help=_.OPT_AT_CONFIG_ID3V1_VERSION) - parser.add_option_group(thumbnails) + parser.add_option_group(id3) - freedb = audiotools.OptionGroup(parser, _(u"FreeDB Options")) + lookup = audiotools.OptionGroup(parser, _.OPT_CAT_CD_LOOKUP) - freedb.add_option( - '--use-freedb', + lookup.add_option( + '--use-musicbrainz', metavar='yes/no', choices=("yes", "no"), - dest='use_freedb') + dest='use_musicbrainz') - freedb.add_option( - '--freedb-server', + lookup.add_option( + '--musicbrainz-server', action='store', metavar='HOSTNAME', - dest='freedb_server', - help=_(u'the FreeDB server to use')) + dest='musicbrainz_server') - freedb.add_option( - '--freedb-port', + lookup.add_option( + '--musicbrainz-port', action='store', metavar='PORT', type='int', - dest='freedb_port', - help=_(u'the FreeDB port to use')) - - parser.add_option_group(freedb) + dest='musicbrainz_port') - musicbrainz = audiotools.OptionGroup(parser, _(u"MusicBrainz Options")) - - musicbrainz.add_option( - '--use-musicbrainz', + lookup.add_option( + '--use-freedb', metavar='yes/no', choices=("yes", "no"), - dest='use_musicbrainz') + dest='use_freedb') - musicbrainz.add_option( - '--musicbrainz-server', + lookup.add_option( + '--freedb-server', action='store', metavar='HOSTNAME', - dest='musicbrainz_server', - help=_(u'the MusicBrainz server to use')) + dest='freedb_server') - musicbrainz.add_option( - '--musicbrainz-port', + lookup.add_option( + '--freedb-port', action='store', metavar='PORT', type='int', - dest='musicbrainz_port', - help=_(u'the MusicBrainz port to use')) + dest='freedb_port') - parser.add_option_group(musicbrainz) + parser.add_option_group(lookup) - id3 = audiotools.OptionGroup(parser, _("ID3 Options")) + system = audiotools.OptionGroup(parser, _.OPT_CAT_SYSTEM) - id3.add_option( - '--id3v2-version', - action='store', - type='choice', - choices=('id3v2.2', 'id3v2.3', 'id3v2.4', 'none'), - dest='id3v2_version', - metavar='VERSION', - help=_('which ID3v2 version to use by default, if any')) - - id3.add_option( - '--id3v1-version', - action='store', - type='choice', - choices=('id3v1.1', 'none'), - dest='id3v1_version', - metavar='VERSION', - help=_('which ID3v1 version to use by default, if any')) + system.add_option('-c', '--cdrom', + action='store', + dest='system_cdrom', + metavar='PATH') - id3.add_option( - '--id3v2-pad', - action='store', - type='choice', - choices=('true', 'false'), - dest='id3_digit_padding', - metavar='true / false', - help=_(u'whether or not to pad ID3v2 digit fields to 2 digits')) + system.add_option('--cdrom-read-offset', + action='store', + type='int', + metavar='INT', + dest='system_cdrom_read_offset', + help=_.OPT_AT_CONFIG_READ_OFFSET) - parser.add_option_group(id3) + system.add_option('--cdrom-write-offset', + action='store', + type='int', + metavar='INT', + dest='system_cdrom_write_offset', + help=_.OPT_AT_CONFIG_WRITE_OFFSET) - replaygain = audiotools.OptionGroup(parser, _("ReplayGain Options")) + system.add_option( + '--fs-encoding', + action='store', + metavar='ENCODING', + dest='system_fs_encoding', + help=_.OPT_AT_CONFIG_FS_ENCODING) - replaygain.add_option( - '--replay-gain', + system.add_option( + '--io-encoding', action='store', - type='choice', - choices=('yes', 'no'), - dest='add_replaygain', - metavar='yes / no', - help=_(u"whether to add ReplayGain metadata by default")) + metavar='ENCODING', + dest='system_io_encoding', + help=_.OPT_AT_CONFIG_IO_ENCODING) - parser.add_option_group(replaygain) + parser.add_option_group(system) - binaries = audiotools.OptionGroup(parser, _(u"Binaries Options")) + binaries = audiotools.OptionGroup(parser, _.OPT_CAT_BINARIES) bins = set([]) for audioclass in audiotools.AVAILABLE_TYPES: @@ -382,16 +1079,15 @@ action='store', metavar='PATH', dest='binary_' + binary) - opt_map["binary_" + binary] = ("Binaries", binary) parser.add_option_group(binaries) parser.add_option( - '--format', - action='store', - metavar='FORMAT', - dest='filename_format', - help=_(u'the format string for new filenames')) + '-I', '--interactive', + action='store_true', + default=False, + dest='interactive', + help=_.OPT_INTERACTIVE_AT_CONFIG) parser.add_option( '-V', '--verbose', @@ -399,255 +1095,110 @@ dest='verbosity', choices=audiotools.VERBOSITY_LEVELS, default=audiotools.DEFAULT_VERBOSITY, - help=_(u'the new default verbosity level')) + help=_.OPT_VERBOSE_AT_CONFIG) (options, args) = parser.parse_args() msg = audiotools.Messenger("audiotools-config", None) if (len(sys.argv) < 2): - FormatSummary.headers(msg) - for audiotype in sorted(audiotools.AVAILABLE_TYPES, - lambda x, y: cmp(x.NAME, y.NAME)): - FormatSummary.from_audiofile( - msg, audiotype, audiotools.BIN).output_rows(msg) - msg.output_rows() - - msg.new_row() - msg.output_column(_(u"CD Burning via track2cd"), True) - msg.output_column(u" : ") - burning_options = [] - if (audiotools.BIN.can_execute(audiotools.BIN["cdrecord"])): - burning_options.append(_(u"without cue")) - if (audiotools.BIN.can_execute(audiotools.BIN["cdrdao"])): - burning_options.append(_(u"with cue")) - if (len(burning_options) == 0): - burning_options.append(_(u"no")) - msg.output_column(u", ".join([s.decode('ascii') - for s in burning_options])) - msg.output_rows() - check_binaries(msg, ["cdrecord", "cdrdao"]) - - msg.output(u"-" * 30) - - msg.output(_(u"System configuration:")) - msg.output(u"") + #no arguments at all so display current default - msg.new_row() - msg.output_column(u"Use MusicBrainz service", True) - msg.output_column(u" : ") - msg.output_column({True: u"yes", - False: u"no"}[audiotools.MUSICBRAINZ_SERVICE]) + display_defaults(audiotools.config, msg) + elif (options.interactive): + #update options interactively - msg.new_row() - msg.output_column(_(u"Default MusicBrainz server"), True) - msg.output_column(u" : ") - msg.output_column(audiotools.MUSICBRAINZ_SERVER.decode('ascii', - 'replace')) - msg.new_row() - msg.output_column(_(u"Default MusicBrainz port"), True) - msg.output_column(u" : ") - msg.output_column(unicode(audiotools.MUSICBRAINZ_PORT)) - msg.blank_row() - - msg.new_row() - msg.output_column(u"Use FreeDB service", True) - msg.output_column(u" : ") - msg.output_column({True: u"yes", - False: u"no"}[audiotools.FREEDB_SERVICE]) - - msg.new_row() - msg.output_column(_(u"Default FreeDB server"), True) - msg.output_column(u" : ") - msg.output_column(audiotools.FREEDB_SERVER.decode('ascii', - 'replace')) - msg.new_row() - msg.output_column(_(u"Default FreeDB port"), True) - msg.output_column(u" : ") - msg.output_column(unicode(audiotools.FREEDB_PORT)) - - msg.blank_row() - - msg.new_row() - msg.output_column(_(u"Default CD-ROM device"), True) - msg.output_column(u" : ") - msg.output_column(audiotools.DEFAULT_CDROM.decode( - audiotools.FS_ENCODING)) - - msg.new_row() - msg.output_column(_(u"CD-ROM sample read offset"), True) - msg.output_column(u" : ") - msg.output_column( - unicode(audiotools.config.getint_default("System", - "cdrom_read_offset", 0))) - - msg.new_row() - msg.output_column(_(u"CD-ROM sample write offset"), True) - msg.output_column(u" : ") - msg.output_column( - unicode(audiotools.config.getint_default("System", - "cdrom_write_offset", 0))) - - msg.blank_row() - - msg.new_row() - msg.output_column(_(u"Default simultaneous jobs"), True) - msg.output_column(u" : ") - msg.output_column(unicode(audiotools.MAX_JOBS)) - - msg.new_row() - msg.output_column(_(u"Default verbosity level"), True) - msg.output_column(u" : ") - msg.output_column(audiotools.DEFAULT_VERBOSITY.decode('ascii')) - - output_options = [] - for player in audiotools.player.AUDIO_OUTPUT: - if (player.available()): - if (len(output_options) == 0): - output_options.append(msg.ansi(player.NAME.decode('ascii'), - [msg.BOLD, - msg.UNDERLINE])) - else: - output_options.append(player.NAME.decode('ascii')) - - msg.new_row() - msg.output_column(_(u"Audio output"), True) - msg.output_column(u" : ") - msg.output_column(u", ".join(output_options)) - - msg.new_row() - msg.output_column(_(u"Filesystem text encoding"), True) - msg.output_column(u" : ") - msg.output_column(audiotools.FS_ENCODING.decode('ascii')) - - msg.new_row() - msg.output_column(_(u"TTY text encoding"), True) - msg.output_column(u" : ") - msg.output_column(audiotools.IO_ENCODING.decode('ascii')) - - msg.blank_row() - - msg.new_row() - msg.output_column(_(u"ID3v2 tag version"), True) - msg.output_column(u" : ") - msg.output_column({"id3v2.2": u"ID3v2.2", - "id3v2.3": u"ID3v2.3", - "id3v2.4": u"ID3v2.4", - "none": _(u"no ID3v2 tags")}.get( - audiotools.config.get_default("ID3", "id3v2", "id3v2.3"), - u"ID3v2.3")) - msg.new_row() - msg.output_column(_(u"ID3v2 digit padding"), True) - msg.output_column(u" : ") - msg.output_column({True: _(u'padded to 2 digits ("01", "02", etc.)'), - False: _(u'no padding ("1", "2", etc.)')}[ - audiotools.config.getboolean_default("ID3", "pad", False)]) - msg.new_row() - msg.output_column(_(u"ID3v1 tag version"), True) - msg.output_column(u" : ") - msg.output_column({"id3v1.1": u"ID3v1.1", - "none": _(u"no ID3v1 tags")}.get( - audiotools.config.get_default("ID3", "id3v1", "id3v1.1"), - u"ID3v1.1")) - msg.blank_row() - - msg.new_row() - msg.output_column(_(u"Can create thumbnails"), True) - msg.output_column(u" : ") - if (audiotools.can_thumbnail()): - msg.output_column(_(u"yes")) + if (not audiotools.ui.AVAILABLE): + audiotools.ui.not_available_message(msg) + sys.exit(1) else: - msg.output_column(_(u"no") + u" " + - _(u"(Python Imaging Library not found)")) - - msg.new_row() - msg.output_column(_(u"Default thumbnail format"), True) - msg.output_column(u" : ") - msg.output_column(audiotools.THUMBNAIL_FORMAT.decode('ascii')) - - msg.new_row() - msg.output_column(_(u"Default thumbnail size"), True) - msg.output_column(u" : ") - msg.output_column(u"%d\u00D7%d" % (audiotools.THUMBNAIL_SIZE, - audiotools.THUMBNAIL_SIZE)) - - msg.blank_row() - - msg.new_row() - msg.output_column(_(u"Add ReplayGain by default"), True) - msg.output_column(u" : ") - msg.output_column({True: u"yes", - False: u"no"}[audiotools.ADD_REPLAYGAIN]) - - msg.blank_row() - msg.output_rows() - - msg.output(_(u"File name format : %s") % \ - (audiotools.FILENAME_FORMAT.decode('ascii', 'replace'))) - else: - import os.path - - config = audiotools.config - for attr in dir(options): - if ((not attr.startswith('_')) and - (not callable(getattr(options, attr))) and - (getattr(options, attr) is not None)): - if (attr in opt_map): - (section, option) = opt_map[attr] - if (not config.has_section(section)): - config.add_section(section) - config.set(section, option, getattr(options, attr)) - - if (options.type is not None): - AudioType = audiotools.TYPE_MAP[options.type] - - if (options.quality is not None): - if (options.quality == 'help'): - if (len(AudioType.COMPRESSION_MODES) > 1): - msg.info(_(u"Available compression types for %s:") % - (AudioType.NAME)) - for mode in AudioType.COMPRESSION_MODES: - msg.new_row() - if (mode == audiotools.__default_quality__( - AudioType.NAME)): - msg.output_column( - msg.ansi(mode.decode('ascii'), - [msg.BOLD, msg.UNDERLINE]), True) - else: - msg.output_column(mode.decode('ascii'), True) - if (mode in AudioType.COMPRESSION_DESCRIPTIONS): - msg.output_column(u" : ") - else: - msg.output_column(u" ") - msg.output_column( - AudioType.COMPRESSION_DESCRIPTIONS.get(mode, - u"")) - msg.info_rows() - else: - msg.error( - _(u"Audio type %s has no compression modes") % - (AudioType.NAME)) - sys.exit(0) - elif (options.quality not in AudioType.COMPRESSION_MODES): - msg.error( - _(u"\"%(quality)s\" is not a supported " + - u"compression mode for type \"%(type)s\"") % - {"quality": options.quality, - "type": AudioType.NAME}) + #apply options to config file + apply_options(options, audiotools.config) + + #run interactive widget here + widget = AudiotoolsConfig(audiotools.config) + loop = audiotools.ui.urwid.MainLoop( + widget, + audiotools.ui.style(), + unhandled_input=widget.handle_text, + pop_ups=True) + try: + loop.run() + msg.ansi_clearscreen() + except (termios.error, IOError): + msg.error(_.ERR_TERMIOS_ERROR) + msg.info(_.ERR_TERMIOS_SUGGESTION) + msg.info(audiotools.ui.xargs_suggestion(sys.argv)) + sys.exit(1) + + #and apply options if widget isn't cancelled + if (not widget.cancelled()): + configpath = os.path.expanduser('~/.audiotools.cfg') + try: + configfile = open(configpath, 'w') + audiotools.config.write(configfile) + configfile.close() + msg.info(_.LAB_AT_CONFIG_FILE_WRITTEN % + (audiotools.Filename(configpath),)) + except IOError, err: + msg.error(_.ERR_OPEN_IOERROR % + (audiotools.Filename(configpath),)) sys.exit(1) - - #we're setting the quality level for a given type - if (not config.has_section("Quality")): - config.add_section("Quality") - config.set("Quality", options.type, options.quality) else: - #we're setting the default type - if (not config.has_section("System")): - config.add_section("System") - config.set("System", "default_type", options.type) + sys.exit(0) + else: + #update options non-interactively + + #verify --format is valid, if present + if (options.filename_format is not None): + try: + audiotools.AudioFile.track_name( + file_path="", + track_metadata=audiotools.MetaData(), + format=options.filename_format) + except audiotools.UnsupportedTracknameField, err: + err.error_msg(msg) + sys.exit(1) + except audiotools.InvalidFilenameFormat, err: + msg.error(unicode(err)) + sys.exit(1) + + #verify --type is valid, if present + if (options.type == 'help'): + audiotools.ui.show_available_formats(msg) + sys.exit(0) + elif (options.type is not None): + AudioType = audiotools.TYPE_MAP[options.type] + else: + AudioType = audiotools.TYPE_MAP[audiotools.DEFAULT_TYPE] + + #verify --quality is valid for type, if present + if (options.quality == 'help'): + audiotools.ui.show_available_qualities(msg, AudioType) + sys.exit(0) + elif ((options.quality is not None) and + (options.quality not in AudioType.COMPRESSION_MODES)): + msg.error(_.ERR_UNSUPPORTED_COMPRESSION_MODE % + {"quality": options.quality, + "type": AudioType.NAME}) + sys.exit(1) + + #verify --joint is positive, if present + if (((options.system_maximum_jobs is not None) and + (options.system_maximum_jobs < 1))): + msg.error(_.ERR_INVALID_JOINT) + sys.exit(1) + + #apply options non-interactively + apply_options(options, audiotools.config) configpath = os.path.expanduser('~/.audiotools.cfg') - configfile = open(configpath, 'w') - config.write(configfile) - configfile.close() - msg.info(_(u'* "%s" written') % (msg.filename(configpath))) + try: + configfile = open(configpath, 'w') + audiotools.config.write(configfile) + configfile.close() + msg.info(_.LAB_AT_CONFIG_FILE_WRITTEN % + (audiotools.Filename(configpath),)) + except IOError, err: + msg.error(_.ERR_OPEN_IOERROR % (audiotools.Filename(configpath),)) + sys.exit(1)
View file
audiotools-2.18.tar.gz/audiotools/__init__.py -> audiotools-2.19.tar.gz/audiotools/__init__.py
Changed
@@ -27,20 +27,11 @@ from . import pcm as pcm -import subprocess import re -import cStringIO import os import os.path import ConfigParser import optparse -import struct -from itertools import izip -import gettext -import unicodedata -import cPickle - -gettext.install("audiotools", unicode=True) class RawConfigParser(ConfigParser.RawConfigParser): @@ -100,6 +91,7 @@ os.path.expanduser('~/.audiotools.cfg')]) BUFFER_SIZE = 0x100000 +FRAMELIST_SIZE = 0x100000 / 4 class __system_binaries__: @@ -139,14 +131,11 @@ ADD_REPLAYGAIN = config.getboolean_default("ReplayGain", "add_by_default", True) -THUMBNAIL_FORMAT = config.get_default("Thumbnail", "format", "jpeg") -THUMBNAIL_SIZE = config.getint_default("Thumbnail", "size", 150) - -VERSION = "2.18" +VERSION = "2.19" -FILENAME_FORMAT = config.get_default( - "Filenames", "format", - '%(track_number)2.2d - %(track_name)s.%(suffix)s') +DEFAULT_FILENAME_FORMAT = '%(track_number)2.2d - %(track_name)s.%(suffix)s' +FILENAME_FORMAT = config.get_default("Filenames", "format", + DEFAULT_FILENAME_FORMAT) FS_ENCODING = config.get_default("System", "fs_encoding", sys.getfilesystemencoding()) @@ -163,6 +152,96 @@ DEFAULT_TYPE = config.get_default("System", "default_type", "wav") +#field name -> (field string, text description) mapping +def __format_fields__(): + from .text import (METADATA_TRACK_NAME, + METADATA_TRACK_NUMBER, + METADATA_TRACK_TOTAL, + METADATA_ALBUM_NAME, + METADATA_ARTIST_NAME, + METADATA_PERFORMER_NAME, + METADATA_COMPOSER_NAME, + METADATA_CONDUCTOR_NAME, + METADATA_MEDIA, + METADATA_ISRC, + METADATA_CATALOG, + METADATA_COPYRIGHT, + METADATA_PUBLISHER, + METADATA_YEAR, + METADATA_DATE, + METADATA_ALBUM_NUMBER, + METADATA_ALBUM_TOTAL, + METADATA_COMMENT, + METADATA_SUFFIX, + METADATA_ALBUM_TRACK_NUMBER, + METADATA_BASENAME) + return {u"track_name": (u"%(track_name)s", + METADATA_TRACK_NAME), + u"track_number": (u"%(track_number)2.2d", + METADATA_TRACK_NUMBER), + u"track_total": (u"%(track_total)d", + METADATA_TRACK_TOTAL), + u"album_name": (u"%(album_name)s", + METADATA_ALBUM_NAME), + u"artist_name": (u"%(artist_name)s", + METADATA_ARTIST_NAME), + u"performer_name": (u"%(performer_name)s", + METADATA_PERFORMER_NAME), + u"composer_name": (u"%(composer_name)s", + METADATA_COMPOSER_NAME), + u"conductor_name": (u"%(conductor_name)s", + METADATA_CONDUCTOR_NAME), + u"media": (u"%(media)s", + METADATA_MEDIA), + u"ISRC": (u"%(ISRC)s", + METADATA_ISRC), + u"catalog": (u"%(catalog)s", + METADATA_CATALOG), + u"copyright": (u"%(copyright)s", + METADATA_COPYRIGHT), + u"publisher": (u"%(publisher)s", + METADATA_PUBLISHER), + u"year": (u"%(year)s", + METADATA_YEAR), + u"date": (u"%(date)s", + METADATA_DATE), + u"album_number": (u"%(album_number)d", + METADATA_ALBUM_NUMBER), + u"album_total": (u"%(album_total)d", + METADATA_ALBUM_TOTAL), + u"comment": (u"%(comment)s", + METADATA_COMMENT), + u"suffix": (u"%(suffix)s", + METADATA_SUFFIX), + u"album_track_number": (u"%(album_track_number)s", + METADATA_ALBUM_TRACK_NUMBER), + u"basename": (u"%(basename)s", + METADATA_BASENAME)} + +FORMAT_FIELDS = __format_fields__() +FORMAT_FIELD_ORDER = (u"track_name", + u"artist_name", + u"album_name", + u"track_number", + u"track_total", + u"album_number", + u"album_total", + u"performer_name", + u"composer_name", + u"conductor_name", + u"catalog", + U"ISRC", + u"publisher", + u"media", + u"year", + u"date", + u"copyright", + u"comment", + u"suffix", + u"album_track_number", + u"basename") + + def __default_quality__(audio_type): quality = DEFAULT_QUALITY.get(audio_type, "") try: @@ -180,7 +259,7 @@ try: import multiprocessing MAX_JOBS = multiprocessing.cpucount() - except (ImportError,AttributeError): + except (ImportError, AttributeError): MAX_JOBS = 1 @@ -236,6 +315,20 @@ "H": 1} +def khz(hz): + """given an integer sample rate value in Hz, + returns a unicode kHz value with suffix + + the string is typically 7-8 characters wide""" + + num = hz / 1000 + den = (hz % 1000) / 100 + if (den == 0): + return u"%dkHz" % (num) + else: + return u"%d.%dkHz" % (num, den) + + def str_width(s): """returns the width of unicode string s, in characters @@ -243,6 +336,8 @@ as well as embedded ANSI sequences """ + import unicodedata + return sum( [__CHAR_WIDTHS__.get(unicodedata.east_asian_width(char), 1) for char in unicodedata.normalize('NFC', __ANSI_SEQUENCE__.sub(u"", s))]) @@ -256,6 +351,8 @@ """ def __init__(self, unicode_string): + import unicodedata + self.__string__ = unicodedata.normalize( 'NFC', __ANSI_SEQUENCE__.sub(u"", unicode(unicode_string))) @@ -574,10 +671,10 @@ this appends a newline to that message""" - self.error(u"[Errno %d] %s: '%s'" % \ - (oserror.errno, - oserror.strerror.decode('utf-8', 'replace'), - self.filename(oserror.filename))) + self.error(u"[Errno %d] %s: '%s'" % + (oserror.errno, + oserror.strerror.decode('utf-8', 'replace'), + Filename(oserror.filename))) def warning(self, s): """displays a warning message unicode string to stderr @@ -599,13 +696,6 @@ sys.stderr.write(s.encode(IO_ENCODING, 'replace')) sys.stderr.write(os.linesep) - def filename(self, s): - """decodes a filename string to unicode - - this uses the system's encoding to perform translation""" - - return s.decode(FS_ENCODING, 'replace') - def ansi(self, s, codes): """generates an ANSI code as a unicode string @@ -641,11 +731,9 @@ """ if (sys.stdout.isatty()): - sys.stdout.write(( - # move cursor to column 0 - u"\u001B[0G" + - # clear everything after cursor - u"\u001B[0K").encode(IO_ENCODING)) + sys.stdout.write((u"\u001B[0G" + # move cursor to column 0 + # clear everything after cursor + u"\u001B[0K").encode(IO_ENCODING)) sys.stdout.flush() def ansi_uplines(self, lines): @@ -662,6 +750,14 @@ sys.stdout.write(u"\u001B[0J") sys.stdout.flush() + def ansi_clearscreen(self): + """clears the entire screen and moves cursor to upper left corner""" + + if (sys.stdout.isatty()): + sys.stdout.write(u"\u001B[2J") + sys.stdout.write(u"\u001B[1;1H") + sys.stdout.flush() + def ansi_err(self, s, codes): """generates an ANSI code as a unicode string @@ -730,6 +826,11 @@ pass + def ansi_clearscreen(self): + """clears the entire screen and moves cursor to upper left corner""" + + pass + class ProgressDisplay: """a class for displaying incremental progress updates to the screen""" @@ -810,10 +911,11 @@ depending on whether output has changed since previously displayed""" - screen_width = self.messenger.terminal_size(sys.stdout)[1] + (screen_height, + screen_width) = self.messenger.terminal_size(sys.stdout) new_output = [progress_row.unicode(screen_width) for progress_row in self.progress_rows - if progress_row is not None] + if progress_row is not None][0:screen_height - 1] for output in new_output: self.messenger.output(output) self.previous_output = new_output @@ -866,6 +968,7 @@ self.refresh() self.last_updated = now + class ReplayGainProgressDisplay(ProgressDisplay): """a specialized ProgressDisplay for handling ReplayGain application""" @@ -875,15 +978,17 @@ ProgressDisplay.__init__(self, messenger) from time import time + from .text import (RG_ADDING_REPLAYGAIN, + RG_APPLYING_REPLAYGAIN) self.time = time self.last_updated = 0 self.lossless_replay_gain = lossless_replay_gain if (lossless_replay_gain): - self.add_row(0, _(u"Adding ReplayGain")) + self.add_row(0, RG_ADDING_REPLAYGAIN) else: - self.add_row(0, _(u"Applying ReplayGain")) + self.add_row(0, RG_APPLYING_REPLAYGAIN) if (sys.stdout.isatty()): self.initial_message = self.initial_message_tty @@ -903,12 +1008,13 @@ def initial_message_nontty(self): """displays a message that ReplayGain application has started""" + from .text import (RG_ADDING_REPLAYGAIN_WAIT, + RG_APPLYING_REPLAYGAIN_WAIT) + if (self.lossless_replay_gain): - self.messenger.info( - _(u"Adding ReplayGain metadata. This may take some time.")) + self.messenger.info(RG_ADDING_REPLAYGAIN_WAIT) else: - self.messenger.info( - _(u"Applying ReplayGain. This may take some time.")) + self.messenger.info(RG_APPLYING_REPLAYGAIN_WAIT) def update_tty(self, current, total): """updates the current status of ReplayGain application""" @@ -928,11 +1034,14 @@ def final_message_tty(self): """displays a message that ReplayGain application is complete""" + from .text import (RG_REPLAYGAIN_ADDED, + RG_REPLAYGAIN_APPLIED) + self.clear() if (self.lossless_replay_gain): - self.messenger.info(_(u"ReplayGain added")) + self.messenger.info(RG_REPLAYGAIN_ADDED) else: - self.messenger.info(_(u"ReplayGain applied")) + self.messenger.info(RG_REPLAYGAIN_APPLIED) def final_message_nontty(self): """displays a message that ReplayGain application is complete""" @@ -1011,13 +1120,6 @@ pass -class InvalidFormat(Exception): - """raised if an audio file cannot be created correctly from from_pcm() - due to having a PCM format unsupported by the output format""" - - pass - - class EncodingError(IOError): """raised if an audio file cannot be created correctly from from_pcm() due to an error by the encoder""" @@ -1043,23 +1145,25 @@ """raised if the encoder does not support the file's channel mask""" def __init__(self, filename, mask): + from .text import ERR_UNSUPPORTED_CHANNEL_MASK + EncodingError.__init__( self, - _(u"Unable to write \"%(target_filename)s\"" + - u" with channel assignment \"%(assignment)s\"") % - {"target_filename": VerboseMessenger(None).filename(filename), - "assignment": audiotools.ChannelMask(mask)}) + ERR_UNSUPPORTED_CHANNEL_MASK % + {"target_filename": Filename(filename), + "assignment": ChannelMask(mask)}) class UnsupportedChannelCount(EncodingError): """raised if the encoder does not support the file's channel count""" def __init__(self, filename, count): + from .text import ERR_UNSUPPORTED_CHANNEL_COUNT + EncodingError.__init__( self, - _(u"Unable to write \"%(target_filename)s\"" + - u" with %(channels)d channel input") % - {"target_filename": VerboseMessenger(None).filename(filename), + ERR_UNSUPPORTED_CHANNEL_COUNT % + {"target_filename": Filename(filename), "channels": count}) @@ -1067,11 +1171,12 @@ """raised if the encoder does not support the file's bits-per-sample""" def __init__(self, filename, bits_per_sample): + from .text import ERR_UNSUPPORTED_BITS_PER_SAMPLE + EncodingError.__init__( self, - _(u"Unable to write \"%(target_filename)s\"" + - u" with %(bps)d bits per sample") % - {"target_filename": VerboseMessenger(None).filename(filename), + ERR_UNSUPPORTED_BITS_PER_SAMPLE % + {"target_filename": Filename(filename), "bps": bits_per_sample}) @@ -1086,6 +1191,188 @@ self.error_message = error_message +def file_type(file): + """given a seekable file stream rewound to the file's start + returns an AudioFile-compatible class that stream is a type of + or None of the stream's type is unknown + + the AudioFile class is not guaranteed to be available""" + + header = file.read(37) + if ((header[4:8] == "ftyp") and (header[8:12] in ('mp41', + 'mp42', + 'M4A ', + 'M4B '))): + + #possibly ALAC or M4A + + from .bitstream import BitstreamReader + from .m4a import get_m4a_atom + + file.seek(0, 0) + reader = BitstreamReader(file, 0) + + #so get contents of moov->trak->mdia->minf->stbl->stsd atom + try: + stsd = get_m4a_atom(reader, + "moov", "trak", "mdia", + "minf", "stbl", "stsd")[1] + (stsd_version, descriptions, + atom_size, atom_type) = stsd.parse("8u 24p 32u 32u 4b") + + if (atom_type == "alac"): + #if first description is "alac" atom, it's an ALAC + return ALACAudio + elif (atom_type == "mp4a"): + #if first description is "mp4a" atom, it's M4A + return M4AAudio + else: + #otherwise, it's unknown + return None + except KeyError: + #no stsd atom, so unknown + return None + except IOError: + #error reading atom, so unknown + return None + elif ((header[0:4] == "FORM") and (header[8:12] == "AIFF")): + return AiffAudio + elif (header[0:4] == ".snd"): + return AuAudio + elif (header[0:4] == "fLaC"): + return FlacAudio + elif ((len(header) >= 4) and (header[0] == "\xFF")): + #possibly MP3 or MP2 + + from .bitstream import BitstreamReader + from cStringIO import StringIO + + #header is at least 32 bits, so no IOError is possible + (frame_sync, + mpeg_id, + layer_description, + protection, + bitrate, + sample_rate, + pad, + private, + channels, + mode_extension, + copy, + original, + emphasis) = BitstreamReader( + StringIO(header), 0).parse("11u 2u 2u 1u 4u 2u 1u " + + "1u 2u 2u 1u 1u 2u") + if (((frame_sync == 0x7FF) and + (mpeg_id == 3) and + (layer_description == 1) and + (bitrate != 0xF) and + (sample_rate != 3) and + (emphasis != 2))): + #MP3s are MPEG-1, Layer-III + return MP3Audio + elif ((frame_sync == 0x7FF) and + (mpeg_id == 3) and + (layer_description == 2) and + (bitrate != 0xF) and + (sample_rate != 3) and + (emphasis != 2)): + #MP2s are MPEG-1, Layer-II + return MP2Audio + else: + #nothing else starts with an initial byte of 0xFF + #so the file is unknown + return None + elif (header[0:4] == "OggS"): + #possibly Ogg FLAC, Ogg Vorbis or Ogg Opus + if (header[0x1C:0x21] == "\x7FFLAC"): + return OggFlacAudio + elif (header[0x1C:0x23] == "\x01vorbis"): + return VorbisAudio + elif (header[0x1C:0x26] == "OpusHead\x01"): + return OpusAudio + else: + return None + elif (header[0:5] == "ajkg\x02"): + return ShortenAudio + elif (header[0:4] == "wvpk"): + return WavPackAudio + elif ((header[0:4] == "RIFF") and (header[8:12] == "WAVE")): + return WaveAudio + elif ((len(header) >= 10) and + (header[0:3] == "ID3") and + (ord(header[3]) in (2, 3, 4))): + #file contains ID3v2 tag + #so it may be MP3, MP2 or FLAC + + #determine sync-safe tag size and skip entire tag + tag_size = 0 + for b in header[6:10]: + tag_size = (tag_size << 7) | (ord(b) % 0x7F) + file.seek(10 + tag_size, 0) + b = file.read(1) + while (b == "\x00"): + #skip NULL bytes after ID3v2 tag + b = file.read(1) + + if (b == ""): + #no data after tag, so file is unknown + return None + elif (b == "\xFF"): + #possibly MP3 or MP2 file + + from .bitstream import BitstreamReader + + try: + (frame_sync, + mpeg_id, + layer_description, + protection, + bitrate, + sample_rate, + pad, + private, + channels, + mode_extension, + copy, + original, + emphasis) = BitstreamReader(file, 0).parse( + "3u 2u 2u 1u 4u 2u 1u 1u 2u 2u 1u 1u 2u") + if (((frame_sync == 0x7) and + (mpeg_id == 3) and + (layer_description == 1) and + (bitrate != 0xF) and + (sample_rate != 3) and + (emphasis != 2))): + #MP3s are MPEG-1, Layer-III + return MP3Audio + elif ((frame_sync == 0x7) and + (mpeg_id == 3) and + (layer_description == 2) and + (bitrate != 0xF) and + (sample_rate != 3) and + (emphasis != 2)): + #MP2s are MPEG-1, Layer-II + return MP2Audio + else: + #nothing else starts with an initial byte of 0xFF + #so the file is unknown + return None + except IOError: + return None + elif (b == "f"): + #possibly FLAC file + if (file.read(3) == "LaC"): + return FlacAudio + else: + return None + else: + #unknown file after ID3 tag + return None + else: + return None + + def open(filename): """returns an AudioFile located at the given filename path @@ -1097,46 +1384,178 @@ raises IOError if some problem occurs attempting to open the file """ - available_types = frozenset(TYPE_MAP.values()) - f = file(filename, "rb") try: - for audioclass in TYPE_MAP.values(): - f.seek(0, 0) - if (audioclass.is_type(f)): - return audioclass(filename) + audio_class = file_type(f) + if ((audio_class is not None) and audio_class.has_binaries(BIN)): + return audio_class(filename) else: raise UnsupportedFile(filename) - finally: f.close() +class DuplicateFile(Exception): + """raised if the same file is included more than once""" + + def __init__(self, filename): + """filename is a Filename object""" + + self.filename = filename + + def __unicode__(self): + from .text import ERR_DUPLICATE_FILE + + return ERR_DUPLICATE_FILE % (self.filename,) + + +class DuplicateOutputFile(Exception): + """raised if the same output file is generated more than once""" + + def __init__(self, filename): + """filename is a Filename object""" + + self.filename = filename + + def __unicode__(self): + from .text import ERR_DUPLICATE_OUTPUT_FILE + + return ERR_DUPLICATE_OUTPUT_FILE % (self.filename,) + + +class OutputFileIsInput(Exception): + """raised if an output file is the same as an input file""" + + def __init__(self, filename): + """filename is a Filename object""" + + self.filename = filename + + def __unicode__(self): + from .text import ERR_OUTPUT_IS_INPUT + + return ERR_OUTPUT_IS_INPUT % (self.filename,) + + +class Filename(tuple): + def __new__(cls, filename): + """filename is a string of the file on disk""" + + filename = str(filename) + try: + stat = os.stat(filename) + return tuple.__new__(cls, [os.path.normpath(filename), + stat.st_dev, + stat.st_ino]) + except OSError: + return tuple.__new__(cls, [os.path.normpath(filename), + None, + None]) + + def disk_file(self): + """returns True if the file exists on disk""" + + return (self[1] is not None) and (self[2] is not None) + + def basename(self): + """returns the basename (no directory) of this file""" + + return Filename(os.path.basename(self[0])) + + def expanduser(self): + """returns a Filename object with user directory expanded""" + + return Filename(os.path.expanduser(self[0])) + + def __repr__(self): + return "Filename(%s, %s, %s)" % \ + (repr(self[0]), repr(self[1]), repr(self[2])) + + def __eq__(self, filename): + if (isinstance(filename, Filename)): + if (self.disk_file() and filename.disk_file()): + #both exist on disk, + #so they compare equally if st_dev and st_ino match + return (self[1] == filename[1]) and (self[2] == filename[2]) + elif ((not self.disk_file()) and (not filename.disk_file())): + #neither exist on disk, + #so they compare equally if their paths match + return self[0] == filename[0] + else: + #one or the other exists on disk + #but not both, so they never match + return False + else: + return False + + def __ne__(self, filename): + return not self == filename + + def __hash__(self): + if (self.disk_file()): + return hash((None, self[1], self[2])) + else: + return hash((self[0], self[1], self[2])) + + def __str__(self): + return self[0] + + def __unicode__(self): + return self[0].decode(FS_ENCODING, "replace") + + #takes a list of filenames #returns a list of AudioFile objects, sorted by track_number() #any unsupported files are filtered out -def open_files(filename_list, sorted=True, messenger=None): - """returns a list of AudioFile objects from a list of filenames +def open_files(filename_list, sorted=True, messenger=None, + no_duplicates=False, warn_duplicates=False, + opened_files=None): + """returns a list of AudioFile objects + from a list of filename strings or Filename objects + + if "sorted" is True, files are sorted by album number then track number + + if "messenger" is given, warnings and errors when opening files + are sent to the given Messenger-compatible object + + if "no_duplicates" is True, including the same file twice + raises a DuplicateFile whose filename value + is the first duplicate filename as a Filename object - files are sorted by album number then track number, by default - unsupported files are filtered out - error messages are sent to messenger, if given + if "warn_duplicates" is True, including the same file twice + results in a warning message to the messenger object, if given + + "opened_files" is a set object containing previously opened + Filename objects and which newly opened Filename objects are added to """ + from .text import (ERR_DUPLICATE_FILE, + ERR_OPEN_IOERROR) + + if (opened_files is None): + opened_files = set([]) + toreturn = [] - if (messenger is None): - messenger = Messenger("audiotools", None) - for filename in filename_list: + for filename in map(Filename, filename_list): try: - toreturn.append(open(filename)) + if (filename in opened_files): + if (no_duplicates): + raise DuplicateFile(filename) + elif (warn_duplicates and (messenger is not None)): + messenger.warning(ERR_DUPLICATE_FILE % (filename,)) + else: + opened_files.add(filename) + + toreturn.append(open(str(filename))) except UnsupportedFile: pass except IOError, err: - messenger.warning( - _(u"Unable to open \"%s\"" % (messenger.filename(filename)))) + if (messenger is not None): + messenger.warning(ERR_OPEN_IOERROR % (filename,)) except InvalidFile, err: - messenger.error(unicode(err)) + if (messenger is not None): + messenger.error(unicode(err)) if (sorted): toreturn.sort(lambda x, y: cmp((x.album_number(), x.track_number()), @@ -1176,14 +1595,13 @@ collection = {} for track in tracks: metadata = track.get_metadata() - if (metadata is not None): - collection.setdefault((track.album_number(), - metadata.album_name), []).append(track) - else: - collection.setdefault((track.album_number(), - None), []).append(track) - for tracks in collection.values(): - yield tracks + collection.setdefault( + (track.album_number(), + metadata.album_name + if metadata is not None else None), []).append(track) + + for key in sorted(collection.keys()): + yield collection[key] class UnknownAudioType(Exception): @@ -1193,7 +1611,9 @@ self.suffix = suffix def error_msg(self, messenger): - messenger.error(_(u"Unsupported audio type \"%s\"") % (self.suffix)) + from .text import ERR_UNSUPPORTED_AUDIO_TYPE + + messenger.error(ERR_UNSUPPORTED_AUDIO_TYPE % (self.suffix,)) class AmbiguousAudioType(UnknownAudioType): @@ -1204,10 +1624,13 @@ self.type_list = type_list def error_msg(self, messenger): - messenger.error(_(u"Ambiguious suffix type \"%s\"") % (self.suffix)) - messenger.info((_(u"Please use the -t option to specify %s") % - (u" or ".join([u"\"%s\"" % (t.NAME.decode('ascii')) - for t in self.type_list])))) + from .text import (ERR_AMBIGUOUS_AUDIO_TYPE, + LAB_T_OPTIONS) + + messenger.error(ERR_AMBIGUOUS_AUDIO_TYPE % (self.suffix,)) + messenger.info(LAB_T_OPTIONS % + (u" or ".join([u"\"%s\"" % (t.NAME.decode('ascii')) + for t in self.type_list]))) def filename_to_type(path): @@ -1295,24 +1718,43 @@ MASK_TO_SPEAKER = dict(map(reversed, map(list, SPEAKER_TO_MASK.items()))) - MASK_TO_NAME = {0x1: _(u"front left"), - 0x2: _(u"front right"), - 0x4: _(u"front center"), - 0x8: _(u"low frequency"), - 0x10: _(u"back left"), - 0x20: _(u"back right"), - 0x40: _(u"front right of center"), - 0x80: _(u"front left of center"), - 0x100: _(u"back center"), - 0x200: _(u"side left"), - 0x400: _(u"side right"), - 0x800: _(u"top center"), - 0x1000: _(u"top front left"), - 0x2000: _(u"top front center"), - 0x4000: _(u"top front right"), - 0x8000: _(u"top back left"), - 0x10000: _(u"top back center"), - 0x20000: _(u"top back right")} + from .text import (MASK_FRONT_LEFT, + MASK_FRONT_RIGHT, + MASK_FRONT_CENTER, + MASK_LFE, + MASK_BACK_LEFT, + MASK_BACK_RIGHT, + MASK_FRONT_RIGHT_OF_CENTER, + MASK_FRONT_LEFT_OF_CENTER, + MASK_BACK_CENTER, + MASK_SIDE_LEFT, + MASK_SIDE_RIGHT, + MASK_TOP_CENTER, + MASK_TOP_FRONT_LEFT, + MASK_TOP_FRONT_CENTER, + MASK_TOP_FRONT_RIGHT, + MASK_TOP_BACK_LEFT, + MASK_TOP_BACK_CENTER, + MASK_TOP_BACK_RIGHT) + + MASK_TO_NAME = {0x1: MASK_FRONT_LEFT, + 0x2: MASK_FRONT_RIGHT, + 0x4: MASK_FRONT_CENTER, + 0x8: MASK_LFE, + 0x10: MASK_BACK_LEFT, + 0x20: MASK_BACK_RIGHT, + 0x40: MASK_FRONT_RIGHT_OF_CENTER, + 0x80: MASK_FRONT_LEFT_OF_CENTER, + 0x100: MASK_BACK_CENTER, + 0x200: MASK_SIDE_LEFT, + 0x400: MASK_SIDE_RIGHT, + 0x800: MASK_TOP_CENTER, + 0x1000: MASK_TOP_FRONT_LEFT, + 0x2000: MASK_TOP_FRONT_CENTER, + 0x4000: MASK_TOP_FRONT_RIGHT, + 0x8000: MASK_TOP_BACK_LEFT, + 0x10000: MASK_TOP_BACK_CENTER, + 0x20000: MASK_TOP_BACK_RIGHT} def __init__(self, mask): """mask should be an integer channel mask value""" @@ -1438,7 +1880,7 @@ big_endian - True if the file's samples are stored big-endian the process, signed and big_endian arguments are optional - pCMReader-compatible objects need only expose the + PCMReader-compatible objects need only expose the sample_rate, channels, channel_mask and bits_per_sample fields along with the read() and close() methods """ @@ -1452,10 +1894,10 @@ self.signed = signed self.big_endian = big_endian - def read(self, bytes): - """try to read a pcm.FrameList of size "bytes" + def read(self, pcm_frames): + """try to read the given number of PCM frames from the stream - this is *not* guaranteed to read exactly that number of bytes + this is *not* guaranteed to read exactly that number of frames it may return less (at the end of the stream, especially) it may return more however, it must always return a non-empty FrameList until the @@ -1465,17 +1907,19 @@ or ValueError if the input file has some sort of error """ - bytes -= (bytes % (self.channels * self.bits_per_sample / 8)) - return pcm.FrameList(self.file.read(max( - bytes, self.channels * self.bits_per_sample / 8)), - self.channels, - self.bits_per_sample, - self.big_endian, - self.signed) + return pcm.FrameList( + self.file.read(max(pcm_frames, 1) * + self.channels * (self.bits_per_sample / 8)), + self.channels, + self.bits_per_sample, + self.big_endian, + self.signed) def close(self): """closes the stream for reading + subsequent calls to read() raise ValueError + any subprocess is waited for also so for proper cleanup may return DecodingError if a helper subprocess exits with an error status""" @@ -1501,7 +1945,7 @@ bits_per_sample) self.error_message = error_message - def read(self, bytes): + def read(self, pcm_frames): """always returns an empty framelist""" return pcm.from_list([], @@ -1525,19 +1969,19 @@ class PCMReaderProgress: - def __init__(self, pcmreader, total_frames, progress): + def __init__(self, pcmreader, total_frames, progress, current_frames=0): self.__read__ = pcmreader.read self.__close__ = pcmreader.close self.sample_rate = pcmreader.sample_rate self.channels = pcmreader.channels self.channel_mask = pcmreader.channel_mask self.bits_per_sample = pcmreader.bits_per_sample - self.current_frames = 0 + self.current_frames = current_frames self.total_frames = total_frames self.progress = progress - def read(self, bytes): - frame = self.__read__(bytes) + def read(self, pcm_frames): + frame = self.__read__(pcm_frames) self.current_frames += frame.frames self.progress(self.current_frames, self.total_frames) return frame @@ -1549,24 +1993,38 @@ class ReorderedPCMReader: """a PCMReader wrapper which reorders its output channels""" - def __init__(self, pcmreader, channel_order): + def __init__(self, pcmreader, channel_order, channel_mask=None): """initialized with a PCMReader and list of channel number integers for example, to swap the channels of a stereo stream: >>> ReorderedPCMReader(reader,[1,0]) + + may raise ValueError if the number of channels specified by + channel_order doesn't match the given channel mask + if channel mask is nonzero """ self.pcmreader = pcmreader self.sample_rate = pcmreader.sample_rate - self.channels = pcmreader.channels - self.channel_mask = pcmreader.channel_mask + self.channels = len(channel_order) + if (channel_mask is None): + self.channel_mask = pcmreader.channel_mask + else: + self.channel_mask = channel_mask + + if (((self.channel_mask != 0) and + (len(ChannelMask(self.channel_mask)) != self.channels))): + #channel_mask is defined but has a different number of channels + #than the channel count attribute + from .text import ERR_CHANNEL_COUNT_MASK_MISMATCH + raise ValueError(ERR_CHANNEL_COUNT_MASK_MISMATCH) self.bits_per_sample = pcmreader.bits_per_sample self.channel_order = channel_order - def read(self, bytes): - """try to read a pcm.FrameList of size 'bytes'""" + def read(self, pcm_frames): + """try to read a pcm.FrameList with the given number of frames""" - framelist = self.pcmreader.read(bytes) + framelist = self.pcmreader.read(pcm_frames) return pcm.from_channels([framelist.channel(channel) for channel in self.channel_order]) @@ -1577,6 +2035,68 @@ self.pcmreader.close() +class RemaskedPCMReader: + """a PCMReader wrapper which changes the channel count and mask""" + + def __init__(self, pcmreader, channel_count, channel_mask): + self.pcmreader = pcmreader + self.sample_rate = pcmreader.sample_rate + self.channels = channel_count + self.channel_mask = channel_mask + self.bits_per_sample = pcmreader.bits_per_sample + + if ((pcmreader.channel_mask != 0) and (channel_mask != 0)): + #both channel masks are defined + #so forward matching channels from pcmreader + #and replace non-matching channels with empty samples + + mask = ChannelMask(channel_mask) + if (len(mask) != channel_count): + from .text import ERR_CHANNEL_COUNT_MASK_MISMATCH + raise ValueError(ERR_CHANNEL_COUNT_MASK_MISMATCH) + reader_channels = ChannelMask(pcmreader.channel_mask).channels() + + self.__channels__ = [(reader_channels.index(c) + if c in reader_channels + else None) for c in mask.channels()] + else: + #at least one channel mask is undefined + #so forward up to "channel_count" channels from pcmreader + #and replace any remainders with empty samples + if (channel_count <= pcmreader.channels): + self.__channels__ = range(channel_count) + else: + self.__channels__ = (range(channel_count) + + [None] * (channel_count - + pcmreader.channels)) + + from .pcm import (from_list, from_channels) + self.blank_channel = from_list([], + 1, + self.pcmreader.bits_per_sample, + True) + self.from_channels = from_channels + + def read(self, pcm_frames): + frame = self.pcmreader.read(pcm_frames) + + if (len(self.blank_channel) != frame.frames): + #ensure blank channel is large enough + from .pcm import from_list + self.blank_channel = from_list([0] * frame.frames, + 1, + self.pcmreader.bits_per_sample, + True) + + return self.from_channels([(frame.channel(c) + if c is not None else + self.blank_channel) + for c in self.__channels__]) + + def close(self): + self.pcmreader.close() + + def transfer_data(from_function, to_function): """sends BUFFER_SIZE strings from from_function to to_function @@ -1602,10 +2122,10 @@ from pcmreader """ - f = pcmreader.read(BUFFER_SIZE) + f = pcmreader.read(FRAMELIST_SIZE) while (len(f) > 0): to_function(f.to_bytes(big_endian, signed)) - f = pcmreader.read(BUFFER_SIZE) + f = pcmreader.read(FRAMELIST_SIZE) def threaded_transfer_framelist_data(pcmreader, to_function, @@ -1624,10 +2144,10 @@ def send_data(pcmreader, queue): try: - s = pcmreader.read(BUFFER_SIZE) + s = pcmreader.read(FRAMELIST_SIZE) while (len(s) > 0): queue.put(s) - s = pcmreader.read(BUFFER_SIZE) + s = pcmreader.read(FRAMELIST_SIZE) queue.put(None) except (IOError, ValueError): queue.put(None) @@ -1667,16 +2187,16 @@ the readers must be closed separately """ - if ((pcmreader1.sample_rate != pcmreader2.sample_rate) or - (pcmreader1.channels != pcmreader2.channels) or - (pcmreader1.bits_per_sample != pcmreader2.bits_per_sample)): + if (((pcmreader1.sample_rate != pcmreader2.sample_rate) or + (pcmreader1.channels != pcmreader2.channels) or + (pcmreader1.bits_per_sample != pcmreader2.bits_per_sample))): return False reader1 = BufferedPCMReader(pcmreader1) reader2 = BufferedPCMReader(pcmreader2) - s1 = reader1.read(BUFFER_SIZE) - s2 = reader2.read(BUFFER_SIZE) + s1 = reader1.read(FRAMELIST_SIZE) + s2 = reader2.read(FRAMELIST_SIZE) while ((len(s1) > 0) and (len(s2) > 0)): if (s1 != s2): @@ -1684,8 +2204,8 @@ transfer_data(reader2.read, lambda x: x) return False else: - s1 = reader1.read(BUFFER_SIZE) - s2 = reader2.read(BUFFER_SIZE) + s1 = reader1.read(FRAMELIST_SIZE) + s2 = reader2.read(FRAMELIST_SIZE) return True @@ -1698,11 +2218,12 @@ (which permits us to store just one big blob of memory at a time) """ - if ((pcmreader1.sample_rate != pcmreader2.sample_rate) or - (pcmreader1.channels != pcmreader2.channels) or - (pcmreader1.bits_per_sample != pcmreader2.bits_per_sample)): + if (((pcmreader1.sample_rate != pcmreader2.sample_rate) or + (pcmreader1.channels != pcmreader2.channels) or + (pcmreader1.bits_per_sample != pcmreader2.bits_per_sample))): return False + import cStringIO try: from hashlib import sha1 as sha except ImportError: @@ -1728,22 +2249,22 @@ may raise IOError or ValueError if problems occur when reading PCM streams""" - if ((pcmreader1.sample_rate != pcmreader2.sample_rate) or - (pcmreader1.channels != pcmreader2.channels) or - (pcmreader1.bits_per_sample != pcmreader2.bits_per_sample)): + if (((pcmreader1.sample_rate != pcmreader2.sample_rate) or + (pcmreader1.channels != pcmreader2.channels) or + (pcmreader1.bits_per_sample != pcmreader2.bits_per_sample))): return 0 - if ((pcmreader1.channel_mask != 0) and - (pcmreader2.channel_mask != 0) and - (pcmreader1.channel_mask != pcmreader2.channel_mask)): + if (((pcmreader1.channel_mask != 0) and + (pcmreader2.channel_mask != 0) and + (pcmreader1.channel_mask != pcmreader2.channel_mask))): return 0 frame_number = 0 reader1 = BufferedPCMReader(pcmreader1) reader2 = BufferedPCMReader(pcmreader2) - framelist1 = reader1.read(BUFFER_SIZE) - framelist2 = reader2.read(BUFFER_SIZE) + framelist1 = reader1.read(FRAMELIST_SIZE) + framelist2 = reader2.read(FRAMELIST_SIZE) while ((len(framelist1) > 0) and (len(framelist2) > 0)): if (framelist1 != framelist2): @@ -1754,92 +2275,88 @@ return frame_number + i else: frame_number += framelist1.frames - framelist1 = reader1.read(BUFFER_SIZE) - framelist2 = reader2.read(BUFFER_SIZE) + framelist1 = reader1.read(FRAMELIST_SIZE) + framelist2 = reader2.read(FRAMELIST_SIZE) return None -class PCMCat(PCMReader): +class PCMCat: """a PCMReader for concatenating several PCMReaders""" def __init__(self, pcmreaders): - """pcmreaders is an iterator of PCMReader objects + """pcmreaders is a list of PCMReader objects - note that this currently does no error checking - to ensure reads have the same sample_rate, channels, - bits_per_sample or channel mask! - one must perform that check prior to building a PCMCat - """ - - self.reader_queue = pcmreaders + all must have the same stream attributes""" - try: - self.first = self.reader_queue.next() - except StopIteration: - raise ValueError(_(u"You must have at least 1 PCMReader")) + self.pcmreaders = list(pcmreaders) + if (len(self.pcmreaders) == 0): + from .text import ERR_NO_PCMREADERS + raise ValueError(ERR_NO_PCMREADERS) + + if (len(set([r.sample_rate for r in self.pcmreaders])) != 1): + from .text import ERR_SAMPLE_RATE_MISMATCH + raise ValueError(ERR_SAMPLE_RATE_MISMATCH) + if (len(set([r.channels for r in self.pcmreaders])) != 1): + from .text import ERR_CHANNEL_COUNT_MISMATCH + raise ValueError(ERR_CHANNEL_COUNT_MISMATCH) + if (len(set([r.bits_per_sample for r in self.pcmreaders])) != 1): + from .text import ERR_BPS_MISMATCH + raise ValueError(ERR_BPS_MISMATCH) + + self.__index__ = 0 + reader = self.pcmreaders[self.__index__] + self.__read__ = reader.read + + self.sample_rate = reader.sample_rate + self.channels = reader.channels + self.channel_mask = reader.channel_mask + self.bits_per_sample = reader.bits_per_sample + + def read(self, pcm_frames): + """try to read a pcm.FrameList with the given number of frames + + raises ValueError if any of the streams is mismatched""" + + #read a FrameList from the current PCMReader + framelist = self.__read__(pcm_frames) + + #while the FrameList is empty + while (len(framelist) == 0): + #move on to the next PCMReader in the queue, if any + self.__index__ += 1 + try: + reader = self.pcmreaders[self.__index__] + self.__read__ = reader.read - self.sample_rate = self.first.sample_rate - self.channels = self.first.channels - self.channel_mask = self.first.channel_mask - self.bits_per_sample = self.first.bits_per_sample + #and read a FrameList from the new PCMReader + framelist = self.__read__(pcm_frames) + except IndexError: + #if no PCMReaders remain, have all further reads + #return empty FrameList objects + #and return an empty FrameList object + self.read = self.read_finished + return self.read_finished(pcm_frames) + else: + #otherwise, return the filled FrameList + return framelist - def read(self, bytes): - """try to read a pcm.FrameList of size 'bytes'""" + def read_finished(self, pcm_frames): + return pcm.from_list([], self.channels, self.bits_per_sample, True) - try: - s = self.first.read(bytes) - if (len(s) > 0): - return s - else: - self.first.close() - self.first = self.reader_queue.next() - return self.read(bytes) - except StopIteration: - return pcm.from_list([], - self.channels, - self.bits_per_sample, - True) + def read_closed(self, pcm_frames): + raise ValueError() def close(self): """closes the stream for reading""" - pass - - -class __buffer__: - def __init__(self, channels, bits_per_sample, framelists=None): - if (framelists is None): - self.buffer = [] - else: - self.buffer = framelists - self.end_frame = pcm.from_list([], channels, bits_per_sample, True) - self.bytes_per_sample = bits_per_sample / 8 - - #returns the length of the entire buffer in bytes - def __len__(self): - if (len(self.buffer) > 0): - return sum(map(len, self.buffer)) * self.bytes_per_sample - else: - return 0 - - def framelist(self): - import operator - - return reduce(operator.concat, self.buffer, self.end_frame) - - def push(self, s): - self.buffer.append(s) - - def pop(self): - return self.buffer.pop(0) - - def unpop(self, s): - self.buffer.insert(0, s) + self.read = self.read_closed + for reader in self.pcmreaders: + reader.close() class BufferedPCMReader: - """a PCMReader which reads exact counts of bytes""" + """a PCMReader which reads exact counts of PCM frames""" def __init__(self, pcmreader): """pcmreader is a regular PCMReader object""" @@ -1849,39 +2366,66 @@ self.channels = pcmreader.channels self.channel_mask = pcmreader.channel_mask self.bits_per_sample = pcmreader.bits_per_sample - self.buffer = __buffer__(self.channels, self.bits_per_sample) - self.reader_finished = False + self.buffer = pcm.from_list([], + self.channels, + self.bits_per_sample, + True) def close(self): """closes the sub-pcmreader and frees our internal buffer""" - del(self.buffer) self.pcmreader.close() + self.read = self.read_closed - def read(self, bytes): - """reads as close to 'bytes' number of bytes without going over + def read(self, pcm_frames): + """reads the given number of PCM frames - this uses an internal buffer to ensure reading the proper - number of bytes on each call + this may return fewer than the given number + at the end of a stream + but will never return more than requested """ - #fill our buffer to at least 'bytes', possibly more - self.__fill__(bytes) - output_framelist = self.buffer.framelist() - (output, remainder) = output_framelist.split( - output_framelist.frame_count(bytes)) - self.buffer.buffer = [remainder] + #fill our buffer to at least "pcm_frames", possibly more + while (self.buffer.frames < pcm_frames): + frame = self.pcmreader.read(FRAMELIST_SIZE) + if (len(frame)): + self.buffer += frame + else: + break + + #chop off the preceding number of PCM frames and return them + (output, self.buffer) = self.buffer.split(pcm_frames) + return output - #try to fill our internal buffer to at least 'bytes' - def __fill__(self, bytes): - while ((len(self.buffer) < bytes) and - (not self.reader_finished)): - s = self.pcmreader.read(BUFFER_SIZE) - if (len(s) > 0): - self.buffer.push(s) - else: - self.reader_finished = True + def read_closed(self, pcm_frames): + raise ValueError() + + +class CounterPCMReader: + """a PCMReader which counts bytes and frames written""" + + def __init__(self, pcmreader): + self.sample_rate = pcmreader.sample_rate + self.channels = pcmreader.channels + self.channel_mask = pcmreader.channel_mask + self.bits_per_sample = pcmreader.bits_per_sample + + self.__pcmreader__ = pcmreader + self.frames_written = 0 + + def bytes_written(self): + return (self.frames_written * + self.channels * + (self.bits_per_sample / 8)) + + def read(self, pcm_frames): + frame = self.__pcmreader__.read(pcm_frames) + self.frames_written += frame.frames + return frame + + def close(self): + self.__pcmreader__.close() class LimitedFileReader: @@ -1919,21 +2463,24 @@ self.channels = self.pcmreader.channels self.channel_mask = self.pcmreader.channel_mask self.bits_per_sample = self.pcmreader.bits_per_sample - self.bytes_per_frame = self.channels * (self.bits_per_sample / 8) - def read(self, bytes): + def read(self, pcm_frames): if (self.total_pcm_frames > 0): - frame = self.pcmreader.read( - min(bytes, - self.total_pcm_frames * self.bytes_per_frame)) + frame = self.pcmreader.read(min(pcm_frames, self.total_pcm_frames)) self.total_pcm_frames -= frame.frames return frame else: - return pcm.FrameList("", self.channels, self.bits_per_sample, - False, True) + return pcm.FrameList("", + self.channels, + self.bits_per_sample, + False, + True) + + def read_closed(self, pcm_frames): + raise ValueError() def close(self): - self.total_pcm_frames = 0 + self.read = self.read_closed def pcm_split(reader, pcm_lengths): @@ -1944,6 +2491,7 @@ as the full stream. reader is closed upon completion """ + import cStringIO import tempfile def chunk_sizes(total_size, chunk_size): @@ -1954,18 +2502,17 @@ full_data = BufferedPCMReader(reader) - for byte_length in [i * reader.channels * reader.bits_per_sample / 8 - for i in pcm_lengths]: - if (byte_length > (BUFFER_SIZE * 10)): + for pcm_length in pcm_lengths: + if (pcm_length > (FRAMELIST_SIZE * 10)): #if the sub-file length is somewhat large, use a temporary file sub_file = tempfile.TemporaryFile() - for size in chunk_sizes(byte_length, BUFFER_SIZE): + for size in chunk_sizes(pcm_length, FRAMELIST_SIZE): sub_file.write(full_data.read(size).to_bytes(False, True)) sub_file.seek(0, 0) else: #if the sub-file length is very small, use StringIO sub_file = cStringIO.StringIO( - full_data.read(byte_length).to_bytes(False, True)) + full_data.read(pcm_length).to_bytes(False, True)) yield PCMReader(sub_file, reader.sample_rate, @@ -1976,374 +2523,98 @@ full_data.close() -#going from many channels to less channels -class __channel_remover__: - def __init__(self, old_channel_mask, new_channel_mask): - old_channels = ChannelMask(old_channel_mask).channels() - self.channels_to_keep = [] - for new_channel in ChannelMask(new_channel_mask).channels(): - if (new_channel in old_channels): - self.channels_to_keep.append(old_channels.index(new_channel)) - - def convert(self, frame_list): - return pcm.from_channels( - [frame_list.channel(i) for i in self.channels_to_keep]) - - -class __channel_adder__: - def __init__(self, channels): - self.channels = channels - - def convert(self, frame_list): - current_channels = [frame_list.channel(i) - for i in xrange(frame_list.channels)] - while (len(current_channels) < self.channels): - current_channels.append(current_channels[0]) - - return pcm.from_channels(current_channels) - - -class __stereo_to_mono__: - def __init__(self): - pass - - def convert(self, frame_list): - return pcm.from_list( - [(l + r) / 2 for l, r in izip(frame_list.channel(0), - frame_list.channel(1))], - 1, frame_list.bits_per_sample, True) - - -#going from many channels to 2 -class __downmixer__: - def __init__(self, old_channel_mask, old_channel_count): - #grab the front_left, front_right, front_center, - #back_left and back_right channels from old frame_list, if possible - #missing channels are replaced with 0-sample channels - #excess channels are dropped entirely - #side_left and side_right may be substituted for back_left/right - #but back channels take precedence - - if (int(old_channel_mask) == 0): - #if the old_channel_mask is undefined - #invent a channel mask based on the channel count - old_channel_mask = {1: ChannelMask.from_fields(front_center=True), - 2: ChannelMask.from_fields(front_left=True, - front_right=True), - 3: ChannelMask.from_fields(front_left=True, - front_right=True, - front_center=True), - 4: ChannelMask.from_fields(front_left=True, - front_right=True, - back_left=True, - back_right=True), - 5: ChannelMask.from_fields(front_left=True, - front_right=True, - front_center=True, - back_left=True, - back_right=True)}[ - min(old_channel_count, 5)] - else: - old_channel_mask = ChannelMask(old_channel_mask) - - #channels_to_keep is an array of channel offsets - #where the index is: - #0 - front_left - #1 - front_right - #2 - front_center - #3 - back/side_left - #4 - back/side_right - #if -1, the channel is blank - self.channels_to_keep = [] - for channel in ["front_left", "front_right", "front_center"]: - if (getattr(old_channel_mask, channel)): - self.channels_to_keep.append(old_channel_mask.index(channel)) - else: - self.channels_to_keep.append(-1) - - if (old_channel_mask.back_left): - self.channels_to_keep.append(old_channel_mask.index("back_left")) - elif (old_channel_mask.side_left): - self.channels_to_keep.append(old_channel_mask.index("side_left")) - else: - self.channels_to_keep.append(-1) - - if (old_channel_mask.back_right): - self.channels_to_keep.append(old_channel_mask.index("back_right")) - elif (old_channel_mask.side_right): - self.channels_to_keep.append(old_channel_mask.index("side_right")) - else: - self.channels_to_keep.append(-1) - - self.has_empty_channels = (-1 in self.channels_to_keep) - - def convert(self, frame_list): - REAR_GAIN = 0.6 - CENTER_GAIN = 0.7 - - if (self.has_empty_channels): - empty_channel = pcm.from_list([0] * frame_list.frames, - 1, - frame_list.bits_per_sample, - True) - - if (self.channels_to_keep[0] != -1): - Lf = frame_list.channel(self.channels_to_keep[0]) - else: - Lf = empty_channel - - if (self.channels_to_keep[1] != -1): - Rf = frame_list.channel(self.channels_to_keep[1]) - else: - Rf = empty_channel - - if (self.channels_to_keep[2] != -1): - C = frame_list.channel(self.channels_to_keep[2]) - else: - C = empty_channel - - if (self.channels_to_keep[3] != -1): - Lr = frame_list.channel(self.channels_to_keep[3]) - else: - Lr = empty_channel - - if (self.channels_to_keep[4] != -1): - Rr = frame_list.channel(self.channels_to_keep[4]) - else: - Rr = empty_channel - - mono_rear = [0.7 * (Lr_i + Rr_i) for Lr_i, Rr_i in izip(Lr, Rr)] - - converter = lambda x: int(round(x)) - - left_channel = pcm.from_list( - [converter(Lf_i + - (REAR_GAIN * mono_rear_i) + - (CENTER_GAIN * C_i)) - for Lf_i, mono_rear_i, C_i in izip(Lf, mono_rear, C)], - 1, - frame_list.bits_per_sample, - True) - - right_channel = pcm.from_list( - [converter(Rf_i - - (REAR_GAIN * mono_rear_i) + - (CENTER_GAIN * C_i)) - for Rf_i, mono_rear_i, C_i in izip(Rf, mono_rear, C)], - 1, - frame_list.bits_per_sample, - True) - - return pcm.from_channels([left_channel, right_channel]) - - -#going from many channels to 1 -class __downmix_to_mono__: - def __init__(self, old_channel_mask, old_channel_count): - self.downmix = __downmixer__(old_channel_mask, old_channel_count) - self.mono = __stereo_to_mono__() - - def convert(self, frame_list): - return self.mono.convert(self.downmix.convert(frame_list)) - - -class __convert_sample_rate__: - def __init__(self, old_sample_rate, new_sample_rate, - channels, bits_per_sample): - from . import resample - - self.resampler = resample.Resampler( - channels, - float(new_sample_rate) / float(old_sample_rate), - 0) - self.unresampled = pcm.FloatFrameList([], channels) - self.bits_per_sample = bits_per_sample - - def convert(self, frame_list): - #FIXME - The floating-point output from resampler.process() - #should be normalized rather than just chopping off - #excessively high or low samples (above 1.0 or below -1.0) - #during conversion to PCM. - #Unfortunately, that'll require building a second pass - #into the conversion process which will complicate PCMConverter - #a lot. - (output, self.unresampled) = self.resampler.process( - self.unresampled + frame_list.to_float(), - (len(frame_list) == 0) and (len(self.unresampled) == 0)) - - return output.to_int(self.bits_per_sample) - - -class __convert_sample_rate_and_bits_per_sample__(__convert_sample_rate__): - def convert(self, frame_list): - (output, self.unresampled) = self.resampler.process( - self.unresampled + frame_list.to_float(), - (len(frame_list) == 0) and (len(self.unresampled) == 0)) - - return __add_dither__(output.to_int(self.bits_per_sample)) - - -class __convert_bits_per_sample__: - def __init__(self, bits_per_sample): - self.bits_per_sample = bits_per_sample - - def convert(self, frame_list): - return __add_dither__( - frame_list.to_float().to_int(self.bits_per_sample)) - - -def __add_dither__(frame_list): - if (frame_list.bits_per_sample >= 16): - random_bytes = map(ord, os.urandom((len(frame_list) / 8) + 1)) - white_noise = [(random_bytes[i / 8] & (1 << (i % 8))) >> (i % 8) - for i in xrange(len(frame_list))] - else: - white_noise = [0] * len(frame_list) - - return pcm.from_list([i ^ w for (i, w) in izip(frame_list, - white_noise)], - frame_list.channels, - frame_list.bits_per_sample, - True) - - -class PCMConverter: +def PCMConverter(pcmreader, + sample_rate, + channels, + channel_mask, + bits_per_sample): """a PCMReader wrapper for converting attributes for example, this can be used to alter sample_rate, bits_per_sample, channel_mask, channel count, or any combination of those attributes. It resamples, downsamples, etc. to achieve the proper output - """ - def __init__(self, pcmreader, - sample_rate, - channels, - channel_mask, - bits_per_sample): - """takes a PCMReader input and the attributes of the new stream""" - - self.sample_rate = sample_rate - self.channels = channels - self.bits_per_sample = bits_per_sample - self.channel_mask = channel_mask - self.reader = pcmreader + may raise ValueError if any of the attributes are unsupported + or invalid + """ - self.conversions = [] - if (self.reader.channels != self.channels): - if (self.channels == 1): - self.conversions.append( - __downmix_to_mono__(pcmreader.channel_mask, - pcmreader.channels)) - elif (self.channels == 2): - self.conversions.append( - __downmixer__(pcmreader.channel_mask, - pcmreader.channels)) - elif (self.channels < pcmreader.channels): - self.conversions.append( - __channel_remover__(pcmreader.channel_mask, - channel_mask)) - elif (self.channels > pcmreader.channels): - self.conversions.append( - __channel_adder__(self.channels)) - - if (self.reader.sample_rate != self.sample_rate): - #if we're converting sample rate and bits-per-sample - #at the same time, short-circuit the conversion to do both at once - #which can be sped up somewhat - if (self.reader.bits_per_sample != self.bits_per_sample): - self.conversions.append( - __convert_sample_rate_and_bits_per_sample__( - self.reader.sample_rate, - self.sample_rate, - self.channels, - self.bits_per_sample)) + if (sample_rate <= 0): + from .text import ERR_INVALID_SAMPLE_RATE + raise ValueError(ERR_INVALID_SAMPLE_RATE) + elif (channels <= 0): + from .text import ERR_INVALID_CHANNEL_COUNT + raise ValueError(ERR_INVALID_CHANNEL_COUNT) + elif (bits_per_sample not in (8, 16, 24)): + from .text import ERR_INVALID_BITS_PER_SAMPLE + raise ValueError(ERR_INVALID_BITS_PER_SAMPLE) + + if ((channel_mask != 0) and (len(ChannelMask(channel_mask)) != channels)): + #channel_mask is defined but has a different number of channels + #than the channel count attribute + from .text import ERR_CHANNEL_COUNT_MASK_MISMATCH + raise ValueError(ERR_CHANNEL_COUNT_MASK_MISMATCH) + + if (pcmreader.channels > channels): + if ((channels == 1) and (channel_mask in (0, 0x4))): + if (pcmreader.channels > 2): + #reduce channel count through downmixing + #followed by averaging + from .pcmconverter import (Averager, Downmixer) + pcmreader = Averager(Downmixer(pcmreader)) else: - self.conversions.append( - __convert_sample_rate__( - self.reader.sample_rate, - self.sample_rate, - self.channels, - self.bits_per_sample)) + #pcmreader.channels == 2 + #so reduce channel count through averaging + from .pcmconverter import Averager + pcmreader = Averager(pcmreader) + elif ((channels == 2) and (channel_mask in (0, 0x3))): + #reduce channel count through downmixing + from .pcmconverter import Downmixer + pcmreader = Downmixer(pcmreader) + else: + #unusual channel count/mask combination + pcmreader = RemaskedPCMReader(pcmreader, + channels, + channel_mask) + elif (pcmreader.channels < channels): + #increase channel count by duplicating first channel + #(this is usually just going from mono to stereo + # since there's no way to summon surround channels + # out of thin air) + pcmreader = ReorderedPCMReader(pcmreader, + range(pcmreader.channels) + + [0] * (channels - pcmreader.channels), + channel_mask) + + if (pcmreader.sample_rate != sample_rate): + #convert sample rate through resampling + from .pcmconverter import Resampler + pcmreader = Resampler(pcmreader, sample_rate) + + if (pcmreader.bits_per_sample != bits_per_sample): + #use bitshifts/dithering to adjust bits-per-sample + from .pcmconverter import BPSConverter + pcmreader = BPSConverter(pcmreader, bits_per_sample) + + return pcmreader + + +def resampled_frame_count(initial_frame_count, + initial_sample_rate, + new_sample_rate): + """given an initial PCM frame count, initial sample rate + and new sample rate, returns the new PCM frame count + once the stream has been resampled""" - else: - if (self.reader.bits_per_sample != self.bits_per_sample): - self.conversions.append( - __convert_bits_per_sample__( - self.bits_per_sample)) - - def read(self, bytes): - """try to read a pcm.FrameList of size 'bytes'""" - - frame_list = self.reader.read(bytes) - - for converter in self.conversions: - frame_list = converter.convert(frame_list) - - return frame_list - - def close(self): - """closes the stream for reading""" - - self.reader.close() - - -class ReplayGainReader: - """a PCMReader which applies ReplayGain on its output""" - - def __init__(self, pcmreader, replaygain, peak): - """fields are: - - pcmreader - a PCMReader object - replaygain - a floating point dB value - peak - the maximum absolute value PCM sample, as a float - - the latter two are typically stored with the file, - split into album gain and track gain pairs - which the user can apply based on preference - """ - - self.reader = pcmreader - self.sample_rate = pcmreader.sample_rate - self.channels = pcmreader.channels - self.channel_mask = pcmreader.channel_mask - self.bits_per_sample = pcmreader.bits_per_sample - - self.replaygain = replaygain - self.peak = peak - self.bytes_per_sample = self.bits_per_sample / 8 - self.multiplier = 10 ** (replaygain / 20) - - #if we're increasing the volume (multipler is positive) - #and that increases the peak beyond 1.0 (which causes clipping) - #reduce the multiplier so that the peak doesn't go beyond 1.0 - if ((self.multiplier * self.peak) > 1.0): - self.multiplier = 1.0 / self.peak - - def read(self, bytes): - """try to read a pcm.FrameList of size 'bytes'""" - - multiplier = self.multiplier - samples = self.reader.read(bytes) - - if (self.bits_per_sample >= 16): - random_bytes = map(ord, os.urandom((len(samples) / 8) + 1)) - white_noise = [(random_bytes[i / 8] & (1 << (i % 8))) >> (i % 8) - for i in xrange(len(samples))] - else: - white_noise = [0] * len(samples) - - return pcm.from_list( - [(int(round(s * multiplier)) ^ w) for (s, w) in - izip(samples, white_noise)], - samples.channels, - samples.bits_per_sample, - True) - - def close(self): - """closes the stream for reading""" - - self.reader.close() + if (initial_sample_rate == new_sample_rate): + return initial_frame_count + else: + from decimal import (Decimal, ROUND_DOWN) + new_frame_count = ((Decimal(initial_frame_count) * + Decimal(new_sample_rate)) / + Decimal(initial_sample_rate)) + return int(new_frame_count.quantize(Decimal("1."), + rounding=ROUND_DOWN)) def applicable_replay_gain(tracks): @@ -2362,8 +2633,7 @@ return False channels = set([track.channels() for track in tracks]) - if ((len(channels) > 1) or - (list(channels)[0] not in (1, 2))): + if ((len(channels) > 1) or (list(channels)[0] not in (1, 2))): return False return True @@ -2375,30 +2645,66 @@ raises ValueError if a problem occurs during calculation""" + if (len(tracks) == 0): + return + from . import replaygain as replaygain + from bisect import bisect + + SUPPORTED_RATES = [8000, 11025, 12000, 16000, 18900, 22050, 24000, + 32000, 37800, 44100, 48000, 56000, 64000, 88200, + 96000, 112000, 128000, 144000, 176400, 192000] + + target_rate = ([SUPPORTED_RATES[0]] + SUPPORTED_RATES)[ + bisect(SUPPORTED_RATES, most_numerous([track.sample_rate() + for track in tracks]))] + + track_frames = [resampled_frame_count(track.total_frames(), + track.sample_rate(), + target_rate) + for track in tracks] + current_frames = 0 + total_frames = sum(track_frames) - sample_rate = set([track.sample_rate() for track in tracks]) - if (len(sample_rate) != 1): - raise ValueError(("at least one track is required " + - "and all must have the same sample rate")) - total_frames = sum([track.total_frames() for track in tracks]) - processed_frames = 0 + rg = replaygain.ReplayGain(target_rate) - rg = replaygain.ReplayGain(list(sample_rate)[0]) gains = [] - for track in tracks: + + for (track, track_frames) in zip(tracks, track_frames): pcm = track.to_pcm() - frame = pcm.read(BUFFER_SIZE) - while (len(frame) > 0): - rg.update(frame) - processed_frames += frame.frames - if (progress is not None): - progress(processed_frames, total_frames) - frame = pcm.read(BUFFER_SIZE) - pcm.close() - (track_gain, track_peak) = rg.title_gain() + + if (pcm.channels > 2): + #add a wrapper to cull any channels above 2 + output_channels = 2 + output_channel_mask = 0x3 + else: + output_channels = pcm.channels + output_channel_mask = pcm.channel_mask + + if (((pcm.channels != output_channels) or + (pcm.channel_mask != output_channel_mask) or + (pcm.sample_rate) != target_rate)): + pcm = PCMConverter(pcm, + target_rate, + output_channels, + output_channel_mask, + pcm.bits_per_sample) + + #finally, perform the gain calculation on the PCMReader + #and accumulate the title gain + if (progress is not None): + (track_gain, track_peak) = rg.title_gain( + PCMReaderProgress(pcm, total_frames, progress, + current_frames=current_frames)) + current_frames += track_frames + else: + (track_gain, track_peak) = rg.title_gain(pcm) gains.append((track, track_gain, track_peak)) + + #once everything is calculated, get the album gain (album_gain, album_peak) = rg.album_gain() + + #yield a set of accumulated track and album gains for (track, track_gain, track_peak) in gains: yield (track, track_gain, track_peak, album_gain, album_peak) @@ -2435,73 +2741,151 @@ class MetaData: """the base class for storing textual AudioFile metadata - this includes things like track name, track number, album name - and so forth. It also includes embedded images, if present + Fields may be None, indicating they're not present + in the underlying metadata implementation. - fields are stored with the same name they are initialized with - except for images, they can all be manipulated directly - (images have dedicated set/get/delete methods instead) - subclasses are expected to override getattr/setattr - so that updating attributes will adjust the low-level attributes - accordingly + Changing a field to a new value will update the underlying metadata + (e.g. vorbiscomment.track_name = u"Foo" + will set a Vorbis comment's "TITLE" field to "Foo") + + Updating the underlying metadata will change the metadata's fields + (e.g. setting a Vorbis comment's "TITLE" field to "bar" + will update vorbiscomment.title_name to u"bar") + + Deleting a field or setting it to None + will remove it from the underlying metadata + (e.g. del(vorbiscomment.track_name) will delete the "TITLE" field) """ - FIELDS = ("track_name", "track_number", "track_total", - "album_name", "artist_name", - "performer_name", "composer_name", "conductor_name", - "media", "ISRC", "catalog", "copyright", - "publisher", "year", "date", "album_number", "album_total", + FIELDS = ("track_name", + "track_number", + "track_total", + "album_name", + "artist_name", + "performer_name", + "composer_name", + "conductor_name", + "media", + "ISRC", + "catalog", + "copyright", + "publisher", + "year", + "date", + "album_number", + "album_total", "comment") - INTEGER_FIELDS = ("track_number", "track_total", - "album_number", "album_total") + INTEGER_FIELDS = ("track_number", + "track_total", + "album_number", + "album_total") + + #this is the order fields should be presented to the user + #to ensure consistency across utilities + FIELD_ORDER = ("track_name", + "artist_name", + "album_name", + "track_number", + "track_total", + "album_number", + "album_total", + "performer_name", + "composer_name", + "conductor_name", + "catalog", + "ISRC", + "publisher", + "media", + "year", + "date", + "copyright", + "comment") + + #this is the name fields should use when presented to the user + #also to ensure constency across utilities + from .text import (METADATA_TRACK_NAME, + METADATA_TRACK_NUMBER, + METADATA_TRACK_TOTAL, + METADATA_ALBUM_NAME, + METADATA_ARTIST_NAME, + METADATA_PERFORMER_NAME, + METADATA_COMPOSER_NAME, + METADATA_CONDUCTOR_NAME, + METADATA_MEDIA, + METADATA_ISRC, + METADATA_CATALOG, + METADATA_COPYRIGHT, + METADATA_PUBLISHER, + METADATA_YEAR, + METADATA_DATE, + METADATA_ALBUM_NUMBER, + METADATA_ALBUM_TOTAL, + METADATA_COMMENT) + + FIELD_NAMES = {"track_name": METADATA_TRACK_NAME, + "track_number": METADATA_TRACK_NUMBER, + "track_total": METADATA_TRACK_TOTAL, + "album_name": METADATA_ALBUM_NAME, + "artist_name": METADATA_ARTIST_NAME, + "performer_name": METADATA_PERFORMER_NAME, + "composer_name": METADATA_COMPOSER_NAME, + "conductor_name": METADATA_CONDUCTOR_NAME, + "media": METADATA_MEDIA, + "ISRC": METADATA_ISRC, + "catalog": METADATA_CATALOG, + "copyright": METADATA_COPYRIGHT, + "publisher": METADATA_PUBLISHER, + "year": METADATA_YEAR, + "date": METADATA_DATE, + "album_number": METADATA_ALBUM_NUMBER, + "album_total": METADATA_ALBUM_TOTAL, + "comment": METADATA_COMMENT} def __init__(self, - track_name=u"", - track_number=0, - track_total=0, - album_name=u"", - artist_name=u"", - performer_name=u"", - composer_name=u"", - conductor_name=u"", - media=u"", - ISRC=u"", - catalog=u"", - copyright=u"", - publisher=u"", - year=u"", - date=u"", - album_number=0, - album_total=0, - comment=u"", + track_name=None, + track_number=None, + track_total=None, + album_name=None, + artist_name=None, + performer_name=None, + composer_name=None, + conductor_name=None, + media=None, + ISRC=None, + catalog=None, + copyright=None, + publisher=None, + year=None, + date=None, + album_number=None, + album_total=None, + comment=None, images=None): - """fields are as follows: - - track_name - the name of this individual track - track_number - the number of this track - track_total - the total number of tracks - album_name - the name of this track's album - artist_name - the song's original creator/composer - performer_name - the song's performing artist - composer_name - the song's composer name - conductor_name - the song's conductor's name - media - the album's media type (CD,tape,etc.) - ISRC - the song's ISRC - catalog - the album's catalog number - copyright - the song's copyright information - publisher - the album's publisher - year - the album's release year - date - the original recording date - album_number - the disc's volume number, if any - album_total - the total number of discs, if any - comment - the track's comment string - images - a list of Image objects - - track_number, track_total, album_number and album_total are ints - images is an optional list of Image objects - the rest are unicode strings """ +| field | type | meaning | +|----------------+---------+--------------------------------------| +| track_name | unicode | the name of this individual track | +| track_number | integer | the number of this track | +| track_total | integer | the total number of tracks | +| album_name | unicode | the name of this track's album | +| artist_name | unicode | the song's original creator/composer | +| performer_name | unicode | the song's performing artist | +| composer_name | unicode | the song's composer name | +| conductor_name | unicode | the song's conductor's name | +| media | unicode | the album's media type | +| ISRC | unicode | the song's ISRC | +| catalog | unicode | the album's catalog number | +| copyright | unicode | the song's copyright information | +| publisher | unicode | the album's publisher | +| year | unicode | the album's release year | +| date | unicode | the original recording date | +| album_number | integer | the disc's volume number | +| album_total | integer | the total number of discs | +| comment | unicode | the track's comment string | +| images | list | list of Image objects | +|----------------+---------+--------------------------------------| +""" #we're avoiding self.foo = foo because #__setattr__ might need to be redefined @@ -2531,17 +2915,14 @@ self.__dict__['__images__'] = list() def __repr__(self): + fields = ["%s=%s" % (field, repr(getattr(self, field))) + for field in MetaData.FIELDS] return ("MetaData(%s)" % ( - ",".join(["%s"] * (len(MetaData.FIELDS))))) % \ - tuple(["%s=%s" % (field, repr(getattr(self, field))) - for field in MetaData.FIELDS]) + ",".join(["%s"] * (len(MetaData.FIELDS))))) % tuple(fields) def __delattr__(self, field): if (field in self.FIELDS): - if (field in self.INTEGER_FIELDS): - self.__dict__[field] = 0 - else: - self.__dict__[field] = u"" + self.__dict__[field] = None else: try: del(self.__dict__[field]) @@ -2559,69 +2940,74 @@ which is not blank""" for (attr, field) in self.fields(): - if (attr in self.INTEGER_FIELDS): - if (field > 0): - yield (attr, field) - else: - if (len(field) > 0): - yield (attr, field) + if (field is not None): + yield (attr, field) def empty_fields(self): """yields an (attr, value) tuple per MetaData field which is blank""" for (attr, field) in self.fields(): - if (attr in self.INTEGER_FIELDS): - if (field == 0): - yield (attr, field) - else: - if (len(field) == 0): - yield (attr, field) + if (field is None): + yield (attr, field) def __unicode__(self): comment_pairs = [] - #handle the first batch of text fields - for (attr, name) in zip( - ["track_name", "artist_name", "performer_name", - "composer_name", "conductor_name", "album_name", - "catalog"], - [u"Title", u"Artist", u"Performer", u"Composer", - u"Conductor", u"Album", u"Catalog"]): - if (len(getattr(self, attr)) > 0): - comment_pairs.append((display_unicode(name), - getattr(self, attr))) - - #handle the numerical fields - if (self.track_total != 0): - comment_pairs.append((display_unicode("Track #"), - u"%d/%d" % (self.track_number, - self.track_total))) - elif (self.track_number != 0): - comment_pairs.append((display_unicode("Track #"), - u"%d" % (self.track_number))) - - if (self.album_total != 0): - comment_pairs.append((display_unicode("Album #"), - u"%d/%d" % (self.album_number, - self.album_total))) - elif (self.album_number != 0): - comment_pairs.append((display_unicode("Album #"), - u"%d" % (self.album_number))) - - #handle the last batch of text fields - for (attr, name) in zip( - ["ISRC", "publisher", "media", "year", "date", "copyright", - "comment"], - [u"ISRC", u"Publisher", u"Media", u"Year", u"Date", - u"Copyright", u"Comment"]): - if (len(getattr(self, attr)) > 0): - comment_pairs.append((display_unicode(name), + for attr in self.FIELD_ORDER: + if (attr == "track_number"): + #combine track number/track total into single field + track_number = self.track_number + track_total = self.track_total + if ((track_number is None) and (track_total is None)): + #nothing to display + pass + elif ((track_number is not None) and (track_total is None)): + comment_pairs.append( + (display_unicode(self.FIELD_NAMES[attr]), + unicode(track_number))) + elif ((track_number is None) and (track_total is not None)): + comment_pairs.append( + (display_unicode(self.FIELD_NAMES[attr]), + u"?/%d" % (track_total,))) + else: + #neither track_number or track_total is None + comment_pairs.append( + (display_unicode(self.FIELD_NAMES[attr]), + u"%d/%d" % (track_number, track_total))) + elif (attr == "track_total"): + pass + elif (attr == "album_number"): + #combine album number/album total into single field + album_number = self.album_number + album_total = self.album_total + if ((album_number is None) and (album_total is None)): + #nothing to display + pass + elif ((album_number is not None) and (album_total is None)): + comment_pairs.append( + (display_unicode(self.FIELD_NAMES[attr]), + unicode(album_number))) + elif ((album_number is None) and (album_total is not None)): + comment_pairs.append( + (display_unicode(self.FIELD_NAMES[attr]), + u"?/%d" % (album_total,))) + else: + #neither album_number or album_total is None + comment_pairs.append( + (display_unicode(self.FIELD_NAMES[attr]), + u"%d/%d" % (album_number, album_total))) + elif (attr == "album_total"): + pass + elif (getattr(self, attr) is not None): + comment_pairs.append((display_unicode(self.FIELD_NAMES[attr]), getattr(self, attr))) #append image data, if necessary + from .text import LAB_PICTURE + for image in self.images(): - comment_pairs.append((display_unicode(_(u"Picture")), + comment_pairs.append((display_unicode(LAB_PICTURE), unicode(image))) #right-align the comment key values @@ -2649,8 +3035,8 @@ def __eq__(self, metadata): for attr in MetaData.FIELDS: - if ((not hasattr(metadata, attr)) or - (getattr(self, attr) != getattr(metadata, attr))): + if ((not hasattr(metadata, attr)) or (getattr(self, attr) != + getattr(metadata, attr))): return False else: return True @@ -2727,7 +3113,8 @@ if (self.supports_images()): self.__images__.append(image) else: - raise ValueError(_(u"This MetaData type does not support images")) + from .text import ERR_PICTURES_UNSUPPORTED + raise ValueError(ERR_PICTURES_UNSUPPORTED) def delete_image(self, image): """deletes an Image object from this metadata @@ -2741,7 +3128,8 @@ if (self.supports_images()): self.__images__.pop(self.__images__.index(image)) else: - raise ValueError(_(u"This MetaData type does not support images")) + from .text import ERR_PICTURES_UNSUPPORTED + raise ValueError(ERR_PICTURES_UNSUPPORTED) def clean(self, fixes_performed): """returns a new MetaData object that's been cleaned of problems @@ -2787,143 +3175,6 @@ if (len(items) == 1)])) -class MetaDataFileException(Exception): - """a superclass of XMCDException and MBXMLException - - this allows one to handle any sort of metadata file exception - consistently""" - - def __unicode__(self): - return _(u"Invalid XMCD or MusicBrainz XML file") - - -class AlbumMetaDataFile: - """a base class for MetaData containing files - - this includes FreeDB's XMCD files - and MusicBrainz's XML files""" - - def __init__(self, album_name, artist_name, year, catalog, - extra, track_metadata): - """track_metadata is a list of tuples. The rest are unicode""" - - self.album_name = album_name - self.artist_name = artist_name - self.year = year - self.catalog = catalog - self.extra = extra - self.track_metadata = track_metadata - - def __len__(self): - return len(self.track_metadata) - - def to_string(self): - """returns this object as a plain string of data""" - - raise NotImplementedError() - - @classmethod - def from_string(cls, string): - """given a plain string, returns an object of this class - - raises MetaDataFileException if a parsing error occurs""" - - raise NotImplementedError() - - def get_track(self, index): - """given a track index (from 0), returns (name, artist, extra) - - name, artist and extra are unicode strings - raises IndexError if out-of-bounds""" - - return self.track_metadata[i] - - def set_track(self, index, name, artist, extra): - """sets the track at the given index (from 0) to the given values - - raises IndexError if out-of-bounds""" - - self.track_metadata[i] = (name, artist, extra) - - @classmethod - def from_tracks(cls, tracks): - """given a list of AudioFile objects, returns an AlbumMetaDataFile - - all files are presumed to be from the same album""" - - raise NotImplementedError() - - @classmethod - def from_cuesheet(cls, cuesheet, total_frames, sample_rate, metadata=None): - """returns an AlbumMetaDataFile from a cuesheet - - this must also include a total_frames and sample_rate integer - this works by generating a set of empty tracks and calling - the from_tracks() method to build a MetaData file with - the proper placeholders - metadata, if present, is applied to all tracks""" - - if (metadata is None): - metadata = MetaData() - - return cls.from_tracks([DummyAudioFile( - length=(pcm_frames * 75) / sample_rate, - metadata=metadata, - track_number=i + 1) for (i, pcm_frames) in enumerate( - cuesheet.pcm_lengths(total_frames))]) - - def track_metadata(self, track_number): - """given a track_number (from 1), returns a MetaData object - - raises IndexError if out-of-bounds or None if track_number is 0""" - - if (track_number == 0): - return None - - (track_name, - track_artist, - track_extra) = self.get_track(track_number - 1) - - if (len(track_artist) == 0): - track_artist = self.artist_name - - return MetaData(track_name=track_name, - track_number=track_number, - track_total=len(self), - album_name=self.album_name, - artist_name=track_artist, - catalog=self.catalog, - year=self.year) - - def get(self, track_index, default): - try: - return self.track_metadata(track_index) - except IndexError: - return default - - def track_metadatas(self): - """iterates over all the MetaData objects in this file""" - - for i in xrange(len(self)): - yield self.track_metadata(i + 1) - - def metadata(self): - """returns a single MetaData object of all consistent fields - - for example, if album_name is the same in all MetaData objects, - the returned object will have that album_name value - if track_name differs, the returned object will not - have a track_name field - """ - - return MetaData(**dict([(field, list(items)[0]) - for (field, items) in - [(field, - set([getattr(track, field) for track - in self.track_metadatas()])) - for field in MetaData.FIELDS] - if (len(items) == 1)])) - ####################### #Image MetaData ####################### @@ -2986,11 +3237,15 @@ 4: "Other"}.get(self.type, "Other") def __repr__(self): - return ("Image(mime_type=%s,width=%s,height=%s,color_depth=%s," + - "color_count=%s,description=%s,type=%s,...)") % \ - (repr(self.mime_type), repr(self.width), repr(self.height), - repr(self.color_depth), repr(self.color_count), - repr(self.description), repr(self.type)) + fields = ["%s=%s" % (attr, getattr(self, attr)) + for attr in ["mime_type", + "width", + "height", + "color_depth", + "color_count", + "description", + "type"]] + return "Image(%s)" % (",".join(fields)) def __unicode__(self): return u"%s (%d\u00D7%d,'%s')" % \ @@ -3010,6 +3265,8 @@ raises InvalidImage if some error occurs during parsing """ + from .image import image_metrics + img = image_metrics(image_data) return Image(data=image_data, @@ -3021,22 +3278,13 @@ description=description, type=type) - def thumbnail(self, width, height, format): - """returns a new Image object with the given attributes - - width and height are integers - format is a string such as "JPEG" - """ - - return Image.new(thumbnail_image(self.data, width, height, format), - self.description, self.type) - def __eq__(self, image): if (image is not None): for attr in ["data", "mime_type", "width", "height", "color_depth", "color_count", "description", "type"]: - if (getattr(self, attr) != getattr(image, attr)): + if ((not hasattr(image, attr)) or (getattr(self, attr) != + getattr(image, attr))): return False else: return True @@ -3046,6 +3294,17 @@ def __ne__(self, image): return not self.__eq__(image) + +class InvalidImage(Exception): + """raised if an image cannot be parsed correctly""" + + def __init__(self, err): + self.err = unicode(err) + + def __unicode__(self): + return self.err + + ####################### #ReplayGain Metadata ####################### @@ -3100,10 +3359,12 @@ self.field = field def error_msg(self, messenger): - messenger.error(_(u"Unknown field \"%s\" in file format") % \ - (self.field)) - messenger.info(_(u"Supported fields are:")) - for field in sorted(MetaData.FIELDS + \ + from .text import (ERR_UNKNOWN_FIELD, + LAB_SUPPORTED_FIELDS) + + messenger.error(ERR_UNKNOWN_FIELD % (self.field,)) + messenger.info(LAB_SUPPORTED_FIELDS) + for field in sorted(MetaData.FIELDS + ("album_track_number", "suffix")): if (field == 'track_number'): messenger.info(u"%(track_number)2.2d") @@ -3113,6 +3374,15 @@ messenger.info(u"%(basename)s") +class InvalidFilenameFormat(Exception): + """raised by AudioFile.track_name() + if its format string contains broken fields""" + + def __unicode__(self): + from .text import ERR_INVALID_FILENAME_FORMAT + return ERR_INVALID_FILENAME_FORMAT + + class AudioFile: """an abstract class representing audio files on disk @@ -3121,6 +3391,7 @@ SUFFIX = "" NAME = "" + DESCRIPTION = u"" DEFAULT_COMPRESSION = "" COMPRESSION_MODES = ("",) COMPRESSION_DESCRIPTIONS = {} @@ -3134,14 +3405,6 @@ self.filename = filename - @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 False - def bits_per_sample(self): """returns an integer number of bits-per-sample this track contains""" @@ -3182,7 +3445,7 @@ if (metadata is not None): raise NotImplementedError() else: - raise ValueError(_(u"metadata not from audio file")) + raise ValueError(ERR_FOREIGN_METADATA) def set_metadata(self, metadata): """takes a MetaData object and sets this track's metadata @@ -3302,18 +3565,17 @@ """returns this track's number as an integer this first checks MetaData and then makes a guess from the filename - if neither yields a good result, returns 0""" + if neither yields a good result, returns None""" metadata = self.get_metadata() - if ((metadata is not None) and (metadata.track_number > 0)): + if (metadata is not None): return metadata.track_number else: + basename = os.path.basename(self.filename) try: - return int(re.findall( - r'\d{2,3}', - os.path.basename(self.filename))[0]) % 100 + return int(re.findall(r'\d{2,3}', basename)[0]) % 100 except IndexError: - return 0 + return None def album_number(self): """returns this track's album number as an integer @@ -3325,13 +3587,12 @@ if (metadata is not None): return metadata.album_number else: + basename = os.path.basename(self.filename) try: - long_track_number = int(re.findall( - r'\d{3}', - os.path.basename(self.filename))[0]) + long_track_number = int(re.findall(r'\d{3}', basename)[0]) return long_track_number / 100 except IndexError: - return 0 + return None @classmethod def track_name(cls, file_path, track_metadata=None, format=None, @@ -3344,8 +3605,12 @@ 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""" + contains invalid template fields + + raises InvalidFilenameFormat if the format string + has broken template fields""" if (format is None): format = FILENAME_FORMAT @@ -3354,22 +3619,29 @@ try: #prefer track_number and album_number from MetaData, if available if (track_metadata is not None): - track_number = track_metadata.track_number - album_number = track_metadata.album_number - track_total = track_metadata.track_total - album_total = track_metadata.album_total + track_number = (track_metadata.track_number + if track_metadata.track_number is not None + else 0) + album_number = (track_metadata.album_number + if track_metadata.album_number is not None + else 0) + track_total = (track_metadata.track_total + if track_metadata.track_total is not None + else 0) + album_total = (track_metadata.album_total + if track_metadata.album_total is not None + else 0) else: + basename = os.path.basename(file_path) try: - track_number = int(re.findall( - r'\d{2,4}', - os.path.basename(file_path))[0]) % 100 + track_number = int(re.findall(r'\d{2,4}', + basename)[0]) % 100 except IndexError: track_number = 0 try: - album_number = int(re.findall( - r'\d{2,4}', - os.path.basename(file_path))[0]) / 100 + album_number = int(re.findall(r'\d{2,4}', + basename)[0]) / 100 except IndexError: album_number = 0 @@ -3394,11 +3666,16 @@ if (track_metadata is not None): for field in track_metadata.FIELDS: - if ((field != "suffix") and - (field not in MetaData.INTEGER_FIELDS)): - format_dict[field.decode('ascii')] = getattr( - track_metadata, - field).replace(u'/', u'-').replace(unichr(0), u' ') + if ((field != "suffix") and (field not in + MetaData.INTEGER_FIELDS)): + if (getattr(track_metadata, field) is not None): + format_dict[field.decode('ascii')] = getattr( + track_metadata, + field).replace(u'/', + u'-').replace(unichr(0), + u' ') + else: + format_dict[field.decode('ascii')] = u"" else: for field in MetaData.FIELDS: if (field not in MetaData.INTEGER_FIELDS): @@ -3412,22 +3689,31 @@ FS_ENCODING, 'replace') except KeyError, error: raise UnsupportedTracknameField(unicode(error.args[0])) + except TypeError: + raise InvalidFilenameFormat() + except ValueError: + raise InvalidFilenameFormat() + + @classmethod + def supports_replay_gain(cls): + """returns True if this class supports ReplayGain""" + + 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 """ - track_names = [track.filename for track in - open_files(filenames) if - isinstance(track, cls)] + return @classmethod - def can_add_replay_gain(cls): - """returns True if we have the necessary binaries to add ReplayGain""" + def can_add_replay_gain(cls, audiofiles): + """given a list of audiofiles, + returns True if this class can add ReplayGain to those files + returns False if not""" return False @@ -3438,7 +3724,7 @@ for example, if it is applied by adding metadata tags rather than altering the file's data itself""" - return True + return False def replay_gain(self): """returns a ReplayGain object of our ReplayGain values @@ -3490,12 +3776,12 @@ decoder = self.to_pcm() pcm_frame_count = 0 try: - framelist = decoder.read(BUFFER_SIZE) + framelist = decoder.read(FRAMELIST_SIZE) while (len(framelist) > 0): pcm_frame_count += framelist.frames if (progress is not None): progress(pcm_frame_count, total_frames) - framelist = decoder.read(BUFFER_SIZE) + framelist = decoder.read(FRAMELIST_SIZE) except (IOError, ValueError), err: raise InvalidFile(str(err)) @@ -3515,9 +3801,11 @@ checks the __system_binaries__ class for which path to check""" - return set([True] + \ - [system_binaries.can_execute(system_binaries[command]) - for command in cls.BINARIES]) == set([True]) + for command in cls.BINARIES: + if (not system_binaries.can_execute(system_binaries[command])): + return False + else: + return True def clean(self, fixes_performed, output_filename=None): """cleans the file of known data and metadata problems @@ -3556,54 +3844,44 @@ class WaveContainer(AudioFile): - """an audio type which supports storing foreign RIFF chunks - - these chunks must be preserved during a round-trip: + def has_foreign_wave_chunks(self): + """returns True if the file has RIFF chunks + other than 'fmt ' and 'data' + which must be preserved during conversion""" - >>> WaveContainer("file", "input.wav").to_wave("output.wav") - """ - - def to_wave(self, wave_filename, progress=None): - """writes the contents of this file to the given .wav filename string + raise NotImplementedError() - raises EncodingError if some error occurs during decoding""" + def wave_header_footer(self): + """returns (header, footer) tuple of strings + containing all data before and after the PCM stream + + may raise ValueError if there's a problem with + the header or footer data + may raise IOError if there's a problem reading + header or footer data from the file + """ - pcmreader = to_pcm_progress(self, progress) - WaveAudio.from_pcm(wave_filename, pcmreader) - pcmreader.close() + raise NotImplementedError() @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 AudioFile compatible object + def from_wave(cls, filename, header, pcmreader, footer, compression=None): + """encodes a new file from wave data - for example, to encode FlacAudio file "flac.flac" from "file.wav" - at compression level "5": + takes a filename string, header string, + PCMReader object, footer string + 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 - >>> flac = FlacAudio.from_wave("file.flac","file.wav","5") - """ + header + pcm data + footer should always result + in the original wave file being restored + without need for any padding bytes - return cls.from_pcm(filename, - to_pcm_progress(WaveAudio(wave_filename), - progress), - compression) - - 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""" + may raise EncodingError if some problem occurs when + encoding the input file""" - return False + raise NotImplementedError() def convert(self, target_path, target_class, compression=None, progress=None): @@ -3614,78 +3892,65 @@ the resulting object may raise EncodingError if some problem occurs during encoding""" - import tempfile - - if (target_class == WaveAudio): - self.to_wave(target_path, progress=progress) - return WaveAudio(target_path) - elif (self.has_foreign_riff_chunks() and - hasattr(target_class, "from_wave")): - temp_wave = tempfile.NamedTemporaryFile(suffix=".wav") + if ((self.has_foreign_wave_chunks() and + hasattr(target_class, "from_wave") and + callable(target_class.from_wave))): + #transfer header and footer when performing PCM conversion try: - #we'll only log the second leg of conversion, - #since that's likely to be the slower portion - self.to_wave(temp_wave.name) - return target_class.from_wave(target_path, - temp_wave.name, - compression, - progress=progress) - finally: - temp_wave.close() + (header, footer) = self.wave_header_footer() + except (ValueError, IOError), err: + raise EncodingError(unicode(err)) + + return target_class.from_wave(target_path, + header, + to_pcm_progress(self, progress), + footer, + compression) else: + #perform standard PCM conversion instead return target_class.from_pcm(target_path, to_pcm_progress(self, progress), compression) class AiffContainer(AudioFile): - """an audio type which supports storing foreign AIFF chunks - - these chunks must be preserved during a round-trip: - - >>> AiffContainer("file", "input.aiff").to_aiff("output.aiff") - """ + def has_foreign_aiff_chunks(self): + """returns True if the file has AIFF chunks + other than 'COMM' and 'SSND' + which must be preserved during conversion""" - def to_aiff(self, aiff_filename, progress=None): - """writes the contents of this file to the given .aiff filename string + raise NotImplementedError() - raises EncodingError if some error occurs during decoding""" + def aiff_header_footer(self): + """returns (header, footer) tuple of strings + containing all data before and after the PCM stream + + may raise ValueError if there's a problem with + the header or footer data + may raise IOError if there's a problem reading + header or footer data from the file""" - pcmreader = to_pcm_progress(self, progress) - AiffAudio.from_pcm(aiff_filename, pcmreader) - pcmreader.close() + raise NotImplementedError() @classmethod - def from_aiff(cls, filename, aiff_filename, compression=None, - progress=None): - """encodes a new AudioFile from an existing .aiff file - - takes a filename string, aiff_filename string - of an existing AiffAudio 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 AudioFile compatible object - - for example, to encode FlacAudio file "flac.flac" from "file.aiff" - at compression level "5": - - >>> flac = FlacAudio.from_wave("file.flac","file.aiff","5") - """ + def from_aiff(cls, filename, header, pcmreader, footer, compression=None): + """encodes a new file from AIFF data - return cls.from_pcm(filename, - to_pcm_progress(AiffAudio(wave_filename)), - compression) + takes a filename string, header string, + PCMReader object, footer string + 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 AiffAudio object - def has_foreign_aiff_chunks(self): - """returns True if the audio file contains non-audio AIFF chunks + header + pcm data + footer should always result + in the original AIFF file being restored + without need for any padding bytes - during transcoding, if the source audio file has foreign AIFF chunks - and the target audio format supports foreign AIFF chunks, - conversion should be routed through .aiff conversion - to avoid losing those chunks""" + may raise EncodingError if some problem occurs when + encoding the input file""" - return False + raise NotImplementedError() def convert(self, target_path, target_class, compression=None, progress=None): @@ -3696,20 +3961,23 @@ the resulting object may raise EncodingError if some problem occurs during encoding""" - if (target_class == AiffAudio): - self.to_aiff(target_path) - return AiffAudio(target_path) - elif (self.has_foreign_aiff_chunks() and - hasattr(target_class, "from_aiff")): - temp_aiff = tempfile.NamedTemporaryFile(suffix=".aiff") + if ((self.has_foreign_aiff_chunks() and + hasattr(target_class, "from_aiff") and + callable(target_class.from_aiff))): + #transfer header and footer when performing PCM conversion + try: - self.to_aiff(temp_aiff.name) - return target_class.from_aiff(target_path, - temp_aiff.name, - compression) - finally: - temp_aiff.close() + (header, footer) = self.aiff_header_footer() + except (ValueError, IOError), err: + raise EncodingError(unicode(err)) + + return target_class.from_aiff(target_path, + header, + to_pcm_progress(self, progress), + footer, + compression) else: + #perform standard PCM conversion instead return target_class.from_pcm(target_path, to_pcm_progress(self, progress), compression) @@ -3813,42 +4081,6 @@ return "%2.2d:%2.2d:%2.2d" % ((i / 75) / 60, (i / 75) % 60, i % 75) -def sheet_to_unicode(sheet, total_frames): - """returns formatted unicode from a cuesheet object and total PCM frames - - its output is pretty-printed for eventual display by trackinfo - """ - - #FIXME? - This (and pcm_lengths() in general) assumes all cuesheets - #have a sample rate of 44100Hz. - #It's difficult to envision a scenario - #in which this assumption doesn't hold - #The point of cuesheets is to manage disc-based data as - #"solid" archives which can be rewritten identically to the original - #yet this only works for CD audio, which must always be 44100Hz. - #DVD-Audio is encoded into AOB files which cannot be mapped to cuesheets - #and SACD's DSD format is beyond the scope of these PCM-centric tools. - - ISRCs = sheet.ISRCs() - - tracks = unicode(os.linesep).join( - [" Track %2.2d - %2.2d:%2.2d%s" % \ - (i + 1, - length / 44100 / 60, - length / 44100 % 60, - (" (ISRC %s)" % (ISRCs[i + 1].decode('ascii', 'replace'))) if - ((i + 1) in ISRCs.keys()) else u"") - for (i, length) in enumerate(sheet.pcm_lengths(total_frames))]) - - if ((sheet.catalog() is not None) and - (len(sheet.catalog()) > 0)): - return u" Catalog - %s%s%s" % \ - (sheet.catalog().decode('ascii', 'replace'), - os.linesep, tracks) - else: - return tracks - - def at_a_time(total, per): """yields "per" integers from "total" until exhausted @@ -3909,27 +4141,6 @@ return -from __image__ import * - -from __wav__ import * - -from __au__ import * -from __ogg__ import * -from __vorbiscomment__ import * -from __id3__ import * -from __aiff__ import * -from __flac__ import * - -from __ape__ import * -from __mp3__ import * -from __vorbis__ import * -from __m4a__ import * -from __wavpack__ import * -from __shn__ import * - -from __dvda__ import * - - ####################### #CD data ####################### @@ -4129,13 +4340,13 @@ self.initial_offset = initial_offset self.pcm_frames = pcm_frames - def read(self, bytes): + def read(self, pcm_frames): if (self.pcm_frames > 0): if (self.initial_offset == 0): #once the initial offset is accounted for, #read a framelist from the pcmreader - framelist = self.pcmreader.read(bytes) + framelist = self.pcmreader.read(pcm_frames) if (framelist.frames <= self.pcm_frames): if (framelist.frames > 0): #return framelist if it has data @@ -4163,10 +4374,10 @@ #remove frames if initial offset is positive #if initial_offset is large, read as many framelists as needed - framelist = self.pcmreader.read(bytes) + framelist = self.pcmreader.read(pcm_frames) while (self.initial_offset > framelist.frames): self.initial_offset -= framelist.frames - framelist = self.pcmreader.read(bytes) + framelist = self.pcmreader.read(pcm_frames) (removed, framelist) = framelist.split(self.initial_offset) self.initial_offset -= removed.frames @@ -4181,7 +4392,7 @@ else: #if the entire framelist is cropped, #return another one entirely - return self.read(bytes) + return self.read(pcm_frames) elif (self.initial_offset < 0): #pad framelist with 0s if initial offset is negative framelist = pcm.from_list([0] * @@ -4198,8 +4409,12 @@ return pcm.FrameList("", self.channels, self.bits_per_sample, False, True) + def read_closed(self, pcm_frames): + raise ValueError() + def close(self): self.pcmreader.close() + self.read = self.read_closed class CDTrackLog(dict): @@ -4221,17 +4436,17 @@ #log format is similar to cdda2wav's def __str__(self): + fields = {"edge": self.get(2, 0), + "atom": self.get(3, 0), + "skip": self.get(6, 0), + "drift": self.get(7, 0), + "drop": self.get(10, 0), + "dup": self.get(11, 0), + "rderr": self.get(12, 0)} return ", ".join(["%%(%s)d %s" % (field, field) for field in ("rderr", "skip", "atom", "edge", - "drop", "dup", "drift")]) % \ - {"edge": self.get(2, 0), - "atom": self.get(3, 0), - "skip": self.get(6, 0), - "drift": self.get(7, 0), - "drop": self.get(10, 0), - "dup": self.get(11, 0), - "rderr": self.get(12, 0)} + "drop", "dup", "drift")]) % fields class CDTrackReader(PCMReader): @@ -4297,21 +4512,23 @@ else: return pcm.from_list([], 2, 16, True) - def read(self, bytes): - """try to read a pcm.FrameList of size 'bytes' + def read(self, pcm_frames): + """try to read a pcm.FrameList with the given number of PCM frames for CD reading, this will be a sector-aligned number""" - #returns a sector-aligned number of bytes - #(divisible by 2352 bytes, basically) - #or at least 1 sector's worth, if 'bytes' is too small - return self.__read_sectors__(max(bytes / 2352, 1)) + #returns a sector-aligned number of PCM frames + #(divisible by 588 frames, basically) + #or at least 1 sector's worth, if "pcm_frames" is too small + return self.__read_sectors__(max(pcm_frames / 588, 1)) + + def read_closed(self, pcm_frames): + raise ValueError() def close(self): """closes the CD track for reading""" - self.position = self.start - self.cursor_placed = False + self.read = self.read_closed class CDTrackReaderAccurateRipCRC: @@ -4350,15 +4567,15 @@ def log(self): return self.cdtrackreader.log() - def read(self, bytes): - frame = self.cdtrackreader.read(bytes) + def read(self, pcm_frames): + frame = self.cdtrackreader.read(pcm_frames) crc_frame = frame if (self.prefix_0s > 0): #substitute frame samples for prefix 0s (substitute, remainder) = crc_frame.split(self.prefix_0s) - self.accuraterip_crc.update(pcm.from_list( - [0] * len(substitute), 2, 16, True)) + self.accuraterip_crc.update( + pcm.from_list([0] * len(substitute), 2, 16, True)) self.prefix_0s -= substitute.frames crc_frame = remainder @@ -4367,8 +4584,8 @@ (remainder, substitute) = crc_frame.split(self.checksum_window) self.checksum_window -= remainder.frames self.accuraterip_crc.update(remainder) - self.accuraterip_crc.update(pcm.from_list( - [0] * len(substitute), 2, 16, True)) + self.accuraterip_crc.update( + pcm.from_list([0] * len(substitute), 2, 16, True)) else: self.checksum_window -= crc_frame.frames self.accuraterip_crc.update(crc_frame) @@ -4405,37 +4622,6 @@ __most_numerous__ = most_numerous -from __freedb__ import * -from __musicbrainz__ import * -from __accuraterip__ import * - - -def read_metadata_file(filename): - """returns an AlbumMetaDataFile-compatible file from a filename string - - raises a MetaDataFileException exception if an error occurs - during reading - """ - - try: - data = file(filename, 'rb').read() - except IOError, msg: - raise MetaDataFileException(str(msg)) - - #try XMCD first - try: - return XMCD.from_string(data) - except XMCDException: - pass - - #then try MusicBrainz - try: - return MusicBrainzReleaseXML.from_string(data) - except MBXMLException: - pass - - #otherwise, throw exception - raise MetaDataFileException(filename) ####################### #CD MetaData Lookup @@ -4478,7 +4664,8 @@ from urllib2 import HTTPError from xml.parsers.expat import ExpatError try: - matches.extend(musicbrainz.perform_lookup( + matches.extend( + musicbrainz.perform_lookup( first_track_number=first_track_number, last_track_number=last_track_number, lead_out_offset=lead_out_offset, @@ -4492,7 +4679,8 @@ from . import freedb from urllib2 import HTTPError try: - matches.extend(freedb.perform_lookup( + matches.extend( + freedb.perform_lookup( offsets=offsets, total_length=total_length, track_count=track_count, @@ -4510,6 +4698,57 @@ return matches +def track_metadata_lookup(audiofiles, + musicbrainz_server="musicbrainz.org", + musicbrainz_port=80, + freedb_server="us.freedb.org", + freedb_port=80, + use_musicbrainz=True, + use_freedb=True): + """given a list of AudioFile objects, + this treats them as a single CD + and generates a set of MetaData objects pulled from lookup services + + returns a metadata[c][t] list of lists + where 'c' is a possible choice + and 't' is the MetaData for a given track (starting from 0) + + this will always return at least one choice, + which may be a list of largely empty MetaData objects + if no match can be found for the CD + """ + + audiofiles.sort(lambda x, y: cmp(x.track_number(), y.track_number())) + track_frames = [f.cd_frames() for f in audiofiles] + track_numbers = [f.track_number() for f in audiofiles] + + return metadata_lookup( + first_track_number=(min(track_numbers) + if None not in track_numbers else 1), + last_track_number=(max(track_numbers) + if None not in track_numbers else + len(track_numbers)), + offsets=[150 + sum(track_frames[0:i]) for i in + xrange(len(track_frames))], + lead_out_offset=150 + sum(track_frames), + total_length=sum(track_frames) - 1, + musicbrainz_server=musicbrainz_server, + musicbrainz_port=musicbrainz_port, + freedb_server=freedb_server, + freedb_port=freedb_port, + use_musicbrainz=use_musicbrainz, + use_freedb=use_freedb) + + +####################### +#DVD-Audio Discs +####################### + + +from .dvda import DVDAudio +from .dvda import InvalidDVDA + + ####################### #Multiple Jobs Handling ####################### @@ -4532,11 +4771,14 @@ if (pid > 0): # parent return pid else: # child - if (kwargs is not None): - function(*args, **kwargs) - else: - function(*args) - sys.exit(0) + try: + if (kwargs is not None): + function(*args, **kwargs) + else: + function(*args) + finally: + #avoid calling Python cleanup handlers + os._exit(0) def run(self, max_processes=1): """performs the queued functions in separate subprocesses @@ -4559,7 +4801,6 @@ #as processes end, keep adding new ones to the pool #until we run out of queued jobs - while (len(self.todo) > 0): try: (pid, return_value) = os.waitpid(0, 0) @@ -4602,6 +4843,8 @@ returns a (pid, reader) tuple where pid is an int of the child job and reader is a file object containing its piped data""" + import cPickle + (pipe_read, pipe_write) = os.pipe() pid = os.fork() if (pid > 0): # parent @@ -4611,11 +4854,14 @@ else: # child os.close(pipe_read) writer = os.fdopen(pipe_write, 'w') - if (kwargs is not None): - cPickle.dump(function(*args, **kwargs), writer) - else: - cPickle.dump(function(*args), writer) - sys.exit(0) + try: + if (kwargs is not None): + cPickle.dump(function(*args, **kwargs), writer) + else: + cPickle.dump(function(*args), writer) + finally: + #avoid calling Python cleanup handlers + os._exit(0) def __add_job__(self): """removes a queued function and adds it to our running pool""" @@ -4635,6 +4881,7 @@ are added to our "return_values" attribute""" import select + import cPickle (readable, writeable, @@ -4676,8 +4923,15 @@ yield result -class ProgressJobQueueComplete(Exception): - pass +def output_progress(u, current, total): + """given a unicode string and current/total integers, + returns a u'[<current>/<total>] <string> unicode string + indicating the current progress""" + + if (total > 1): + return u"[%%%d.d/%%d] %%s" % (len(str(total))) % (current, total, u) + else: + return u class ExecProgressQueue: @@ -4692,6 +4946,7 @@ self.running_job_pool = {} self.results = {} self.exception = None + self.completed_job_number = 0 def execute(self, function, progress_text=None, @@ -4699,6 +4954,12 @@ *args, **kwargs): """queues the given function and arguments to be run in parallel + function must have an additional "progress" argument + not present in "*args" or "**kwargs" which is called + with (current, total) integer arguments by the function + on a regular basis to update its progress + similar to: function(*args, progress=prog(current, total), **kwargs) + progress_text should be a unicode string to be displayed while running completion_output is either a unicode string, @@ -4753,16 +5014,24 @@ #remove job from progress display self.progress_display.delete_row(job_id) + #increment finished job number for X/Y display + self.completed_job_number += 1 + #display job's output message completion_output = job.completion_output if (completion_output is not None): if (callable(completion_output)): output = completion_output(value) if (output is not None): - self.progress_display.messenger.info(unicode(output)) + self.progress_display.messenger.info( + output_progress(unicode(output), + self.completed_job_number, + self.max_job_id)) else: self.progress_display.messenger.info( - unicode(completion_output)) + output_progress(unicode(completion_output), + self.completed_job_number, + self.max_job_id)) else: #job raised an exception if (self.exception is None): @@ -4814,9 +5083,13 @@ #wait some amount of time before polling job pool again time.sleep(0.25) + self.max_job_id = 0 + self.completed_job_number = 0 + if (self.exception is not None): raise self.exception + class __ProgressQueueJob__: def __init__(self, pid, progress, result, completion_output): """pid is the child process's PID @@ -4846,6 +5119,7 @@ import mmap import struct + import cPickle progress = mmap.mmap(-1, 16) # 2, 64-bit fields of progress data (r3, w3) = os.pipe() # for sending final result from child->parent @@ -4861,10 +5135,11 @@ os.close(r3) try: - result = (True, function(*args, - progress= - __PollingProgress__(progress).progress, - **kwargs)) + function_result = function( + *args, + progress=__PollingProgress__(progress).progress, + **kwargs) + result = (True, function_result) except Exception, exception: result = (False, exception) @@ -4873,7 +5148,8 @@ result_pipe.flush() result_pipe.close() progress.close() - sys.exit(0) + #avoid calling Python cleanup handlers + os._exit(0) def is_completed(self): """returns True if the job is completed @@ -4881,6 +5157,8 @@ with the function's return value and the child process will be disposed of""" + import cPickle + if (os.waitpid(self.__pid__, os.WNOHANG) != (0, 0)): try: self.result = cPickle.load(self.__result__) @@ -4916,11 +5194,30 @@ self.memory.seek(0, 0) self.memory.write(struct.pack(">QQ", current, total)) - -#***ApeAudio temporarily removed*** -#Without a legal alternative to mac-port, I shall have to re-implement -#Monkey's Audio with my own code in order to make it available again. -#Yet another reason to avoid that unpleasant file format... +from .au import AuAudio +from .wav import WaveAudio +from .aiff import AiffAudio +from .flac import FlacAudio +from .flac import OggFlacAudio +from .wavpack import WavPackAudio +from .shn import ShortenAudio +from .mp3 import MP3Audio +from .mp3 import MP2Audio +from .vorbis import VorbisAudio +from .m4a import M4AAudio +from .m4a import ALACAudio +from .opus import OpusAudio + +from .ape import ApeTag +from .flac import FlacMetaData +from .id3 import ID3CommentPair +from .id3v1 import ID3v1Comment +from .id3 import ID3v22Comment +from .id3 import ID3v23Comment +from .id3 import ID3v24Comment +from .m4a_atoms import M4A_META_Atom +from .vorbiscomment import VorbisComment +from .opus import OpusTags AVAILABLE_TYPES = (FlacAudio, OggFlacAudio, @@ -4933,7 +5230,8 @@ M4AAudio, ALACAudio, WavPackAudio, - ShortenAudio) + ShortenAudio, + OpusAudio) TYPE_MAP = dict([(track_type.NAME, track_type) for track_type in AVAILABLE_TYPES
View file
audiotools-2.19.tar.gz/audiotools/accuraterip.py
Added
@@ -0,0 +1,180 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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 . import DiscID + + +class AccurateRipDiscID: + def __init__(self, offsets): + """offsets is a list of CD track offsets, in CD sectors + + these offsets *do not* include the 150 sector lead-in""" + + self.__offsets__ = offsets + + def track_count(self): + return len(self.__offsets__) - 1 + + def id1(self): + return sum(self.__offsets__) + + def id2(self): + return sum([max(offset, 1) * (i + 1) + for (i, offset) in enumerate(self.__offsets__)]) + + def id3(self): + return DiscID( + tracks=[y - x for (x, y) in zip(self.__offsets__, + self.__offsets__[1:])], + offsets=[offset + 150 for offset in self.__offsets__[0:-1]], + length=self.__offsets__[-1]) + + @classmethod + def from_cdda(cls, cdda): + return cls([cdda.cdda.track_offsets(i)[0] for i in + xrange(1, len(cdda) + 2)]) + + @classmethod + def from_tracks(cls, tracks): + offsets = [0] + for track in tracks: + offsets.append(offsets[-1] + track.cd_frames()) + + return cls(offsets) + + def db_filename(self): + return ("dBAR-%(tracks)3.3d-%(id1)8.8x-%(id2)8.8x-%(id3)s.bin" % + {"tracks": self.track_count(), + "id1": self.id1(), + "id2": self.id2(), + "id3": self.id3()}) + + def url(self): + id1 = self.id1() + + return ("http://www.accuraterip.com/accuraterip/%.1x/%.1x/%.1x/%s" % + (id1 & 0xF, + (id1 >> 4) & 0xF, + (id1 >> 8) & 0xF, + self.db_filename())) + + def __repr__(self): + return "AccurateRipDiscID(%s)" % (repr(self.__offsets__)) + + +class AccurateRipEntry: + # ACCURATERIP_DB_ENTRY = Con.GreedyRepeater( + # Con.Struct("db_entry", + # Con.ULInt8("track_count"), + # Con.ULInt32("disc_id1"), + # Con.ULInt32("disc_id2"), + # Con.ULInt32("freedb_id"), + # Con.StrictRepeater(lambda ctx: ctx["track_count"], + # Con.Struct("tracks", + # Con.ULInt8("confidence"), + # Con.ULInt32("crc"), + # Con.ULInt32("crc2"))))) + + def __init__(self, disc_id1, disc_id2, freedb_id, track_entries): + """disc_id1, disc_id2 and freedb_id are ints + + track_entries is a list of lists of AccurateRipTrackEntry objects""" + + self.disc_id1 = disc_id1 + self.disc_id2 = disc_id2 + self.freedb_id = freedb_id + self.track_entries = track_entries + + def __repr__(self): + return "AccurateRipEntry(%s, %s, %s, %s)" % \ + (repr(self.disc_id1), + repr(self.disc_id2), + repr(self.freedb_id), + repr(self.track_entries)) + + def __getitem__(self, key): + #returns a list of 0 or more AccurateRipTrackEntry objects + return self.track_entries[key] + + def __len__(self): + return len(self.track_entries) + + @classmethod + def parse_string(cls, string): + """given a string, returns an AccurateRipEntry object""" + + entries = cls.ACCURATERIP_DB_ENTRY.parse(string) + + if (len(entries) == 0): + raise ValueError("no AccurateRip entries found") + + #all disc IDs should be identical + #and zip the track entries together + return cls( + disc_id1=entries[0].disc_id1, + disc_id2=entries[0].disc_id2, + freedb_id=entries[0].freedb_id, + track_entries=[ + [AccurateRipTrackEntry(confidence=track.confidence, + crc=track.crc, + crc2=track.crc2) for track in tracks] + for tracks in zip(*[entry.tracks for entry in entries])]) + + @classmethod + def from_disc_id(cls, disc_id): + """given an AccurateRipDiscID, returns an AccurateRipEntry + or None if the given disc ID has no matches in the database""" + + import urllib + + response = urllib.urlopen(disc_id.url()) + if (response.getcode() == 200): + return cls.parse_string(response.read()) + else: + return None + + +class AccurateRipTrackEntry: + def __init__(self, confidence, crc, crc2): + self.confidence = confidence + self.crc = crc + self.crc2 = crc2 + + def __repr__(self): + return "AccurateRipTrackEntry(%s, %s, %s)" % \ + (self.confidence, + self.crc, + self.crc2) + + +class AccurateRipTrackCRC: + def __init__(self): + self.crc = 0 + self.track_index = 1 + from .cdio import accuraterip_crc + self.accuraterip_crc = accuraterip_crc + + def __int__(self): + return self.crc + + def update(self, frame): + (self.crc, self.track_index) = self.accuraterip_crc(self.crc, + self.track_index, + frame)
View file
audiotools-2.19.tar.gz/audiotools/aiff.py
Added
@@ -0,0 +1,1086 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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 (InvalidFile, PCMReader, AiffContainer) +from .pcm import FrameList +import struct + + +def parse_ieee_extended(bitstream): + """returns a parsed 80-bit IEEE extended value from BitstreamReader + this is used to handle AIFF's sample rate field""" + + (signed, exponent, mantissa) = bitstream.parse("1u 15u 64U") + if ((exponent == 0) and (mantissa == 0)): + return 0 + elif (exponent == 0x7FFF): + return 1.79769313486231e+308 + else: + f = mantissa * (2.0 ** (exponent - 16383 - 63)) + return f if not signed else -f + + +def build_ieee_extended(bitstream, value): + """writes an 80-bit IEEE extended value to BitstreamWriter + this is used to handle AIFF's sample rate field""" + + from math import frexp + + if (value < 0): + signed = 1 + value = abs(value) + else: + signed = 0 + + (fmant, exponent) = frexp(value) + if ((exponent > 16384) or (fmant >= 1)): + exponent = 0x7FFF + mantissa = 0 + else: + exponent += 16382 + mantissa = fmant * (2 ** 64) + + bitstream.build("1u 15u 64U", (signed, exponent, mantissa)) + + +def pad_data(pcm_frames, channels, bits_per_sample): + """returns True if the given stream combination + requires an extra padding byte at the end of the 'data' chunk""" + + return (pcm_frames * channels * (bits_per_sample / 8)) % 2 + + +def validate_header(header): + """given header string as returned by aiff_header_footer() + returns (total size, ssnd size) + where total size is the size of the file in bytes + and ssnd size is the size of the SSND chunk in bytes + (including the 8 prefix bytes in the chunk + but *not* including any padding byte at the end) + + the size of the SSND chunk and of the total file should be validated + after the file has been completely written + such that len(header) + len(SSND chunk) + len(footer) = total size + + raises ValueError if the header is invalid""" + + import cStringIO + from .bitstream import BitstreamReader + + header_size = len(header) + aiff_file = BitstreamReader(cStringIO.StringIO(header), 0) + try: + #ensure header starts with FORM<size>AIFF chunk + (form, remaining_size, aiff) = aiff_file.parse("4b 32u 4b") + if (form != "FORM"): + from .text import ERR_AIFF_NOT_AIFF + raise ValueError(ERR_AIFF_NOT_AIFF) + elif (aiff != "AIFF"): + from .text import ERR_AIFF_INVALID_AIFF + raise ValueError(ERR_AIFF_INVALID_AIFF) + else: + total_size = remaining_size + 8 + header_size -= 12 + + comm_found = False + + while (header_size > 0): + #ensure each chunk header is valid + (chunk_id, chunk_size) = aiff_file.parse("4b 32u") + if (not frozenset(chunk_id).issubset(AiffAudio.PRINTABLE_ASCII)): + from .text import ERR_AIFF_INVALID_CHUNK + raise ValueError(ERR_AIFF_INVALID_CHUNK) + else: + header_size -= 8 + + if (chunk_id == "COMM"): + if (not comm_found): + #skip COMM chunk when found + comm_found = True + if (chunk_size % 2): + aiff_file.skip_bytes(chunk_size + 1) + header_size -= (chunk_size + 1) + else: + aiff_file.skip_bytes(chunk_size) + header_size -= chunk_size + else: + #ensure only one COMM chunk is found + from .text import ERR_AIFF_MULTIPLE_COMM_CHUNKS + raise ValueError(ERR_AIFF_MULTIPLE_COMM_CHUNKS) + elif (chunk_id == "SSND"): + if (not comm_found): + #ensure at least one COMM chunk is found + from .text import ERR_AIFF_PREMATURE_SSND_CHUNK + raise ValueError(ERR_AIFF_PREMATURE_SSND_CHUNK) + elif (header_size > 8): + #ensure exactly 8 bytes remain after SSND chunk header + from .text import ERR_AIFF_HEADER_EXTRA_SSND + raise ValueError(ERR_AIFF_HEADER_EXTRA_SSND) + elif (header_size < 8): + from .text import ERR_AIFF_HEADER_MISSING_SSND + raise ValueError(ERR_AIFF_HEADER_MISSING_SSND) + else: + return (total_size, chunk_size - 8) + else: + #skip the full contents of non-audio chunks + if (chunk_size % 2): + aiff_file.skip_bytes(chunk_size + 1) + header_size -= (chunk_size + 1) + else: + aiff_file.skip_bytes(chunk_size) + header_size -= chunk_size + else: + #header parsed with no SSND chunks found + from .text import ERR_AIFF_NO_SSND_CHUNK + raise ValueError(ERR_AIFF_NO_SSND_CHUNK) + except IOError: + from .text import ERR_AIFF_HEADER_IOERROR + raise ValueError(ERR_AIFF_HEADER_IOERROR) + + +def validate_footer(footer, ssnd_bytes_written): + """given a footer string as returned by aiff_header_footer() + and PCM stream parameters, returns True if the footer is valid + + raises ValueError is the footer is invalid""" + + import cStringIO + from .bitstream import BitstreamReader + + total_size = len(footer) + aiff_file = BitstreamReader(cStringIO.StringIO(footer), 0) + try: + #ensure footer is padded properly if necessary + #based on size of data bytes written + if (ssnd_bytes_written % 2): + aiff_file.skip_bytes(1) + total_size -= 1 + + while (total_size > 0): + (chunk_id, chunk_size) = aiff_file.parse("4b 32u") + if (not frozenset(chunk_id).issubset(AiffAudio.PRINTABLE_ASCII)): + from .text import ERR_AIFF_INVALID_CHUNK + raise ValueError(ERR_AIFF_INVALID_CHUNK) + else: + total_size -= 8 + + if (chunk_id == "COMM"): + #ensure no COMM chunks are found + from .text import ERR_AIFF_MULTIPLE_COMM_CHUNKS + raise ValueError(ERR_AIFF_MULTIPLE_COMM_CHUNKS) + elif (chunk_id == "SSND"): + #ensure no SSND chunks are found + from .text import ERR_AIFF_MULTIPLE_SSND_CHUNKS + raise ValueError(ERR_AIFF_MULTIPLE_SSND_CHUNKS) + else: + #skip the full contents of non-audio chunks + if (chunk_size % 2): + aiff_file.skip_bytes(chunk_size + 1) + total_size -= (chunk_size + 1) + else: + aiff_file.skip_bytes(chunk_size) + total_size -= chunk_size + else: + return True + except IOError: + from .text import ERR_AIFF_FOOTER_IOERROR + raise ValueError(ERR_AIFF_FOOTER_IOERROR) + + +####################### +#AIFF +####################### + + +class AIFF_Chunk: + """a raw chunk of AIFF data""" + + def __init__(self, chunk_id, chunk_size, chunk_data): + """chunk_id should be a binary string of ASCII + chunk_size is the length of chunk_data + chunk_data should be a binary string of chunk data""" + + #FIXME - check chunk_id's validity + + self.id = chunk_id + self.__size__ = chunk_size + self.__data__ = chunk_data + + def __repr__(self): + return "AIFF_Chunk(%s)" % (repr(self.id)) + + def size(self): + """returns size of chunk in bytes + not including any spacer byte for odd-sized chunks""" + + return self.__size__ + + def total_size(self): + """returns the total size of the chunk + including the 8 byte ID/size and any padding byte""" + + if (self.__size__ % 2): + return 8 + self.__size__ + 1 + else: + return 8 + self.__size__ + + def data(self): + """returns chunk data as file-like object""" + + import cStringIO + + return cStringIO.StringIO(self.__data__) + + def verify(self): + """returns True if chunk size matches chunk's data""" + + return self.__size__ == len(self.__data__) + + def write(self, f): + """writes the entire chunk to the given output file object + returns size of entire chunk (including header and spacer) + in bytes""" + + f.write(self.id) + f.write(struct.pack(">I", self.__size__)) + f.write(self.__data__) + if (self.__size__ % 2): + f.write(chr(0)) + return self.total_size() + + +class AIFF_File_Chunk(AIFF_Chunk): + """a raw chunk of AIFF data taken from an existing file""" + + def __init__(self, chunk_id, chunk_size, aiff_file, chunk_data_offset): + """chunk_id should be a binary string of ASCII + chunk_size is the size of the chunk in bytes + (not counting any spacer byte) + aiff_file is the file this chunk belongs to + chunk_data_offset is the offset to the chunk's data bytes + (not including the 8 byte header)""" + + self.id = chunk_id + self.__size__ = chunk_size + self.__aiff_file__ = aiff_file + self.__offset__ = chunk_data_offset + + def __repr__(self): + return "AIFF_File_Chunk(%s)" % (repr(self.id)) + + def data(self): + """returns chunk data as file-like object""" + + from . import LimitedFileReader + + self.__wav_file__.seek(self.__offset__) + return LimitedFileReader(self.__wav_file__, self.size()) + + def verify(self): + """returns True if chunk size matches chunk's data""" + + self.__aiff_file__.seek(self.__offset__) + to_read = self.__size__ + while (to_read > 0): + s = self.__aiff_file__.read(min(0x100000, to_read)) + if (len(s) == 0): + return False + else: + to_read -= len(s) + return True + + def write(self, f): + """writes the entire chunk to the given output file object + returns size of entire chunk (including header and spacer) + in bytes""" + + f.write(self.id) + f.write(struct.pack(">I", self.__size__)) + self.__aiff_file__.seek(self.__offset__) + to_write = self.__size__ + while (to_write > 0): + s = self.__aiff_file__.read(min(0x100000, to_write)) + f.write(s) + to_write -= len(s) + + if (self.__size__ % 2): + f.write(chr(0)) + return self.total_size() + + +def parse_comm(comm): + """given a COMM chunk (without the 8 byte name/size header) + returns (channels, total_sample_frames, bits_per_sample, + sample_rate, channel_mask) + where channel_mask is a ChannelMask object and the rest are ints + may raise IOError if an error occurs reading the chunk""" + + from . import ChannelMask + + (channels, + total_sample_frames, + bits_per_sample) = comm.parse("16u 32u 16u") + sample_rate = int(parse_ieee_extended(comm)) + + if (channels <= 2): + channel_mask = ChannelMask.from_channels(channels) + else: + channel_mask = ChannelMask(0) + + return (channels, total_sample_frames, bits_per_sample, + sample_rate, channel_mask) + + +class AiffReader(PCMReader): + """a subclass of PCMReader for reading AIFF file contents""" + + def __init__(self, aiff_file, + sample_rate, channels, channel_mask, bits_per_sample, + total_frames, process=None): + """aiff_file should be a file-like object of aiff data + + sample_rate, channels, channel_mask and bits_per_sample are ints""" + + self.file = aiff_file + self.sample_rate = sample_rate + self.channels = channels + self.channel_mask = channel_mask + self.bits_per_sample = bits_per_sample + self.remaining_frames = total_frames + self.bytes_per_frame = self.channels * self.bits_per_sample / 8 + + self.process = process + + from .bitstream import BitstreamReader + from . import __capped_stream_reader__ + from .text import (ERR_AIFF_NOT_AIFF, + ERR_AIFF_INVALID_AIFF, + ERR_AIFF_NO_SSND_CHUNK) + + #build a capped reader for the ssnd chunk + aiff_reader = BitstreamReader(aiff_file, 0) + try: + (form, aiff) = aiff_reader.parse("4b 32p 4b") + if (form != 'FORM'): + raise InvalidAIFF(ERR_AIFF_NOT_AIFF) + elif (aiff != 'AIFF'): + raise InvalidAIFF(ERR_AIFF_INVALID_AIFF) + + while (True): + (chunk_id, chunk_size) = aiff_reader.parse("4b 32u") + if (chunk_id == 'SSND'): + #adjust for the SSND alignment + aiff_reader.skip(64) + self.aiff = __capped_stream_reader__(self.file, chunk_size) + self.ssnd_chunk_size = chunk_size - 8 + break + else: + aiff_reader.skip_bytes(chunk_size) + if (chunk_size % 2): + aiff_reader.skip(8) + except IOError: + raise InvalidAIFF(ERR_AIFF_NO_SSND_CHUNK) + + def read(self, pcm_frames): + """try to read a pcm.FrameList with the given number of PCM frames""" + + #align bytes downward if an odd number is read in + bytes_per_frame = self.channels * (self.bits_per_sample / 8) + requested_frames = max(1, pcm_frames) + pcm_data = self.aiff.read(requested_frames * bytes_per_frame) + if ((len(pcm_data) == 0) and (self.ssnd_chunk_size > 0)): + from .text import ERR_AIFF_TRUNCATED_SSND_CHUNK + raise IOError(ERR_AIFF_TRUNCATED_SSND_CHUNK) + else: + self.ssnd_chunk_size -= len(pcm_data) + + try: + return FrameList(pcm_data, + self.channels, + self.bits_per_sample, + True, + True) + except ValueError: + from .text import ERR_AIFF_TRUNCATED_SSND_CHUNK + raise IOError(ERR_AIFF_TRUNCATED_SSND_CHUNK) + + def close(self): + """closes the stream for reading + + any subprocess is waited for also so for proper cleanup""" + + self.file.close() + if (self.process is not None): + if (self.process.wait() != 0): + from . import DecodingError + + raise DecodingError() + + +class InvalidAIFF(InvalidFile): + """raised if some problem occurs parsing AIFF chunks""" + + pass + + +class AiffAudio(AiffContainer): + """an AIFF audio file""" + + SUFFIX = "aiff" + NAME = SUFFIX + DESCRIPTION = u"Audio Interchange File Format" + + PRINTABLE_ASCII = frozenset([chr(i) for i in xrange(0x20, 0x7E + 1)]) + + def __init__(self, filename): + """filename is a plain string""" + + from . import ChannelMask + + self.filename = filename + + self.__channels__ = 0 + self.__bits_per_sample__ = 0 + self.__sample_rate__ = 0 + self.__channel_mask__ = ChannelMask(0) + self.__total_sample_frames__ = 0 + + from .bitstream import BitstreamReader + + try: + for chunk in self.chunks(): + if (chunk.id == "COMM"): + try: + (self.__channels__, + self.__total_sample_frames__, + self.__bits_per_sample__, + self.__sample_rate__, + self.__channel_mask__) = parse_comm( + BitstreamReader(chunk.data(), 0)) + break + except IOError: + continue + except IOError: + raise InvalidAIFF("I/O error reading wave") + + 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""" + + return self.__channel_mask__ + + def lossless(self): + """returns True""" + + return True + + def total_frames(self): + """returns the total PCM frames of the track as an integer""" + + return self.__total_sample_frames__ + + def sample_rate(self): + """returns the rate of the track's audio as an integer number of Hz""" + + return self.__sample_rate__ + + def chunks(self): + """yields a AIFF_Chunk compatible objects for each chunk in file""" + + from .text import (ERR_AIFF_NOT_AIFF, + ERR_AIFF_INVALID_AIFF, + ERR_AIFF_INVALID_CHUNK_ID, + ERR_AIFF_INVALID_CHUNK) + + aiff_file = file(self.filename, 'rb') + try: + (form, + total_size, + aiff) = struct.unpack(">4sI4s", aiff_file.read(12)) + except struct.error: + raise InvalidAIFF(ERR_AIFF_INVALID_AIFF) + + if (form != 'FORM'): + raise InvalidAIFF(ERR_AIFF_NOT_AIFF) + elif (aiff != 'AIFF'): + raise InvalidAIFF(ERR_AIFF_INVALID_AIFF) + else: + total_size -= 4 + + while (total_size > 0): + #read the chunk header and ensure its validity + try: + data = aiff_file.read(8) + (chunk_id, + chunk_size) = struct.unpack(">4sI", data) + except struct.error: + raise InvalidAIFF(ERR_AIFF_INVALID_AIFF) + + if (not frozenset(chunk_id).issubset(self.PRINTABLE_ASCII)): + raise InvalidAIFF(ERR_AIFF_INVALID_CHUNK_ID) + else: + total_size -= 8 + + #yield AIFF_Chunk or AIFF_File_Chunk depending on chunk size + if (chunk_size >= 0x100000): + #if chunk is too large, yield a File_Chunk + yield AIFF_File_Chunk(chunk_id, + chunk_size, + file(self.filename, "rb"), + aiff_file.tell()) + aiff_file.seek(chunk_size, 1) + else: + #otherwise, yield a raw data Chunk + yield AIFF_Chunk(chunk_id, chunk_size, + aiff_file.read(chunk_size)) + + if (chunk_size % 2): + if (len(aiff_file.read(1)) < 1): + raise InvalidAIFF(ERR_AIFF_INVALID_CHUNK) + total_size -= (chunk_size + 1) + else: + total_size -= chunk_size + + @classmethod + def aiff_from_chunks(cls, filename, chunk_iter): + """builds a new AIFF file from a chunk data iterator + + filename is the path to the AIFF file to build + chunk_iter should yield AIFF_Chunk-compatible objects + """ + + aiff_file = file(filename, 'wb') + try: + total_size = 4 + + #write an unfinished header with a placeholder size + aiff_file.write(struct.pack(">4sI4s", "FORM", total_size, "AIFF")) + + #write the individual chunks + for chunk in chunk_iter: + total_size += chunk.write(aiff_file) + + #once the chunks are done, go back and re-write the header + aiff_file.seek(0, 0) + aiff_file.write(struct.pack(">4sI4s", "FORM", total_size, "AIFF")) + finally: + aiff_file.close() + + def get_metadata(self): + """returns a MetaData object, or None + + raises IOError if unable to read the file""" + + from .bitstream import BitstreamReader + from .id3 import ID3v22Comment + + for chunk in self.chunks(): + if (chunk.id == 'ID3 '): + return ID3v22Comment.parse(BitstreamReader(chunk.data(), 0)) + else: + return None + + def update_metadata(self, metadata): + """takes this track's current MetaData object + as returned by get_metadata() and sets this track's metadata + with any fields updated in that object + + raises IOError if unable to write the file + """ + + import tempfile + from . import transfer_data + from .id3 import ID3v22Comment + from .bitstream import BitstreamRecorder + from .text import ERR_FOREIGN_METADATA + + if (metadata is None): + return + elif (not isinstance(metadata, ID3v22Comment)): + raise ValueError(ERR_FOREIGN_METADATA) + + def chunk_filter(chunks, id3_chunk_data): + for chunk in chunks: + if (chunk.id == "ID3 "): + yield AIFF_Chunk("ID3 ", + len(id3_chunk_data), + id3_chunk_data) + else: + yield chunk + + #turn our ID3v2.2 tag into a raw binary chunk + id3_chunk = BitstreamRecorder(0) + metadata.build(id3_chunk) + id3_chunk = id3_chunk.data() + + #generate a temporary AIFF file in which our new ID3v2.2 chunk + #replaces the existing ID3v2.2 chunk + new_aiff = tempfile.NamedTemporaryFile(suffix=self.SUFFIX) + self.__class__.aiff_from_chunks(new_aiff.name, + chunk_filter(self.chunks(), + id3_chunk)) + + #replace the existing file with data from the temporary file + new_file = open(new_aiff.name, 'rb') + old_file = open(self.filename, 'wb') + transfer_data(new_file.read, old_file.write) + old_file.close() + new_file.close() + + def set_metadata(self, metadata): + """takes a MetaData object and sets this track's metadata + + this metadata includes track name, album name, and so on + raises IOError if unable to write the file""" + + from .id3 import ID3v22Comment + + if (metadata is None): + return + elif (self.get_metadata() is not None): + #current file has metadata, so replace it with new metadata + self.update_metadata(ID3v22Comment.converted(metadata)) + else: + #current file has no metadata, so append new ID3 block + + import tempfile + from .bitstream import BitstreamRecorder + from . import transfer_data + + def chunk_filter(chunks, id3_chunk_data): + for chunk in chunks: + yield chunk + + yield AIFF_Chunk("ID3 ", + len(id3_chunk_data), + id3_chunk_data) + + #turn our ID3v2.2 tag into a raw binary chunk + id3_chunk = BitstreamRecorder(0) + ID3v22Comment.converted(metadata).build(id3_chunk) + id3_chunk = id3_chunk.data() + + #generate a temporary AIFF file in which our new ID3v2.2 chunk + #is appended to the file's set of chunks + new_aiff = tempfile.NamedTemporaryFile(suffix=self.SUFFIX) + self.__class__.aiff_from_chunks(new_aiff.name, + chunk_filter(self.chunks(), + id3_chunk)) + + #replace the existing file with data from the temporary file + new_file = open(new_aiff.name, 'rb') + old_file = open(self.filename, 'wb') + transfer_data(new_file.read, old_file.write) + old_file.close() + new_file.close() + + def delete_metadata(self): + """deletes the track's MetaData + + this removes or unsets tags as necessary in order to remove all data + raises IOError if unable to write the file""" + + def chunk_filter(chunks): + for chunk in chunks: + if (chunk.id == 'ID3 '): + continue + else: + yield chunk + + import tempfile + from . import transfer_data + + new_aiff = tempfile.NamedTemporaryFile(suffix=self.SUFFIX) + self.__class__.aiff_from_chunks(new_aiff.name, + chunk_filter(self.chunks())) + + new_file = open(new_aiff.name, 'rb') + old_file = open(self.filename, 'wb') + transfer_data(new_file.read, old_file.write) + old_file.close() + new_file.close() + + def to_pcm(self): + """returns a PCMReader object containing the track's PCM data""" + + return AiffReader(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()), + total_frames=self.__total_sample_frames__) + + @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 AiffAudio object""" + + from .bitstream import BitstreamWriter + from . import EncodingError + from . import DecodingError + from . import FRAMELIST_SIZE + + try: + f = open(filename, 'wb') + aiff = BitstreamWriter(f, 0) + except IOError, msg: + raise EncodingError(str(msg)) + + try: + total_size = 0 + data_size = 0 + total_pcm_frames = 0 + + #write out the basic headers first + #we'll be back later to clean up the sizes + aiff.build("4b 32u 4b", ("FORM", total_size, "AIFF")) + total_size += 4 + + aiff.build("4b 32u", ("COMM", 0x12)) + total_size += 8 + + aiff.build("16u 32u 16u", (pcmreader.channels, + total_pcm_frames, + pcmreader.bits_per_sample)) + build_ieee_extended(aiff, pcmreader.sample_rate) + total_size += 0x12 + + aiff.build("4b 32u", ("SSND", data_size)) + total_size += 8 + + aiff.build("32u 32u", (0, 0)) + data_size += 8 + total_size += 8 + + #dump pcmreader's FrameLists into the file as big-endian + try: + framelist = pcmreader.read(FRAMELIST_SIZE) + while (len(framelist) > 0): + bytes = framelist.to_bytes(True, True) + f.write(bytes) + + total_size += len(bytes) + data_size += len(bytes) + total_pcm_frames += framelist.frames + + framelist = pcmreader.read(FRAMELIST_SIZE) + except (IOError, ValueError), err: + cls.__unlink__(filename) + raise EncodingError(str(err)) + except Exception, err: + cls.__unlink__(filename) + raise err + + #handle odd-sized data chunks + if (data_size % 2): + aiff.write(8, 0) + total_size += 1 + + #close the PCM reader and flush our output + try: + pcmreader.close() + except DecodingError, err: + cls.__unlink__(filename) + raise EncodingError(err.error_message) + f.flush() + + if (total_size < (2 ** 32)): + #go back to the beginning and rewrite the header + f.seek(0, 0) + aiff.build("4b 32u 4b", ("FORM", total_size, "AIFF")) + aiff.build("4b 32u", ("COMM", 0x12)) + aiff.build("16u 32u 16u", (pcmreader.channels, + total_pcm_frames, + pcmreader.bits_per_sample)) + build_ieee_extended(aiff, pcmreader.sample_rate) + aiff.build("4b 32u", ("SSND", data_size)) + else: + import os + + os.unlink(filename) + raise EncodingError("PCM data too large for aiff file") + + finally: + f.close() + + return AiffAudio(filename) + + def has_foreign_aiff_chunks(self): + """returns True if the audio file contains non-audio AIFF chunks""" + + return (set(['COMM', 'SSND']) != set([c.id for c in self.chunks()])) + + def aiff_header_footer(self): + """returns (header, footer) tuple of strings + containing all data before and after the PCM stream + + if self.has_foreign_aiff_chunks() is False, + may raise ValueError if the file has no header and footer + for any reason""" + + from .bitstream import BitstreamReader + from .bitstream import BitstreamRecorder + from .text import (ERR_AIFF_NOT_AIFF, + ERR_AIFF_INVALID_AIFF, + ERR_AIFF_INVALID_CHUNK_ID) + + head = BitstreamRecorder(0) + tail = BitstreamRecorder(0) + current_block = head + + aiff_file = BitstreamReader(open(self.filename, 'rb'), 0) + try: + #transfer the 12-byte "RIFFsizeWAVE" header to head + (form, size, aiff) = aiff_file.parse("4b 32u 4b") + if (form != 'FORM'): + raise InvalidAIFF(ERR_AIFF_NOT_AIFF) + elif (aiff != 'AIFF'): + raise InvalidAIFF(ERR_AIFF_INVALID_AIFF) + else: + current_block.build("4b 32u 4b", (form, size, aiff)) + total_size = size - 4 + + while (total_size > 0): + #transfer each chunk header + (chunk_id, chunk_size) = aiff_file.parse("4b 32u") + if (not frozenset(chunk_id).issubset(self.PRINTABLE_ASCII)): + raise InvalidAIFF(ERR_AIFF_INVALID_CHUNK_ID) + else: + current_block.build("4b 32u", (chunk_id, chunk_size)) + total_size -= 8 + + #and transfer the full content of non-audio chunks + if (chunk_id != "SSND"): + if (chunk_size % 2): + current_block.write_bytes( + aiff_file.read_bytes(chunk_size + 1)) + total_size -= (chunk_size + 1) + else: + current_block.write_bytes( + aiff_file.read_bytes(chunk_size)) + total_size -= chunk_size + else: + #transfer alignment as part of SSND's chunk header + align = aiff_file.parse("32u 32u") + current_block.build("32u 32u", align) + aiff_file.skip_bytes(chunk_size - 8) + current_block = tail + + if (chunk_size % 2): + current_block.write_bytes(aiff_file.read_bytes(1)) + total_size -= (chunk_size + 1) + else: + total_size -= chunk_size + + return (head.data(), tail.data()) + finally: + aiff_file.close() + + @classmethod + def from_aiff(cls, filename, header, pcmreader, footer, compression=None): + """encodes a new file from AIFF data + + takes a filename string, header string, + PCMReader object, footer string + 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 AiffAudio object + + header + pcm data + footer should always result + in the original AIFF file being restored + without need for any padding bytes + + may raise EncodingError if some problem occurs when + encoding the input file""" + + from . import (DecodingError, EncodingError, FRAMELIST_SIZE) + from struct import unpack + + #ensure header validates correctly + try: + (total_size, ssnd_size) = validate_header(header) + except ValueError, err: + raise EncodingError(str(err)) + + try: + #write header to output file + f = open(filename, "wb") + f.write(header) + + #write PCM data to output file + SSND_bytes_written = 0 + s = pcmreader.read(FRAMELIST_SIZE).to_bytes(True, True) + while (len(s) > 0): + SSND_bytes_written += len(s) + f.write(s) + s = pcmreader.read(FRAMELIST_SIZE).to_bytes(True, True) + + #ensure output data size matches the "SSND" chunk's size + if (ssnd_size != SSND_bytes_written): + cls.__unlink__(filename) + from .text import ERR_AIFF_TRUNCATED_SSND_CHUNK + raise EncodingError(ERR_AIFF_TRUNCATED_SSND_CHUNK) + + #ensure footer validates correctly + try: + validate_footer(footer, SSND_bytes_written) + #before writing it to disk + f.write(footer) + except ValueError, err: + cls.__unlink__(filename) + raise EncodingError(str(err)) + + f.close() + + #ensure total size is correct + if ((len(header) + ssnd_size + len(footer)) != total_size): + cls.__unlink__(filename) + from .text import ERR_AIFF_INVALID_SIZE + raise EncodingError(ERR_AIFF_INVALID_SIZE) + + return cls(filename) + except IOError, err: + cls.__unlink__(filename) + raise EncodingError(str(err)) + except DecodingError, err: + cls.__unlink__(filename) + raise EncodingError(err.error_message) + + 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""" + + from . import CounterPCMReader + from . import transfer_framelist_data + from . import to_pcm_progress + + try: + (header, footer) = self.aiff_header_footer() + except IOError, err: + raise InvalidAIFF(unicode(err)) + except ValueError, err: + raise InvalidAIFF(unicode(err)) + + #ensure header is valid + try: + (total_size, data_size) = validate_header(header) + except ValueError, err: + raise InvalidAIFF(unicode(err)) + + #ensure "ssnd" chunk has all its data + counter = CounterPCMReader(to_pcm_progress(self, progress)) + try: + transfer_framelist_data(counter, lambda f: f) + except IOError: + from .text import ERR_AIFF_TRUNCATED_SSND_CHUNK + raise InvalidAIFF(ERR_AIFF_TRUNCATED_SSND_CHUNK) + + data_bytes_written = counter.bytes_written() + + #ensure output data size matches the "ssnd" chunk's size + if (data_size != data_bytes_written): + from .text import ERR_AIFF_TRUNCATED_SSND_CHUNK + raise InvalidAIFF(ERR_AIFF_TRUNCATED_SSND_CHUNK) + + #ensure footer validates correctly + try: + validate_footer(footer, data_bytes_written) + except ValueError, err: + from .text import ERR_AIFF_INVALID_SIZE + raise InvalidAIFF(ERR_AIFF_INVALID_SIZE) + + #ensure total size is correct + if ((len(header) + data_size + len(footer)) != total_size): + from .text import ERR_AIFF_INVALID_SIZE + raise InvalidAIFF(ERR_AIFF_INVALID_SIZE) + + return True + + def clean(self, fixes_performed, output_filename=None): + """cleans the file of known data and metadata problems + + fixes_performed is a list-like object which is appended + with Unicode strings of fixed problems + + output_filename is an optional filename of the fixed file + if present, a new AudioFile is returned + otherwise, only a dry-run is performed and no new file is written + + raises IOError if unable to write the file or its metadata + raises ValueError if the file has errors of some sort + """ + + from .text import (CLEAN_AIFF_MULTIPLE_COMM_CHUNKS, + CLEAN_AIFF_REORDERED_SSND_CHUNK, + CLEAN_AIFF_MULTIPLE_SSND_CHUNKS) + + chunk_queue = [] + pending_data = None + + for chunk in self.chunks(): + if (chunk.id == "COMM"): + if ("COMM" in [c.id for c in chunk_queue]): + fixes_performed.append( + CLEAN_AIFF_MULTIPLE_COMM_CHUNKS) + else: + chunk_queue.append(chunk) + if (pending_data is not None): + chunk_queue.append(pending_data) + pending_data = None + elif (chunk.id == "SSND"): + if ("COMM" not in [c.id for c in chunk_queue]): + fixes_performed.append(CLEAN_AIFF_REORDERED_SSND_CHUNK) + pending_data = chunk + elif ("SSND" in [c.id for c in chunk_queue]): + fixes_performed.append(CLEAN_AIFF_MULTIPLE_SSND_CHUNKS) + else: + chunk_queue.append(chunk) + else: + chunk_queue.append(chunk) + + old_metadata = self.get_metadata() + if (old_metadata is not None): + fixed_metadata = old_metadata.clean(fixes_performed) + else: + fixed_metadata = old_metadata + + if (output_filename is not None): + AiffAudio.aiff_from_chunks(output_filename, chunk_queue) + fixed_aiff = AiffAudio(output_filename) + if (fixed_metadata is not None): + fixed_aiff.update_metadata(fixed_metadata) + return fixed_aiff
View file
audiotools-2.19.tar.gz/audiotools/ape.py
Added
@@ -0,0 +1,1218 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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 . import (AudioFile, MetaData) + + +#takes a pair of integers (or None) for the current and total values +#returns a unicode string of their combined pair +#for example, __number_pair__(2,3) returns u"2/3" +#whereas __number_pair__(4,0) returns u"4" +def __number_pair__(current, total): + def empty(i): + return i is None + + unslashed_format = u"%d" + slashed_format = u"%d/%d" + + if (empty(current) and empty(total)): + return unslashed_format % (0,) + elif ((not empty(current)) and empty(total)): + return unslashed_format % (current,) + elif (empty(current) and (not empty(total))): + return slashed_format % (0, total) + else: + #neither current or total are empty + return slashed_format % (current, total) + + +def limited_transfer_data(from_function, to_function, + max_bytes): + """transfers up to max_bytes from from_function to to_function + or as many bytes as from_function generates as strings""" + + BUFFER_SIZE = 0x100000 + s = from_function(BUFFER_SIZE) + while ((len(s) > 0) and (max_bytes > 0)): + if (len(s) > max_bytes): + s = s[0:max_bytes] + to_function(s) + max_bytes -= len(s) + s = from_function(BUFFER_SIZE) + + +####################### +#MONKEY'S AUDIO +####################### + + +class ApeTagItem: + """a single item in the ApeTag, typically a unicode value""" + + FORMAT = "32u [ 1u 2u 29p ]" + + def __init__(self, item_type, read_only, key, data): + """fields are as follows: + + item_type is 0 = UTF-8, 1 = binary, 2 = external, 3 = reserved + read_only is 1 if the item is read only + key is an ASCII string + data is a binary string of the data itself + """ + + self.type = item_type + self.read_only = read_only + self.key = key + self.data = data + + def __eq__(self, item): + for attr in ["type", "read_only", "key", "data"]: + if ((not hasattr(item, attr)) or (getattr(self, attr) != + getattr(item, attr))): + return False + else: + return True + + def total_size(self): + """returns total size of item in bytes""" + + return 4 + 4 + len(self.key) + 1 + len(self.data) + + def copy(self): + """returns a duplicate ApeTagItem""" + + return ApeTagItem(self.type, + self.read_only, + self.key, + self.data) + + def __repr__(self): + return "ApeTagItem(%s,%s,%s,%s)" % \ + (repr(self.type), + repr(self.read_only), + repr(self.key), + repr(self.data)) + + def raw_info_pair(self): + """returns a human-readable key/value pair of item data""" + + if (self.type == 0): # text + if (self.read_only): + return (self.key.decode('ascii'), + u"(read only) %s" % (self.data.decode('utf-8'))) + else: + return (self.key.decode('ascii'), self.data.decode('utf-8')) + elif (self.type == 1): # binary + return (self.key.decode('ascii'), + u"(binary) %d bytes" % (len(self.data))) + elif (self.type == 2): # external + return (self.key.decode('ascii'), + u"(external) %d bytes" % (len(self.data))) + else: # reserved + return (self.key.decode('ascii'), + u"(reserved) %d bytes" % (len(self.data))) + + def __str__(self): + return self.data + + def __unicode__(self): + return self.data.rstrip(chr(0)).decode('utf-8', 'replace') + + @classmethod + def parse(cls, reader): + """returns an ApeTagItem parsed from the given BitstreamReader""" + + (item_value_length, + read_only, + encoding) = reader.parse(cls.FORMAT) + + key = [] + c = reader.read(8) + while (c != 0): + key.append(chr(c)) + c = reader.read(8) + + value = reader.read_bytes(item_value_length) + + return cls(encoding, read_only, "".join(key), value) + + def build(self, writer): + """writes the ApeTagItem values to the given BitstreamWriter""" + + writer.build("%s %db 8u %db" % (self.FORMAT, + len(self.key), + len(self.data)), + (len(self.data), + self.read_only, + self.type, + self.key, 0, self.data)) + + @classmethod + def binary(cls, key, data): + """returns an ApeTagItem of binary data + + key is an ASCII string, data is a binary string""" + + return cls(1, 0, key, data) + + @classmethod + def external(cls, key, data): + """returns an ApeTagItem of external data + + key is an ASCII string, data is a binary string""" + + return cls(2, 0, key, data) + + @classmethod + def string(cls, key, data): + """returns an ApeTagItem of text data + + key is an ASCII string, data is a unicode string""" + + return cls(0, 0, key, data.encode('utf-8', 'replace')) + + +class ApeTag(MetaData): + """a complete APEv2 tag""" + + HEADER_FORMAT = "8b 32u 32u 32u [ 1u 2u 26p 1u 1u 1u ] 64p" + + ITEM = ApeTagItem + + ATTRIBUTE_MAP = {'track_name': 'Title', + 'track_number': 'Track', + 'track_total': 'Track', + 'album_number': 'Media', + 'album_total': 'Media', + 'album_name': 'Album', + 'artist_name': 'Artist', + #"Performer" is not a defined APEv2 key + #it would be nice to have, yet would not be standard + 'performer_name': 'Performer', + 'composer_name': 'Composer', + 'conductor_name': 'Conductor', + 'ISRC': 'ISRC', + 'catalog': 'Catalog', + 'copyright': 'Copyright', + 'publisher': 'Publisher', + 'year': 'Year', + 'date': 'Record Date', + 'comment': 'Comment'} + + INTEGER_ITEMS = ('Track', 'Media') + + def __init__(self, tags, contains_header=True, contains_footer=True): + """constructs an ApeTag from a list of ApeTagItem objects""" + + for tag in tags: + if (not isinstance(tag, ApeTagItem)): + raise ValueError("%s is not ApeTag" % (repr(tag))) + self.__dict__["tags"] = list(tags) + self.__dict__["contains_header"] = contains_header + self.__dict__["contains_footer"] = contains_footer + + def __repr__(self): + return "ApeTag(%s, %s, %s)" % (repr(self.tags), + repr(self.contains_header), + repr(self.contains_footer)) + + def total_size(self): + """returns the minimum size of the total ApeTag, in bytes""" + + size = 0 + if (self.contains_header): + size += 32 + for tag in self.tags: + size += tag.total_size() + if (self.contains_footer): + size += 32 + return size + + def __eq__(self, metadata): + if (isinstance(metadata, ApeTag)): + if (set(self.keys()) != set(metadata.keys())): + return False + + for tag in self.tags: + try: + if (tag.data != metadata[tag.key].data): + return False + except KeyError: + return False + else: + return True + elif (isinstance(metadata, MetaData)): + return MetaData.__eq__(self, metadata) + else: + return False + + def keys(self): + return [tag.key for tag in self.tags] + + def __contains__(self, key): + for tag in self.tags: + if (tag.key == key): + return True + else: + return False + + def __getitem__(self, key): + for tag in self.tags: + if (tag.key == key): + return tag + else: + raise KeyError(key) + + def get(self, key, default): + try: + return self[key] + except KeyError: + return default + + def __setitem__(self, key, value): + for i in xrange(len(self.tags)): + if (self.tags[i].key == key): + self.tags[i] = value + return + else: + self.tags.append(value) + + def index(self, key): + for (i, tag) in enumerate(self.tags): + if (tag.key == key): + return i + else: + raise ValueError(key) + + def __delitem__(self, key): + old_tag_count = len(self.tags) + self.tags = [tag for tag in self.tags if tag.key != key] + if (len(self.tags) == old_tag_count): + raise KeyError(key) + + def __getattr__(self, attr): + import re + + if (attr == 'track_number'): + try: + track_text = unicode(self["Track"]) + track = re.search(r'\d+', track_text) + if (track is not None): + track_number = int(track.group(0)) + if ((track_number == 0) and + (re.search(r'/.*?(\d+)', + track_text) is not None)): + #if track_total is nonzero and track_number is 0 + #track_number is a placeholder + #so treat track_number as None + return None + else: + return track_number + else: + #"Track" isn't an integer + return None + except KeyError: + #no "Track" in list of items + return None + elif (attr == 'track_total'): + try: + track = re.search(r'/.*?(\d+)', unicode(self["Track"])) + if (track is not None): + return int(track.group(1)) + else: + #no slashed integer field in "Track" + return None + except KeyError: + #no "Track" in list of items + return None + elif (attr == 'album_number'): + try: + media_text = unicode(self["Media"]) + media = re.search(r'\d+', media_text) + if (media is not None): + album_number = int(media.group(0)) + if ((album_number == 0) and + (re.search(r'/.*?(\d+)', + media_text) is not None)): + #if album_total is nonzero and album_number is 0 + #album_number is a placeholder + #so treat album_number as None + return None + else: + return album_number + else: + #"Media" isn't an integer + return None + except KeyError: + #no "Media" in list of items + return None + elif (attr == 'album_total'): + try: + media = re.search(r'/.*?(\d+)', unicode(self["Media"])) + if (media is not None): + return int(media.group(1)) + else: + #no slashed integer field in "Media" + return None + except KeyError: + #no "Media" in list of items + return None + elif (attr in self.ATTRIBUTE_MAP): + try: + return unicode(self[self.ATTRIBUTE_MAP[attr]]) + except KeyError: + return None + elif (attr in MetaData.FIELDS): + return None + else: + try: + return self.__dict__[attr] + except AttrError: + raise AttributeError(attr) + + #if an attribute is updated (e.g. self.track_name) + #make sure to update the corresponding dict pair + def __setattr__(self, attr, value): + if (attr in self.ATTRIBUTE_MAP): + if (value is not None): + import re + + if (attr == 'track_number'): + try: + self['Track'].data = re.sub(r'\d+', + str(int(value)), + self['Track'].data, + 1) + except KeyError: + self['Track'] = self.ITEM.string( + 'Track', __number_pair__(value, self.track_total)) + elif (attr == 'track_total'): + try: + if (re.search(r'/\D*\d+', + self['Track'].data) is not None): + self['Track'].data = re.sub( + r'(/\D*)(\d+)', + "\\g<1>" + str(int(value)), + self['Track'].data, + 1) + else: + self['Track'].data = "%s/%d" % ( + self['Track'].data, + value) + except KeyError: + self['Track'] = self.ITEM.string( + 'Track', __number_pair__(self.track_number, value)) + elif (attr == 'album_number'): + try: + self['Media'].data = re.sub(r'\d+', + str(int(value)), + self['Media'].data, + 1) + except KeyError: + self['Media'] = self.ITEM.string( + 'Media', __number_pair__(value, self.album_total)) + elif (attr == 'album_total'): + try: + if (re.search(r'/\D*\d+', + self['Media'].data) is not None): + self['Media'].data = re.sub( + r'(/\D*)(\d+)', + "\\g<1>" + str(int(value)), + self['Media'].data, + 1) + else: + self['Media'].data = "%s/%d" % ( + self['Media'].data, + value) + except KeyError: + self['Media'] = self.ITEM.string( + 'Media', __number_pair__(self.album_number, value)) + else: + self[self.ATTRIBUTE_MAP[attr]] = self.ITEM.string( + self.ATTRIBUTE_MAP[attr], value) + else: + delattr(self, attr) + else: + self.__dict__[attr] = value + + def __delattr__(self, attr): + import re + + if (attr == 'track_number'): + try: + #if "Track" field contains a slashed total + if (re.search(r'\d+.*?/.*?\d+', + self['Track'].data) is not None): + #replace unslashed portion with 0 + self['Track'].data = re.sub(r'\d+', + str(int(0)), + self['Track'].data, + 1) + else: + #otherwise, remove "Track" field + del(self['Track']) + except KeyError: + pass + elif (attr == 'track_total'): + try: + track_number = re.search(r'\d+', + self["Track"].data.split("/")[0]) + #if track number is nonzero + if (((track_number is not None) and + (int(track_number.group(0)) != 0))): + #if "Track" field contains a slashed total + #remove slashed total from "Track" field + self['Track'].data = re.sub(r'\s*/.*', + "", + self['Track'].data) + else: + #if "Track" field contains a slashed total + if (re.search(r'/\D*?\d+', + self['Track'].data) is not None): + #remove "Track" field entirely + del(self['Track']) + except KeyError: + pass + elif (attr == 'album_number'): + try: + #if "Media" field contains a slashed total + if (re.search(r'\d+.*?/.*?\d+', + self['Media'].data) is not None): + #replace unslashed portion with 0 + self['Media'].data = re.sub(r'\d+', + str(int(0)), + self['Media'].data, + 1) + else: + #otherwise, remove "Media" field + del(self['Media']) + except KeyError: + pass + elif (attr == 'album_total'): + try: + album_number = re.search(r'\d+', + self["Media"].data.split("/")[0]) + #if album number is nonzero + if (((album_number is not None) and + (int(album_number.group(0)) != 0))): + #if "Media" field contains a slashed total + #remove slashed total from "Media" field + self['Media'].data = re.sub(r'\s*/.*', + "", + self['Media'].data) + else: + #if "Media" field contains a slashed total + if (re.search(r'/\D*?\d+', + self['Media'].data) is not None): + #remove "Media" field entirely + del(self['Media']) + except KeyError: + pass + elif (attr in self.ATTRIBUTE_MAP): + try: + del(self[self.ATTRIBUTE_MAP[attr]]) + except KeyError: + pass + elif (attr in MetaData.FIELDS): + pass + else: + try: + del(self.__dict__[attr]) + except AttrError: + raise AttributeError(attr) + + @classmethod + def converted(cls, metadata): + """converts a MetaData object to an ApeTag object""" + + if (metadata is None): + return None + elif (isinstance(metadata, ApeTag)): + return ApeTag([tag.copy() for tag in metadata.tags], + contains_header=metadata.contains_header, + contains_footer=metadata.contains_footer) + else: + tags = cls([]) + for (field, key) in cls.ATTRIBUTE_MAP.items(): + if (((field not in cls.INTEGER_FIELDS) and + (getattr(metadata, field) is not None))): + tags[key] = cls.ITEM.string( + key, unicode(getattr(metadata, field))) + + if (((metadata.track_number is not None) or + (metadata.track_total is not None))): + tags["Track"] = cls.ITEM.string( + "Track", __number_pair__(metadata.track_number, + metadata.track_total)) + + if (((metadata.album_number is not None) or + (metadata.album_total is not None))): + tags["Media"] = cls.ITEM.string( + "Media", __number_pair__(metadata.album_number, + metadata.album_total)) + + for image in metadata.images(): + tags.add_image(image) + + return tags + + def raw_info(self): + """returns the ApeTag as a human-readable unicode string""" + + from os import linesep + from . import display_unicode + + #align tag values on the "=" sign + if (len(self.tags) > 0): + max_indent = max([len(display_unicode(tag.raw_info_pair()[0])) + for tag in self.tags]) + tag_strings = [u"%s%s = %s" % + (u" " * (max_indent - len(display_unicode(key))), + key, value) for (key, value) in + [tag.raw_info_pair() for tag in self.tags]] + else: + tag_strings = [] + + return linesep.decode('ascii').join([u"APEv2:"] + tag_strings) + + @classmethod + def supports_images(cls): + """returns True""" + + return True + + def __parse_image__(self, key, type): + from . import Image + import cStringIO + + data = cStringIO.StringIO(self[key].data) + description = [] + c = data.read(1) + while (c != '\x00'): + description.append(c) + c = data.read(1) + + return Image.new(data.read(), + "".join(description).decode('utf-8', 'replace'), + type) + + def add_image(self, image): + """embeds an Image object in this metadata""" + + if (image.type == 0): + self['Cover Art (front)'] = self.ITEM.binary( + 'Cover Art (front)', + image.description.encode('utf-8', 'replace') + + chr(0) + + image.data) + elif (image.type == 1): + self['Cover Art (back)'] = self.ITEM.binary( + 'Cover Art (back)', + image.description.encode('utf-8', 'replace') + + chr(0) + + image.data) + + def delete_image(self, image): + """deletes an Image object from this metadata""" + + if ((image.type == 0) and 'Cover Art (front)' in self.keys()): + del(self['Cover Art (front)']) + elif ((image.type == 1) and 'Cover Art (back)' in self.keys()): + del(self['Cover Art (back)']) + + def images(self): + """returns a list of embedded Image objects""" + + #APEv2 supports only one value per key + #so a single front and back cover are all that is possible + img = [] + if ('Cover Art (front)' in self.keys()): + img.append(self.__parse_image__('Cover Art (front)', 0)) + if ('Cover Art (back)' in self.keys()): + img.append(self.__parse_image__('Cover Art (back)', 1)) + return img + + @classmethod + def read(cls, apefile): + """returns an ApeTag object from an APEv2 tagged file object + + may return None if the file object has no tag""" + + from .bitstream import BitstreamReader + + apefile.seek(-32, 2) + reader = BitstreamReader(apefile, 1) + + (preamble, + version, + tag_size, + item_count, + read_only, + item_encoding, + is_header, + no_footer, + has_header) = reader.parse(cls.HEADER_FORMAT) + + if ((preamble != "APETAGEX") or (version != 2000)): + return None + + apefile.seek(-tag_size, 2) + + return cls([ApeTagItem.parse(reader) + for i in xrange(item_count)], + contains_header=has_header, + contains_footer=True) + + def build(self, writer): + """outputs an APEv2 tag to writer""" + + from .bitstream import BitstreamRecorder + + tags = BitstreamRecorder(1) + + for tag in self.tags: + tag.build(tags) + + if (self.contains_header): + writer.build(ApeTag.HEADER_FORMAT, + ("APETAGEX", # preamble + 2000, # version + tags.bytes() + 32, # tag size + len(self.tags), # item count + 0, # read only + 0, # encoding + 1, # is header + not self.contains_footer, # no footer + self.contains_header)) # has header + + tags.copy(writer) + if (self.contains_footer): + writer.build(ApeTag.HEADER_FORMAT, + ("APETAGEX", # preamble + 2000, # version + tags.bytes() + 32, # tag size + len(self.tags), # item count + 0, # read only + 0, # encoding + 0, # is header + not self.contains_footer, # no footer + self.contains_header)) # has header + + def clean(self, fixes_applied): + import re + from .text import (CLEAN_REMOVE_DUPLICATE_TAG, + CLEAN_REMOVE_TRAILING_WHITESPACE, + CLEAN_REMOVE_LEADING_WHITESPACE, + CLEAN_FIX_TAG_FORMATTING, + CLEAN_REMOVE_EMPTY_TAG) + + used_tags = set([]) + tag_items = [] + for tag in self.tags: + if (tag.key.upper() in used_tags): + fixes_applied.append( + CLEAN_REMOVE_DUPLICATE_TAG % + {"field": tag.key.decode('ascii')}) + elif (tag.type == 0): + used_tags.add(tag.key.upper()) + text = unicode(tag) + + #check trailing whitespace + fix1 = text.rstrip() + if (fix1 != text): + fixes_applied.append( + CLEAN_REMOVE_TRAILING_WHITESPACE % + {"field": tag.key.decode('ascii')}) + + #check leading whitespace + fix2 = fix1.lstrip() + if (fix2 != fix1): + fixes_applied.append( + CLEAN_REMOVE_LEADING_WHITESPACE % + {"field": tag.key.decode('ascii')}) + + if (tag.key in self.INTEGER_ITEMS): + if (u"/" in fix2): + #item is a slashed field of some sort + (current, total) = fix2.split(u"/", 1) + current_int = re.search(r'\d+', current) + total_int = re.search(r'\d+', total) + if ((current_int is None) and (total_int is None)): + #neither side contains an integer value + #so ignore it altogether + fix3 = fix2 + elif ((current_int is not None) and + (total_int is None)): + fix3 = u"%d" % (int(current_int.group(0))) + elif ((current_int is None) and + (total_int is not None)): + fix3 = u"%d/%d" % (0, int(total_int.group(0))) + else: + #both sides contain an int + fix3 = u"%d/%d" % (int(current_int.group(0)), + int(total_int.group(0))) + else: + #item contains no slash + current_int = re.search(r'\d+', fix2) + if (current_int is not None): + #item contains an integer + fix3 = unicode(int(current_int.group(0))) + else: + #item contains no integer value so ignore it + #(although 'Track' should only contain + # integers, 'Media' may contain strings + # so it may be best to simply ignore that case) + fix3 = fix2 + + if (fix3 != fix2): + fixes_applied.append( + CLEAN_FIX_TAG_FORMATTING % + {"field": tag.key.decode('ascii')}) + else: + fix3 = fix2 + + if (len(fix3) > 0): + tag_items.append(ApeTagItem.string(tag.key, fix3)) + else: + fixes_applied.append( + CLEAN_REMOVE_EMPTY_TAG % + {"field": tag.key.decode('ascii')}) + else: + used_tags.add(tag.key.upper()) + tag_items.append(tag) + + return self.__class__(tag_items, + self.contains_header, + self.contains_footer) + + +class ApeTaggedAudio: + """a class for handling audio formats with APEv2 tags + + this class presumes there will be a filename attribute which + can be opened and checked for tags, or written if necessary""" + + def get_metadata(self): + """returns an ApeTag object, or None + + raises IOError if unable to read the file""" + + f = file(self.filename, 'rb') + try: + return ApeTag.read(f) + finally: + f.close() + + def update_metadata(self, metadata): + """takes this track's current MetaData object + as returned by get_metadata() and sets this track's metadata + with any fields updated in that object + + raises IOError if unable to write the file + """ + + if (metadata is None): + return + elif (not isinstance(metadata, ApeTag)): + from .text import ERR_FOREIGN_METADATA + raise ValueError(ERR_FOREIGN_METADATA) + + from .bitstream import BitstreamReader, BitstreamWriter + from . import transfer_data + + f = file(self.filename, "r+b") + f.seek(-32, 2) + + (preamble, + version, + tag_size, + item_count, + read_only, + item_encoding, + is_header, + no_footer, + has_header) = BitstreamReader(f, 1).parse(ApeTag.HEADER_FORMAT) + + if ((preamble == 'APETAGEX') and (version == 2000)): + if (has_header): + old_tag_size = 32 + tag_size + else: + old_tag_size = tag_size + + if (metadata.total_size() >= old_tag_size): + #metadata has grown + #so append it to existing file + f.seek(-old_tag_size, 2) + metadata.build(BitstreamWriter(f, 1)) + else: + #metadata has shrunk + #so rewrite file with smaller metadata + import tempfile + from os.path import getsize + rewritten = tempfile.TemporaryFile() + + #copy everything but the last "old_tag_size" bytes + #from existing file to rewritten file + f = open(self.filename, "rb") + limited_transfer_data(f.read, rewritten.write, + getsize(self.filename) - + old_tag_size) + f.close() + + #append new tag to rewritten file + metadata.build(BitstreamWriter(rewritten, 1)) + + #finally, overwrite current file with rewritten file + rewritten.seek(0, 0) + f = open(self.filename, "wb") + transfer_data(rewritten.read, f.write) + f.close() + rewritten.close() + else: + #no existing metadata, so simply append a fresh tag + f = file(self.filename, "ab") + metadata.build(BitstreamWriter(f, 1)) + f.close() + + def set_metadata(self, metadata): + """takes a MetaData object and sets this track's metadata + + raises IOError if unable to write the file""" + + if (metadata is None): + return + + from .bitstream import BitstreamWriter + + old_metadata = self.get_metadata() + new_metadata = ApeTag.converted(metadata) + + if (old_metadata is not None): + #transfer ReplayGain tags from old metadata to new metadata + for tag in ["replaygain_track_gain", + "replaygain_track_peak", + "replaygain_album_gain", + "replaygain_album_peak"]: + try: + #if old_metadata has tag, shift it over + new_metadata[tag] = old_metadata[tag] + except KeyError: + try: + #otherwise, if new_metadata has tag, delete it + del(new_metadata[tag]) + except KeyError: + #if neither has tag, ignore it + continue + + #transfer Cuesheet from old metadata to new metadata + if ("Cuesheet" in old_metadata): + new_metadata["Cuesheet"] = old_metadata["Cuesheet"] + elif ("Cuesheet" in new_metadata): + del(new_metadata["Cuesheet"]) + + self.update_metadata(new_metadata) + else: + #delete ReplayGain tags from new metadata + for tag in ["replaygain_track_gain", + "replaygain_track_peak", + "replaygain_album_gain", + "replaygain_album_peak"]: + try: + del(new_metadata[tag]) + except KeyError: + continue + + #delete Cuesheet from new metadata + if ("Cuesheet" in new_metadata): + del(new_metadata["Cuesheet"]) + + #no existing metadata, so simply append a fresh tag + f = file(self.filename, "ab") + new_metadata.build(BitstreamWriter(f, 1)) + f.close() + + def delete_metadata(self): + """deletes the track's MetaData + + raises IOError if unable to write the file""" + + from .bitstream import BitstreamReader, BitstreamWriter + from . import transfer_data + + f = file(self.filename, "r+b") + f.seek(-32, 2) + + (preamble, + version, + tag_size, + item_count, + read_only, + item_encoding, + is_header, + no_footer, + has_header) = BitstreamReader(f, 1).parse(ApeTag.HEADER_FORMAT) + + if ((preamble == 'APETAGEX') and (version == 2000)): + #there's existing metadata to delete + #so rewrite file without trailing metadata tag + if (has_header): + old_tag_size = 32 + tag_size + else: + old_tag_size = tag_size + + import tempfile + from os.path import getsize + rewritten = tempfile.TemporaryFile() + + #copy everything but the last "old_tag_size" bytes + #from existing file to rewritten file + f = open(self.filename, "rb") + limited_transfer_data(f.read, rewritten.write, + getsize(self.filename) - + old_tag_size) + f.close() + + #finally, overwrite current file with rewritten file + rewritten.seek(0, 0) + f = open(self.filename, "wb") + transfer_data(rewritten.read, f.write) + f.close() + rewritten.close() + + +class ApeAudio(ApeTaggedAudio, AudioFile): + """a Monkey's Audio file""" + + SUFFIX = "ape" + NAME = SUFFIX + DEFAULT_COMPRESSION = "5000" + COMPRESSION_MODES = tuple([str(x * 1000) for x in range(1, 6)]) + BINARIES = ("mac",) + + # FILE_HEAD = Con.Struct("ape_head", + # Con.String('id', 4), + # Con.ULInt16('version')) + + # #version >= 3.98 + # APE_DESCRIPTOR = Con.Struct("ape_descriptor", + # Con.ULInt16('padding'), + # Con.ULInt32('descriptor_bytes'), + # Con.ULInt32('header_bytes'), + # Con.ULInt32('seektable_bytes'), + # Con.ULInt32('header_data_bytes'), + # Con.ULInt32('frame_data_bytes'), + # Con.ULInt32('frame_data_bytes_high'), + # Con.ULInt32('terminating_data_bytes'), + # Con.String('md5', 16)) + + # APE_HEADER = Con.Struct("ape_header", + # Con.ULInt16('compression_level'), + # Con.ULInt16('format_flags'), + # Con.ULInt32('blocks_per_frame'), + # Con.ULInt32('final_frame_blocks'), + # Con.ULInt32('total_frames'), + # Con.ULInt16('bits_per_sample'), + # Con.ULInt16('number_of_channels'), + # Con.ULInt32('sample_rate')) + + # #version <= 3.97 + # APE_HEADER_OLD = Con.Struct("ape_header_old", + # Con.ULInt16('compression_level'), + # Con.ULInt16('format_flags'), + # Con.ULInt16('number_of_channels'), + # Con.ULInt32('sample_rate'), + # Con.ULInt32('header_bytes'), + # Con.ULInt32('terminating_bytes'), + # Con.ULInt32('total_frames'), + # Con.ULInt32('final_frame_blocks')) + + def __init__(self, filename): + """filename is a plain string""" + + AudioFile.__init__(self, filename) + + (self.__samplespersec__, + self.__channels__, + self.__bitspersample__, + self.__totalsamples__) = ApeAudio.__ape_info__(filename) + + @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) == "MAC " + + def lossless(self): + """returns True""" + + return True + + @classmethod + def supports_foreign_riff_chunks(cls): + """returns True""" + + return True + + def has_foreign_riff_chunks(self): + """returns True""" + + #FIXME - this isn't strictly true + #I'll need a way to detect foreign chunks in APE's stream + #without decoding it first, + #but since I'm not supporting APE anyway, I'll take the lazy way out + return True + + def bits_per_sample(self): + """returns an integer number of bits-per-sample this track contains""" + + return self.__bitspersample__ + + def channels(self): + """returns an integer number of channels this track contains""" + + return self.__channels__ + + def total_frames(self): + """returns the total PCM frames of the track as an integer""" + + return self.__totalsamples__ + + def sample_rate(self): + """returns the rate of the track's audio as an integer number of Hz""" + + return self.__samplespersec__ + + @classmethod + def __ape_info__(cls, filename): + f = file(filename, 'rb') + try: + file_head = cls.FILE_HEAD.parse_stream(f) + + if (file_head.id != 'MAC '): + from .text import ERR_APE_INVALID_HEADER + raise InvalidFile(ERR_APE_INVALID_HEADER) + + if (file_head.version >= 3980): # the latest APE file type + descriptor = cls.APE_DESCRIPTOR.parse_stream(f) + header = cls.APE_HEADER.parse_stream(f) + + return (header.sample_rate, + header.number_of_channels, + header.bits_per_sample, + ((header.total_frames - 1) * + header.blocks_per_frame) + + header.final_frame_blocks) + else: # old-style APE file (obsolete) + header = cls.APE_HEADER_OLD.parse_stream(f) + + if (file_head.version >= 3950): + blocks_per_frame = 0x48000 + elif ((file_head.version >= 3900) or + ((file_head.version >= 3800) and + (header.compression_level == 4000))): + blocks_per_frame = 0x12000 + else: + blocks_per_frame = 0x2400 + + if (header.format_flags & 0x01): + bits_per_sample = 8 + elif (header.format_flags & 0x08): + bits_per_sample = 24 + else: + bits_per_sample = 16 + + return (header.sample_rate, + header.number_of_channels, + bits_per_sample, + ((header.total_frames - 1) * + blocks_per_frame) + + header.final_frame_blocks) + + finally: + f.close() + + def to_wave(self, wave_filename): + """writes the contents of this file to the given .wav filename string + + raises EncodingError if some error occurs during decoding""" + + from . import BIN + from . import transfer_data + import subprocess + import os + + if (self.filename.endswith(".ape")): + devnull = file(os.devnull, "wb") + sub = subprocess.Popen([BIN['mac'], + self.filename, + wave_filename, + '-d'], + stdout=devnull, + stderr=devnull) + sub.wait() + devnull.close() + else: + devnull = file(os.devnull, 'ab') + import tempfile + ape = tempfile.NamedTemporaryFile(suffix='.ape') + f = file(self.filename, 'rb') + transfer_data(f.read, ape.write) + f.close() + ape.flush() + sub = subprocess.Popen([BIN['mac'], + ape.name, + wave_filename, + '-d'], + stdout=devnull, + stderr=devnull) + sub.wait() + ape.close() + devnull.close() + + @classmethod + def from_wave(cls, filename, wave_filename, compression=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 ApeAudio object""" + + from . import BIN + import subprocess + import os + + if (str(compression) not in cls.COMPRESSION_MODES): + compression = cls.DEFAULT_COMPRESSION + + devnull = file(os.devnull, "wb") + sub = subprocess.Popen([BIN['mac'], + wave_filename, + filename, + "-c%s" % (compression)], + stdout=devnull, + stderr=devnull) + sub.wait() + devnull.close() + return ApeAudio(filename)
View file
audiotools-2.19.tar.gz/audiotools/au.py
Added
@@ -0,0 +1,261 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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 . import (AudioFile, InvalidFile, PCMReader) +from .pcm import FrameList + + +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 + self.bytes_per_frame = self.channels * self.bits_per_sample / 8 + + def read(self, pcm_frames): + """try to read a pcm.FrameList with the given number of PCM frames""" + + #align bytes downward if an odd number is read in + pcm_data = self.file.read(max(pcm_frames, 1) * self.bytes_per_frame) + if ((len(pcm_data) == 0) and (self.data_size > 0)): + raise IOError("data ends prematurely") + else: + self.data_size -= len(pcm_data) + + try: + return 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 + DESCRIPTION = u"Sun Au" + + def __init__(self, filename): + AudioFile.__init__(self, filename) + + from .bitstream import BitstreamReader + from .text import (ERR_AU_INVALID_HEADER, + ERR_AU_UNSUPPORTED_FORMAT) + + try: + f = file(filename, 'rb') + + (magic_number, + self.__data_offset__, + self.__data_size__, + encoding_format, + self.__sample_rate__, + self.__channels__) = BitstreamReader(f, 0).parse( + "4b 32u 32u 32u 32u 32u") + except IOError, msg: + raise InvalidAU(str(msg)) + + if (magic_number != '.snd'): + raise InvalidAU(ERR_AU_INVALID_HEADER) + try: + self.__bits_per_sample__ = {2: 8, 3: 16, 4: 24}[encoding_format] + except KeyError: + raise InvalidAU(ERR_AU_UNSUPPORTED_FORMAT) + + 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): + from . import ChannelMask + + """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.__data_size__ / + (self.__bits_per_sample__ / 8) / + self.__channels__) + + 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()) + + 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""" + + import struct + + f = file(self.filename, 'rb') + (magic_number, data_offset) = struct.unpack(">4sI", f.read(8)) + header = f.read(data_offset - struct.calcsize(">4sI")) + return (struct.pack(">4sI%ds" % (len(header)), + magic_number, data_offset, header), "") + + @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""" + + from .bitstream import BitstreamWriter + from . import FRAMELIST_SIZE + from . import EncodingError + from . import DecodingError + + if (pcmreader.bits_per_sample not in (8, 16, 24)): + from . import Filename + from .text import ERR_UNSUPPORTED_BITS_PER_SAMPLE + from . import UnsupportedBitsPerSample + raise UnsupportedBitsPerSample( + ERR_UNSUPPORTED_BITS_PER_SAMPLE % + {"target_filename": Filename(filename), + "bps": pcmreader.bits_per_sample}) + + data_size = 0 + encoding_format = {8: 2, 16: 3, 24: 4}[pcmreader.bits_per_sample] + + try: + f = file(filename, 'wb') + au = BitstreamWriter(f, 0) + except IOError, err: + raise EncodingError(str(err)) + try: + #write a dummy header + au.build("4b 32u 32u 32u 32u 32u", + (".snd", 24, data_size, encoding_format, + pcmreader.sample_rate, pcmreader.channels)) + + #write our big-endian PCM data + try: + framelist = pcmreader.read(FRAMELIST_SIZE) + while (len(framelist) > 0): + bytes = framelist.to_bytes(True, True) + f.write(bytes) + data_size += len(bytes) + framelist = pcmreader.read(FRAMELIST_SIZE) + except (IOError, ValueError), err: + cls.__unlink__(filename) + raise EncodingError(str(err)) + except Exception, err: + cls.__unlink__(filename) + raise err + + if (data_size < 2 ** 32): + #rewind and write a complete header + f.seek(0, 0) + au.build("4b 32u 32u 32u 32u 32u", + (".snd", 24, data_size, encoding_format, + pcmreader.sample_rate, pcmreader.channels)) + else: + cls.__unlink__(filename) + raise EncodingError("PCM data too large for Sun AU file") + finally: + f.close() + + try: + pcmreader.close() + except DecodingError, err: + cls.__unlink__(filename) + 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)
View file
audiotools-2.18.tar.gz/audiotools/cue.py -> audiotools-2.19.tar.gz/audiotools/cue.py
Changed
@@ -19,11 +19,8 @@ """the cuesheet handling module""" -import re -from audiotools import SheetException, parse_timestamp, build_timestamp -import gettext +from audiotools import SheetException -gettext.install("audiotools", unicode=True) ################### #Cue Sheet Parsing @@ -56,6 +53,8 @@ token is an integer such as TAG or NUMBER line is a line number integer""" + import re + full_length = len(cuedata) cuedata = cuedata.lstrip('efbbbf'.decode('hex')) line_number = 1 @@ -101,8 +100,9 @@ break if (len(cuedata) > 0): - raise CueException(_(u"Invalid token at char %d") % \ - (full_length - len(cuedata))) + from .text import ERR_CUE_INVALID_TOKEN + raise CueException(ERR_CUE_INVALID_TOKEN % + (full_length - len(cuedata))) def get_value(tokens, accept, error): @@ -120,9 +120,10 @@ if ((element & accept) != 0): return token else: - raise CueException(_(u"%(error)s at line %(line)d") % \ - {"error": error, - "line": line_number}) + from .text import ERR_CUE_ERROR + raise CueException(ERR_CUE_ERROR % + {"error": error, + "line": line_number}) def parse(tokens): @@ -131,6 +132,19 @@ raises CueException if a parsing error occurs """ + from .text import (ERR_CUE_INVALID_TRACK_NUMBER, + ERR_CUE_INVALID_TRACK_TYPE, + ERR_CUE_EXCESS_DATA, + ERR_CUE_MISSING_FILENAME, + ERR_CUE_MISSING_FILETYPE, + ERR_CUE_INVALID_TAG, + ERR_CUE_INVALID_DATA, + ERR_CUE_INVALID_FLAG, + ERR_CUE_INVALID_TIMESTAMP, + ERR_CUE_INVALID_INDEX_NUMBER, + ERR_CUE_MISSING_TAG, + ERR_CUE_MISSING_VALUE) + def skip_to_eol(tokens): (token, element, line_number) = tokens.next() while (element != EOL): @@ -154,9 +168,9 @@ cuesheet.tracks[track.number] = track track = Track(get_value(tokens, NUMBER, - _(u"Invalid track number")), + ERR_CUE_INVALID_TRACK_NUMBER), get_value(tokens, TAG | STRING, - _(u"Invalid track type"))) + ERR_CUE_INVALID_TRACK_TYPE)) get_value(tokens, EOL, "Excess data") @@ -169,25 +183,24 @@ cuesheet.attribs[token] = get_value( tokens, STRING | TAG | NUMBER | ISRC, - _(u"Missing value")) + ERR_CUE_MISSING_VALUE) - get_value(tokens, EOL, _(u"Excess data")) + get_value(tokens, EOL, ERR_CUE_EXCESS_DATA) elif (token == 'FILE'): filename = get_value(tokens, STRING, - _(u"Missing filename")) + ERR_CUE_MISSING_FILENAME) filetype = get_value(tokens, STRING | TAG, - _(u"Missing file type")) + ERR_CUE_MISSING_FILETYPE) cuesheet.attribs[token] = (filename, filetype) - get_value(tokens, EOL, _(u"Excess data")) + get_value(tokens, EOL, ERR_CUE_EXCESS_DATA) else: - raise CueException( - _(u"Invalid tag %(tag)s at line %(line)d") % \ - {"tag": token, - "line": line_number}) + raise CueException(ERR_CUE_INVALID_TAG % + {"tag": token, + "line": line_number}) #otherwise, we're adding data to the current track else: if (token in ('ISRC', 'PERFORMER', @@ -197,45 +210,44 @@ STRING | TAG | NUMBER | ISRC, "Missing value") - get_value(tokens, EOL, _(u"Invalid data")) + get_value(tokens, EOL, ERR_CUE_INVALID_DATA) elif (token == 'FLAGS'): flags = [] s = get_value(tokens, STRING | TAG | EOL, - _(u"Invalid flag")) + ERR_CUE_INVALID_FLAG) while (('\n' not in s) and ('\r' not in s)): flags.append(s) s = get_value(tokens, STRING | TAG | EOL, - _(u"Invalid flag")) + ERR_CUE_INVALID_FLAG) track.attribs[token] = ",".join(flags) elif (token in ('POSTGAP', 'PREGAP')): track.attribs[token] = get_value( tokens, TIMESTAMP, - _(u"Invalid timestamp")) - get_value(tokens, EOL, _(u"Excess data")) + ERR_CUE_INVALID_TIMESTAMP) + get_value(tokens, EOL, ERR_CUE_EXCESS_DATA) elif (token == 'INDEX'): index_number = get_value(tokens, NUMBER, - _(u"Invalid index number")) + ERR_CUE_INVALID_INDEX_NUMBER) index_timestamp = get_value(tokens, TIMESTAMP, - _(u"Invalid timestamp")) + ERR_CUE_INVALID_TIMESTAMP) track.indexes[index_number] = index_timestamp - get_value(tokens, EOL, _(u"Excess data")) + get_value(tokens, EOL, ERR_CUE_EXCESS_DATA) elif (token in ('FILE',)): skip_to_eol(tokens) else: - raise CueException( - _(u"Invalid tag %(tag)s at line %(line)d") % \ - {"tag": token, - "line": line_number}) + raise CueException(ERR_CUE_INVALID_TAG % + {"tag": token, + "line": line_number}) else: - raise CueException(_(u"Missing tag at line %d") % ( - line_number)) + raise CueException(ERR_CUE_MISSING_TAG % + (line_number,)) except StopIteration: if (track is not None): cuesheet.tracks[track.number] = track @@ -243,6 +255,8 @@ def __attrib_str__(attrib): + import re + if (isinstance(attrib, tuple)): return " ".join([__attrib_str__(a) for a in attrib]) elif (re.match(r'^[A-Z]+$', attrib) is not None): @@ -264,7 +278,7 @@ def __str__(self): return "\r\n".join(["%s %s" % (key, __attrib_str__(value)) - for key, value in self.attribs.items()] + \ + for key, value in self.attribs.items()] + [str(track) for track in sorted(self.tracks.values())]) @@ -299,7 +313,7 @@ [self.tracks[key].indexes[k] for k in sorted(self.tracks[key].indexes.keys())]) - def pcm_lengths(self, total_length): + def pcm_lengths(self, total_length, sample_rate): """yields a list of PCM lengths for all audio tracks within the file total_length is the length of the entire file in PCM frames""" @@ -311,8 +325,9 @@ if (previous is None): previous = current else: - track_length = (current[max(current.keys())] - - previous[max(previous.keys())]) * (44100 / 75) + track_length = ((current[max(current.keys())] - + previous[max(previous.keys())]) * + sample_rate / 75) total_length -= track_length yield track_length previous = current @@ -336,6 +351,7 @@ """ import cStringIO + from . import build_timestamp catalog = sheet.catalog() # a catalog string, or None indexes = list(sheet.indexes()) # a list of index tuples @@ -384,9 +400,9 @@ def __str__(self): return (" TRACK %2.2d %s\r\n" % (self.number, self.type)) + \ "\r\n".join([" %s %s" % (key, __attrib_str__(value)) - for key, value in self.attribs.items()] + \ - [" INDEX %2.2d %2.2d:%2.2d:%2.2d" % \ - (k, v / 75 / 60, v / 75 % 60, v % 75) + for key, value in self.attribs.items()] + + [" INDEX %2.2d %2.2d:%2.2d:%2.2d" % + (k, v / 75 / 60, v / 75 % 60, v % 75) for (k, v) in sorted(self.indexes.items())]) def ISRC(self): @@ -407,11 +423,13 @@ try: f = open(filename, 'r') except IOError, msg: - raise CueException(unicode(_(u"Unable to read cuesheet"))) + from .text import ERR_CUE_IOERROR + raise CueException(ERR_CUE_IOERROR) try: sheet = parse(tokens(f.read())) if (not sheet.single_file_type()): - raise CueException(_(u"Cuesheet not formatted for disc images")) + from .text import ERR_CUE_INVALID_FORMAT + raise CueException(ERR_CUE_INVALID_FORMAT) else: return sheet finally:
View file
audiotools-2.18.tar.gz/audiotools/delta.py -> audiotools-2.19.tar.gz/audiotools/delta.py
Changed
@@ -27,7 +27,7 @@ import subprocess import tempfile import whichdb -from audiotools import BIN, transfer_data +from . import BIN, transfer_data import cStringIO @@ -102,9 +102,8 @@ self.cursor.execute( "INSERT INTO patch (patch_id, patch_data) VALUES (?, ?)", [None, - base64.b64encode( - UndoDB.build_patch(file_data1, - file_data2)).decode('ascii')]) + base64.b64encode(UndoDB.build_patch(file_data1, + file_data2)).decode('ascii')]) patch_id = self.cursor.lastrowid try: self.cursor.execute("""INSERT INTO source_file (
View file
audiotools-2.19.tar.gz/audiotools/dvda.py
Added
@@ -0,0 +1,582 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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 + + +class DVDAudio: + """an object representing an entire DVD-Audio disc + + a DVDAudio object contains one or more DVDATitle objects + (accessible via the .titlesets attribute) + typically, only the first DVDTitle is interesting + each DVDATitle then contains one or more DVDATrack objects + """ + + SECTOR_SIZE = 2048 + PTS_PER_SECOND = 90000 + + def __init__(self, audio_ts_path, cdrom_device=None): + """a DVD-A which contains PCMReader-compatible track objects""" + + import re + import os + import os.path + + self.audio_ts_path = audio_ts_path + self.cdrom_device = cdrom_device + + #an inventory of AUDIO_TS files converted to uppercase keys + self.files = dict([(name.upper(), + os.path.join(audio_ts_path, name)) + for name in os.listdir(audio_ts_path)]) + + titleset_numbers = list(self.__titlesets__()) + + #for each titleset, read an ATS_XX_0.IFO file + #each titleset contains one or more DVDATitle objects + #and each DVDATitle object contains one or more DVDATrack objects + self.titlesets = [self.__titles__(titleset) for titleset in + titleset_numbers] + + #for each titleset, calculate the lengths of the corresponding AOBs + #in terms of 2048 byte sectors + self.aob_sectors = [] + for titleset in titleset_numbers: + aob_re = re.compile("ATS_%2.2d_\\d\\.AOB" % (titleset)) + titleset_aobs = dict([(key, value) for (key, value) in + self.files.items() + if (aob_re.match(key))]) + for aob_length in [os.path.getsize(titleset_aobs[key]) / + DVDAudio.SECTOR_SIZE + for key in sorted(titleset_aobs.keys())]: + if (len(self.aob_sectors) == 0): + self.aob_sectors.append( + (0, aob_length)) + else: + self.aob_sectors.append( + (self.aob_sectors[-1][1], + self.aob_sectors[-1][1] + aob_length)) + + def __getitem__(self, key): + return self.titlesets[key] + + def __len__(self): + return len(self.titlesets) + + def __titlesets__(self): + """return valid audio titleset integers from AUDIO_TS.IFO""" + + from .bitstream import BitstreamReader + + try: + f = open(self.files['AUDIO_TS.IFO'], 'rb') + except (KeyError, IOError): + from .text import ERR_DVDA_IOERROR_AUDIO_TS + raise InvalidDVDA(ERR_DVDA_IOERROR_AUDIO_TS) + try: + (identifier, + AMG_start_sector, + AMGI_end_sector, + DVD_version, + volume_count, + volume_number, + disc_side, + autoplay, + ts_to_sv, + video_titlesets, + audio_titlesets, + provider_information) = BitstreamReader(f, 0).parse( + "12b 32u 12P 32u 16u 4P 16u 16u 8u 4P 8u 32u 10P 8u 8u 40b") + + if (identifier != 'DVDAUDIO-AMG'): + from .text import ERR_DVDA_INVALID_AUDIO_TS + raise InvalidDVDA(ERR_DVDA_INVALID_AUDIO_TS) + + for titleset in xrange(1, audio_titlesets + 1): + #ensure there are IFO files and AOBs + #for each valid titleset + if (("ATS_%2.2d_0.IFO" % (titleset) in + self.files.keys()) and + ("ATS_%2.2d_1.AOB" % (titleset) in + self.files.keys())): + yield titleset + finally: + f.close() + + def __titles__(self, titleset): + """returns a list of DVDATitle objects for the given titleset""" + + #this requires bouncing all over the ATS_XX_0.IFO file + + import os + from .bitstream import BitstreamReader + + try: + f = open(self.files['ATS_%2.2d_0.IFO' % (titleset)], 'rb') + except (KeyError, IOError): + from .text import ERR_DVDA_IOERROR_ATS + raise InvalidDVDA(ERR_DVDA_IOERROR_ATS % (titleset)) + try: + #ensure the file's identifier is correct + #which is all we care about from the first sector + if (f.read(12) != 'DVDAUDIO-ATS'): + from .text import ERR_DVDA_INVALID_ATS + raise InvalidDVDA(ERR_DVDA_INVALID_ATS % (titleset)) + + #seek to the second sector and read the title count + #and list of title table offset values + f.seek(DVDAudio.SECTOR_SIZE, os.SEEK_SET) + ats_reader = BitstreamReader(f, 0) + (title_count, last_byte_address) = ats_reader.parse("16u 16p 32u") + title_offsets = [ats_reader.parse("8u 24p 32u")[1] for title in + xrange(title_count)] + + titles = [] + + for (title_number, title_offset) in enumerate(title_offsets): + #for each title, seek to its title table + #and read the title's values and its track timestamps + f.seek(DVDAudio.SECTOR_SIZE + title_offset, os.SEEK_SET) + ats_reader = BitstreamReader(f, 0) + (tracks, + indexes, + track_length, + sector_pointers_table) = ats_reader.parse( + "16p 8u 8u 32u 4P 16u 2P") + timestamps = [ats_reader.parse("32p 8u 8p 32u 32u 48p") + for track in xrange(tracks)] + + #seek to the title's sector pointers table + #and read the first and last sector values for title's tracks + f.seek(DVDAudio.SECTOR_SIZE + + title_offset + + sector_pointers_table, + os.SEEK_SET) + ats_reader = BitstreamReader(f, 0) + sector_pointers = [ats_reader.parse("32u 32u 32u") + for i in xrange(indexes)] + if ((len(sector_pointers) > 1) and + (set([p[0] for p in sector_pointers[1:]]) != + set([0x01000000]))): + from .text import ERR_DVDA_INVALID_SECTOR_POINTER + raise InvalidDVDA(ERR_DVDA_INVALID_SECTOR_POINTER) + else: + sector_pointers = [None] + sector_pointers + + #build a preliminary DVDATitle object + #which we'll populate with track data + dvda_title = DVDATitle(dvdaudio=self, + titleset=titleset, + title=title_number + 1, + pts_length=track_length, + tracks=[]) + + #for each track, determine its first and last sector + #based on the sector pointers between the track's + #initial index and the next track's initial index + for (track_number, + (timestamp, + next_timestamp)) in enumerate(zip(timestamps, + timestamps[1:])): + (index_number, first_pts, pts_length) = timestamp + next_timestamp_index = next_timestamp[0] + dvda_title.tracks.append( + DVDATrack( + dvdaudio=self, + titleset=titleset, + title=dvda_title, + track=track_number + 1, + first_pts=first_pts, + pts_length=pts_length, + first_sector=sector_pointers[index_number][1], + last_sector=sector_pointers[ + next_timestamp_index - 1][2])) + + #for the last track, its sector pointers + #simply consume what remains on the list + (index_number, first_pts, pts_length) = timestamps[-1] + dvda_title.tracks.append( + DVDATrack( + dvdaudio=self, + titleset=titleset, + title=dvda_title, + track=len(timestamps), + first_pts=first_pts, + pts_length=pts_length, + first_sector=sector_pointers[index_number][1], + last_sector=sector_pointers[-1][2])) + + #fill in the title's info such as sample_rate, channels, etc. + dvda_title.__parse_info__() + + titles.append(dvda_title) + + return titles + finally: + f.close() + + +class InvalidDVDA(Exception): + pass + + +class DVDATitle: + """an object representing a DVD-Audio title + + contains one or more DVDATrack objects + which may are accessible via __getitem__ + """ + + def __init__(self, dvdaudio, titleset, title, pts_length, tracks): + """length is in PTS ticks, tracks is a list of DVDATrack objects""" + + self.dvdaudio = dvdaudio + self.titleset = titleset + self.title = title + self.pts_length = pts_length + self.tracks = tracks + + self.sample_rate = 0 + self.channels = 0 + self.channel_mask = 0 + self.bits_per_sample = 0 + self.stream_id = 0 + + def __parse_info__(self): + """generates a cache of sample_rate, bits-per-sample, etc""" + + import re + import os.path + from .bitstream import BitstreamReader + + if (len(self.tracks) == 0): + return + + #Why is this post-init processing necessary? + #DVDATrack references DVDATitle + #so a DVDATitle must exist when DVDATrack is initialized. + #But because reading this info requires knowing the sector + #of the first track, we wind up with a circular dependency. + #Doing a "post-process" pass fixes that problem. + + #find the AOB file of the title's first track + track_sector = self[0].first_sector + titleset = re.compile("ATS_%2.2d_\\d\\.AOB" % (self.titleset)) + for aob_path in sorted([self.dvdaudio.files[key] for key in + self.dvdaudio.files.keys() + if (titleset.match(key))]): + aob_sectors = os.path.getsize(aob_path) / DVDAudio.SECTOR_SIZE + if (track_sector > aob_sectors): + track_sector -= aob_sectors + else: + break + else: + from .text import ERR_DVDA_NO_TRACK_SECTOR + raise ValueError(ERR_DVDA_NO_TRACK_SECTOR) + + #open that AOB file and seek to that track's first sector + aob_file = open(aob_path, 'rb') + try: + aob_file.seek(track_sector * DVDAudio.SECTOR_SIZE) + aob_reader = BitstreamReader(aob_file, 0) + + #read and validate the pack header + #(there's one pack header per sector, at the sector's start) + (sync_bytes, + marker1, + current_pts_high, + marker2, + current_pts_mid, + marker3, + current_pts_low, + marker4, + scr_extension, + marker5, + bit_rate, + marker6, + stuffing_length) = aob_reader.parse( + "32u 2u 3u 1u 15u 1u 15u 1u 9u 1u 22u 2u 5p 3u") + aob_reader.skip_bytes(stuffing_length) + if (sync_bytes != 0x1BA): + from .text import ERR_DVDA_INVALID_AOB_SYNC + raise InvalidDVDA(ERR_DVDA_INVALID_AOB_SYNC) + if (((marker1 != 1) or (marker2 != 1) or (marker3 != 1) or + (marker4 != 1) or (marker5 != 1) or (marker6 != 3))): + from .text import ERR_DVDA_INVALID_AOB_MARKER + raise InvalidDVDA(ERR_DVDA_INVALID_AOB_MARKER) + packet_pts = ((current_pts_high << 30) | + (current_pts_mid << 15) | + current_pts_low) + + #skip packets until one with a stream ID of 0xBD is found + (start_code, + stream_id, + packet_length) = aob_reader.parse("24u 8u 16u") + if (start_code != 1): + from .text import ERR_DVDA_INVALID_AOB_START + raise InvalidDVDA(ERR_DVDA_INVALID_AOB_START) + while (stream_id != 0xBD): + aob_reader.skip_bytes(packet_length) + (start_code, + stream_id, + packet_length) = aob_reader.parse("24u 8u 16u") + if (start_code != 1): + from .text import ERR_DVDA_INVALID_AOB_START + raise InvalidDVDA(ERR_DVDA_INVALID_AOB_START) + + #parse the PCM/MLP header in the packet data + (pad1_size,) = aob_reader.parse("16p 8u") + aob_reader.skip_bytes(pad1_size) + (stream_id, crc) = aob_reader.parse("8u 8u 8p") + if (stream_id == 0xA0): # PCM + #read a PCM reader + (pad2_size, + first_audio_frame, + padding2, + group1_bps, + group2_bps, + group1_sample_rate, + group2_sample_rate, + padding3, + channel_assignment) = aob_reader.parse( + "8u 16u 8u 4u 4u 4u 4u 8u 8u") + else: # MLP + aob_reader.skip_bytes(aob_reader.read(8)) # skip pad2 + #read a total frame size + MLP major sync header + (total_frame_size, + sync_words, + stream_type, + group1_bps, + group2_bps, + group1_sample_rate, + group2_sample_rate, + unknown1, + channel_assignment, + unknown2) = aob_reader.parse( + "4p 12u 16p 24u 8u 4u 4u 4u 4u 11u 5u 48u") + + #return the values indicated by the header + self.sample_rate = DVDATrack.SAMPLE_RATE[group1_sample_rate] + self.channels = DVDATrack.CHANNELS[channel_assignment] + self.channel_mask = DVDATrack.CHANNEL_MASK[channel_assignment] + self.bits_per_sample = DVDATrack.BITS_PER_SAMPLE[group1_bps] + self.stream_id = stream_id + + finally: + aob_file.close() + + def __len__(self): + return len(self.tracks) + + def __getitem__(self, index): + return self.tracks[index] + + def __repr__(self): + return "DVDATitle(%s)" % \ + (",".join(["%s=%s" % (key, getattr(self, key)) + for key in ["titleset", "title", "pts_length", + "tracks"]])) + + def info(self): + """returns a (sample_rate, channels, channel_mask, bps, type) tuple""" + + return (self.sample_rate, + self.channels, + self.channel_mask, + self.bits_per_sample, + self.stream_id) + + def to_pcm(self): + """returns a PCMReader-compatible object of Title data + this PCMReader requires the use of its next_track() method + which indicates the total number of PTS ticks to read + from the next track in the title""" + + from audiotools.decoders import DVDA_Title + + args = {"audio_ts": self.dvdaudio.audio_ts_path, + "titleset": 1, + "start_sector": self.tracks[0].first_sector, + "end_sector": self.tracks[-1].last_sector} + if (self.dvdaudio.cdrom_device is not None): + args["cdrom"] = self.dvdaudio.cdrom_device + return DVDA_Title(**args) + + def total_frames(self): + """returns the title's total PCM frames as an integer""" + + import decimal + + return int((decimal.Decimal(self.pts_length) / + DVDAudio.PTS_PER_SECOND * + self.sample_rate).quantize(decimal.Decimal(1), + rounding=decimal.ROUND_UP)) + + def metadata_lookup(self, musicbrainz_server="musicbrainz.org", + musicbrainz_port=80, + freedb_server="us.freedb.org", + freedb_port=80, + use_musicbrainz=True, + use_freedb=True): + """generates a set of MetaData objects from DVD title + + returns a metadata[c][t] list of lists + where 'c' is a possible choice + and 't' is the MetaData for a given track (starting from 0) + + this will always return at least one choice, + which may be a list of largely empty MetaData objects + if no match can be found for the title""" + + from audiotools import metadata_lookup + + PTS_PER_FRAME = DVDAudio.PTS_PER_SECOND / 75 + + offsets = [150] + for track in self.tracks[0:-1]: + offsets.append(offsets[-1] + (track.pts_length / PTS_PER_FRAME)) + + return metadata_lookup(first_track_number=1, + last_track_number=len(self), + offsets=offsets, + lead_out_offset=self.pts_length / PTS_PER_FRAME, + total_length=self.pts_length / PTS_PER_FRAME, + musicbrainz_server=musicbrainz_server, + musicbrainz_port=musicbrainz_port, + freedb_server=freedb_server, + freedb_port=freedb_port, + use_musicbrainz=use_musicbrainz, + use_freedb=use_freedb) + + +class DVDATrack: + """an object representing an individual DVD-Audio track""" + + SAMPLE_RATE = [48000, 96000, 192000, 0, 0, 0, 0, 0, + 44100, 88200, 176400, 0, 0, 0, 0, 0] + CHANNELS = [1, 2, 3, 4, 3, 4, 5, 3, 4, 5, 4, 5, 6, 4, 5, 4, 5, 6, 5, 5, 6] + CHANNEL_MASK = [0x4, 0x3, 0x103, 0x33, 0xB, 0x10B, 0x3B, 0x7, + 0x107, 0x37, 0xF, 0x10F, 0x3F, 0x107, 0x37, 0xF, + 0x10F, 0x3F, 0x3B, 0x37, 0x3F] + BITS_PER_SAMPLE = [16, 20, 24] + [0] * 13 + + def __init__(self, dvdaudio, + titleset, title, track, + first_pts, pts_length, + first_sector, last_sector): + self.dvdaudio = dvdaudio + self.titleset = titleset + self.title = title + self.track = track + self.first_pts = first_pts + self.pts_length = pts_length + self.first_sector = first_sector + self.last_sector = last_sector + + def __repr__(self): + return "DVDATrack(%s)" % \ + (", ".join(["%s=%s" % (attr, getattr(self, attr)) + for attr in ["titleset", + "title", + "track", + "first_pts", + "pts_length", + "first_sector", + "last_sector"]])) + + def total_frames(self): + """returns the track's total PCM frames as an integer + + this is based on its PTS ticks and the title's sample rate""" + + import decimal + + return int((decimal.Decimal(self.pts_length) / + DVDAudio.PTS_PER_SECOND * + self.title.sample_rate).quantize(decimal.Decimal(1), + rounding=decimal.ROUND_UP) + ) + + def sectors(self): + """iterates (aob_file, start_sector, end_sector) + + for each AOB file necessary to extract the track's data + in the order in which they should be read""" + + track_sectors = Rangeset(self.first_sector, + self.last_sector + 1) + + for (i, (start_sector, + end_sector)) in enumerate(self.dvdaudio.aob_sectors): + aob_sectors = Rangeset(start_sector, end_sector) + intersection = aob_sectors & track_sectors + if (len(intersection)): + yield (self.dvdaudio.files["ATS_%2.2d_%d.AOB" % + (self.titleset, i + 1)], + intersection.start - start_sector, + intersection.end - start_sector) + + +class Rangeset: + """an optimized combination of range() and set()""" + + #The purpose of this class is for finding the subset of + #two Rangesets, such as with: + # + # >>> Rangeset(1, 10) & Rangeset(5, 15) + # Rangeset(5, 10) + # + #which returns another Rangeset object. + #This is preferable to performing: + # + # >>> set(range(1, 10)) & set(range(5, 15)) + # set([8, 9, 5, 6, 7]) + # + #which allocates lots of unnecessary values + #when all we're interested in is the min and max. + + def __init__(self, start, end): + self.start = start + self.end = end + + def __repr__(self): + return "Rangeset(%s, %s)" % (repr(self.start), repr(self.end)) + + def __len__(self): + return int(self.end - self.start) + + def __getitem__(self, i): + if (i >= 0): + if (i < len(self)): + return self.start + i + else: + raise IndexError(i) + else: + if (-i - 1 < len(self)): + return self.end + i + else: + raise IndexError(i) + + def __and__(self, rangeset): + min_point = max(self.start, rangeset.start) + max_point = min(self.end, rangeset.end) + + if (min_point <= max_point): + return Rangeset(min_point, max_point) + else: + return Rangeset(0, 0)
View file
audiotools-2.19.tar.gz/audiotools/flac.py
Added
@@ -0,0 +1,3356 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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 . import (AudioFile, MetaData, InvalidFile, Image, + WaveContainer, AiffContainer) +from .vorbiscomment import VorbisComment +from .id3 import skip_id3v2_comment + + +####################### +#FLAC +####################### + + +class InvalidFLAC(InvalidFile): + pass + + +class FlacMetaDataBlockTooLarge(Exception): + """raised if one attempts to build a FlacMetaDataBlock too large""" + + pass + + +class FlacMetaData(MetaData): + """a class for managing a native FLAC's metadata""" + + def __init__(self, blocks): + self.__dict__["block_list"] = list(blocks) + + def has_block(self, block_id): + """returns True if the given block ID is present""" + + return block_id in [b.BLOCK_ID for b in self.block_list] + + def add_block(self, block): + """adds the given block to our list of blocks""" + + #the specification only requires that STREAMINFO be first + #the rest are largely arbitrary, + #though I like to keep PADDING as the last block for aesthetic reasons + PREFERRED_ORDER = [Flac_STREAMINFO.BLOCK_ID, + Flac_SEEKTABLE.BLOCK_ID, + Flac_CUESHEET.BLOCK_ID, + Flac_VORBISCOMMENT.BLOCK_ID, + Flac_PICTURE.BLOCK_ID, + Flac_APPLICATION.BLOCK_ID, + Flac_PADDING.BLOCK_ID] + + stop_blocks = set( + PREFERRED_ORDER[PREFERRED_ORDER.index(block.BLOCK_ID) + 1:]) + + for (index, old_block) in enumerate(self.block_list): + if (old_block.BLOCK_ID in stop_blocks): + self.block_list.insert(index, block) + break + else: + self.block_list.append(block) + + def get_block(self, block_id): + """returns the first instance of the given block_id + + may raise IndexError if the block is not in our list of blocks""" + + for block in self.block_list: + if (block.BLOCK_ID == block_id): + return block + else: + raise IndexError() + + def get_blocks(self, block_id): + """returns all instances of the given block_id in our list of blocks""" + + return [b for b in self.block_list if (b.BLOCK_ID == block_id)] + + def replace_blocks(self, block_id, blocks): + """replaces all instances of the given block_id with + blocks taken from the given list + + if insufficient matching blocks are present, + this uses add_block() to populate the remainder + + if additional matching blocks are present, + they are removed + """ + + new_blocks = [] + + for block in self.block_list: + if (block.BLOCK_ID == block_id): + if (len(blocks) > 0): + new_blocks.append(blocks.pop(0)) + else: + pass + else: + new_blocks.append(block) + + self.block_list = new_blocks + + while (len(blocks) > 0): + self.add_block(blocks.pop(0)) + + def __setattr__(self, key, value): + if (key in self.FIELDS): + try: + vorbis_comment = self.get_block(Flac_VORBISCOMMENT.BLOCK_ID) + except IndexError: + #add VORBIS comment block if necessary + from . import VERSION + + vorbis_comment = Flac_VORBISCOMMENT( + [], u"Python Audio Tools %s" % (VERSION)) + + self.add_block(vorbis_comment) + + setattr(vorbis_comment, key, value) + else: + self.__dict__[key] = value + + def __getattr__(self, key): + if (key in self.FIELDS): + try: + return getattr(self.get_block(Flac_VORBISCOMMENT.BLOCK_ID), + key) + except IndexError: + #no VORBIS comment block, so all values are None + return None + else: + try: + return self.__dict__[key] + except KeyError: + raise AttributeError(key) + + def __delattr__(self, key): + if (key in self.FIELDS): + try: + delattr(self.get_block(Flac_VORBISCOMMENT.BLOCK_ID), key) + except IndexError: + #no VORBIS comment block, so nothing to delete + pass + else: + try: + del(self.__dict__[key]) + except KeyError: + raise AttributeError(key) + + @classmethod + def converted(cls, metadata): + """takes a MetaData object and returns a FlacMetaData object""" + + if (metadata is None): + return None + elif (isinstance(metadata, FlacMetaData)): + return cls([block.copy() for block in metadata.block_list]) + else: + return cls([Flac_VORBISCOMMENT.converted(metadata)] + + [Flac_PICTURE.converted(image) + for image in metadata.images()] + + [Flac_PADDING(4096)]) + + def add_image(self, image): + """embeds an Image object in this metadata""" + + self.add_block(Flac_PICTURE.converted(image)) + + def delete_image(self, image): + """deletes an image object from this metadata""" + + self.block_list = [b for b in self.block_list + if not ((b.BLOCK_ID == Flac_PICTURE.BLOCK_ID) and + (b == image))] + + def images(self): + """returns a list of embedded Image objects""" + + return self.get_blocks(Flac_PICTURE.BLOCK_ID) + + @classmethod + def supports_images(cls): + """returns True""" + + return True + + def clean(self, fixes_performed): + """returns a new FlacMetaData object that's been cleaned of problems + + any fixes performed are appended to fixes_performed as unicode""" + + from .text import (CLEAN_FLAC_REORDERED_STREAMINFO, + CLEAN_FLAC_MULITPLE_STREAMINFO, + CLEAN_FLAC_MULTIPLE_VORBISCOMMENT, + CLEAN_FLAC_MULTIPLE_SEEKTABLE, + CLEAN_FLAC_MULTIPLE_CUESHEET, + CLEAN_FLAC_UNDEFINED_BLOCK) + + cleaned_blocks = [] + + for block in self.block_list: + if (block.BLOCK_ID == Flac_STREAMINFO.BLOCK_ID): + #reorder STREAMINFO block to be first, if necessary + if (len(cleaned_blocks) == 0): + cleaned_blocks.append(block) + elif (cleaned_blocks[0].BLOCK_ID != block.BLOCK_ID): + fixes_performed.append( + CLEAN_FLAC_REORDERED_STREAMINFO) + cleaned_blocks.insert(0, block) + else: + fixes_performed.append( + CLEAN_FLAC_MULITPLE_STREAMINFO) + elif (block.BLOCK_ID == Flac_VORBISCOMMENT.BLOCK_ID): + if (block.BLOCK_ID in [b.BLOCK_ID for b in cleaned_blocks]): + #remove redundant VORBIS_COMMENT blocks + fixes_performed.append( + CLEAN_FLAC_MULTIPLE_VORBISCOMMENT) + else: + #recursively clean up the text fields in FlacVorbisComment + cleaned_blocks.append(block.clean(fixes_performed)) + elif (block.BLOCK_ID == Flac_PICTURE.BLOCK_ID): + #recursively clean up any image blocks + cleaned_blocks.append(block.clean(fixes_performed)) + elif (block.BLOCK_ID == Flac_APPLICATION.BLOCK_ID): + cleaned_blocks.append(block) + elif (block.BLOCK_ID == Flac_SEEKTABLE.BLOCK_ID): + #remove redundant seektable, if necessary + if (block.BLOCK_ID in [b.BLOCK_ID for b in cleaned_blocks]): + fixes_performed.append( + CLEAN_FLAC_MULTIPLE_SEEKTABLE) + else: + cleaned_blocks.append(block.clean(fixes_performed)) + elif (block.BLOCK_ID == Flac_CUESHEET.BLOCK_ID): + #remove redundant cuesheet, if necessary + if (block.BLOCK_ID in [b.BLOCK_ID for b in cleaned_blocks]): + fixes_performed.append( + CLEAN_FLAC_MULTIPLE_CUESHEET) + else: + cleaned_blocks.append(block) + elif (block.BLOCK_ID == Flac_PADDING.BLOCK_ID): + cleaned_blocks.append(block) + else: + #remove undefined blocks + fixes_performed.append(CLEAN_FLAC_UNDEFINED_BLOCK) + + return self.__class__(cleaned_blocks) + + def __repr__(self): + return "FlacMetaData(%s)" % (self.block_list) + + @classmethod + def parse(cls, reader): + """returns a FlacMetaData object from the given BitstreamReader + which has already parsed the 4-byte 'fLaC' file ID""" + + block_list = [] + + last = 0 + + while (last != 1): + (last, block_type, block_length) = reader.parse("1u7u24u") + + if (block_type == 0): # STREAMINFO + block_list.append( + Flac_STREAMINFO.parse(reader.substream(block_length))) + elif (block_type == 1): # PADDING + block_list.append( + Flac_PADDING.parse( + reader.substream(block_length), block_length)) + elif (block_type == 2): # APPLICATION + block_list.append( + Flac_APPLICATION.parse( + reader.substream(block_length), block_length)) + elif (block_type == 3): # SEEKTABLE + block_list.append( + Flac_SEEKTABLE.parse( + reader.substream(block_length), block_length / 18)) + elif (block_type == 4): # VORBIS_COMMENT + block_list.append( + Flac_VORBISCOMMENT.parse( + reader.substream(block_length))) + elif (block_type == 5): # CUESHEET + block_list.append( + Flac_CUESHEET.parse(reader.substream(block_length))) + elif (block_type == 6): # PICTURE + block_list.append( + Flac_PICTURE.parse(reader.substream(block_length))) + elif ((block_type >= 7) and (block_type <= 126)): + from .text import ERR_FLAC_RESERVED_BLOCK + raise ValueError(ERR_FLAC_RESERVED_BLOCK % (block_type)) + else: + from .text import ERR_FLAC_INVALID_BLOCK + raise ValueError(ERR_FLAC_INVALID_BLOCK) + + return cls(block_list) + + def raw_info(self): + """returns human-readable metadata as a unicode string""" + + from os import linesep + + return linesep.decode('ascii').join( + ["FLAC Tags:"] + [block.raw_info() for block in self.blocks()]) + + def blocks(self): + """yields FlacMetaData's individual metadata blocks""" + + for block in self.block_list: + yield block + + def build(self, writer): + """writes the FlacMetaData to the given BitstreamWriter + not including the 4-byte 'fLaC' file ID""" + + from . import iter_last + + for (last_block, + block) in iter_last(iter([b for b in self.blocks() + if (b.size() < (2 ** 24))])): + if (not last_block): + writer.build("1u7u24u", (0, block.BLOCK_ID, block.size())) + else: + writer.build("1u7u24u", (1, block.BLOCK_ID, block.size())) + + block.build(writer) + + def size(self): + """returns the size of all metadata blocks + including the block headers + but not including the 4-byte 'fLaC' file ID""" + + from operator import add + + return reduce(add, [4 + b.size() for b in self.block_list], 0) + + +class Flac_STREAMINFO: + BLOCK_ID = 0 + + def __init__(self, minimum_block_size, maximum_block_size, + minimum_frame_size, maximum_frame_size, + sample_rate, channels, bits_per_sample, + total_samples, md5sum): + """all values are non-negative integers except for md5sum + which is a 16-byte binary string""" + + self.minimum_block_size = minimum_block_size + self.maximum_block_size = maximum_block_size + self.minimum_frame_size = minimum_frame_size + self.maximum_frame_size = maximum_frame_size + self.sample_rate = sample_rate + self.channels = channels + self.bits_per_sample = bits_per_sample + self.total_samples = total_samples + self.md5sum = md5sum + + def copy(self): + """returns a duplicate of this metadata block""" + + return Flac_STREAMINFO(self.minimum_block_size, + self.maximum_block_size, + self.minimum_frame_size, + self.maximum_frame_size, + self.sample_rate, + self.channels, + self.bits_per_sample, + self.total_samples, + self.md5sum) + + def __eq__(self, block): + for attr in ["minimum_block_size", + "maximum_block_size", + "minimum_frame_size", + "maximum_frame_size", + "sample_rate", + "channels", + "bits_per_sample", + "total_samples", + "md5sum"]: + if ((not hasattr(block, attr)) or (getattr(self, attr) != + getattr(block, attr))): + return False + else: + return True + + def __repr__(self): + return ("Flac_STREAMINFO(%s)" % + ",".join(["%s=%s" % (key, repr(getattr(self, key))) + for key in ["minimum_block_size", + "maximum_block_size", + "minimum_frame_size", + "maximum_frame_size", + "sample_rate", + "channels", + "bits_per_sample", + "total_samples", + "md5sum"]])) + + def raw_info(self): + """returns a human-readable version of this metadata block + as unicode""" + + from os import linesep + + return linesep.decode('ascii').join( + [u" STREAMINFO:", + u" minimum block size = %d" % (self.minimum_block_size), + u" maximum block size = %d" % (self.maximum_block_size), + u" minimum frame size = %d" % (self.minimum_frame_size), + u" maximum frame size = %d" % (self.maximum_frame_size), + u" sample rate = %d" % (self.sample_rate), + u" channels = %d" % (self.channels), + u" bits-per-sample = %d" % (self.bits_per_sample), + u" total samples = %d" % (self.total_samples), + u" MD5 sum = %s" % + (u"".join(["%2.2X" % (ord(b)) for b in self.md5sum]))]) + + @classmethod + def parse(cls, reader): + """returns this metadata block from a BitstreamReader""" + + values = reader.parse("16u16u24u24u20u3u5u36U16b") + values[5] += 1 # channels + values[6] += 1 # bits-per-sample + return cls(*values) + + def build(self, writer): + """writes this metadata block to a BitstreamWriter""" + + writer.build("16u16u24u24u20u3u5u36U16b", + (self.minimum_block_size, + self.maximum_block_size, + self.minimum_frame_size, + self.maximum_frame_size, + self.sample_rate, + self.channels - 1, + self.bits_per_sample - 1, + self.total_samples, + self.md5sum)) + + def size(self): + """the size of this metadata block + not including the 4-byte block header""" + + return 34 + + +class Flac_PADDING: + BLOCK_ID = 1 + + def __init__(self, length): + self.length = length + + def copy(self): + """returns a duplicate of this metadata block""" + + return Flac_PADDING(self.length) + + def __repr__(self): + return "Flac_PADDING(%d)" % (self.length) + + def raw_info(self): + """returns a human-readable version of this metadata block + as unicode""" + + from os import linesep + + return linesep.decode('ascii').join( + [u" PADDING:", + u" length = %d" % (self.length)]) + + @classmethod + def parse(cls, reader, block_length): + """returns this metadata block from a BitstreamReader""" + + reader.skip_bytes(block_length) + return cls(length=block_length) + + def build(self, writer): + """writes this metadata block to a BitstreamWriter""" + + writer.write_bytes(chr(0) * self.length) + + def size(self): + """the size of this metadata block + not including the 4-byte block header""" + + return self.length + + +class Flac_APPLICATION: + BLOCK_ID = 2 + + def __init__(self, application_id, data): + self.application_id = application_id + self.data = data + + def __eq__(self, block): + for attr in ["application_id", "data"]: + if ((not hasattr(block, attr)) or (getattr(self, attr) != + getattr(block, attr))): + return False + else: + return True + + def copy(self): + """returns a duplicate of this metadata block""" + + return Flac_APPLICATION(self.application_id, + self.data) + + def __repr__(self): + return "Flac_APPLICATION(%s, %s)" % (repr(self.application_id), + repr(self.data)) + + def raw_info(self): + """returns a human-readable version of this metadata block + as unicode""" + + from os import linesep + + return u" APPLICATION:%s %s (%d bytes)" % \ + (linesep.decode('ascii'), + self.application_id.decode('ascii'), + len(self.data)) + + @classmethod + def parse(cls, reader, block_length): + """returns this metadata block from a BitstreamReader""" + + return cls(application_id=reader.read_bytes(4), + data=reader.read_bytes(block_length - 4)) + + def build(self, writer): + """writes this metadata block to a BitstreamWriter""" + + writer.write_bytes(self.application_id) + writer.write_bytes(self.data) + + def size(self): + """the size of this metadata block + not including the 4-byte block header""" + + return len(self.application_id) + len(self.data) + + +class Flac_SEEKTABLE: + BLOCK_ID = 3 + + def __init__(self, seekpoints): + """seekpoints is a list of + (PCM frame offset, byte offset, PCM frame count) tuples""" + self.seekpoints = seekpoints + + def __eq__(self, block): + if (hasattr(block, "seekpoints")): + return self.seekpoints == block.seekpoints + else: + return False + + def copy(self): + """returns a duplicate of this metadata block""" + + return Flac_SEEKTABLE(self.seekpoints[:]) + + def __repr__(self): + return "Flac_SEEKTABLE(%s)" % (repr(self.seekpoints)) + + def raw_info(self): + """returns a human-readable version of this metadata block + as unicode""" + + from os import linesep + + return linesep.decode('ascii').join( + [u" SEEKTABLE:", + u" first sample file offset frame samples"] + + [u" %14.1d %13.1X %15.d" % seekpoint + for seekpoint in self.seekpoints]) + + @classmethod + def parse(cls, reader, total_seekpoints): + """returns this metadata block from a BitstreamReader""" + + return cls([tuple(reader.parse("64U64U16u")) + for i in xrange(total_seekpoints)]) + + def build(self, writer): + """writes this metadata block to a BitstreamWriter""" + + for seekpoint in self.seekpoints: + writer.build("64U64U16u", seekpoint) + + def size(self): + """the size of this metadata block + not including the 4-byte block header""" + + from .bitstream import format_size + + return (format_size("64U64U16u") / 8) * len(self.seekpoints) + + def clean(self, fixes_performed): + """removes any empty seek points + and ensures PCM frame offset and byte offset + are both incrementing""" + + nonempty_points = [seekpoint for seekpoint in self.seekpoints + if (seekpoint[2] != 0)] + if (len(nonempty_points) != len(self.seekpoints)): + from .text import CLEAN_FLAC_REMOVE_SEEKPOINTS + fixes_performed.append(CLEAN_FLAC_REMOVE_SEEKPOINTS) + + ascending_order = list(set(nonempty_points)) + ascending_order.sort() + + if (ascending_order != nonempty_points): + from .text import CLEAN_FLAC_REORDER_SEEKPOINTS + fixes_performed.append(CLEAN_FLAC_REORDER_SEEKPOINTS) + + return Flac_SEEKTABLE(ascending_order) + + +class Flac_VORBISCOMMENT(VorbisComment): + BLOCK_ID = 4 + + def copy(self): + """returns a duplicate of this metadata block""" + + return Flac_VORBISCOMMENT(self.comment_strings[:], + self.vendor_string) + + def __repr__(self): + return "Flac_VORBISCOMMENT(%s, %s)" % \ + (repr(self.comment_strings), repr(self.vendor_string)) + + def raw_info(self): + """returns a human-readable version of this metadata block + as unicode""" + + from os import linesep + from . import display_unicode + + #align the text strings on the "=" sign, if any + + if (len(self.comment_strings) > 0): + max_indent = max([len(display_unicode(comment.split(u"=", 1)[0])) + for comment in self.comment_strings + if u"=" in comment]) + else: + max_indent = 0 + + comment_strings = [] + for comment in self.comment_strings: + if (u"=" in comment): + comment_strings.append( + u" " * (4 + max_indent - + len(display_unicode(comment.split(u"=", 1)[0]))) + + comment) + else: + comment_strings.append(u" " * 4 + comment) + + return linesep.decode('ascii').join( + [u" VORBIS_COMMENT:", + u" %s" % (self.vendor_string)] + + comment_strings) + + @classmethod + def converted(cls, metadata): + """converts a MetaData object to a Flac_VORBISCOMMENT object""" + + if ((metadata is None) or (isinstance(metadata, Flac_VORBISCOMMENT))): + return metadata + else: + #make VorbisComment do all the work, + #then lift its data into a new Flac_VORBISCOMMENT + metadata = VorbisComment.converted(metadata) + return cls(metadata.comment_strings, + metadata.vendor_string) + + @classmethod + def parse(cls, reader): + """returns this metadata block from a BitstreamReader""" + + reader.set_endianness(1) + vendor_string = reader.read_bytes(reader.read(32)).decode('utf-8', + 'replace') + return cls([reader.read_bytes(reader.read(32)).decode('utf-8', + 'replace') + for i in xrange(reader.read(32))], vendor_string) + + def build(self, writer): + """writes this metadata block to a BitstreamWriter""" + + writer.set_endianness(1) + vendor_string = self.vendor_string.encode('utf-8') + writer.build("32u%db" % (len(vendor_string)), + (len(vendor_string), vendor_string)) + writer.write(32, len(self.comment_strings)) + for comment_string in self.comment_strings: + comment_string = comment_string.encode('utf-8') + writer.build("32u%db" % (len(comment_string)), + (len(comment_string), comment_string)) + writer.set_endianness(0) + + def size(self): + """the size of this metadata block + not including the 4-byte block header""" + + from operator import add + + return (4 + len(self.vendor_string.encode('utf-8')) + + 4 + + reduce(add, [4 + len(comment.encode('utf-8')) + for comment in self.comment_strings], 0)) + + +class Flac_CUESHEET: + BLOCK_ID = 5 + + def __init__(self, catalog_number, lead_in_samples, is_cdda, tracks): + self.catalog_number = catalog_number + self.lead_in_samples = lead_in_samples + self.is_cdda = is_cdda + self.tracks = tracks + + def copy(self): + """returns a duplicate of this metadata block""" + + return Flac_CUESHEET(self.catalog_number, + self.lead_in_samples, + self.is_cdda, + [track.copy() for track in self.tracks]) + + def __eq__(self, cuesheet): + for attr in ["catalog_number", + "lead_in_samples", + "is_cdda", + "tracks"]: + if ((not hasattr(cuesheet, attr)) or (getattr(self, attr) != + getattr(cuesheet, attr))): + return False + else: + return True + + def __repr__(self): + return ("Flac_CUESHEET(%s)" % + ",".join(["%s=%s" % (key, repr(getattr(self, key))) + for key in ["catalog_number", + "lead_in_samples", + "is_cdda", + "tracks"]])) + + def raw_info(self): + """returns a human-readable version of this metadata block + as unicode""" + + from os import linesep + + return linesep.decode('ascii').join( + [u" CUESHEET:", + u" catalog number = %s" % + (self.catalog_number.decode('ascii', 'replace')), + u" lead-in samples = %d" % (self.lead_in_samples), + u" is CDDA = %d" % (self.is_cdda), + u"%9s %5s %8s %13s %12s" % (u"track", + u"audio", + u"pre-emph", + u"offset", + u"ISRC")] + + [track.raw_info() for track in self.tracks]) + + @classmethod + def parse(cls, reader): + """returns this metadata block from a BitstreamReader""" + + (catalog_number, + lead_in_samples, + is_cdda, + track_count) = reader.parse("128b64U1u2071p8u") + return cls(catalog_number, + lead_in_samples, + is_cdda, + [Flac_CUESHEET_track.parse(reader) + for i in xrange(track_count)]) + + def build(self, writer): + """writes this metadata block to a BitstreamWriter""" + + writer.build("128b64U1u2071p8u", + (self.catalog_number, + self.lead_in_samples, + self.is_cdda, + len(self.tracks))) + for track in self.tracks: + track.build(writer) + + def size(self): + """the size of this metadata block + not including the 4-byte block header""" + + from .bitstream import BitstreamAccumulator + + a = BitstreamAccumulator(0) + self.build(a) + return a.bytes() + + @classmethod + def converted(cls, sheet, total_frames, sample_rate=44100): + """converts a cuesheet compatible object to Flac_CUESHEET objects + + a total_frames integer (in PCM frames) is also required + """ + + if (sheet.catalog() is None): + catalog_number = chr(0) * 128 + else: + catalog_number = sheet.catalog() + (chr(0) * + (128 - len(sheet.catalog()))) + + ISRCs = sheet.ISRCs() + + return cls( + catalog_number=catalog_number, + lead_in_samples=sample_rate * 2, + is_cdda=1 if sample_rate == 44100 else 0, + tracks=[Flac_CUESHEET_track( + offset=indexes[0] * sample_rate / 75, + number=i + 1, + ISRC=ISRCs.get(i + 1, chr(0) * 12), + track_type=0, + pre_emphasis=0, + index_points=[ + Flac_CUESHEET_index( + offset=(index - indexes[0]) * sample_rate / 75, + number=point_number + (1 if len(indexes) == 1 + else 0)) + for (point_number, index) in enumerate(indexes)]) + for (i, indexes) in enumerate(sheet.indexes())] + + # lead-out track + [Flac_CUESHEET_track(offset=total_frames, + number=170, + ISRC=chr(0) * 12, + track_type=0, + pre_emphasis=0, + index_points=[])]) + + def catalog(self): + """returns the cuesheet's catalog number as a plain string""" + + catalog_number = self.catalog_number.rstrip(chr(0)) + + if (len(catalog_number) > 0): + return catalog_number + else: + return None + + def ISRCs(self): + """returns a dict of ISRC values as plain strings""" + + return dict([(track.number, track.ISRC) for track in + self.tracks + if ((track.number != 170) and + (len(track.ISRC.strip(chr(0))) > 0))]) + + def indexes(self, sample_rate=44100): + """returns a list of (start, end) integer tuples""" + + return [tuple([(index.offset + track.offset) * 75 / sample_rate + for index in + sorted(track.index_points, + lambda i1, i2: cmp(i1.number, i2.number))]) + for track in + sorted(self.tracks, lambda t1, t2: cmp(t1.number, t2.number)) + if (track.number != 170)] + + def pcm_lengths(self, total_length, sample_rate): + """returns a list of PCM lengths for all cuesheet audio tracks + + note that the total_length and sample_rate variables + are only for compatibility + as FLAC's CUESHEET blocks store sample counts directly + """ + + if (len(self.tracks) > 0): + return [(current.offset + + max([i.offset for i in current.index_points] + [0])) - + ((previous.offset + + max([i.offset for i in previous.index_points] + [0]))) + for (previous, current) in + zip(self.tracks, self.tracks[1:])] + else: + return [] + + +class Flac_CUESHEET_track: + def __init__(self, offset, number, ISRC, track_type, pre_emphasis, + index_points): + self.offset = offset + self.number = number + self.ISRC = ISRC + self.track_type = track_type + self.pre_emphasis = pre_emphasis + self.index_points = index_points + + def copy(self): + """returns a duplicate of this metadata block""" + + return Flac_CUESHEET_track(self.offset, + self.number, + self.ISRC, + self.track_type, + self.pre_emphasis, + [index.copy() for index in + self.index_points]) + + def __repr__(self): + return ("Flac_CUESHEET_track(%s)" % + ",".join(["%s=%s" % (key, repr(getattr(self, key))) + for key in ["offset", + "number", + "ISRC", + "track_type", + "pre_emphasis", + "index_points"]])) + + def raw_info(self): + """returns a human-readable version of this track as unicode""" + + if (len(self.ISRC.strip(chr(0))) > 0): + return u"%9.d %5s %8s %13.d %12s" % \ + (self.number, + u"yes" if self.track_type == 0 else u"no", + u"yes" if self.pre_emphasis == 1 else u"no", + self.offset, + self.ISRC) + else: + return u"%9.d %5s %8s %13.d" % \ + (self.number, + u"yes" if self.track_type == 0 else u"no", + u"yes" if self.pre_emphasis == 1 else u"no", + self.offset) + + def __eq__(self, track): + for attr in ["offset", + "number", + "ISRC", + "track_type", + "pre_emphasis", + "index_points"]: + if ((not hasattr(track, attr)) or (getattr(self, attr) != + getattr(track, attr))): + return False + else: + return True + + @classmethod + def parse(cls, reader): + """returns this cuesheet track from a BitstreamReader""" + + (offset, + number, + ISRC, + track_type, + pre_emphasis, + index_points) = reader.parse("64U8u12b1u1u110p8u") + return cls(offset, number, ISRC, track_type, pre_emphasis, + [Flac_CUESHEET_index.parse(reader) + for i in xrange(index_points)]) + + def build(self, writer): + """writes this cuesheet track to a BitstreamWriter""" + + writer.build("64U8u12b1u1u110p8u", + (self.offset, + self.number, + self.ISRC, + self.track_type, + self.pre_emphasis, + len(self.index_points))) + for index_point in self.index_points: + index_point.build(writer) + + +class Flac_CUESHEET_index: + def __init__(self, offset, number): + self.offset = offset + self.number = number + + def copy(self): + """returns a duplicate of this metadata block""" + + return Flac_CUESHEET_index(self.offset, self.number) + + def __repr__(self): + return "Flac_CUESHEET_index(%s, %s)" % (repr(self.offset), + repr(self.number)) + + def __eq__(self, index): + try: + return ((self.offset == index.offset) and + (self.number == index.number)) + except AttributeError: + return False + + @classmethod + def parse(cls, reader): + """returns this cuesheet index from a BitstreamReader""" + + (offset, number) = reader.parse("64U8u24p") + + return cls(offset, number) + + def build(self, writer): + """writes this cuesheet index to a BitstreamWriter""" + + writer.build("64U8u24p", (self.offset, self.number)) + + +class Flac_PICTURE(Image): + BLOCK_ID = 6 + + def __init__(self, picture_type, mime_type, description, + width, height, color_depth, color_count, data): + self.__dict__["data"] = data + self.__dict__["mime_type"] = mime_type + self.__dict__["width"] = width + self.__dict__["height"] = height + self.__dict__["color_depth"] = color_depth + self.__dict__["color_count"] = color_count + self.__dict__["description"] = description + self.__dict__["picture_type"] = picture_type + + def copy(self): + """returns a duplicate of this metadata block""" + + return Flac_PICTURE(self.picture_type, + self.mime_type, + self.description, + self.width, + self.height, + self.color_depth, + self.color_count, + self.data) + + def __getattr__(self, key): + if (key == "type"): + #convert FLAC picture_type to Image type + # + # | Item | FLAC Picture ID | Image type | + # |--------------+-----------------+------------| + # | Other | 0 | 4 | + # | Front Cover | 3 | 0 | + # | Back Cover | 4 | 1 | + # | Leaflet Page | 5 | 2 | + # | Media | 6 | 3 | + + return {0: 4, 3: 0, 4: 1, 5: 2, 6: 3}.get(self.picture_type, 4) + else: + try: + return self.__dict__[key] + except KeyError: + raise AttributeError(key) + + def __setattr__(self, key, value): + if (key == "type"): + #convert Image type to FLAC picture_type + # + # | Item | Image type | FLAC Picture ID | + # |--------------+------------+-----------------| + # | Other | 4 | 0 | + # | Front Cover | 0 | 3 | + # | Back Cover | 1 | 4 | + # | Leaflet Page | 2 | 5 | + # | Media | 3 | 6 | + + self.picture_type = {4: 0, 0: 3, 1: 4, 2: 5, 3: 6}.get(value, 0) + else: + self.__dict__[key] = value + + def __repr__(self): + return ("Flac_PICTURE(%s)" % + ",".join(["%s=%s" % (key, repr(getattr(self, key))) + for key in ["picture_type", + "mime_type", + "description", + "width", + "height", + "color_depth", + "color_count"]])) + + def raw_info(self): + """returns a human-readable version of this metadata block + as unicode""" + + from os import linesep + + return linesep.decode('ascii').join( + [u" PICTURE:", + u" picture type = %d" % (self.picture_type), + u" MIME type = %s" % (self.mime_type), + u" description = %s" % (self.description), + u" width = %d" % (self.width), + u" height = %d" % (self.height), + u" color depth = %d" % (self.color_depth), + u" color count = %d" % (self.color_count), + u" bytes = %d" % (len(self.data))]) + + @classmethod + def parse(cls, reader): + """returns this metadata block from a BitstreamReader""" + + return cls( + picture_type=reader.read(32), + mime_type=reader.read_bytes(reader.read(32)).decode('ascii'), + description=reader.read_bytes(reader.read(32)).decode('utf-8'), + width=reader.read(32), + height=reader.read(32), + color_depth=reader.read(32), + color_count=reader.read(32), + data=reader.read_bytes(reader.read(32))) + + def build(self, writer): + """writes this metadata block to a BitstreamWriter""" + + writer.build("32u [ 32u%db ] [32u%db ] 32u 32u 32u 32u [ 32u%db ]" % + (len(self.mime_type.encode('ascii')), + len(self.description.encode('utf-8')), + len(self.data)), + (self.picture_type, + len(self.mime_type.encode('ascii')), + self.mime_type.encode('ascii'), + len(self.description.encode('utf-8')), + self.description.encode('utf-8'), + self.width, + self.height, + self.color_depth, + self.color_count, + len(self.data), + self.data)) + + def size(self): + """the size of this metadata block + not including the 4-byte block header""" + + from .bitstream import format_size + + return format_size( + "32u [ 32u%db ] [32u%db ] 32u 32u 32u 32u [ 32u%db ]" % + (len(self.mime_type.encode('ascii')), + len(self.description.encode('utf-8')), + len(self.data))) / 8 + + @classmethod + def converted(cls, image): + """converts an Image object to a FlacPictureComment""" + + return cls( + picture_type={4: 0, 0: 3, 1: 4, 2: 5, 3: 6}.get(image.type, 0), + mime_type=image.mime_type, + description=image.description, + width=image.width, + height=image.height, + color_depth=image.color_depth, + color_count=image.color_count, + data=image.data) + + def type_string(self): + """returns the image's type as a human readable plain string + + for example, an image of type 0 returns "Front Cover" + """ + + return {0: "Other", + 1: "File icon", + 2: "Other file icon", + 3: "Cover (front)", + 4: "Cover (back)", + 5: "Leaflet page", + 6: "Media", + 7: "Lead artist / lead performer / soloist", + 8: "Artist / Performer", + 9: "Conductor", + 10: "Band / Orchestra", + 11: "Composer", + 12: "Lyricist / Text writer", + 13: "Recording Location", + 14: "During recording", + 15: "During performance", + 16: "Movie / Video screen capture", + 17: "A bright colored fish", + 18: "Illustration", + 19: "Band/Artist logotype", + 20: "Publisher / Studio logotype"}.get(self.picture_type, + "Other") + + def clean(self, fixes_performed): + from .image import image_metrics + + img = image_metrics(self.data) + + if (((self.mime_type != img.mime_type) or + (self.width != img.width) or + (self.height != img.height) or + (self.color_depth != img.bits_per_pixel) or + (self.color_count != img.color_count))): + from .text import CLEAN_FIX_IMAGE_FIELDS + fixes_performed.append(CLEAN_FIX_IMAGE_FIELDS) + return self.__class__.converted( + Image(type=self.type, + mime_type=img.mime_type, + description=self.description, + width=img.width, + height=img.height, + color_depth=img.bits_per_pixel, + color_count=img.color_count, + data=self.data)) + else: + return self + + +class FlacAudio(WaveContainer, AiffContainer): + """a Free Lossless Audio Codec file""" + + from .text import (COMP_FLAC_0, + COMP_FLAC_8) + + SUFFIX = "flac" + NAME = SUFFIX + DESCRIPTION = u"Free Lossless Audio Codec" + DEFAULT_COMPRESSION = "8" + COMPRESSION_MODES = tuple(map(str, range(0, 9))) + COMPRESSION_DESCRIPTIONS = {"0": COMP_FLAC_0, + "8": COMP_FLAC_8} + + METADATA_CLASS = FlacMetaData + + def __init__(self, filename): + """filename is a plain string""" + + AudioFile.__init__(self, filename) + self.__samplerate__ = 0 + self.__channels__ = 0 + self.__bitspersample__ = 0 + self.__total_frames__ = 0 + self.__stream_offset__ = 0 + self.__md5__ = chr(0) * 16 + + try: + self.__read_streaminfo__() + except IOError, msg: + raise InvalidFLAC(str(msg)) + + def channel_mask(self): + """returns a ChannelMask object of this track's channel layout""" + + from . import ChannelMask + + if (self.channels() <= 2): + return ChannelMask.from_channels(self.channels()) + + try: + metadata = self.get_metadata() + if (metadata is not None): + return ChannelMask( + int(metadata.get_block( + Flac_VORBISCOMMENT.BLOCK_ID)[ + u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"][0], 16)) + else: + #proceed to generate channel mask + raise ValueError() + except (IndexError, KeyError, ValueError): + #if there is no VORBIS_COMMENT block + #or no WAVEFORMATEXTENSIBLE_CHANNEL_MASK in that block + #or it's not an integer, + #use FLAC's default mask based on channels + if (self.channels() == 3): + return ChannelMask.from_fields( + front_left=True, front_right=True, front_center=True) + elif (self.channels() == 4): + return ChannelMask.from_fields( + front_left=True, front_right=True, + back_left=True, back_right=True) + elif (self.channels() == 5): + return ChannelMask.from_fields( + front_left=True, front_right=True, front_center=True, + back_left=True, back_right=True) + elif (self.channels() == 6): + return ChannelMask.from_fields( + front_left=True, front_right=True, front_center=True, + back_left=True, back_right=True, + low_frequency=True) + else: + return ChannelMask(0) + + def lossless(self): + """returns True""" + + return True + + def get_metadata(self): + """returns a MetaData object, or None + + raises IOError if unable to read the file""" + + #FlacAudio *always* returns a FlacMetaData object + #even if the blocks aren't present + #so there's no need to test for None + + f = file(self.filename, 'rb') + try: + f.seek(self.__stream_offset__, 0) + if (f.read(4) != 'fLaC'): + return None + else: + from .bitstream import BitstreamReader + + return FlacMetaData.parse(BitstreamReader(f, 0)) + finally: + f.close() + + def update_metadata(self, metadata): + """takes this track's current MetaData object + as returned by get_metadata() and sets this track's metadata + with any fields updated in that object + + raises IOError if unable to write the file + """ + + from .bitstream import BitstreamWriter + from .bitstream import BitstreamAccumulator + from .bitstream import BitstreamReader + from operator import add + + if (metadata is None): + return + + if (not isinstance(metadata, FlacMetaData)): + from .text import ERR_FOREIGN_METADATA + raise ValueError(ERR_FOREIGN_METADATA) + + has_padding = len(metadata.get_blocks(Flac_PADDING.BLOCK_ID)) > 0 + + if (has_padding): + total_padding_size = sum( + [b.size() for b in metadata.get_blocks(Flac_PADDING.BLOCK_ID)]) + else: + total_padding_size = 0 + + metadata_delta = metadata.size() - self.metadata_length() + + if (has_padding and (metadata_delta <= total_padding_size)): + #if padding size is larger than change in metadata + #shrink padding blocks so that new size matches old size + #(if metadata_delta is negative, + # this will enlarge padding blocks as necessary) + + for padding in metadata.get_blocks(Flac_PADDING.BLOCK_ID): + if (metadata_delta > 0): + #extract bytes from PADDING blocks + #until the metadata_delta is exhausted + if (metadata_delta <= padding.length): + padding.length -= metadata_delta + metadata_delta = 0 + else: + metadata_delta -= padding.length + padding.length = 0 + elif (metadata_delta < 0): + #dump all our new bytes into the first PADDING block found + padding.length -= metadata_delta + metadata_delta = 0 + else: + break + + #then overwrite the beginning of the file + stream = file(self.filename, 'r+b') + stream.write('fLaC') + metadata.build(BitstreamWriter(stream, 0)) + stream.close() + else: + #if padding is smaller than change in metadata, + #or file has no padding, + #rewrite entire file to fit new metadata + + import tempfile + from . import transfer_data + + stream = file(self.filename, 'rb') + stream.seek(self.__stream_offset__, 0) + + if (stream.read(4) != 'fLaC'): + from .text import ERR_FLAC_INVALID_FILE + raise InvalidFLAC(ERR_FLAC_INVALID_FILE) + + #skip the existing metadata blocks + stop = 0 + reader = BitstreamReader(stream, 0) + while (stop == 0): + (stop, length) = reader.parse("1u 7p 24u") + reader.skip_bytes(length) + + #write the remaining data stream to a temp file + file_data = tempfile.TemporaryFile() + transfer_data(stream.read, file_data.write) + file_data.seek(0, 0) + + #finally, rebuild our file using new metadata and old stream + stream = file(self.filename, 'wb') + stream.write('fLaC') + writer = BitstreamWriter(stream, 0) + metadata.build(writer) + writer.flush() + transfer_data(file_data.read, stream.write) + file_data.close() + stream.close() + + def set_metadata(self, metadata): + """takes a MetaData object and sets this track's metadata + + this metadata includes track name, album name, and so on + raises IOError if unable to read or write the file""" + + new_metadata = self.METADATA_CLASS.converted(metadata) + + if (new_metadata is None): + return + + old_metadata = self.get_metadata() + if (old_metadata is None): + #this shouldn't happen + old_metadata = FlacMetaData([]) + + #replace old metadata's VORBIS_COMMENT with one from new metadata + #(if any) + if (new_metadata.has_block(Flac_VORBISCOMMENT.BLOCK_ID)): + new_vorbiscomment = new_metadata.get_block( + Flac_VORBISCOMMENT.BLOCK_ID) + + if (old_metadata.has_block(Flac_VORBISCOMMENT.BLOCK_ID)): + #both new and old metadata has a VORBIS_COMMENT block + + old_vorbiscomment = old_metadata.get_block( + Flac_VORBISCOMMENT.BLOCK_ID) + + #update vendor string from our current VORBIS_COMMENT block + new_vorbiscomment.vendor_string = \ + old_vorbiscomment.vendor_string + + #update REPLAYGAIN_* tags from our current VORBIS_COMMENT block + for key in [u"REPLAYGAIN_TRACK_GAIN", + u"REPLAYGAIN_TRACK_PEAK", + u"REPLAYGAIN_ALBUM_GAIN", + u"REPLAYGAIN_ALBUM_PEAK", + u"REPLAYGAIN_REFERENCE_LOUDNESS"]: + try: + new_vorbiscomment[key] = old_vorbiscomment[key] + except KeyError: + new_vorbiscomment[key] = [] + + #update WAVEFORMATEXTENSIBLE_CHANNEL_MASK + #from our current VORBIS_COMMENT block, if any + if (((self.channels() > 2) or + (self.bits_per_sample() > 16)) and + (u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK" in + old_vorbiscomment.keys())): + new_vorbiscomment[u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"] = \ + old_vorbiscomment[u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"] + elif (u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK" in + new_vorbiscomment.keys()): + new_vorbiscomment[ + u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"] = [] + + old_metadata.replace_blocks(Flac_VORBISCOMMENT.BLOCK_ID, + [new_vorbiscomment]) + else: + #new metadata has VORBIS_COMMENT block, + #but old metadata does not + + #remove REPLAYGAIN_* tags from new VORBIS_COMMENT block + for key in [u"REPLAYGAIN_TRACK_GAIN", + u"REPLAYGAIN_TRACK_PEAK", + u"REPLAYGAIN_ALBUM_GAIN", + u"REPLAYGAIN_ALBUM_PEAK", + u"REPLAYGAIN_REFERENCE_LOUDNESS"]: + new_vorbiscomment[key] = [] + + #update WAVEFORMATEXTENSIBLE_CHANNEL_MASK + #from our actual mask if necessary + if ((self.channels() > 2) or (self.bits_per_sample() > 16)): + new_vorbiscomment[u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"] = [ + u"0x%.4X" % (self.channel_mask())] + + old_metadata.add_block(new_vorbiscomment) + else: + #new metadata has no VORBIS_COMMENT block + pass + + #replace old metadata's PICTURE blocks with those from new metadata + old_metadata.replace_blocks( + Flac_PICTURE.BLOCK_ID, + new_metadata.get_blocks(Flac_PICTURE.BLOCK_ID)) + + #everything else remains as-is + + self.update_metadata(old_metadata) + + def metadata_length(self): + """returns the length of all FLAC metadata blocks as an integer + + not including the 4 byte "fLaC" file header""" + + from .bitstream import BitstreamReader + + counter = 0 + f = file(self.filename, 'rb') + try: + f.seek(self.__stream_offset__, 0) + reader = BitstreamReader(f, 0) + + if (reader.read_bytes(4) != 'fLaC'): + from .text import ERR_FLAC_INVALID_FILE + raise InvalidFLAC(ERR_FLAC_INVALID_FILE) + + stop = 0 + while (stop == 0): + (stop, block_id, length) = reader.parse("1u 7u 24u") + counter += 4 + + reader.skip_bytes(length) + counter += length + + return counter + finally: + f.close() + + def delete_metadata(self): + """deletes the track's MetaData + + this removes or unsets tags as necessary in order to remove all data + raises IOError if unable to write the file""" + + self.set_metadata(MetaData()) + + @classmethod + def __block_ids__(cls, flacfile): + """yields a block_id int per metadata block + + raises ValueError if a block_id is invalid + """ + + valid_block_ids = frozenset(range(0, 6 + 1)) + from .bitstream import BitstreamReader + reader = BitstreamReader(flacfile, 0) + stop = 0 + while (stop == 0): + (stop, block_id, length) = reader.parse("1u 7u 24u") + if (block_id in valid_block_ids): + yield block_id + else: + from .text import ERR_FLAC_INVALID_BLOCK + raise ValueError(ERR_FLAC_INVALID_BLOCK) + reader.skip_bytes(length) + + def set_cuesheet(self, cuesheet): + """imports cuesheet data from a Cuesheet-compatible object + + this are objects with catalog(), ISRCs(), indexes(), and pcm_lengths() + methods. Raises IOError if an error occurs setting the cuesheet""" + + if (cuesheet is not None): + metadata = self.get_metadata() + if (metadata is not None): + metadata.add_block( + Flac_CUESHEET.converted( + cuesheet, self.total_frames(), self.sample_rate())) + self.update_metadata(metadata) + + def get_cuesheet(self): + """returns the embedded Cuesheet-compatible object, or None + + raises IOError if a problem occurs when reading the file""" + + try: + metadata = self.get_metadata() + if (metadata is not None): + return metadata.get_block(Flac_CUESHEET.BLOCK_ID) + else: + return None + except IndexError: + return None + + def to_pcm(self): + """returns a PCMReader object containing the track's PCM data""" + + from . import decoders + from . import PCMReaderError + + try: + return decoders.FlacDecoder(self.filename, + self.channel_mask(), + self.__stream_offset__) + except (IOError, ValueError), msg: + #The only time this is likely to occur is + #if the FLAC is modified between when FlacAudio + #is initialized and when to_pcm() is called. + return PCMReaderError(error_message=str(msg), + 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, + encoding_function=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 FlacAudio object""" + + from .encoders import encode_flac + from . import EncodingError + from . import UnsupportedChannelCount + from . import BufferedPCMReader + from . import __default_quality__ + + if ((compression is None) or (compression not in + cls.COMPRESSION_MODES)): + compression = __default_quality__(cls.NAME) + + encoding_options = { + "0": {"block_size": 1152, + "max_lpc_order": 0, + "min_residual_partition_order": 0, + "max_residual_partition_order": 3}, + "1": {"block_size": 1152, + "max_lpc_order": 0, + "adaptive_mid_side": True, + "min_residual_partition_order": 0, + "max_residual_partition_order": 3}, + "2": {"block_size": 1152, + "max_lpc_order": 0, + "exhaustive_model_search": True, + "min_residual_partition_order": 0, + "max_residual_partition_order": 3}, + "3": {"block_size": 4096, + "max_lpc_order": 6, + "min_residual_partition_order": 0, + "max_residual_partition_order": 4}, + "4": {"block_size": 4096, + "max_lpc_order": 8, + "adaptive_mid_side": True, + "min_residual_partition_order": 0, + "max_residual_partition_order": 4}, + "5": {"block_size": 4096, + "max_lpc_order": 8, + "mid_side": True, + "min_residual_partition_order": 0, + "max_residual_partition_order": 5}, + "6": {"block_size": 4096, + "max_lpc_order": 8, + "mid_side": True, + "min_residual_partition_order": 0, + "max_residual_partition_order": 6}, + "7": {"block_size": 4096, + "max_lpc_order": 8, + "mid_side": True, + "exhaustive_model_search": True, + "min_residual_partition_order": 0, + "max_residual_partition_order": 6}, + "8": {"block_size": 4096, + "max_lpc_order": 12, + "mid_side": True, + "exhaustive_model_search": True, + "min_residual_partition_order": 0, + "max_residual_partition_order": 6}}[compression] + + if (pcmreader.channels > 8): + raise UnsupportedChannelCount(filename, pcmreader.channels) + + if (int(pcmreader.channel_mask) == 0): + if (pcmreader.channels <= 6): + channel_mask = {1: 0x0004, + 2: 0x0003, + 3: 0x0007, + 4: 0x0033, + 5: 0x0037, + 6: 0x003F}[pcmreader.channels] + else: + channel_mask = 0 + + elif (int(pcmreader.channel_mask) not in + (0x0001, # 1ch - mono + 0x0004, # 1ch - mono + 0x0003, # 2ch - left, right + 0x0007, # 3ch - left, right, center + 0x0033, # 4ch - left, right, back left, back right + 0x0603, # 4ch - left, right, side left, side right + 0x0037, # 5ch - L, R, C, back left, back right + 0x0607, # 5ch - L, R, C, side left, side right + 0x003F, # 6ch - L, R, C, LFE, back left, back right + 0x060F)): # 6ch - L, R, C, LFE, side left, side right + from . import UnsupportedChannelMask + + raise UnsupportedChannelMask(filename, + int(pcmreader.channel_mask)) + else: + channel_mask = int(pcmreader.channel_mask) + + try: + offsets = (encode_flac if encoding_function is None + else encoding_function)(filename, + pcmreader= + BufferedPCMReader(pcmreader), + **encoding_options) + flac = FlacAudio(filename) + metadata = flac.get_metadata() + assert(metadata is not None) + + #generate SEEKTABLE from encoder offsets and add it to metadata + seekpoint_interval = pcmreader.sample_rate * 10 + + metadata.add_block( + flac.seektable( + [(byte_offset, + pcm_frames) for byte_offset, pcm_frames in offsets], + seekpoint_interval)) + + #if channels or bps is too high, + #automatically generate and add channel mask + if ((((pcmreader.channels > 2) or + (pcmreader.bits_per_sample > 16)) and + (channel_mask != 0))): + vorbis = metadata.get_block(Flac_VORBISCOMMENT.BLOCK_ID) + vorbis[u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"] = [ + u"0x%.4X" % (channel_mask)] + + flac.update_metadata(metadata) + + return flac + except (IOError, ValueError), err: + cls.__unlink__(filename) + raise EncodingError(str(err)) + except Exception, err: + cls.__unlink__(filename) + raise err + + def seektable(self, offsets=None, seekpoint_interval=None): + """returns a new Flac_SEEKTABLE object + created from parsing the FLAC file itself""" + + from bisect import bisect_right + + if (offsets is None): + metadata_length = (self.__stream_offset__ + + 4 + self.metadata_length()) + offsets = [(byte_offset - metadata_length, + pcm_frames) for byte_offset, pcm_frames in + self.to_pcm().offsets()] + + if (seekpoint_interval is None): + seekpoint_interval = self.sample_rate() * 10 + + total_samples = 0 + all_frames = {} + sample_offsets = [] + for (byte_offset, pcm_frames) in offsets: + all_frames[total_samples] = (byte_offset, pcm_frames) + sample_offsets.append(total_samples) + total_samples += pcm_frames + + seekpoints = [] + for pcm_frame in xrange(0, + self.total_frames(), + seekpoint_interval): + flac_frame = bisect_right(sample_offsets, pcm_frame) - 1 + seekpoints.append((sample_offsets[flac_frame], + all_frames[sample_offsets[flac_frame]][0], + all_frames[sample_offsets[flac_frame]][1])) + + return Flac_SEEKTABLE(seekpoints) + + def has_foreign_wave_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""" + + try: + metadata = self.get_metadata() + if (metadata is not None): + return 'riff' in [ + block.application_id for block in + metadata.get_blocks(Flac_APPLICATION.BLOCK_ID)] + else: + return False + except IOError: + return False + + def wave_header_footer(self): + """returns (header, footer) tuple of strings + containing all data before and after the PCM stream + + may raise ValueError if there's a problem with + the header or footer data + may raise IOError if there's a problem reading + header or footer data from the file + """ + + from .wav import pad_data + + header = [] + if (pad_data(self.total_frames(), + self.channels(), + self.bits_per_sample())): + footer = [chr(0)] + else: + footer = [] + current_block = header + + metadata = self.get_metadata() + if (metadata is None): + raise ValueError("no foreign RIFF chunks") + + #convert individual chunks into combined header and footer strings + for block in metadata.get_blocks(Flac_APPLICATION.BLOCK_ID): + if (block.application_id == "riff"): + chunk_id = block.data[0:4] + #combine APPLICATION metadata blocks up to "data" as header + if (chunk_id != "data"): + current_block.append(block.data) + else: + #combine APPLICATION metadata blocks past "data" as footer + current_block.append(block.data) + current_block = footer + + #return tuple of header and footer + if ((len(header) != 0) or (len(footer) != 0)): + return ("".join(header), "".join(footer)) + else: + raise ValueError("no foreign RIFF chunks") + + @classmethod + def from_wave(cls, filename, header, pcmreader, footer, compression=None): + """encodes a new file from wave data + + takes a filename string, header string, + PCMReader object, footer string + 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 + + may raise EncodingError if some problem occurs when + encoding the input file""" + + from .bitstream import BitstreamReader + from .bitstream import BitstreamRecorder + from .bitstream import format_byte_size + import cStringIO + from .wav import (pad_data, WaveAudio) + from . import (EncodingError, CounterPCMReader) + + #split header and footer into distinct chunks + header_len = len(header) + footer_len = len(footer) + fmt_found = False + blocks = [] + try: + #read everything from start of header to "data<size>" + #chunk header + r = BitstreamReader(cStringIO.StringIO(header), 1) + (riff, remaining_size, wave) = r.parse("4b 32u 4b") + if (riff != "RIFF"): + from .text import ERR_WAV_NOT_WAVE + raise EncodingError(ERR_WAV_NOT_WAVE) + elif (wave != "WAVE"): + from .text import ERR_WAV_INVALID_WAVE + raise EncodingError(ERR_WAV_INVALID_WAVE) + else: + block_data = BitstreamRecorder(1) + block_data.build("4b 32u 4b", (riff, remaining_size, wave)) + blocks.append(Flac_APPLICATION("riff", block_data.data())) + total_size = remaining_size + 8 + header_len -= format_byte_size("4b 32u 4b") + + while (header_len): + block_data = BitstreamRecorder(1) + (chunk_id, chunk_size) = r.parse("4b 32u") + #ensure chunk ID is valid + if (not frozenset(chunk_id).issubset( + WaveAudio.PRINTABLE_ASCII)): + from .text import ERR_WAV_INVALID_CHUNK + raise EncodingError(ERR_WAV_INVALID_CHUNK) + else: + header_len -= format_byte_size("4b 32u") + block_data.build("4b 32u", (chunk_id, chunk_size)) + + if (chunk_id == "data"): + #transfer only "data" chunk header to APPLICATION block + if (header_len != 0): + from .text import ERR_WAV_HEADER_EXTRA_DATA + raise EncodingError(ERR_WAV_HEADER_EXTRA_DATA % + (header_len)) + elif (not fmt_found): + from .text import ERR_WAV_NO_FMT_CHUNK + raise EncodingError(ERR_WAV_NO_FMT_CHUNK) + else: + blocks.append( + Flac_APPLICATION("riff", block_data.data())) + data_chunk_size = chunk_size + break + elif (chunk_id == "fmt "): + if (not fmt_found): + fmt_found = True + if (chunk_size % 2): + #transfer padded chunk to APPLICATION block + block_data.write_bytes( + r.read_bytes(chunk_size + 1)) + header_len -= (chunk_size + 1) + else: + #transfer un-padded chunk to APPLICATION block + block_data.write_bytes( + r.read_bytes(chunk_size)) + header_len -= chunk_size + + blocks.append( + Flac_APPLICATION("riff", block_data.data())) + else: + from .text import ERR_WAV_MULTIPLE_FMT + raise EncodingError(ERR_WAV_MULTIPLE_FMT) + else: + if (chunk_size % 2): + #transfer padded chunk to APPLICATION block + block_data.write_bytes(r.read_bytes(chunk_size + 1)) + header_len -= (chunk_size + 1) + else: + #transfer un-padded chunk to APPLICATION block + block_data.write_bytes(r.read_bytes(chunk_size)) + header_len -= chunk_size + + blocks.append(Flac_APPLICATION("riff", block_data.data())) + else: + from .text import ERR_WAV_NO_DATA_CHUNK + raise EncodingError(ERR_WAV_NO_DATA_CHUNK) + except IOError: + from .text import ERR_WAV_HEADER_IOERROR + raise EncodingError(ERR_WAV_HEADER_IOERROR) + + try: + #read everything from start of footer to end of footer + r = BitstreamReader(cStringIO.StringIO(footer), 1) + #skip initial footer pad byte + if (data_chunk_size % 2): + r.skip_bytes(1) + footer_len -= 1 + + while (footer_len): + block_data = BitstreamRecorder(1) + (chunk_id, chunk_size) = r.parse("4b 32u") + + if (not frozenset(chunk_id).issubset( + WaveAudio.PRINTABLE_ASCII)): + #ensure chunk ID is valid + from .text import ERR_WAV_INVALID_CHUNK + raise EncodingError(ERR_WAV_INVALID_CHUNK) + elif (chunk_id == "fmt "): + #multiple "fmt " chunks is an error + from .text import ERR_WAV_MULTIPLE_FMT + raise EncodingError(ERR_WAV_MULTIPLE_FMT) + elif (chunk_id == "data"): + #multiple "data" chunks is an error + from .text import ERR_WAV_MULTIPLE_DATA + raise EncodingError(ERR_WAV_MULTIPLE_DATA) + else: + footer_len -= format_byte_size("4b 32u") + block_data.build("4b 32u", (chunk_id, chunk_size)) + + if (chunk_size % 2): + #transfer padded chunk to APPLICATION block + block_data.write_bytes(r.read_bytes(chunk_size + 1)) + footer_len -= (chunk_size + 1) + else: + #transfer un-padded chunk to APPLICATION block + block_data.write_bytes(r.read_bytes(chunk_size)) + footer_len -= chunk_size + + blocks.append(Flac_APPLICATION("riff", block_data.data())) + except IOError: + from .text import ERR_WAV_FOOTER_IOERROR + raise EncodingError(ERR_WAV_FOOTER_IOERROR) + + counter = CounterPCMReader(pcmreader) + + #perform standard FLAC encode from PCMReader + flac = cls.from_pcm(filename, counter, compression) + + data_bytes_written = counter.bytes_written() + + #ensure processed PCM data equals size of "data" chunk + if (data_bytes_written != data_chunk_size): + cls.__unlink__(filename) + from .text import ERR_WAV_TRUNCATED_DATA_CHUNK + raise EncodingError(ERR_WAV_TRUNCATED_DATA_CHUNK) + + #ensure total size of header + PCM + footer matches wav's header + if ((len(header) + data_bytes_written + len(footer)) != total_size): + cls.__unlink__(filename) + from .text import ERR_WAV_INVALID_SIZE + raise EncodingError(ERR_WAV_INVALID_SIZE) + + #add chunks as APPLICATION metadata blocks + metadata = flac.get_metadata() + if (metadata is not None): + for block in blocks: + metadata.add_block(block) + flac.update_metadata(metadata) + + #return encoded FLAC file + return flac + + def has_foreign_aiff_chunks(self): + """returns True if the audio file contains non-audio AIFF chunks""" + + try: + metadata = self.get_metadata() + if (metadata is not None): + return 'aiff' in [ + block.application_id for block in + metadata.get_blocks(Flac_APPLICATION.BLOCK_ID)] + else: + return False + except IOError: + return False + + def aiff_header_footer(self): + """returns (header, footer) tuple of strings + containing all data before and after the PCM stream + + if self.has_foreign_aiff_chunks() is False, + may raise ValueError if the file has no header and footer + for any reason""" + + from .aiff import pad_data + + header = [] + if (pad_data(self.total_frames(), + self.channels(), + self.bits_per_sample())): + footer = [chr(0)] + else: + footer = [] + current_block = header + + metadata = self.get_metadata() + if (metadata is None): + raise ValueError("no foreign AIFF chunks") + + #convert individual chunks into combined header and footer strings + for block in metadata.get_blocks(Flac_APPLICATION.BLOCK_ID): + if (block.application_id == "aiff"): + chunk_id = block.data[0:4] + #combine APPLICATION metadata blocks up to "SSND" as header + if (chunk_id != "SSND"): + current_block.append(block.data) + else: + #combine APPLICATION metadata blocks past "SSND" as footer + current_block.append(block.data) + current_block = footer + + #return tuple of header and footer + if ((len(header) != 0) or (len(footer) != 0)): + return ("".join(header), "".join(footer)) + else: + raise ValueError("no foreign AIFF chunks") + + @classmethod + def from_aiff(cls, filename, header, pcmreader, footer, compression=None): + """encodes a new file from AIFF data + + takes a filename string, header string, + PCMReader object, footer string + 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 AiffAudio object + + header + pcm data + footer should always result + in the original AIFF file being restored + without need for any padding bytes + + may raise EncodingError if some problem occurs when + encoding the input file""" + + from .bitstream import BitstreamReader + from .bitstream import BitstreamRecorder + from .bitstream import format_byte_size + import cStringIO + from .aiff import (pad_data, AiffAudio) + from . import (EncodingError, CounterPCMReader) + + #split header and footer into distinct chunks + header_len = len(header) + footer_len = len(footer) + comm_found = False + blocks = [] + try: + #read everything from start of header to "SSND<size>" + #chunk header + r = BitstreamReader(cStringIO.StringIO(header), 0) + (form, remaining_size, aiff) = r.parse("4b 32u 4b") + if (form != "FORM"): + from .text import ERR_AIFF_NOT_AIFF + raise EncodingError(ERR_AIFF_NOT_AIFF) + elif (aiff != "AIFF"): + from .text import ERR_AIFF_INVALID_AIFF + raise EncodingError(ERR_AIFF_INVALID_AIFF) + else: + block_data = BitstreamRecorder(0) + block_data.build("4b 32u 4b", (form, remaining_size, aiff)) + blocks.append(Flac_APPLICATION("aiff", block_data.data())) + total_size = remaining_size + 8 + header_len -= format_byte_size("4b 32u 4b") + + while (header_len): + block_data = BitstreamRecorder(0) + (chunk_id, chunk_size) = r.parse("4b 32u") + #ensure chunk ID is valid + if (not frozenset(chunk_id).issubset( + AiffAudio.PRINTABLE_ASCII)): + from .text import ERR_AIFF_INVALID_CHUNK + raise EncodingError(ERR_AIFF_INVALID_CHUNK) + else: + header_len -= format_byte_size("4b 32u") + block_data.build("4b 32u", (chunk_id, chunk_size)) + + if (chunk_id == "SSND"): + #transfer only "SSND" chunk header to APPLICATION block + #(including 8 bytes after ID/size header) + if (header_len > 8): + from .text import ERR_AIFF_HEADER_EXTRA_SSND + raise EncodingError(ERR_AIFF_HEADER_EXTRA_SSND) + elif (header_len < 8): + from .text import ERR_AIFF_HEADER_MISSING_SSND + raise EncodingError(ERR_AIFF_HEADER_MISSING_SSND) + elif (not comm_found): + from .text import ERR_AIFF_NO_COMM_CHUNK + raise EncodingError(ERR_AIFF_NO_COMM_CHUNK) + else: + block_data.write_bytes(r.read_bytes(8)) + blocks.append( + Flac_APPLICATION("aiff", block_data.data())) + ssnd_chunk_size = (chunk_size - 8) + break + elif (chunk_id == "COMM"): + if (not comm_found): + comm_found = True + if (chunk_size % 2): + #transfer padded chunk to APPLICATION block + block_data.write_bytes( + r.read_bytes(chunk_size + 1)) + header_len -= (chunk_size + 1) + else: + #transfer un-padded chunk to APPLICATION block + block_data.write_bytes( + r.read_bytes(chunk_size)) + header_len -= chunk_size + blocks.append( + Flac_APPLICATION("aiff", block_data.data())) + else: + from .text import ERR_AIFF_MULTIPLE_COMM_CHUNKS + raise EncodingError(ERR_AIFF_MULTIPLE_COMM_CHUNKS) + else: + if (chunk_size % 2): + #transfer padded chunk to APPLICATION block + block_data.write_bytes(r.read_bytes(chunk_size + 1)) + header_len -= (chunk_size + 1) + else: + #transfer un-padded chunk to APPLICATION block + block_data.write_bytes(r.read_bytes(chunk_size)) + header_len -= chunk_size + + blocks.append(Flac_APPLICATION("aiff", block_data.data())) + else: + from .text import ERR_AIFF_NO_SSND_CHUNK + raise EncodingError(ERR_AIFF_NO_SSND_CHUNK) + except IOError: + from .text import ERR_AIFF_HEADER_IOERROR + raise EncodingError(ERR_AIFF_HEADER_IOERROR) + + try: + #read everything from start of footer to end of footer + r = BitstreamReader(cStringIO.StringIO(footer), 0) + #skip initial footer pad byte + if (ssnd_chunk_size % 2): + r.skip_bytes(1) + footer_len -= 1 + + while (footer_len): + block_data = BitstreamRecorder(0) + (chunk_id, chunk_size) = r.parse("4b 32u") + + if (not frozenset(chunk_id).issubset( + AiffAudio.PRINTABLE_ASCII)): + #ensure chunk ID is valid + from .text import ERR_AIFF_INVALID_CHUNK + raise EncodingError(ERR_AIFF_INVALID_CHUNK) + elif (chunk_id == "COMM"): + #multiple "COMM" chunks is an error + from .text import ERR_AIFF_MULTIPLE_COMM_CHUNKS + raise EncodingError(ERR_AIFF_MULTIPLE_COMM_CHUNKS) + elif (chunk_id == "SSND"): + #multiple "SSND" chunks is an error + from .text import ERR_AIFF_MULTIPLE_SSND_CHUNKS + raise EncodingError(ERR_AIFF_MULTIPLE_SSND_CHUNKS) + else: + footer_len -= format_byte_size("4b 32u") + block_data.build("4b 32u", (chunk_id, chunk_size)) + + if (chunk_size % 2): + #transfer padded chunk to APPLICATION block + block_data.write_bytes(r.read_bytes(chunk_size + 1)) + footer_len -= (chunk_size + 1) + else: + #transfer un-padded chunk to APPLICATION block + block_data.write_bytes(r.read_bytes(chunk_size)) + footer_len -= chunk_size + + blocks.append(Flac_APPLICATION("aiff", block_data.data())) + except IOError: + from .text import ERR_AIFF_FOOTER_IOERROR + raise EncodingError(ERR_AIFF_FOOTER_IOERROR) + + counter = CounterPCMReader(pcmreader) + + #perform standard FLAC encode from PCMReader + flac = cls.from_pcm(filename, counter, compression) + + ssnd_bytes_written = counter.bytes_written() + + #ensure processed PCM data equals size of "SSND" chunk + if (ssnd_bytes_written != ssnd_chunk_size): + cls.__unlink__(filename) + from .text import ERR_AIFF_TRUNCATED_SSND_CHUNK + raise EncodingError(ERR_AIFF_TRUNCATED_SSND_CHUNK) + + #ensure total size of header + PCM + footer matches aiff's header + if ((len(header) + ssnd_bytes_written + len(footer)) != total_size): + cls.__unlink__(filename) + from .text import ERR_AIFF_INVALID_SIZE + raise EncodingError(ERR_AIFF_INVALID_SIZE) + + #add chunks as APPLICATION metadata blocks + metadata = flac.get_metadata() + if (metadata is not None): + for block in blocks: + metadata.add_block(block) + flac.update_metadata(metadata) + + #return encoded FLAC file + return flac + + 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 a FLAC has embedded RIFF *and* embedded AIFF chunks, + #RIFF takes precedence if the target format supports both. + #It's hard to envision a scenario in which that would happen. + + import tempfile + from . import WaveAudio + from . import AiffAudio + from . import to_pcm_progress + + if ((self.has_foreign_wave_chunks() and + hasattr(target_class, "from_wave") and + callable(target_class.from_wave))): + return WaveContainer.convert(self, + target_path, + target_class, + compression, + progress) + elif (self.has_foreign_aiff_chunks() and + hasattr(target_class, "from_aiff") and + callable(target_class.from_aiff)): + return AiffContainer.convert(self, + target_path, + target_class, + compression, + progress) + else: + return target_class.from_pcm(target_path, + to_pcm_progress(self, progress), + compression) + + def bits_per_sample(self): + """returns an integer number of bits-per-sample this track contains""" + + return self.__bitspersample__ + + def channels(self): + """returns an integer number of channels this track contains""" + + return self.__channels__ + + def total_frames(self): + """returns the total PCM frames of the track as an integer""" + + return self.__total_frames__ + + def sample_rate(self): + """returns the rate of the track's audio as an integer number of Hz""" + + return self.__samplerate__ + + def __read_streaminfo__(self): + valid_header_types = frozenset(range(0, 6 + 1)) + f = file(self.filename, "rb") + try: + self.__stream_offset__ = skip_id3v2_comment(f) + f.read(4) + + from .bitstream import BitstreamReader + + reader = BitstreamReader(f, 0) + + stop = 0 + + while (stop == 0): + (stop, header_type, length) = reader.parse("1u 7u 24u") + if (header_type not in valid_header_types): + from .text import ERR_FLAC_INVALID_BLOCK + raise InvalidFLAC(ERR_FLAC_INVALID_BLOCK) + elif (header_type == 0): + (self.__samplerate__, + self.__channels__, + self.__bitspersample__, + self.__total_frames__, + self.__md5__) = reader.parse("80p 20u 3u 5u 36U 16b") + self.__channels__ += 1 + self.__bitspersample__ += 1 + break + else: + #though the STREAMINFO should always be first, + #we'll be permissive and check them all if necessary + reader.skip_bytes(length) + finally: + f.close() + + @classmethod + def can_add_replay_gain(cls, audiofiles): + """given a list of audiofiles, + returns True if this class can add ReplayGain to those files + returns False if not""" + + for audiofile in audiofiles: + if (not isinstance(audiofile, FlacAudio)): + return False + else: + return True + + @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 . import open_files + from . import calculate_replay_gain + + tracks = [track for track in open_files(filenames) if + isinstance(track, cls)] + + if (len(tracks) > 0): + for (track, + track_gain, + track_peak, + album_gain, + album_peak) in calculate_replay_gain(tracks, progress): + try: + metadata = track.get_metadata() + if (metadata is None): + return + except IOError: + return + try: + comment = metadata.get_block( + Flac_VORBISCOMMENT.BLOCK_ID) + except IndexError: + from . import VERSION + + comment = Flac_VORBISCOMMENT( + [], u"Python Audio Tools %s" % (VERSION)) + metadata.add_block(comment) + + comment["REPLAYGAIN_TRACK_GAIN"] = [ + "%1.2f dB" % (track_gain)] + comment["REPLAYGAIN_TRACK_PEAK"] = [ + "%1.8f" % (track_peak)] + comment["REPLAYGAIN_ALBUM_GAIN"] = [ + "%1.2f dB" % (album_gain)] + comment["REPLAYGAIN_ALBUM_PEAK"] = ["%1.8f" % (album_peak)] + comment["REPLAYGAIN_REFERENCE_LOUDNESS"] = [u"89.0 dB"] + track.update_metadata(metadata) + + @classmethod + def supports_replay_gain(cls): + """returns True if this class supports ReplayGain""" + + return True + + @classmethod + def lossless_replay_gain(cls): + """returns True""" + + return True + + def replay_gain(self): + """returns a ReplayGain object of our ReplayGain values + + returns None if we have no values""" + + from . import ReplayGain + + try: + metadata = self.get_metadata() + if (metadata is not None): + vorbis_metadata = metadata.get_block( + Flac_VORBISCOMMENT.BLOCK_ID) + else: + return None + except (IndexError, IOError): + return None + + if (set(['REPLAYGAIN_TRACK_PEAK', 'REPLAYGAIN_TRACK_GAIN', + 'REPLAYGAIN_ALBUM_PEAK', 'REPLAYGAIN_ALBUM_GAIN']).issubset( + [key.upper() for key in vorbis_metadata.keys()])): + # we have ReplayGain data + try: + return ReplayGain( + vorbis_metadata['REPLAYGAIN_TRACK_GAIN'][0][0:-len(" dB")], + vorbis_metadata['REPLAYGAIN_TRACK_PEAK'][0], + vorbis_metadata['REPLAYGAIN_ALBUM_GAIN'][0][0:-len(" dB")], + vorbis_metadata['REPLAYGAIN_ALBUM_PEAK'][0]) + except ValueError: + return None + else: + return None + + def __eq__(self, audiofile): + if (isinstance(audiofile, FlacAudio)): + return self.__md5__ == audiofile.__md5__ + elif (isinstance(audiofile, AudioFile)): + from . import FRAMELIST_SIZE + + try: + from hashlib import md5 + except ImportError: + from md5 import new as md5 + + p = audiofile.to_pcm() + m = md5() + s = p.read(FRAMELIST_SIZE) + while (len(s) > 0): + m.update(s.to_bytes(False, True)) + s = p.read(FRAMELIST_SIZE) + p.close() + return m.digest() == self.__md5__ + else: + return False + + def clean(self, fixes_performed, output_filename=None): + """cleans the file of known data and metadata problems + + fixes_performed is a list-like object which is appended + with Unicode strings of fixed problems + + output_filename is an optional filename of the fixed file + if present, a new AudioFile is returned + otherwise, only a dry-run is performed and no new file is written + + raises IOError if unable to write the file or its metadata + """ + + import os.path + + def seektable_valid(seektable, metadata_offset, input_file): + from .bitstream import BitstreamReader + reader = BitstreamReader(input_file, 0) + + for (pcm_frame_offset, + seekpoint_offset, + pcm_frame_count) in seektable.seekpoints: + input_file.seek(seekpoint_offset + metadata_offset) + try: + (sync_code, + reserved1, + reserved2) = reader.parse( + "14u 1u 1p 4p 4p 4p 3p 1u") + if (((sync_code != 0x3FFE) or + (reserved1 != 0) or + (reserved2 != 0))): + return False + except IOError: + return False + else: + return True + + if (output_filename is None): + #dry run only + + input_f = open(self.filename, "rb") + try: + #remove ID3 tags from before and after FLAC stream + stream_offset = skip_id3v2_comment(input_f) + if (stream_offset > 0): + from .text import CLEAN_FLAC_REMOVE_ID3V2 + fixes_performed.append(CLEAN_FLAC_REMOVE_ID3V2) + try: + input_f.seek(-128, 2) + if (input_f.read(3) == 'TAG'): + from .text import CLEAN_FLAC_REMOVE_ID3V1 + fixes_performed.append(CLEAN_FLAC_REMOVE_ID3V1) + except IOError: + #file isn't 128 bytes long + pass + + #fix empty MD5SUM + if (self.__md5__ == chr(0) * 16): + from .text import CLEAN_FLAC_POPULATE_MD5 + fixes_performed.append(CLEAN_FLAC_POPULATE_MD5) + + metadata = self.get_metadata() + if (metadata is None): + return + + #fix missing WAVEFORMATEXTENSIBLE_CHANNEL_MASK + if ((self.channels() > 2) or (self.bits_per_sample() > 16)): + from .text import CLEAN_FLAC_ADD_CHANNELMASK + try: + if (u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK" not in + metadata.get_block( + Flac_VORBISCOMMENT.BLOCK_ID).keys()): + fixes_performed.append(CLEAN_FLAC_ADD_CHANNELMASK) + except IndexError: + fixes_performed.append(CLEAN_FLAC_ADD_CHANNELMASK) + + #fix an invalid SEEKTABLE, if present + try: + if (not seektable_valid( + metadata.get_block(Flac_SEEKTABLE.BLOCK_ID), + stream_offset + 4 + self.metadata_length(), + input_f)): + from .text import CLEAN_FLAC_FIX_SEEKTABLE + fixes_performed.append(CLEAN_FLAC_FIX_SEEKTABLE) + except IndexError: + pass + + #fix any remaining metadata problems + metadata.clean(fixes_performed) + + finally: + input_f.close() + else: + #perform complete fix + + input_f = open(self.filename, "rb") + try: + #remove ID3 tags from before and after FLAC stream + stream_size = os.path.getsize(self.filename) + + stream_offset = skip_id3v2_comment(input_f) + if (stream_offset > 0): + from .text import CLEAN_FLAC_REMOVE_ID3V2 + fixes_performed.append(CLEAN_FLAC_REMOVE_ID3V2) + stream_size -= stream_offset + + try: + input_f.seek(-128, 2) + if (input_f.read(3) == 'TAG'): + from .text import CLEAN_FLAC_REMOVE_ID3V1 + fixes_performed.append(CLEAN_FLAC_REMOVE_ID3V1) + stream_size -= 128 + except IOError: + #file isn't 128 bytes long + pass + + output_f = open(output_filename, "wb") + try: + input_f.seek(stream_offset, 0) + while (stream_size > 0): + s = input_f.read(4096) + if (len(s) > stream_size): + s = s[0:stream_size] + output_f.write(s) + stream_size -= len(s) + finally: + output_f.close() + + output_track = self.__class__(output_filename) + + metadata = self.get_metadata() + if (metadata is not None): + #fix empty MD5SUM + if (self.__md5__ == chr(0) * 16): + from hashlib import md5 + from . import transfer_framelist_data + + md5sum = md5() + transfer_framelist_data( + self.to_pcm(), + md5sum.update, + signed=True, + big_endian=False) + metadata.get_block( + Flac_STREAMINFO.BLOCK_ID).md5sum = md5sum.digest() + from .text import CLEAN_FLAC_POPULATE_MD5 + fixes_performed.append(CLEAN_FLAC_POPULATE_MD5) + + #fix missing WAVEFORMATEXTENSIBLE_CHANNEL_MASK + if (((self.channels() > 2) or + (self.bits_per_sample() > 16))): + try: + vorbis_comment = metadata.get_block( + Flac_VORBISCOMMENT.BLOCK_ID) + except IndexError: + from . import VERSION + + vorbis_comment = Flac_VORBISCOMMENT( + [], u"Python Audio Tools %s" % (VERSION)) + + if ((u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK" not in + vorbis_comment.keys())): + from .text import CLEAN_FLAC_ADD_CHANNELMASK + fixes_performed.append(CLEAN_FLAC_ADD_CHANNELMASK) + vorbis_comment[ + u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"] = \ + [u"0x%.4X" % (self.channel_mask())] + + metadata.replace_blocks( + Flac_VORBISCOMMENT.BLOCK_ID, + [vorbis_comment]) + + #fix an invalid SEEKTABLE, if present + try: + if (not seektable_valid( + metadata.get_block(Flac_SEEKTABLE.BLOCK_ID), + stream_offset + 4 + self.metadata_length(), + input_f)): + from .text import CLEAN_FLAC_FIX_SEEKTABLE + fixes_performed.append(CLEAN_FLAC_FIX_SEEKTABLE) + + metadata.replace_blocks(Flac_SEEKTABLE.BLOCK_ID, + [self.seektable()]) + except IndexError: + pass + + #fix remaining metadata problems + #which automatically shifts STREAMINFO to the right place + #(the message indicating the fix has already been output) + output_track.update_metadata( + metadata.clean(fixes_performed)) + + return output_track + finally: + input_f.close() + + +class FLAC_Data_Chunk: + def __init__(self, total_frames, pcmreader): + self.id = "data" + self.__total_frames__ = total_frames + self.__pcmreader__ = pcmreader + + def __repr__(self): + return "FLAC_Data_Chunk()" + + def size(self): + """returns size of chunk in bytes + not including any spacer byte for odd-sized chunks""" + + return (self.__total_frames__ * + self.__pcmreader__.channels * + (self.__pcmreader__.bits_per_sample / 8)) + + def verify(self): + "returns True" + + return True + + def write(self, f): + """writes the entire chunk to the given output file object + returns size of entire chunk (including header and spacer) + in bytes""" + + from struct import pack + from . import FRAMELIST_SIZE + + f.write(self.id) + f.write(pack("<I", self.size())) + bytes_written = 8 + signed = (self.__pcmreader__.bits_per_sample > 8) + s = self.__pcmreader__.read(FRAMELIST_SIZE) + while (len(s) > 0): + b = s.to_bytes(False, signed) + f.write(b) + bytes_written += len(b) + s = self.__pcmreader__.read(FRAMELIST_SIZE) + + if (bytes_written % 2): + f.write(chr(0)) + bytes_written += 1 + + return bytes_written + + +class FLAC_SSND_Chunk(FLAC_Data_Chunk): + def __init__(self, total_frames, pcmreader): + self.id = "SSND" + self.__total_frames__ = total_frames + self.__pcmreader__ = pcmreader + + def __repr__(self): + return "FLAC_SSND_Chunk()" + + def size(self): + """returns size of chunk in bytes + not including any spacer byte for odd-sized chunks""" + + return 8 + (self.__total_frames__ * + self.__pcmreader__.channels * + (self.__pcmreader__.bits_per_sample / 8)) + + def write(self, f): + """writes the entire chunk to the given output file object + returns size of entire chunk (including header and spacer) + in bytes""" + + from struct import pack + from . import FRAMELIST_SIZE + + f.write(self.id) + f.write(pack(">I", self.size())) + bytes_written = 8 + f.write(pack(">II", 0, 0)) + bytes_written += 8 + s = self.__pcmreader__.read(FRAMELIST_SIZE) + while (len(s) > 0): + b = s.to_bytes(True, True) + f.write(b) + bytes_written += len(b) + s = self.__pcmreader__.read(FRAMELIST_SIZE) + + if (bytes_written % 2): + f.write(chr(0)) + bytes_written += 1 + + return bytes_written + + +####################### +#Ogg FLAC +####################### + + +class OggFlacMetaData(FlacMetaData): + @classmethod + def converted(cls, metadata): + """takes a MetaData object and returns an OggFlacMetaData object""" + + if (metadata is None): + return None + elif (isinstance(metadata, FlacMetaData)): + return cls([block.copy() for block in metadata.block_list]) + else: + return cls([Flac_VORBISCOMMENT.converted(metadata)] + + [Flac_PICTURE.converted(image) + for image in metadata.images()]) + + def __repr__(self): + return ("OggFlacMetaData(%s)" % (repr(self.block_list))) + + @classmethod + def parse(cls, reader): + """returns an OggFlacMetaData object from the given BitstreamReader + + raises IOError or ValueError if an error occurs reading MetaData""" + + from .ogg import read_ogg_packets + + streaminfo = None + applications = [] + seektable = None + vorbis_comment = None + cuesheet = None + pictures = [] + + packets = read_ogg_packets(reader) + + streaminfo_packet = packets.next() + streaminfo_packet.set_endianness(0) + + (packet_byte, + ogg_signature, + major_version, + minor_version, + header_packets, + flac_signature, + block_type, + block_length, + minimum_block_size, + maximum_block_size, + minimum_frame_size, + maximum_frame_size, + sample_rate, + channels, + bits_per_sample, + total_samples, + md5sum) = streaminfo_packet.parse( + "8u 4b 8u 8u 16u 4b 8u 24u 16u 16u 24u 24u 20u 3u 5u 36U 16b") + + block_list = [Flac_STREAMINFO(minimum_block_size=minimum_block_size, + maximum_block_size=maximum_block_size, + minimum_frame_size=minimum_frame_size, + maximum_frame_size=maximum_frame_size, + sample_rate=sample_rate, + channels=channels + 1, + bits_per_sample=bits_per_sample + 1, + total_samples=total_samples, + md5sum=md5sum)] + + for (i, packet) in zip(range(header_packets), packets): + packet.set_endianness(0) + (block_type, length) = packet.parse("1p 7u 24u") + if (block_type == 1): # PADDING + block_list.append(Flac_PADDING.parse(packet, length)) + if (block_type == 2): # APPLICATION + block_list.append(Flac_APPLICATION.parse(packet, length)) + elif (block_type == 3): # SEEKTABLE + block_list.append(Flac_SEEKTABLE.parse(packet, length / 18)) + elif (block_type == 4): # VORBIS_COMMENT + block_list.append(Flac_VORBISCOMMENT.parse(packet)) + elif (block_type == 5): # CUESHEET + block_list.append(Flac_CUESHEET.parse(packet)) + elif (block_type == 6): # PICTURE + block_list.append(Flac_PICTURE.parse(packet)) + elif ((block_type >= 7) and (block_type <= 126)): + from .text import ERR_FLAC_RESERVED_BLOCK + raise ValueError(ERR_FLAC_RESERVED_BLOCK % (block_type)) + elif (block_type == 127): + from .text import ERR_FLAC_INVALID_BLOCK + raise ValueError(ERR_FLAC_INVALID_BLOCK) + + return cls(block_list) + + def build(self, oggwriter): + """oggwriter is an OggStreamWriter-compatible object""" + + from .bitstream import BitstreamRecorder + from .bitstream import format_size + from . import iter_first, iter_last + + packet = BitstreamRecorder(0) + + #build extended Ogg FLAC STREAMINFO block + #which will always occupy its own page + streaminfo = self.get_block(Flac_STREAMINFO.BLOCK_ID) + + #all our non-STREAMINFO blocks that are small enough + #to fit in the output stream + valid_blocks = [b for b in self.blocks() + if ((b.BLOCK_ID != Flac_STREAMINFO.BLOCK_ID) and + (b.size() < (2 ** 24)))] + + packet.build( + "8u 4b 8u 8u 16u 4b 8u 24u 16u 16u 24u 24u 20u 3u 5u 36U 16b", + (0x7F, + "FLAC", + 1, + 0, + len(valid_blocks), + "fLaC", + 0, + format_size("16u 16u 24u 24u 20u 3u 5u 36U 16b") / 8, + streaminfo.minimum_block_size, + streaminfo.maximum_block_size, + streaminfo.minimum_frame_size, + streaminfo.maximum_frame_size, + streaminfo.sample_rate, + streaminfo.channels - 1, + streaminfo.bits_per_sample - 1, + streaminfo.total_samples, + streaminfo.md5sum)) + oggwriter.write_page(0, [packet.data()], 0, 1, 0) + + #FIXME - adjust non-STREAMINFO blocks to use fewer pages + + #pack remaining metadata blocks into as few pages as possible, if any + if (len(valid_blocks)): + for (last_block, block) in iter_last(iter(valid_blocks)): + packet.reset() + if (not last_block): + packet.build("1u 7u 24u", + (0, block.BLOCK_ID, block.size())) + else: + packet.build("1u 7u 24u", + (1, block.BLOCK_ID, block.size())) + block.build(packet) + for (first_page, page_segments) in iter_first( + oggwriter.segments_to_pages( + oggwriter.packet_to_segments(packet.data()))): + oggwriter.write_page(0 if first_page else -1, + page_segments, + 0 if first_page else 1, 0, 0) + + +class __Counter__: + def __init__(self): + self.value = 0 + + def count_byte(self, i): + self.value += 1 + + def __int__(self): + return self.value + + +class OggFlacAudio(FlacAudio): + """a Free Lossless Audio Codec file inside an Ogg container""" + + from .text import (COMP_FLAC_0, COMP_FLAC_8) + + SUFFIX = "oga" + NAME = SUFFIX + DESCRIPTION = u"Ogg FLAC" + DEFAULT_COMPRESSION = "8" + COMPRESSION_MODES = tuple(map(str, range(0, 9))) + COMPRESSION_DESCRIPTIONS = {"0": COMP_FLAC_0, + "8": COMP_FLAC_8} + BINARIES = ("flac",) + + METADATA_CLASS = OggFlacMetaData + + def __init__(self, filename): + """filename is a plain string""" + + AudioFile.__init__(self, filename) + self.__samplerate__ = 0 + self.__channels__ = 0 + self.__bitspersample__ = 0 + self.__total_frames__ = 0 + + try: + self.__read_streaminfo__() + except IOError, msg: + raise InvalidFLAC(str(msg)) + + def bits_per_sample(self): + """returns an integer number of bits-per-sample this track contains""" + + return self.__bitspersample__ + + def channels(self): + """returns an integer number of channels this track contains""" + + return self.__channels__ + + def total_frames(self): + """returns the total PCM frames of the track as an integer""" + + return self.__total_frames__ + + def sample_rate(self): + """returns the rate of the track's audio as an integer number of Hz""" + + return self.__samplerate__ + + def get_metadata(self): + """returns a MetaData object, or None + + raise ValueError if some error reading metadata + raises IOError if unable to read the file""" + + f = open(self.filename, "rb") + try: + from .bitstream import BitstreamReader + + try: + return OggFlacMetaData.parse(BitstreamReader(f, 1)) + except ValueError: + return None + finally: + f.close() + + def update_metadata(self, metadata): + """takes this track's current MetaData object + as returned by get_metadata() and sets this track's metadata + with any fields updated in that object + + raises IOError if unable to write the file + """ + + if (metadata is None): + return None + + if (not isinstance(metadata, OggFlacMetaData)): + from .text import ERR_FOREIGN_METADATA + raise ValueError(ERR_FOREIGN_METADATA) + + #always overwrite Ogg FLAC with fresh metadata + # + #The trouble with Ogg FLAC padding is that Ogg header overhead + #requires a variable amount of overhead bytes per Ogg page + #which makes it very difficult to calculate how many + #bytes to allocate to the PADDING packet. + #We'd have to build a bunch of empty pages for padding + #then go back and fill-in the initial padding page's length + #field before re-checksumming it. + + import tempfile + + from .bitstream import BitstreamWriter + from .bitstream import BitstreamRecorder + from .bitstream import BitstreamAccumulator + from .bitstream import BitstreamReader + from .ogg import OggStreamReader, OggStreamWriter + from . import transfer_data + + new_file = tempfile.TemporaryFile() + try: + original_file = file(self.filename, 'rb') + try: + original_reader = BitstreamReader(original_file, 1) + original_ogg = OggStreamReader(original_reader) + + new_writer = BitstreamWriter(new_file, 1) + new_ogg = OggStreamWriter(new_writer, + self.__serial_number__) + + #write our new comment blocks to the new file + metadata.build(new_ogg) + + #skip the metadata packets in the original file + OggFlacMetaData.parse(original_reader) + + #transfer the remaining pages from the original file + #(which are re-sequenced and re-checksummed automatically) + for (granule_position, + segments, + continuation, + first_page, + last_page) in original_ogg.pages(): + new_ogg.write_page(granule_position, + segments, + continuation, + first_page, + last_page) + finally: + original_file.close() + + #copy temporary file data over our original file + original_file = file(self.filename, "wb") + try: + new_file.seek(0, 0) + transfer_data(new_file.read, original_file.write) + new_file.close() + finally: + original_file.close() + finally: + new_file.close() + + def metadata_length(self): + """returns the length of all Ogg FLAC metadata blocks as an integer + + this includes all Ogg page headers""" + + from .bitstream import BitstreamReader + + f = file(self.filename, 'rb') + try: + byte_count = __Counter__() + ogg_stream = BitstreamReader(f, 1) + ogg_stream.add_callback(byte_count.count_byte) + + OggFlacMetaData.parse(ogg_stream) + + return int(byte_count) + finally: + f.close() + + def __read_streaminfo__(self): + from .bitstream import BitstreamReader + + f = open(self.filename, "rb") + try: + ogg_reader = BitstreamReader(f, 1) + (magic_number, + version, + header_type, + granule_position, + self.__serial_number__, + page_sequence_number, + checksum, + segment_count) = ogg_reader.parse("4b 8u 8u 64S 32u 32u 32u 8u") + + if (magic_number != 'OggS'): + from .text import ERR_OGG_INVALID_MAGIC_NUMBER + raise InvalidFLAC(ERR_OGG_INVALID_MAGIC_NUMBER) + if (version != 0): + from .text import ERR_OGG_INVALID_VERSION + raise InvalidFLAC(ERR_OGG_INVALID_VERSION) + + segment_length = ogg_reader.read(8) + + ogg_reader.set_endianness(0) + + (packet_byte, + ogg_signature, + major_version, + minor_version, + self.__header_packets__, + flac_signature, + block_type, + block_length, + minimum_block_size, + maximum_block_size, + minimum_frame_size, + maximum_frame_size, + self.__samplerate__, + self.__channels__, + self.__bitspersample__, + self.__total_frames__, + self.__md5__) = ogg_reader.parse( + "8u 4b 8u 8u 16u 4b 8u 24u 16u 16u 24u 24u 20u 3u 5u 36U 16b") + + if (packet_byte != 0x7F): + from .text import ERR_OGGFLAC_INVALID_PACKET_BYTE + raise InvalidFLAC(ERR_OGGFLAC_INVALID_PACKET_BYTE) + if (ogg_signature != 'FLAC'): + from .text import ERR_OGGFLAC_INVALID_OGG_SIGNATURE + raise InvalidFLAC(ERR_OGGFLAC_INVALID_OGG_SIGNATURE) + if (major_version != 1): + from .text import ERR_OGGFLAC_INVALID_MAJOR_VERSION + raise InvalidFLAC(ERR_OGGFLAC_INVALID_MAJOR_VERSION) + if (minor_version != 0): + from .text import ERR_OGGFLAC_INVALID_MINOR_VERSION + raise InvalidFLAC(ERR_OGGFLAC_INVALID_MINOR_VERSION) + if (flac_signature != 'fLaC'): + from .text import ERR_OGGFLAC_VALID_FLAC_SIGNATURE + raise InvalidFLAC(ERR_OGGFLAC_VALID_FLAC_SIGNATURE) + + self.__channels__ += 1 + self.__bitspersample__ += 1 + finally: + f.close() + + def to_pcm(self): + """returns a PCMReader object containing the track's PCM data""" + + from . import decoders + from . import PCMReaderError + + try: + return decoders.OggFlacDecoder(self.filename, + self.channel_mask()) + except (IOError, ValueError), msg: + #The only time this is likely to occur is + #if the Ogg FLAC is modified between when OggFlacAudio + #is initialized and when to_pcm() is called. + return PCMReaderError(error_message=str(msg), + 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 OggFlacAudio object""" + + from . import BIN + from . import transfer_framelist_data + from . import ignore_sigint + from . import EncodingError + from . import DecodingError + from . import UnsupportedChannelCount + from . import __default_quality__ + import subprocess + import os + + SUBSTREAM_SAMPLE_RATES = frozenset([8000, 16000, 22050, 24000, 32000, + 44100, 48000, 96000]) + SUBSTREAM_BITS = frozenset([8, 12, 16, 20, 24]) + + if ((compression is None) or (compression not in + cls.COMPRESSION_MODES)): + compression = __default_quality__(cls.NAME) + + if (((pcmreader.sample_rate in SUBSTREAM_SAMPLE_RATES) and + (pcmreader.bits_per_sample in SUBSTREAM_BITS))): + lax = [] + else: + lax = ["--lax"] + + if (pcmreader.channels > 8): + raise UnsupportedChannelCount(filename, pcmreader.channels) + + if (int(pcmreader.channel_mask) == 0): + if (pcmreader.channels <= 6): + channel_mask = {1: 0x0004, + 2: 0x0003, + 3: 0x0007, + 4: 0x0033, + 5: 0x0037, + 6: 0x003F}[pcmreader.channels] + else: + channel_mask = 0 + + elif (int(pcmreader.channel_mask) not in + (0x0001, # 1ch - mono + 0x0004, # 1ch - mono + 0x0003, # 2ch - left, right + 0x0007, # 3ch - left, right, center + 0x0033, # 4ch - left, right, back left, back right + 0x0603, # 4ch - left, right, side left, side right + 0x0037, # 5ch - L, R, C, back left, back right + 0x0607, # 5ch - L, R, C, side left, side right + 0x003F, # 6ch - L, R, C, LFE, back left, back right + 0x060F)): # 6ch - L, R, C, LFE, side left, side right + from . import UnsupportedChannelMask + + raise UnsupportedChannelMask(filename, + int(pcmreader.channel_mask)) + else: + channel_mask = int(pcmreader.channel_mask) + + devnull = file(os.devnull, 'ab') + + sub = subprocess.Popen([BIN['flac']] + lax + + ["-s", "-f", "-%s" % (compression), + "-V", "--ogg", + "--endian=little", + "--channels=%d" % (pcmreader.channels), + "--bps=%d" % (pcmreader.bits_per_sample), + "--sample-rate=%d" % (pcmreader.sample_rate), + "--sign=signed", + "--force-raw-format", + "-o", filename, "-"], + stdin=subprocess.PIPE, + stdout=devnull, + stderr=devnull, + preexec_fn=ignore_sigint) + + try: + transfer_framelist_data(pcmreader, sub.stdin.write) + except (ValueError, IOError), err: + sub.stdin.close() + sub.wait() + cls.__unlink__(filename) + raise EncodingError(str(err)) + except Exception, err: + sub.stdin.close() + sub.wait() + cls.__unlink__(filename) + raise err + + try: + pcmreader.close() + except DecodingError, err: + raise EncodingError(err.error_message) + sub.stdin.close() + devnull.close() + + if (sub.wait() == 0): + oggflac = OggFlacAudio(filename) + if ((((pcmreader.channels > 2) or + (pcmreader.bits_per_sample > 16)) and + (channel_mask != 0))): + metadata = oggflac.get_metadata() + vorbis = metadata.get_block(Flac_VORBISCOMMENT.BLOCK_ID) + vorbis[u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"] = [ + u"0x%.4X" % (channel_mask)] + oggflac.update_metadata(metadata) + return oggflac + else: + #FIXME + raise EncodingError(u"error encoding file with flac") + + def sub_pcm_tracks(self): + """yields a PCMReader object per cuesheet track + + this currently does nothing since the FLAC reference + decoder has limited support for Ogg FLAC + """ + + return iter([]) + + 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""" + + from .verify import ogg as verify_ogg_stream + + #Ogg stream 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, err: + raise InvalidFLAC(str(err)) + try: + try: + result = verify_ogg_stream(f) + if (progress is not None): + progress(1, 1) + return result is None + except (IOError, ValueError), err: + raise InvalidFLAC(str(err)) + finally: + f.close()
View file
audiotools-2.18.tar.gz/audiotools/freedb.py -> audiotools-2.19.tar.gz/audiotools/freedb.py
Changed
@@ -17,8 +17,6 @@ #along with this program; if not, write to the Free Software #Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import audiotools - class DiscID: def __init__(self, offsets, total_length, track_count): @@ -68,6 +66,7 @@ from urllib import urlencode from itertools import izip from time import sleep + from . import VERSION RESPONSE = re.compile(r'(\d{3}) (.+?)[\r\n]+') QUERY_RESULT = re.compile(r'(\S+) ([0-9a-fA-F]{8}) (.+)') @@ -79,10 +78,10 @@ #and get a list of category/disc id/title results #if any matches are found m = urlopen("http://%s:%d/~cddb/cddb.cgi" % (freedb_server, freedb_port), - urlencode({"hello": "user %s %s %s" % \ - (getfqdn(), - "audiotools", - audiotools.VERSION), + urlencode({"hello": "user %s %s %s" % + (getfqdn(), + "audiotools", + VERSION), "proto": str(6), "cmd": ("cddb query %(disc_id)s %(track_count)d " + "%(offsets)s %(seconds)d") % @@ -131,13 +130,15 @@ if (len(matches) > 0): #for each result, query FreeDB for XMCD file data for (category, disc_id, title) in matches: + from . import VERSION + sleep(1) # add a slight delay to keep the server happy m = urlopen("http://%s:%d/~cddb/cddb.cgi" % (freedb_server, freedb_port), - urlencode({"hello": "user %s %s %s" % \ - (getfqdn(), - "audiotools", - audiotools.VERSION), + urlencode({"hello": "user %s %s %s" % + (getfqdn(), + "audiotools", + VERSION), "proto": str(6), "cmd": ("cddb read %(category)s " + "%(disc_id)s") % @@ -174,10 +175,10 @@ if (" / " in dtitle): (album_artist, album_name) = dtitle.split(" / ", 1) else: - album_artist = "" + album_artist = None album_name = dtitle - year = freedb_file.get("DYEAR", "") + year = freedb_file.get("DYEAR", None) ttitles = [(int(m.group(1)), value) for (m, value) in [(TTITLE.match(key), value) for (key, value) in @@ -197,10 +198,14 @@ track_artist = album_artist track_name = ttitle - yield audiotools.MetaData( + from . import MetaData + + yield MetaData( track_name=track_name.decode('utf-8', 'replace'), track_number=tracknum + 1, track_total=track_total, album_name=album_name.decode('utf-8', 'replace'), - artist_name=track_artist.decode('utf-8', 'replace'), - year=year.decode('utf-8', 'replace')) + artist_name=(track_artist.decode('utf-8', 'replace') + if track_artist is not None else None), + year=(year.decode('utf-8', 'replace') if + year is not None else None))
View file
audiotools-2.19.tar.gz/audiotools/id3.py
Added
@@ -0,0 +1,2312 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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 . import (MetaData, Image, InvalidImage) +import codecs + +from id3v1 import ID3v1Comment + + +def is_latin_1(unicode_string): + """returns True if the given unicode string is a subset of latin-1""" + + return frozenset(unicode_string).issubset( + frozenset(map(unichr, range(32, 127) + range(160, 256)))) + + +class UCS2Codec(codecs.Codec): + """a special unicode codec for UCS-2 + + this is a subset of UTF-16 with no support for surrogate pairs, + limiting it to U+0000-U+FFFF""" + + @classmethod + def fix_char(cls, c): + """a filter which changes overly large c values to 'unknown'""" + + if (ord(c) <= 0xFFFF): + return c + else: + return u"\ufffd" + + def encode(self, input, errors='strict'): + """encodes unicode input to plain UCS-2 strings""" + + return codecs.utf_16_encode(u"".join(map(self.fix_char, input)), + errors) + + def decode(self, input, errors='strict'): + """decodes plain UCS-2 strings to unicode""" + + (chars, size) = codecs.utf_16_decode(input, errors, True) + return (u"".join(map(self.fix_char, chars)), size) + + +class UCS2CodecStreamWriter(UCS2Codec, codecs.StreamWriter): + pass + + +class UCS2CodecStreamReader(UCS2Codec, codecs.StreamReader): + pass + + +def __reg_ucs2__(name): + if (name == 'ucs2'): + return (UCS2Codec().encode, + UCS2Codec().decode, + UCS2CodecStreamReader, + UCS2CodecStreamWriter) + else: + return None + +codecs.register(__reg_ucs2__) + + +def decode_syncsafe32(reader): + """returns a syncsafe32 integer from a BitstreamReader""" + + from operator import or_ + return reduce(or_, + [size << (7 * (3 - i)) + for (i, size) in + enumerate(reader.parse("1p 7u 1p 7u 1p 7u 1p 7u"))]) + + +def encode_syncsafe32(writer, value): + """writes a syncsafe32 integer to a BitstreamWriter""" + + writer.build("1p 7u 1p 7u 1p 7u 1p 7u", + [(value >> (7 * i)) & 0x7F for i in [3, 2, 1, 0]]) + + +class C_string: + TERMINATOR = {'ascii': chr(0), + 'latin_1': chr(0), + 'latin-1': chr(0), + 'ucs2': chr(0) * 2, + 'utf_16': chr(0) * 2, + 'utf-16': chr(0) * 2, + 'utf_16be': chr(0) * 2, + 'utf-16be': chr(0) * 2, + 'utf_8': chr(0), + 'utf-8': chr(0)} + + def __init__(self, encoding, unicode_string): + """encoding is a string such as 'utf-8', 'latin-1', etc""" + + self.encoding = encoding + self.unicode_string = unicode_string + + def __repr__(self): + return "C_string(%s, %s)" % (repr(self.encoding), + repr(self.unicode_string)) + + def __unicode__(self): + return self.unicode_string + + def __getitem__(self, char): + return self.unicode_string[char] + + def __len__(self): + return len(self.unicode_string) + + def __cmp__(self, c_string): + return cmp(self.unicode_string, c_string.unicode_string) + + @classmethod + def parse(cls, encoding, reader): + """returns a C_string with the given encoding string + from the given BitstreamReader + raises LookupError if encoding is unknown + raises IOError if a problem occurs reading the stream + """ + + try: + terminator = cls.TERMINATOR[encoding] + terminator_size = len(terminator) + except KeyError: + raise LookupError(encoding) + + s = [] + char = reader.read_bytes(terminator_size) + while (char != terminator): + s.append(char) + char = reader.read_bytes(terminator_size) + + return cls(encoding, "".join(s).decode(encoding, 'replace')) + + def build(self, writer): + """writes our C_string data to the given BitstreamWriter + with the appropriate terminator""" + + writer.write_bytes(self.unicode_string.encode(self.encoding, + 'replace')) + writer.write_bytes(self.TERMINATOR[self.encoding]) + + def size(self): + """returns the length of our C string in bytes""" + + return (len(self.unicode_string.encode(self.encoding, 'replace')) + + len(self.TERMINATOR[self.encoding])) + + +def __attrib_equals__(attributes, o1, o2): + for attrib in attributes: + if (((not hasattr(o1, attrib)) or + (not hasattr(o2, attrib)) or + (getattr(o1, attrib) != getattr(o2, attrib)))): + return False + else: + return True + + +#takes a pair of integers (or None) for the current and total values +#returns a unicode string of their combined pair +#for example, __number_pair__(2,3) returns u"2/3" +#whereas __number_pair__(4,0) returns u"4" +def __number_pair__(current, total): + from . import config + + if (config.getboolean_default("ID3", "pad", False)): + unslashed_format = u"%2.2d" + slashed_format = u"%2.2d/%2.2d" + else: + unslashed_format = u"%d" + slashed_format = u"%d/%d" + + if (current is None): + if (total is None): + return unslashed_format % (0,) + else: + return slashed_format % (0, total) + else: # current is not None + if (total is None): + return unslashed_format % (current,) + else: + return slashed_format % (current, total) + + +def read_id3v2_comment(filename): + """given a filename, returns an ID3v22Comment or a subclass + + for example, if the file is ID3v2.3 tagged, + this returns an ID3v23Comment + """ + + from .bitstream import BitstreamReader + + reader = BitstreamReader(file(filename, "rb"), 0) + reader.mark() + try: + (tag, version_major, version_minor) = reader.parse("3b 8u 8u") + if (tag != 'ID3'): + raise ValueError("invalid ID3 header") + elif (version_major == 0x2): + reader.rewind() + return ID3v22Comment.parse(reader) + elif (version_major == 0x3): + reader.rewind() + return ID3v23Comment.parse(reader) + elif (version_major == 0x4): + reader.rewind() + return ID3v24Comment.parse(reader) + else: + raise ValueError("unsupported ID3 version") + finally: + reader.unmark() + reader.close() + + +def skip_id3v2_comment(file): + """seeks past an ID3v2 comment if found in the file stream + returns the number of bytes skipped + + the stream must be seekable, obviously""" + + from .bitstream import BitstreamReader + + bytes_skipped = 0 + reader = BitstreamReader(file, 0) + reader.mark() + try: + (tag_id, version_major, version_minor) = reader.parse("3b 8u 8u 8p") + except IOError, err: + reader.unmark() + raise err + + if ((tag_id == 'ID3') and (version_major in (2, 3, 4))): + reader.unmark() + + #parse the header + bytes_skipped += 6 + tag_size = decode_syncsafe32(reader) + bytes_skipped += 4 + + #skip to the end of its length + reader.skip_bytes(tag_size) + bytes_skipped += tag_size + + #skip any null bytes after the IDv2 tag + reader.mark() + try: + byte = reader.read(8) + while (byte == 0): + reader.unmark() + bytes_skipped += 1 + reader.mark() + byte = reader.read(8) + + reader.rewind() + reader.unmark() + + return bytes_skipped + except IOError, err: + reader.unmark() + raise err + else: + reader.rewind() + reader.unmark() + return 0 + + +############################################################ +# ID3v2.2 Comment +############################################################ + + +class ID3v22_Frame: + def __init__(self, frame_id, data): + self.id = frame_id + self.data = data + + def copy(self): + return self.__class__(self.id, self.data) + + def __repr__(self): + return "ID3v22_Frame(%s, %s)" % (repr(self.id), repr(self.data)) + + def raw_info(self): + if (len(self.data) > 20): + return u"%s = %s\u2026" % \ + (self.id.decode('ascii', 'replace'), + u"".join([u"%2.2X" % (ord(b)) for b in self.data[0:20]])) + else: + return u"%s = %s" % \ + (self.id.decode('ascii', 'replace'), + u"".join([u"%2.2X" % (ord(b)) for b in self.data])) + + def __eq__(self, frame): + return __attrib_equals__(["id", "data"], self, frame) + + @classmethod + def parse(cls, frame_id, frame_size, reader): + """given a frame_id string, frame_size int and BitstreamReader + of the remaining frame data, returns a parsed ID3v2?_Frame""" + + return cls(frame_id, reader.read_bytes(frame_size)) + + def build(self, writer): + """writes this frame to the given BitstreamWriter + not including its frame header""" + + writer.write_bytes(self.data) + + def size(self): + """returns the size of this frame in bytes + not including the frame header""" + + return len(self.data) + + @classmethod + def converted(cls, frame_id, o): + """given foreign data, returns an ID3v22_Frame""" + + raise NotImplementedError() + + def clean(self, fixes_applied): + """returns a cleaned ID3v22_Frame, + or None if the frame should be removed entirely + any fixes are appended to fixes_applied as unicode string""" + + return self.__class__(self.id, self.data) + + +class ID3v22_T__Frame: + NUMERICAL_IDS = ('TRK', 'TPA') + + def __init__(self, frame_id, encoding, data): + """fields are as follows: + | frame_id | 3 byte frame ID string | + | encoding | 1 byte encoding int | + | data | text data as raw string | + """ + + assert((encoding == 0) or (encoding == 1)) + + self.id = frame_id + self.encoding = encoding + self.data = data + + def copy(self): + return self.__class__(self.id, self.encoding, self.data) + + def __repr__(self): + return "ID3v22_T__Frame(%s, %s, %s)" % \ + (repr(self.id), repr(self.encoding), repr(self.data)) + + def raw_info(self): + """returns a human-readable version of this frame as unicode""" + + return u"%s = (%s) %s" % \ + (self.id.decode('ascii'), + {0: u"Latin-1", 1: u"UCS-2"}[self.encoding], + unicode(self)) + + def __eq__(self, frame): + return __attrib_equals__(["id", "encoding", "data"], self, frame) + + def __unicode__(self): + return self.data.decode( + {0: 'latin-1', 1: 'ucs2'}[self.encoding], + 'replace').split(unichr(0), 1)[0] + + def number(self): + """if the frame is numerical, returns the track/album_number portion + raises TypeError if not""" + + import re + + if (self.id in self.NUMERICAL_IDS): + unicode_value = unicode(self) + int_string = re.search(r'\d+', unicode_value) + if (int_string is not None): + int_value = int(int_string.group(0)) + if (int_value == 0): + total_string = re.search(r'/\D*?(\d+)', unicode_value) + if (total_string is not None): + #don't return placeholder 0 value + #when a track_total value is present + #but track_number value is 0 + return None + else: + return int_value + else: + return int_value + else: + return None + else: + raise TypeError() + + def total(self): + """if the frame is numerical, returns the track/album_total portion + raises TypeError if not""" + + import re + + if (self.id in self.NUMERICAL_IDS): + int_value = re.search(r'/\D*?(\d+)', unicode(self)) + if (int_value is not None): + return int(int_value.group(1)) + else: + return None + else: + raise TypeError() + + @classmethod + def parse(cls, frame_id, frame_size, reader): + """given a frame_id string, frame_size int and BitstreamReader + of the remaining frame data, returns a parsed text frame""" + + encoding = reader.read(8) + return cls(frame_id, encoding, reader.read_bytes(frame_size - 1)) + + def build(self, writer): + """writes the frame's data to the BitstreamWriter + not including its frame header""" + + writer.build("8u %db" % (len(self.data)), (self.encoding, self.data)) + + def size(self): + """returns the frame's total size + not including its frame header""" + + return 1 + len(self.data) + + @classmethod + def converted(cls, frame_id, unicode_string): + """given a unicode string, returns a text frame""" + + if (is_latin_1(unicode_string)): + return cls(frame_id, 0, unicode_string.encode('latin-1')) + else: + return cls(frame_id, 1, unicode_string.encode('ucs2')) + + def clean(self, fixes_performed): + """returns a cleaned frame, + or None if the frame should be removed entirely + any fixes are appended to fixes_applied as unicode string""" + + from .text import (CLEAN_REMOVE_EMPTY_TAG, + CLEAN_REMOVE_TRAILING_WHITESPACE, + CLEAN_REMOVE_LEADING_WHITESPACE, + CLEAN_REMOVE_LEADING_ZEROES, + CLEAN_ADD_LEADING_ZEROES) + + field = self.id.decode('ascii') + value = unicode(self) + + #check for an empty tag + if (len(value.strip()) == 0): + fixes_performed.append(CLEAN_REMOVE_EMPTY_TAG % + {"field": field}) + return None + + #check trailing whitespace + fix1 = value.rstrip() + if (fix1 != value): + fixes_performed.append(CLEAN_REMOVE_TRAILING_WHITESPACE % + {"field": field}) + + #check leading whitespace + fix2 = fix1.lstrip() + if (fix2 != fix1): + fixes_performed.append(CLEAN_REMOVE_LEADING_WHITESPACE % + {"field": field}) + + #check leading zeroes for a numerical tag + if (self.id in self.NUMERICAL_IDS): + fix3 = __number_pair__(self.number(), self.total()) + if (fix3 != fix2): + from . import config + + if (config.getboolean_default("ID3", "pad", False)): + fixes_performed.append(CLEAN_ADD_LEADING_ZEROES % + {"field": field}) + else: + fixes_performed.append(CLEAN_REMOVE_LEADING_ZEROES % + {"field": field}) + else: + fix3 = fix2 + + return self.__class__.converted(self.id, fix3) + + +class ID3v22_TXX_Frame: + def __init__(self, encoding, description, data): + self.id = 'TXX' + + self.encoding = encoding + self.description = description + self.data = data + + def copy(self): + return self.__class__(self.encoding, + self.description, + self.data) + + def __repr__(self): + return "ID3v22_TXX_Frame(%s, %s, %s)" % \ + (repr(self.encoding), repr(self.description), repr(self.data)) + + def raw_info(self): + """returns a human-readable version of this frame as unicode""" + + return u"%s = (%s, \"%s\") %s" % \ + (self.id, + {0: u"Latin-1", 1: u"UCS-2"}[self.encoding], + self.description, + unicode(self)) + + def __eq__(self, frame): + return __attrib_equals__(["id", "encoding", "description", "data"]) + + def __unicode__(self): + return self.data.decode( + {0: 'latin-1', 1: 'ucs2'}[self.encoding], + 'replace').split(unichr(0), 1)[0] + + @classmethod + def parse(cls, frame_id, frame_size, reader): + """given a frame_id string, frame_size int and BitstreamReader + of the remaining frame data, returns a parsed text frame""" + + encoding = reader.read(8) + description = C_string.parse({0: "latin-1", 1: "ucs2"}[encoding], + reader) + data = reader.read_bytes(frame_size - 1 - description.size()) + + return cls(encoding, description, data) + + def build(self, writer): + """writes this frame to the given BitstreamWriter + not including its frame header""" + + writer.write(8, self.encoding) + self.description.build(writer) + writer.write_bytes(self.data) + + def size(self): + """returns the size of this frame in bytes + not including the frame header""" + + return 1 + self.description.size() + len(self.data) + + def clean(self, fixes_performed): + """returns a cleaned frame, + or None if the frame should be removed entirely + any fixes are appended to fixes_applied as unicode string""" + + from audiotools.text import (CLEAN_REMOVE_EMPTY_TAG, + CLEAN_REMOVE_TRAILING_WHITESPACE, + CLEAN_REMOVE_LEADING_WHITESPACE) + + field = self.id.decode('ascii') + value = unicode(self) + + #check for an empty tag + if (len(value.strip()) == 0): + fixes_performed.append(CLEAN_REMOVE_EMPTY_TAG % + {"field": field}) + return None + + #check trailing whitespace + fix1 = value.rstrip() + if (fix1 != value): + fixes_performed.append(CLEAN_REMOVE_TRAILING_WHITESPACE % + {"field": field}) + + #check leading whitespace + fix2 = fix1.lstrip() + if (fix2 != fix1): + fixes_performed.append(CLEAN_REMOVE_LEADING_WHITESPACE % + {"field": field}) + + return self.__class__(self.encoding, self.description, fix2) + + +class ID3v22_W__Frame: + def __init__(self, frame_id, data): + self.id = frame_id + self.data = data + + def copy(self): + return self.__class__(self.id, self.data) + + def __repr__(self): + return "ID3v22_W__Frame(%s, %s)" % \ + (repr(self.id), repr(self.data)) + + def raw_info(self): + """returns a human-readable version of this frame as unicode""" + + return u"%s = %s" % (self.id.decode('ascii'), + self.data.decode('ascii', 'replace')) + + def __eq__(self, frame): + return __attrib_equals__(["id", "data"], self, frame) + + @classmethod + def parse(cls, frame_id, frame_size, reader): + return cls(frame_id, reader.read_bytes(frame_size)) + + def build(self, writer): + """writes this frame to the given BitstreamWriter + not including its frame header""" + + writer.write_bytes(self.data) + + def size(self): + """returns the size of this frame in bytes + not including the frame header""" + + return len(self.data) + + def clean(self, fixes_applied): + """returns a cleaned frame, + or None if the frame should be removed entirely + any fixes are appended to fixes_applied as unicode string""" + + return self.__class__(self.id, self.data) + + +class ID3v22_WXX_Frame: + def __init__(self, encoding, description, data): + self.id = 'WXX' + + self.encoding = encoding + self.description = description + self.data = data + + def copy(self): + return self.__class__(self.encoding, + self.description, + self.data) + + def __repr__(self): + return "ID3v22_WXX_Frame(%s, %s, %s)" % \ + (repr(self.encoding), repr(self.description), repr(self.data)) + + def raw_info(self): + """returns a human-readable version of this frame as unicode""" + + return u"%s = (%s, \"%s\") %s" % \ + (self.id, + {0: u"Latin-1", 1: u"UCS-2"}[self.encoding], + self.description, + self.data.decode('ascii', 'replace')) + + def __eq__(self, frame): + return __attrib_equals__(["id", "encoding", "description", "data"]) + + @classmethod + def parse(cls, frame_id, frame_size, reader): + """given a frame_id string, frame_size int and BitstreamReader + of the remaining frame data, returns a parsed text frame""" + + encoding = reader.read(8) + description = C_string.parse({0: "latin-1", 1: "ucs2"}[encoding], + reader) + data = reader.read_bytes(frame_size - 1 - description.size()) + + return cls(encoding, description, data) + + def build(self, writer): + """writes this frame to the given BitstreamWriter + not including its frame header""" + + writer.write(8, self.encoding) + self.description.build(writer) + writer.write_bytes(self.data) + + def size(self): + """returns the size of this frame in bytes + not including the frame header""" + + return 1 + self.description.size() + len(self.data) + + def clean(self, fixes_performed): + """returns a cleaned frame, + or None if the frame should be removed entirely + any fixes are appended to fixes_applied as unicode string""" + + return self.__class__(self.encoding, + self.description, + self.data) + + +class ID3v22_COM_Frame: + def __init__(self, encoding, language, short_description, data): + """fields are as follows: + | encoding | 1 byte int of the comment's text encoding | + | language | 3 byte string of the comment's language | + | short_description | C_string of a short description | + | data | plain string of the comment data itself | + """ + + self.id = "COM" + self.encoding = encoding + self.language = language + self.short_description = short_description + self.data = data + + def copy(self): + return self.__class__(self.encoding, + self.language, + self.short_description, + self.data) + + def __repr__(self): + return "ID3v22_COM_Frame(%s, %s, %s, %s)" % \ + (repr(self.encoding), repr(self.language), + repr(self.short_description), repr(self.data)) + + def raw_info(self): + """returns a human-readable version of this frame as unicode""" + + return u"COM = (%s, %s, \"%s\") %s" % \ + ({0: u'Latin-1', 1: 'UCS-2'}[self.encoding], + self.language.decode('ascii', 'replace'), + self.short_description, + self.data.decode({0: 'latin-1', 1: 'ucs2'}[self.encoding])) + + def __eq__(self, frame): + return __attrib_equals__(["encoding", + "language", + "short_description", + "data"], self, frame) + + def __unicode__(self): + return self.data.decode({0: 'latin-1', 1: 'ucs2'}[self.encoding], + 'replace') + + @classmethod + def parse(cls, frame_id, frame_size, reader): + """given a frame_id string, frame_size int and BitstreamReader + of the remaining frame data, returns a parsed ID3v22_COM_Frame""" + + (encoding, language) = reader.parse("8u 3b") + short_description = C_string.parse({0: 'latin-1', 1: 'ucs2'}[encoding], + reader) + data = reader.read_bytes(frame_size - (4 + short_description.size())) + + return cls(encoding, language, short_description, data) + + def build(self, writer): + """writes this frame to the given BitstreamWriter + not including its frame header""" + + writer.build("8u 3b", (self.encoding, self.language)) + self.short_description.build(writer) + writer.write_bytes(self.data) + + def size(self): + """returns the size of this frame in bytes + not including the frame header""" + + return 4 + self.short_description.size() + len(self.data) + + @classmethod + def converted(cls, frame_id, unicode_string): + if (is_latin_1(unicode_string)): + return cls(0, "eng", C_string("latin-1", u""), + unicode_string.encode('latin-1')) + else: + return cls(1, "eng", C_string("ucs2", u""), + unicode_string.encode('ucs2')) + + def clean(self, fixes_performed): + """returns a cleaned frame of the same class + or None if the frame should be omitted + fix text will be appended to fixes_performed, if necessary""" + + from audiotools.text import (CLEAN_REMOVE_EMPTY_TAG, + CLEAN_REMOVE_TRAILING_WHITESPACE, + CLEAN_REMOVE_LEADING_WHITESPACE) + + field = self.id.decode('ascii') + text_encoding = {0: 'latin-1', 1: 'ucs2'} + + value = self.data.decode(text_encoding[self.encoding], 'replace') + + #check for an empty tag + if (len(value.strip()) == 0): + fixes_performed.append(CLEAN_REMOVE_EMPTY_TAG % + {"field": field}) + return None + + #check trailing whitespace + fix1 = value.rstrip() + if (fix1 != value): + fixes_performed.append(CLEAN_REMOVE_TRAILING_WHITESPACE % + {"field": field}) + + #check leading whitespace + fix2 = fix1.lstrip() + if (fix2 != fix1): + fixes_performed.append(CLEAN_REMOVE_LEADING_WHITESPACE % + {"field": field}) + + #stripping whitespace shouldn't alter text/description encoding + + return self.__class__(self.encoding, + self.language, + self.short_description, + fix2.encode(text_encoding[self.encoding])) + + +class ID3v22_PIC_Frame(Image): + def __init__(self, image_format, picture_type, description, data): + """fields are as follows: + | image_format | a 3 byte image format, such as 'JPG' | + | picture_type | a 1 byte field indicating front cover, etc. | + | description | a description of the image as a C_string | + | data | image data itself as a raw string | + """ + + self.id = 'PIC' + + #add PIC-specific fields + self.pic_format = image_format + self.pic_type = picture_type + self.pic_description = description + + #figure out image metrics from raw data + try: + metrics = Image.new(data, u'', 0) + except InvalidImage: + metrics = Image(data=data, mime_type=u'', + width=0, height=0, color_depth=0, color_count=0, + description=u'', type=0) + + #then initialize Image parent fields from metrics + self.mime_type = metrics.mime_type + self.width = metrics.width + self.height = metrics.height + self.color_depth = metrics.color_depth + self.color_count = metrics.color_count + self.data = data + + def copy(self): + return ID3v22_PIC_Frame(self.pic_format, + self.pic_type, + self.pic_description, + self.data) + + def __repr__(self): + return "ID3v22_PIC_Frame(%s, %s, %s, ...)" % \ + (repr(self.pic_format), repr(self.pic_type), + repr(self.pic_description)) + + def raw_info(self): + """returns a human-readable version of this frame as unicode""" + + return u"PIC = (%s, %d\u00D7%d, %s, \"%s\") %d bytes" % \ + (self.type_string(), + self.width, + self.height, + self.mime_type, + self.pic_description, + len(self.data)) + + def type_string(self): + return {0: "Other", + 1: "32x32 pixels 'file icon' (PNG only)", + 2: "Other file icon", + 3: "Cover (front)", + 4: "Cover (back)", + 5: "Leaflet page", + 6: "Media (e.g. label side of CD)", + 7: "Lead artist/lead performer/soloist", + 8: "Artist / Performer", + 9: "Conductor", + 10: "Band / Orchestra", + 11: "Composer", + 12: "Lyricist / Text writer", + 13: "Recording Location", + 14: "During recording", + 15: "During performance", + 16: "Movie/Video screen capture", + 17: "A bright coloured fish", + 18: "Illustration", + 19: "Band/Artist logotype", + 20: "Publisher/Studio logotype"}.get(self.pic_type, "Other") + + def __getattr__(self, attr): + if (attr == 'type'): + return {3: 0, # front cover + 4: 1, # back cover + 5: 2, # leaflet page + 6: 3 # media + }.get(self.pic_type, 4) # other + elif (attr == 'description'): + return unicode(self.pic_description) + else: + raise AttributeError(attr) + + def __setattr__(self, attr, value): + if (attr == 'type'): + self.__dict__["pic_type"] = {0: 3, # front cover + 1: 4, # back cover + 2: 5, # leaflet page + 3: 6, # media + }.get(value, 0) # other + elif (attr == 'description'): + if (is_latin_1(value)): + self.__dict__["pic_description"] = C_string('latin-1', value) + else: + self.__dict__["pic_description"] = C_string('ucs2', value) + else: + self.__dict__[attr] = value + + @classmethod + def parse(cls, frame_id, frame_size, reader): + (encoding, image_format, picture_type) = reader.parse("8u 3b 8u") + description = C_string.parse({0: 'latin-1', + 1: 'ucs2'}[encoding], reader) + data = reader.read_bytes(frame_size - (5 + description.size())) + return cls(image_format, + picture_type, + description, + data) + + def build(self, writer): + """writes this frame to the given BitstreamWriter + not including its frame header""" + + writer.build("8u 3b 8u", ({'latin-1': 0, + 'ucs2': 1}[self.pic_description.encoding], + self.pic_format, + self.pic_type)) + self.pic_description.build(writer) + writer.write_bytes(self.data) + + def size(self): + """returns the size of this frame in bytes + not including the frame header""" + + return (5 + self.pic_description.size() + len(self.data)) + + @classmethod + def converted(cls, frame_id, image): + if (is_latin_1(image.description)): + description = C_string('latin-1', image.description) + else: + description = C_string('ucs2', image.description) + + return cls(image_format={u"image/png": u"PNG", + u"image/jpeg": u"JPG", + u"image/jpg": u"JPG", + u"image/x-ms-bmp": u"BMP", + u"image/gif": u"GIF", + u"image/tiff": u"TIF"}.get(image.mime_type, + 'UNK'), + picture_type={0: 3, # front cover + 1: 4, # back cover + 2: 5, # leaflet page + 3: 6, # media + }.get(image.type, 0), # other + description=description, + data=image.data) + + def clean(self, fixes_performed): + """returns a cleaned ID3v22_PIC_Frame, + or None if the frame should be removed entirely + any fixes are appended to fixes_applied as unicode string""" + + #all the fields are derived from the image data + #so there's no need to test for a mismatch + + #not sure if it's worth testing for bugs in the description + #or format fields + + return ID3v22_PIC_Frame(self.pic_format, + self.pic_type, + self.pic_description, + self.data) + + +class ID3v22Comment(MetaData): + NAME = u'ID3v2.2' + + ATTRIBUTE_MAP = {'track_name': 'TT2', + 'track_number': 'TRK', + 'track_total': 'TRK', + 'album_name': 'TAL', + 'artist_name': 'TP1', + 'performer_name': 'TP2', + 'conductor_name': 'TP3', + 'composer_name': 'TCM', + 'media': 'TMT', + 'ISRC': 'TRC', + 'copyright': 'TCR', + 'publisher': 'TPB', + 'year': 'TYE', + 'date': 'TRD', + 'album_number': 'TPA', + 'album_total': 'TPA', + 'comment': 'COM'} + + RAW_FRAME = ID3v22_Frame + TEXT_FRAME = ID3v22_T__Frame + USER_TEXT_FRAME = ID3v22_TXX_Frame + WEB_FRAME = ID3v22_W__Frame + USER_WEB_FRAME = ID3v22_WXX_Frame + COMMENT_FRAME = ID3v22_COM_Frame + IMAGE_FRAME = ID3v22_PIC_Frame + IMAGE_FRAME_ID = 'PIC' + + def __init__(self, frames): + self.__dict__["frames"] = frames[:] + + def copy(self): + return self.__class__([frame.copy() for frame in self]) + + def __repr__(self): + return "ID3v22Comment(%s)" % (repr(self.frames)) + + def __iter__(self): + return iter(self.frames) + + def raw_info(self): + """returns a human-readable version of this frame as unicode""" + + from os import linesep + + return linesep.decode('ascii').join( + ["%s:" % (self.NAME)] + + [frame.raw_info() for frame in self]) + + @classmethod + def parse(cls, reader): + """given a BitstreamReader, returns a parsed ID3v22Comment""" + + (id3, + major_version, + minor_version, + flags) = reader.parse("3b 8u 8u 8u") + if (id3 != 'ID3'): + raise ValueError("invalid ID3 header") + elif (major_version != 0x02): + raise ValueError("invalid major version") + elif (minor_version != 0x00): + raise ValueError("invalid minor version") + total_size = decode_syncsafe32(reader) + + frames = [] + + while (total_size > 0): + (frame_id, frame_size) = reader.parse("3b 24u") + + if (frame_id == chr(0) * 3): + break + elif (frame_id == 'TXX'): + frames.append( + cls.USER_TEXT_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + elif (frame_id == 'WXX'): + frames.append( + cls.USER_WEB_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + elif (frame_id == 'COM'): + frames.append( + cls.COMMENT_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + elif (frame_id == 'PIC'): + frames.append( + cls.IMAGE_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + elif (frame_id.startswith('T')): + frames.append( + cls.TEXT_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + elif (frame_id.startswith('W')): + frames.append( + cls.WEB_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + else: + frames.append( + cls.RAW_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + + total_size -= (6 + frame_size) + + return cls(frames) + + def build(self, writer): + """writes the complete ID3v22Comment data + to the given BitstreamWriter""" + + from operator import add + + writer.build("3b 8u 8u 8u", ("ID3", 0x02, 0x00, 0x00)) + encode_syncsafe32(writer, + reduce(add, [6 + frame.size() for frame in self], 0)) + + for frame in self: + writer.build("3b 24u", (frame.id, frame.size())) + frame.build(writer) + + def size(self): + """returns the total size of the ID3v22Comment, including its header""" + + from operator import add + + return reduce(add, [6 + frame.size() for frame in self], 10) + + def __len__(self): + return len(self.frames) + + def __getitem__(self, frame_id): + frames = [frame for frame in self if (frame.id == frame_id)] + if (len(frames) > 0): + return frames + else: + raise KeyError(frame_id) + + def __setitem__(self, frame_id, frames): + new_frames = frames[:] + updated_frames = [] + + for old_frame in self: + if (old_frame.id == frame_id): + try: + #replace current frame with newly set frame + updated_frames.append(new_frames.pop(0)) + except IndexError: + #no more newly set frames, so remove current frame + continue + else: + #passthrough unmatched frames + updated_frames.append(old_frame) + else: + #append any leftover frames + for new_frame in new_frames: + updated_frames.append(new_frame) + + self.__dict__["frames"] = updated_frames + + def __delitem__(self, frame_id): + updated_frames = [frame for frame in self if frame.id != frame_id] + if (len(updated_frames) < len(self)): + self.__dict__["frames"] = updated_frames + else: + raise KeyError(frame_id) + + def keys(self): + return list(set([frame.id for frame in self])) + + def values(self): + return [self[key] for key in self.keys()] + + def items(self): + return [(key, self[key]) for key in self.keys()] + + def __getattr__(self, attr): + if (attr in self.ATTRIBUTE_MAP): + try: + frame = self[self.ATTRIBUTE_MAP[attr]][0] + if (attr in ('track_number', 'album_number')): + return frame.number() + elif (attr in ('track_total', 'album_total')): + return frame.total() + else: + return unicode(frame) + except KeyError: + return None + elif (attr in self.FIELDS): + return None + else: + raise AttributeError(attr) + + def __setattr__(self, attr, value): + if (attr in self.ATTRIBUTE_MAP): + if (value is not None): + import re + + frame_id = self.ATTRIBUTE_MAP[attr] + if (attr == 'track_number'): + try: + new_frame = self.TEXT_FRAME.converted( + frame_id, + re.sub(r'\d+', + unicode(int(value)), + unicode(self[frame_id][0]), + 1)) + except KeyError: + new_frame = self.TEXT_FRAME.converted( + frame_id, + __number_pair__(value, self.track_total)) + elif (attr == 'track_total'): + try: + if (re.search(r'/\D*\d+', + unicode(self[frame_id][0])) is not None): + new_frame = self.TEXT_FRAME.converted( + frame_id, + re.sub(r'(/\D*)(\d+)', + u"\\g<1>" + unicode(int(value)), + unicode(self[frame_id][0]), + 1)) + else: + new_frame = self.TEXT_FRAME.converted( + frame_id, + u"%s/%d" % (unicode(self[frame_id][0]), + int(value))) + except KeyError: + new_frame = self.TEXT_FRAME.converted( + frame_id, + __number_pair__(self.track_number, value)) + elif (attr == 'album_number'): + try: + new_frame = self.TEXT_FRAME.converted( + frame_id, + re.sub(r'\d+', + unicode(int(value)), + unicode(self[frame_id][0]), + 1)) + except KeyError: + new_frame = self.TEXT_FRAME.converted( + frame_id, + __number_pair__(value, self.album_total)) + elif (attr == 'album_total'): + try: + if (re.search(r'/\D*\d+', + unicode(self[frame_id][0])) is not None): + new_frame = self.TEXT_FRAME.converted( + frame_id, + re.sub(r'(/\D*)(\d+)', + u"\\g<1>" + unicode(int(value)), + unicode(self[frame_id][0]), + 1)) + else: + new_frame = self.TEXT_FRAME.converted( + frame_id, + u"%s/%d" % (unicode(self[frame_id][0]), + int(value))) + except KeyError: + new_frame = self.TEXT_FRAME.converted( + frame_id, + __number_pair__(self.album_number, value)) + elif (attr == 'comment'): + new_frame = self.COMMENT_FRAME.converted( + frame_id, value) + else: + new_frame = self.TEXT_FRAME.converted( + frame_id, unicode(value)) + + try: + self[frame_id] = [new_frame] + self[frame_id][1:] + except KeyError: + self[frame_id] = [new_frame] + else: + delattr(self, attr) + elif (attr in MetaData.FIELDS): + pass + else: + self.__dict__[attr] = value + + def __delattr__(self, attr): + if (attr in self.ATTRIBUTE_MAP): + updated_frames = [] + delete_frame_id = self.ATTRIBUTE_MAP[attr] + for frame in self: + if (frame.id == delete_frame_id): + if ((attr == 'track_number') or (attr == 'album_number')): + import re + + #if *_number field contains a slashed total + if (re.search(r'\d+.*?/.*?\d+', + unicode(frame)) is not None): + #replace unslashed portion with 0 + updated_frames.append( + self.TEXT_FRAME.converted( + frame.id, + re.sub(r'\d+', + unicode(int(0)), + unicode(frame), + 1))) + else: + #otherwise, remove *_number field + continue + elif ((attr == 'track_total') or + (attr == 'album_total')): + import re + + #if *_number is nonzero + _number = re.search(r'\d+', + unicode(frame).split(u"/")[0]) + if (((_number is not None) and + (int(_number.group(0)) != 0))): + #if field contains a slashed total + #remove slashed total from field + updated_frames.append( + self.TEXT_FRAME.converted( + frame.id, + re.sub(r'\s*/\D*\d+.*', + u"", + unicode(frame), + 1))) + else: + #if field contains a slashed total + #remove field entirely + if (re.search(r'/.*?\d+', + unicode(frame)) is not None): + continue + else: + #no number or total, + #so pass frame through unchanged + updated_frames.append(frame) + else: + #handle the textual fields + #which are simply deleted outright + continue + else: + updated_frames.append(frame) + + self.__dict__["frames"] = updated_frames + + elif (attr in MetaData.FIELDS): + #ignore deleted attributes which are in MetaData + #but we don't support + pass + else: + try: + del(self.__dict__[attr]) + except KeyError: + raise AttributeError(attr) + + def images(self): + return [frame for frame in self if (frame.id == self.IMAGE_FRAME_ID)] + + def add_image(self, image): + self.frames.append( + self.IMAGE_FRAME.converted(self.IMAGE_FRAME_ID, image)) + + def delete_image(self, image): + self.__dict__["frames"] = [frame for frame in self if + ((frame.id != self.IMAGE_FRAME_ID) or + (frame != image))] + + @classmethod + def converted(cls, metadata): + """converts a MetaData object to an ID3v2*Comment object""" + + if (metadata is None): + return None + elif (cls is metadata.__class__): + return cls([frame.copy() for frame in metadata]) + + frames = [] + + for (attr, key) in cls.ATTRIBUTE_MAP.items(): + value = getattr(metadata, attr) + if ((attr not in cls.INTEGER_FIELDS) and (value is not None)): + if (attr == 'comment'): + frames.append(cls.COMMENT_FRAME.converted(key, value)) + else: + frames.append(cls.TEXT_FRAME.converted(key, value)) + + if (((metadata.track_number is not None) or + (metadata.track_total is not None))): + frames.append( + cls.TEXT_FRAME.converted( + cls.ATTRIBUTE_MAP["track_number"], + __number_pair__(metadata.track_number, + metadata.track_total))) + + if (((metadata.album_number is not None) or + (metadata.album_total is not None))): + frames.append( + cls.TEXT_FRAME.converted( + cls.ATTRIBUTE_MAP["album_number"], + __number_pair__(metadata.album_number, + metadata.album_total))) + + for image in metadata.images(): + frames.append(cls.IMAGE_FRAME.converted(cls.IMAGE_FRAME_ID, image)) + + if (hasattr(cls, 'ITUNES_COMPILATION_ID')): + frames.append( + cls.TEXT_FRAME.converted( + cls.ITUNES_COMPILATION_ID, u'1')) + + return cls(frames) + + def clean(self, fixes_performed): + """returns a new MetaData object that's been cleaned of problems""" + + return self.__class__([filtered_frame for filtered_frame in + [frame.clean(fixes_performed) for frame in self] + if filtered_frame is not None]) + + +############################################################ +# ID3v2.3 Comment +############################################################ + + +class ID3v23_Frame(ID3v22_Frame): + def __repr__(self): + return "ID3v23_Frame(%s, %s)" % (repr(self.id), repr(self.data)) + + +class ID3v23_T___Frame(ID3v22_T__Frame): + NUMERICAL_IDS = ('TRCK', 'TPOS') + + def __repr__(self): + return "ID3v23_T___Frame(%s, %s, %s)" % \ + (repr(self.id), repr(self.encoding), repr(self.data)) + + +class ID3v23_TXXX_Frame(ID3v22_TXX_Frame): + def __init__(self, encoding, description, data): + self.id = 'TXXX' + + self.encoding = encoding + self.description = description + self.data = data + + def __repr__(self): + return "ID3v23_TXXX_Frame(%s, %s, %s)" % \ + (repr(self.encoding), repr(self.description), repr(self.data)) + + +class ID3v23_W___Frame(ID3v22_W__Frame): + def __repr__(self): + return "ID3v23_W___Frame(%s, %s)" % \ + (repr(self.id), repr(self.data)) + + +class ID3v23_WXXX_Frame(ID3v22_WXX_Frame): + def __init__(self, encoding, description, data): + self.id = 'WXXX' + + self.encoding = encoding + self.description = description + self.data = data + + def __repr__(self): + return "ID3v23_WXXX_Frame(%s, %s, %s)" % \ + (repr(self.encoding), repr(self.description), repr(self.data)) + + +class ID3v23_APIC_Frame(ID3v22_PIC_Frame): + def __init__(self, mime_type, picture_type, description, data): + """fields are as follows: + | mime_type | a C_string of the image's MIME type | + | picture_type | a 1 byte field indicating front cover, etc. | + | description | a description of the image as a C_string | + | data | image data itself as a raw string | + """ + + self.id = 'APIC' + + #add APIC-specific fields + self.pic_type = picture_type + self.pic_description = description + self.pic_mime_type = mime_type + + #figure out image metrics from raw data + try: + metrics = Image.new(data, u'', 0) + except InvalidImage: + metrics = Image(data=data, mime_type=u'', + width=0, height=0, color_depth=0, color_count=0, + description=u'', type=0) + + #then initialize Image parent fields from metrics + self.width = metrics.width + self.height = metrics.height + self.color_depth = metrics.color_depth + self.color_count = metrics.color_count + self.data = data + + def copy(self): + return self.__class__(self.pic_mime_type, + self.pic_type, + self.pic_description, + self.data) + + def __repr__(self): + return "ID3v23_APIC_Frame(%s, %s, %s, ...)" % \ + (repr(self.pic_mime_type), repr(self.pic_type), + repr(self.pic_description)) + + def raw_info(self): + """returns a human-readable version of this frame as unicode""" + + return u"APIC = (%s, %d\u00D7%d, %s, \"%s\") %d bytes" % \ + (self.type_string(), + self.width, + self.height, + self.pic_mime_type, + self.pic_description, + len(self.data)) + + def __getattr__(self, attr): + if (attr == 'type'): + return {3: 0, # front cover + 4: 1, # back cover + 5: 2, # leaflet page + 6: 3 # media + }.get(self.pic_type, 4) # other + elif (attr == 'description'): + return unicode(self.pic_description) + elif (attr == 'mime_type'): + return unicode(self.pic_mime_type) + else: + raise AttributeError(attr) + + def __setattr__(self, attr, value): + if (attr == 'type'): + self.__dict__["pic_type"] = {0: 3, # front cover + 1: 4, # back cover + 2: 5, # leaflet page + 3: 6, # media + }.get(value, 0) # other + elif (attr == 'description'): + if (is_latin_1(value)): + self.__dict__["pic_description"] = C_string('latin-1', value) + else: + self.__dict__["pic_description"] = C_string('ucs2', value) + elif (attr == 'mime_type'): + self.__dict__["pic_mime_type"] = C_string('ascii', value) + else: + self.__dict__[attr] = value + + @classmethod + def parse(cls, frame_id, frame_size, reader): + """parses this frame from the given BitstreamReader""" + + encoding = reader.read(8) + mime_type = C_string.parse('ascii', reader) + picture_type = reader.read(8) + description = C_string.parse({0: 'latin-1', + 1: 'ucs2'}[encoding], reader) + data = reader.read_bytes(frame_size - (1 + + mime_type.size() + + 1 + + description.size())) + + return cls(mime_type, picture_type, description, data) + + def build(self, writer): + """writes this frame to the given BitstreamWriter + not including its frame header""" + + writer.write(8, {'latin-1': 0, + 'ucs2': 1}[self.pic_description.encoding]) + self.pic_mime_type.build(writer) + writer.write(8, self.pic_type) + self.pic_description.build(writer) + writer.write_bytes(self.data) + + def size(self): + """returns the size of this frame in bytes + not including the frame header""" + + return (1 + + self.pic_mime_type.size() + + 1 + + self.pic_description.size() + + len(self.data)) + + @classmethod + def converted(cls, frame_id, image): + if (is_latin_1(image.description)): + description = C_string('latin-1', image.description) + else: + description = C_string('ucs2', image.description) + + return cls(mime_type=C_string('ascii', image.mime_type), + picture_type={0: 3, # front cover + 1: 4, # back cover + 2: 5, # leaflet page + 3: 6, # media + }.get(image.type, 0), # other + description=description, + data=image.data) + + def clean(self, fixes_performed): + """returns a cleaned ID3v23_APIC_Frame, + or None if the frame should be removed entirely + any fixes are appended to fixes_applied as unicode string""" + + actual_mime_type = Image.new(self.data, u"", 0).mime_type + if (unicode(self.pic_mime_type) != actual_mime_type): + from audiotools.text import (CLEAN_FIX_IMAGE_FIELDS) + fixes_performed.append(CLEAN_FIX_IMAGE_FIELDS) + return ID3v23_APIC_Frame( + C_string('ascii', actual_mime_type.encode('ascii')), + self.pic_type, + self.pic_description, + self.data) + else: + return ID3v23_APIC_Frame( + self.pic_mime_type, + self.pic_type, + self.pic_description, + self.data) + + +class ID3v23_COMM_Frame(ID3v22_COM_Frame): + def __init__(self, encoding, language, short_description, data): + """fields are as follows: + | encoding | 1 byte int of the comment's text encoding | + | language | 3 byte string of the comment's language | + | short_description | C_string of a short description | + | data | plain string of the comment data itself | + """ + + self.id = "COMM" + self.encoding = encoding + self.language = language + self.short_description = short_description + self.data = data + + def __repr__(self): + return "ID3v23_COMM_Frame(%s, %s, %s, %s)" % \ + (repr(self.encoding), repr(self.language), + repr(self.short_description), repr(self.data)) + + def raw_info(self): + """returns a human-readable version of this frame as unicode""" + + return u"COMM = (%s, %s, \"%s\") %s" % \ + ({0: u'Latin-1', 1: 'UCS-2'}[self.encoding], + self.language.decode('ascii', 'replace'), + self.short_description, + self.data.decode({0: 'latin-1', 1: 'ucs2'}[self.encoding])) + + +class ID3v23Comment(ID3v22Comment): + NAME = u'ID3v2.3' + + ATTRIBUTE_MAP = {'track_name': 'TIT2', + 'track_number': 'TRCK', + 'track_total': 'TRCK', + 'album_name': 'TALB', + 'artist_name': 'TPE1', + 'performer_name': 'TPE2', + 'composer_name': 'TCOM', + 'conductor_name': 'TPE3', + 'media': 'TMED', + 'ISRC': 'TSRC', + 'copyright': 'TCOP', + 'publisher': 'TPUB', + 'year': 'TYER', + 'date': 'TRDA', + 'album_number': 'TPOS', + 'album_total': 'TPOS', + 'comment': 'COMM'} + + RAW_FRAME = ID3v23_Frame + TEXT_FRAME = ID3v23_T___Frame + WEB_FRAME = ID3v23_W___Frame + USER_TEXT_FRAME = ID3v23_TXXX_Frame + USER_WEB_FRAME = ID3v23_WXXX_Frame + COMMENT_FRAME = ID3v23_COMM_Frame + IMAGE_FRAME = ID3v23_APIC_Frame + IMAGE_FRAME_ID = 'APIC' + ITUNES_COMPILATION_ID = 'TCMP' + + def __repr__(self): + return "ID3v23Comment(%s)" % (repr(self.frames)) + + @classmethod + def parse(cls, reader): + """given a BitstreamReader, returns a parsed ID3v23Comment""" + + (id3, + major_version, + minor_version, + flags) = reader.parse("3b 8u 8u 8u") + if (id3 != 'ID3'): + raise ValueError("invalid ID3 header") + elif (major_version != 0x03): + raise ValueError("invalid major version") + elif (minor_version != 0x00): + raise ValueError("invalid minor version") + total_size = decode_syncsafe32(reader) + + frames = [] + + while (total_size > 0): + (frame_id, frame_size, frame_flags) = reader.parse("4b 32u 16u") + + if (frame_id == chr(0) * 4): + break + elif (frame_id == 'TXXX'): + frames.append( + cls.USER_TEXT_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + elif (frame_id == 'WXXX'): + frames.append( + cls.USER_WEB_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + elif (frame_id == 'COMM'): + frames.append( + cls.COMMENT_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + elif (frame_id == 'APIC'): + frames.append( + cls.IMAGE_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + elif (frame_id.startswith('T')): + frames.append( + cls.TEXT_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + elif (frame_id.startswith('W')): + frames.append( + cls.WEB_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + else: + frames.append( + cls.RAW_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + + total_size -= (10 + frame_size) + + return cls(frames) + + def build(self, writer): + """writes the complete ID3v23Comment data + to the given BitstreamWriter""" + + from operator import add + + writer.build("3b 8u 8u 8u", ("ID3", 0x03, 0x00, 0x00)) + encode_syncsafe32(writer, + reduce(add, + [10 + frame.size() for frame in self], 0)) + + for frame in self: + writer.build("4b 32u 16u", (frame.id, frame.size(), 0)) + frame.build(writer) + + def size(self): + """returns the total size of the ID3v23Comment, including its header""" + + from operator import add + + return reduce(add, [10 + frame.size() for frame in self], 10) + + +############################################################ +# ID3v2.4 Comment +############################################################ + + +class ID3v24_Frame(ID3v23_Frame): + def __repr__(self): + return "ID3v24_Frame(%s, %s)" % (repr(self.id), repr(self.data)) + + +class ID3v24_T___Frame(ID3v23_T___Frame): + def __init__(self, frame_id, encoding, data): + assert((encoding == 0) or (encoding == 1) or + (encoding == 2) or (encoding == 3)) + + self.id = frame_id + self.encoding = encoding + self.data = data + + def __repr__(self): + return "ID3v24_T___Frame(%s, %s, %s)" % \ + (repr(self.id), repr(self.encoding), repr(self.data)) + + def __unicode__(self): + return self.data.decode( + {0: u"latin-1", + 1: u"utf-16", + 2: u"utf-16BE", + 3: u"utf-8"}[self.encoding], 'replace').split(unichr(0), 1)[0] + + def raw_info(self): + """returns a human-readable version of this frame as unicode""" + + return u"%s = (%s) %s" % (self.id.decode('ascii'), + {0: u"Latin-1", + 1: u"UTF-16", + 2: u"UTF-16BE", + 3: u"UTF-8"}[self.encoding], + unicode(self)) + + @classmethod + def converted(cls, frame_id, unicode_string): + """given a unicode string, returns a text frame""" + + if (is_latin_1(unicode_string)): + return cls(frame_id, 0, unicode_string.encode('latin-1')) + else: + return cls(frame_id, 3, unicode_string.encode('utf-8')) + + +class ID3v24_TXXX_Frame(ID3v23_TXXX_Frame): + def __repr__(self): + return "ID3v24_TXXX_Frame(%s, %s, %s)" % \ + (repr(self.encoding), repr(self.description), repr(self.data)) + + def raw_info(self): + """returns a human-readable version of this frame as unicode""" + + return u"%s = (%s, \"%s\") %s" % \ + (self.id, + {0: u"Latin-1", + 1: u"UTF-16", + 2: u"UTF-16BE", + 3: u"UTF-8"}[self.encoding], + self.description, + unicode(self)) + + def __unicode__(self): + return self.data.decode( + {0: u"latin-1", + 1: u"utf-16", + 2: u"utf-16BE", + 3: u"utf-8"}[self.encoding], 'replace').split(unichr(0), 1)[0] + + @classmethod + def parse(cls, frame_id, frame_size, reader): + """given a frame_id string, frame_size int and BitstreamReader + of the remaining frame data, returns a parsed text frame""" + + encoding = reader.read(8) + description = C_string.parse({0: "latin-1", + 1: "utf-16", + 2: "utf-16be", + 3: "utf-8"}[encoding], + reader) + data = reader.read_bytes(frame_size - 1 - description.size()) + + return cls(encoding, description, data) + + +class ID3v24_APIC_Frame(ID3v23_APIC_Frame): + def __repr__(self): + return "ID3v24_APIC_Frame(%s, %s, %s, ...)" % \ + (repr(self.pic_mime_type), repr(self.pic_type), + repr(self.pic_description)) + + def __setattr__(self, attr, value): + if (attr == 'type'): + self.__dict__["pic_type"] = {0: 3, # front cover + 1: 4, # back cover + 2: 5, # leaflet page + 3: 6, # media + }.get(value, 0) # other + elif (attr == 'description'): + if (is_latin_1(value)): + self.__dict__["pic_description"] = C_string('latin-1', value) + else: + self.__dict__["pic_description"] = C_string('utf-8', value) + elif (attr == 'mime_type'): + self.__dict__["pic_mime_type"] = C_string('ascii', value) + else: + self.__dict__[attr] = value + + @classmethod + def parse(cls, frame_id, frame_size, reader): + """parses this frame from the given BitstreamReader""" + + encoding = reader.read(8) + mime_type = C_string.parse('ascii', reader) + picture_type = reader.read(8) + description = C_string.parse({0: 'latin-1', + 1: 'utf-16', + 2: 'utf-16be', + 3: 'utf-8'}[encoding], reader) + data = reader.read_bytes(frame_size - (1 + + mime_type.size() + + 1 + + description.size())) + + return cls(mime_type, picture_type, description, data) + + def build(self, writer): + """writes this frame to the given BitstreamWriter + not including its frame header""" + + writer.write(8, {'latin-1': 0, + 'utf-16': 1, + 'utf-16be': 2, + 'utf-8': 3}[self.pic_description.encoding]) + self.pic_mime_type.build(writer) + writer.write(8, self.pic_type) + self.pic_description.build(writer) + writer.write_bytes(self.data) + + @classmethod + def converted(cls, frame_id, image): + if (is_latin_1(image.description)): + description = C_string('latin-1', image.description) + else: + description = C_string('utf-8', image.description) + + return cls(mime_type=C_string('ascii', image.mime_type), + picture_type={0: 3, # front cover + 1: 4, # back cover + 2: 5, # leaflet page + 3: 6, # media + }.get(image.type, 0), # other + description=description, + data=image.data) + + def clean(self, fixes_performed): + """returns a cleaned ID3v24_APIC_Frame, + or None if the frame should be removed entirely + any fixes are appended to fixes_applied as unicode string""" + + actual_mime_type = Image.new(self.data, u"", 0).mime_type + if (unicode(self.pic_mime_type) != actual_mime_type): + from audiotools.text import (CLEAN_FIX_IMAGE_FIELDS) + fixes_performed.append(CLEAN_FIX_IMAGE_FIELDS) + return ID3v24_APIC_Frame( + C_string('ascii', + actual_mime_type.encode('ascii')), + self.pic_type, + self.pic_description, + self.data) + else: + return ID3v24_APIC_Frame( + self.pic_mime_type, + self.pic_type, + self.pic_description, + self.data) + + +class ID3v24_W___Frame(ID3v23_W___Frame): + def __repr__(self): + return "ID3v24_W___Frame(%s, %s)" % \ + (repr(self.id), repr(self.data)) + + +class ID3v24_WXXX_Frame(ID3v23_WXXX_Frame): + def __repr__(self): + return "ID3v24_WXXX_Frame(%s, %s, %s)" % \ + (repr(self.encoding), repr(self.description), repr(self.data)) + + def raw_info(self): + """returns a human-readable version of this frame as unicode""" + + return u"%s = (%s, \"%s\") %s" % \ + (self.id, + {0: u'Latin-1', + 1: u'UTF-16', + 2: u'UTF-16BE', + 3: u'UTF-8'}[self.encoding], + self.description, + self.data.decode('ascii', 'replace')) + + @classmethod + def parse(cls, frame_id, frame_size, reader): + """given a frame_id string, frame_size int and BitstreamReader + of the remaining frame data, returns a parsed text frame""" + + encoding = reader.read(8) + description = C_string.parse({0: 'latin-1', + 1: 'utf-16', + 2: 'utf-16be', + 3: 'utf-8'}[encoding], + reader) + data = reader.read_bytes(frame_size - 1 - description.size()) + + return cls(encoding, description, data) + + +class ID3v24_COMM_Frame(ID3v23_COMM_Frame): + def __repr__(self): + return "ID3v24_COMM_Frame(%s, %s, %s, %s)" % \ + (repr(self.encoding), repr(self.language), + repr(self.short_description), repr(self.data)) + + def __unicode__(self): + return self.data.decode({0: 'latin-1', + 1: 'utf-16', + 2: 'utf-16be', + 3: 'utf-8'}[self.encoding], 'replace') + + def raw_info(self): + """returns a human-readable version of this frame as unicode""" + + return u"COMM = (%s, %s, \"%s\") %s" % \ + ({0: u'Latin-1', + 1: u'UTF-16', + 2: u'UTF-16BE', + 3: u'UTF-8'}[self.encoding], + self.language.decode('ascii', 'replace'), + self.short_description, + self.data.decode({0: 'latin-1', + 1: 'utf-16', + 2: 'utf-16be', + 3: 'utf-8'}[self.encoding])) + + @classmethod + def parse(cls, frame_id, frame_size, reader): + """given a frame_id string, frame_size int and BitstreamReader + of the remaining frame data, returns a parsed ID3v22_COM_Frame""" + + (encoding, language) = reader.parse("8u 3b") + short_description = C_string.parse({0: 'latin-1', + 1: 'utf-16', + 2: 'utf-16be', + 3: 'utf-8'}[encoding], + reader) + data = reader.read_bytes(frame_size - (4 + short_description.size())) + + return cls(encoding, language, short_description, data) + + @classmethod + def converted(cls, frame_id, unicode_string): + if (is_latin_1(unicode_string)): + return cls(0, "eng", C_string("latin-1", u""), + unicode_string.encode('latin-1')) + else: + return cls(3, "eng", C_string("utf-8", u""), + unicode_string.encode('utf-8')) + + def clean(self, fixes_performed): + """returns a cleaned frame of the same class + or None if the frame should be omitted + fix text will be appended to fixes_performed, if necessary""" + + from .text import (CLEAN_REMOVE_EMPTY_TAG, + CLEAN_REMOVE_TRAILING_WHITESPACE, + CLEAN_REMOVE_LEADING_WHITESPACE) + + field = self.id.decode('ascii') + text_encoding = {0: 'latin-1', + 1: 'utf-16', + 2: 'utf-16be', + 3: 'utf-8'} + + value = self.data.decode(text_encoding[self.encoding], 'replace') + + #check for an empty tag + if (len(value.strip()) == 0): + fixes_performed.append(CLEAN_REMOVE_EMPTY_TAG % + {"field": field}) + return None + + #check trailing whitespace + fix1 = value.rstrip() + if (fix1 != value): + fixes_performed.append(CLEAN_REMOVE_TRAILING_WHITESPACE % + {"field": field}) + + #check leading whitespace + fix2 = fix1.lstrip() + if (fix2 != fix1): + fixes_performed.append(CLEAN_REMOVE_LEADING_WHITESPACE % + {"field": field}) + + #stripping whitespace shouldn't alter text/description encoding + + return self.__class__(self.encoding, + self.language, + self.short_description, + fix2.encode(text_encoding[self.encoding])) + + +class ID3v24Comment(ID3v23Comment): + NAME = u'ID3v2.4' + + RAW_FRAME = ID3v24_Frame + TEXT_FRAME = ID3v24_T___Frame + USER_TEXT_FRAME = ID3v24_TXXX_Frame + WEB_FRAME = ID3v24_W___Frame + USER_WEB_FRAME = ID3v24_WXXX_Frame + COMMENT_FRAME = ID3v24_COMM_Frame + IMAGE_FRAME = ID3v24_APIC_Frame + IMAGE_FRAME_ID = 'APIC' + ITUNES_COMPILATION_ID = 'TCMP' + + def __repr__(self): + return "ID3v24Comment(%s)" % (repr(self.frames)) + + @classmethod + def parse(cls, reader): + """given a BitstreamReader, returns a parsed ID3v24Comment""" + + (id3, + major_version, + minor_version, + flags) = reader.parse("3b 8u 8u 8u") + if (id3 != 'ID3'): + raise ValueError("invalid ID3 header") + elif (major_version != 0x04): + raise ValueError("invalid major version") + elif (minor_version != 0x00): + raise ValueError("invalid minor version") + total_size = decode_syncsafe32(reader) + + frames = [] + + while (total_size > 0): + frame_id = reader.read_bytes(4) + frame_size = decode_syncsafe32(reader) + flags = reader.read(16) + + if (frame_id == chr(0) * 4): + break + elif (frame_id == 'TXXX'): + frames.append( + cls.USER_TEXT_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + elif (frame_id == 'WXXX'): + frames.append( + cls.USER_WEB_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + elif (frame_id == 'COMM'): + frames.append( + cls.COMMENT_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + elif (frame_id == 'APIC'): + frames.append( + cls.IMAGE_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + elif (frame_id.startswith('T')): + frames.append( + cls.TEXT_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + elif (frame_id.startswith('W')): + frames.append( + cls.WEB_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + else: + frames.append( + cls.RAW_FRAME.parse( + frame_id, frame_size, reader.substream(frame_size))) + + total_size -= (10 + frame_size) + + return cls(frames) + + def build(self, writer): + """writes the complete ID3v24Comment data + to the given BitstreamWriter""" + + from operator import add + + writer.build("3b 8u 8u 8u", ("ID3", 0x04, 0x00, 0x00)) + encode_syncsafe32(writer, + reduce(add, [10 + frame.size() for frame in self], + 0)) + + for frame in self: + writer.write_bytes(frame.id) + encode_syncsafe32(writer, frame.size()) + writer.write(16, 0) + frame.build(writer) + + def size(self): + """returns the total size of the ID3v24Comment, including its header""" + + from operator import add + + return reduce(add, [10 + frame.size() for frame in self], 10) + + +ID3v2Comment = ID3v22Comment + + +class ID3CommentPair(MetaData): + """a pair of ID3v2/ID3v1 comments + + these can be manipulated as a set""" + + def __init__(self, id3v2_comment, id3v1_comment): + """id3v2 and id3v1 are ID3v2Comment and ID3v1Comment objects or None + + values in ID3v2 take precendence over ID3v1, if present""" + + self.__dict__['id3v2'] = id3v2_comment + self.__dict__['id3v1'] = id3v1_comment + + if (self.id3v2 is not None): + base_comment = self.id3v2 + elif (self.id3v1 is not None): + base_comment = self.id3v1 + else: + raise ValueError("ID3v2 and ID3v1 cannot both be blank") + + def __getattr__(self, key): + if (key in self.FIELDS): + if (((self.id3v2 is not None) and + (getattr(self.id3v2, key) is not None))): + return getattr(self.id3v2, key) + elif (self.id3v1 is not None): + return getattr(self.id3v1, key) + else: + raise ValueError("ID3v2 and ID3v1 cannot both be blank") + else: + raise AttributeError(key) + + def __setattr__(self, key, value): + self.__dict__[key] = value + + if (self.id3v2 is not None): + setattr(self.id3v2, key, value) + if (self.id3v1 is not None): + setattr(self.id3v1, key, value) + + def __delattr__(self, key): + if (self.id3v2 is not None): + delattr(self.id3v2, key) + if (self.id3v1 is not None): + delattr(self.id3v1, key) + + @classmethod + def converted(cls, metadata, + id3v2_class=ID3v23Comment, + id3v1_class=ID3v1Comment): + """takes a MetaData object and returns an ID3CommentPair object""" + + if (metadata is None): + return None + elif (isinstance(metadata, ID3CommentPair)): + return ID3CommentPair( + metadata.id3v2.__class__.converted(metadata.id3v2), + metadata.id3v1.__class__.converted(metadata.id3v1)) + elif (isinstance(metadata, ID3v2Comment)): + return ID3CommentPair(metadata, + id3v1_class.converted(metadata)) + else: + return ID3CommentPair( + id3v2_class.converted(metadata), + id3v1_class.converted(metadata)) + + def raw_info(self): + """returns a human-readable version of this metadata pair + as a unicode string""" + + if ((self.id3v2 is not None) and (self.id3v1 is not None)): + #both comments present + from os import linesep + + return (self.id3v2.raw_info() + + linesep.decode('ascii') * 2 + + self.id3v1.raw_info()) + elif (self.id3v2 is not None): + #only ID3v2 + return self.id3v2.raw_info() + elif (self.id3v1 is not None): + #only ID3v1 + return self.id3v1.raw_info() + else: + return u'' + + #ImageMetaData passthroughs + def images(self): + """returns a list of embedded Image objects""" + + if (self.id3v2 is not None): + return self.id3v2.images() + else: + return [] + + def add_image(self, image): + """embeds an Image object in this metadata""" + + if (self.id3v2 is not None): + self.id3v2.add_image(image) + + def delete_image(self, image): + """deletes an Image object from this metadata""" + + if (self.id3v2 is not None): + self.id3v2.delete_image(image) + + @classmethod + def supports_images(cls): + """returns True""" + + return True + + def clean(self, fixes_performed): + if (self.id3v2 is not None): + new_id3v2 = self.id3v2.clean(fixes_performed) + else: + new_id3v2 = None + + if (self.id3v1 is not None): + new_id3v1 = self.id3v1.clean(fixes_performed) + else: + new_id3v1 = None + + return ID3CommentPair(new_id3v2, new_id3v1)
View file
audiotools-2.19.tar.gz/audiotools/id3v1.py
Added
@@ -0,0 +1,293 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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 MetaData + + +class ID3v1Comment(MetaData): + """a complete ID3v1.1 tag""" + + #All ID3v1 tags are treated as ID3v1.1 + #because plain ID3v1 tags don't support track number + #which means it'll be impossible to "promote" a tag later on. + + ID3v1_FIELDS = {"track_name": "__track_name__", + "artist_name": "__artist_name__", + "album_name": "__album_name__", + "year": "__year__", + "comment": "__comment__", + "track_number": "__track_number__"} + + FIELD_LENGTHS = {"track_name": 30, + "artist_name": 30, + "album_name": 30, + "year": 4, + "comment": 28} + + def __init__(self, track_name=chr(0) * 30, + artist_name=chr(0) * 30, + album_name=chr(0) * 30, + year=chr(0) * 4, + comment=chr(0) * 28, + track_number=chr(0), + genre=chr(0)): + """fields are as follows: + + | field | length | + |--------------+--------| + | track_name | 30 | + | artist_name | 30 | + | album_name | 30 | + | year | 4 | + | comment | 28 | + | track_number | 1 | + | genre | 1 | + |--------------+--------| + + all are binary strings of the given length + and must not be any shorter or longer + """ + + if (len(track_name) != 30): + raise ValueError("track_name must be exactly 30 bytes") + if (len(artist_name) != 30): + raise ValueError("artist_name must be exactly 30 bytes") + if (len(album_name) != 30): + raise ValueError("album_name must be exactly 30 bytes") + if (len(year) != 4): + raise ValueError("year must be exactly 30 bytes") + if (len(comment) != 28): + raise ValueError("comment must be exactly 28 bytes") + if (len(track_number) != 1): + raise ValueError("track_number must be exactly 1 byte") + if (len(genre) != 1): + raise ValueError("genre must be exactly 1 byte") + + self.__dict__['__track_name__'] = track_name + self.__dict__['__artist_name__'] = artist_name + self.__dict__['__album_name__'] = album_name + self.__dict__['__year__'] = year + self.__dict__['__comment__'] = comment + self.__dict__['__track_number__'] = track_number + self.__dict__['__genre__'] = genre + + def __repr__(self): + return "ID3v1Comment(%s, %s, %s, %s, %s, %s, %s)" % \ + (repr(self.__dict__['__track_name__']), + repr(self.__dict__['__artist_name__']), + repr(self.__dict__['__album_name__']), + repr(self.__dict__['__year__']), + repr(self.__dict__['__comment__']), + repr(self.__dict__['__track_number__']), + repr(self.__dict__['__genre__'])) + + def __getattr__(self, attr): + if (attr == "track_number"): + number = ord(self.__dict__['__track_number__']) + if (number > 0): + return number + else: + return None + elif (attr in self.ID3v1_FIELDS): + value = self.__dict__[self.ID3v1_FIELDS[attr]].rstrip( + chr(0)).decode('ascii', 'replace') + if (len(value) > 0): + return value + else: + return None + elif (attr in self.FIELDS): + return None + else: + raise AttributeError(attr) + + def __setattr__(self, attr, value): + if (attr == "track_number"): + if (value is None): + self.__dict__['__track_number__'] = chr(0) + else: + self.__dict__['__track_number__'] = chr(min(int(value), 0xFF)) + elif (attr in self.FIELD_LENGTHS): + if (value is None): + delattr(self, attr) + else: + #all are text fields + encoded = value.encode('ascii', 'replace') + if (len(encoded) < self.FIELD_LENGTHS[attr]): + self.__dict__[self.ID3v1_FIELDS[attr]] = \ + encoded + chr(0) * (self.FIELD_LENGTHS[attr] - + len(encoded)) + elif (len(encoded) > self.FIELD_LENGTHS[attr]): + self.__dict__[self.ID3v1_FIELDS[attr]] = \ + encoded[0:self.FIELD_LENGTHS[attr]] + else: + self.__dict__[self.ID3v1_FIELDS[attr]] = encoded + elif (attr in self.FIELDS): + #field not supported by ID3v1Comment, so ignore it + pass + else: + self.__dict__[attr] = value + + def __delattr__(self, attr): + if (attr == "track_number"): + self.__dict__['__track_number__'] = chr(0) + elif (attr in self.FIELD_LENGTHS): + self.__dict__[self.ID3v1_FIELDS[attr]] = \ + chr(0) * self.FIELD_LENGTHS[attr] + elif (attr in self.FIELDS): + #field not supported by ID3v1Comment, so ignore it + pass + else: + del(self.__dict__[attr]) + + def raw_info(self): + """returns a human-readable version of this metadata + as a unicode string""" + + from os import linesep + + return linesep.decode('ascii').join( + [u"ID3v1.1:"] + + [u"%s = %s" % (label, getattr(self, attr)) + for (label, attr) in [(u" track name", "track_name"), + (u" artist name", "artist_name"), + (u" album name", "album_name"), + (u" year", "year"), + (u" comment", "comment"), + (u"track number", "track_number")] + if (getattr(self, attr) is not None)] + + [u" genre = %d" % (ord(self.__genre__))]) + + @classmethod + def parse(cls, mp3_file): + """given an MP3 file, returns an ID3v1Comment + + raises ValueError if the comment is invalid""" + + from .bitstream import BitstreamReader + + mp3_file.seek(-128, 2) + reader = BitstreamReader(mp3_file, 0) + (tag, + track_name, + artist_name, + album_name, + year, + comment, + track_number, + genre) = reader.parse("3b 30b 30b 30b 4b 28b 8p 1b 1b") + if (tag != 'TAG'): + raise ValueError(u"invalid ID3v1 tag") + + return ID3v1Comment(track_name=track_name, + artist_name=artist_name, + album_name=album_name, + year=year, + comment=comment, + track_number=track_number, + genre=genre) + + def build(self, mp3_file): + """given an MP3 file positioned at the file's end, generate a tag""" + + from .bitstream import BitstreamWriter + + BitstreamWriter(mp3_file, 0).build( + "3b 30b 30b 30b 4b 28b 8p 1b 1b", + ("TAG", + self.__dict__['__track_name__'], + self.__dict__['__artist_name__'], + self.__dict__['__album_name__'], + self.__dict__['__year__'], + self.__dict__['__comment__'], + self.__dict__['__track_number__'], + self.__dict__['__genre__'])) + + @classmethod + def supports_images(cls): + """returns False""" + + return False + + @classmethod + def converted(cls, metadata): + """converts a MetaData object to an ID3v1Comment object""" + + if (metadata is None): + return None + elif (isinstance(metadata, ID3v1Comment)): + #duplicate all fields as-is + return ID3v1Comment(track_name=metadata.__track_name__, + artist_name=metadata.__artist_name__, + album_name=metadata.__album_name__, + year=metadata.__year__, + comment=metadata.__comment__, + track_number=metadata.__track_number__, + genre=metadata.__genre__) + else: + #convert fields using setattr + id3v1 = ID3v1Comment() + for attr in ["track_name", + "artist_name", + "album_name", + "year", + "comment", + "track_number"]: + setattr(id3v1, attr, getattr(metadata, attr)) + return id3v1 + + def images(self): + """returns an empty list of Image objects""" + + return [] + + def clean(self, fixes_performed): + """returns a new ID3v1Comment object that's been cleaned of problems""" + + from .text import (CLEAN_REMOVE_TRAILING_WHITESPACE, + CLEAN_REMOVE_LEADING_WHITESPACE) + + fields = {} + for (init_attr, + attr, + name) in [("track_name", "__track_name__", u"title"), + ("artist_name", "__artist_name__", u"artist"), + ("album_name", "__album_name__", u"album"), + ("year", "__year__", u"year"), + ("comment", "__comment__", u"comment")]: + #strip out trailing NULL bytes + initial_value = getattr(self, attr).rstrip(chr(0)) + + fix1 = initial_value.rstrip() + if (fix1 != initial_value): + fixes_performed.append(CLEAN_REMOVE_TRAILING_WHITESPACE % + {"field": name}) + fix2 = fix1.lstrip() + if (fix2 != fix1): + fixes_performed.append(CLEAN_REMOVE_LEADING_WHITESPACE % + {"field": name}) + + #restore trailing NULL bytes + fields[init_attr] = (fix2 + chr(0) * + (self.FIELD_LENGTHS[init_attr] - len(fix2))) + + #copy non-text fields as-is + fields["track_number"] = self.__track_number__ + fields["genre"] = self.__genre__ + + return ID3v1Comment(**fields)
View file
audiotools-2.19.tar.gz/audiotools/image.py
Added
@@ -0,0 +1,414 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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 + +import imghdr +from .bitstream import BitstreamReader, format_size +from . import InvalidImage + + +def __jpeg__(h, f): + if (h[0:3] == "FFD8FF".decode('hex')): + return 'jpeg' + else: + return None + + +imghdr.tests.append(__jpeg__) + + +def image_metrics(file_data): + """returns an ImageMetrics subclass from a string of file data + + raises InvalidImage if there is an error parsing the file + or its type is unknown""" + + import cStringIO + + header = imghdr.what(None, file_data) + + file = cStringIO.StringIO(file_data) + try: + if (header == 'jpeg'): + return __JPEG__.parse(file) + elif (header == 'png'): + return __PNG__.parse(file) + elif (header == 'gif'): + return __GIF__.parse(file) + elif (header == 'bmp'): + return __BMP__.parse(file) + elif (header == 'tiff'): + return __TIFF__.parse(file) + else: + from .text import ERR_IMAGE_UNKNOWN_TYPE + raise InvalidImage(ERR_IMAGE_UNKNOWN_TYPE) + finally: + file.close() + + +####################### +#JPEG +####################### + + +class ImageMetrics: + """a container for image data""" + + def __init__(self, width, height, bits_per_pixel, color_count, mime_type): + """fields are as follows: + + width - image width as an integer number of pixels + height - image height as an integer number of pixels + bits_per_pixel - the number of bits per pixel as an integer + color_count - for palette-based images, the total number of colors + mime_type - the image's MIME type, as a string + + all of the ImageMetrics subclasses implement these fields + in addition, they all implement a parse() classmethod + used to parse binary string data and return something + imageMetrics compatible + """ + + self.width = width + self.height = height + self.bits_per_pixel = bits_per_pixel + self.color_count = color_count + self.mime_type = mime_type + + def __repr__(self): + return "ImageMetrics(%s,%s,%s,%s,%s)" % \ + (repr(self.width), + repr(self.height), + repr(self.bits_per_pixel), + repr(self.color_count), + repr(self.mime_type)) + + +class InvalidJPEG(InvalidImage): + """raised if a JPEG cannot be parsed correctly""" + + pass + + +class __JPEG__(ImageMetrics): + def __init__(self, width, height, bits_per_pixel): + ImageMetrics.__init__(self, width, height, bits_per_pixel, + 0, u'image/jpeg') + + @classmethod + def parse(cls, file): + def segments(reader): + if (reader.read(8) != 0xFF): + from .text import ERR_IMAGE_INVALID_JPEG_MARKER + raise InvalidJPEG(ERR_IMAGE_INVALID_JPEG_MARKER) + segment_type = reader.read(8) + + while (segment_type != 0xDA): + if (segment_type not in (0xD8, 0xD9)): + yield (segment_type, reader.substream(reader.read(16) - 2)) + else: + yield (segment_type, None) + + if (reader.read(8) != 0xFF): + from .text import ERR_IMAGE_INVALID_JPEG_MARKER + raise InvalidJPEG(ERR_IMAGE_INVALID_JPEG_MARKER) + segment_type = reader.read(8) + + try: + for (segment_type, + segment_data) in segments(BitstreamReader(file, 0)): + if (segment_type in (0xC0, 0xC1, 0xC2, 0xC3, + 0xC5, 0XC5, 0xC6, 0xC7, + 0xC9, 0xCA, 0xCB, 0xCD, + 0xCE, 0xCF)): # start of frame + (data_precision, + image_height, + image_width, + components) = segment_data.parse("8u 16u 16u 8u") + return __JPEG__(width=image_width, + height=image_height, + bits_per_pixel=data_precision * components) + except IOError: + from .text import ERR_IMAGE_IOERROR_JPEG + raise InvalidJPEG(ERR_IMAGE_IOERROR_JPEG) + +####################### +#PNG +####################### + + +class InvalidPNG(InvalidImage): + """raised if a PNG cannot be parsed correctly""" + + pass + + +class __PNG__(ImageMetrics): + def __init__(self, width, height, bits_per_pixel, color_count): + ImageMetrics.__init__(self, width, height, bits_per_pixel, color_count, + u'image/png') + + @classmethod + def parse(cls, file): + def chunks(reader): + if (reader.read_bytes(8) != '\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'): + from .text import ERR_IMAGE_INVALID_PNG + raise InvalidPNG(ERR_IMAGE_INVALID_PNG) + (chunk_length, chunk_type) = reader.parse("32u 4b") + while (chunk_type != 'IEND'): + yield (chunk_type, + chunk_length, + reader.substream(chunk_length)) + chunk_crc = reader.read(32) + (chunk_length, chunk_type) = reader.parse("32u 4b") + + ihdr = None + plte_length = 0 + + try: + for (chunk_type, + chunk_length, + chunk_data) in chunks(BitstreamReader(file, 0)): + if (chunk_type == 'IHDR'): + ihdr = chunk_data + elif (chunk_type == 'PLTE'): + plte_length = chunk_length + + if (ihdr is None): + from .text import ERR_IMAGE_INVALID_PNG + raise InvalidPNG(ERR_IMAGE_INVALID_PNG) + + (width, + height, + bit_depth, + color_type, + compression_method, + filter_method, + interlace_method) = ihdr.parse("32u 32u 8u 8u 8u 8u 8u") + except IOError: + from .text import ERR_IMAGE_IOERROR_PNG + raise InvalidPNG(ERR_IMAGE_IOERROR_PNG) + + if (color_type == 0): # grayscale + return cls(width=width, + height=height, + bits_per_pixel=bit_depth, + color_count=0) + elif (color_type == 2): # RGB + return cls(width=width, + height=height, + bits_per_pixel=bit_depth * 3, + color_count=0) + elif (color_type == 3): # palette + if ((plte_length % 3) != 0): + from .text import ERR_IMAGE_INVALID_PLTE + raise InvalidPNG(ERR_IMAGE_INVALID_PLTE) + else: + return cls(width=width, + height=height, + bits_per_pixel=8, + color_count=plte_length / 3) + elif (color_type == 4): # grayscale + alpha + return cls(width=width, + height=height, + bits_per_pixel=bit_depth * 2, + color_count=0) + elif (color_type == 6): # RGB + alpha + return cls(width=width, + height=height, + bits_per_pixel=bit_depth * 4, + color_count=0) + +####################### +#BMP +####################### + + +class InvalidBMP(InvalidImage): + """raised if a BMP cannot be parsed correctly""" + + pass + + +class __BMP__(ImageMetrics): + def __init__(self, width, height, bits_per_pixel, color_count): + ImageMetrics.__init__(self, width, height, bits_per_pixel, color_count, + u'image/x-ms-bmp') + + @classmethod + def parse(cls, file): + try: + (magic_number, + file_size, + data_offset, + header_size, + width, + height, + color_planes, + bits_per_pixel, + compression_method, + image_size, + horizontal_resolution, + vertical_resolution, + colors_used, + important_colors_used) = BitstreamReader(file, 1).parse( + "2b 32u 16p 16p 32u " + + "32u 32u 32u 16u 16u 32u 32u 32u 32u 32u 32u") + except IOError: + from .text import ERR_IMAGE_IOERROR_BMP + raise InvalidBMP(ERR_IMAGE_IOERROR_BMP) + + if (magic_number != 'BM'): + from .text import ERR_IMAGE_INVALID_BMP + raise InvalidBMP(ERR_IMAGE_INVALID_BMP) + else: + return cls(width=width, + height=height, + bits_per_pixel=bits_per_pixel, + color_count=colors_used) + + +####################### +#GIF +####################### + + +class InvalidGIF(InvalidImage): + """raised if a GIF cannot be parsed correctly""" + + pass + + +class __GIF__(ImageMetrics): + def __init__(self, width, height, color_count): + ImageMetrics.__init__(self, width, height, 8, color_count, + u'image/gif') + + @classmethod + def parse(cls, file): + try: + (gif, + version, + width, + height, + color_table_size) = BitstreamReader(file, 1).parse( + "3b 3b 16u 16u 3u 5p") + except IOError: + from .text import ERR_IMAGE_IOERROR_GIF + raise InvalidGIF(ERR_IMAGE_IOERROR_GIF) + + if (gif != 'GIF'): + from .text import ERR_IMAGE_INVALID_GIF + raise InvalidGIF(ERR_IMAGE_INVALID_GIF) + else: + return cls(width=width, + height=height, + color_count=2 ** (color_table_size + 1)) + + +####################### +#TIFF +####################### + + +class InvalidTIFF(InvalidImage): + """raised if a TIFF cannot be parsed correctly""" + + pass + + +class __TIFF__(ImageMetrics): + def __init__(self, width, height, bits_per_pixel, color_count): + ImageMetrics.__init__(self, width, height, + bits_per_pixel, color_count, + u'image/tiff') + + @classmethod + def parse(cls, file): + def tags(file, order): + while (True): + reader = BitstreamReader(file, order) + #read all the tags in an IFD + tag_count = reader.read(16) + sub_reader = reader.substream(tag_count * 12) + next_ifd = reader.read(32) + + for i in xrange(tag_count): + (tag_code, + tag_datatype, + tag_value_count) = sub_reader.parse("16u 16u 32u") + if (tag_datatype == 1): # BYTE type + tag_struct = "8u" * tag_value_count + elif (tag_datatype == 3): # SHORT type + tag_struct = "16u" * tag_value_count + elif (tag_datatype == 4): # LONG type + tag_struct = "32u" * tag_value_count + else: # all other types + tag_struct = "4b" + if (format_size(tag_struct) <= 32): + yield (tag_code, sub_reader.parse(tag_struct)) + sub_reader.skip(32 - format_size(tag_struct)) + else: + offset = sub_reader.read(32) + file.seek(offset, 0) + yield (tag_code, + BitstreamReader(file, order).parse(tag_struct)) + + if (next_ifd != 0): + file.seek(next_ifd, 0) + else: + break + + try: + byte_order = file.read(2) + if (byte_order == 'II'): + order = 1 + elif (byte_order == 'MM'): + order = 0 + else: + from .text import ERR_IMAGE_INVALID_TIFF + raise InvalidTIFF(ERR_IMAGE_INVALID_TIFF) + reader = BitstreamReader(file, order) + if (reader.read(16) != 42): + from .text import ERR_IMAGE_INVALID_TIFF + raise InvalidTIFF(ERR_IMAGE_INVALID_TIFF) + + initial_ifd = reader.read(32) + file.seek(initial_ifd, 0) + + width = 0 + height = 0 + bits_per_pixel = 0 + color_count = 0 + for (tag_id, tag_values) in tags(file, order): + if (tag_id == 0x0100): + width = tag_values[0] + elif (tag_id == 0x0101): + height = tag_values[0] + elif (tag_id == 0x0102): + bits_per_pixel = sum(tag_values) + elif (tag_id == 0x0140): + color_count = len(tag_values) / 3 + except IOError: + from .text import ERR_IMAGE_IOERROR_TIFF + raise InvalidTIFF(ERR_IMAGE_IOERROR_TIFF) + + return cls(width=width, + height=height, + bits_per_pixel=bits_per_pixel, + color_count=color_count)
View file
audiotools-2.19.tar.gz/audiotools/m4a.py
Added
@@ -0,0 +1,1289 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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, BIN, Image) +from .m4a_atoms import * + +####################### +#M4A File +####################### + + +class InvalidM4A(InvalidFile): + pass + + +def get_m4a_atom(reader, *atoms): + """given a BitstreamReader and atom name strings + returns a (size, substream) of the final atom data + (not including its 64-bit size/name header) + after traversing the parent atoms + """ + + from . import iter_last + + for (last, next_atom) in iter_last(iter(atoms)): + try: + (length, stream_atom) = reader.parse("32u 4b") + while (stream_atom != next_atom): + reader.skip_bytes(length - 8) + (length, stream_atom) = reader.parse("32u 4b") + if (last): + return (length - 8, reader.substream(length - 8)) + else: + reader = reader.substream(length - 8) + except IOError: + raise KeyError(next_atom) + + +def get_m4a_atom_offset(reader, *atoms): + """given a BitstreamReader and atom name strings + returns a (size, offset) of the final atom data + (including its 64-bit size/name header) + after traversing the parent atoms""" + + from . import iter_last + + offset = 0 + + for (last, next_atom) in iter_last(iter(atoms)): + try: + (length, stream_atom) = reader.parse("32u 4b") + offset += 8 + while (stream_atom != next_atom): + reader.skip_bytes(length - 8) + offset += (length - 8) + (length, stream_atom) = reader.parse("32u 4b") + offset += 8 + if (last): + return (length, offset - 8) + else: + reader = reader.substream(length - 8) + except IOError: + raise KeyError(next_atom) + + +class M4ATaggedAudio: + def __init__(self, filename): + self.filename = filename + + def get_metadata(self): + """returns a MetaData object, or None + + raises IOError if unable to read the file""" + + from .bitstream import BitstreamReader + + reader = BitstreamReader(file(self.filename, 'rb'), 0) + try: + try: + (meta_size, + meta_reader) = get_m4a_atom(reader, "moov", "udta", "meta") + except KeyError: + return None + + return M4A_META_Atom.parse("meta", meta_size, meta_reader, + {"hdlr": M4A_HDLR_Atom, + "ilst": M4A_Tree_Atom, + "free": M4A_FREE_Atom, + "\xa9alb": M4A_ILST_Leaf_Atom, + "\xa9ART": M4A_ILST_Leaf_Atom, + 'aART': M4A_ILST_Leaf_Atom, + "\xa9cmt": M4A_ILST_Leaf_Atom, + "covr": M4A_ILST_Leaf_Atom, + "cpil": M4A_ILST_Leaf_Atom, + "cprt": M4A_ILST_Leaf_Atom, + "\xa9day": M4A_ILST_Leaf_Atom, + "disk": M4A_ILST_Leaf_Atom, + "gnre": M4A_ILST_Leaf_Atom, + "----": M4A_ILST_Leaf_Atom, + "pgap": M4A_ILST_Leaf_Atom, + "rtng": M4A_ILST_Leaf_Atom, + "tmpo": M4A_ILST_Leaf_Atom, + "\xa9grp": M4A_ILST_Leaf_Atom, + "\xa9nam": M4A_ILST_Leaf_Atom, + "\xa9too": M4A_ILST_Leaf_Atom, + "trkn": M4A_ILST_Leaf_Atom, + "\xa9wrt": M4A_ILST_Leaf_Atom}) + finally: + reader.close() + + def update_metadata(self, metadata, old_metadata=None): + """takes this track's updated MetaData object + as returned by get_metadata() and sets this track's metadata + with any fields updated in that object + + old_metadata is the unmodifed metadata returned by get_metadata() + + raises IOError if unable to write the file + """ + + from .bitstream import BitstreamWriter + from .bitstream import BitstreamReader + import os.path + + if (metadata is None): + return + + if (not isinstance(metadata, M4A_META_Atom)): + from .text import ERR_FOREIGN_METADATA + raise ValueError(ERR_FOREIGN_METADATA) + + if (old_metadata is None): + #get_metadata() result may still be None, and that's okay + old_metadata = self.get_metadata() + + #M4A streams often have *two* "free" atoms we can attempt to resize + + #first, attempt to resize the one inside the "meta" atom + if ((old_metadata is not None) and + metadata.has_child("free") and + ((metadata.size() - metadata["free"].size()) <= + old_metadata.size())): + + metadata.replace_child( + M4A_FREE_Atom(old_metadata.size() - + (metadata.size() - + metadata["free"].size()))) + + f = file(self.filename, 'r+b') + (meta_size, meta_offset) = get_m4a_atom_offset( + BitstreamReader(f, 0), "moov", "udta", "meta") + f.seek(meta_offset + 8, 0) + metadata.build(BitstreamWriter(f, 0)) + f.close() + return + else: + #if there's insufficient room, + #attempt to resize the outermost "free" also + + #this is only possible if the file is laid out correctly, + #with "free" coming after "moov" but before "mdat" + #FIXME + + #if neither fix is possible, the whole file must be rewritten + #which also requires adjusting the "stco" atom offsets + m4a_tree = M4A_Tree_Atom.parse( + None, + os.path.getsize(self.filename), + BitstreamReader(file(self.filename, "rb"), 0), + {"moov": M4A_Tree_Atom, + "trak": M4A_Tree_Atom, + "mdia": M4A_Tree_Atom, + "minf": M4A_Tree_Atom, + "stbl": M4A_Tree_Atom, + "stco": M4A_STCO_Atom, + "udta": M4A_Tree_Atom}) + + #find initial mdat offset + initial_mdat_offset = m4a_tree.child_offset("mdat") + + #adjust moov -> udta -> meta atom + #(generating sub-atoms as necessary) + if (not m4a_tree.has_child("moov")): + return + else: + moov = m4a_tree["moov"] + if (not moov.has_child("udta")): + moov.append_child(M4A_Tree_Atom("udta", [])) + udta = moov["udta"] + if (not udta.has_child("meta")): + udta.append_child(metadata) + else: + udta.replace_child(metadata) + + #find new mdat offset + new_mdat_offset = m4a_tree.child_offset("mdat") + + #adjust moov -> trak -> mdia -> minf -> stbl -> stco offsets + #based on the difference between the new mdat position and the old + try: + delta_offset = new_mdat_offset - initial_mdat_offset + stco = m4a_tree["moov"]["trak"]["mdia"]["minf"]["stbl"]["stco"] + stco.offsets = [offset + delta_offset for offset in + stco.offsets] + except KeyError: + #if there is no stco atom, don't worry about it + pass + + #then write entire tree back to disk + writer = BitstreamWriter(file(self.filename, "wb"), 0) + m4a_tree.build(writer) + + def set_metadata(self, metadata): + """takes a MetaData object and sets this track's metadata + + this metadata includes track name, album name, and so on + raises IOError if unable to write the file""" + + if (metadata is None): + return + + old_metadata = self.get_metadata() + metadata = M4A_META_Atom.converted(metadata) + + #replace file-specific atoms in new metadata + #with ones from old metadata (if any) + #which can happen if we're shifting metadata + #from one M4A file to another + file_specific_atoms = frozenset(['\xa9too', '----', 'pgap', 'tmpo']) + + if (metadata.has_ilst_atom()): + metadata.ilst_atom().leaf_atoms = filter( + lambda atom: atom.name not in file_specific_atoms, + metadata.ilst_atom()) + + if (old_metadata.has_ilst_atom()): + metadata.ilst_atom().leaf_atoms.extend( + filter(lambda atom: atom.name in file_specific_atoms, + old_metadata.ilst_atom())) + + self.update_metadata(metadata, old_metadata) + + def delete_metadata(self): + """deletes the track's MetaData + + this removes or unsets tags as necessary in order to remove all data + raises IOError if unable to write the file""" + + from . import MetaData + + self.set_metadata(MetaData()) + + +class M4AAudio_faac(M4ATaggedAudio, AudioFile): + """an M4A audio file using faac/faad binaries for I/O""" + + SUFFIX = "m4a" + NAME = SUFFIX + DESCRIPTION = u"Advanced Audio Coding" + DEFAULT_COMPRESSION = "100" + COMPRESSION_MODES = tuple(["10"] + map(str, range(50, 500, 25)) + ["500"]) + BINARIES = ("faac", "faad") + + def __init__(self, filename): + """filename is a plain string""" + + from .bitstream import BitstreamReader + + self.filename = filename + + #first, fetch the mdia atom + #which is the parent of both the mp4a and mdhd atoms + try: + mdia = get_m4a_atom(BitstreamReader(file(filename, 'rb'), 0), + "moov", "trak", "mdia")[1] + except IOError: + from .text import ERR_M4A_IOERROR + raise InvalidM4A(ERR_M4A_IOERROR) + except KeyError: + from .text import ERR_M4A_MISSING_MDIA + raise InvalidM4A(ERR_M4A_MISSING_MDIA) + mdia.mark() + try: + try: + stsd = get_m4a_atom(mdia, "minf", "stbl", "stsd")[1] + except KeyError: + from .text import ERR_M4A_MISSING_STSD + raise InvalidM4A(ERR_M4A_MISSING_STSD) + + #then, fetch the mp4a atom for bps, channels and sample rate + try: + (stsd_version, descriptions) = stsd.parse("8u 24p 32u") + (mp4a, + self.__channels__, + self.__bits_per_sample__) = stsd.parse( + "32p 4b 48p 16p 16p 16p 4P 16u 16u 16p 16p 32p") + except IOError: + from .text import ERR_M4A_INVALID_MP4A + raise InvalidM4A(ERR_M4A_INVALID_MP4A) + + #finally, fetch the mdhd atom for total track length + mdia.rewind() + try: + mdhd = get_m4a_atom(mdia, "mdhd")[1] + except KeyError: + from .text import ERR_M4A_MISSING_MDHD + raise InvalidM4A(ERR_M4A_MISSING_MDHD) + try: + (version, ) = mdhd.parse("8u 24p") + if (version == 0): + (self.__sample_rate__, + self.__length__,) = mdhd.parse("32p 32p 32u 32u 2P 16p") + elif (version == 1): + (self.__sample_rate__, + self.__length__,) = mdhd.parse("64p 64p 32u 64U 2P 16p") + else: + from .text import ERR_M4A_UNSUPPORTED_MDHD + raise InvalidM4A(ERR_M4A_UNSUPPORTED_MDHD) + except IOError: + from .text import ERR_M4A_INVALID_MDHD + raise InvalidM4A(ERR_M4A_INVALID_MDHD) + finally: + mdia.unmark() + + def channel_mask(self): + """returns a ChannelMask object of this track's channel layout""" + + from . import ChannelMask + + #M4A seems to use the same channel assignment + #as old-style RIFF WAVE/FLAC + if (self.channels() == 1): + return ChannelMask.from_fields( + front_center=True) + elif (self.channels() == 2): + return ChannelMask.from_fields( + front_left=True, front_right=True) + elif (self.channels() == 3): + return ChannelMask.from_fields( + front_left=True, front_right=True, front_center=True) + elif (self.channels() == 4): + return ChannelMask.from_fields( + front_left=True, front_right=True, + back_left=True, back_right=True) + elif (self.channels() == 5): + return ChannelMask.from_fields( + front_left=True, front_right=True, front_center=True, + back_left=True, back_right=True) + elif (self.channels() == 6): + return ChannelMask.from_fields( + front_left=True, front_right=True, front_center=True, + back_left=True, back_right=True, + low_frequency=True) + else: + return ChannelMask(0) + + def lossless(self): + """returns False""" + + return False + + 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.__bits_per_sample__ + + def sample_rate(self): + """returns the rate of the track's audio as an integer number of Hz""" + + return self.__sample_rate__ + + def cd_frames(self): + """returns the total length of the track in CD frames + + each CD frame is 1/75th of a second""" + + return (self.__length__ - 1024) / self.__sample_rate__ * 75 + + def total_frames(self): + """returns the total PCM frames of the track as an integer""" + + return self.__length__ - 1024 + + def to_pcm(self): + """returns a PCMReader object containing the track's PCM data""" + + from . import PCMReader + import subprocess + import os + + devnull = file(os.devnull, "ab") + + sub = subprocess.Popen([BIN['faad'], "-f", str(2), "-w", + self.filename], + stdout=subprocess.PIPE, + stderr=devnull) + return PCMReader( + sub.stdout, + sample_rate=self.sample_rate(), + channels=self.channels(), + channel_mask=int(self.channel_mask()), + bits_per_sample=self.bits_per_sample(), + process=sub) + + @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 M4AAudio object""" + + from . import PCMConverter + from . import transfer_data + from . import transfer_framelist_data + from . import ignore_sigint + from . import EncodingError + from . import DecodingError + from . import ChannelMask + from . import __default_quality__ + import subprocess + import os + + if ((compression is None) or (compression not in + cls.COMPRESSION_MODES)): + compression = __default_quality__(cls.NAME) + + if (pcmreader.channels > 2): + pcmreader = PCMConverter(pcmreader, + sample_rate=pcmreader.sample_rate, + channels=2, + channel_mask=ChannelMask.from_channels(2), + bits_per_sample=pcmreader.bits_per_sample) + + #faac requires files to end with .m4a for some reason + if (not filename.endswith(".m4a")): + import tempfile + actual_filename = filename + tempfile = tempfile.NamedTemporaryFile(suffix=".m4a") + filename = tempfile.name + else: + actual_filename = tempfile = None + + devnull = file(os.devnull, "ab") + + sub = subprocess.Popen([BIN['faac'], + "-q", compression, + "-P", + "-R", str(pcmreader.sample_rate), + "-B", str(pcmreader.bits_per_sample), + "-C", str(pcmreader.channels), + "-X", + "-o", filename, + "-"], + stdin=subprocess.PIPE, + stderr=devnull, + stdout=devnull, + preexec_fn=ignore_sigint) + #Note: faac handles SIGINT on its own, + #so trying to ignore it doesn't work like on most other encoders. + + try: + transfer_framelist_data(pcmreader, sub.stdin.write) + except (ValueError, IOError), err: + sub.stdin.close() + sub.wait() + cls.__unlink__(filename) + raise EncodingError(str(err)) + except Exception, err: + sub.stdin.close() + sub.wait() + cls.__unlink__(filename) + raise err + + try: + pcmreader.close() + except DecodingError, err: + raise EncodingError(err.error_message) + sub.stdin.close() + + if (sub.wait() == 0): + if (tempfile is not None): + filename = actual_filename + f = file(filename, 'wb') + tempfile.seek(0, 0) + transfer_data(tempfile.read, f.write) + f.close() + tempfile.close() + + return M4AAudio(filename) + else: + if (tempfile is not None): + tempfile.close() + raise EncodingError(u"unable to write file with faac") + + @classmethod + def supports_replay_gain(cls): + """returns True if this class supports ReplayGain""" + + return False + + @classmethod + def can_add_replay_gain(cls, audiofiles): + """returns False""" + + return False + + @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 + """ + + import subprocess + import os + from . import open_files + + track_names = [track.filename for track in + open_files(filenames) if + isinstance(track, cls)] + + if (progress is not None): + progress(0, 1) + + #helpfully, aacgain is flag-for-flag compatible with mp3gain + if ((len(track_names) > 0) and (BIN.can_execute(BIN['aacgain']))): + devnull = file(os.devnull, 'ab') + sub = subprocess.Popen([BIN['aacgain'], '-k', '-q', '-r'] + + track_names, + stdout=devnull, + stderr=devnull) + sub.wait() + + devnull.close() + + if (progress is not None): + progress(1, 1) + + +class M4AAudio_nero(M4AAudio_faac): + """an M4A audio file using neroAacEnc/neroAacDec binaries for I/O""" + + from .text import (COMP_NERO_LOW, COMP_NERO_HIGH) + + DEFAULT_COMPRESSION = "0.5" + COMPRESSION_MODES = ("0.4", "0.5", + "0.6", "0.7", "0.8", "0.9", "1.0") + COMPRESSION_DESCRIPTIONS = {"0.4": COMP_NERO_LOW, + "1.0": COMP_NERO_HIGH} + BINARIES = ("neroAacDec", "neroAacEnc") + + @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 M4AAudio object""" + + import tempfile + import os + import os.path + from . import PCMConverter + from . import WaveAudio + from . import __default_quality__ + + if ((compression is None) or (compression not in + cls.COMPRESSION_MODES)): + compression = __default_quality__(cls.NAME) + + tempwavefile = tempfile.NamedTemporaryFile(suffix=".wav") + try: + if (pcmreader.sample_rate > 96000): + tempwave = WaveAudio.from_pcm( + tempwavefile.name, + PCMConverter(pcmreader, + sample_rate=96000, + channels=pcmreader.channels, + channel_mask=pcmreader.channel_mask, + bits_per_sample=pcmreader.bits_per_sample)) + else: + tempwave = WaveAudio.from_pcm( + tempwavefile.name, + pcmreader) + + cls.__from_wave__(filename, tempwave.filename, compression) + return cls(filename) + finally: + if (os.path.isfile(tempwavefile.name)): + tempwavefile.close() + else: + tempwavefile.close_called = True + + def to_pcm(self): + import tempfile + from . import EncodingError + from . import PCMReaderError + from .wav import WaveReader + + f = tempfile.NamedTemporaryFile(suffix=".wav") + try: + self.__to_wave__(f.name) + return WaveReader(wave_file=file(f.name, "rb"), + sample_rate=self.sample_rate(), + channels=self.channels(), + channel_mask=int(self.channel_mask()), + bits_per_sample=self.bits_per_sample()) + except EncodingError, err: + return PCMReaderError(error_message=err.error_message, + sample_rate=self.sample_rate(), + channels=self.channels(), + channel_mask=int(self.channel_mask()), + bits_per_sample=self.bits_per_sample()) + + def __to_wave__(self, wave_file, progress=None): + """writes the contents of this file to the given .wav filename string + + raises EncodingError if some error occurs during decoding""" + + import subprocess + import os + from . import EncodingError + + devnull = file(os.devnull, "w") + try: + sub = subprocess.Popen([BIN["neroAacDec"], + "-if", self.filename, + "-of", wave_file], + stdout=devnull, + stderr=devnull) + if (sub.wait() != 0): + raise EncodingError(u"unable to write file with neroAacDec") + finally: + devnull.close() + + @classmethod + def __from_wave__(cls, filename, wave_filename, compression): + import subprocess + import os + from . import EncodingError + + devnull = file(os.devnull, "w") + try: + sub = subprocess.Popen([BIN["neroAacEnc"], + "-q", compression, + "-if", wave_filename, + "-of", filename], + stdout=devnull, + stderr=devnull) + + if (sub.wait() != 0): + raise EncodingError(u"neroAacEnc unable to write file") + else: + return cls(filename) + finally: + devnull.close() + +if (BIN.can_execute(BIN["neroAacEnc"]) and BIN.can_execute(BIN["neroAacDec"])): + M4AAudio = M4AAudio_nero +else: + M4AAudio = M4AAudio_faac + + +class M4ACovr(Image): + """a subclass of Image to store M4A 'covr' atoms""" + + def __init__(self, image_data): + self.image_data = image_data + + img = Image.new(image_data, u'', 0) + + Image.__init__(self, + data=image_data, + mime_type=img.mime_type, + width=img.width, + height=img.height, + color_depth=img.color_depth, + color_count=img.color_count, + description=img.description, + type=img.type) + + @classmethod + def converted(cls, image): + """given an Image object, returns an M4ACovr object""" + + return M4ACovr(image.data) + + +class __counter__: + def __init__(self): + self.val = 0 + + def incr(self): + self.val += 1 + + def __int__(self): + return self.val + + +class InvalidALAC(InvalidFile): + pass + + +class ALACAudio(M4ATaggedAudio, AudioFile): + """an Apple Lossless audio file""" + + SUFFIX = "m4a" + NAME = "alac" + DESCRIPTION = u"Apple Lossless" + DEFAULT_COMPRESSION = "" + COMPRESSION_MODES = ("",) + BINARIES = tuple() + + BLOCK_SIZE = 4096 + INITIAL_HISTORY = 10 + HISTORY_MULTIPLIER = 40 + MAXIMUM_K = 14 + + def __init__(self, filename): + """filename is a plain string""" + + from .bitstream import BitstreamReader + + self.filename = filename + + #first, fetch the mdia atom + #which is the parent of both the alac and mdhd atoms + try: + mdia = get_m4a_atom(BitstreamReader(file(filename, 'rb'), 0), + "moov", "trak", "mdia")[1] + except IOError: + from .text import ERR_ALAC_IOERROR + raise InvalidALAC(ERR_ALAC_IOERROR) + except KeyError: + from .text import ERR_M4A_MISSING_MDIA + raise InvalidALAC(ERR_M4A_MISSING_MDIA) + mdia.mark() + try: + try: + stsd = get_m4a_atom(mdia, "minf", "stbl", "stsd")[1] + except KeyError: + from .text import ERR_M4A_MISSING_STSD + raise InvalidALAC(ERR_M4A_MISSING_STSD) + + #then, fetch the alac atom for bps, channels and sample rate + try: + #though some of these fields are parsed redundantly + #in .to_pcm(), we still need to parse them here + #to fetch values for .bits_per_sample(), etc. + (stsd_version, descriptions) = stsd.parse("8u 24p 32u") + (alac1, + alac2, + self.__max_samples_per_frame__, + self.__bits_per_sample__, + self.__history_multiplier__, + self.__initial_history__, + self.__maximum_k__, + self.__channels__, + self.__sample_rate__) = stsd.parse( + #ignore much of the stuff in the "high" ALAC atom + "32p 4b 6P 16p 16p 16p 4P 16p 16p 16p 16p 4P" + + #and use the attributes in the "low" ALAC atom instead + "32p 4b 4P 32u 8p 8u 8u 8u 8u 8u 16p 32p 32p 32u") + except IOError: + from .text import ERR_ALAC_INVALID_ALAC + raise InvalidALAC(ERR_ALAC_INVALID_ALAC) + + if ((alac1 != 'alac') or (alac2 != 'alac')): + from .text import ERR_ALAC_INVALID_ALAC + mdia.unmark() + raise InvalidALAC(ERR_ALAC_INVALID_ALAC) + + #finally, fetch the mdhd atom for total track length + mdia.rewind() + try: + mdhd = get_m4a_atom(mdia, "mdhd")[1] + except KeyError: + from .text import ERR_M4A_MISSING_MDHD + raise InvalidALAC(ERR_M4A_MISSING_MDHD) + try: + (version, ) = mdhd.parse("8u 24p") + if (version == 0): + (self.__length__,) = mdhd.parse("32p 32p 32p 32u 2P 16p") + elif (version == 1): + (self.__length__,) = mdhd.parse("64p 64p 32p 64U 2P 16p") + else: + from .text import ERR_M4A_UNSUPPORTED_MDHD + raise InvalidALAC(ERR_M4A_UNSUPPORTED_MDHD) + except IOError: + from .text import ERR_M4A_INVALID_MDHD + raise InvalidALAC(ERR_M4A_INVALID_MDHD) + finally: + mdia.unmark() + + 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.__bits_per_sample__ + + 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.__length__ + + def channel_mask(self): + """returns a ChannelMask object of this track's channel layout""" + + from . import ChannelMask + + return { + 1: ChannelMask.from_fields( + front_center=True), + 2: ChannelMask.from_fields( + front_left=True, + front_right=True), + 3: ChannelMask.from_fields( + front_center=True, + front_left=True, + front_right=True), + 4: ChannelMask.from_fields( + front_center=True, + front_left=True, + front_right=True, + back_center=True), + 5: ChannelMask.from_fields( + front_center=True, + front_left=True, + front_right=True, + back_left=True, + back_right=True), + 6: ChannelMask.from_fields( + front_center=True, + front_left=True, + front_right=True, + back_left=True, + back_right=True, + low_frequency=True), + 7: ChannelMask.from_fields( + front_center=True, + front_left=True, + front_right=True, + back_left=True, + back_right=True, + back_center=True, + low_frequency=True), + 8: ChannelMask.from_fields( + front_center=True, + front_left_of_center=True, + front_right_of_center=True, + front_left=True, + front_right=True, + back_left=True, + back_right=True, + low_frequency=True)}.get(self.channels(), ChannelMask(0)) + + def cd_frames(self): + """returns the total length of the track in CD frames + + each CD frame is 1/75th of a second""" + + try: + return (self.total_frames() * 75) / self.sample_rate() + except ZeroDivisionError: + return 0 + + def lossless(self): + """returns True""" + + return True + + def to_pcm(self): + """returns a PCMReader object containing the track's PCM data""" + + from .decoders import ALACDecoder + from . import PCMReaderError + + try: + return ALACDecoder(self.filename) + except (IOError, ValueError), msg: + return PCMReaderError(error_message=str(msg), + 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, + block_size=4096, encoding_function=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 ALACAudio object""" + + if (pcmreader.bits_per_sample not in (16, 24)): + from . import UnsupportedBitsPerSample + + raise UnsupportedBitsPerSample(filename, pcmreader.bits_per_sample) + + if (int(pcmreader.channel_mask) not in + (0x0001, # 1ch - mono + 0x0004, # 1ch - mono + 0x0003, # 2ch - left, right + 0x0007, # 3ch - center, left, right + 0x0107, # 4ch - center, left, right, back center + 0x0037, # 5ch - center, left, right, back left, back right + 0x003F, # 6ch - C, L, R, back left, back right, LFE + 0x013F, # 7ch - C, L, R, bL, bR, back center, LFE + 0x00FF, # 8ch - C, cL, cR, L, R, bL, bR, LFE + 0x0000)): # undefined + from . import UnsupportedChannelMask + + raise UnsupportedChannelMask(filename, + int(pcmreader.channel_mask)) + + from .encoders import encode_alac + from .bitstream import BitstreamWriter + from . import transfer_data + from . import EncodingError + from . import BufferedPCMReader + from . import at_a_time + import time + import tempfile + + mdat_file = tempfile.TemporaryFile() + + #perform encode_alac() on pcmreader to our output file + #which returns a tuple of output values + #which are various fields for the "alac" atom + try: + (frame_sample_sizes, + frame_byte_sizes, + frame_file_offsets, + mdat_size) = (encode_alac if encoding_function is None else + encoding_function)(file=mdat_file, + pcmreader= + BufferedPCMReader(pcmreader), + block_size=block_size, + initial_history= + cls.INITIAL_HISTORY, + history_multiplier= + cls.HISTORY_MULTIPLIER, + maximum_k= + cls.MAXIMUM_K) + except (IOError, ValueError), err: + raise EncodingError(str(err)) + + #use the fields from encode_alac() to populate our ALAC atoms + create_date = long(time.time()) + 2082844800 + total_pcm_frames = sum(frame_sample_sizes) + + stts_frame_counts = {} + for sample_size in frame_sample_sizes: + stts_frame_counts.setdefault(sample_size, __counter__()).incr() + stts_frame_counts = dict([(k, int(v)) for (k, v) + in stts_frame_counts.items()]) + + offsets = frame_file_offsets[:] + chunks = [] + for frames in at_a_time(len(frame_file_offsets), 5): + if (frames > 0): + chunks.append(offsets[0:frames]) + offsets = offsets[frames:] + del(offsets) + + #add the size of ftyp + moov + free to our absolute file offsets + pre_mdat_size = (8 + cls.__ftyp_atom__().size() + + 8 + cls.__moov_atom__(pcmreader, + create_date, + mdat_size, + total_pcm_frames, + frame_sample_sizes, + stts_frame_counts, + chunks, + frame_byte_sizes).size() + + 8 + cls.__free_atom__(0x1000).size()) + + chunks = [[chunk + pre_mdat_size for chunk in chunk_list] + for chunk_list in chunks] + + #then regenerate our live ftyp, moov and free atoms + #with actual data + ftyp = cls.__ftyp_atom__() + + moov = cls.__moov_atom__(pcmreader, + create_date, + mdat_size, + total_pcm_frames, + frame_sample_sizes, + stts_frame_counts, + chunks, + frame_byte_sizes) + + free = cls.__free_atom__(0x1000) + + #build our complete output file + try: + f = file(filename, 'wb') + m4a_writer = BitstreamWriter(f, 0) + m4a_writer.build("32u 4b", (ftyp.size() + 8, ftyp.name)) + ftyp.build(m4a_writer) + m4a_writer.build("32u 4b", (moov.size() + 8, moov.name)) + moov.build(m4a_writer) + m4a_writer.build("32u 4b", (free.size() + 8, free.name)) + free.build(m4a_writer) + mdat_file.seek(0, 0) + transfer_data(mdat_file.read, f.write) + mdat_file.close() + except (IOError), err: + mdat_file.close() + raise EncodingError(str(err)) + + return cls(filename) + + @classmethod + def __ftyp_atom__(cls): + return M4A_FTYP_Atom(major_brand='M4A ', + major_brand_version=0, + compatible_brands=['M4A ', + 'mp42', + 'isom', + chr(0) * 4]) + + @classmethod + def __moov_atom__(cls, pcmreader, + create_date, + mdat_size, + total_pcm_frames, + frame_sample_sizes, + stts_frame_counts, + chunks, + frame_byte_sizes): + return M4A_Tree_Atom( + "moov", + [cls.__mvhd_atom__(pcmreader, create_date, total_pcm_frames), + M4A_Tree_Atom( + "trak", + [cls.__tkhd_atom__(create_date, total_pcm_frames), + M4A_Tree_Atom( + "mdia", + [cls.__mdhd_atom__(pcmreader, + create_date, + total_pcm_frames), + cls.__hdlr_atom__(), + M4A_Tree_Atom("minf", + [cls.__smhd_atom__(), + M4A_Tree_Atom( + "dinf", + [cls.__dref_atom__()]), + M4A_Tree_Atom( + "stbl", + [cls.__stsd_atom__( + pcmreader, + mdat_size, + frame_sample_sizes, + frame_byte_sizes), + cls.__stts_atom__( + stts_frame_counts), + cls.__stsc_atom__( + chunks), + cls.__stsz_atom__( + frame_byte_sizes), + cls.__stco_atom__( + chunks)])])])]), + M4A_Tree_Atom("udta", [cls.__meta_atom__()])]) + + @classmethod + def __mvhd_atom__(cls, pcmreader, create_date, total_pcm_frames): + return M4A_MVHD_Atom(version=0, + flags=0, + created_utc_date=create_date, + modified_utc_date=create_date, + time_scale=pcmreader.sample_rate, + duration=total_pcm_frames, + playback_speed=0x10000, + user_volume=0x100, + geometry_matrices=[0x10000, + 0, + 0, + 0, + 0x10000, + 0, + 0, + 0, + 0x40000000], + qt_preview=0, + qt_still_poster=0, + qt_selection_time=0, + qt_current_time=0, + next_track_id=2) + + @classmethod + def __tkhd_atom__(cls, create_date, total_pcm_frames): + return M4A_TKHD_Atom(version=0, + track_in_poster=0, + track_in_preview=1, + track_in_movie=1, + track_enabled=1, + created_utc_date=create_date, + modified_utc_date=create_date, + track_id=1, + duration=total_pcm_frames, + video_layer=0, + qt_alternate=0, + volume=0x100, + geometry_matrices=[0x10000, + 0, + 0, + 0, + 0x10000, + 0, + 0, + 0, + 0x40000000], + video_width=0, + video_height=0) + + @classmethod + def __mdhd_atom__(cls, pcmreader, create_date, total_pcm_frames): + return M4A_MDHD_Atom(version=0, + flags=0, + created_utc_date=create_date, + modified_utc_date=create_date, + sample_rate=pcmreader.sample_rate, + track_length=total_pcm_frames, + language=[ord(c) - 0x60 for c in "und"], + quality=0) + + @classmethod + def __hdlr_atom__(cls): + return M4A_HDLR_Atom(version=0, + flags=0, + qt_type=chr(0) * 4, + qt_subtype='soun', + qt_manufacturer=chr(0) * 4, + qt_reserved_flags=0, + qt_reserved_flags_mask=0, + component_name="", + padding_size=1) + + @classmethod + def __smhd_atom__(cls): + return M4A_SMHD_Atom(version=0, + flags=0, + audio_balance=0) + + @classmethod + def __dref_atom__(cls): + return M4A_DREF_Atom(version=0, + flags=0, + references=[M4A_Leaf_Atom("url ", + "\x00\x00\x00\x01")]) + + @classmethod + def __stsd_atom__(cls, pcmreader, mdat_size, frame_sample_sizes, + frame_byte_sizes): + return M4A_STSD_Atom( + version=0, + flags=0, + descriptions=[ + M4A_ALAC_Atom( + reference_index=1, + qt_version=0, + qt_revision_level=0, + qt_vendor=chr(0) * 4, + channels=pcmreader.channels, + bits_per_sample=pcmreader.bits_per_sample, + qt_compression_id=0, + audio_packet_size=0, + sample_rate=0xAC440000, # regardless of actual sample rate + sub_alac=M4A_SUB_ALAC_Atom( + max_samples_per_frame=max(frame_sample_sizes), + bits_per_sample=pcmreader.bits_per_sample, + history_multiplier=cls.HISTORY_MULTIPLIER, + initial_history=cls.INITIAL_HISTORY, + maximum_k=cls.MAXIMUM_K, + channels=pcmreader.channels, + unknown=0x00FF, + max_coded_frame_size=max(frame_byte_sizes), + bitrate=((mdat_size * 8 * pcmreader.sample_rate) / + sum(frame_sample_sizes)), + sample_rate=pcmreader.sample_rate))]) + + @classmethod + def __stts_atom__(cls, stts_frame_counts): + return M4A_STTS_Atom( + version=0, + flags=0, + times=[(int(stts_frame_counts[samples]), samples) + for samples in reversed(sorted(stts_frame_counts.keys()))]) + + @classmethod + def __stsc_atom__(cls, chunks): + return M4A_STSC_Atom( + version=0, + flags=0, + blocks=[(i + 1, current, 1) for (i, (current, previous)) + in enumerate(zip(map(len, chunks), [0] + map(len, chunks))) + if (current != previous)]) + + @classmethod + def __stsz_atom__(cls, frame_byte_sizes): + return M4A_STSZ_Atom( + version=0, + flags=0, + byte_size=0, + block_sizes=frame_byte_sizes) + + @classmethod + def __stco_atom__(cls, chunks): + return M4A_STCO_Atom( + version=0, + flags=0, + offsets=[chunk[0] for chunk in chunks]) + + @classmethod + def __meta_atom__(cls): + from . import VERSION + + return M4A_META_Atom( + version=0, + flags=0, + leaf_atoms=[ + M4A_HDLR_Atom(version=0, + flags=0, + qt_type=chr(0) * 4, + qt_subtype='mdir', + qt_manufacturer='appl', + qt_reserved_flags=0, + qt_reserved_flags_mask=0, + component_name="", + padding_size=1), + M4A_Tree_Atom( + "ilst", + [M4A_ILST_Leaf_Atom( + '\xa9too', + [M4A_ILST_Unicode_Data_Atom( + 0, 1, "Python Audio Tools %s" % (VERSION))])]), + M4A_FREE_Atom(1024)]) + + @classmethod + def __free_atom__(cls, size): + return M4A_FREE_Atom(size)
View file
audiotools-2.19.tar.gz/audiotools/m4a_atoms.py
Added
@@ -0,0 +1,1923 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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 . import MetaData, Image +from .image import image_metrics + +#M4A atoms are typically laid on in the file as follows: +# ftyp +# mdat +# moov/ +# +mvhd +# +iods +# +trak/ +# +-tkhd +# +-mdia/ +# +--mdhd +# +--hdlr +# +--minf/ +# +---smhd +# +---dinf/ +# +----dref +# +---stbl/ +# +----stsd +# +----stts +# +----stsz +# +----stsc +# +----stco +# +----ctts +# +udta/ +# +-meta +# +#Where atoms ending in / are container atoms and the rest are leaf atoms. +#'mdat' is where the file's audio stream is stored +#the rest are various bits of metadata + + +def parse_sub_atoms(data_size, reader, parsers): + """data size is the length of the parent atom's data + reader is a BitstreamReader + parsers is a dict of leaf_name->parser() + where parser is defined as: + parser(leaf_name, leaf_data_size, BitstreamReader, parsers) + as a sort of recursive parsing handler + """ + + leaf_atoms = [] + + while (data_size > 0): + (leaf_size, leaf_name) = reader.parse("32u 4b") + leaf_atoms.append( + parsers.get(leaf_name, M4A_Leaf_Atom).parse( + leaf_name, + leaf_size - 8, + reader.substream(leaf_size - 8), + parsers)) + data_size -= leaf_size + + return leaf_atoms + +#build(), parse() and size() work on atom data +#but not the atom's size and name values + + +class M4A_Tree_Atom: + def __init__(self, name, leaf_atoms): + """name should be a 4 byte string + + children should be a list of M4A_Tree_Atoms or M4A_Leaf_Atoms""" + + self.name = name + try: + iter(leaf_atoms) + except TypeError: + from .text import ERR_M4A_INVALID_LEAF_ATOMS + raise TypeError(ERR_M4A_INVALID_LEAF_ATOMS) + self.leaf_atoms = leaf_atoms + + def copy(self): + """returns a newly copied instance of this atom + and new instances of any sub-atoms it contains""" + + return M4A_Tree_Atom(self.name, [leaf.copy() for leaf in self]) + + def __repr__(self): + return "M4A_Tree_Atom(%s, %s)" % \ + (repr(self.name), repr(self.leaf_atoms)) + + def __eq__(self, atom): + for attr in ["name", "leaf_atoms"]: + if ((not hasattr(atom, attr)) or (getattr(self, attr) != + getattr(atom, attr))): + return False + else: + return True + + def __iter__(self): + for leaf in self.leaf_atoms: + yield leaf + + def __getitem__(self, atom_name): + return self.get_child(atom_name) + + def get_child(self, atom_name): + """returns the first instance of the given child atom + raises KeyError if the child is not found""" + + for leaf in self: + if (leaf.name == atom_name): + return leaf + else: + raise KeyError(atom_name) + + def has_child(self, atom_name): + """returns True if the given atom name + is an immediate child of this atom""" + + for leaf in self: + if (leaf.name == atom_name): + return True + else: + return False + + def add_child(self, atom_obj): + """adds the given child atom to this container""" + + self.leaf_atoms.append(atom_obj) + + def remove_child(self, atom_name): + """removes the first instance of the given atom from this container""" + + new_leaf_atoms = [] + data_deleted = False + for leaf_atom in self: + if ((leaf_atom.name == atom_name) and (not data_deleted)): + data_deleted = True + else: + new_leaf_atoms.append(leaf_atom) + + self.leaf_atoms = new_leaf_atoms + + def replace_child(self, atom_obj): + """replaces the first instance of the given atom's name + with the given atom""" + + new_leaf_atoms = [] + data_replaced = False + for leaf_atom in self: + if ((leaf_atom.name == atom_obj.name) and (not data_replaced)): + new_leaf_atoms.append(atom_obj) + data_replaced = True + else: + new_leaf_atoms.append(leaf_atom) + + self.leaf_atoms = new_leaf_atoms + + def child_offset(self, *child_path): + """given a path to the given child atom + returns its offset within this parent + + raises KeyError if the child cannot be found""" + + offset = 0 + next_child = child_path[0] + for leaf_atom in self: + if (leaf_atom.name == next_child): + if (len(child_path) > 1): + return (offset + 8 + + leaf_atom.child_offset(*(child_path[1:]))) + else: + return offset + else: + offset += (8 + leaf_atom.size()) + else: + raise KeyError(next_child) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + return cls(name, parse_sub_atoms(data_size, reader, parsers)) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + for sub_atom in self: + writer.build("32u 4b", (sub_atom.size() + 8, sub_atom.name)) + sub_atom.build(writer) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return sum([8 + sub_atom.size() for sub_atom in self]) + + +class M4A_Leaf_Atom: + def __init__(self, name, data): + """name should be a 4 byte string + + data should be a binary string of atom data""" + + self.name = name + self.data = data + + def copy(self): + """returns a newly copied instance of this atom + and new instances of any sub-atoms it contains""" + + return M4A_Leaf_Atom(self.name, self.data) + + def __repr__(self): + return "M4A_Leaf_Atom(%s, %s)" % \ + (repr(self.name), repr(self.data)) + + def __eq__(self, atom): + for attr in ["name", "data"]: + if ((not hasattr(atom, attr)) or (getattr(self, attr) != + getattr(atom, attr))): + return False + else: + return True + + def __unicode__(self): + #FIXME - should make this more informative, if possible + return self.data.encode('hex')[0:40].decode('ascii') + + def raw_info(self): + """returns a line of human-readable information about the atom""" + + if (len(self.data) > 20): + return u"%s : %s\u2026" % \ + (self.name.decode('ascii', 'replace'), + u"".join([u"%2.2X" % (ord(b)) for b in self.data[0:20]])) + else: + return u"%s : %s" % \ + (self.name.decode('ascii', 'replace'), + u"".join([u"%2.2X" % (ord(b)) for b in self.data])) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + return cls(name, reader.read_bytes(data_size)) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.write_bytes(self.data) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return len(self.data) + + +class M4A_FTYP_Atom(M4A_Leaf_Atom): + def __init__(self, major_brand, major_brand_version, compatible_brands): + self.name = 'ftyp' + self.major_brand = major_brand + self.major_brand_version = major_brand_version + self.compatible_brands = compatible_brands + + def __repr__(self): + return "M4A_FTYP_Atom(%s, %s, %s)" % \ + (repr(self.major_brand), + repr(self.major_brand_version), + repr(self.compatible_brands)) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + assert(name == 'ftyp') + return cls(reader.read_bytes(4), + reader.read(32), + [reader.read_bytes(4) + for i in xrange((data_size - 8) / 4)]) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("4b 32u %s" % ("4b" * len(self.compatible_brands)), + [self.major_brand, + self.major_brand_version] + + self.compatible_brands) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return 4 + 4 + (4 * len(self.compatible_brands)) + + +class M4A_MVHD_Atom(M4A_Leaf_Atom): + def __init__(self, version, flags, created_utc_date, modified_utc_date, + time_scale, duration, playback_speed, user_volume, + geometry_matrices, qt_preview, qt_still_poster, + qt_selection_time, qt_current_time, next_track_id): + self.name = 'mvhd' + self.version = version + self.flags = flags + self.created_utc_date = created_utc_date + self.modified_utc_date = modified_utc_date + self.time_scale = time_scale + self.duration = duration + self.playback_speed = playback_speed + self.user_volume = user_volume + self.geometry_matrices = geometry_matrices + self.qt_preview = qt_preview + self.qt_still_poster = qt_still_poster + self.qt_selection_time = qt_selection_time + self.qt_current_time = qt_current_time + self.next_track_id = next_track_id + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + assert(name == 'mvhd') + (version, flags) = reader.parse("8u 24u") + + if (version == 0): + atom_format = "32u 32u 32u 32u 32u 16u 10P" + else: + atom_format = "64U 64U 32u 64U 32u 16u 10P" + (created_utc_date, + modified_utc_date, + time_scale, + duration, + playback_speed, + user_volume) = reader.parse(atom_format) + + geometry_matrices = reader.parse("32u" * 9) + + (qt_preview, + qt_still_poster, + qt_selection_time, + qt_current_time, + next_track_id) = reader.parse("64U 32u 64U 32u 32u") + + return cls(version=version, + flags=flags, + created_utc_date=created_utc_date, + modified_utc_date=modified_utc_date, + time_scale=time_scale, + duration=duration, + playback_speed=playback_speed, + user_volume=user_volume, + geometry_matrices=geometry_matrices, + qt_preview=qt_preview, + qt_still_poster=qt_still_poster, + qt_selection_time=qt_selection_time, + qt_current_time=qt_current_time, + next_track_id=next_track_id) + + def __repr__(self): + return "MVHD_Atom(%s)" % ( + ",".join(map(repr, + [self.version, self.flags, + self.created_utc_date, self.modified_utc_date, + self.time_scale, self.duration, self.playback_speed, + self.user_volume, self.geometry_matrices, + self.qt_preview, self.qt_still_poster, + self.qt_selection_time, self.qt_current_time, + self.next_track_id]))) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("8u 24u", (self.version, self.flags)) + + if (self.version == 0): + atom_format = "32u 32u 32u 32u 32u 16u 10P" + else: + atom_format = "64U 64U 32u 64U 32u 16u 10P" + + writer.build(atom_format, + (self.created_utc_date, self.modified_utc_date, + self.time_scale, self.duration, + self.playback_speed, self.user_volume)) + + writer.build("32u" * 9, self.geometry_matrices) + + writer.build("64U 32u 64U 32u 32u", + (self.qt_preview, self.qt_still_poster, + self.qt_selection_time, self.qt_current_time, + self.next_track_id)) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + if (self.version == 0): + return 100 + else: + return 112 + + +class M4A_TKHD_Atom(M4A_Leaf_Atom): + def __init__(self, version, track_in_poster, track_in_preview, + track_in_movie, track_enabled, created_utc_date, + modified_utc_date, track_id, duration, video_layer, + qt_alternate, volume, geometry_matrices, + video_width, video_height): + self.name = 'tkhd' + self.version = version + self.track_in_poster = track_in_poster + self.track_in_preview = track_in_preview + self.track_in_movie = track_in_movie + self.track_enabled = track_enabled + self.created_utc_date = created_utc_date + self.modified_utc_date = modified_utc_date + self.track_id = track_id + self.duration = duration + self.video_layer = video_layer + self.qt_alternate = qt_alternate + self.volume = volume + self.geometry_matrices = geometry_matrices + self.video_width = video_width + self.video_height = video_height + + def __repr__(self): + return "M4A_TKHD_Atom(%s)" % ( + ",".join(map(repr, + [self.version, self.track_in_poster, + self.track_in_preview, self.track_in_movie, + self.track_enabled, self.created_utc_date, + self.modified_utc_date, self.track_id, + self.duration, self.video_layer, self.qt_alternate, + self.volume, self.geometry_matrices, + self.video_width, self.video_height]))) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + (version, + track_in_poster, + track_in_preview, + track_in_movie, + track_enabled) = reader.parse("8u 20p 1u 1u 1u 1u") + + if (version == 0): + atom_format = "32u 32u 32u 4P 32u 8P 16u 16u 16u 2P" + else: + atom_format = "64U 64U 32u 4P 64U 8P 16u 16u 16u 2P" + (created_utc_date, + modified_utc_date, + track_id, + duration, + video_layer, + qt_alternate, + volume) = reader.parse(atom_format) + + geometry_matrices = reader.parse("32u" * 9) + (video_width, video_height) = reader.parse("32u 32u") + + return cls(version=version, + track_in_poster=track_in_poster, + track_in_preview=track_in_preview, + track_in_movie=track_in_movie, + track_enabled=track_enabled, + created_utc_date=created_utc_date, + modified_utc_date=modified_utc_date, + track_id=track_id, + duration=duration, + video_layer=video_layer, + qt_alternate=qt_alternate, + volume=volume, + geometry_matrices=geometry_matrices, + video_width=video_width, + video_height=video_height) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("8u 20p 1u 1u 1u 1u", + (self.version, self.track_in_poster, + self.track_in_preview, self.track_in_movie, + self.track_enabled)) + if (self.version == 0): + atom_format = "32u 32u 32u 4P 32u 8P 16u 16u 16u 2P" + else: + atom_format = "64U 64U 32u 4P 64U 8P 16u 16u 16u 2P" + writer.build(atom_format, + (self.created_utc_date, self.modified_utc_date, + self.track_id, self.duration, self.video_layer, + self.qt_alternate, self.volume)) + writer.build("32u" * 9, self.geometry_matrices) + writer.build("32u 32u", (self.video_width, self.video_height)) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + if (self.version == 0): + return 84 + else: + return 96 + + +class M4A_MDHD_Atom(M4A_Leaf_Atom): + def __init__(self, version, flags, created_utc_date, modified_utc_date, + sample_rate, track_length, language, quality): + self.name = 'mdhd' + self.version = version + self.flags = flags + self.created_utc_date = created_utc_date + self.modified_utc_date = modified_utc_date + self.sample_rate = sample_rate + self.track_length = track_length + self.language = language + self.quality = quality + + def __repr__(self): + return "M4A_MDHD_Atom(%s)" % \ + (",".join(map(repr, + [self.version, self.flags, self.created_utc_date, + self.modified_utc_date, self.sample_rate, + self.track_length, self.language, self.quality]))) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + assert(name == 'mdhd') + (version, flags) = reader.parse("8u 24u") + if (version == 0): + atom_format = "32u 32u 32u 32u" + else: + atom_format = "64U 64U 32u 64U" + (created_utc_date, + modified_utc_date, + sample_rate, + track_length) = reader.parse(atom_format) + language = reader.parse("1p 5u 5u 5u") + quality = reader.read(16) + + return cls(version=version, + flags=flags, + created_utc_date=created_utc_date, + modified_utc_date=modified_utc_date, + sample_rate=sample_rate, + track_length=track_length, + language=language, + quality=quality) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("8u 24u", (self.version, self.flags)) + if (self.version == 0): + atom_format = "32u 32u 32u 32u" + else: + atom_format = "64U 64U 32u 64U" + writer.build(atom_format, + (self.created_utc_date, self.modified_utc_date, + self.sample_rate, self.track_length)) + writer.build("1p 5u 5u 5u", self.language) + writer.write(16, self.quality) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + if (self.version == 0): + return 24 + else: + return 36 + + +class M4A_SMHD_Atom(M4A_Leaf_Atom): + def __init__(self, version, flags, audio_balance): + self.name = 'smhd' + self.version = version + self.flags = flags + self.audio_balance = audio_balance + + def __repr__(self): + return "M4A_SMHD_Atom(%s)" % \ + (",".join(map(repr, (self.version, + self.flags, + self.audio_balance)))) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + return cls(*reader.parse("8u 24u 16u 16p")) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("8u 24u 16u 16p", + (self.version, self.flags, self.audio_balance)) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return 8 + + +class M4A_DREF_Atom(M4A_Leaf_Atom): + def __init__(self, version, flags, references): + self.name = 'dref' + self.version = version + self.flags = flags + self.references = references + + def __repr__(self): + return "M4A_DREF_Atom(%s)" % \ + (",".join(map(repr, (self.version, + self.flags, + self.references)))) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + (version, flags, reference_count) = reader.parse("8u 24u 32u") + references = [] + for i in xrange(reference_count): + (leaf_size, leaf_name) = reader.parse("32u 4b") + references.append( + M4A_Leaf_Atom.parse( + leaf_name, leaf_size - 8, + reader.substream(leaf_size - 8), {})) + return cls(version, flags, references) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("8u 24u 32u", (self.version, + self.flags, + len(self.references))) + + for reference_atom in self.references: + writer.build("32u 4b", (reference_atom.size() + 8, + reference_atom.name)) + reference_atom.build(writer) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return 8 + sum([reference_atom.size() + 8 + for reference_atom in self.references]) + + +class M4A_STSD_Atom(M4A_Leaf_Atom): + def __init__(self, version, flags, descriptions): + self.name = 'stsd' + self.version = version + self.flags = flags + self.descriptions = descriptions + + def __repr__(self): + return "M4A_STSD_Atom(%s, %s, %s)" % \ + (repr(self.version), repr(self.flags), repr(self.descriptions)) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + (version, flags, description_count) = reader.parse("8u 24u 32u") + descriptions = [] + for i in xrange(description_count): + (leaf_size, leaf_name) = reader.parse("32u 4b") + descriptions.append( + parsers.get(leaf_name, M4A_Leaf_Atom).parse( + leaf_name, + leaf_size - 8, + reader.substream(leaf_size - 8), + parsers)) + return cls(version=version, + flags=flags, + descriptions=descriptions) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("8u 24u 32u", (self.version, + self.flags, + len(self.descriptions))) + + for description_atom in self.descriptions: + writer.build("32u 4b", (description_atom.size() + 8, + description_atom.name)) + description_atom.build(writer) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return 8 + sum([8 + description_atom.size() + for description_atom in self.descriptions]) + + +class M4A_STTS_Atom(M4A_Leaf_Atom): + def __init__(self, version, flags, times): + self.name = 'stts' + self.version = version + self.flags = flags + self.times = times + + def __repr__(self): + return "M4A_STTS_Atom(%s, %s, %s)" % \ + (repr(self.version), repr(self.flags), repr(self.times)) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + (version, flags) = reader.parse("8u 24u") + return cls(version=version, + flags=flags, + times=[tuple(reader.parse("32u 32u")) + for i in xrange(reader.read(32))]) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("8u 24u 32u", (self.version, self.flags, len(self.times))) + for time in self.times: + writer.build("32u 32u", time) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return 8 + (8 * len(self.times)) + + +class M4A_STSC_Atom(M4A_Leaf_Atom): + def __init__(self, version, flags, blocks): + self.name = 'stsc' + self.version = version + self.flags = flags + self.blocks = blocks + + def __repr__(self): + return "M4A_STSC_Atom(%s, %s, %s)" % \ + (repr(self.version), repr(self.flags), repr(self.blocks)) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + (version, flags) = reader.parse("8u 24u") + return cls(version=version, + flags=flags, + blocks=[tuple(reader.parse("32u 32u 32u")) + for i in xrange(reader.read(32))]) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("8u 24u 32u", + (self.version, self.flags, len(self.blocks))) + for block in self.blocks: + writer.build("32u 32u 32u", block) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return 8 + (12 * len(self.blocks)) + + +class M4A_STSZ_Atom(M4A_Leaf_Atom): + def __init__(self, version, flags, byte_size, block_sizes): + self.name = 'stsz' + self.version = version + self.flags = flags + self.byte_size = byte_size + self.block_sizes = block_sizes + + def __repr__(self): + return "M4A_STSZ_Atom(%s, %s, %s, %s)" % \ + (repr(self.version), repr(self.flags), repr(self.byte_size), + repr(self.block_sizes)) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + (version, flags, byte_size) = reader.parse("8u 24u 32u") + return cls(version=version, + flags=flags, + byte_size=byte_size, + block_sizes=[reader.read(32) for i in + xrange(reader.read(32))]) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("8u 24u 32u 32u", (self.version, + self.flags, + self.byte_size, + len(self.block_sizes))) + for size in self.block_sizes: + writer.write(32, size) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return 12 + (4 * len(self.block_sizes)) + + +class M4A_STCO_Atom(M4A_Leaf_Atom): + def __init__(self, version, flags, offsets): + self.name = 'stco' + self.version = version + self.flags = flags + self.offsets = offsets + + def __repr__(self): + return "M4A_STCO_Atom(%s, %s, %s)" % \ + (self.version, self.flags, self.offsets) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + assert(name == "stco") + (version, flags, offset_count) = reader.parse("8u 24u 32u") + return cls(version, flags, + [reader.read(32) for i in xrange(offset_count)]) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("8u 24u 32u", (self.version, self.flags, + len(self.offsets))) + for offset in self.offsets: + writer.write(32, offset) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return 8 + (4 * len(self.offsets)) + + +class M4A_ALAC_Atom(M4A_Leaf_Atom): + def __init__(self, reference_index, qt_version, qt_revision_level, + qt_vendor, channels, bits_per_sample, qt_compression_id, + audio_packet_size, sample_rate, sub_alac): + self.name = 'alac' + self.reference_index = reference_index + self.qt_version = qt_version + self.qt_revision_level = qt_revision_level + self.qt_vendor = qt_vendor + self.channels = channels + self.bits_per_sample = bits_per_sample + self.qt_compression_id = qt_compression_id + self.audio_packet_size = audio_packet_size + self.sample_rate = sample_rate + self.sub_alac = sub_alac + + def __repr__(self): + return "M4A_ALAC_Atom(%s)" % \ + ",".join(map(repr, [self.reference_index, + self.qt_version, + self.qt_revision_level, + self.qt_vendor, + self.channels, + self.bits_per_sample, + self.qt_compression_id, + self.audio_packet_size, + self.sample_rate, + self.sub_alac])) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + (reference_index, + qt_version, + qt_revision_level, + qt_vendor, + channels, + bits_per_sample, + qt_compression_id, + audio_packet_size, + sample_rate) = reader.parse( + "6P 16u 16u 16u 4b 16u 16u 16u 16u 32u") + (sub_alac_size, sub_alac_name) = reader.parse("32u 4b") + sub_alac = M4A_SUB_ALAC_Atom.parse(sub_alac_name, + sub_alac_size - 8, + reader.substream(sub_alac_size - 8), + {}) + return cls(reference_index=reference_index, + qt_version=qt_version, + qt_revision_level=qt_revision_level, + qt_vendor=qt_vendor, + channels=channels, + bits_per_sample=bits_per_sample, + qt_compression_id=qt_compression_id, + audio_packet_size=audio_packet_size, + sample_rate=sample_rate, + sub_alac=sub_alac) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("6P 16u 16u 16u 4b 16u 16u 16u 16u 32u", + (self.reference_index, + self.qt_version, + self.qt_revision_level, + self.qt_vendor, + self.channels, + self.bits_per_sample, + self.qt_compression_id, + self.audio_packet_size, + self.sample_rate)) + writer.build("32u 4b", (self.sub_alac.size() + 8, + self.sub_alac.name)) + self.sub_alac.build(writer) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return 28 + 8 + self.sub_alac.size() + + +class M4A_SUB_ALAC_Atom(M4A_Leaf_Atom): + def __init__(self, max_samples_per_frame, bits_per_sample, + history_multiplier, initial_history, maximum_k, + channels, unknown, max_coded_frame_size, bitrate, + sample_rate): + self.name = 'alac' + self.max_samples_per_frame = max_samples_per_frame + self.bits_per_sample = bits_per_sample + self.history_multiplier = history_multiplier + self.initial_history = initial_history + self.maximum_k = maximum_k + self.channels = channels + self.unknown = unknown + self.max_coded_frame_size = max_coded_frame_size + self.bitrate = bitrate + self.sample_rate = sample_rate + + def __repr__(self): + return "M4A_SUB_ALAC_Atom(%s)" % \ + (",".join(map(repr, [self.max_samples_per_frame, + self.bits_per_sample, + self.history_multiplier, + self.initial_history, + self.maximum_k, + self.channels, + self.unknown, + self.max_coded_frame_size, + self.bitrate, + self.sample_rate]))) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + return cls( + *reader.parse( + "4P 32u 8p 8u 8u 8u 8u 8u 16u 32u 32u 32u")) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("4P 32u 8p 8u 8u 8u 8u 8u 16u 32u 32u 32u", + (self.max_samples_per_frame, + self.bits_per_sample, + self.history_multiplier, + self.initial_history, + self.maximum_k, + self.channels, + self.unknown, + self.max_coded_frame_size, + self.bitrate, + self.sample_rate)) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return 28 + + +class M4A_META_Atom(MetaData, M4A_Tree_Atom): + UNICODE_ATTRIB_TO_ILST = {"track_name": "\xa9nam", + "album_name": "\xa9alb", + "artist_name": "\xa9ART", + "composer_name": "\xa9wrt", + "copyright": "cprt", + "year": "\xa9day", + "comment": "\xa9cmt"} + + INT_ATTRIB_TO_ILST = {"track_number": "trkn", + "album_number": "disk"} + + TOTAL_ATTRIB_TO_ILST = {"track_total": "trkn", + "album_total": "disk"} + + def __init__(self, version, flags, leaf_atoms): + M4A_Tree_Atom.__init__(self, "meta", leaf_atoms) + self.__dict__["version"] = version + self.__dict__["flags"] = flags + + def __repr__(self): + return "M4A_META_Atom(%s, %s, %s)" % \ + (repr(self.version), repr(self.flags), repr(self.leaf_atoms)) + + def has_ilst_atom(self): + """returns True if this atom contains an ILST sub-atom""" + + for a in self.leaf_atoms: + if (a.name == 'ilst'): + return True + else: + return False + + def ilst_atom(self): + """returns the first ILST sub-atom, or None""" + + for a in self.leaf_atoms: + if (a.name == 'ilst'): + return a + else: + return None + + def add_ilst_atom(self): + """place new ILST atom after the first HDLR atom, if any""" + + for (index, atom) in enumerate(self.leaf_atoms): + if (atom.name == 'hdlr'): + self.leaf_atoms.insert(index, M4A_Tree_Atom('ilst', [])) + break + else: + self.leaf_atoms.append(M4A_Tree_Atom('ilst', [])) + + def raw_info(self): + """returns a Unicode string of low-level MetaData information + + whereas __unicode__ is meant to contain complete information + at a very high level + raw_info() should be more developer-specific and with + very little adjustment or reordering to the data itself + """ + + from os import linesep + from . import display_unicode + + if (self.has_ilst_atom()): + comment_lines = [u"M4A:"] + + for atom in self.ilst_atom(): + if (hasattr(atom, "raw_info_lines")): + comment_lines.extend(atom.raw_info_lines()) + else: + comment_lines.append(u"%s : (%d bytes)" % + (atom.name.decode('ascii', 'replace'), + atom.size())) + + return linesep.decode('ascii').join(comment_lines) + else: + return u"" + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + assert(name == "meta") + (version, flags) = reader.parse("8u 24u") + return cls(version, flags, + parse_sub_atoms(data_size - 4, reader, parsers)) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("8u 24u", (self.version, self.flags)) + for sub_atom in self: + writer.build("32u 4b", (sub_atom.size() + 8, sub_atom.name)) + sub_atom.build(writer) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return 4 + sum([8 + sub_atom.size() for sub_atom in self]) + + def __getattr__(self, attr): + if (attr in self.UNICODE_ATTRIB_TO_ILST): + if (self.has_ilst_atom()): + try: + return unicode( + self.ilst_atom()[ + self.UNICODE_ATTRIB_TO_ILST[attr]]['data']) + except KeyError: + return None + else: + return None + elif (attr in self.INT_ATTRIB_TO_ILST): + if (self.has_ilst_atom()): + try: + return self.ilst_atom()[ + self.INT_ATTRIB_TO_ILST[attr]]['data'].number() + except KeyError: + return None + else: + return None + elif (attr in self.TOTAL_ATTRIB_TO_ILST): + if (self.has_ilst_atom()): + try: + return self.ilst_atom()[ + self.TOTAL_ATTRIB_TO_ILST[attr]]['data'].total() + except KeyError: + return None + else: + return None + elif (attr in self.FIELDS): + return None + else: + raise AttributeError(attr) + + def __setattr__(self, attr, value): + def new_data_atom(attribute, value): + if (attribute in self.UNICODE_ATTRIB_TO_ILST): + return M4A_ILST_Unicode_Data_Atom(0, 1, value.encode('utf-8')) + elif (attribute == "track_number"): + return M4A_ILST_TRKN_Data_Atom(int(value), 0) + elif (attribute == "track_total"): + return M4A_ILST_TRKN_Data_Atom(0, int(value)) + elif (attribute == "album_number"): + return M4A_ILST_DISK_Data_Atom(int(value), 0) + elif (attribute == "album_total"): + return M4A_ILST_DISK_Data_Atom(0, int(value)) + else: + raise ValueError(value) + + def replace_data_atom(attribute, parent_atom, value): + new_leaf_atoms = [] + data_replaced = False + for leaf_atom in parent_atom.leaf_atoms: + if ((leaf_atom.name == 'data') and (not data_replaced)): + if (attribute == "track_number"): + new_leaf_atoms.append( + M4A_ILST_TRKN_Data_Atom(int(value), + leaf_atom.track_total)) + elif (attribute == "track_total"): + new_leaf_atoms.append( + M4A_ILST_TRKN_Data_Atom(leaf_atom.track_number, + int(value))) + elif (attribute == "album_number"): + new_leaf_atoms.append( + M4A_ILST_DISK_Data_Atom(int(value), + leaf_atom.disk_total)) + elif (attribute == "album_total"): + new_leaf_atoms.append( + M4A_ILST_DISK_Data_Atom(leaf_atom.disk_number, + int(value))) + else: + new_leaf_atoms.append(new_data_atom(attribute, value)) + + data_replaced = True + else: + new_leaf_atoms.append(leaf_atom) + + parent_atom.leaf_atoms = new_leaf_atoms + + if (value is None): + delattr(self, attr) + return + + ilst_leaf = self.UNICODE_ATTRIB_TO_ILST.get( + attr, + self.INT_ATTRIB_TO_ILST.get( + attr, + self.TOTAL_ATTRIB_TO_ILST.get( + attr, + None))) + + if (ilst_leaf is not None): + if (not self.has_ilst_atom()): + self.add_ilst_atom() + + #an ilst atom is present, so check its sub-atoms + for ilst_atom in self.ilst_atom(): + if (ilst_atom.name == ilst_leaf): + #atom already present, so adjust its data sub-atom + replace_data_atom(attr, ilst_atom, value) + break + else: + #atom not present, so append new parent and data sub-atom + self.ilst_atom().add_child( + M4A_ILST_Leaf_Atom(ilst_leaf, + [new_data_atom(attr, value)])) + else: + #attribute is not an atom, so pass it through + self.__dict__[attr] = value + return + + def __delattr__(self, attr): + if (self.has_ilst_atom()): + ilst_atom = self.ilst_atom() + + if (attr in self.UNICODE_ATTRIB_TO_ILST): + ilst_atom.leaf_atoms = filter( + lambda atom: atom.name != + self.UNICODE_ATTRIB_TO_ILST[attr], + ilst_atom) + elif (attr == "track_number"): + if (self.track_total is None): + #if track_number and track_total are both 0 + #remove trkn atom + ilst_atom.leaf_atoms = filter( + lambda atom: atom.name != "trkn", ilst_atom) + else: + self.track_number = 0 + elif (attr == "track_total"): + if (self.track_number is None): + #if track_number and track_total are both 0 + #remove trkn atom + ilst_atom.leaf_atoms = filter( + lambda atom: atom.name != "trkn", ilst_atom) + else: + self.track_total = 0 + elif (attr == "album_number"): + if (self.album_total is None): + #if album_number and album_total are both 0 + #remove disk atom + ilst_atom.leaf_atoms = filter( + lambda atom: atom.name != "disk", ilst_atom) + else: + self.album_number = 0 + elif (attr == "album_total"): + if (self.album_number is None): + #if album_number and album_total are both 0 + #remove disk atom + ilst_atom.leaf_atoms = filter( + lambda atom: atom.name != "disk", ilst_atom) + else: + self.album_total = 0 + else: + try: + del(self.__dict__[attr]) + except KeyError: + raise AttributeError(attr) + + def images(self): + """returns a list of embedded Image objects""" + + if (self.has_ilst_atom()): + return [atom['data'] for atom in self.ilst_atom() + if ((atom.name == 'covr') and (atom.has_child('data')))] + else: + return [] + + def add_image(self, image): + """embeds an Image object in this metadata""" + + if (not self.has_ilst_atom()): + self.add_ilst_atom() + + ilst_atom = self.ilst_atom() + + #filter out old cover image before adding new one + ilst_atom.leaf_atoms = filter( + lambda atom: not ((atom.name == 'covr') and + (atom.has_child('data'))), + ilst_atom) + [M4A_ILST_Leaf_Atom( + 'covr', + [M4A_ILST_COVR_Data_Atom.converted(image)])] + + def delete_image(self, image): + """deletes an Image object from this metadata""" + + if (self.has_ilst_atom()): + ilst_atom = self.ilst_atom() + + ilst_atom.leaf_atoms = filter( + lambda atom: not ((atom.name == 'covr') and + (atom.has_child('data')) and + (atom['data'].data == image.data)), + ilst_atom) + + @classmethod + def converted(cls, metadata): + """converts metadata from another class to this one, if necessary + + takes a MetaData-compatible object (or None) + and returns a new MetaData subclass with the data fields converted""" + + if (metadata is None): + return None + elif (isinstance(metadata, cls)): + return cls(metadata.version, + metadata.flags, + [leaf.copy() for leaf in metadata]) + + ilst_atoms = [ + M4A_ILST_Leaf_Atom( + cls.UNICODE_ATTRIB_TO_ILST[attrib], + [M4A_ILST_Unicode_Data_Atom(0, 1, value.encode('utf-8'))]) + for (attrib, value) in metadata.filled_fields() + if (attrib in cls.UNICODE_ATTRIB_TO_ILST)] + + if (((metadata.track_number is not None) or + (metadata.track_total is not None))): + ilst_atoms.append( + M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(metadata.track_number if + (metadata.track_number + is not None) else 0, + metadata.track_total if + (metadata.track_total + is not None) else 0)])) + + if (((metadata.album_number is not None) or + (metadata.album_total is not None))): + ilst_atoms.append( + M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_DISK_Data_Atom(metadata.album_number if + (metadata.album_number + is not None) else 0, + metadata.album_total if + (metadata.album_total + is not None) else 0)])) + + if (len(metadata.front_covers()) > 0): + ilst_atoms.append( + M4A_ILST_Leaf_Atom( + 'covr', + [M4A_ILST_COVR_Data_Atom.converted( + metadata.front_covers()[0])])) + + ilst_atoms.append( + M4A_ILST_Leaf_Atom( + 'cpil', + [M4A_Leaf_Atom('data', + '\x00\x00\x00\x15\x00\x00\x00\x00\x01')])) + + return cls(0, 0, [M4A_HDLR_Atom(0, 0, '\x00\x00\x00\x00', + 'mdir', 'appl', 0, 0, '', 0), + M4A_Tree_Atom('ilst', ilst_atoms), + M4A_FREE_Atom(1024)]) + + @classmethod + def supports_images(self): + """returns True""" + + return True + + def clean(self, fixes_performed): + """returns a new MetaData object that's been cleaned of problems + + any fixes performed are appended to fixes_performed as Unicode""" + + def cleaned_atom(atom): + #numerical fields are stored in bytes, + #so no leading zeroes are possible + + #image fields don't store metadata, + #so no field problems are possible there either + + if (atom.name in self.UNICODE_ATTRIB_TO_ILST.values()): + text = atom['data'].data.decode('utf-8') + fix1 = text.rstrip() + if (fix1 != text): + from .text import CLEAN_REMOVE_TRAILING_WHITESPACE + fixes_performed.append( + CLEAN_REMOVE_TRAILING_WHITESPACE % + {"field": atom.name.lstrip('\xa9').decode('ascii')}) + fix2 = fix1.lstrip() + if (fix2 != fix1): + from .text import CLEAN_REMOVE_LEADING_WHITESPACE + fixes_performed.append( + CLEAN_REMOVE_LEADING_WHITESPACE % + {"field": atom.name.lstrip('\xa9').decode('ascii')}) + if (len(fix2) > 0): + return M4A_ILST_Leaf_Atom( + atom.name, + [M4A_ILST_Unicode_Data_Atom(0, 1, + fix2.encode('utf-8'))]) + else: + from .text import CLEAN_REMOVE_EMPTY_TAG + fixes_performed.append( + CLEAN_REMOVE_EMPTY_TAG % + {"field": atom.name.lstrip('\xa9').decode('ascii')}) + return None + else: + return atom + + if (self.has_ilst_atom()): + return M4A_META_Atom( + self.version, + self.flags, + [M4A_Tree_Atom('ilst', + filter(lambda atom: atom is not None, + map(cleaned_atom, self.ilst_atom())))]) + else: + #if no ilst atom, return a copy of the meta atom as-is + return M4A_META_Atom( + self.version, + self.flags, + [M4A_Tree_Atom('ilst', + [atom.copy() for atom in self.ilst_atom()])]) + + +class M4A_ILST_Leaf_Atom(M4A_Tree_Atom): + def copy(self): + """returns a newly copied instance of this atom + and new instances of any sub-atoms it contains""" + + return M4A_ILST_Leaf_Atom(self.name, [leaf.copy() for leaf in self]) + + def __repr__(self): + return "M4A_ILST_Leaf_Atom(%s, %s)" % \ + (repr(self.name), repr(self.leaf_atoms)) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + return cls( + name, + parse_sub_atoms(data_size, reader, + {"data": {"\xa9alb": M4A_ILST_Unicode_Data_Atom, + "\xa9ART": M4A_ILST_Unicode_Data_Atom, + "\xa9cmt": M4A_ILST_Unicode_Data_Atom, + "cprt": M4A_ILST_Unicode_Data_Atom, + "\xa9day": M4A_ILST_Unicode_Data_Atom, + "\xa9grp": M4A_ILST_Unicode_Data_Atom, + "\xa9nam": M4A_ILST_Unicode_Data_Atom, + "\xa9too": M4A_ILST_Unicode_Data_Atom, + "\xa9wrt": M4A_ILST_Unicode_Data_Atom, + 'aART': M4A_ILST_Unicode_Data_Atom, + "covr": M4A_ILST_COVR_Data_Atom, + "trkn": M4A_ILST_TRKN_Data_Atom, + "disk": M4A_ILST_DISK_Data_Atom + }.get(name, M4A_Leaf_Atom)})) + + def __unicode__(self): + try: + return unicode(filter(lambda f: f.name == 'data', + self.leaf_atoms)[0]) + except IndexError: + return u"" + + def raw_info_lines(self): + """yields lines of human-readable information about the atom""" + + for leaf_atom in self.leaf_atoms: + name = self.name.replace("\xa9", " ").decode('ascii') + if (hasattr(leaf_atom, "raw_info")): + yield u"%s : %s" % (name, leaf_atom.raw_info()) + else: + yield u"%s : %s" % (name, repr(leaf_atom)) # FIXME + + +class M4A_ILST_Unicode_Data_Atom(M4A_Leaf_Atom): + def __init__(self, type, flags, data): + self.name = "data" + self.type = type + self.flags = flags + self.data = data + + def copy(self): + """returns a newly copied instance of this atom + and new instances of any sub-atoms it contains""" + + return M4A_ILST_Unicode_Data_Atom(self.type, self.flags, self.data) + + def __repr__(self): + return "M4A_ILST_Unicode_Data_Atom(%s, %s, %s)" % \ + (repr(self.type), repr(self.flags), repr(self.data)) + + def __eq__(self, atom): + for attr in ["type", "flags", "data"]: + if ((not hasattr(atom, attr)) or (getattr(self, attr) != + getattr(atom, attr))): + return False + else: + return True + + def raw_info(self): + """returns a line of human-readable information about the atom""" + + return self.data.decode('utf-8') + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + assert(name == "data") + (type, flags) = reader.parse("8u 24u 32p") + return cls(type, flags, reader.read_bytes(data_size - 8)) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("8u 24u 32p %db" % (len(self.data)), + (self.type, self.flags, self.data)) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return 8 + len(self.data) + + def __unicode__(self): + return self.data.decode('utf-8') + + +class M4A_ILST_TRKN_Data_Atom(M4A_Leaf_Atom): + def __init__(self, track_number, track_total): + self.name = "data" + self.track_number = track_number + self.track_total = track_total + + def copy(self): + """returns a newly copied instance of this atom + and new instances of any sub-atoms it contains""" + + return M4A_ILST_TRKN_Data_Atom(self.track_number, self.track_total) + + def __repr__(self): + return "M4A_ILST_TRKN_Data_Atom(%d, %d)" % \ + (self.track_number, self.track_total) + + def __eq__(self, atom): + for attr in ["track_number", "track_total"]: + if ((not hasattr(atom, attr)) or (getattr(self, attr) != + getattr(atom, attr))): + return False + else: + return True + + def __unicode__(self): + if (self.track_total > 0): + return u"%d/%d" % (self.track_number, self.track_total) + else: + return unicode(self.track_number) + + def raw_info(self): + """returns a line of human-readable information about the atom""" + + return u"%d/%d" % (self.track_number, self.track_total) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + assert(name == "data") + #FIXME - handle mis-sized TRKN data atoms + return cls(*reader.parse("64p 16p 16u 16u 16p")) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("64p 16p 16u 16u 16p", + (self.track_number, self.track_total)) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return 16 + + def number(self): + """returns this atom's track_number field + or None if the field is 0""" + + if (self.track_number != 0): + return self.track_number + else: + return None + + def total(self): + """returns this atom's track_total field + or None if the field is 0""" + + if (self.track_total != 0): + return self.track_total + else: + return None + + +class M4A_ILST_DISK_Data_Atom(M4A_Leaf_Atom): + def __init__(self, disk_number, disk_total): + self.name = "data" + self.disk_number = disk_number + self.disk_total = disk_total + + def copy(self): + """returns a newly copied instance of this atom + and new instances of any sub-atoms it contains""" + + return M4A_ILST_DISK_Data_Atom(self.disk_number, self.disk_total) + + def __repr__(self): + return "M4A_ILST_DISK_Data_Atom(%d, %d)" % \ + (self.disk_number, self.disk_total) + + def __eq__(self, atom): + for attr in ["disk_number", "disk_total"]: + if ((not hasattr(atom, attr)) or (getattr(self, attr) != + getattr(atom, attr))): + return False + else: + return True + + def __unicode__(self): + if (self.disk_total > 0): + return u"%d/%d" % (self.disk_number, self.disk_total) + else: + return unicode(self.disk_number) + + def raw_info(self): + """returns a line of human-readable information about the atom""" + + return u"%d/%d" % (self.disk_number, self.disk_total) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + assert(name == "data") + #FIXME - handle mis-sized DISK data atoms + return cls(*reader.parse("64p 16p 16u 16u")) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("64p 16p 16u 16u", + (self.disk_number, self.disk_total)) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return 14 + + def number(self): + """returns this atom's disc_number field""" + + if (self.disk_number != 0): + return self.disk_number + else: + return None + + def total(self): + """returns this atom's disk_total field""" + + if (self.disk_total != 0): + return self.disk_total + else: + return None + + +class M4A_ILST_COVR_Data_Atom(Image, M4A_Leaf_Atom): + def __init__(self, version, flags, image_data): + self.version = version + self.flags = flags + self.name = "data" + + img = image_metrics(image_data) + Image.__init__(self, + data=image_data, + mime_type=img.mime_type, + width=img.width, + height=img.height, + color_depth=img.bits_per_pixel, + color_count=img.color_count, + description=u"", + type=0) + + def copy(self): + """returns a newly copied instance of this atom + and new instances of any sub-atoms it contains""" + + return M4A_ILST_COVR_Data_Atom(self.version, self.flags, self.data) + + def __repr__(self): + return "M4A_ILST_COVR_Data_Atom(%s, %s, ...)" % \ + (self.version, self.flags) + + def raw_info(self): + """returns a line of human-readable information about the atom""" + + if (len(self.data) > 20): + return (u"(%d bytes) " % (len(self.data)) + + u"".join([u"%2.2X" % (ord(b)) for b in self.data[0:20]]) + + u"\u2026") + else: + return (u"(%d bytes) " % (len(self.data)) + + u"".join([u"%2.2X" % (ord(b)) for b in self.data[0:20]])) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + assert(name == "data") + (version, flags) = reader.parse("8u 24u 32p") + return cls(version, flags, reader.read_bytes(data_size - 8)) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("8u 24u 32p %db" % (len(self.data)), + (self.version, self.flags, self.data)) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return 8 + len(self.data) + + @classmethod + def converted(cls, image): + """given an Image-compatible object, + returns a new M4A_ILST_COVR_Data_Atom object""" + + return cls(0, 0, image.data) + + +class M4A_HDLR_Atom(M4A_Leaf_Atom): + def __init__(self, version, flags, qt_type, qt_subtype, + qt_manufacturer, qt_reserved_flags, qt_reserved_flags_mask, + component_name, padding_size): + self.name = 'hdlr' + self.version = version + self.flags = flags + self.qt_type = qt_type + self.qt_subtype = qt_subtype + self.qt_manufacturer = qt_manufacturer + self.qt_reserved_flags = qt_reserved_flags + self.qt_reserved_flags_mask = qt_reserved_flags_mask + self.component_name = component_name + self.padding_size = padding_size + + def copy(self): + """returns a newly copied instance of this atom + and new instances of any sub-atoms it contains""" + + return M4A_HDLR_Atom(self.version, + self.flags, + self.qt_type, + self.qt_subtype, + self.qt_manufacturer, + self.qt_reserved_flags, + self.qt_reserved_flags_mask, + self.component_name, + self.padding_size) + + def __repr__(self): + return "M4A_HDLR_Atom(%s, %s, %s, %s, %s, %s, %s, %s, %d)" % \ + (self.version, self.flags, repr(self.qt_type), + repr(self.qt_subtype), repr(self.qt_manufacturer), + self.qt_reserved_flags, self.qt_reserved_flags_mask, + repr(self.component_name), self.padding_size) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + assert(name == 'hdlr') + (version, + flags, + qt_type, + qt_subtype, + qt_manufacturer, + qt_reserved_flags, + qt_reserved_flags_mask) = reader.parse( + "8u 24u 4b 4b 4b 32u 32u") + component_name = reader.read_bytes(reader.read(8)) + return cls(version, flags, qt_type, qt_subtype, + qt_manufacturer, qt_reserved_flags, + qt_reserved_flags_mask, component_name, + data_size - len(component_name) - 25) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.build("8u 24u 4b 4b 4b 32u 32u 8u %db %dP" % + (len(self.component_name), + self.padding_size), + (self.version, + self.flags, + self.qt_type, + self.qt_subtype, + self.qt_manufacturer, + self.qt_reserved_flags, + self.qt_reserved_flags_mask, + len(self.component_name), + self.component_name)) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return 25 + len(self.component_name) + self.padding_size + + +class M4A_FREE_Atom(M4A_Leaf_Atom): + def __init__(self, bytes): + self.name = "free" + self.bytes = bytes + + def copy(self): + """returns a newly copied instance of this atom + and new instances of any sub-atoms it contains""" + + return M4A_FREE_Atom(self.bytes) + + def __repr__(self): + return "M4A_FREE_Atom(%d)" % (self.bytes) + + @classmethod + def parse(cls, name, data_size, reader, parsers): + """given a 4 byte name, data_size int, BitstreamReader + and dict of {"atom":handler} sub-parsers, + returns an atom of this class""" + + assert(name == "free") + reader.skip_bytes(data_size) + return cls(data_size) + + def build(self, writer): + """writes the atom to the given BitstreamWriter + not including its 64-bit size / name header""" + + writer.write_bytes(chr(0) * self.bytes) + + def size(self): + """returns the atom's size + not including its 64-bit size / name header""" + + return self.bytes
View file
audiotools-2.19.tar.gz/audiotools/mp3.py
Added
@@ -0,0 +1,808 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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) + + +####################### +#MP3 +####################### + + +class InvalidMP3(InvalidFile): + """raised by invalid files during MP3 initialization""" + + pass + + +class MP3Audio(AudioFile): + """an MP3 audio file""" + + from .text import (COMP_LAME_0, + COMP_LAME_6, + COMP_LAME_MEDIUM, + COMP_LAME_STANDARD, + COMP_LAME_EXTREME, + COMP_LAME_INSANE) + + SUFFIX = "mp3" + NAME = SUFFIX + DESCRIPTION = u"MPEG-1 Audio Layer III" + DEFAULT_COMPRESSION = "2" + #0 is better quality/lower compression + #9 is worse quality/higher compression + COMPRESSION_MODES = ("0", "1", "2", "3", "4", "5", "6", + "medium", "standard", "extreme", "insane") + COMPRESSION_DESCRIPTIONS = {"0": COMP_LAME_0, + "6": COMP_LAME_6, + "medium": COMP_LAME_MEDIUM, + "standard": COMP_LAME_STANDARD, + "extreme": COMP_LAME_EXTREME, + "insane": COMP_LAME_INSANE} + BINARIES = ("lame", "mpg123") + REPLAYGAIN_BINARIES = ("mp3gain", ) + + SAMPLE_RATE = ((11025, 12000, 8000, None), # MPEG-2.5 + (None, None, None, None), # reserved + (22050, 24000, 16000, None), # MPEG-2 + (44100, 48000, 32000, None)) # MPEG-1 + + BIT_RATE = ( + #MPEG-2.5 + ( + #reserved + (None,) * 16, + #layer III + (None, 8000, 16000, 24000, 32000, 40000, 48000, 56000, + 64000, 80000, 96000, 112000, 128000, 144000, 160000, None), + #layer II + (None, 8000, 16000, 24000, 32000, 40000, 48000, 56000, + 64000, 80000, 96000, 112000, 128000, 144000, 160000, None), + #layer I + (None, 32000, 48000, 56000, 64000, 80000, 96000, 112000, + 128000, 144000, 160000, 176000, 192000, 224000, 256000, None), + ), + #reserved + ((None,) * 16, ) * 4, + #MPEG-2 + ( + #reserved + (None,) * 16, + #layer III + (None, 8000, 16000, 24000, 32000, 40000, 48000, 56000, + 64000, 80000, 96000, 112000, 128000, 144000, 160000, None), + #layer II + (None, 8000, 16000, 24000, 32000, 40000, 48000, 56000, + 64000, 80000, 96000, 112000, 128000, 144000, 160000, None), + #layer I + (None, 32000, 48000, 56000, 64000, 80000, 96000, 112000, + 128000, 144000, 160000, 176000, 192000, 224000, 256000, None)), + #MPEG-1 + ( + #reserved + (None,) * 16, + #layer III + (None, 32000, 40000, 48000, 56000, 64000, 80000, 96000, + 112000, 128000, 160000, 192000, 224000, 256000, 320000, None), + #layer II + (None, 32000, 48000, 56000, 64000, 80000, 96000, 112000, + 128000, 160000, 192000, 224000, 256000, 320000, 384000, None), + #layer I + (None, 32000, 64000, 96000, 128000, 160000, 192000, 224000, + 256000, 288000, 320000, 352000, 384000, 416000, 448000, None))) + + PCM_FRAMES_PER_MPEG_FRAME = (None, 1152, 1152, 384) + + def __init__(self, filename): + """filename is a plain string""" + + AudioFile.__init__(self, filename) + + from .bitstream import BitstreamReader + import cStringIO + + try: + mp3file = open(filename, "rb") + except IOError, msg: + raise InvalidMP3(str(msg)) + + try: + try: + header_bytes = MP3Audio.__find_next_mp3_frame__(mp3file) + except IOError: + from .text import ERR_MP3_FRAME_NOT_FOUND + raise InvalidMP3(ERR_MP3_FRAME_NOT_FOUND) + + (frame_sync, + mpeg_id, + layer, + bit_rate, + sample_rate, + pad, + channels) = BitstreamReader(mp3file, 0).parse( + "11u 2u 2u 1p 4u 2u 1u 1p 2u 6p") + + self.__samplerate__ = self.SAMPLE_RATE[mpeg_id][sample_rate] + if (self.__samplerate__ is None): + from .text import ERR_MP3_INVALID_SAMPLE_RATE + raise InvalidMP3(ERR_MP3_INVALID_SAMPLE_RATE) + if (channels in (0, 1, 2)): + self.__channels__ = 2 + else: + self.__channels__ = 1 + + first_frame = mp3file.read(self.frame_length(mpeg_id, + layer, + bit_rate, + sample_rate, + pad) - 4) + + if ("Xing" in first_frame): + #pull length from Xing header, if present + self.__pcm_frames__ = ( + BitstreamReader( + cStringIO.StringIO( + first_frame[first_frame.index("Xing"): + first_frame.index("Xing") + 160]), + 0).parse("32p 32p 32u 32p 832p")[0] * + self.PCM_FRAMES_PER_MPEG_FRAME[layer]) + else: + #otherwise, bounce through file frames + reader = BitstreamReader(mp3file, 0) + self.__pcm_frames__ = 0 + + try: + (frame_sync, + mpeg_id, + layer, + bit_rate, + sample_rate, + pad) = reader.parse("11u 2u 2u 1p 4u 2u 1u 9p") + + while (frame_sync == 0x7FF): + self.__pcm_frames__ += \ + self.PCM_FRAMES_PER_MPEG_FRAME[layer] + + reader.skip_bytes(self.frame_length(mpeg_id, + layer, + bit_rate, + sample_rate, + pad) - 4) + + (frame_sync, + mpeg_id, + layer, + bit_rate, + sample_rate, + pad) = reader.parse("11u 2u 2u 1p 4u 2u 1u 9p") + except IOError: + pass + except ValueError, err: + raise InvalidMP3(unicode(err)) + finally: + mp3file.close() + + def lossless(self): + """returns False""" + + return False + + def to_pcm(self): + """returns a PCMReader object containing the track's PCM data""" + + from . import PCMReader + from . import BIN + from . import ChannelMask + import subprocess + import sys + import os + + BIG_ENDIAN = sys.byteorder == 'big' + + sub = subprocess.Popen([BIN["mpg123"], "-qs", self.filename], + stdout=subprocess.PIPE, + stderr=file(os.devnull, "a")) + + return PCMReader( + sub.stdout, + sample_rate=self.sample_rate(), + channels=self.channels(), + bits_per_sample=16, + channel_mask=int(ChannelMask.from_channels(self.channels())), + process=sub, + big_endian=BIG_ENDIAN) + + @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 MP3Audio object""" + + from . import transfer_framelist_data + from . import BIN + from . import ignore_sigint + from . import EncodingError + from . import DecodingError + from . import ChannelMask + from . import __default_quality__ + import decimal + import bisect + import subprocess + import os + + if (((compression is None) or + (compression not in cls.COMPRESSION_MODES))): + compression = __default_quality__(cls.NAME) + + if ((pcmreader.channels > 2) or (pcmreader.sample_rate not in + (32000, 48000, 44100))): + from . import PCMConverter + + pcmreader = PCMConverter( + pcmreader, + sample_rate=[32000, + 32000, + 44100, + 48000][bisect.bisect([32000, + 44100, + 48000], + pcmreader.sample_rate)], + channels=min(pcmreader.channels, 2), + channel_mask=ChannelMask.from_channels( + min(pcmreader.channels, 2)), + bits_per_sample=16) + + if (pcmreader.channels > 1): + mode = "j" + else: + mode = "m" + + devnull = file(os.devnull, 'ab') + + if (str(compression) in map(str, range(0, 10))): + compression = ["-V" + str(compression)] + else: + compression = ["--preset", str(compression)] + + sub = subprocess.Popen( + [BIN['lame'], "--quiet", + "-r", + "-s", str(decimal.Decimal(pcmreader.sample_rate) / 1000), + "--bitwidth", str(pcmreader.bits_per_sample), + "--signed", "--little-endian", + "-m", mode] + compression + ["-", filename], + stdin=subprocess.PIPE, + stdout=devnull, + stderr=devnull, + preexec_fn=ignore_sigint) + + try: + transfer_framelist_data(pcmreader, sub.stdin.write) + except (IOError, ValueError), err: + sub.stdin.close() + sub.wait() + cls.__unlink__(filename) + raise EncodingError(str(err)) + except Exception, err: + sub.stdin.close() + sub.wait() + cls.__unlink__(filename) + raise err + + try: + pcmreader.close() + except DecodingError, err: + cls.__unlink__(filename) + raise EncodingError(err.error_message) + sub.stdin.close() + + devnull.close() + + if (sub.wait() == 0): + return MP3Audio(filename) + else: + cls.__unlink__(filename) + raise EncodingError(u"error encoding file with lame") + + def bits_per_sample(self): + """returns an integer number of bits-per-sample this track contains""" + + return 16 + + def channels(self): + """returns an integer number of channels this track contains""" + + return self.__channels__ + + def sample_rate(self): + """returns the rate of the track's audio as an integer number of Hz""" + + return self.__samplerate__ + + def get_metadata(self): + """returns a MetaData object, or None + + raises IOError if unable to read the file""" + + from .id3 import ID3CommentPair + from .id3 import read_id3v2_comment + from .id3v1 import ID3v1Comment + + f = file(self.filename, "rb") + try: + if (f.read(3) != "ID3"): # no ID3v2 tag, try ID3v1 + try: + # no ID3v2, yes ID3v1 + return ID3v1Comment.parse(f) + except ValueError: + # no ID3v2, no ID3v1 + return None + else: + id3v2 = read_id3v2_comment(self.filename) + + try: + # yes IDv2, yes ID3v1 + return ID3CommentPair(id3v2, + ID3v1Comment.parse(f)) + except ValueError: + # yes ID3v2, no ID3v1 + return id3v2 + finally: + f.close() + + def update_metadata(self, metadata): + """takes this track's current MetaData object + as returned by get_metadata() and sets this track's metadata + with any fields updated in that object + + raises IOError if unable to write the file + """ + + from .id3 import ID3v2Comment + from .id3 import ID3CommentPair + from .id3v1 import ID3v1Comment + + if (metadata is None): + return + elif (not (isinstance(metadata, ID3v2Comment) or + isinstance(metadata, ID3CommentPair) or + isinstance(metadata, ID3v1Comment))): + from .text import ERR_FOREIGN_METADATA + raise ValueError(ERR_FOREIGN_METADATA) + + #get the original MP3 data + f = file(self.filename, "rb") + MP3Audio.__find_mp3_start__(f) + data_start = f.tell() + MP3Audio.__find_last_mp3_frame__(f) + data_end = f.tell() + f.seek(data_start, 0) + mp3_data = f.read(data_end - data_start) + f.close() + + from .bitstream import BitstreamWriter + + #write id3v2 + data + id3v1 to file + f = file(self.filename, "wb") + if (isinstance(metadata, ID3CommentPair)): + metadata.id3v2.build(BitstreamWriter(f, 0)) + f.write(mp3_data) + metadata.id3v1.build(f) + elif (isinstance(metadata, ID3v2Comment)): + metadata.build(BitstreamWriter(f, 0)) + f.write(mp3_data) + elif (isinstance(metadata, ID3v1Comment)): + f.write(mp3_data) + metadata.build(f) + f.close() + + def set_metadata(self, metadata): + """takes a MetaData object and sets this track's metadata + + this metadata includes track name, album name, and so on + raises IOError if unable to write the file""" + + from .id3 import ID3v2Comment + from .id3 import ID3v22Comment + from .id3 import ID3v23Comment + from .id3 import ID3v24Comment + from .id3 import ID3CommentPair + from .id3v1 import ID3v1Comment + + if (metadata is None): + return + + if (not (isinstance(metadata, ID3v2Comment) or + isinstance(metadata, ID3CommentPair) or + isinstance(metadata, ID3v1Comment))): + from . import config + + DEFAULT_ID3V2 = "id3v2.3" + DEFAULT_ID3V1 = "id3v1.1" + + id3v2_class = {"id3v2.2": ID3v22Comment, + "id3v2.3": ID3v23Comment, + "id3v2.4": ID3v24Comment, + "none": None}.get(config.get_default("ID3", + "id3v2", + DEFAULT_ID3V2), + DEFAULT_ID3V2) + id3v1_class = {"id3v1.1": ID3v1Comment, + "none": None}.get(config.get_default("ID3", + "id3v1", + DEFAULT_ID3V1), + DEFAULT_ID3V1) + if ((id3v2_class is not None) and (id3v1_class is not None)): + self.update_metadata( + ID3CommentPair.converted(metadata, + id3v2_class=id3v2_class, + id3v1_class=id3v1_class)) + elif (id3v2_class is not None): + self.update_metadata(id3v2_class.converted(metadata)) + elif (id3v1_class is not None): + self.update_metadata(id3v1_class.converted(metadata)) + else: + return + else: + self.update_metadata(metadata) + + def delete_metadata(self): + """deletes the track's MetaData + + this removes or unsets tags as necessary in order to remove all data + raises IOError if unable to write the file""" + + #get the original MP3 data + f = file(self.filename, "rb") + MP3Audio.__find_mp3_start__(f) + data_start = f.tell() + MP3Audio.__find_last_mp3_frame__(f) + data_end = f.tell() + f.seek(data_start, 0) + mp3_data = f.read(data_end - data_start) + f.close() + + #write data to file + f = file(self.filename, "wb") + f.write(mp3_data) + f.close() + + #places mp3file at the position of the next MP3 frame's start + @classmethod + def __find_next_mp3_frame__(cls, mp3file): + from .id3 import skip_id3v2_comment + + #if we're starting at an ID3v2 header, skip it to save a bunch of time + bytes_skipped = skip_id3v2_comment(mp3file) + + #then find the next mp3 frame + from .bitstream import BitstreamReader + + reader = BitstreamReader(mp3file, 0) + reader.mark() + try: + (sync, + mpeg_id, + layer_description) = reader.parse("11u 2u 2u 1p") + except IOError, err: + reader.unmark() + raise err + + while (not ((sync == 0x7FF) and + (mpeg_id in (0, 2, 3)) and + (layer_description in (1, 2, 3)))): + reader.rewind() + reader.unmark() + reader.skip(8) + bytes_skipped += 1 + reader.mark() + try: + (sync, + mpeg_id, + layer_description) = reader.parse("11u 2u 2u 1p") + except IOError, err: + reader.unmark() + raise err + else: + reader.rewind() + reader.unmark() + return bytes_skipped + + @classmethod + def __find_mp3_start__(cls, mp3file): + """places mp3file at the position of the MP3 file's start""" + + from .id3 import skip_id3v2_comment + + #if we're starting at an ID3v2 header, skip it to save a bunch of time + skip_id3v2_comment(mp3file) + + from .bitstream import BitstreamReader + + reader = BitstreamReader(mp3file, 0) + + #skip over any bytes that aren't a valid MPEG header + reader.mark() + (frame_sync, mpeg_id, layer) = reader.parse("11u 2u 2u 1p") + while (not ((frame_sync == 0x7FF) and + (mpeg_id in (0, 2, 3)) and + (layer in (1, 2, 3)))): + reader.rewind() + reader.unmark() + reader.skip(8) + reader.mark() + reader.rewind() + reader.unmark() + + @classmethod + def __find_last_mp3_frame__(cls, mp3file): + """places mp3file at the position of the last MP3 frame's end + + (either the last byte in the file or just before the ID3v1 tag) + this may not be strictly accurate if ReplayGain data is present, + since APEv2 tags came before the ID3v1 tag, + but we're not planning to change that tag anyway + """ + + mp3file.seek(-128, 2) + if (mp3file.read(3) == 'TAG'): + mp3file.seek(-128, 2) + return + else: + mp3file.seek(0, 2) + return + + def frame_length(self, mpeg_id, layer, bit_rate, sample_rate, pad): + """returns the total MP3 frame length in bytes + + the given arguments are the header's bit values + mpeg_id = 2 bits + layer = 2 bits + bit_rate = 4 bits + sample_rate = 2 bits + pad = 1 bit + """ + + sample_rate = self.SAMPLE_RATE[mpeg_id][sample_rate] + if (sample_rate is None): + from .text import ERR_MP3_INVALID_SAMPLE_RATE + raise ValueError(ERR_MP3_INVALID_SAMPLE_RATE) + bit_rate = self.BIT_RATE[mpeg_id][layer][bit_rate] + if (bit_rate is None): + from .text import ERR_MP3_INVALID_BIT_RATE + raise ValueError(ERR_MP3_INVALID_BIT_RATE) + if (layer == 3): # layer I + return (((12 * bit_rate) / sample_rate) + pad) * 4 + else: # layer II/III + return ((144 * bit_rate) / sample_rate) + pad + + def total_frames(self): + """returns the total PCM frames of the track as an integer""" + + return self.__pcm_frames__ + + @classmethod + def can_add_replay_gain(cls, audiofiles): + """given a list of audiofiles, + returns True if this class can add ReplayGain to those files + returns False if not""" + + for audiofile in audiofiles: + if (not isinstance(audiofile, MP3Audio)): + return False + else: + from . import BIN + + return BIN.can_execute(BIN['mp3gain']) + + @classmethod + def supports_replay_gain(cls): + """returns True if this class supports 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 . import BIN + from . import open_files + import subprocess + import os + + track_names = [track.filename for track in + open_files(filenames) if + isinstance(track, cls)] + + if (progress is not None): + progress(0, 1) + + if ((len(track_names) > 0) and (BIN.can_execute(BIN['mp3gain']))): + devnull = file(os.devnull, 'ab') + sub = subprocess.Popen([BIN['mp3gain'], '-f', '-k', '-q', '-r'] + + track_names, + stdout=devnull, + stderr=devnull) + sub.wait() + + devnull.close() + + if (progress is not None): + progress(1, 1) + + 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""" + + from . import verify + try: + f = open(self.filename, 'rb') + except IOError, err: + raise InvalidMP3(str(err)) + + #MP3 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: + try: + #skip ID3v2/ID3v1 tags during verification + self.__find_mp3_start__(f) + start = f.tell() + self.__find_last_mp3_frame__(f) + end = f.tell() + f.seek(start, 0) + + verify.mpeg(f, start, end) + if (progress is not None): + progress(1, 1) + + return True + except (IOError, ValueError), err: + raise InvalidMP3(str(err)) + finally: + f.close() + + +####################### +#MP2 AUDIO +####################### + +class MP2Audio(MP3Audio): + """an MP2 audio file""" + + from .text import (COMP_TWOLAME_64, + COMP_TWOLAME_384) + + SUFFIX = "mp2" + NAME = SUFFIX + DESCRIPTION = u"MPEG-1 Audio Layer II" + DEFAULT_COMPRESSION = str(192) + COMPRESSION_MODES = tuple(map(str, (64, 96, 112, 128, 160, 192, + 224, 256, 320, 384))) + COMPRESSION_DESCRIPTIONS = {"64": COMP_TWOLAME_64, + "384": COMP_TWOLAME_384} + BINARIES = ("mpg123", "twolame") + + @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 MP2Audio object""" + + from . import transfer_framelist_data + from . import BIN + from . import ignore_sigint + from . import EncodingError + from . import DecodingError + from . import __default_quality__ + import decimal + import bisect + import subprocess + import os + + if (((compression is None) or + (compression not in cls.COMPRESSION_MODES))): + compression = __default_quality__(cls.NAME) + + if (((pcmreader.channels > 2) or + (pcmreader.sample_rate not in (32000, 48000, 44100)) or + (pcmreader.bits_per_sample != 16))): + from . import PCMConverter + + pcmreader = PCMConverter( + pcmreader, + sample_rate=[32000, + 32000, + 44100, + 48000][bisect.bisect([32000, + 44100, + 48000], + pcmreader.sample_rate)], + channels=min(pcmreader.channels, 2), + channel_mask=pcmreader.channel_mask, + bits_per_sample=16) + + devnull = file(os.devnull, 'ab') + + sub = subprocess.Popen([BIN['twolame'], "--quiet", + "-r", + "-s", str(pcmreader.sample_rate), + "--samplesize", str(pcmreader.bits_per_sample), + "-N", str(pcmreader.channels), + "-m", "a", + "-b", compression, + "-", + filename], + stdin=subprocess.PIPE, + stdout=devnull, + stderr=devnull, + preexec_fn=ignore_sigint) + + try: + transfer_framelist_data(pcmreader, sub.stdin.write) + except (ValueError, IOError), err: + sub.stdin.close() + sub.wait() + cls.__unlink__(filename) + raise EncodingError(str(err)) + except Exception, err: + sub.stdin.close() + sub.wait() + cls.__unlink__(filename) + raise err + + try: + pcmreader.close() + except DecodingError, err: + cls.__unlink__(filename) + raise EncodingError(err.error_message) + + sub.stdin.close() + devnull.close() + + if (sub.wait() == 0): + return MP2Audio(filename) + else: + cls.__unlink__(filename) + raise EncodingError(u"twolame exited with error")
View file
audiotools-2.18.tar.gz/audiotools/musicbrainz.py -> audiotools-2.19.tar.gz/audiotools/musicbrainz.py
Changed
@@ -17,8 +17,6 @@ #along with this program; if not, write to the Free Software #Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import audiotools - class DiscID: def __init__(self, first_track_number, last_track_number, @@ -167,13 +165,13 @@ try: album_name = text(get_node(release, u"title")) except KeyError: - album_name = u"" + album_name = None #<release> may contain <artist-credit> try: album_artist = artist(get_node(release, u"artist-credit")) except KeyError: - album_artist = u"" + album_artist = None #<release> may contain <label-info-list> try: @@ -184,29 +182,29 @@ try: catalog = text(get_node(label_info, u"catalog-number")) except KeyError: - catalog = u"" + catalog = None #<label-info> may contain <label> #and <label> may contain <name> try: publisher = text(get_node(label_info, u"label", u"name")) except KeyError: - publisher = u"" + publisher = None #we'll use the first result found break else: #<label-info-list> with no <label-info> tags - catalog = u"" - publisher = u"" + catalog = None + publisher = None except KeyError: - catalog = u"" - publisher = u"" + catalog = None + publisher = None #<release> may contain <date> try: year = text(get_node(release, u"date"))[0:4] except: - year = u"" + year = None #find exact disc in <medium-list> tag #depending on disc_id value @@ -234,15 +232,15 @@ #if multiple discs in <medium-list>, #populate album number and album total - if (medium_list.hasAttribute(u"count") and - (int(medium_list.getAttribute(u"count")) > 1)): + if ((medium_list.hasAttribute(u"count") and + (int(medium_list.getAttribute(u"count")) > 1))): album_total = int(medium_list.getAttribute(u"count")) try: album_number = int(text(get_node(medium, u"position"))) except KeyError: - album_number = 0 + album_number = None else: - album_total = album_number = 0 + album_total = album_number = None #<medium> must contain <track-list> tracks = get_nodes(get_node(medium, u"track-list"), u"track") @@ -271,7 +269,7 @@ try: track_name = text(get_node(recording, u"title")) except KeyError: - track_name = u"" + track_name = None #<recording> may contain <artist-credit> if (track_artist is None): @@ -283,9 +281,6 @@ except KeyError: #no <recording> in <track> - if (track_name is None): - track_name = u"" - if (track_artist is None): track_artist = album_artist @@ -296,19 +291,21 @@ track_number = i + 1 #yield complete MetaData object - yield audiotools.MetaData(track_name=track_name, - track_number=track_number, - track_total=track_total, - album_name=album_name, - artist_name=track_artist, - performer_name=u"", - composer_name=u"", - conductor_name=u"", - ISRC=u"", - catalog=catalog, - copyright=u"", - publisher=publisher, - year=year, - album_number=album_number, - album_total=album_total, - comment=u"") + from . import MetaData + + yield MetaData(track_name=track_name, + track_number=track_number, + track_total=track_total, + album_name=album_name, + artist_name=track_artist, + performer_name=None, + composer_name=None, + conductor_name=None, + ISRC=None, + catalog=catalog, + copyright=None, + publisher=publisher, + year=year, + album_number=album_number, + album_total=album_total, + comment=None)
View file
audiotools-2.19.tar.gz/audiotools/ogg.py
Added
@@ -0,0 +1,340 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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 + + +def read_ogg_packets(reader): + """given a BitstreamReader + yields BitstreamReader substream objects + for each packet within the Ogg stream""" + + from .bitstream import Substream + + header_type = 0 + packet = Substream(1) + + while (not (header_type & 0x4)): + (magic_number, + version, + header_type, + granule_position, + serial_number, + page_sequence_number, + checksum, + segment_count) = reader.parse("4b 8u 8u 64S 32u 32u 32u 8u") + for segment_length in [reader.read(8) for i in xrange(segment_count)]: + reader.substream_append(packet, segment_length) + if (segment_length != 255): + yield packet + packet = Substream(1) + + +def read_ogg_packets_data(reader): + """given a BitstreamReader + yields binary strings + for each packet within the Ogg stream""" + + header_type = 0 + packet = [] + + while (not (header_type & 0x4)): + (magic_number, + version, + header_type, + granule_position, + serial_number, + page_sequence_number, + checksum, + segment_count) = reader.parse("4b 8u 8u 64S 32u 32u 32u 8u") + for segment_length in [reader.read(8) for i in xrange(segment_count)]: + packet.append(reader.read_bytes(segment_length)) + if (segment_length != 255): + yield "".join(packet) + packet = [] + + +class OggChecksum: + """calculates the checksum of Ogg pages + the final checksum may be determined by int(ogg_checksum_instance)""" + + CRC_LOOKUP = (0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, + 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005, + 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, + 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, + 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, + 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75, + 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, + 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd, + 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, + 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, + 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81, + 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d, + 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, + 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95, + 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, + 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, + 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae, + 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072, + 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, + 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca, + 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, + 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, + 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066, + 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba, + 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, + 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692, + 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, + 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, + 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e, + 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, + 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, + 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a, + 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, + 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, + 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f, + 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53, + 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, + 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b, + 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, + 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, + 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7, + 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b, + 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, + 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3, + 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, + 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, + 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f, + 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3, + 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, + 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c, + 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, + 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, + 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30, + 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec, + 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, + 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654, + 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, + 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, + 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18, + 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4, + 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, + 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c, + 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, + 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4) + + def __init__(self): + self.checksum = 0 + + def reset(self): + """clears the accumulated checksum so the object may be reused""" + + self.checksum = 0 + + def update(self, byte): + """given a byte integer, updates the running checksum""" + + self.checksum = (((self.checksum << 8) ^ + self.CRC_LOOKUP[((self.checksum >> 24) & 0xFF) ^ + byte]) & 0xFFFFFFFF) + + def __int__(self): + return self.checksum + + +class OggStreamReader: + def __init__(self, reader): + """reader is a BitstreamReader object""" + + self.reader = reader + + #try to grab a few useful bits of info + self.reader.mark() + (magic_number, + version, + self.serial_number) = self.reader.parse("4b 8u 8p 64p 32u") + if (magic_number != "OggS"): + from .text import ERR_OGG_INVALID_MAGIC_NUMBER + raise ValueError(ERR_OGG_INVALID_MAGIC_NUMBER) + elif (version != 0): + from .text import ERR_OGG_INVALID_VERSION + raise ValueError(ERR_OGG_INVALID_VERSION) + + self.reader.rewind() + self.reader.unmark() + self.checksum = OggChecksum() + self.reader.add_callback(self.checksum.update) + + def read_page(self): + """returns a tuple of (granule_position, + segments, + continuation, + first_page, + last_page) + + raises ValueError if the page checksum is invalid""" + + self.checksum.reset() + + #grab all the header data up to page checksum + (magic_number, + version, + continuation, + first_page, + last_page, + granule_position, + serial_number, + page_sequence_number) = self.reader.parse( + "4b 8u 1u 1u 1u 5p 64S 32u 32u") + + #update checksum with placeholder value + old_callback = self.reader.pop_callback() + old_callback(0) + old_callback(0) + old_callback(0) + old_callback(0) + checksum = self.reader.read(32) + self.reader.add_callback(old_callback) + + #grab all the segment data + segments = self.reader.parse( + "".join(["%db" % (segment_length) for segment_length in + self.reader.parse("8u" * self.reader.read(8))])) + + #verify calculated checksum against found checksum + if (int(self.checksum) != checksum): + from .text import ERR_OGG_CHECKSUM_MISMATCH + raise ValueError(ERR_OGG_CHECKSUM_MISMATCH) + else: + return (granule_position, + segments, + continuation, + first_page, + last_page) + + def pages(self): + """yields a tuple of (granule_position, + segments, + continuation, + first_page, + last_page) + + for each page in the stream + raises ValueError if a page checksum is invalid""" + + while (True): + page = self.read_page() + yield page + if (page[-1]): + break + + +class OggStreamWriter: + def __init__(self, writer, serial_number): + """writer is a BitstreamWriter + + serial_number is a signed integer""" + + from .bitstream import BitstreamRecorder + + self.writer = writer + self.serial_number = serial_number + self.sequence_number = 0 + self.temp = BitstreamRecorder(1) + self.checksum = OggChecksum() + self.temp.add_callback(self.checksum.update) + + def packet_to_segments(self, packet): + """yields a segment string per segment of the packet string + + where each segment is a string up to 255 bytes long + + the final segment will be 0 bytes if the packet + is equally divisible by 255 bytes""" + + use_pad = len(packet) % 255 == 0 + + while (len(packet) > 0): + yield packet[0:255] + packet = packet[255:] + + if (use_pad): + yield "" + + def segments_to_pages(self, segments): + """given an iterator of segment strings, + + yields a list of strings where each list is up to 255 segments + """ + + page = [] + for segment in segments: + page.append(segment) + if (len(page) == 255): + yield page + page = [] + + if (len(page) > 0): + yield page + + def write_page(self, granule_position, segments, + continuation, first_page, last_page): + """granule_position is a signed long + + segments is a list of strings containing binary data + + continuation, first_page and last_page indicate this page's + position in the Ogg stream + """ + + from .bitstream import format_size + + assert(len(segments) < 0x100) + assert(max(map(len, segments)) < 0x100) + + self.temp.reset() + self.checksum.reset() + + #write a checksummed header to temp + self.temp.build("4b 8u 1u 1u 1u 5p 64S 32u 32u", + ("OggS", 0, continuation, first_page, last_page, + granule_position, self.serial_number, + self.sequence_number)) + self.checksum.update(0) + self.checksum.update(0) + self.checksum.update(0) + self.checksum.update(0) + + #write the segment lengths and segment data to temp + self.temp.write(8, len(segments)) + self.temp.build("8u" * len(segments), map(len, segments)) + self.temp.build("".join(["%db" % (len(segment)) + for segment in segments]), segments) + + #transfer everything from the page start to the page checksum + #from temp to the final stream + self.temp.split(self.writer, self.temp, + format_size("4b 8u 8u 64S 32u 32u") / 8) + + #write the calculated page checksum to the final stream + self.writer.write(32, int(self.checksum)) + + #transfer everything past the page checksum from temp to final + self.temp.copy(self.writer) + + #increment sequence number + self.sequence_number += 1
View file
audiotools-2.19.tar.gz/audiotools/opus.py
Added
@@ -0,0 +1,510 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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) +from .vorbis import (VorbisAudio, VorbisChannelMask) +from .vorbiscomment import VorbisComment + + +class InvalidOpus(InvalidFile): + pass + + +####################### +#Vorbis File +####################### + +class OpusAudio(VorbisAudio): + """an Opus file""" + + SUFFIX = "opus" + NAME = "opus" + DESCRIPTION = u"Opus Audio Codec" + DEFAULT_COMPRESSION = "10" + COMPRESSION_MODES = tuple(map(str, range(0, 11))) + COMPRESSION_DESCRIPTIONS = {"0": u"lowest quality, fastest encode", + "10": u"best quality, slowest encode"} + BINARIES = ("opusenc", "opusdec") + + def __init__(self, filename): + """filename is a plain string""" + + AudioFile.__init__(self, filename) + self.__channels__ = 0 + self.__channel_mask__ = 0 + + #get channel count and channel mask from first packet + from .bitstream import BitstreamReader + try: + f = open(filename, "rb") + try: + ogg_reader = BitstreamReader(f, 1) + (magic_number, + version, + header_type, + granule_position, + self.__serial_number__, + page_sequence_number, + checksum, + segment_count) = ogg_reader.parse( + "4b 8u 8u 64S 32u 32u 32u 8u") + + if (magic_number != 'OggS'): + from .text import ERR_OGG_INVALID_MAGIC_NUMBER + raise InvalidFLAC(ERR_OGG_INVALID_MAGIC_NUMBER) + if (version != 0): + from .text import ERR_OGG_INVALID_VERSION + raise InvalidFLAC(ERR_OGG_INVALID_VERSION) + + segment_length = ogg_reader.read(8) + + (opushead, + version, + self.__channels__, + pre_skip, + input_sample_rate, + output_gain, + mapping_family) = ogg_reader.parse( + "8b 8u 8u 16u 32u 16s 8u") + + if (opushead != "OpusHead"): + from .text import ERR_OPUS_INVALID_TYPE + raise InvalidOpus(ERR_OPUS_INVALID_TYPE) + if (version != 1): + from .text import ERR_OPUS_INVALID_VERSION + raise InvalidOpus(ERR_OPUS_INVALID_VERSION) + if (self.__channels__ == 0): + from .text import ERR_OPUS_INVALID_CHANNELS + raise InvalidOpus(ERR_OPUS_INVALID_CHANNELS) + + #FIXME - assign channel mask from mapping family + if (mapping_family == 0): + if (self.__channels__ == 1): + self.__channel_mask__ = VorbisChannelMask(0x4) + elif (self.__channels__ == 2): + self.__channel_mask__ = VorbisChannelMask(0x3) + else: + self.__channel_mask__ = VorbisChannelMask(0) + else: + (stream_count, + coupled_stream_count) = ogg_reader.parse("8u 8u") + if (self.__channels__ != + ((coupled_stream_count * 2) + + (stream_count - coupled_stream_count))): + from .text import ERR_OPUS_INVALID_CHANNELS + raise InvalidOpus(ERR_OPUS_INVALID_CHANNELS) + channel_mapping = [ogg_reader.read(8) + for i in xrange(self.__channels__)] + finally: + f.close() + except IOError, msg: + raise InvalidOpus(str(msg)) + + def update_metadata(self, metadata): + """takes this track's current MetaData object + as returned by get_metadata() and sets this track's metadata + with any fields updated in that object + + raises IOError if unable to write the file + """ + + from .bitstream import BitstreamReader + from .bitstream import BitstreamRecorder + from .bitstream import BitstreamWriter + from .ogg import OggStreamWriter + from .ogg import OggStreamReader + from .ogg import read_ogg_packets_data + from . import iter_first + from .vorbiscomment import VorbisComment + + if (metadata is None): + return + + if (not isinstance(metadata, OpusTags)): + from .text import ERR_FOREIGN_METADATA + raise ValueError(ERR_FOREIGN_METADATA) + + original_reader = BitstreamReader(open(self.filename, "rb"), 1) + original_ogg = OggStreamReader(original_reader) + original_serial_number = original_ogg.serial_number + original_packets = read_ogg_packets_data(original_reader) + + #save the current file's identification page/packet + #(the ID packet is always fixed size, and fits in one page) + identification_page = original_ogg.read_page() + + #discard the current file's comment packet + original_packets.next() + + #save all the subsequent Ogg pages + data_pages = list(original_ogg.pages()) + + del(original_ogg) + del(original_packets) + original_reader.close() + + updated_writer = BitstreamWriter(open(self.filename, "wb"), 1) + updated_ogg = OggStreamWriter(updated_writer, original_serial_number) + + #write the identification packet in its own page + updated_ogg.write_page(*identification_page) + + #write the new comment packet in its own page(s) + comment_writer = BitstreamRecorder(1) + comment_writer.write_bytes("OpusTags") + vendor_string = metadata.vendor_string.encode('utf-8') + comment_writer.build("32u %db" % (len(vendor_string)), + (len(vendor_string), vendor_string)) + comment_writer.write(32, len(metadata.comment_strings)) + for comment_string in metadata.comment_strings: + comment_string = comment_string.encode('utf-8') + comment_writer.build("32u %db" % (len(comment_string)), + (len(comment_string), comment_string)) + + for (first_page, segments) in iter_first( + updated_ogg.segments_to_pages( + updated_ogg.packet_to_segments(comment_writer.data()))): + updated_ogg.write_page(0, segments, 0 if first_page else 1, 0, 0) + + #write the subsequent Ogg pages + for page in data_pages: + updated_ogg.write_page(*page) + + @classmethod + def supports_replay_gain(cls): + """returns True if this class supports ReplayGain""" + + return False + + def set_metadata(self, metadata): + """takes a MetaData object and sets this track's metadata + + this metadata includes track name, album name, and so on + raises IOError if unable to write the file""" + + if (metadata is not None): + metadata = OpusTags.converted(metadata) + + old_metadata = self.get_metadata() + + #port vendor string from old metadata to new metadata + metadata.vendor_string = old_metadata.vendor_string + + #remove REPLAYGAIN_* tags from new metadata (if any) + for key in [u"REPLAYGAIN_TRACK_GAIN", + u"REPLAYGAIN_TRACK_PEAK", + u"REPLAYGAIN_ALBUM_GAIN", + u"REPLAYGAIN_ALBUM_PEAK", + u"REPLAYGAIN_REFERENCE_LOUDNESS"]: + try: + metadata[key] = old_metadata[key] + except KeyError: + metadata[key] = [] + + #port "ENCODER" tag from old metadata to new metadata + if (u"ENCODER" in old_metadata): + metadata[u"ENCODER"] = old_metadata[u"ENCODER"] + + self.update_metadata(metadata) + + def get_metadata(self): + """returns a MetaData object, or None + + raises IOError if unable to read the file""" + + from .bitstream import BitstreamReader + from .ogg import read_ogg_packets + from .vorbiscomment import VorbisComment + + packets = read_ogg_packets( + BitstreamReader(open(self.filename, "rb"), 1)) + + identification = packets.next() + comment = packets.next() + + if (comment.read_bytes(8) != "OpusTags"): + return None + else: + vendor_string = \ + comment.read_bytes(comment.read(32)).decode('utf-8') + comment_strings = [ + comment.read_bytes(comment.read(32)).decode('utf-8') + for i in xrange(comment.read(32))] + return OpusTags(comment_strings, vendor_string) + + def delete_metadata(self): + """deletes the track's MetaData + + this removes or unsets tags as necessary in order to remove all data + raises IOError if unable to write the file""" + + from . import MetaData + + #the comment packet is required, + #so simply zero out its contents + self.set_metadata(MetaData()) + + def total_frames(self): + """returns the total PCM frames of the track as an integer""" + + from .bitstream import BitstreamReader + + pcm_samples = 0 + end_of_stream = 0 + try: + ogg_stream = BitstreamReader(file(self.filename, "rb"), 1) + while (end_of_stream == 0): + (magic_number, + version, + end_of_stream, + granule_position, + page_segment_count) = ogg_stream.parse( + "4b 8u 1p 1p 1u 5p 64S 32p 32p 32p 8u") + ogg_stream.skip_bytes(sum([ogg_stream.read(8) for i in + xrange(page_segment_count)])) + + if ((magic_number != "OggS") or (version != 0)): + return 0 + if (granule_position >= 0): + pcm_samples = granule_position + + ogg_stream.close() + return pcm_samples + except IOError: + return 0 + + def sample_rate(self): + """returns the rate of the track's audio as an integer number of Hz""" + + return 48000 + + def to_pcm(self): + """returns a PCMReader object containing the track's PCM data + + if an error occurs initializing a decoder, this should + return a PCMReaderError with an appropriate error message""" + + from . import PCMReader + from . import BIN + import subprocess + import os + + sub = subprocess.Popen([BIN["opusdec"], "--quiet", + "--rate", str(48000), + self.filename, "-"], + stdout=subprocess.PIPE, + stderr=file(os.devnull, "a")) + + pcmreader = PCMReader(sub.stdout, + sample_rate=self.sample_rate(), + channels=self.channels(), + channel_mask=int(self.channel_mask()), + bits_per_sample=self.bits_per_sample(), + process=sub) + + if (self.channels() <= 2): + return pcmreader + elif (self.channels() <= 8): + from . import ReorderedPCMReader + + standard_channel_mask = self.channel_mask() + vorbis_channel_mask = VorbisChannelMask(self.channel_mask()) + return ReorderedPCMReader( + pcmreader, + [vorbis_channel_mask.channels().index(channel) for channel in + standard_channel_mask.channels()]) + else: + return pcmreader + + @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 AudioFile-compatible object + + may raise EncodingError if some problem occurs when + encoding the input file. This includes an error + in the input stream, a problem writing the output file, + or even an EncodingError subclass such as + "UnsupportedBitsPerSample" if the input stream + is formatted in a way this class is unable to support + """ + + from . import transfer_framelist_data + from . import BIN + from . import ignore_sigint + from . import EncodingError + from . import DecodingError + from . import UnsupportedChannelMask + from . import __default_quality__ + from .vorbis import VorbisChannelMask + from . import ChannelMask + import subprocess + import os + + if (((compression is None) or + (compression not in cls.COMPRESSION_MODES))): + compression = __default_quality__(cls.NAME) + + devnull = file(os.devnull, 'ab') + + sub = subprocess.Popen([BIN["opusenc"], "--quiet", + "--comp", compression, + "--raw", + "--raw-bits", str(pcmreader.bits_per_sample), + "--raw-rate", str(pcmreader.sample_rate), + "--raw-chan", str(pcmreader.channels), + "--raw-endianness", str(0), + "-", filename], + stdin=subprocess.PIPE, + stdout=devnull, + stderr=devnull) + + if ((pcmreader.channels <= 2) or (int(pcmreader.channel_mask) == 0)): + try: + transfer_framelist_data(pcmreader, sub.stdin.write) + except (IOError, ValueError), err: + sub.stdin.close() + sub.wait() + cls.__unlink__(filename) + raise EncodingError(str(err)) + except Exception, err: + sub.stdin.close() + sub.wait() + cls.__unlink__(filename) + raise err + elif (pcmreader.channels <= 8): + if (int(pcmreader.channel_mask) in + (0x7, # FR, FC, FL + 0x33, # FR, FL, BR, BL + 0x37, # FR, FC, FL, BL, BR + 0x3f, # FR, FC, FL, BL, BR, LFE + 0x70f, # FL, FC, FR, SL, SR, BC, LFE + 0x63f)): # FL, FC, FR, SL, SR, BL, BR, LFE + + standard_channel_mask = ChannelMask(pcmreader.channel_mask) + vorbis_channel_mask = VorbisChannelMask(standard_channel_mask) + else: + raise UnsupportedChannelMask(filename, + int(pcmreader.channel_mask)) + + try: + from . import ReorderedPCMReader + + transfer_framelist_data( + ReorderedPCMReader( + pcmreader, + [standard_channel_mask.channels().index(channel) + for channel in vorbis_channel_mask.channels()]), + sub.stdin.write) + except (IOError, ValueError), err: + sub.stdin.close() + sub.wait() + cls.__unlink__(filename) + raise EncodingError(str(err)) + except Exception, err: + sub.stdin.close() + sub.wait() + cls.__unlink__(filename) + raise err + + else: + raise UnsupportedChannelMask(filename, + int(pcmreader.channel_mask)) + + sub.stdin.close() + + if (sub.wait() == 0): + return OpusAudio(filename) + else: + raise EncodingError(u"unable to encode file with opusenc") + + 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""" + + #Ogg stream 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, err: + raise InvalidOpus(str(err)) + try: + try: + from . import verify + verify.ogg(f) + if (progress is not None): + progress(1, 1) + return True + except (IOError, ValueError), err: + raise InvalidOpus(str(err)) + finally: + f.close() + + +class OpusTags(VorbisComment): + @classmethod + def converted(cls, metadata): + """converts metadata from another class to OpusTags""" + + from . import VERSION + + if (metadata is None): + return None + elif (isinstance(metadata, OpusTags)): + return cls(metadata.comment_strings[:], + metadata.vendor_string) + elif (metadata.__class__.__name__ == 'FlacMetaData'): + if (metadata.has_block(4)): + vorbis_comment = metadata.get_block(4) + return cls(vorbis_comment.comment_strings[:], + vorbis_comment.vendor_string) + else: + return cls([], u"Python Audio Tools %s" % (VERSION)) + elif (metadata.__class__.__name__ in ('Flac_VORBISCOMMENT', + 'VorbisComment')): + return cls(metadata.comment_strings[:], + metadata.vendor_string) + else: + comment_strings = [] + + for (attr, key) in cls.ATTRIBUTE_MAP.items(): + value = getattr(metadata, attr) + if (value is not None): + comment_strings.append(u"%s=%s" % (key, value)) + + return cls(comment_strings, u"Python Audio Tools %s" % (VERSION)) + + def __repr__(self): + return "OpusTags(%s, %s)" % \ + (repr(self.comment_strings), repr(self.vendor_string)) + + def __comment_name__(self): + return u"Opus Tags"
View file
audiotools-2.18.tar.gz/audiotools/player.py -> audiotools-2.19.tar.gz/audiotools/player.py
Changed
@@ -18,15 +18,7 @@ #Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import os -import sys import cPickle -import select -import audiotools -import time -import Queue -import threading - (RG_NO_REPLAYGAIN, RG_TRACK_GAIN, RG_ALBUM_GAIN) = range(3) @@ -46,14 +38,72 @@ next_track_callback is a function with no arguments which is called by the player when the current track is finished""" - self.command_queue = Queue.Queue() - self.worker = PlayerThread(audio_output, - self.command_queue, - replay_gain) - self.thread = threading.Thread(target=self.worker.run, - args=(next_track_callback,)) - self.thread.daemon = True - self.thread.start() + self.__closed__ = True + + import os + import sys + import mmap + import threading + + def call_next_track(response_f): + while (True): + try: + result = cPickle.load(response_f) + next_track_callback() + except (IOError, EOFError): + break + + #2, 64-bit fields of progress data + self.__progress__ = mmap.mmap(-1, 16) + + #for sending commands from player to child process + (command_r, command_w) = os.pipe() + + #for receiving "next track" indicator from child process + (response_r, response_w) = os.pipe() + + self.__pid__ = os.fork() + if (self.__pid__ > 0): + #parent + os.close(command_r) + os.close(response_w) + + self.command_queue = os.fdopen(command_w, "wb") + + thread = threading.Thread(target=call_next_track, + args=(os.fdopen(response_r, "rb"),)) + thread.daemon = True + thread.start() + + self.__closed__ = False + else: + #child + os.close(command_w) + os.close(response_r) + devnull = open(os.devnull, "r+b") + os.dup2(devnull.fileno(), sys.stdin.fileno()) + os.dup2(devnull.fileno(), sys.stdout.fileno()) + os.dup2(devnull.fileno(), sys.stderr.fileno()) + try: + PlayerProcess( + audio_output=audio_output, + command_file=os.fdopen(command_r, "rb"), + next_track_file=os.fdopen(response_w, "wb"), + progress_file=self.__progress__, + replay_gain=replay_gain).run() + finally: + #avoid calling Python cleanup handlers + os._exit(0) + + def __del__(self): + if (self.__pid__ > 0): + import os + + if (not self.__closed__): + self.close() + + self.command_queue.close() + os.waitpid(self.__pid__, 0) def open(self, track): """opens the given AudioFile for playing @@ -61,12 +111,14 @@ stops playing the current file, if any""" self.track = track - self.command_queue.put(("open", [track])) + cPickle.dump(("open", [track]), self.command_queue) + self.command_queue.flush() def play(self): """begins or resumes playing an opened AudioFile, if any""" - self.command_queue.put(("play", [])) + cPickle.dump(("play", []), self.command_queue) + self.command_queue.flush() def set_replay_gain(self, replay_gain): """sets the given ReplayGain level to apply during playback @@ -75,55 +127,73 @@ replayGain cannot be applied mid-playback one must stop() and play() a file for it to take effect""" - self.command_queue.put(("set_replay_gain", [replay_gain])) + cPickle.dump(("set_replay_gain", [replay_gain]), self.command_queue) + self.command_queue.flush() def pause(self): """pauses playback of the current file playback may be resumed with play() or toggle_play_pause()""" - self.command_queue.put(("pause", [])) + cPickle.dump(("pause", []), self.command_queue) + self.command_queue.flush() def toggle_play_pause(self): """pauses the file if playing, play the file if paused""" - self.command_queue.put(("toggle_play_pause", [])) + cPickle.dump(("toggle_play_pause", []), self.command_queue) + self.command_queue.flush() def stop(self): """stops playback of the current file if play() is called, playback will start from the beginning""" - self.command_queue.put(("stop", [])) + cPickle.dump(("stop", []), self.command_queue) + self.command_queue.flush() def close(self): """closes the player for playback the player thread is halted and the AudioOutput is closed""" - self.command_queue.put(("exit", [])) + cPickle.dump(("exit", []), self.command_queue) + self.command_queue.flush() + self.__closed__ = True def progress(self): """returns a (pcm_frames_played, pcm_frames_total) tuple this indicates the current playback status in PCM frames""" - return (self.worker.frames_played, self.worker.total_frames) + import struct + + self.__progress__.seek(0, 0) + (frames_played, + total_frames) = struct.unpack(">QQ", self.__progress__.read(16)) + + return (frames_played, total_frames) (PLAYER_STOPPED, PLAYER_PAUSED, PLAYER_PLAYING) = range(3) -class PlayerThread: - """the Player class' subthread +class PlayerProcess: + """the Player class' subprocess this should not be instantiated directly; player will do so automatically""" - def __init__(self, audio_output, command_queue, + def __init__(self, + audio_output, + command_file, + next_track_file, + progress_file, replay_gain=RG_NO_REPLAYGAIN): self.audio_output = audio_output - self.command_queue = command_queue + self.command_file = command_file + self.next_track_file = next_track_file + self.progress_file = progress_file self.replay_gain = replay_gain self.track = None @@ -131,22 +201,25 @@ self.frames_played = 0 self.total_frames = 0 self.state = PLAYER_STOPPED + self.set_progress(self.frames_played, self.total_frames) def open(self, track): self.stop() self.track = track self.frames_played = 0 self.total_frames = track.total_frames() + self.set_progress(self.frames_played, self.total_frames) def pause(self): if (self.state == PLAYER_PLAYING): + self.audio_output.pause() self.state = PLAYER_PAUSED def play(self): if (self.track is not None): if (self.state == PLAYER_STOPPED): if (self.replay_gain == RG_TRACK_GAIN): - from audiotools.replaygain import ReplayGainReader + from .replaygain import ReplayGainReader replay_gain = self.track.replay_gain() if (replay_gain is not None): @@ -157,7 +230,7 @@ else: pcmreader = self.track.to_pcm() elif (self.replay_gain == RG_ALBUM_GAIN): - from audiotools.replaygain import ReplayGainReader + from .replaygain import ReplayGainReader replay_gain = self.track.replay_gain() if (replay_gain is not None): @@ -176,12 +249,19 @@ channels=pcmreader.channels, channel_mask=pcmreader.channel_mask, bits_per_sample=pcmreader.bits_per_sample) - self.pcmconverter = ThreadedPCMConverter( - pcmreader, + + from . import BufferedPCMReader + + if (self.pcmconverter is not None): + self.pcmconverter.close() + self.pcmconverter = PCMConverter( + BufferedPCMReader(pcmreader), self.audio_output.framelist_converter()) self.frames_played = 0 self.state = PLAYER_PLAYING + self.set_progress(self.frames_played, self.total_frames) elif (self.state == PLAYER_PAUSED): + self.audio_output.resume() self.state = PLAYER_PLAYING elif (self.state == PLAYER_PLAYING): pass @@ -203,38 +283,59 @@ self.pcmconverter = None self.frames_played = 0 self.state = PLAYER_STOPPED + self.set_progress(self.frames_played, self.total_frames) + + def run(self): + import select - def run(self, next_track_callback=lambda: None): while (True): - if ((self.state == PLAYER_STOPPED) or - (self.state == PLAYER_PAUSED)): - (command, args) = self.command_queue.get(True) + if (self.state in (PLAYER_STOPPED, PLAYER_PAUSED)): + (command, args) = cPickle.load(self.command_file) if (command == "exit"): self.audio_output.close() return else: getattr(self, command)(*args) else: - try: - (command, args) = self.command_queue.get_nowait() + (r_list, + w_list, + x_list) = select.select([self.command_file], + [], + [], + 0) + if (len(r_list)): + (command, args) = cPickle.load(self.command_file) if (command == "exit"): + self.audio_output.close() return else: getattr(self, command)(*args) - except Queue.Empty: + else: if (self.frames_played < self.total_frames): (data, frames) = self.pcmconverter.read() if (frames > 0): self.audio_output.play(data) self.frames_played += frames if (self.frames_played >= self.total_frames): - next_track_callback() + cPickle.dump(None, self.next_track_file) + self.next_track_file.flush() else: self.frames_played = self.total_frames - next_track_callback() + cPickle.dump(None, self.next_track_file) + self.next_track_file.flush() + + self.set_progress(self.frames_played, + self.total_frames) else: self.stop() + def set_progress(self, current, total): + import struct + + self.progress_file.seek(0, 0) + self.progress_file.write(struct.pack(">QQ", current, total)) + self.progress_file.flush() + class CDPlayer: """a class for operating a CDDA player @@ -249,6 +350,9 @@ next_track_callback is a function with no arguments which is called by the player when the current track is finished""" + import Queue + import threading + self.command_queue = Queue.Queue() self.worker = CDPlayerThread(cdda, audio_output, @@ -308,7 +412,7 @@ """the CDPlayer class' subthread this should not be instantiated directly; - cDPlayer will do so automatically""" + CDPlayer will do so automatically""" def __init__(self, cdda, audio_output, command_queue): self.cdda = cdda @@ -337,18 +441,22 @@ def play(self): if (self.track is not None): if (self.state == PLAYER_STOPPED): + if (self.pcmconverter is not None): + self.pcmconverter.close() self.pcmconverter = ThreadedPCMConverter( self.track, self.framelist_converter) self.frames_played = 0 self.state = PLAYER_PLAYING elif (self.state == PLAYER_PAUSED): + self.audio_output.resume() self.state = PLAYER_PLAYING elif (self.state == PLAYER_PLAYING): pass def pause(self): if (self.state == PLAYER_PLAYING): + self.audio_output.pause() self.state = PLAYER_PAUSED def toggle_play_pause(self): @@ -367,9 +475,10 @@ self.state = PLAYER_STOPPED def run(self, next_track_callback=lambda: None): + import Queue + while (True): - if ((self.state == PLAYER_STOPPED) or - (self.state == PLAYER_PAUSED)): + if (self.state in (PLAYER_STOPPED, PLAYER_PAUSED)): (command, args) = self.command_queue.get(True) if (command == "exit"): self.audio_output.close() @@ -398,10 +507,33 @@ self.stop() +class PCMConverter: + def __init__(self, pcmreader, converter): + self.pcmreader = pcmreader + self.converter = converter + self.buffer_size = (pcmreader.sample_rate * + pcmreader.channels * + (pcmreader.bits_per_sample / 8)) / 20 + + def read(self): + try: + frame = self.pcmreader.read(self.buffer_size) + return (self.converter(frame), frame.frames) + except (ValueError, IOError): + return (None, 0) + + def close(self): + from . import DecodingError + try: + self.pcmreader.close() + except DecodingError: + pass + + class ThreadedPCMConverter: """a class for decoding a PCMReader in a seperate thread - pCMReader's data is queued such that even if decoding and + PCMReader's data is queued such that even if decoding and conversion are relatively time-consuming, read() will continue smoothly""" @@ -412,16 +544,27 @@ and returns an object suitable for the current AudioOutput object upon conclusion, the PCMReader is automatically closed""" + import Queue + import threading + self.decoded_data = Queue.Queue() self.stop_decoding = threading.Event() - def convert(pcmreader, buffer_size, converter, decoded_data, + def convert(pcmreader, + buffer_size, + converter, + decoded_data, stop_decoding): try: frame = pcmreader.read(buffer_size) while ((not stop_decoding.is_set()) and (len(frame) > 0)): - decoded_data.put((converter(frame), frame.frames)) - frame = pcmreader.read(buffer_size) + try: + decoded_data.put((converter(frame), frame.frames), + True, + 1) + frame = pcmreader.read(buffer_size) + except Queue.Full: + pass else: decoded_data.put((None, 0)) pcmreader.close() @@ -429,14 +572,10 @@ decoded_data.put((None, 0)) pcmreader.close() - buffer_size = (pcmreader.sample_rate * - pcmreader.channels * - (pcmreader.bits_per_sample / 8)) / 20 - self.thread = threading.Thread( target=convert, args=(pcmreader, - buffer_size, + pcmreader.sample_rate / 20, converter, self.decoded_data, self.stop_decoding)) @@ -510,6 +649,16 @@ raise NotImplementedError() + def pause(self): + """pauses audio output, with the expectation it will be resumed""" + + raise NotImplementedError() + + def resume(self): + """resumes playing paused audio output""" + + raise NotImplementedError() + def close(self): """closes the output stream""" @@ -550,8 +699,20 @@ def play(self, data): """plays a chunk of converted data""" + import time + time.sleep(float(data) / self.sample_rate) + def pause(self): + """pauses audio output, with the expectation it will be resumed""" + + pass + + def resume(self): + """resumes playing paused audio output""" + + pass + def close(self): """closes the output stream""" @@ -613,9 +774,9 @@ elif (self.bits_per_sample == 16): return lambda f: f.to_bytes(False, True) elif (self.bits_per_sample == 24): - import audiotools.pcm + from .pcm import from_list - return lambda f: audiotools.pcm.from_list( + return lambda f: from_list( [i >> 8 for i in list(f)], self.channels, 16, True).to_bytes(False, True) else: @@ -626,6 +787,16 @@ self.ossaudio.writeall(data) + def pause(self): + """pauses audio output, with the expectation it will be resumed""" + + pass + + def resume(self): + """resumes playing paused audio output""" + + pass + def close(self): """closes the output stream""" @@ -656,6 +827,7 @@ if (not self.initialized): import subprocess + from . import BIN self.sample_rate = sample_rate self.channels = channels @@ -672,7 +844,7 @@ raise ValueError("Unsupported bits-per-sample") self.pacat = subprocess.Popen( - [audiotools.BIN["pacat"], + [BIN["pacat"], "-n", "Python Audio Tools", "--rate", str(sample_rate), "--format", format, @@ -708,6 +880,20 @@ self.pacat.stdin.write(data) self.pacat.stdin.flush() + def pause(self): + """pauses audio output, with the expectation it will be resumed""" + + #may need to update this if I ever switch PulseAudio + #to the low-level interface instead of using pacat + pass + + def resume(self): + """resumes playing paused audio output""" + + #may need to update this if I ever switch PulseAudio + #to the low-level interface instead of using pacat + pass + def close(self): """closes the output stream""" @@ -719,8 +905,9 @@ @classmethod def server_alive(cls): import subprocess + from . import BIN - dev = subprocess.Popen([audiotools.BIN["pactl"], "stat"], + dev = subprocess.Popen([BIN["pactl"], "stat"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) dev.stdout.read() @@ -731,15 +918,17 @@ def available(cls): """returns True if PulseAudio is available and running on the system""" - return (audiotools.BIN.can_execute(audiotools.BIN["pacat"]) and - audiotools.BIN.can_execute(audiotools.BIN["pactl"]) and + from . import BIN + + return (BIN.can_execute(BIN["pacat"]) and + BIN.can_execute(BIN["pactl"]) and cls.server_alive()) -class PortAudioOutput(AudioOutput): - """an AudioOutput subclass for PortAudio output""" +class CoreAudioOutput(AudioOutput): + """an AudioOutput subclass for CoreAudio output""" - NAME = "PortAudio" + NAME = "CoreAudio" def init(self, sample_rate, channels, channel_mask, bits_per_sample): """initializes the output stream @@ -747,21 +936,16 @@ this *must* be called prior to play() and close()""" if (not self.initialized): - import pyaudio - self.sample_rate = sample_rate self.channels = channels self.channel_mask = channel_mask self.bits_per_sample = bits_per_sample - self.pyaudio = pyaudio.PyAudio() - self.stream = self.pyaudio.open( - format=self.pyaudio.get_format_from_width( - self.bits_per_sample / 8, False), - channels=self.channels, - rate=self.sample_rate, - output=True) - + from .output import CoreAudio + self.coreaudio = CoreAudio(sample_rate, + channels, + channel_mask, + bits_per_sample) self.initialized = True else: self.close() @@ -780,25 +964,39 @@ def play(self, data): """plays a chunk of converted data""" - self.stream.write(data) + self.coreaudio.play(data) + + def pause(self): + """pauses audio output, with the expectation it will be resumed""" + + self.coreaudio.pause() + + def resume(self): + """resumes playing paused audio output""" + + self.coreaudio.resume() def close(self): """closes the output stream""" if (self.initialized): - self.stream.close() - self.pyaudio.terminate() self.initialized = False + self.coreaudio.flush() + self.coreaudio.close() @classmethod def available(cls): """returns True if the AudioOutput is available on the system""" try: - import pyaudio + from .output import CoreAudio + return True except ImportError: return False -AUDIO_OUTPUT = (PulseAudioOutput, OSSAudioOutput, - PortAudioOutput, NULLAudioOutput) + +AUDIO_OUTPUT = (PulseAudioOutput, + OSSAudioOutput, + CoreAudioOutput, + NULLAudioOutput)
View file
audiotools-2.18.tar.gz/audiotools/py_decoders/alac.py -> audiotools-2.19.tar.gz/audiotools/py_decoders/alac.py
Changed
@@ -58,10 +58,10 @@ self.maximum_k, self.channels, self.sample_rate) = stsd.parse( - #ignore much of the stuff in the "high" ALAC atom - "32p 4b 6P 16p 16p 16p 4P 16p 16p 16p 16p 4P" + - #and use the attributes in the "low" ALAC atom instead - "32p 4b 4P 32u 8p 8u 8u 8u 8u 8u 16p 32p 32p 32u") + #ignore much of the stuff in the "high" ALAC atom + "32p 4b 6P 16p 16p 16p 4P 16p 16p 16p 16p 4P" + + #and use the attributes in the "low" ALAC atom instead + "32p 4b 4P 32u 8p 8u 8u 8u 8u 8u 16p 32p 32p 32u") self.channel_mask = {1: 0x0004, 2: 0x0003, @@ -115,7 +115,7 @@ except IOError: raise KeyError(next_atom) - def read(self, bytes): + def read(self, pcm_frames): #if the stream is exhausted, return an empty pcm.FrameList object if (self.total_pcm_frames == 0): return from_list([], self.channels, self.bits_per_sample, True) @@ -216,10 +216,9 @@ #optional uncompressed LSB values if (uncompressed_lsb_size > 0): - uncompressed_lsbs = [self.reader.read( - uncompressed_lsb_size * 8) - for i in xrange(sample_count * - channel_count)] + uncompressed_lsbs = [ + self.reader.read(uncompressed_lsb_size * 8) + for i in xrange(sample_count * channel_count)] else: uncompressed_lsbs = [] @@ -256,8 +255,9 @@ assert(len(channel) == len(uncompressed_lsbs[i::channel_count])) channels.append([s << (uncompressed_lsb_size * 8) | l - for (s, l) in zip( - channel, uncompressed_lsbs[i::channel_count])]) + for (s, l) in + zip(channel, + uncompressed_lsbs[i::channel_count])]) return channels else: return decorrelated_channels
View file
audiotools-2.18.tar.gz/audiotools/py_decoders/flac.py -> audiotools-2.19.tar.gz/audiotools/py_decoders/flac.py
Changed
@@ -31,7 +31,7 @@ SUBFRAME_FIXED, SUBFRAME_LPC) = range(4) - def __init__(self, filename): + def __init__(self, filename, channel_mask): self.reader = BitstreamReader(open(filename, "rb"), 0) if (self.reader.read_bytes(4) != 'fLaC'): @@ -52,6 +52,7 @@ self.maximum_frame_size = block_reader.read(24) self.sample_rate = block_reader.read(20) self.channels = block_reader.read(3) + 1 + self.channel_mask = channel_mask self.bits_per_sample = block_reader.read(5) + 1 self.total_frames = block_reader.read64(36) self.md5sum = block_reader.read_bytes(16) @@ -59,15 +60,15 @@ #these are frame header lookup tables #which vary slightly depending on STREAMINFO's values self.BLOCK_SIZE = [self.maximum_block_size, - 192, 576, 1152, + 192, 576, 1152, 2304, 4608, None, None, - 256, 512, 1024, 2048, + 256, 512, 1024, 2048, 4096, 8192, 16384, 32768] self.SAMPLE_RATE = [self.sample_rate, 88200, 176400, 192000, - 8000, 16000, 22050, 24000, + 8000, 16000, 22050, 24000, 32000, 44100, 48000, 96000, - None, None, None, None] + None, None, None, None] self.BITS_PER_SAMPLE = [self.bits_per_sample, 8, 12, None, 16, 20, 24, None] @@ -82,7 +83,7 @@ else: yield (block_id, block_size, self.reader.substream(block_size)) - def read(self, bytes): + def read(self, pcm_frames): #if the stream is exhausted, #verify its MD5 sum and return an empty pcm.FrameList object if (self.total_frames < 1): @@ -99,7 +100,7 @@ channel_assignment, bits_per_sample) = self.read_frame_header() channel_count = self.CHANNEL_COUNT[channel_assignment] - if (channel_count == None): + if (channel_count is None): raise ValueError("invalid channel assignment") #channel data will be a list of signed sample lists, one per channel @@ -107,22 +108,19 @@ channel_data = [] for channel_number in xrange(channel_count): - if ((channel_assignment == 0x8) and - (channel_number == 1)): + if ((channel_assignment == 0x8) and (channel_number == 1)): #for left-difference assignment #the difference channel has 1 additional bit channel_data.append(self.read_subframe(block_size, bits_per_sample + 1)) - elif ((channel_assignment == 0x9) and - (channel_number == 0)): + elif ((channel_assignment == 0x9) and (channel_number == 0)): #for difference-right assignment #the difference channel has 1 additional bit channel_data.append(self.read_subframe(block_size, bits_per_sample + 1)) - elif ((channel_assignment == 0xA) and - (channel_number == 1)): - #for mid-side assignment - #the side channel has 1 additional bit + elif ((channel_assignment == 0xA) and (channel_number == 1)): + #for average-difference assignment + #the difference channel has 1 additional bit channel_data.append(self.read_subframe(block_size, bits_per_sample + 1)) else: @@ -225,8 +223,7 @@ #unpack the 3 bit bits-per-sample field #this never requires additional bits - if ((bits_per_sample_bits == 0x3) or - (bits_per_sample_bits == 0x7)): + if ((bits_per_sample_bits == 0x3) or (bits_per_sample_bits == 0x7)): raise ValueError("invalid bits per sample") else: bits_per_sample = self.BITS_PER_SAMPLE[bits_per_sample_bits] @@ -311,33 +308,29 @@ return residuals elif (order == 1): for residual in residuals: - samples.append( - samples[-1] + - residual) + samples.append(samples[-1] + + residual) return samples elif (order == 2): for residual in residuals: - samples.append( - (2 * samples[-1]) - - samples[-2] + - residual) + samples.append((2 * samples[-1]) - + samples[-2] + + residual) return samples elif (order == 3): for residual in residuals: - samples.append( - (3 * samples[-1]) - - (3 * samples[-2]) + - samples[-3] + - residual) + samples.append((3 * samples[-1]) - + (3 * samples[-2]) + + samples[-3] + + residual) return samples elif (order == 4): for residual in residuals: - samples.append( - (4 * samples[-1]) - - (6 * samples[-2]) + - (4 * samples[-3]) - - samples[-4] + - residual) + samples.append((4 * samples[-1]) - + (6 * samples[-2]) + + (4 * samples[-3]) - + samples[-4] + + residual) return samples else: raise ValueError("unsupported FIXED subframe order")
View file
audiotools-2.18.tar.gz/audiotools/py_decoders/shn.py -> audiotools-2.19.tar.gz/audiotools/py_decoders/shn.py
Changed
@@ -19,7 +19,8 @@ from audiotools.bitstream import BitstreamReader from audiotools.pcm import from_list, from_channels -from audiotools import parse_fmt, parse_comm +from audiotools.wav import parse_fmt +from audiotools.aiff import parse_comm import cStringIO @@ -79,7 +80,7 @@ self.sample_rate, bits_per_sample, channel_mask) = parse_fmt( - wave.substream(chunk_size)) + wave.substream(chunk_size)) self.channel_mask = int(channel_mask) return else: @@ -110,7 +111,7 @@ bits_per_sample, self.sample_rate, channel_mask) = parse_comm( - aiff.substream(chunk_size)) + aiff.substream(chunk_size)) self.channel_mask = int(channel_mask) return else: @@ -172,7 +173,7 @@ return (file_type, channels, block_length, max_LPC, number_of_means) - def read(self, bytes): + def read(self, pcm_frames): if (self.stream_finished): return from_channels([from_list([], 1, self.bits_per_sample, @@ -184,8 +185,8 @@ unshifted = [] while (True): command = self.unsigned(2) - if ((0 <= command) and (command <= 3) or - (7 <= command) and (command <= 8)): + if (((0 <= command) and (command <= 3) or + (7 <= command) and (command <= 8))): #audio data commands if (command == 0): # DIFF0 samples.append(self.read_diff0(self.block_length,
View file
audiotools-2.18.tar.gz/audiotools/py_decoders/wavpack.py -> audiotools-2.19.tar.gz/audiotools/py_decoders/wavpack.py
Changed
@@ -49,8 +49,8 @@ sub_blocks_data.mark() try: for sub_block in sub_blocks(sub_blocks_data, sub_blocks_size): - if ((sub_block.metadata_function == 7) and - (sub_block.nondecoder_data == 1)): + if (((sub_block.metadata_function == 7) and + (sub_block.nondecoder_data == 1))): self.sample_rate = sub_block.data.read( sub_block.data_size() * 8) break @@ -63,8 +63,8 @@ self.bits_per_sample = [8, 16, 24, 32][block_header.bits_per_sample] if (block_header.initial_block and block_header.final_block): - if ((block_header.mono_output == 0) or - (block_header.false_stereo == 1)): + if (((block_header.mono_output == 0) or + (block_header.false_stereo == 1))): self.channels = 2 self.channel_mask = 0x3 else: @@ -74,8 +74,8 @@ #look for channel mask sub block sub_blocks_data.mark() for sub_block in sub_blocks(sub_blocks_data, sub_blocks_size): - if ((sub_block.metadata_function == 13) and - (sub_block.nondecoder_data == 0)): + if (((sub_block.metadata_function == 13) and + (sub_block.nondecoder_data == 0))): self.channels = sub_block.data.read(8) self.channel_mask = sub_block.data.read( (sub_block.data_size() - 1) * 8) @@ -93,7 +93,7 @@ self.md5_checked = False self.md5sum = md5() - def read(self, bytes): + def read(self, pcm_frames): if (self.pcm_finished): if (not self.md5_checked): self.reader.mark() @@ -105,10 +105,10 @@ self.reader.substream(sub_blocks_size) for sub_block in sub_blocks(sub_blocks_data, sub_blocks_size): - if ((sub_block.metadata_function == 6) and - (sub_block.nondecoder_data == 1)): - if (sub_block.data.read_bytes(16) != - self.md5sum.digest()): + if (((sub_block.metadata_function == 6) and + (sub_block.nondecoder_data == 1))): + if ((sub_block.data.read_bytes(16) != + self.md5sum.digest())): raise ValueError("invalid stream MD5 sum") except (IOError, ValueError): #no error if a block isn't found @@ -135,8 +135,8 @@ if (block_header.final_block == 1): break - if ((block_header.block_index + block_header.block_samples) >= - block_header.total_samples): + if ((block_header.block_index + + block_header.block_samples) >= block_header.total_samples): self.pcm_finished = True #combine channels of audio data into single block @@ -208,11 +208,10 @@ @classmethod def read(cls, reader): - return cls(*reader.parse( - "4b 32u 16u 8u 8u 32u 32u 32u" + - "2u 1u 1u 1u 1u 1u 1u 1u " + - "1u 1u 1u 1u 5u 5u 4u 2p 1u 1u 1p" + - "32u")) + return cls(*reader.parse("4b 32u 16u 8u 8u 32u 32u 32u" + + "2u 1u 1u 1u 1u 1u 1u 1u " + + "1u 1u 1u 1u 5u 5u 4u 2p 1u 1u 1p" + + "32u")) class Sub_Block: @@ -301,7 +300,7 @@ if (metadata_function == 2): (decorrelation_terms, decorrelation_deltas) = read_decorrelation_terms( - sub_block_size, actual_size_1_less, sub_block_data) + sub_block_size, actual_size_1_less, sub_block_data) decorrelation_terms_read = True if (metadata_function == 3): if (not decorrelation_terms_read): @@ -333,8 +332,8 @@ extended_integers_read = True if (metadata_function == 10): if (not entropies_read): - raise ValueError( - "bitstream sub block before entropy variables sub block") + raise ValueError("bitstream sub block before " + + "entropy variables sub block") residuals = read_bitstream(block_header, entropies, sub_block_data) residuals_read = True @@ -354,8 +353,7 @@ raise ValueError("bitstream sub block not found") if ((block_header.mono_output == 0) and (block_header.false_stereo == 0)): - if (decorrelation_terms_read and - len(decorrelation_terms) > 0): + if (decorrelation_terms_read and len(decorrelation_terms) > 0): decorrelated = decorrelate_channels(residuals, decorrelation_terms, decorrelation_deltas, @@ -371,8 +369,8 @@ channels_crc = calculate_crc(left_right) if (channels_crc != block_header.CRC): - raise ValueError("CRC mismatch (0x%8.8X != 0x%8.8X)" % \ - (channels_crc, block_header.CRC)) + raise ValueError("CRC mismatch (0x%8.8X != 0x%8.8X)" % + (channels_crc, block_header.CRC)) if (block_header.extended_size_integers == 1): un_shifted = undo_extended_integers(zero_bits, @@ -384,8 +382,7 @@ return un_shifted else: - if (decorrelation_terms_read and - len(decorrelation_terms) > 0): + if (decorrelation_terms_read and len(decorrelation_terms) > 0): decorrelated = decorrelate_channels(residuals, decorrelation_terms, decorrelation_deltas, @@ -396,8 +393,8 @@ channels_crc = calculate_crc(decorrelated) if (channels_crc != block_header.CRC): - raise ValueError("CRC mismatch (0x%8.8X != 0x%8.8X)" % \ - (channels_crc, block_header.CRC)) + raise ValueError("CRC mismatch (0x%8.8X != 0x%8.8X)" % + (channels_crc, block_header.CRC)) if (block_header.extended_size_integers == 1): un_shifted = undo_extended_integers(zero_bits,
View file
audiotools-2.18.tar.gz/audiotools/py_encoders/alac.py -> audiotools-2.19.tar.gz/audiotools/py_encoders/alac.py
Changed
@@ -55,7 +55,7 @@ block_size=4096, initial_history=10, history_multiplier=40, - maximum_K=14, + maximum_k=14, interlacing_shift=2, min_interlacing_leftweight=0, max_interlacing_leftweight=4): @@ -63,7 +63,7 @@ options = Encoding_Options(block_size, initial_history, history_multiplier, - maximum_K, + maximum_k, interlacing_shift, min_interlacing_leftweight, max_interlacing_leftweight) @@ -83,17 +83,13 @@ mdat.write_bytes("mdat") #read FrameList objects until stream is empty - frame = pcmreader.read(block_size * - pcmreader.channels * - (pcmreader.bits_per_sample / 8)) + frame = pcmreader.read(block_size) while (len(frame) > 0): frame_sample_sizes.append(frame.frames) frame_file_offsets.append(int(mdat_length)) encode_frameset(mdat, pcmreader, options, frame) frame_byte_sizes.append(int(mdat_length) - frame_file_offsets[-1]) - frame = pcmreader.read(block_size * - pcmreader.channels * - (pcmreader.bits_per_sample / 8)) + frame = pcmreader.read(block_size) #finally, return to start of mdat and write actual length mdat.byte_align() @@ -118,11 +114,11 @@ frame.channel(1)]) elif (pcmreader.channels == 3): for pair in [[frame.channel(2)], - [frame.channel(0), frame_channel(1)]]: + [frame.channel(0), frame.channel(1)]]: encode_frame(writer, pcmreader, options, pair) elif (pcmreader.channels == 4): for pair in [[frame.channel(2)], - [frame.channel(0), frame_channel(1)], + [frame.channel(0), frame.channel(1)], [frame.channel(3)]]: encode_frame(writer, pcmreader, options, pair) elif (pcmreader.channels == 5): @@ -213,6 +209,7 @@ uncompressed_LSBs = 0 LSBs = [] else: + from audiotools.pcm import from_list #extract uncompressed LSBs uncompressed_LSBs = (pcmreader.bits_per_sample - 16) / 8 LSBs = [] @@ -220,7 +217,10 @@ for c in xrange(len(channels)): LSBs.append(channels[c][i] % (2 ** (pcmreader.bits_per_sample - 16))) - channels[c][i] >>= (pcmreader.bits_per_sample - 16) + + channels = [from_list([i >> (pcmreader.bits_per_sample - 16) + for i in channel], 1, 16, True) + for channel in channels] if (len(channels) == 1): encode_non_interlaced_frame(writer, @@ -247,8 +247,8 @@ channels) for i in xrange(len(interlaced_frames) - 1): - if (interlaced_frames[i].bits() < - min([f.bits() for f in interlaced_frames[i + 1:]])): + if ((interlaced_frames[i].bits() < + min([f.bits() for f in interlaced_frames[i + 1:]]))): interlaced_frames[i].copy(writer) break else: @@ -353,7 +353,7 @@ autocorrelated = [sum([s1 * s2 for s1, s2 in zip(windowed, windowed[lag:])]) - for lag in xrange(0, 9)] + for lag in xrange(0, 9)] assert(len(autocorrelated) == 9) @@ -424,7 +424,7 @@ lp_coefficients.append([c1 - (ki * c2) for (c1, c2) in zip(lp_coefficients[i - 1], - reversed(lp_coefficients[i - 1]))] + [ki]) + reversed(lp_coefficients[i - 1]))] + [ki]) error.append(error[i - 1] * (1 - ki ** 2)) @@ -481,12 +481,14 @@ lpc_sum = sum([(c * (s - base_sample)) for (c, s) in zip(qlp_coefficients, - reversed(channel[i - len(qlp_coefficients):i]))]) + reversed(channel[i - + len(qlp_coefficients):i]))]) residual = truncate_bits( channel[i] - base_sample - - ((lpc_sum + (1 << (QLP_SHIFT_NEEDED - 1))) >> QLP_SHIFT_NEEDED), + ((lpc_sum + (1 << (QLP_SHIFT_NEEDED - 1))) >> + QLP_SHIFT_NEEDED), sample_size) residuals.append(residual)
View file
audiotools-2.18.tar.gz/audiotools/py_encoders/flac.py -> audiotools-2.19.tar.gz/audiotools/py_encoders/flac.py
Changed
@@ -92,13 +92,22 @@ self.qlp_precision = 13 -def encode_flac(filename, pcmreader, +def encode_flac(filename, + pcmreader, block_size=4096, max_lpc_order=8, - adaptive_mid_side=False, + min_residual_partition_order=0, + max_residual_partition_order=5, mid_side=True, + adaptive_mid_side=False, exhaustive_model_search=False, - max_residual_partition_order=5): + disable_verbatim_subframes=False, + disable_constant_subframes=False, + disable_fixed_subframes=False, + disable_lpc_subframes=False): + + current_offset = 0 + frame_offsets = [] options = Encoding_Options(block_size, max_lpc_order, @@ -126,33 +135,31 @@ #walk through PCM reader's FrameLists frame_number = 0 - frame = pcmreader.read(block_size * - (pcmreader.bits_per_sample / 8) * - pcmreader.channels) + frame = pcmreader.read(block_size) flac_frame = BitstreamRecorder(0) while (len(frame) > 0): - + frame_offsets.append((current_offset, frame.frames)) streaminfo.input_update(frame) flac_frame.reset() encode_flac_frame(flac_frame, pcmreader, options, frame_number, frame) - + current_offset += flac_frame.bytes() streaminfo.output_update(flac_frame) flac_frame.copy(writer) frame_number += 1 - frame = pcmreader.read(block_size * - (pcmreader.bits_per_sample / 8) * - pcmreader.channels) + frame = pcmreader.read(block_size) #return to beginning of file and rewrite STREAMINFO block output_file.seek(8, 0) streaminfo.write(writer) writer.close() + return frame_offsets + def encode_flac_frame(writer, pcmreader, options, frame_number, frame): crc16 = CRC16() @@ -206,8 +213,8 @@ average_subframe.copy(writer) difference_subframe.copy(writer) else: - if ((left_subframe.bits() + right_subframe.bits()) < - (average_subframe.bits() + difference_subframe.bits())): + if (((left_subframe.bits() + right_subframe.bits()) < + (average_subframe.bits() + difference_subframe.bits()))): write_frame_header(writer, pcmreader, frame_number, frame, 0x1) left_subframe.copy(writer) right_subframe.copy(writer) @@ -271,8 +278,8 @@ 176400: 2, 192000: 3}.get(pcmreader.sample_rate, None) if (encoded_sample_rate is None): - if (((pcmreader.sample_rate % 1000) == 0) and - (pcmreader.sample_rate <= 255000)): + if ((((pcmreader.sample_rate % 1000) == 0) and + (pcmreader.sample_rate <= 255000))): encoded_sample_rate = 12 elif (((pcmreader.sample_rate % 10) == 0) and (pcmreader.sample_rate <= 655350)): @@ -377,7 +384,8 @@ encode_fixed_subframe(fixed_subframe, options, wasted_bps, - bits_per_sample, samples) + bits_per_sample, + samples) if (options.max_lpc_order > 0): (lpc_order, @@ -398,8 +406,8 @@ qlp_coeffs, samples) - if ((bits_per_sample * len(samples)) < - min(fixed_subframe.bits(), lpc_subframe.bits())): + if (((bits_per_sample * len(samples)) < + min(fixed_subframe.bits(), lpc_subframe.bits()))): encode_verbatim_subframe(writer, wasted_bps, bits_per_sample, samples) elif (fixed_subframe.bits() < lpc_subframe.bits()): @@ -407,8 +415,7 @@ else: lpc_subframe.copy(writer) else: - if ((bits_per_sample * len(samples)) < - fixed_subframe.bits()): + if ((bits_per_sample * len(samples)) < fixed_subframe.bits()): encode_verbatim_subframe(writer, wasted_bps, bits_per_sample, samples) else: @@ -433,7 +440,8 @@ writer.write(1, 0) #write frame data - writer.build(("%ds" % (bits_per_sample)) * len(samples), samples) + writer.build(("%ds" % (bits_per_sample - wasted_bps)) * len(samples), + samples) def encode_fixed_subframe(writer, options, wasted_bps, bits_per_sample, @@ -445,15 +453,18 @@ residuals = [samples] total_error = [sum(map(abs, residuals[-1][4:]))] - for order in xrange(1, 5): - residuals.append(next_order(residuals[-1])) - total_error.append(sum(map(abs, residuals[-1][4 - order:]))) + if (len(samples) > 4): + for order in xrange(1, 5): + residuals.append(next_order(residuals[-1])) + total_error.append(sum(map(abs, residuals[-1][4 - order:]))) - for order in xrange(4): - if (total_error[order] < min(total_error[order + 1:])): - break + for order in xrange(4): + if (total_error[order] < min(total_error[order + 1:])): + break + else: + order = 4 else: - order = 4 + order = 0 #then write the subframe to disk @@ -580,60 +591,63 @@ zip(samples, tukey_window(len(samples), 0.5))] #compute autocorrelation values - #FIXME - ensure max_lpc_order > sample count - autocorrelation_values = [sum([x * y for x, y in zip(windowed, - windowed[lag:])]) - for lag in xrange(0, options.max_lpc_order + 1)] - - if ((len(autocorrelation_values) > 1) and - (set(autocorrelation_values) != set([1.0]))): - - (lp_coefficients, - error) = compute_lp_coefficients(autocorrelation_values) - - if (not options.exhaustive_model_search): - #if not performing exhaustive model search, - #estimate which set of LP coefficients is best - #and return those - - order = estimate_best_lpc_order(options, - len(samples), - bits_per_sample, - error) - (qlp_coeffs, - qlp_shift_needed) = quantize_coefficients( - options.qlp_precision, lp_coefficients, order) - - return (order, qlp_coeffs, qlp_shift_needed) - else: - #if performing exhaustive model search, - #build LPC subframe from each set of LP coefficients - #and return the one that is smallest - - best_subframe_size = 2 ** 32 - best_order = None - best_coeffs = None - best_shift_needed = None - for order in xrange(1, options.max_lpc_order + 1): + if (len(samples) > (options.max_lpc_order + 1)): + autocorrelation_values = [ + sum([x * y for x, y in zip(windowed, windowed[lag:])]) + for lag in xrange(0, options.max_lpc_order + 1)] + + if ((len(autocorrelation_values) > 1) and + (set(autocorrelation_values) != + set([0.0]))): + (lp_coefficients, + error) = compute_lp_coefficients(autocorrelation_values) + + if (not options.exhaustive_model_search): + #if not performing exhaustive model search, + #estimate which set of LP coefficients is best + #and return those + + order = estimate_best_lpc_order(options, + len(samples), + bits_per_sample, + error) (qlp_coeffs, qlp_shift_needed) = quantize_coefficients( - options.qlp_precision, lp_coefficients, order) - - subframe = BitstreamAccumulator(0) - encode_lpc_subframe(subframe, options, - wasted_bps, bits_per_sample, - order, options.qlp_precision, - qlp_shift_needed, qlp_coeffs, samples) - if (subframe.bits() < best_subframe_size): - best_subframe_size = subframe.bits() - best_order = order - best_coeffs = qlp_coeffs - best_shift_needed = qlp_shift_needed - - return (best_order, best_coeffs, best_shift_needed) + options.qlp_precision, + lp_coefficients, + order) + + return (order, qlp_coeffs, qlp_shift_needed) + else: + #if performing exhaustive model search, + #build LPC subframe from each set of LP coefficients + #and return the one that is smallest + + best_subframe_size = 2 ** 32 + best_order = None + best_coeffs = None + best_shift_needed = None + for order in xrange(1, options.max_lpc_order + 1): + (qlp_coeffs, + qlp_shift_needed) = quantize_coefficients( + options.qlp_precision, lp_coefficients, order) + + subframe = BitstreamAccumulator(0) + encode_lpc_subframe(subframe, options, + wasted_bps, bits_per_sample, + order, options.qlp_precision, + qlp_shift_needed, qlp_coeffs, samples) + if (subframe.bits() < best_subframe_size): + best_subframe_size = subframe.bits() + best_order = order + best_coeffs = qlp_coeffs + best_shift_needed = qlp_shift_needed + + return (best_order, best_coeffs, best_shift_needed) + else: + return (1, [0], 0) else: - #FIXME - return a simple set of coefficients here - raise NotImplementedError() + return (1, [0], 0) def compute_lp_coefficients(autocorrelation): @@ -651,7 +665,7 @@ lp_coefficients.append([c1 - (ki * c2) for (c1, c2) in zip(lp_coefficients[i - 1], - reversed(lp_coefficients[i - 1]))] + [ki]) + reversed(lp_coefficients[i - 1]))] + [ki]) error.append(error[i - 1] * (1 - ki ** 2)) return (lp_coefficients, error) @@ -691,9 +705,12 @@ from math import log l = max(map(abs, lp_coefficients[order - 1])) - qlp_shift_needed = min((qlp_precision - 1) - - (int(log(l) / log(2)) - 1) - 1, - (2 ** 4) - 1) + if (l > 0): + qlp_shift_needed = min((qlp_precision - 1) - + (int(log(l) / log(2)) - 1) - 1, + (2 ** 4) - 1) + else: + qlp_shift_needed = 0 if (qlp_shift_needed < -(2 ** 4)): raise ValueError("too much negative shift needed")
View file
audiotools-2.18.tar.gz/audiotools/py_encoders/shn.py -> audiotools-2.19.tar.gz/audiotools/py_encoders/shn.py
Changed
@@ -38,8 +38,13 @@ FN_VERBATIM) = range(10) -def encode_shn(filename, pcmreader, is_big_endian, signed_samples, - header_data, footer_data="", block_size=256): +def encode_shn(filename, + pcmreader, + is_big_endian, + signed_samples, + header_data, + footer_data="", + block_size=256): """filename is a string to the output file's path pcmreader is a PCMReader object header_data and footer_data are binary strings @@ -98,9 +103,7 @@ #split PCMReader into block_size chunks #and continue until the number of PCM frames is 0 - frame = pcmreader.read(block_size * - (pcmreader.bits_per_sample / 8) * - pcmreader.channels) + frame = pcmreader.read(block_size) while (len(frame) > 0): #if the chunk isn't block_size frames long, #issue a command to change it @@ -129,7 +132,6 @@ #issue a new BITSHIFT command wasted_bits = wasted_bps(channel) if (wasted_bits != left_shift): - print "writing wasted bits : %s" % (wasted_bits) write_unsigned(writer, COMMAND_SIZE, FN_BITSHIFT) write_unsigned(writer, BITSHIFT_SIZE, wasted_bits) left_shift = wasted_bits @@ -160,9 +162,7 @@ wrapped_channels[c] = shifted #and get another set of channels to encode - frame = pcmreader.read(block_size * - (pcmreader.bits_per_sample / 8) * - pcmreader.channels) + frame = pcmreader.read(block_size) #once all PCM data has been sent #if there's any footer data, write it as another VERBATIM block
View file
audiotools-2.18.tar.gz/audiotools/py_encoders/wavpack.py -> audiotools-2.19.tar.gz/audiotools/py_encoders/wavpack.py
Changed
@@ -60,8 +60,7 @@ total_size = 4 * 3 # 'RIFF' + size + 'WAVE' total_size += 4 * 2 # 'fmt ' + size - if ((pcmreader.channels <= 2) and - (pcmreader.bits_per_sample <= 16)): + if ((pcmreader.channels <= 2) and (pcmreader.bits_per_sample <= 16)): #classic fmt chunk fmt = "16u 16u 32u 32u 16u 16u" fmt_fields = (1, # compression code @@ -217,7 +216,7 @@ CorrelationParameters(3, 2, [0, 0], [[0] * 3, [0] * 3]), CorrelationParameters(17, 2, [0, 0], [[0] * 2, - [0] * 2]), + [0] * 2]), CorrelationParameters(2, 2, [0, 0], [[0] * 2, [0] * 2]), CorrelationParameters(18, 2, [0, 0], [[0] * 2, @@ -321,9 +320,15 @@ for c in xrange(channel_count)] -def encode_wavpack(filename, pcmreader, block_size, +def encode_wavpack(filename, + pcmreader, + block_size, + false_stereo=False, + wasted_bits=False, + joint_stereo=False, correlation_passes=0, - wave_header=None, wave_footer=None): + wave_header=None, + wave_footer=None): pcmreader = BufferedPCMReader(pcmreader) output_file = open(filename, "wb") writer = BitstreamWriter(output_file, 1) @@ -337,9 +342,7 @@ block_index = 0 #walk through PCM reader's FrameLists - frame = pcmreader.read(block_size * - (pcmreader.bits_per_sample / 8) * - pcmreader.channels) + frame = pcmreader.read(block_size) while (len(frame) > 0): context.total_frames += frame.frames context.md5sum.update( @@ -363,9 +366,7 @@ c += parameters.channel_count block_index += frame.frames - frame = pcmreader.read(block_size * - (pcmreader.bits_per_sample / 8) * - pcmreader.channels) + frame = pcmreader.read(block_size) #write MD5 sum and optional Wave footer in final block sub_blocks = BitstreamRecorder(1) @@ -1264,8 +1265,8 @@ while (i < (len(channels) * len(channels[0]))): r = channels[i % len(channels)][i / len(channels)] - if ((entropies[0][0] < 2) and (entropies[1][0] < 2) and - unary_undefined(u_i_2, r_i_1.m)): + if (((entropies[0][0] < 2) and (entropies[1][0] < 2) and + unary_undefined(u_i_2, r_i_1.m))): if ((r_i_1.zeroes is not None) and (r_i_1.m is None)): #in a block of zeroes if (r == 0):
View file
audiotools-2.19.tar.gz/audiotools/shn.py
Added
@@ -0,0 +1,586 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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, ChannelMask, InvalidFile, + WaveContainer, AiffContainer) + +import os.path + + +class InvalidShorten(InvalidFile): + pass + + +class ShortenAudio(WaveContainer, AiffContainer): + """a Shorten audio file""" + + SUFFIX = "shn" + NAME = SUFFIX + DESCRIPTION = u"Shorten" + + def __init__(self, filename): + """filename is a plain string""" + + from .bitstream import BitstreamReader + from . import ChannelMask + import cStringIO + + AudioFile.__init__(self, filename) + try: + f = open(filename, 'rb') + except IOError, msg: + raise InvalidShorten(str(msg)) + + reader = BitstreamReader(f, 0) + try: + if (reader.parse("4b 8u") != ["ajkg", 2]): + #FIXME + raise InvalidShorten("invalid Shorten header") + except IOError: + #FIXME + raise InvalidShorten("invalid Shorten header") + + def read_unsigned(r, c): + MSB = r.unary(1) + LSB = r.read(c) + return MSB * 2 ** c + LSB + + def read_long(r): + return read_unsigned(r, read_unsigned(r, 2)) + + #populate channels and bits_per_sample from Shorten header + (file_type, + self.__channels__, + block_length, + max_LPC, + number_of_means, + bytes_to_skip) = [read_long(reader) for i in xrange(6)] + + if ((1 <= file_type) and (file_type <= 2)): + self.__bits_per_sample__ = 8 + elif ((3 <= file_type) and (file_type <= 6)): + self.__bits_per_sample__ = 16 + else: + #FIXME + raise InvalidShorten("unsupported Shorten file type") + + #setup some default dummy metadata + self.__sample_rate__ = 44100 + if (self.__channels__ == 1): + self.__channel_mask__ = ChannelMask(0x4) + elif (self.__channels__ == 2): + self.__channel_mask__ = ChannelMask(0x3) + else: + self.__channel_mask__ = ChannelMask(0) + self.__total_frames__ = 0 + + #populate sample_rate and total_frames from first VERBATIM command + command = read_unsigned(reader, 2) + if (command == 9): + verbatim_bytes = "".join([chr(read_unsigned(reader, 8) & 0xFF) + for i in xrange(read_unsigned(reader, + 5))]) + try: + wave = BitstreamReader(cStringIO.StringIO(verbatim_bytes), 1) + header = wave.read_bytes(12) + if (header.startswith("RIFF") and header.endswith("WAVE")): + #got RIFF/WAVE header, so parse wave blocks as needed + total_size = len(verbatim_bytes) - 12 + while (total_size >= 8): + (chunk_id, chunk_size) = wave.parse("4b 32u") + total_size -= 8 + if (chunk_id == 'fmt '): + from .wav import parse_fmt + + (channels, + self.__sample_rate__, + bits_per_sample, + self.__channel_mask__) = parse_fmt( + wave.substream(chunk_size)) + elif (chunk_id == 'data'): + self.__total_frames__ = \ + (chunk_size / + (self.__channels__ * + (self.__bits_per_sample__ / 8))) + else: + if (chunk_size % 2): + wave.read_bytes(chunk_size + 1) + total_size -= (chunk_size + 1) + else: + wave.read_bytes(chunk_size) + total_size -= chunk_size + except (IOError, ValueError): + pass + + try: + aiff = BitstreamReader(cStringIO.StringIO(verbatim_bytes), 0) + header = aiff.read_bytes(12) + if (header.startswith("FORM") and header.endswith("AIFF")): + #got FORM/AIFF header, so parse aiff blocks as needed + total_size = len(verbatim_bytes) - 12 + while (total_size >= 8): + (chunk_id, chunk_size) = aiff.parse("4b 32u") + total_size -= 8 + if (chunk_id == 'COMM'): + from .aiff import parse_comm + + (channels, + total_sample_frames, + bits_per_sample, + self.__sample_rate__, + self.__channel_mask__) = parse_comm( + aiff.substream(chunk_size)) + elif (chunk_id == 'SSND'): + #subtract 8 bytes for "offset" and "block size" + self.__total_frames__ = \ + ((chunk_size - 8) / + (self.__channels__ * + (self.__bits_per_sample__ / 8))) + else: + if (chunk_size % 2): + aiff.read_bytes(chunk_size + 1) + total_size -= (chunk_size + 1) + else: + aiff.read_bytes(chunk_size) + total_size -= chunk_size + except IOError: + pass + + 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""" + + return self.__channel_mask__ + + def lossless(self): + """returns True""" + + return True + + def total_frames(self): + """returns the total PCM frames of the track as an integer""" + + return self.__total_frames__ + + def sample_rate(self): + """returns the rate of the track's audio as an integer number of Hz""" + + return self.__sample_rate__ + + def to_pcm(self): + """returns a PCMReader object containing the track's PCM data""" + + from .decoders import SHNDecoder + from . import PCMReaderError + + try: + return SHNDecoder(self.filename) + except (IOError, ValueError), msg: + #these may not be accurate if the Shorten file is broken + #but if it is broken, there'll be no way to + #cross-check the results anyway + return PCMReaderError(error_message=str(msg), + sample_rate=44100, + channels=2, + channel_mask=0x3, + bits_per_sample=16) + + @classmethod + def from_pcm(cls, filename, pcmreader, compression=None, + block_size=256, encoding_function=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 ShortenAudio object""" + + #can't build artificial header because we don't know + #how long the PCMReader will be and there's no way + #to go back and write one later because all the byte values + #are stored variable-sized + #so we have to build a temporary Wave file instead + + from . import UnsupportedBitsPerSample + + if (pcmreader.bits_per_sample not in (8, 16)): + raise UnsupportedBitsPerSample(filename, pcmreader.bits_per_sample) + + from . import WaveAudio + import tempfile + + f = tempfile.NamedTemporaryFile(suffix=".wav") + try: + w = WaveAudio.from_pcm(f.name, pcmreader) + (header, footer) = w.wave_header_footer() + return cls.from_wave(filename, + header, + w.to_pcm(), + footer, + compression, + block_size, + encoding_function) + finally: + if (os.path.isfile(f.name)): + f.close() + else: + f.close_called = True + + def has_foreign_wave_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""" + + from . import decoders + from . import bitstream + import cStringIO + + try: + (head, tail) = decoders.SHNDecoder(self.filename).pcm_split() + header = bitstream.BitstreamReader(cStringIO.StringIO(head), 1) + (RIFF, SIZE, WAVE) = header.parse("4b 32u 4b") + if ((RIFF != 'RIFF') or (WAVE != 'WAVE')): + return False + + #if the tail has room for chunks, there must be some foreign ones + if (len(tail) >= 8): + return True + + #otherwise, check the header for foreign chunks + total_size = len(head) - bitstream.format_byte_size("4b 32u 4b") + while (total_size >= 8): + (chunk_id, chunk_size) = header.parse("4b 32u") + total_size -= bitstream.format_byte_size("4b 32u") + if (chunk_id not in ('fmt ', 'data')): + return True + else: + if (chunk_size % 2): + header.skip_bytes(chunk_size + 1) + total_size -= chunk_size + 1 + else: + header.skip_bytes(chunk_size) + total_size -= chunk_size + else: + #no foreign chunks found + return False + except IOError: + return False + + def wave_header_footer(self): + """returns (header, footer) tuple of strings + containing all data before and after the PCM stream + + may raise ValueError if there's a problem with + the header or footer data + may raise IOError if there's a problem reading + header or footer data from the file""" + + from . import decoders + from . import bitstream + import cStringIO + + (head, tail) = decoders.SHNDecoder(self.filename).pcm_split() + header = bitstream.BitstreamReader(cStringIO.StringIO(head), 1) + (RIFF, SIZE, WAVE) = header.parse("4b 32u 4b") + if ((RIFF != 'RIFF') or (WAVE != 'WAVE')): + raise ValueError("invalid wave header") + else: + return (head, tail) + + @classmethod + def from_wave(cls, filename, header, pcmreader, footer, compression=None, + block_size=256, encoding_function=None): + """encodes a new file from wave data + + takes a filename string, header string, + PCMReader object, footer string + 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 + + header + pcm data + footer should always result + in the original wave file being restored + without need for any padding bytes + + may raise EncodingError if some problem occurs when + encoding the input file""" + + from . import (CounterPCMReader, + BufferedPCMReader, + UnsupportedBitsPerSample, + EncodingError) + from .wav import (validate_header, validate_footer) + + if (encoding_function is None): + from .encoders import encode_shn + else: + encode_shn = encoding_function + + if (pcmreader.bits_per_sample not in (8, 16)): + raise UnsupportedBitsPerSample(filename, pcmreader.bits_per_sample) + + #ensure header is valid + try: + (total_size, data_size) = validate_header(header) + except ValueError, err: + raise EncodingError(str(err)) + + counter = CounterPCMReader(pcmreader) + + try: + if (len(footer) == 0): + encode_shn(filename=filename, + pcmreader=BufferedPCMReader(counter), + is_big_endian=False, + signed_samples=pcmreader.bits_per_sample == 16, + header_data=header, + block_size=block_size) + else: + encode_shn(filename=filename, + pcmreader=BufferedPCMReader(counter), + is_big_endian=False, + signed_samples=pcmreader.bits_per_sample == 16, + header_data=header, + footer_data=footer, + block_size=block_size) + + data_bytes_written = counter.bytes_written() + + #ensure output data size matches the "data" chunk's size + if (data_size != data_bytes_written): + from .text import ERR_WAV_TRUNCATED_DATA_CHUNK + raise EncodingError(ERR_WAV_TRUNCATED_DATA_CHUNK) + + #ensure footer validates correctly + try: + validate_footer(footer, data_bytes_written) + except ValueError, err: + raise EncodingError(str(err)) + + #ensure total size is correct + if ((len(header) + data_size + len(footer)) != total_size): + from .text import ERR_WAV_INVALID_SIZE + raise EncodingError(ERR_WAV_INVALID_SIZE) + + return cls(filename) + except IOError, err: + cls.__unlink__(filename) + raise EncodingError(str(err)) + except Exception, err: + cls.__unlink__(filename) + raise err + + def has_foreign_aiff_chunks(self): + """returns True if the audio file contains non-audio AIFF chunks + + during transcoding, if the source audio file has foreign AIFF chunks + and the target audio format supports foreign AIFF chunks, + conversion should be routed through .aiff conversion + to avoid losing those chunks""" + + from . import decoders + from . import bitstream + import cStringIO + + try: + (head, tail) = decoders.SHNDecoder(self.filename).pcm_split() + header = bitstream.BitstreamReader(cStringIO.StringIO(head), 0) + (FORM, SIZE, AIFF) = header.parse("4b 32u 4b") + if ((FORM != 'FORM') or (AIFF != 'AIFF')): + return False + + #if the tail has room for chunks, there must be some foreign ones + if (len(tail) >= 8): + return True + + #otherwise, check the header for foreign chunks + total_size = len(head) - bitstream.format_byte_size("4b 32u 4b") + while (total_size >= 8): + (chunk_id, chunk_size) = header.parse("4b 32u") + total_size -= bitstream.format_byte_size("4b 32u") + if (chunk_id not in ('COMM', 'SSND')): + return True + else: + if (chunk_size % 2): + header.skip_bytes(chunk_size + 1) + total_size -= chunk_size + 1 + else: + header.skip_bytes(chunk_size) + total_size -= chunk_size + else: + #no foreign chunks found + return False + except IOError: + return False + + def aiff_header_footer(self): + """returns (header, footer) tuple of strings + containing all data before and after the PCM stream + + if self.has_foreign_aiff_chunks() is False, + may raise ValueError if the file has no header and footer + for any reason""" + + from . import decoders + from . import bitstream + import cStringIO + + (head, tail) = decoders.SHNDecoder(self.filename).pcm_split() + header = bitstream.BitstreamReader(cStringIO.StringIO(head), 0) + (FORM, SIZE, AIFF) = header.parse("4b 32u 4b") + if ((FORM != 'FORM') or (AIFF != 'AIFF')): + raise ValueError("invalid AIFF header") + else: + return (head, tail) + + @classmethod + def from_aiff(cls, filename, header, pcmreader, footer, compression=None, + block_size=256, encoding_function=None): + """encodes a new file from AIFF data + + takes a filename string, header string, + PCMReader object, footer string + 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 AiffAudio object + + header + pcm data + footer should always result + in the original AIFF file being restored + without need for any padding bytes + + may raise EncodingError if some problem occurs when + encoding the input file""" + + from . import (CounterPCMReader, + BufferedPCMReader, + UnsupportedBitsPerSample, + EncodingError) + from .aiff import (validate_header, validate_footer) + + if (encoding_function is None): + from .encoders import encode_shn + else: + encode_shn = encoding_function + + if (pcmreader.bits_per_sample not in (8, 16)): + raise UnsupportedBitsPerSample(filename, pcmreader.bits_per_sample) + + #ensure header is valid + try: + (total_size, ssnd_size) = validate_header(header) + except ValueError, err: + raise EncodingError(str(err)) + + counter = CounterPCMReader(pcmreader) + + try: + if (len(footer) == 0): + encode_shn(filename=filename, + pcmreader=BufferedPCMReader(counter), + is_big_endian=True, + signed_samples=True, + header_data=header, + block_size=block_size) + else: + encode_shn(filename=filename, + pcmreader=BufferedPCMReader(counter), + is_big_endian=True, + signed_samples=True, + header_data=header, + footer_data=footer, + block_size=block_size) + + ssnd_bytes_written = counter.bytes_written() + + #ensure output data size matches the "SSND" chunk's size + if (ssnd_size != ssnd_bytes_written): + from .text import ERR_AIFF_TRUNCATED_SSND_CHUNK + raise EncodingError(ERR_AIFF_TRUNCATED_SSND_CHUNK) + + #ensure footer validates correctly + try: + validate_footer(footer, ssnd_bytes_written) + except ValueError, err: + raise EncodingError(str(err)) + + #ensure total size is correct + if ((len(header) + ssnd_size + len(footer)) != total_size): + from .text import ERR_AIFF_INVALID_SIZE + raise EncodingError(ERR_AIFF_INVALID_SIZE) + + return cls(filename) + except IOError, err: + cls.__unlink__(filename) + raise EncodingError(str(err)) + except Exception, err: + cls.__unlink__(filename) + raise err + + 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""" + + #A Shorten file cannot contain both RIFF and AIFF chunks + #at the same time. + + import tempfile + from . import WaveAudio + from . import AiffAudio + from . import to_pcm_progress + + if ((self.has_foreign_wave_chunks() and + hasattr(target_class, "from_wave") and + callable(target_class.from_wave))): + return WaveContainer.convert(self, + target_path, + target_class, + compression, + progress) + elif (self.has_foreign_aiff_chunks() and + hasattr(target_class, "from_aiff") and + callable(target_class.from_aiff)): + return AiffContainer.convert(self, + target_path, + target_class, + compression, + progress) + else: + return target_class.from_pcm(target_path, + to_pcm_progress(self, progress), + compression)
View file
audiotools-2.19.tar.gz/audiotools/text.py
Added
@@ -0,0 +1,639 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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 + +"""a text strings module""" + +#Utility usage +USAGE_AT_CONFIG = u"%prog [options]" +USAGE_CD2TRACK = u"%prog [options] [track #] [track #] ..." +USAGE_CDINFO = u"%prog [options]" +USAGE_CDPLAY = u"%prog [track 1] [track 2] ..." +USAGE_TRACKPLAY = u"%prog [track 1] [track 2] ..." +USAGE_COVERDUMP = u"%prog [-d directory] <track>" +USAGE_COVERTAG = u"%prog [OPTIONS] <track 1> [track 2] ..." +USAGE_COVERVIEW = u"%prog [OPTIONS] [track]" +USAGE_DVDA2TRACK = u"%prog [options] [track #] [track #] ..." +USAGE_DVDAINFO = u"%prog [options]" +USAGE_TRACK2CD = u"%prog [options] <track 1> [track 2] ..." +USAGE_TRACK2TRACK = u"%prog [options] <track 1> [track 2] ..." +USAGE_TRACKCAT = u"%prog [options] [-o output] <track 1> [track 2] ..." +USAGE_TRACKCMP = u"%prog <file 1> <file 2>" +USAGE_TRACKCMP_CDIMAGE = u"<CD image> <track 1> <track 2> ..." +USAGE_TRACKCMP_FILES = u"<track 1> <track 2>" +USAGE_TRACKSPLIT = u"%prog [options] [-d directory] <track>" +USAGE_TRACKINFO = u"%prog [options] <track 1> [track 2] ..." +USAGE_TRACKLENGTH = u"%prog <track 1> [track 2] ..." +USAGE_TRACKLINT = \ + u"%prog [options] [--fix] [--undo] [--db file] <track 1> [track 2] ..." +USAGE_TRACKRENAME = u"%prog [options] <track 1> [track 2] ..." +USAGE_TRACKVERIFY = u"%prog <track 1> [track 2] ..." +USAGE_TRACKTAG = u"%prog [options] <track 1> [track 2] ..." + +#Utility Options +OPT_VERBOSE = u"the verbosity level to execute at" +OPT_VERBOSE_AT_CONFIG = u"the new default verbosity level" +OPT_TYPE = u"the type of audio track to create" +OPT_TYPE_AT_CONFIG = u"the default audio type to use, " + \ + u"or the type for a given default quality level" +OPT_TYPE_TRACKVERIFY = u"a type of audio to accept" +OPT_QUALITY = u"the quality to store audio tracks at" +OPT_QUALITY_AT_CONFIG = u"the default quality level for a given audio type" +OPT_DIR = u"the directory to store new audio tracks" +OPT_DIR_IMAGES = u"the directory to store extracted images" +OPT_FORMAT = u"the format string for new filenames" +OPT_METADATA_LOOKUP = u"perform metadata lookup" +OPT_NO_MUSICBRAINZ = u"do not query MusicBrainz for metadata" +OPT_NO_FREEDB = u"do not query FreeDB for metadata" +OPT_INTERACTIVE_METADATA = u"edit metadata interactively" +OPT_INTERACTIVE_OPTIONS = u"edit metadata and output options interactively" +OPT_INTERACTIVE_PLAY = u"play in interactive mode" +OPT_INTERACTIVE_AT_CONFIG = u"edit options interactively" +OPT_OUTPUT_PLAY = u"the method to play audio (choose from: %s)" +OPT_OUTPUT_TRACK2TRACK = u"output filename to use, overriding default and -d" +OPT_OUTPUT_TRACKCAT = u"the output file" +OPT_DEFAULT = u"when multiple choices are available, " + \ + u"select the first one automatically" +OPT_ALBUM_NUMBER = \ + u"the album number of this disc, if it is one of a series of albums" +OPT_ALBUM_TOTAL = \ + u"the total albums of this disc\'s set, if it is one of a series of albums" +OPT_REPLAY_GAIN = u"add ReplayGain metadata to newly created tracks" +OPT_REPLAY_GAIN_TRACKTAG = u"add ReplayGain metadata to tracks" +OPT_NO_REPLAY_GAIN = u"do not add ReplayGain metadata in newly created tracks" +OPT_PLAYBACK_TRACK_GAIN = u"apply track ReplayGain during playback, if present" +OPT_PLAYBACK_ALBUM_GAIN = u"apply album ReplayGain during playback, if present" +OPT_SHUFFLE = u"shuffle tracks" +OPT_PREFIX = u"add a prefix to the output image" +OPT_NO_GTK = u"don't use PyGTK for GUI" +OPT_NO_TKINTER = u"don't use Tkinter for GUI" +OPT_AUDIO_TS = u"location of AUDIO_TS directory" +OPT_DVDA_TITLE = u"DVD-Audio title number to extract tracks from" +OPT_TRACK_START = u"the starting track number of the title being extracted" +OPT_TRACK_TOTAL = \ + u"the total number of tracks, if the extracted title is only a subset" +OPT_SPEED = u"the speed to burn the CD at" +OPT_CUESHEET_TRACK2CD = u"the cuesheet to use for writing tracks" +OPT_JOINT = u"the maximum number of processes to run at a time" +OPT_CUESHEET_TRACKCAT = u"a cuesheet to embed in the output file" +OPT_CUESHEET_TRACKSPLIT = u"the cuesheet to use for splitting track" +OPT_NO_SUMMARY = u"suppress summary output" +OPT_TRACKLINT_FIX = u"perform suggest fixes" +OPT_TRACKLINT_DB = u"undo database file" +OPT_TRACKLINT_UNDO = u"undo performed fixes" +OPT_TRACKTAG_COMMENT_FILE = u"a file containing comment text" +OPT_TRACKTAG_REPLACE = u"completely replace all metadata" +OPT_TRACKTAG_CUESHEET = u"a cuesheet to import or get audio metadata from" +OPT_TRACKTAG_REMOVE_IMAGES = u"remove existing images prior to adding new ones" +OPT_TRACKTAG_FRONT_COVER = u"an image file of the front cover" +OPT_TRACKTAG_BACK_COVER = u"an image file of the back cover" +OPT_TRACKTAG_LEAFLET = u"an image file of a leaflet page" +OPT_TRACKTAG_MEDIA = u"an image file of the media" +OPT_TRACKTAG_OTHER_IMAGE = u"an image file related to the track" +OPT_AT_CONFIG_READ_OFFSET = u"the CD-ROM read offset to use" +OPT_AT_CONFIG_WRITE_OFFSET = u"the CD-ROM write offset to use" +OPT_AT_CONFIG_FS_ENCODING = u"the filesystem's text encoding" +OPT_AT_CONFIG_IO_ENCODING = u"the system's text encoding" +OPT_AT_CONFIG_ID3V2_VERSION = u"which ID3v2 version to use by default, if any" +OPT_AT_CONFIG_ID3V1_VERSION = u"which ID3v1 version to use by default, if any" +OPT_AT_CONFIG_ID3V2_PAD = \ + u"whether or not to pad ID3v2 digit fields to 2 digits" +OPT_CAT_EXTRACTION = u"Extraction Options" +OPT_CAT_CD_LOOKUP = u"CD Lookup Options" +OPT_CAT_DVDA_LOOKUP = u"DVD-A Lookup Options" +OPT_CAT_METADATA = u"Metadata Options" +OPT_CAT_CONVERSION = u"Conversion Options" +OPT_CAT_ENCODING = u"Encoding Options" +OPT_CAT_TEXT = u"Text Options" +OPT_CAT_IMAGE = u"Image Options" +OPT_CAT_REMOVAL = u"Removal Options" +OPT_CAT_SYSTEM = u"System Options" +OPT_CAT_TRANSCODING = u"Transcoding Options" +OPT_CAT_ID3 = u"ID3 Options" +OPT_CAT_REPLAYGAIN = u"ReplayGain Options" +OPT_CAT_BINARIES = u"Binaries Options" + +#MetaData Fields +METADATA_TRACK_NAME = u"Track Name" +METADATA_TRACK_NUMBER = u"Track Number" +METADATA_TRACK_TOTAL = u"Track Total" +METADATA_ALBUM_NAME = u"Album Name" +METADATA_ARTIST_NAME = u"Artist Name" +METADATA_PERFORMER_NAME = u"Performer Name" +METADATA_COMPOSER_NAME = u"Composer Name" +METADATA_CONDUCTOR_NAME = u"Conductor Name" +METADATA_MEDIA = u"Media" +METADATA_ISRC = u"ISRC" +METADATA_CATALOG = u"Catalog Number" +METADATA_COPYRIGHT = u"Copyright" +METADATA_PUBLISHER = u"Publisher" +METADATA_YEAR = u"Release Year" +METADATA_DATE = u"Recording Date" +METADATA_ALBUM_NUMBER = u"Album Number" +METADATA_ALBUM_TOTAL = u"Album Total" +METADATA_COMMENT = u"Comment" + +#Derived MetaData Fields +METADATA_SUFFIX = u"File Name Suffix" +METADATA_ALBUM_TRACK_NUMBER = u"Combined Album and Track Number" +METADATA_BASENAME = u"File Name Without Suffix" + +#ReplayGain +RG_ADDING_REPLAYGAIN = u"Adding ReplayGain" +RG_APPLYING_REPLAYGAIN = u"Applying ReplayGain" +RG_ADDING_REPLAYGAIN_WAIT = \ + u"Adding ReplayGain metadata. This may take some time." +RG_APPLYING_REPLAYGAIN_WAIT = u"Applying ReplayGain. This may take some time." +RG_REPLAYGAIN_ADDED = u"ReplayGain added" +RG_REPLAYGAIN_APPLIED = u"ReplayGain applied" + +#Labels +LAB_ENCODE = u"%(source)s -> %(destination)s" +LAB_PICTURE = u"Picture" +LAB_T_OPTIONS = u"Please use the -t option to specify %s" +LAB_AVAILABLE_COMPRESSION_TYPES = u"Available quality modes for \"%s\":" +LAB_AVAILABLE_FORMATS = u"Available output formats:" +LAB_OUTPUT_FORMATS = u"Output Formats" +LAB_OUTPUT_TYPE = u"type" +LAB_OUTPUT_QUALITY = u"quality" +LAB_OUTPUT_TYPE_DESCRIPTION = u"name" +LAB_OUTPUT_QUALITY_DESCRIPTION = u"description" +LAB_SUPPORTED_FIELDS = u"Supported fields are:" +LAB_CD2TRACK_PROGRESS = u"track %(track_number)2.2d -> %(filename)s" +LAB_CD2TRACK_LOG = u"Rip log : " +LAB_CD2TRACK_APPLY = u"Extract Tracks" +LAB_TOTAL_TRACKS = u"Total Tracks" +LAB_TOTAL_LENGTH = u"Total Length" +LAB_TRACK_LENGTH = u"%d:%2.2d" +LAB_TRACK_LENGTH_FRAMES = u"%2d:%2.2d (%d frames)" +LAB_FREEDB_ID = u"FreeDB disc ID" +LAB_MUSICBRAINZ_ID = u"MusicBrainz disc ID" +LAB_CDINFO_LENGTH = u"Length" +LAB_CDINFO_FRAMES = u"Frames" +LAB_CDINFO_OFFSET = u"Offset" +LAB_PLAY_BUTTON = u"Play" +LAB_PAUSE_BUTTON = u"Pause" +LAB_NEXT_BUTTON = u"Next" +LAB_PREVIOUS_BUTTON = u"Prev" +LAB_APPLY_BUTTON = u"Apply" +LAB_QUIT_BUTTON = u"Quit" +LAB_CANCEL_BUTTON = u"Cancel" +LAB_NEXT_BUTTON = u"Next" +LAB_PREVIOUS_BUTTON = u"Previous" +LAB_BROWSE_BUTTON = u"browse" +LAB_FIELDS_BUTTON = u"fields" +LAB_PLAY_STATUS = u"%(count)d tracks, %(min)d:%(sec)2.2d minutes" +LAB_PLAY_STATUS_1 = u"%(count)d track, %(min)d:%(sec)2.2d minutes" +LAB_PLAY_TRACK = u"Track" +LAB_CLOSE = u"Close" +LAB_TRACK = u"track" +LAB_X_OF_Y = u"%d / %d" +LAB_TRACK_X_OF_Y = u"track %2.1d / %d" +LAB_CHOOSE_FILE = u"Choose an audio file" +LAB_CHOOSE_DIRECTORY = u"Choose directory" +LAB_ADD_FIELD = u"Add field" +LAB_COVERVIEW_ABOUT = \ + u"A viewer for displaying images embedded in audio files." +LAB_AUDIOTOOLS_URL = u"http://audiotools.sourceforge.net" +LAB_BYTE_SIZE = u"%d bytes" +LAB_DIMENSIONS = u"%d \u00D7 %d" +LAB_BITS_PER_PIXEL = u"%d bits" +LAB_SELECT_BEST_MATCH = u"Select Best Match" +LAB_TRACK_METADATA = u"Track Metadata" +LAB_DVDA_TRACK = u"title %(title_number)d - track %(track_number)2.2d" +LAB_DVDA_TITLE = u"Title %d" +LAB_DVDA_TRACKS = u" (%d tracks)" +LAB_DVDA_TITLE_INFO = \ + u"%(minutes)2.2d:%(seconds)2.2d " + \ + u"%(channels)dch %(rate)s %(bits)d-bit " + \ + u"%(type)s" +LAB_DVDAINFO_TITLE = u"Title" +LAB_DVDAINFO_TRACK = u"Track" +LAB_DVDAINFO_LENGTH = u"Length" +LAB_DVDAINFO_FILENAME = u"Filename" +LAB_DVDAINFO_STARTSECTOR = u"Start Sector" +LAB_DVDAINFO_ENDSECTOR = u"End Sector" +LAB_DVDAINFO_TICKS = u"PTS Ticks" +LAB_DVDA2TRACK_APPLY = u"Extract Tracks" +LAB_CONVERTING_FILE = u"Converting audio file" +LAB_TRACK2TRACK_APPLY = u"Convert Tracks" +LAB_TRACK2TRACK_APPLY_1 = u"Convert Track" +LAB_TRACK2TRACK_NEXT = u"Next Album" +LAB_TRACKCAT_INPUT = u"%d tracks" +LAB_TRACKCAT_APPLY = u"Concatenate Tracks" +LAB_TRACKCMP_CMP = u"%(file1)s <> %(file2)s" +LAB_TRACKCMP_OK = u"OK" +LAB_TRACKCMP_MISMATCH = u"differ at PCM frame %(frame_number)d" +LAB_TRACKCMP_TYPE_MISMATCH = u"must be either files or directories" +LAB_TRACKCMP_ERROR = u"error" +LAB_TRACKCMP_MISSING = u"missing" +LAB_TRACKCMP_RESULTS = u"Results:" +LAB_TRACKCMP_HEADER_SUCCESS = u"success" +LAB_TRACKCMP_HEADER_FAILURE = u"failure" +LAB_TRACKCMP_HEADER_TOTAL = u"total" +LAB_TRACKINFO_BITRATE = u"%(bitrate)4.4s kbps: %(filename)s" +LAB_TRACKINFO_PERCENTAGE = u"%(percentage)3.3s%%: %(filename)s" +LAB_TRACKINFO_ATTRIBS = \ + u"%(minutes)2.2d:%(seconds)2.2d " + \ + u"%(channels)dch %(rate)s %(bits)d-bit: %(filename)s" +LAB_TRACKINFO_REPLAYGAIN = u"ReplayGain:" +LAB_TRACKINFO_TRACK_GAIN = u"Track Gain : " +LAB_TRACKINFO_TRACK_PEAK = u"Track Peak : " +LAB_TRACKINFO_ALBUM_GAIN = u"Album Gain : " +LAB_TRACKINFO_ALBUM_PEAK = u"Album Peak : " +LAB_TRACKINFO_CUESHEET = u"Cuesheet:" +LAB_TRACKINFO_CUESHEET_TRACK = u"track" +LAB_TRACKINFO_CUESHEET_LENGTH = u"length" +LAB_TRACKINFO_CUESHEET_ISRC = u"ISRC" +LAB_TRACKINFO_CHANNELS = u"Assigned Channels:" +LAB_TRACKINFO_CHANNEL = u"channel %(channel_number)d - %(channel_name)s" +LAB_TRACKINFO_UNDEFINED = u"undefined" +LAB_TRACKLENGTH = u"%(hours)d:%(minutes)2.2d:%(seconds)2.2d" +LAB_TRACKLENGTH_FILE_FORMAT = u"format" +LAB_TRACKLENGTH_FILE_COUNT = u"count" +LAB_TRACKLENGTH_FILE_LENGTH = u"length" +LAB_TRACKLENGTH_FILE_SIZE = u"size" +LAB_TRACKLENGTH_FILE_TOTAL = u"total" +LAB_TRACKLINT_RESTORED = u"Restored: %s" +LAB_TRACKLINT_MESSAGE = u"* %(filename)s: %(message)s" +LAB_TRACKRENAME_RENAME = u"Rename Files" +LAB_TRACKSPLIT_APPLY = u"Split Track" +LAB_TRACKVERIFY_RESULTS = u"Results:" +LAB_TRACKVERIFY_RESULT_FORMAT = u"format" +LAB_TRACKVERIFY_RESULT_SUCCESS = u"success" +LAB_TRACKVERIFY_RESULT_FAILURE = u"failure" +LAB_TRACKVERIFY_RESULT_TOTAL = u"total" +LAB_TRACKVERIFY_RESULT = u"%(path)s : %(result)s" +LAB_TRACKVERIFY_OK = u"OK" +LAB_TRACKTAG_UPDATING = u"updating tracks" +LAB_TRACKTAG_UPDATED = u"%d tracks updated" +LAB_TRACKTAG_UPDATED_1 = u"1 track updated" +LAB_TRACKTAG_APPLY = u"Apply" +LAB_KEY_NEXT = u" - next %s" +LAB_KEY_PREVIOUS = u" - previous %s" +LAB_KEY_SELECT = u" - select" +LAB_KEY_TOGGLE_OPEN = u" - toggle open" +LAB_KEY_CANCEL = u" - cancel" +LAB_KEY_CLEAR_FORMAT = u" - clear format" +LAB_TRACKTAG_UPDATE_TRACK_NAME = u"the name of the track" +LAB_TRACKTAG_UPDATE_ARTIST_NAME = u"the name of the artist" +LAB_TRACKTAG_UPDATE_PERFORMER_NAME = u"the name of the performer" +LAB_TRACKTAG_UPDATE_COMPOSER_NAME = u"the name of the composer" +LAB_TRACKTAG_UPDATE_CONDUCTOR_NAME = u"the name of the conductor" +LAB_TRACKTAG_UPDATE_ALBUM_NAME = u"the name of the album" +LAB_TRACKTAG_UPDATE_CATALOG = u"the catalog number of the album" +LAB_TRACKTAG_UPDATE_TRACK_NUMBER = u"the number of the track in the album" +LAB_TRACKTAG_UPDATE_TRACK_TOTAL = \ + u"the total number of tracks in the album" +LAB_TRACKTAG_UPDATE_ALBUM_NUMBER = \ + u"the number of the album in a set of albums" +LAB_TRACKTAG_UPDATE_ALBUM_TOTAL = \ + u"the total number of albums in a set of albums" +LAB_TRACKTAG_UPDATE_ISRC = u"the ISRC of the track" +LAB_TRACKTAG_UPDATE_PUBLISHER = u"the publisher of the album" +LAB_TRACKTAG_UPDATE_MEDIA = u"the media type of the album, such as \"CD\"" +LAB_TRACKTAG_UPDATE_YEAR = u"the year of release" +LAB_TRACKTAG_UPDATE_DATE = u"the date of recording" +LAB_TRACKTAG_UPDATE_COPYRIGHT = u"copyright information" +LAB_TRACKTAG_UPDATE_COMMENT = u"a text comment" +LAB_TRACKTAG_REMOVE_TRACK_NAME = u"remove track name" +LAB_TRACKTAG_REMOVE_ARTIST_NAME = u"remove track artist" +LAB_TRACKTAG_REMOVE_PERFORMER_NAME = u"remove track performer" +LAB_TRACKTAG_REMOVE_COMPOSER_NAME = u"remove track composer" +LAB_TRACKTAG_REMOVE_CONDUCTOR_NAME = u"remove track conductor" +LAB_TRACKTAG_REMOVE_ALBUM_NAME = u"remove album name" +LAB_TRACKTAG_REMOVE_CATALOG = u"remove catalog number" +LAB_TRACKTAG_REMOVE_TRACK_NUMBER = u"remove track number" +LAB_TRACKTAG_REMOVE_TRACK_TOTAL = u"remove total number of tracks" +LAB_TRACKTAG_REMOVE_ALBUM_NUMBER = u"remove album number" +LAB_TRACKTAG_REMOVE_ALBUM_TOTAL = u"remove total number of albums" +LAB_TRACKTAG_REMOVE_ISRC = u"remove ISRC" +LAB_TRACKTAG_REMOVE_PUBLISHER = u"remove publisher" +LAB_TRACKTAG_REMOVE_MEDIA = u"remove album's media type" +LAB_TRACKTAG_REMOVE_YEAR = u"remove release year" +LAB_TRACKTAG_REMOVE_DATE = u"remove recording date" +LAB_TRACKTAG_REMOVE_COPYRIGHT = u"remove copyright information" +LAB_TRACKTAG_REMOVE_COMMENT = u"remove text comment" +LAB_AT_CONFIG_CD_BURNING = u"CD Burning via track2cd" +LAB_AT_CONFIG_WITHOUT_CUE = u"without cue" +LAB_AT_CONFIG_WITH_CUE = u"with cue" +LAB_AT_CONFIG_YES = u"yes" +LAB_AT_CONFIG_NO = u"no" +LAB_AT_CONFIG_SYS_CONFIG = u"System configuration:" +LAB_AT_CONFIG_USE_MUSICBRAINZ = u"Use MusicBrainz service" +LAB_AT_CONFIG_MUSICBRAINZ_SERVER = u"Default MusicBrainz server" +LAB_AT_CONFIG_MUSICBRAINZ_PORT = u"Default MusicBrainz port" +LAB_AT_CONFIG_USE_FREEDB = u"Use FreeDB service" +LAB_AT_CONFIG_FREEDB_SERVER = u"Default FreeDB server" +LAB_AT_CONFIG_FREEDB_PORT = u"Default FreeDB port" +LAB_AT_CONFIG_DEFAULT_CDROM = u"Default CD-ROM device" +LAB_AT_CONFIG_CDROM_READ_OFFSET = u"CD-ROM sample read offset" +LAB_AT_CONFIG_CDROM_WRITE_OFFSET = u"CD-ROM sample write offset" +LAB_AT_CONFIG_JOBS = u"Default simultaneous jobs" +LAB_AT_CONFIG_VERBOSITY = u"Default verbosity level" +LAB_AT_CONFIG_AUDIO_OUTPUT = u"Audio output" +LAB_AT_CONFIG_FS_ENCODING = u"Filesystem text encoding" +LAB_AT_CONFIG_IO_ENCODING = u"TTY text encoding" +LAB_AT_CONFIG_ID3V2_VERSION = u"ID3v2 tag version" +LAB_AT_CONFIG_ID3V2_ID3V22 = u"ID3v2.2" +LAB_AT_CONFIG_ID3V2_ID3V23 = u"ID3v2.3" +LAB_AT_CONFIG_ID3V2_ID3V24 = u"ID3v2.4" +LAB_AT_CONFIG_ID3V2_NONE = u"no ID3v2 tags" +LAB_AT_CONFIG_ID3V2_PADDING = u"ID3v2 digit padding" +LAB_AT_CONFIG_ID3V2_PADDING_YES = u"padded (\"01\", \"02\", \u2026)" +LAB_AT_CONFIG_ID3V2_PADDING_NO = u"not padded (\"1\", \"2\", \u2026)" +LAB_AT_CONFIG_ID3V1_VERSION = u"ID3v1 tag version" +LAB_AT_CONFIG_ID3V1_ID3V11 = u"ID3v1.1" +LAB_AT_CONFIG_ID3V1_NONE = u"no ID3v1 tags" +LAB_AT_CONFIG_ADD_REPLAY_GAIN = u"Add ReplayGain by default" +LAB_AT_CONFIG_FORMAT = u"File name format : %s" +LAB_AT_CONFIG_FILE_WRITTEN = u"* \"%s\" written" +LAB_AT_CONFIG_FOUND = u"found" +LAB_AT_CONFIG_NOT_FOUND = u"not found" +LAB_AT_CONFIG_TYPE = u" type " +LAB_AT_CONFIG_BINARIES = u"Binaries" +LAB_AT_CONFIG_QUALITY = u" quality " +LAB_AT_CONFIG_REPLAY_GAIN = u" ReplayGain " +LAB_AT_CONFIG_DEFAULT = u"Default" +LAB_AT_CONFIG_TYPE = u"Type" +LAB_AT_CONFIG_DEFAULT_QUALITY = u"Default Quality" +LAB_OUTPUT_OPTIONS = u"Output Options" +LAB_OPTIONS_OUTPUT = u"Output" +LAB_OPTIONS_OUTPUT_DIRECTORY = u"Dir" +LAB_OPTIONS_FILENAME_FORMAT = u"Format" +LAB_OPTIONS_AUDIO_CLASS = u"Type" +LAB_OPTIONS_AUDIO_QUALITY = u"Quality" +LAB_OPTIONS_OUTPUT_FILES = u"Output Files" +LAB_OPTIONS_OUTPUT_FILES_1 = u"Output File" + +#Compression settings +COMP_FLAC_0 = u"least compresson, fastest compression speed" +COMP_FLAC_8 = u"most compression, slowest compression speed" +COMP_NERO_LOW = u"lowest quality, corresponds to neroAacEnc -q 0.4" +COMP_NERO_HIGH = u"highest quality, corresponds to neroAacEnc -q 1" +COMP_LAME_0 = u"high quality, larger files, corresponds to lame's -V0" +COMP_LAME_6 = u"lower quality, smaller files, corresponds to lame's -V6" +COMP_LAME_MEDIUM = u"corresponds to lame's --preset medium" +COMP_LAME_STANDARD = u"corresponds to lame's --preset standard" +COMP_LAME_EXTREME = u"corresponds to lame's --preset extreme" +COMP_LAME_INSANE = u"corresponds to lame's --preset insane" +COMP_TWOLAME_64 = u"total bitrate of 64kbps" +COMP_TWOLAME_384 = u"total bitrate of 384kbps" +COMP_VORBIS_0 = u"very low quality, corresponds to oggenc -q 0" +COMP_VORBIS_10 = u"very high quality, corresponds to oggenc -q 10" +COMP_WAVPACK_VERYFAST = u"fastest encode/decode, worst compression" +COMP_WAVPACK_VERYHIGH = u"slowest encode/decode, best compression" + +#Errors +ERR_1_FILE_REQUIRED = u"you must specify exactly 1 supported audio file" +ERR_FILES_REQUIRED = u"you must specify at least 1 supported audio file" +ERR_UNSUPPORTED_CHANNEL_MASK = \ + u"unable to write \"%(target_filename)s\" " + \ + u"with channel assignment \"%(assignment)s\"" +ERR_UNSUPPORTED_BITS_PER_SAMPLE = \ + u"unable to write \"%(target_filename)s\" " + \ + u"with %(bps)d bits per sample" +ERR_UNSUPPORTED_CHANNEL_COUNT = \ + u"unable to write \"%(target_filename)s\" " + \ + u"with %(channels)d channel input" +ERR_DUPLICATE_FILE = u"file \"%s\" included more than once" +ERR_OUTPUT_IS_INPUT = u"\"%s\" cannot be both input and output file" +ERR_OPEN_IOERROR = u"unable to open \"%s\"" +ERR_ENCODING_ERROR = u"unable to write \"%s\"" +ERR_UNSUPPORTED_AUDIO_TYPE = u"unsupported audio type \"%s\"" +ERR_UNSUPPORTED_FILE = u"unsupported File '%s'" +ERR_INVALID_FILE = u"invalid File '%s'" +ERR_INVALID_SAMPLE_RATE = u"invalid sample rate" +ERR_INVALID_CHANNEL_COUNT = u"invalid channel count" +ERR_INVALID_BITS_PER_SAMPLE = u"invalid bits-per-sample" +ERR_AMBIGUOUS_AUDIO_TYPE = u"ambiguous suffix type \"%s\"" +ERR_CHANNEL_COUNT_MASK_MISMATCH = u"channel count and channel mask mismatch" +ERR_NO_PCMREADERS = u"you must have at least 1 PCMReader" +ERR_PICTURES_UNSUPPORTED = u"this MetaData type does not support images" +ERR_UNKNOWN_FIELD = u"unknown field \"%s\" in file format" +ERR_INVALID_FILENAME_FORMAT = u"invalid filename format string" +ERR_FOREIGN_METADATA = u"metadata not from audio file" +ERR_AIFF_NOT_AIFF = u"not an AIFF file" +ERR_AIFF_INVALID_AIFF = u"invalid AIFF file" +ERR_AIFF_INVALID_CHUNK_ID = u"invalid AIFF chunk ID" +ERR_AIFF_INVALID_CHUNK = u"invalid AIFF chunk" +ERR_AIFF_MULTIPLE_COMM_CHUNKS = u"multiple COMM chunks found" +ERR_AIFF_PREMATURE_SSND_CHUNK = u"SSND chunk found before fmt" +ERR_AIFF_MULTIPLE_SSND_CHUNKS = u"multiple SSND chunks found" +ERR_AIFF_TRUNCATED_CHUNK = u"truncated %s chunk found" +ERR_AIFF_NO_COMM_CHUNK = u"COMM chunk not found" +ERR_AIFF_NO_SSND_CHUNK = u"SSND chunk not found" +ERR_AIFF_HEADER_EXTRA_SSND = u"extra data after SSND chunk header" +ERR_AIFF_HEADER_MISSING_SSND = u"missing data in SSND chunk header" +ERR_AIFF_HEADER_IOERROR = u"I/O error reading header data" +ERR_AIFF_FOOTER_IOERROR = u"I/O error reading footer data" +ERR_AIFF_TRUNCATED_SSND_CHUNK = u"premature end of SSND chunk" +ERR_AIFF_INVALID_SIZE = u"total aiff file size mismatch" +ERR_APE_INVALID_HEADER = u"invalid Monkey's Audio header" +ERR_AU_INVALID_HEADER = u"invalid Sun AU header" +ERR_AU_UNSUPPORTED_FORMAT = u"unsupported Sun AU format" +ERR_CUE_INVALID_TOKEN = u"invalid token at char %d" +ERR_CUE_ERROR = u"%(error)s at line %(line)d" +ERR_CUE_INVALID_TRACK_NUMBER = u"invalid track number" +ERR_CUE_INVALID_TRACK_TYPE = u"invalid track type" +ERR_CUE_MISSING_VALUE = u"missing value" +ERR_CUE_EXCESS_DATA = u"excess data" +ERR_CUE_MISSING_FILENAME = u"missing filename" +ERR_CUE_MISSING_FILETYPE = u"missing file type" +ERR_CUE_INVALID_TAG = u"invalid tag %(tag)s at line %(line)d" +ERR_CUE_INVALID_DATA = u"invalid data" +ERR_CUE_INVALID_FLAG = u"invalid flag" +ERR_CUE_INVALID_TIMESTAMP = u"invalid timestamp" +ERR_CUE_INVALID_INDEX_NUMBER = u"invalid index number" +ERR_CUE_MISSING_TAG = u"missing tag at line %d" +ERR_CUE_IOERROR = u"unable to read cuesheet" +ERR_CUE_INVALID_FORMAT = u"cuesheet not formatted for disc images" +ERR_DVDA_IOERROR_AUDIO_TS = u"unable to open AUDIO_TS.IFO" +ERR_DVDA_INVALID_AUDIO_TS = u"invalid AUDIO_TS.IFO" +ERR_DVDA_IOERROR_ATS = u"unable to open ATS_%2.2d_0.IFO" +ERR_DVDA_INVALID_ATS = u"invalid ATS_%2.2d_0.IFO" +ERR_DVDA_INVALID_SECTOR_POINTER = u"invalid sector pointer" +ERR_DVDA_NO_TRACK_SECTOR = u"unable to find track sector in AOB files" +ERR_DVDA_INVALID_AOB_SYNC = u"invalid AOB sync bytes" +ERR_DVDA_INVALID_AOB_MARKER = u"invalid AOB marker bits" +ERR_DVDA_INVALID_AOB_START = u"invalid AOB packet start code" +ERR_FLAC_RESERVED_BLOCK = u"reserved metadata block type %d" +ERR_FLAC_INVALID_BLOCK = u"invalid metadata block type" +ERR_FLAC_INVALID_FILE = u"Invalid FLAC file" +ERR_OGG_INVALID_MAGIC_NUMBER = u"invalid Ogg magic number" +ERR_OGG_INVALID_VERSION = u"invalid Ogg version" +ERR_OGG_CHECKSUM_MISMATCH = u"Ogg page checksum mismatch" +ERR_OGGFLAC_INVALID_PACKET_BYTE = u"invalid packet byte" +ERR_OGGFLAC_INVALID_OGG_SIGNATURE = u"invalid Ogg signature" +ERR_OGGFLAC_INVALID_MAJOR_VERSION = u"invalid major version" +ERR_OGGFLAC_INVALID_MINOR_VERSION = u"invalid minor version" +ERR_OGGFLAC_VALID_FLAC_SIGNATURE = u"invalid FLAC signature" +ERR_IMAGE_UNKNOWN_TYPE = u"unknown image type" +ERR_IMAGE_INVALID_JPEG_MARKER = u"invalid JPEG segment marker" +ERR_IMAGE_IOERROR_JPEG = "I/O error reading JPEG data" +ERR_IMAGE_INVALID_PNG = u"invalid PNG" +ERR_IMAGE_IOERROR_PNG = "I/O error reading PNG data" +ERR_IMAGE_INVALID_PLTE = u"invalid PLTE chunk length" +ERR_IMAGE_INVALID_BMP = u"invalid BMP" +ERR_IMAGE_IOERROR_BMP = "I/O error reading BMP data" +ERR_IMAGE_INVALID_TIFF = u"invalid TIFF" +ERR_IMAGE_IOERROR_TIFF = u"I/O error reading TIFF data" +ERR_IMAGE_INVALID_GIF = u"invalid GIF" +ERR_IMAGE_IOERROR_GIF = u"I/O error reading GIF data" +ERR_M4A_IOERROR = u"I/O error opening M4A file" +ERR_M4A_MISSING_MDIA = u"required mdia atom not found" +ERR_M4A_MISSING_STSD = u"required stsd atom not found" +ERR_M4A_INVALID_MP4A = u"invalid mp4a atom" +ERR_M4A_MISSING_MDHD = u"required mdhd atom not found" +ERR_M4A_UNSUPPORTED_MDHD = u"unsupported mdhd version" +ERR_M4A_INVALID_MDHD = u"invalid mdhd atom" +ERR_M4A_INVALID_LEAF_ATOMS = u"leaf atoms must be a list" +ERR_ALAC_IOERROR = u"I/O error opening ALAC file" +ERR_ALAC_INVALID_ALAC = u"invalid alac atom" +ERR_MP3_FRAME_NOT_FOUND = u"MP3 frame not found" +ERR_MP3_INVALID_SAMPLE_RATE = u"invalid sample rate" +ERR_MP3_INVALID_BIT_RATE = u"invalid bit rate" +ERR_TOC_NO_HEADER = u"no CD_DA TOC header found" +ERR_VORBIS_INVALID_TYPE = u"invalid Vorbis type" +ERR_VORBIS_INVALID_HEADER = u"invalid Vorbis header" +ERR_VORBIS_INVALID_VERSION = u"invalid Vorbis version" +ERR_VORBIS_INVALID_FRAMING_BIT = u"invalid framing bit" +ERR_OPUS_INVALID_TYPE = u"invalid Opus header" +ERR_OPUS_INVALID_VERSION = u"invalid Opus version" +ERR_OPUS_INVALID_CHANNELS = u"invalid Open channel count" +ERR_WAV_NOT_WAVE = u"not a RIFF WAVE file" +ERR_WAV_INVALID_WAVE = u"invalid RIFF WAVE file" +ERR_WAV_NO_DATA_CHUNK = u"data chunk not found" +ERR_WAV_INVALID_CHUNK = u"invalid RIFF WAVE chunk ID" +ERR_WAV_MULTIPLE_FMT = u"multiple fmt chunks found" +ERR_WAV_PREMATURE_DATA = u"data chunk found before fmt" +ERR_WAV_MULTIPLE_DATA = u"multiple data chunks found" +ERR_WAV_TRUNCATED_CHUNK = u"truncated %s chunk found" +ERR_WAV_NO_FMT_CHUNK = u"fmt chunk not found" +ERR_WAV_HEADER_EXTRA_DATA = u"%d bytes found after data chunk header" +ERR_WAV_HEADER_IOERROR = u"I/O error reading header data" +ERR_WAV_FOOTER_IOERROR = u"I/O error reading footer data" +ERR_WAV_TRUNCATED_DATA_CHUNK = u"premature end of data chunk" +ERR_WAV_INVALID_SIZE = u"total wave file size mismatch" +ERR_WAVPACK_INVALID_HEADER = u"WavPack header ID invalid" +ERR_WAVPACK_UNSUPPORTED_FMT = u"unsupported FMT compression" +ERR_WAVPACK_INVALID_FMT = u"invalid FMT chunk" +ERR_WAVPACK_NO_FMT = u"FMT chunk not found in WavPack" +ERR_NO_COMPRESSION_MODES = u"Audio type \"%s\" has no quality modes" +ERR_UNSUPPORTED_COMPRESSION_MODE = \ + u"\"%(quality)s\" is not a supported compression mode " + \ + u"for type \"%(type)s\"" +ERR_INVALID_CDDA = u". Is that an audio cd?" +ERR_NO_CDDA = u"no CD in drive" +ERR_NO_EMPTY_CDDA = u"no audio tracks found on CD" +ERR_NO_OUTPUT_FILE = u"you must specify an output file" +ERR_DUPLICATE_OUTPUT_FILE = u"output file \"%s\" occurs more than once" +ERR_URWID_REQUIRED = u"Urwid 1.0 or better is required for interactive mode" +ERR_GET_URWID1 = \ + u"Please download and install urwid from http://excess.org/urwid/" +ERR_GET_URWID2 = u"or your system's package manager." +ERR_TERMIOS_ERROR = u"unable to get tty settings" +ERR_TERMIOS_SUGGESTION = \ + u"if piping arguments via xargs(1), try:" +ERR_NO_GUI = u"neither PyGTK nor Tkinter is available" +ERR_NO_AUDIO_TS = \ + u"you must specify the DVD-Audio's AUDIO_TS directory with -A" +ERR_INVALID_TITLE_NUMBER = u"title number must be greater than 0" +ERR_INVALID_JOINT = u"you must run at least 1 process at a time" +ERR_NO_CDRDAO = u"unable to find \"cdrdao\" executable" +ERR_GET_CDRDAO = u"please install \"cdrdao\" to burn CDs" +ERR_NO_CDRECORD = u"unable to find \"cdrecord\" executable" +ERR_GET_CDRECORD = u"please install \"cdrecord\" to burn CDs" +ERR_SAMPLE_RATE_MISMATCH = u"all audio files must have the same sample rate" +ERR_CHANNEL_COUNT_MISMATCH = \ + u"all audio files must have the same channel count" +ERR_CHANNEL_MASK_MISMATCH = \ + u"all audio files must have the same channel assignment" +ERR_BPS_MISMATCH = u"all audio files must have the same bits per sample" +ERR_TRACK2CD_INVALIDFILE = u"not all files are valid. Unable to write CD" +ERR_TRACK2TRACK_O_AND_D = u"-o and -d options are not compatible" +ERR_TRACK2TRACK_O_AND_D_SUGGESTION = \ + u"please specify either -o or -d but not both" +ERR_TRACK2TRACK_O_AND_FORMAT = u"--format has no effect when used with -o" +ERR_TRACK2TRACK_O_AND_MULTIPLE = \ + u"you may specify only 1 input file for use with -o" +ERR_TRACKCMP_TYPE_MISMATCH = u"both files to be compared must be audio files" +ERR_TRACKSPLIT_NO_CUESHEET = u"you must specify a cuesheet to split audio file" +ERR_TRACKSPLIT_OVERLONG_CUESHEET = u"cuesheet too long for track being split" +ERR_NO_UNDO_DB = u"cannot perform undo without undo db" +ERR_RENAME = u"unable to rename \"%(source)s\" to \"%(target)s\"" +ERR_INVALID_IMAGE = u"%(filename)s: %(message)s" +ERR_TRACKTAG_COMMENT_NOT_UTF8 = \ + u"comment file \"%s\" does not appear to be UTF-8 text" +ERR_TRACKTAG_COMMENT_IOERROR = u"unable to open comment file \"%s\"" +ERR_OUTPUT_DUPLICATE_NAME = u"all output tracks must have different names" +ERR_OUTPUT_OUTPUTS_ARE_INPUT = \ + u"output tracks must have different names than input tracks" +ERR_OUTPUT_INVALID_FORMAT = u"output tracks must have valid format string" + +#Cleaning messages +CLEAN_REMOVE_DUPLICATE_TAG = u"removed duplicate tag %(field)s" +CLEAN_REMOVE_TRAILING_WHITESPACE = \ + u"removed trailing whitespace from %(field)s" +CLEAN_REMOVE_LEADING_WHITESPACE = u"removed leading whitespace from %(field)s" +CLEAN_REMOVE_LEADING_WHITESPACE_ZEROES = \ + u"removed leading whitespace/zeroes from %(field)s" +CLEAN_REMOVE_LEADING_ZEROES = u"removed leading zeroes from %(field)s" +CLEAN_ADD_LEADING_ZEROES = u"added leading zeroes to %(field)s" +CLEAN_REMOVE_EMPTY_TAG = u"removed empty field %(field)s" +CLEAN_FIX_TAG_FORMATTING = u"fixed formatting for %(field)s" +CLEAN_FIX_IMAGE_FIELDS = u"fixed embedded image metadata fields" +CLEAN_AIFF_MULTIPLE_COMM_CHUNKS = u"removed duplicate COMM chunk" +CLEAN_AIFF_REORDERED_SSND_CHUNK = u"moved COMM chunk after SSND chunk" +CLEAN_AIFF_MULTIPLE_SSND_CHUNKS = u"removed duplicate SSND chunk" +CLEAN_FLAC_REORDERED_STREAMINFO = u"moved STREAMINFO to first block" +CLEAN_FLAC_MULITPLE_STREAMINFO = u"removed redundant STREAMINFO block" +CLEAN_FLAC_MULTIPLE_VORBISCOMMENT = u"removed redundant VORBIS_COMMENT block" +CLEAN_FLAC_MULTIPLE_SEEKTABLE = u"removed redundant SEEKTABLE block" +CLEAN_FLAC_MULTIPLE_CUESHEET = u"removed redundant CUESHEET block" +CLEAN_FLAC_UNDEFINED_BLOCK = u"removed undefined block" +CLEAN_FLAC_REMOVE_SEEKPOINTS = u"removed empty seekpoints from seektable" +CLEAN_FLAC_REORDER_SEEKPOINTS = u"reordered seektable to be in ascending order" +CLEAN_FLAC_REMOVE_ID3V2 = u"removed ID3v2 tag" +CLEAN_FLAC_REMOVE_ID3V1 = u"removed ID3v1 tag" +CLEAN_FLAC_POPULATE_MD5 = u"populated empty MD5SUM" +CLEAN_FLAC_ADD_CHANNELMASK = u"added WAVEFORMATEXTENSIBLE_CHANNEL_MASK" +CLEAN_FLAC_FIX_SEEKTABLE = u"fixed invalid SEEKTABLE" +CLEAN_WAV_MULTIPLE_FMT_CHUNKS = u"removed duplicate fmt chunk" +CLEAN_WAV_REORDERED_DATA_CHUNK = u"moved data chunk after fmt chunk" +CLEAN_WAV_MULTIPLE_DATA_CHUNKS = u"removed multiple data chunk" + +#Channel names +MASK_FRONT_LEFT = u"front left" +MASK_FRONT_RIGHT = u"front right" +MASK_FRONT_CENTER = u"front center" +MASK_LFE = u"low frequency" +MASK_BACK_LEFT = u"back left" +MASK_BACK_RIGHT = u"back right" +MASK_FRONT_RIGHT_OF_CENTER = u"front right of center" +MASK_FRONT_LEFT_OF_CENTER = u"front left of center" +MASK_BACK_CENTER = u"back center" +MASK_SIDE_LEFT = u"side left" +MASK_SIDE_RIGHT = u"side right" +MASK_TOP_CENTER = u"top center" +MASK_TOP_FRONT_LEFT = u"top front left" +MASK_TOP_FRONT_CENTER = u"top front center" +MASK_TOP_FRONT_RIGHT = u"top front right" +MASK_TOP_BACK_LEFT = u"top back left" +MASK_TOP_BACK_CENTER = u"top back center" +MASK_TOP_BACK_RIGHT = u"top back right"
View file
audiotools-2.18.tar.gz/audiotools/toc.py -> audiotools-2.19.tar.gz/audiotools/toc.py
Changed
@@ -19,11 +19,7 @@ """the TOC file handling module""" -import re -from audiotools import SheetException, parse_timestamp, build_timestamp -import gettext - -gettext.install("audiotools", unicode=True) +from . import SheetException ################### #TOC Parsing @@ -41,12 +37,16 @@ raises TOCException if some problem occurs parsing the file""" + import re + from . import parse_timestamp + TRACKLINE = re.compile(r'TRACK AUDIO') lines = list(lines) if ('CD_DA' not in [line.strip() for line in lines]): - raise TOCException(_(u"No CD_DA TOC header found")) + from .text import ERR_TOC_NO_HEADER + raise TOCException(ERR_TOC_NO_HEADER) lines = iter(lines) @@ -70,8 +70,8 @@ else: if (track is not None): track.lines.append(line) - if (line.startswith('FILE') or - line.startswith('AUDIOFILE')): + if ((line.startswith('FILE') or + line.startswith('AUDIOFILE'))): if ('"' in line): track.indexes = map( parse_timestamp, @@ -108,6 +108,8 @@ if present, this value is typically a CD's UPC code""" + import re + for line in self.lines: if (line.startswith('CATALOG')): result = re.search(r'"(.+)"', line) @@ -127,7 +129,7 @@ else: yield (track.indexes[0],) - def pcm_lengths(self, total_length): + def pcm_lengths(self, total_length, sample_rate): """yields a list of PCM lengths for all audio tracks within the file total_length is the length of the entire file in PCM frames""" @@ -138,7 +140,8 @@ if (previous is None): previous = current else: - track_length = (max(current) - max(previous)) * (44100 / 75) + track_length = ((max(current) - max(previous)) * + sample_rate / 75) total_length -= track_length yield track_length previous = current @@ -162,6 +165,7 @@ """ import cStringIO + from . import build_timestamp catalog = sheet.catalog() # a catalog string, or None indexes = list(sheet.indexes()) # a list of index tuples @@ -183,17 +187,17 @@ data.write("ISRC \"%s\"\n" % (ISRCs[tracknum])) if (next is not None): - data.write("AUDIOFILE \"%s\" %s %s\n" % \ - (filename, - build_timestamp(current[0]), - build_timestamp(next[0] - current[0]))) + data.write("AUDIOFILE \"%s\" %s %s\n" % + (filename, + build_timestamp(current[0]), + build_timestamp(next[0] - current[0]))) else: - data.write("AUDIOFILE \"%s\" %s\n" % \ - (filename, - build_timestamp(current[0]))) + data.write("AUDIOFILE \"%s\" %s\n" % + (filename, + build_timestamp(current[0]))) if (len(current) > 1): - data.write("START %s\n" % \ - (build_timestamp(current[-1] - current[0]))) + data.write("START %s\n" % + (build_timestamp(current[-1] - current[0]))) if (next is not None): data.write("\n") @@ -221,6 +225,8 @@ def ISRC(self): """returns the track's ISRC value, or None""" + import re + for line in self.lines: if (line.startswith('ISRC')): match = re.search(r'"(.+)"', line)
View file
audiotools-2.18.tar.gz/audiotools/ui.py -> audiotools-2.19.tar.gz/audiotools/ui.py
Changed
@@ -23,6 +23,10 @@ try: import urwid + + if (urwid.version.VERSION < (1, 0, 0)): + raise ImportError() + AVAILABLE = True class DownEdit(urwid.Edit): @@ -30,17 +34,21 @@ when the enter key is pressed, typically for moving to the next element in a form""" - def __init__(self, caption='', edit_text='', multiline=False, - align='left', wrap='space', allow_tab=False, - edit_pos=None, layout=None, key_map={}): - urwid.Edit.__init__(self, caption=caption, - edit_text=edit_text, - multiline=multiline, - align=align, - wrap=wrap, - allow_tab=allow_tab, - edit_pos=edit_pos, - layout=layout) + def __init__(self, *args, **kwargs): + urwid.Edit.__init__(self, *args, **kwargs) + self.__key_map__ = {"enter": "down"} + + def keypress(self, size, key): + return urwid.Edit.keypress(self, size, + self.__key_map__.get(key, key)) + + class DownIntEdit(urwid.IntEdit): + """a subclass of urwid.IntEdit which performs a down-arrow keypress + when the enter key is pressed, + typically for moving to the next element in a form""" + + def __init__(self, *args, **kwargs): + urwid.IntEdit.__init__(self, *args, **kwargs) self.__key_map__ = {"enter": "down"} def keypress(self, size, key): @@ -50,8 +58,8 @@ class FocusFrame(urwid.Frame): """a special Frame widget which performs callbacks on focus changes""" - def __init__(self, body, header=None, footer=None, focus_part='body'): - urwid.Frame.__init__(self, body, header, footer, focus_part) + def __init__(self, *args, **kwargs): + urwid.Frame.__init__(self, *args, **kwargs) self.focus_callback = None self.focus_callback_arg = None @@ -70,597 +78,2541 @@ else: self.focus_callback(self, part) - class Track(urwid.LineBox): - FIELDS = [(u"Track Name", "track_name"), - (u"Track Number", "track_number"), - (u"Album", "album_name"), - (u"Artist", "artist_name"), - (u"Performer", "performer_name"), - (u"Composer", "composer_name"), - (u"Conductor", "conductor_name"), - (u"ISRC", "ISRC"), - (u"Copyright", "copyright"), - (u"Recording Date", "date"), - (u"Comment", "comment")] - - def __init__(self, metadata): - """takes a MetaData object and constructs a Track widget - for modifying track-specific metadata""" + def get_focus(widget): + #something to smooth out the differences between Urwid versions - self.metadata = metadata + if (hasattr(widget, "get_focus") and callable(widget.get_focus)): + return widget.get_focus() + else: + return widget.focus_part - #setup contained editing widgets - self.track_name = DownEdit(edit_text=metadata.track_name) - if (metadata.track_total != 0): - self.track_number = urwid.Text("%d of %d" % - (metadata.track_number, - metadata.track_total)) - else: - self.track_number = urwid.Text("%d" % (metadata.track_number)) - for field in ["album_name", - "artist_name", - "performer_name", - "composer_name", - "conductor_name", - "ISRC", - "copyright", - "date", - "comment"]: - setattr(self, field, - DownEdit(edit_text=getattr(metadata, field))) + class OutputFiller(urwid.Frame): + """a class for selecting MetaData and populating output parameters + for multiple input tracks""" + + def __init__(self, + track_labels, + metadata_choices, + input_filenames, + output_directory, + format_string, + output_class, + quality, + completion_label=u"Apply"): + """track_labels is a list of unicode strings, one per track - field_width = max([len(f) for (f, a) in Track.FIELDS]) + 2 + metadata_choices[c][t] + is a MetaData object for choice number "c" and track number "t" + all choices must have the same number of tracks - #initialize ourself - urwid.LineBox.__init__( - self, - urwid.ListBox([urwid.Columns([ - ("fixed", field_width, - urwid.Text(field + u": ", - align="right")), - ("weight", 1, getattr(self, attr))]) - for (field, attr) in Track.FIELDS])) + input_filenames is a list of Filename objects for input files + the number of input files must equal the number of metadata objects + in each metadata choice - #setup widget title, if supported by Urwid - if (hasattr(self, "set_title") and callable(self.set_title)): - self.set_title(u"track fields") + output_directory is a string of the default output dir - def field_count(self): - return len(self.FIELDS) + format_string is a UTF-8 encoded format string - def get_metadata(self): - """returns a populated MetaData object""" + output_class is the default AudioFile-compatible class - #anything not present is populated by Album widget - return audiotools.MetaData( - track_name=self.track_name.get_edit_text(), - track_number=self.metadata.track_number, - track_total=self.metadata.track_total, - album_name=self.album_name.get_edit_text(), - artist_name=self.artist_name.get_edit_text(), - performer_name=self.performer_name.get_edit_text(), - composer_name=self.composer_name.get_edit_text(), - conductor_name=self.conductor_name.get_edit_text(), - ISRC=self.ISRC.get_edit_text(), - copyright=self.copyright.get_edit_text(), - date=self.date.get_edit_text(), - comment=self.comment.get_edit_text()) - - class Album(urwid.LineBox): - FIELDS = [(u"Album Name", "album_name"), - (u"Album Number", "album_number"), - (u"Artist", "artist_name"), - (u"Performer", "performer_name"), - (u"Composer", "composer_name"), - (u"Conductor", "conductor_name"), - (u"Media", "media"), - (u"Catalog #", "catalog"), - (u"Copyright", "copyright"), - (u"Publisher", "publisher"), - (u"Release Year", "year"), - (u"Recording Date", "date"), - (u"Comment", "comment")] - - LINKED_FIELDS = ["artist_name", - "performer_name", - "composer_name", - "conductor_name", - "copyright", - "date", - "comment"] - - def __init__(self, tracks): - """takes a list of Track objects and constructs an Album widget - for modifying album-specific metadata - (which may propagate to tracks)""" - - self.tracks = tracks - - def update_name(widget, new_value): - widget._attrib = [("albumname", len(new_value))] - - #setup album name, which should be consistent between tracks - album_name = set([t.metadata.album_name for t in tracks]).pop() - self.album_name = DownEdit(edit_text=album_name) - update_name(self.album_name, album_name) - urwid.connect_signal(self.album_name, - 'change', - update_name) - for t in tracks: - t.album_name = self.album_name - - #self.number is the album_number field, which should be consistent - self.number = set([t.metadata.album_number for t in tracks]).pop() - - #self.total is the album_total field, which should be consistent - self.total = set([t.metadata.album_total for t in tracks]).pop() - - #setup album number field, - if ((self.number != 0) or (self.total != 0)): - if (self.total != 0): - self.album_number = urwid.Text(u"%d of %d" % - (self.number, self.total)) - else: - self.album_number = urwid.Text(u"%d" % (self.number)) - else: - self.album_number = urwid.Text(u"") - - #build editable fields for album-specific metadata - for field in ["artist_name", - "performer_name", - "composer_name", - "conductor_name", - "media", - "catalog", - "copyright", - "publisher", - "year", - "date", - "comment"]: - setattr(self, field, - DownEdit(edit_text=audiotools.most_numerous( - [getattr(t.metadata, field) for t in tracks], - empty_list=u"", - all_differ=u"various"))) - - def field_changed(widget, new_value, attr): - for track in self.tracks: - if (getattr(track, attr).edit_text == widget.edit_text): - getattr(track, attr).set_edit_text(new_value) - - #link fields common to both albums and tracks - for attr in Album.LINKED_FIELDS: - urwid.connect_signal(getattr(self, attr), 'change', - field_changed, attr) - - field_width = max([len(f) for (f, a) in Album.FIELDS]) + 2 - - #initialize ourself - if ((self.number != 0) or (self.total != 0)): - urwid.LineBox.__init__( - self, - urwid.ListBox( - [urwid.Columns([ - ("fixed", field_width, - urwid.Text(field + u": ", - align="right")), - ("weight", 1, getattr(self, attr))]) - for (field, attr) in Album.FIELDS])) - else: - #omit "Album Number" row if number and total are both missing - #(a very common case) - urwid.LineBox.__init__( - self, - urwid.ListBox( - [urwid.Columns([ - ("fixed", field_width, - urwid.Text(field + u": ", - align="right")), - ("weight", 1, getattr(self, attr))]) - for (field, attr) in Album.FIELDS - if (attr != "album_number")])) - - #setup widget title, if supported by Urwid - if (hasattr(self, "set_title") and callable(self.set_title)): - self.set_title(u"album fields") - - def field_count(self): - if ((self.number != 0) or (self.total != 0)): - return len(self.FIELDS) - else: - return len(self.FIELDS) - 1 - - def get_metadata(self): - """yields a populated MetaData object per track""" - - for track in self.tracks: - metadata = track.get_metadata() - metadata.album_number = self.number - metadata.album_total = self.total - metadata.media = self.media.get_edit_text() - metadata.catalog = self.catalog.get_edit_text() - metadata.publisher = self.publisher.get_edit_text() - metadata.year = self.year.get_edit_text() + quality is a string of the default output quality to use + """ - yield metadata + self.__cancelled__ = True - class AlbumList(urwid.Pile): - def __init__(self, albums, select_item): - """takes a list of Album objects - and a select_item() callback for when an album or track is selected - and returns a tree-like widget for editing an album or tracks""" - - self.albums = albums - self.radios = [] # all our radio button-like checkboxes - rows = [] - - def unselect_others(checkbox, state_change): - for radio in self.radios: - if (radio is not checkbox): - radio.set_state(False, do_callback=False) - - for album in albums: - #the checkbox for selecting an album - checkbox = urwid.CheckBox(u"", - on_state_change=select_item, - user_data=album) - urwid.connect_signal(checkbox, "change", unselect_others) - self.radios.append(checkbox) - - #setup album row depending on if it has an album number or not - if (album.number != 0): - album_digits = len(str(album.number)) - rows.append( - urwid.Columns( - [("fixed", 4, checkbox), - ("fixed", album_digits + 1, - urwid.Text(u"%%%d.d " % (album_digits) % \ - (album.number))), - ("fixed", 2, urwid.Text(u": ")), - ("weight", 1, album.album_name)])) - else: - rows.append( - urwid.Columns([("fixed", 4, checkbox), - ("fixed", 2, urwid.Text(u": ")), - ("weight", 1, album.album_name)])) - - #the largest number of digits in a track_number field - track_digits = max([len(str(t.metadata.track_number)) - for t in album.tracks]) - - #setup track rows - for (last, track) in audiotools.iter_last(iter(album.tracks)): - #the checkbox for selecting a track - checkbox = urwid.CheckBox(u"", - on_state_change=select_item, - user_data=track) - urwid.connect_signal(checkbox, "change", unselect_others) - self.radios.append(checkbox) - - #prefixed differently depending on its position - if (last): - prefix = u" \u2514\u2500" - else: - prefix = u" \u251c\u2500" + #ensure label count equals path count + assert(len(track_labels) == len(input_filenames)) - #setup track row - rows.append( - urwid.Columns( - [("fixed", len(prefix), urwid.Text(prefix)), - ("fixed", 4, checkbox), - ("fixed", track_digits + 1, - urwid.Text(u"%%%d.d " % (track_digits) % \ - (track.metadata.track_number))), - ("fixed", 2, urwid.Text(u": ")), - ("weight", 1, track.track_name)])) - - urwid.Pile.__init__(self, rows) - - def get_metadata(self): - """for each album, yields a generator of MetaData objects, like: - - for album in albumlist: - for metadata in album: - <process MetaData object> - """ + #ensure there's at least one set of choices + assert(len(metadata_choices) > 0) - for album in self.albums: - yield album.get_metadata() + #ensure file path count is equal to metadata track count + assert(len(metadata_choices[0]) == len(input_filenames)) - def get_focus(widget): - #something to smooth out the differences between Urwid versions + for f in input_filenames: + assert(isinstance(f, audiotools.Filename)) - if (hasattr(widget, "get_focus") and callable(widget.get_focus)): - return widget.get_focus() - else: - return widget.focus_part + from audiotools.text import (LAB_CANCEL_BUTTON, + LAB_NEXT_BUTTON, + LAB_PREVIOUS_BUTTON) + + #setup status bars for output messages + self.metadata_status = urwid.Text(u"") + self.options_status = urwid.Text(u"") + + #setup a widget for populating metadata fields + metadata_buttons = urwid.Filler( + urwid.Columns( + widget_list=[('weight', 1, + urwid.Button(LAB_CANCEL_BUTTON, + on_press=self.exit)), + ('weight', 2, + urwid.Button(LAB_NEXT_BUTTON, + on_press=self.next))], + dividechars=3, + focus_column=1)) + + self.metadata = MetaDataFiller(track_labels, + metadata_choices, + self.metadata_status) + self.metadata_frame = urwid.Pile( + [("weight", 1, self.metadata), + ("fixed", 1, metadata_buttons)]) + + #setup a widget for populating output parameters + options_buttons = urwid.Filler( + urwid.Columns( + widget_list=[('weight', 1, + urwid.Button(LAB_PREVIOUS_BUTTON, + on_press=self.previous)), + ('weight', 2, + urwid.Button(completion_label, + on_press=self.complete))], + dividechars=3, + focus_column=1)) + + self.options = OutputOptions( + output_dir=output_directory, + format_string=format_string, + audio_class=output_class, + quality=quality, + input_filenames=input_filenames, + metadatas=[None for t in input_filenames], + extra_widgets=[("fixed", 1, options_buttons)]) + + self.options.set_focus(options_buttons) + + #finish initialization + urwid.Frame.__init__(self, + body=self.metadata_frame, + footer=self.metadata_status) - #the MetaDataFiller UI states are: - #| | selecting match | fields closed | fields open | - #|----------+-----------------+-------------------+-----------------------| - #| controls | N/A | APPLY_LIST | APPLY_LIST_W_FIELDS | - #| list | SELECTING_MATCH | EDITING_LIST_ONLY | EDITING_LIST_W_FIELDS | - #| fields | N/A | N/A | EDITING_FIELDS | - #|----------+-----------------+-------------------+-----------------------| + def exit(self, button): + self.__cancelled__ = True + raise urwid.ExitMainLoop() + + def previous(self, button): + self.set_body(self.metadata_frame) + self.set_footer(self.metadata_status) + + def next(self, button): + self.options.set_metadatas( + list(self.metadata.populated_metadata())) + self.set_body(self.options) + self.set_footer(self.options_status) + + def complete(self, button): + if (self.options.has_collisions): + from audiotools.text import ERR_OUTPUT_OUTPUTS_ARE_INPUT + self.options_status.set_text(ERR_OUTPUT_OUTPUTS_ARE_INPUT) + elif (self.options.has_duplicates): + from audiotools.text import ERR_OUTPUT_DUPLICATE_NAME + self.options_status.set_text(ERR_OUTPUT_DUPLICATE_NAME) + elif (self.options.has_errors): + from audiotools.text import ERR_OUTPUT_INVALID_FORMAT + self.options_status.set_text(ERR_OUTPUT_INVALID_FORMAT) + else: + self.__cancelled__ = False + raise urwid.ExitMainLoop() + + def cancelled(self): + """returns True if the widget was cancelled, + False if exited normally""" + + return self.__cancelled__ + + def handle_text(self, i): + if (self.get_body() is self.metadata_frame): + if (i == 'esc'): + self.exit(None) + elif (i == 'f1'): + self.metadata.select_previous_item() + elif (i == 'f2'): + self.metadata.select_next_item() + else: + if (i == 'esc'): + self.previous(None) + + def output_tracks(self): + """yields (output_class, + output_filename, + output_quality, + output_metadata) tuple for each input audio file + + output_metadata is a newly created MetaData object""" + + #Note that output_tracks() creates new MetaData objects + #while process_output_options() reuses inputted MetaData objects. + #This is because we don't want to modify MetaData objects + #in the event they're being used elsewhere. + + from itertools import izip + + (audiofile_class, + quality, + output_filenames) = self.options.selected_options() + for (metadata, + output_filename) in izip(self.metadata.populated_metadata(), + iter(output_filenames)): + yield (audiofile_class, + output_filename, + quality, + metadata) + + def output_directory(self): + """returns the currently selected output directory + as a plain string""" + + return self.options.output_directory.get_directory() + + def format_string(self): + """returns the current format string + as a plain, UTF-8 encoded string""" + + return self.options.output_format.get_edit_text().encode('utf-8') + + def output_class(self): + """returns the current AudioFile-compatible output class""" + + return self.options.selected_options()[0] + + def quality(self): + """returns the current quality string""" + + return self.options.selected_options()[1] + + class SingleOutputFiller(urwid.Frame): + """a class for selecting MetaData and populating output parameters + for a single input track""" + + def __init__(self, + track_label, + metadata_choices, + input_filenames, + output_file, + output_class, + quality, + completion_label=u"Apply"): + """track_label is a unicode string + + metadata_choices is a list of MetaData objects, + one per possible metadata choice to apply + + input_filenames is a list or set of Filename objects + + output_file is a string of the default output filename + + output_class is the default AudioFile-compatible class + + quality is a string of the default output quality to use""" + + self.input_filenames = input_filenames + self.__cancelled__ = True + + #ensure there's at least one choice + assert(len(metadata_choices) > 0) + + #ensure input file is a Filename object + for f in input_filenames: + assert(isinstance(f, audiotools.Filename)) + + from audiotools.text import (LAB_CANCEL_BUTTON, + LAB_OUTPUT_OPTIONS) + + #setup status bar for output messages + self.status = urwid.Text(u"") + + #setup a widget for cancel/finish buttons + output_buttons = urwid.Filler( + urwid.Columns( + widget_list=[ + ('weight', 1, urwid.Button(LAB_CANCEL_BUTTON, + on_press=self.exit)), + ('weight', 2, urwid.Button(completion_label, + on_press=self.complete))], + dividechars=3, + focus_column=1)) + + #setup a widget for populating output parameters + self.options = SingleOutputOptions( + output_filename=output_file, + audio_class=output_class, + quality=quality) + + #combine metadata and output options into single widget + self.metadata = MetaDataFiller( + track_labels=[track_label], + metadata_choices=[[m] for m in metadata_choices], + status=self.status) + + body = urwid.Pile( + [("weight", 1, self.metadata), + ("fixed", 5, urwid.LineBox(self.options, + title=LAB_OUTPUT_OPTIONS)), + ("fixed", 1, output_buttons)]) + + #finish initialization + urwid.Frame.__init__(self, + body=body, + footer=self.status) + + def exit(self, button): + self.__cancelled__ = True + raise urwid.ExitMainLoop() + + def complete(self, button): + output_filename = self.options.selected_options()[2] + + #ensure output filename isn't same as input filename + if (output_filename in self.input_filenames): + from audiotools.text import ERR_OUTPUT_IS_INPUT + self.status.set_text( + ERR_OUTPUT_IS_INPUT % (output_filename,)) + else: + self.__cancelled__ = False + raise urwid.ExitMainLoop() - class MetaDataFiller(urwid.Frame): + def cancelled(self): + return self.__cancelled__ + + def handle_text(self, i): + if (i == 'esc'): + self.exit(None) + elif (i == 'f1'): + self.metadata.select_previous_item() + elif (i == 'f2'): + self.metadata.select_next_item() + + def output_track(self): + """returns (output_class, + output_filename, + output_quality, + output_metadata) + + output_metadata is a newly created MetaData object""" + + (output_class, + output_quality, + output_filename) = self.options.selected_options() + + return (output_class, + output_filename, + output_quality, + list(self.metadata.populated_metadata())[0]) + + class MetaDataFiller(urwid.Pile): """a class for selecting the MetaData to apply to tracks""" - (SELECTING_MATCH, - APPLY_LIST_W_FIELDS, - APPLY_LIST, - EDITING_LIST_W_FIELDS, - EDITING_LIST_ONLY, - EDITING_FIELDS, - UNKNOWN_STATE) = range(7) + def __init__(self, track_labels, metadata_choices, status): + """track_labels is a list of unicode strings, one per track - def __init__(self, metadata_choices): - """metadata_choices[c][t] + metadata_choices[c][t] is a MetaData object for choice number "c" and track number "t" this widget allows the user to populate a set of MetaData objects which can be applied to tracks + + status is an urwid.Text object """ + #there must be at least one choice assert(len(metadata_choices) > 0) + #all choices must have at least 1 track + assert(min(map(len, metadata_choices)) > 0) + + #and all choices must have the same number of tracks + assert(len(set(map(len, metadata_choices))) == 1) + + from audiotools.text import (LAB_SELECT_BEST_MATCH, + LAB_TRACK_METADATA, + LAB_KEY_NEXT, + LAB_KEY_PREVIOUS) + self.metadata_choices = metadata_choices - #a list of AlbumList objects, one for each possible choice - self.album_lists = [audiotools.ui.AlbumList( - [audiotools.ui.Album(map(audiotools.ui.Track, - metadata_choice))], - self.select_album_or_track) - for metadata_choice in metadata_choices] - - self.selected_album_list = self.album_lists[0] - - self.select_header = urwid.LineBox( - urwid.Text(u"select best match", align='center')) - - widgets = [] - for (choice, album) in zip(metadata_choices, self.album_lists): - widgets.append(urwid.Button(choice[0].album_name, - on_press=self.select_best_match, - user_data=album)) - widgets.append(urwid.Divider()) - self.select_album = urwid.ListBox(widgets) - - #a simple widget for going back or applying selected album - if (len(metadata_choices) == 1): - #no back button for selecting choice if only 1 choice - self.back_apply = urwid.LineBox( - urwid.GridFlow([ - urwid.Button("Apply", - on_press=self.apply_selection)], - 10, 5, 1, 'center')) - else: - self.back_apply = urwid.LineBox( - urwid.GridFlow([ - urwid.Button("Back", - on_press=self.back_to_select), - urwid.Button("Apply", - on_press=self.apply_selection)], - 10, 5, 1, 'center')) - self.back_apply.base_widget.set_focus(1) - - self.collapsed = urwid.Divider(div_char=u'\u2500') - - #header will be either an album selection box - #or a set of controls - - #body will be either the album preview area - #or a place to edit an album's track list - - #footer will either be a collapsed line - #or a place to edit album/track field data - if (len(metadata_choices) == 1): - #automatically shift to selected choice - #if only one available - self.work_area = audiotools.ui.FocusFrame( - header=self.back_apply, - body=urwid.Filler(self.album_lists[0], valign='top'), - footer=self.collapsed, - focus_part="header") - else: - #otherwise, offer a choice of albums to select - self.work_area = audiotools.ui.FocusFrame( - header=self.select_header, - body=self.select_album, - footer=self.collapsed, - focus_part="body") - self.work_area.set_focus_callback(self.update_focus) + self.status = status + + #setup a MetaDataEditor for each possible match + self.edit_matches = [ + MetaDataEditor( + [(i, label, track) for (i, (track, label)) in + enumerate(zip(choice, track_labels))], + on_swivel_change=self.swiveled) + for choice in metadata_choices] + self.selected_match = self.edit_matches[0] + + #place selector at top only if there's more than one match + if (len(metadata_choices) > 1): + #setup radio button for each possible match + matches = [] + radios = [urwid.RadioButton(matches, + (choice[0].album_name + if (choice[0].album_name + is not None) + else u""), + on_state_change=self.select_match, + user_data=i) + for (i, choice) in enumerate(metadata_choices)] + for radio in radios: + radio._label.set_wrap_mode(urwid.CLIP) + + #put radio buttons in pretty container + select_match = urwid.LineBox(urwid.ListBox(radios)) + + if (hasattr(select_match, "set_title")): + select_match.set_title(LAB_SELECT_BEST_MATCH) + + widgets = [("fixed", + len(metadata_choices) + 2, + select_match)] + else: + widgets = [] + + self.track_metadata = urwid.Frame(body=self.edit_matches[0]) + + widgets.append(("weight", 1, + urwid.LineBox(self.track_metadata, + title=LAB_TRACK_METADATA))) + + urwid.Pile.__init__(self, widgets) + + def select_match(self, radio, selected, match): + if (selected): + self.selected_match = self.edit_matches[match] + self.track_metadata.set_body(self.selected_match) + + def swiveled(self, radio_button, selected, swivel): + if (selected): + from .text import (LAB_KEY_NEXT, + LAB_KEY_PREVIOUS) + + keys = [] + if (radio_button.previous_radio_button() is not None): + keys.extend([('key', u"F1"), + LAB_KEY_PREVIOUS % (swivel.swivel_type)]) + if (radio_button.next_radio_button() is not None): + if (len(keys) > 0): + keys.append(u" ") + keys.extend([('key', u"F2"), + LAB_KEY_NEXT % (swivel.swivel_type)]) + + if (len(keys) > 0): + self.status.set_text(keys) + else: + self.status.set_text(u"") + + def select_previous_item(self): + """selects the previous item (track or field) + if possible""" + + self.selected_match.select_previous_item() + + def select_next_item(self): + """selects the next item (track or field) + if possible""" + + self.selected_match.select_next_item() + + def populated_metadata(self): + """yields a new, populated MetaData object per track, + depending on the current selection and its values.""" + + for (track_id, metadata) in self.selected_match.metadata(): + yield metadata + + class MetaDataEditor(urwid.Frame): + """a class for editing MetaData values for a set of tracks""" + + def __init__(self, tracks, + on_text_change=None, + on_swivel_change=None): + """tracks is a list of (id, label, MetaData) tuples + in the order they are to be displayed + where id is some unique hashable ID value + label is a unicode string + and MetaData is an audiotools.MetaData-compatible object or None + + on_text_change is a callback for when any text field is modified + + on_swivel_change is a callback for when + tracks and fields are swapped + """ + + #a list of track IDs in the order they appear + self.track_ids = [] - self.status = urwid.Text(u"", align='left') + #a list of (track_id, label) tuples in the order they should appear + track_labels = [] + + #the order metadata fields should appear + field_labels = [(attr, audiotools.MetaData.FIELD_NAMES[attr]) + for attr in audiotools.MetaData.FIELD_ORDER] + + #a dict of track_id->TrackMetaData values + self.metadata_edits = {} + + #determine the base metadata all others should be linked against + base_metadata = {} + for (track_id, track_label, metadata) in tracks: + self.track_ids.append(track_id) + for (attr, value) in (metadata if metadata is not None + else audiotools.MetaData()).fields(): + base_metadata.setdefault(attr, set([])).add(value) + + base_metadata = BaseMetaData( + metadata=audiotools.MetaData( + **dict([(field, list(values)[0]) + for (field, values) in base_metadata.items() + if (len(values) == 1)])), + on_change=on_text_change) + + #populate the track_labels and metadata_edits lookup tables + for (track_id, track_label, metadata) in tracks: + if (track_id not in self.metadata_edits): + track_labels.append((track_id, track_label)) + self.metadata_edits[track_id] = TrackMetaData( + metadata=(metadata if metadata is not None + else audiotools.MetaData()), + base_metadata=base_metadata, + on_change=on_text_change) + else: + #no_duplicates via open_files should filter this case + raise ValueError("same track ID cannot appear twice") + + swivel_radios = [] + + track_radios_order = [] + track_radios = {} + + field_radios_order = [] + field_radios = {} + + #generate radio buttons for track labels + for (track_id, track_label) in track_labels: + radio = OrderedRadioButton(ordered_group=track_radios_order, + group=swivel_radios, + label=('label', track_label), + state=False) + + swivel = Swivel( + swivel_type=u"track", + left_top_widget=urwid.Text(('label', 'fields')), + left_alignment='fixed', + left_width=4 + 14, + left_radios=field_radios, + left_ids=[field_id for (field_id, label) in field_labels], + right_top_widget=urwid.Text(('label', track_label), + wrap=urwid.CLIP), + right_alignment='weight', + right_width=1, + right_widgets=[getattr(self.metadata_edits[track_id], + field_id) + for (field_id, label) in field_labels]) + + radio._label.set_wrap_mode(urwid.CLIP) + + urwid.connect_signal(radio, + 'change', + self.activate_swivel, + swivel) + + if (on_swivel_change is not None): + urwid.connect_signal(radio, + 'change', + on_swivel_change, + swivel) + + track_radios[track_id] = radio + + #generate radio buttons for metadata labels + for (field_id, field_label) in field_labels: + radio = OrderedRadioButton(ordered_group=field_radios_order, + group=swivel_radios, + label=('label', field_label), + state=False) + + swivel = Swivel( + swivel_type=u"field", + left_top_widget=urwid.Text(('label', u'files')), + left_alignment='weight', + left_width=1, + left_radios=track_radios, + left_ids=[track_id for (track_id, track) + in track_labels], + right_top_widget=urwid.Text(('label', field_label)), + right_alignment='weight', + right_width=2, + right_widgets=[getattr(self.metadata_edits[track_id], + field_id) + for (track_id, track) in track_labels]) + + radio._label.set_align_mode('right') + + urwid.connect_signal(radio, + 'change', + self.activate_swivel, + swivel) + + if (on_swivel_change is not None): + urwid.connect_signal(radio, + 'change', + on_swivel_change, + swivel) + + field_radios[field_id] = radio urwid.Frame.__init__( self, - body=self.work_area, - footer=self.status, - focus_part="body") - - if (len(metadata_choices) == 1): - self.set_state_message(self.APPLY_LIST) - else: - self.set_state_message(self.SELECTING_MATCH) - - def preview_album(self, radio_button, new_state, user_data): - if (new_state): - self.work_area.set_body(user_data) - - def select_best_match(self, button, album_list): - self.selected_album_list = album_list - - #update control box with <back>, <select> buttons - self.work_area.set_header(self.back_apply) - - #update preview area with editable area - self.work_area.set_body( - urwid.Filler(album_list, valign='top')) - - self.work_area.set_footer(self.collapsed) - self.work_area.set_focus('header') - self.set_state_message(self.get_state()) - - def select_album_or_track(self, checkbox, state_change, - user_data=None): - if ((state_change == True) and (user_data is not None)): - #select item - self.work_area.set_footer( - urwid.BoxAdapter(user_data, user_data.field_count())) - self.work_area.set_focus('footer') - elif (state_change == False): - #unselect item - self.work_area.set_footer(self.collapsed) - self.work_area.set_focus('body') - - def back_to_select(self, button): - for album in self.album_lists: - for radio in album.radios: - radio.set_state(False, do_callback=False) - - self.work_area.set_header(self.select_header) - self.work_area.set_body(self.select_album) - self.work_area.set_footer(self.collapsed) - self.work_area.set_focus('body') - self.set_state_message(self.get_state()) + header=urwid.Columns( + [("fixed", 1, urwid.Text(u"")), + ("weight", 1, urwid.Text(u""))]), + body=urwid.ListBox([])) + + if (len(self.metadata_edits) != 1): + #if more than one track, select track_name radio button + field_radios["track_name"].set_state(True) + else: + #if only one track, select that track's radio button + track_radios[track_labels[0][0]].set_state(True) - def apply_selection(self, button): - raise urwid.ExitMainLoop() + def activate_swivel(self, radio_button, selected, swivel): + if (selected): + self.selected_radio = radio_button + + #add new entries according to swivel's values + self.set_body( + urwid.ListBox( + [urwid.Columns([(swivel.left_alignment, + swivel.left_width, + left_widget), + (swivel.right_alignment, + swivel.right_width, + right_widget)]) + for (left_widget, + right_widget) in swivel.rows()])) + + #update header with swivel's values + self.set_header( + urwid.Columns( + [(swivel.left_alignment, + swivel.left_width, + urwid.Text(u"")), + (swivel.right_alignment, + swivel.right_width, + LinkedWidgetHeader(swivel.right_top_widget))])) + else: + pass + + def select_previous_item(self): + previous_radio = self.selected_radio.previous_radio_button() + if (previous_radio is not None): + previous_radio.set_state(True) + + def select_next_item(self): + next_radio = self.selected_radio.next_radio_button() + if (next_radio is not None): + next_radio.set_state(True) + + def metadata(self): + """yields a (track_id, MetaData) tuple + per edited metadata track + + MetaData objects are newly created""" + + for track_id in self.track_ids: + yield (track_id, + self.metadata_edits[track_id].edited_metadata()) + + class OrderedRadioButton(urwid.RadioButton): + def __init__(self, ordered_group, group, label, + state='first True', on_state_change=None, user_data=None): + urwid.RadioButton.__init__(self, + group, + label, + state, + on_state_change, + user_data) + ordered_group.append(self) + self.ordered_group = ordered_group + + def previous_radio_button(self): + for (current_radio, + previous_radio) in zip(self.ordered_group, + [None] + self.ordered_group): + if (current_radio is self): + return previous_radio + else: + return None + + def next_radio_button(self): + for (current_radio, + next_radio) in zip(self.ordered_group, + self.ordered_group[1:] + [None]): + if (current_radio is self): + return next_radio + else: + return None + + class LinkedWidgetHeader(urwid.Columns): + def __init__(self, widget): + urwid.Columns.__init__(self, + [("fixed", 3, urwid.Text(u" ")), + ("weight", 1, widget), + ("fixed", 4, urwid.Text(u""))]) + + class LinkedWidgetDivider(urwid.Columns): + def __init__(self): + urwid.Columns.__init__( + self, + [("fixed", 3, urwid.Text(u"\u2500\u2534\u2500")), + ("weight", 1, urwid.Divider(u"\u2500")), + ("fixed", 4, urwid.Text(u"\u2500" * 4))]) + + class LinkedWidgets(urwid.Columns): + def __init__(self, checkbox_group, linked_widget, unlinked_widget, + initially_linked): + """linked_widget is shown when the linking checkbox is checked + otherwise unlinked_widget is shown""" + + self.linked_widget = linked_widget + self.unlinked_widget = unlinked_widget + self.checkbox_group = checkbox_group + + self.checkbox = urwid.CheckBox(u"", + state=initially_linked, + on_state_change=self.swap_link) + self.checkbox_group.append(self.checkbox) + + urwid.Columns.__init__( + self, + [("fixed", 3, urwid.Text(u" : ")), + ("weight", 1, + linked_widget if initially_linked else unlinked_widget), + ("fixed", 4, self.checkbox)]) + + def swap_link(self, checkbox, linked): + if (linked): + #if nothing else linked in this checkbox group, + #set linked text to whatever the last unlinked text as + if (set([cb.get_state() for cb in self.checkbox_group + if (cb is not checkbox)]) == set([False])): + self.linked_widget.set_edit_text( + self.unlinked_widget.get_edit_text()) + self.widget_list[1] = self.linked_widget + self.set_focus(2) + else: + #set unlinked text to whatever the last linked text was + self.unlinked_widget.set_edit_text( + self.linked_widget.get_edit_text()) + self.widget_list[1] = self.unlinked_widget + self.set_focus(2) + + def value(self): + if (self.checkbox.get_state()): + widget = self.linked_widget + else: + widget = self.unlinked_widget + + if (hasattr(widget, "value") and callable(widget.value)): + return widget.value() + elif (hasattr(widget, "get_edit_text") and + callable(widget.get_edit_text)): + return widget.get_edit_text() + else: + return None + + class BaseMetaData: + def __init__(self, metadata, on_change=None): + """metadata is a MetaData object + on_change is a callback for when the text field is modified""" + + self.metadata = metadata + self.checkbox_groups = {} + for field in metadata.FIELDS: + if (field not in metadata.INTEGER_FIELDS): + value = getattr(metadata, field) + widget = DownEdit(edit_text=value if value is not None + else u"") + else: + value = getattr(metadata, field) + widget = DownIntEdit(default=value if value is not None + else 0) + + if (on_change is not None): + urwid.connect_signal(widget, 'change', on_change) + setattr(self, field, widget) + self.checkbox_groups[field] = [] + + class TrackMetaData: + NEVER_LINK = frozenset(["track_name", "track_number", "ISRC"]) + + def __init__(self, metadata, base_metadata, on_change=None): + """metadata is a MetaData object + base_metadata is a BaseMetaData object to link against + on_change is a callback for when the text field is modified""" + + for field in metadata.FIELDS: + if (field not in metadata.INTEGER_FIELDS): + value = getattr(metadata, field) + widget = DownEdit(edit_text=value if value is not None + else u"") + else: + value = getattr(metadata, field) + widget = DownIntEdit(default=value if value is not None + else 0) + + if (on_change is not None): + urwid.connect_signal(widget, 'change', on_change) + + linked_widget = LinkedWidgets( + checkbox_group=base_metadata.checkbox_groups[field], + linked_widget=getattr(base_metadata, field), + unlinked_widget=widget, + initially_linked=((field not in self.NEVER_LINK) and + (getattr(metadata, + field) == + getattr(base_metadata.metadata, + field)))) + + setattr(self, field, linked_widget) + + def edited_metadata(self): + """returns a new MetaData object of the track's + current value based on its widgets' values""" + + return audiotools.MetaData( + **dict([(attr, value) for (attr, value) in + [(attr, getattr(self, attr).value()) + for attr in audiotools.MetaData.FIELDS] + if ((len(value) > 0) if + (attr not in + audiotools.MetaData.INTEGER_FIELDS) else + (value > 0))])) + + class Swivel: + """this is a container for the objects of a swiveling operation""" + + def __init__(self, swivel_type, + left_top_widget, + left_alignment, + left_width, + left_radios, + left_ids, + right_top_widget, + right_alignment, + right_width, + right_widgets): + assert(len(left_ids) == len(right_widgets)) + + self.swivel_type = swivel_type + self.left_top_widget = left_top_widget + self.left_alignment = left_alignment + self.left_width = left_width + self.left_radios = left_radios + self.left_ids = left_ids + self.right_top_widget = right_top_widget + self.right_alignment = right_alignment + self.right_width = right_width + self.right_widgets = right_widgets + + def rows(self): + for (left_id, right_widget) in zip(self.left_ids, + self.right_widgets): + yield (self.left_radios[left_id], right_widget) + + def tab_complete(path): + """given a partially-completed directory path string + returns a path string completed as far as possible + """ + + import os.path + + (base, remainder) = os.path.split(path) + if (os.path.isdir(base)): + try: + candidate_dirs = [d for d in os.listdir(base) + if (d.startswith(remainder) and + os.path.isdir(os.path.join(base, d)))] + if (len(candidate_dirs) == 0): + #no possible matches to tab complete + return path + elif (len(candidate_dirs) == 1): + #one possible match to tab complete + return os.path.join(base, candidate_dirs[0]) + os.sep + else: + #multiple possible matches to tab complete + #so complete as much as possible + return os.path.join(base, + os.path.commonprefix(candidate_dirs)) + except OSError: + #unable to read base dir to complete the rest + return path + else: + #base doesn't exist, + #so we don't know how to complete the rest + return path + + def tab_complete_file(path): + """given a partially-completed file path string + returns a path string completed as far as possible""" + + import os.path + + (base, remainder) = os.path.split(path) + if (os.path.isdir(base)): + try: + candidates = [f for f in os.listdir(base) + if f.startswith(remainder)] + if (len(candidates) == 0): + #no possible matches to tab complete + return path + elif (len(candidates) == 1): + #one possible match to tab complete + path = os.path.join(base, candidates[0]) + if (os.path.isdir(path)): + return path + os.sep + else: + return path + else: + #multiple possible matches to tab complete + #so complete as much as possible + return os.path.join(base, + os.path.commonprefix(candidates)) + except OSError: + #unable to read base dir to complete the rest + return path + else: + #base doesn't exist, + #so we don't know how to complete the rest + return path + + def pop_directory(path): + """given a path string, + returns a new path string with one directory removed if possible""" + + import os.path + + base = os.path.split(path.rstrip(os.sep))[0] + if (base == ''): + return base + elif (not base.endswith(os.sep)): + return base + os.sep + else: + return base + + def split_at_cursor(edit): + """returns a (prefix, suffix) unicode pair + of text before and after the urwid.Edit widget's cursor""" + + return (edit.get_edit_text()[0:edit.edit_pos], + edit.get_edit_text()[edit.edit_pos:]) + + class SelectButtons(urwid.Pile): + def __init__(self, widget_list, focus_item=None, cancelled=None): + """cancelled is a callback which is called + when the esc key is pressed + it takes no arguments""" + + urwid.Pile.__init__(self, widget_list, focus_item) + self.cancelled = cancelled - def get_state(self): - if (self.work_area.get_body() is self.select_album): - #selecting match - return self.SELECTING_MATCH - elif (self.work_area.get_header() is self.back_apply): - if (self.work_area.get_footer() is self.collapsed): - #match selected, fields closed - if (get_focus(self.work_area) == 'header'): - return self.APPLY_LIST - elif (get_focus(self.work_area) == 'body'): - return self.EDITING_LIST_ONLY + def keypress(self, size, key): + key = urwid.Pile.keypress(self, size, key) + if ((key == "esc") and (self.cancelled is not None)): + self.cancelled() + return + else: + return key + + class BottomLineBox(urwid.LineBox): + """a LineBox that places its title at the bottom instead of the top""" + + def __init__(self, original_widget, title="", + tlcorner=u"\u250c", tline=u"\u2500", lline=u"\u2502", + trcorner=u"\u2510", blcorner=u"\u2514", rline=u"\u2502", + bline=u"\u2500", brcorner=u"\u2518"): + tline, bline = urwid.Divider(tline), urwid.Divider(bline) + lline, rline = urwid.SolidFill(lline), urwid.SolidFill(rline) + tlcorner, trcorner = urwid.Text(tlcorner), urwid.Text(trcorner) + blcorner, brcorner = urwid.Text(blcorner), urwid.Text(brcorner) + + self.title_widget = urwid.Text(self.format_title(title)) + self.tline_widget = urwid.Columns( + [tline, ('flow', self.title_widget), tline]) + + top = urwid.Columns( + [('fixed', 1, tlcorner), bline, ('fixed', 1, trcorner)]) + + middle = urwid.Columns( + [('fixed', 1, lline), original_widget, ('fixed', 1, rline)], + box_columns=[0, 2], + focus_column=1) + + bottom = urwid.Columns( + [('fixed', 1, blcorner), + self.tline_widget, + ('fixed', 1, brcorner)]) + + pile = urwid.Pile( + [('flow', top), middle, ('flow', bottom)], focus_item=1) + + urwid.WidgetDecoration.__init__(self, original_widget) + urwid.WidgetWrap.__init__(self, pile) + + class SelectOneDialog(urwid.WidgetWrap): + signals = ['close'] + + def __init__(self, select_one, items, selected_value, + label=None): + self.select_one = select_one + self.items = items + + selected_button = 0 + buttons = [] + for (i, (l, value)) in enumerate(items): + buttons.append(urwid.Button(label=l, + on_press=self.select_button, + user_data=(l, value))) + if (value == selected_value): + selected_button = i + pile = SelectButtons(buttons, + selected_button, + lambda: self._emit("close")) + fill = urwid.Filler(pile) + if (label is not None): + linebox = urwid.LineBox(fill, title=label) + else: + linebox = urwid.LineBox(fill) + self.__super.__init__(linebox) + + def select_button(self, button, label_value): + (label, value) = label_value + self.select_one.make_selection(label, value) + self._emit("close") + + class SelectOne(urwid.PopUpLauncher): + def __init__(self, items, selected_value=None, on_change=None, + user_data=None, label=None): + """items is a list of (unicode, value) tuples + where value can be any sort of object + + selected_value is a selected object + + on_change is a callback which takes a new selected object + which is called as on_change(new_value, [user_data]) + + label is a unicode label string for the selection box""" + + self.__select_button__ = urwid.Button(u"") + self.__super.__init__(self.__select_button__) + urwid.connect_signal( + self.original_widget, + 'click', + lambda button: self.open_pop_up()) + + assert(len(items) > 0) + + self.__items__ = items + self.__selected_value__ = None # set by make_selection, below + self.__on_change__ = None + self.__user_data__ = None + self.__label__ = label + + if (selected_value is not None): + try: + (label, value) = [pair for pair in items + if pair[1] == selected_value][0] + except IndexError: + (label, value) = items[0] + else: + (label, value) = items[0] + + self.make_selection(label, value) + self.__on_change__ = on_change + self.__user_data__ = user_data + + def create_pop_up(self): + pop_up = SelectOneDialog(self, + self.__items__, + self.__selected_value__, + self.__label__) + urwid.connect_signal( + pop_up, + 'close', + lambda button: self.close_pop_up()) + return pop_up + + def get_pop_up_parameters(self): + return {'left': 0, + 'top': 1, + 'overlay_width': max([4 + len(i[0]) for i in + self.__items__]) + 2, + 'overlay_height': len(self.__items__) + 2} + + def make_selection(self, label, value): + self.__select_button__.set_label(label) + self.__selected_value__ = value + if (self.__on_change__ is not None): + if (self.__user_data__ is not None): + self.__on_change__(value, self.__user_data__) + else: + self.__on_change__(value) + + def selection(self): + return self.__selected_value__ + + def set_items(self, items, selected_value): + self.__items__ = items + self.make_selection([label for (label, value) in items if + value is selected_value][0], + selected_value) + + class SelectDirectory(urwid.Columns): + def __init__(self, initial_directory, on_change=None, user_data=None): + self.edit = EditDirectory(initial_directory) + urwid.Columns.__init__(self, + [('weight', 1, self.edit), + ('fixed', 10, BrowseDirectory(self.edit))]) + if (on_change is not None): + urwid.connect_signal(self.edit, + 'change', + on_change, + user_data) + + def set_directory(self, directory): + #FIXME - allow this to be assigned externally + raise NotImplementedError() + + def get_directory(self): + return self.edit.get_directory() + + class EditDirectory(urwid.Edit): + def __init__(self, initial_directory): + """initial_directory is a plain string + in the default filesystem encoding + + this directory has username expanded + and is converted to the absolute path""" + + import os.path + FS_ENCODING = audiotools.FS_ENCODING + + urwid.Edit.__init__( + self, + edit_text=os.path.abspath( + os.path.expanduser(initial_directory)).decode(FS_ENCODING), + wrap='clip', + allow_tab=False) + + def keypress(self, size, key): + key = urwid.Edit.keypress(self, size, key) + FS_ENCODING = audiotools.FS_ENCODING + import os.path + + if (key == 'tab'): + #only tab complete stuff before cursor + (prefix, suffix) = split_at_cursor(self) + new_prefix = tab_complete( + os.path.abspath( + os.path.expanduser( + prefix.encode(FS_ENCODING)))).decode(FS_ENCODING) + + self.set_edit_text(new_prefix + suffix) + self.set_edit_pos(len(new_prefix)) + elif (key == 'ctrl w'): + #only delete stuff before cursor + (prefix, suffix) = split_at_cursor(self) + new_prefix = pop_directory( + os.path.abspath( + os.path.expanduser( + prefix.encode(FS_ENCODING)))).decode(FS_ENCODING) + + self.set_edit_text(new_prefix + suffix) + self.set_edit_pos(len(new_prefix)) + else: + return key + + def set_directory(self, directory): + """directory is a plain directory string to set""" + + FS_ENCODING = audiotools.FS_ENCODING + + new_text = directory.decode(FS_ENCODING) + self.set_edit_text(new_text) + self.set_edit_pos(len(new_text)) + + def get_directory(self): + """returns selected directory as a plain string""" + + FS_ENCODING = audiotools.FS_ENCODING + return self.get_edit_text().encode(FS_ENCODING) + + class BrowseDirectory(urwid.PopUpLauncher): + def __init__(self, edit_directory): + """edit_directory is an EditDirectory object""" + + from audiotools.text import LAB_BROWSE_BUTTON + + self.__super.__init__( + urwid.Button(LAB_BROWSE_BUTTON, + on_press=lambda button: self.open_pop_up())) + self.edit_directory = edit_directory + + def create_pop_up(self): + pop_up = BrowseDirectoryDialog(self.edit_directory) + urwid.connect_signal(pop_up, "close", + lambda button: self.close_pop_up()) + return pop_up + + def get_pop_up_parameters(self): + #FIXME - make these values dynamic + #based on edit_directory's location + return {'left': 0, + 'top': 1, + 'overlay_width': 70, + 'overlay_height': 20} + + class BrowseDirectoryDialog(urwid.WidgetWrap): + signals = ['close'] + + def __init__(self, edit_directory): + """edit_directory is an EditDirectory object""" + + from audiotools.text import (LAB_KEY_SELECT, + LAB_KEY_TOGGLE_OPEN, + LAB_KEY_CANCEL, + LAB_CHOOSE_DIRECTORY) + + browser = DirectoryBrowser( + edit_directory.get_directory(), + directory_selected=self.select_directory, + cancelled=lambda: self._emit("close")) + + frame = urwid.LineBox(urwid.Frame( + body=browser, + footer=urwid.Text([('key', 'enter'), + LAB_KEY_SELECT, + u" ", + ('key', 'space'), + LAB_KEY_TOGGLE_OPEN, + u" ", + ('key', 'esc'), + LAB_KEY_CANCEL])), + title=LAB_CHOOSE_DIRECTORY) + + self.__super.__init__(frame) + self.edit_directory = edit_directory + + def select_directory(self, selected_directory): + self.edit_directory.set_directory(selected_directory) + self._emit("close") + + class DirectoryBrowser(urwid.TreeListBox): + def __init__(self, initial_directory, + directory_selected=None, + cancelled=None): + import os + import os.path + + def path_iter(path): + if (path == os.sep): + yield path + else: + path = path.rstrip(os.sep) + if (len(path) > 0): + (head, tail) = os.path.split(path) + for part in path_iter(head): + yield part + yield tail else: - return self.UNKNOWN_STATE + return + + topnode = DirectoryNode(os.sep) + + for path_part in path_iter( + os.path.abspath( + os.path.expanduser(initial_directory))): + try: + if (path_part == "/"): + node = topnode + else: + node = node.get_child_node(path_part) + widget = node.get_widget() + widget.expanded = True + widget.update_expanded_icon() + except urwid.treetools.TreeWidgetError: + break + + urwid.TreeListBox.__init__(self, urwid.TreeWalker(topnode)) + self.set_focus(node) + self.directory_selected = directory_selected + self.cancelled = cancelled + + def selected_directory(self): + import os + import os.path + + def focused_nodes(): + (widget, node) = self.get_focus() + while (not node.is_root()): + yield node.get_key() + node = node.get_parent() + else: + yield os.sep + + return os.path.join(*reversed(list(focused_nodes()))) + os.sep + + def unhandled_input(self, size, input): + input = urwid.TreeListBox.unhandled_input(self, size, input) + if (input == 'enter'): + if (self.directory_selected is not None): + self.directory_selected(self.selected_directory()) + else: + return input + elif (input == 'esc'): + if (self.cancelled is not None): + self.cancelled() else: - #match selected, fields open - if (get_focus(self.work_area) == 'header'): - return self.APPLY_LIST_W_FIELDS - elif (get_focus(self.work_area) == 'body'): - return self.EDITING_LIST_W_FIELDS + return input + else: + return input + + class DirectoryWidget(urwid.TreeWidget): + indent_cols = 1 + + def __init__(self, node): + self.__super.__init__(node) + self.expanded = False + self.update_expanded_icon() + + def keypress(self, size, key): + key = urwid.TreeWidget.keypress(self, size, key) + if (key == " "): + self.expanded = not self.expanded + self.update_expanded_icon() + else: + return key + + def get_display_text(self): + node = self.get_node() + if node.get_depth() == 0: + return "/" + else: + return node.get_key() + + class ErrorWidget(urwid.TreeWidget): + indent_cols = 1 + + def get_display_text(self): + return ('error', u"(error/permission denied)") + + class ErrorNode(urwid.TreeNode): + def load_widget(self): + return ErrorWidget(self) + + class DirectoryNode(urwid.ParentNode): + def __init__(self, path, parent=None): + import os + import os.path + + if (path == os.sep): + urwid.ParentNode.__init__(self, + value=path, + key=None, + parent=parent, + depth=0) + else: + urwid.ParentNode.__init__(self, + value=path, + key=os.path.basename(path), + parent=parent, + depth=path.count(os.sep)) + + def load_parent(self): + import os.path + + (parentname, myname) = os.path.split(self.get_value()) + parent = DirectoryNode(parentname) + parent.set_child_node(self.get_key(), self) + return parent + + def load_child_keys(self): + import os.path + + dirs = [] + try: + path = self.get_value() + for d in sorted(os.listdir(path)): + if ((not d.startswith(".")) and + os.path.isdir( + os.path.join(path, d))): + dirs.append(d) + except OSError, e: + depth = self.get_depth() + 1 + self._children[None] = ErrorNode(self, parent=self, key=None, + depth=depth) + return [None] + + return dirs + + def load_child_node(self, key): + """Return a DirectoryNode""" + + import os.path + + index = self.get_child_index(key) + path = os.path.join(self.get_value(), key) + return DirectoryNode(path, parent=self) + + def load_widget(self): + return DirectoryWidget(self) + + class EditFilename(urwid.Edit): + def __init__(self, initial_filename): + """initial_filename is a plain string + in the default filesystem encoding + + this filename has username expanded + and is converted to the absolute path""" + + import os.path + FS_ENCODING = audiotools.FS_ENCODING + + urwid.Edit.__init__( + self, + edit_text=os.path.abspath( + os.path.expanduser(initial_filename)).decode(FS_ENCODING), + wrap="clip", + allow_tab=False) + + def keypress(self, size, key): + key = urwid.Edit.keypress(self, size, key) + FS_ENCODING = audiotools.FS_ENCODING + import os.path + + if (key == 'tab'): + #only tab complete stuff before cursor + (prefix, suffix) = split_at_cursor(self) + new_prefix = tab_complete_file( + os.path.abspath( + os.path.expanduser( + prefix.encode(FS_ENCODING)))).decode(FS_ENCODING) + + self.set_edit_text(new_prefix + suffix) + self.set_edit_pos(len(new_prefix)) + elif (key == 'ctrl w'): + #only delete stuff before cursor + (prefix, suffix) = split_at_cursor(self) + new_prefix = pop_directory( + os.path.abspath( + os.path.expanduser( + prefix.encode(FS_ENCODING)))).decode(FS_ENCODING) + + self.set_edit_text(new_prefix + suffix) + self.set_edit_pos(len(new_prefix)) + else: + return key + + def set_filename(self, filename): + """filename is a plain filename string to set""" + + self.set_edit_text(filename.decode(audiotools.FS_ENCODING)) + + def get_filename(self): + """returns selected filename as a plain string""" + + return self.get_edit_text().encode(audiotools.FS_ENCODING) + + class BrowseFields(urwid.PopUpLauncher): + def __init__(self, output_format): + """output_format is an Edit object""" + + from audiotools.text import LAB_FIELDS_BUTTON + self.__super.__init__( + urwid.Button(LAB_FIELDS_BUTTON, + on_press=lambda button: self.open_pop_up())) + self.output_format = output_format + + def create_pop_up(self): + pop_up = BrowseFieldsDialog(self.output_format) + urwid.connect_signal(pop_up, "close", + lambda button: self.close_pop_up()) + return pop_up + + def get_pop_up_parameters(self): + return { + 'left': 0, + 'top': 1, + 'overlay_width': (max([len(label) + 4 + for (string, label) in + audiotools.FORMAT_FIELDS.values()]) + + 2), + 'overlay_height': len(audiotools.FORMAT_FIELDS.values()) + 2} + + class BrowseFieldsDialog(urwid.WidgetWrap): + signals = ['close'] + + def __init__(self, output_format): + from audiotools.text import (LAB_KEY_CANCEL, + LAB_KEY_CLEAR_FORMAT, + LAB_ADD_FIELD) + + self.__super.__init__( + urwid.LineBox( + urwid.Frame(body=FieldsList(output_format, self.close), + footer=urwid.Text([('key', 'del'), + LAB_KEY_CLEAR_FORMAT, + u" ", + ('key', 'esc'), + LAB_KEY_CANCEL])), + title=LAB_ADD_FIELD)) + + def close(self): + self._emit("close") + + class FieldsList(urwid.ListBox): + def __init__(self, output_format, close): + urwid.ListBox.__init__( + self, + [urwid.Button(label, + on_press=self.select_field, + user_data=(output_format, string)) + for (string, label) in + [audiotools.FORMAT_FIELDS[field] for field in + audiotools.FORMAT_FIELD_ORDER]]) + self.output_format = output_format + self.close = close + + def select_field(self, button, field_value): + (field, value) = field_value + field.insert_text(value) + self.close() + + def cancel(self): + self.close() + + def keypress(self, size, input): + input = urwid.ListBox.keypress(self, size, input) + if (input == 'esc'): + self.cancel() + elif (input == 'delete'): + self.output_format.set_edit_text(u"") + else: + return input + + class OutputOptions(urwid.Pile): + """a widget for selecting the typical set of output options + for multiple input files: --dir, --format, --type, --quality""" + + def __init__(self, + output_dir, + format_string, + audio_class, + quality, + input_filenames, + metadatas, + extra_widgets=None): + """ + | field | value | meaning | + |-----------------+------------+----------------------------------| + | output_dir | string | default output directory | + | format_string | string | format string to use for files | + | audio_class | AudioFile | audio class of output files | + | quality | string | quality level of output | + | input_filenames | [Filename] | Filename objects for input files | + | metadatas | [MetaData] | MetaData objects for input files | + + note that the length of input_filenames + must equal length of metadatas + """ + + assert(len(input_filenames) == len(metadatas)) + + for f in input_filenames: + assert(isinstance(f, audiotools.Filename)) + + from audiotools.text import (ERR_INVALID_FILENAME_FORMAT, + LAB_OPTIONS_FILENAME_FORMAT, + LAB_OUTPUT_OPTIONS, + LAB_OPTIONS_OUTPUT_DIRECTORY, + LAB_OPTIONS_AUDIO_CLASS, + LAB_OPTIONS_AUDIO_QUALITY, + LAB_OPTIONS_OUTPUT_FILES, + LAB_OPTIONS_OUTPUT_FILES_1) + + self.input_filenames = input_filenames + self.metadatas = metadatas + self.selected_class = audio_class + self.selected_quality = quality + self.has_collisions = False # if input files same as output + self.has_duplicates = False # if any track names are duplicates + self.has_errors = False # if format string is invalid + + self.output_format = urwid.Edit( + edit_text=format_string.decode('utf-8'), + wrap='clip') + urwid.connect_signal(self.output_format, + 'change', + self.format_changed) + + self.browse_fields = BrowseFields(self.output_format) + + self.output_directory = SelectDirectory(output_dir, + self.directory_changed) + + self.output_tracks_frame = urwid.Frame( + body=urwid.Filler(urwid.Text(u""))) + + output_tracks_frame_linebox = urwid.LineBox( + self.output_tracks_frame, + title=(LAB_OPTIONS_OUTPUT_FILES if + (len(input_filenames) != 1) else + LAB_OPTIONS_OUTPUT_FILES_1)) + + self.output_tracks = [urwid.Text(u"") for path in input_filenames] + + self.output_tracks_list = urwid.ListBox(self.output_tracks) + + self.invalid_output_format = urwid.Filler( + urwid.Text(ERR_INVALID_FILENAME_FORMAT, + align="center")) + + self.output_quality = SelectOne( + items=[(u"N/A", "")], + label=LAB_OPTIONS_AUDIO_QUALITY) + + self.output_type = SelectOne( + items=sorted([(u"%s - %s" % (t.NAME.decode('ascii'), + t.DESCRIPTION), t) for t in + audiotools.AVAILABLE_TYPES + if t.has_binaries(audiotools.BIN)], + lambda x, y: cmp(x[0], y[0])), + selected_value=audio_class, + on_change=self.select_type, + label=LAB_OPTIONS_AUDIO_CLASS) + + self.select_type(audio_class, quality) + + header = urwid.Pile( + [urwid.Columns([('fixed', 10, + urwid.Text(('label', + u"%s : " % + (LAB_OPTIONS_OUTPUT_DIRECTORY)), + align="right")), + ('weight', 1, self.output_directory)]), + urwid.Columns([('fixed', 10, + urwid.Text(('label', + u"%s : " % + (LAB_OPTIONS_FILENAME_FORMAT)), + align="right")), + ('weight', 1, self.output_format), + ('fixed', 10, self.browse_fields)]), + urwid.Columns([('fixed', 10, + urwid.Text(('label', + u"%s : " % + (LAB_OPTIONS_AUDIO_CLASS)), + align="right")), + ('weight', 1, self.output_type)]), + urwid.Columns([('fixed', 10, + urwid.Text(('label', + u"%s : " % + (LAB_OPTIONS_AUDIO_QUALITY)), + align="right")), + ('weight', 1, self.output_quality)])]) + + widgets = [('fixed', 6, + urwid.Filler(urwid.LineBox( + header, + title=LAB_OUTPUT_OPTIONS))), + ('weight', 1, output_tracks_frame_linebox)] + + if (extra_widgets is not None): + widgets.extend(extra_widgets) + + urwid.Pile.__init__(self, widgets) + + self.update_tracks() + + def select_type(self, audio_class, default_quality=None): + self.selected_class = audio_class + + if (len(audio_class.COMPRESSION_MODES) < 2): + #one audio quality for selected type + try: + quality = audio_class.COMPRESSION_MODES[0] + except IndexError: + #this shouldn't happen, but just in case + quality = "" + self.output_quality.set_items([(u"N/A", quality)], quality) + else: + #two or more audio qualities for selected type + qualities = audio_class.COMPRESSION_MODES + if (default_quality is not None): + default = [q for q in qualities if + q == default_quality][0] + else: + default = [q for q in qualities if + q == audio_class.DEFAULT_COMPRESSION][0] + self.output_quality.set_items( + [(q.decode('ascii') if q not in + audio_class.COMPRESSION_DESCRIPTIONS else + u"%s - %s" % (q.decode('ascii'), + audio_class.COMPRESSION_DESCRIPTIONS[q]), + q) + for q in qualities], + default) + + self.update_tracks() + + def directory_changed(self, widget, new_value): + FS_ENCODING = audiotools.FS_ENCODING + self.update_tracks(output_directory=new_value.encode(FS_ENCODING)) + + def format_changed(self, widget, new_value): + self.update_tracks(filename_format=new_value) + + def update_tracks(self, output_directory=None, filename_format=None): + FS_ENCODING = audiotools.FS_ENCODING + import os.path + + #get the output directory + if (output_directory is None): + output_directory = self.output_directory.get_directory() + + #get selected audio format + audio_class = self.selected_class + + #get current filename format + if (filename_format is None): + filename_format = \ + self.output_format.get_edit_text().encode('utf-8') + try: + #generate list of Filename objects + #from paths, metadatas and format + #with selected output directory prepended + self.output_filenames = [ + audiotools.Filename( + os.path.join(output_directory, + audio_class.track_name(str(filename), + metadata, + filename_format))) + for (filename, + metadata) in zip(self.input_filenames, + self.metadatas)] + + #check for duplicates in output/input files + #(don't care if input files are duplicated) + input_filenames = frozenset([f for f in self.input_filenames + if f.disk_file()]) + output_filenames = set([]) + collisions = set([]) + + self.has_collisions = False + self.has_duplicates = False + for path in self.output_filenames: + if (path in input_filenames): + collisions.add(path) + self.has_collisions = True + elif (path in output_filenames): + collisions.add(path) + self.has_duplicates = True else: - return self.EDITING_FIELDS + output_filenames.add(path) + + #and populate output files list + for (filename, track) in zip(self.output_filenames, + self.output_tracks): + if (filename not in collisions): + track.set_text(unicode(filename)) + else: + track.set_text(("duplicate", unicode(filename))) + if ((self.output_tracks_frame.get_body() is not + self.output_tracks_list)): + self.output_tracks_frame.set_body( + self.output_tracks_list) + self.has_errors = False + except (audiotools.UnsupportedTracknameField, + audiotools.InvalidFilenameFormat): + #if there's an error calling track_name, + #populate files list with an error message + if ((self.output_tracks_frame.get_body() is not + self.invalid_output_format)): + self.output_tracks_frame.set_body( + self.invalid_output_format) + self.has_errors = True + + def selected_options(self): + """returns (AudioFile class, quality string, [output Filename]) + based on selected options in the UI""" + + import os.path + + return (self.selected_class, + self.output_quality.selection(), + [f.expanduser() for f in self.output_filenames]) + + def set_metadatas(self, metadatas): + """metadatas is a list of MetaData objects + (some of which may be None)""" + + if (len(metadatas) != len(self.metadatas)): + raise ValueError("new metadatas must have same count as old") + + self.metadatas = metadatas + self.update_tracks() + + class SingleOutputOptions(urwid.ListBox): + """a widget for selecting the typical set of output options + for a single input file: --output, --type, --quality""" + + def __init__(self, + output_filename, + audio_class, + quality): + """ + | field | value | meaning | + |-----------------+-----------+----------------------------| + | output_filename | string | default output filename | + | audio_class | AudioFile | audio class of output file | + | quality | string | quality level of output | + """ + + #FIXME - add support for directory selector + #FIXME - add support for field populator + + from audiotools.text import (LAB_OPTIONS_OUTPUT, + LAB_OPTIONS_AUDIO_CLASS, + LAB_OPTIONS_AUDIO_QUALITY) + + self.output_filename = EditFilename( + initial_filename=output_filename) + + self.output_quality = SelectOne( + items=[(u"N/A", "")], + label=LAB_OPTIONS_AUDIO_QUALITY) + + self.output_type = SelectOne( + items=sorted([(u"%s - %s" % (t.NAME.decode('ascii'), + t.DESCRIPTION), t) for t in + audiotools.AVAILABLE_TYPES + if t.has_binaries(audiotools.BIN)], + lambda x, y: cmp(x[0], y[0])), + selected_value=audio_class, + on_change=self.select_type, + label=LAB_OPTIONS_AUDIO_CLASS) + + self.select_type(audio_class, quality) + + filename_widget = urwid.Columns( + [('fixed', 10, urwid.Text(('label', + u"%s : " % + (LAB_OPTIONS_OUTPUT)), + align="right")), + ('weight', 1, self.output_filename)]) + + class_widget = urwid.Columns( + [('fixed', 10, + urwid.Text(('label', + u"%s : " % + (LAB_OPTIONS_AUDIO_CLASS)), + align="right")), + ('weight', 1, self.output_type)]) + + quality_widget = urwid.Columns( + [('fixed', 10, + urwid.Text(('label', + u"%s : " % + (LAB_OPTIONS_AUDIO_QUALITY)), + align="right")), + ('weight', 1, self.output_quality)]) + + urwid.ListBox.__init__(self, [filename_widget, + class_widget, + quality_widget]) + + def set_metadata(self, metadata): + """setting metadata allows output field to be populated + by metadata fields""" + + pass # FIXME + + def select_type(self, audio_class, default_quality=None): + self.selected_class = audio_class + + if (len(audio_class.COMPRESSION_MODES) < 2): + #one audio quality for selected type + try: + quality = audio_class.COMPRESSION_MODES[0] + except IndexError: + #this shouldn't happen, but just in case + quality = "" + self.output_quality.set_items([(u"N/A", quality)], quality) else: - return self.UNKNOWN_STATE + #two or more audio qualities for selected type + qualities = audio_class.COMPRESSION_MODES + if (default_quality is not None): + default = [q for q in qualities if + q == default_quality][0] + else: + default = [q for q in qualities if + q == audio_class.DEFAULT_COMPRESSION][0] + self.output_quality.set_items( + [(q.decode('ascii') if q not in + audio_class.COMPRESSION_DESCRIPTIONS else + u"%s - %s" % (q.decode('ascii'), + audio_class.COMPRESSION_DESCRIPTIONS[q]), + q) + for q in qualities], + default) + + def selected_options(self): + """returns (AudioFile class, quality string, output Filename) + based on selected options in the UI""" + + return (self.selected_class, + self.output_quality.selection(), + audiotools.Filename(self.output_filename.get_filename())) + + class MappedButton(urwid.Button): + def __init__(self, label, on_press=None, user_data=None, + key_map={}): + urwid.Button.__init__(self, label=label, + on_press=on_press, + user_data=user_data) + self.__key_map__ = key_map - def handle_text(self, i): - state = self.get_state() - if (state == self.SELECTING_MATCH): + def keypress(self, size, key): + return urwid.Button.keypress(self, + size, + self.__key_map__.get(key, key)) + + class MappedRadioButton(urwid.RadioButton): + def __init__(self, group, label, state='first True', + on_state_change=None, user_data=None, + key_map={}): + urwid.RadioButton.__init__(self, group=group, + label=label, + state=state, + on_state_change=on_state_change, + user_data=user_data) + self.__key_map__ = key_map + + def keypress(self, size, key): + return urwid.RadioButton.keypress(self, + size, + self.__key_map__.get(key, key)) + + class AudioProgressBar(urwid.ProgressBar): + def __init__(self, normal, complete, sample_rate, current=0, done=100, + satt=None): + urwid.ProgressBar.__init__(self, + normal=normal, + complete=complete, + current=current, + done=done, + satt=satt) + self.sample_rate = sample_rate + + def get_text(self): + from audiotools.text import LAB_TRACK_LENGTH + + try: + return LAB_TRACK_LENGTH % \ + ((self.current / self.sample_rate) / 60, + (self.current / self.sample_rate) % 60) + except ZeroDivisionError: + return LAB_TRACK_LENGTH % (0, 0) + + class PlayerGUI(urwid.Frame): + def __init__(self, player, tracks, track_len): + """player is a Player-compatible object + + tracks is a list of (track_name, seconds_length, user_data) tuples + where "track_name" is a unicode string + to place in the radio buttons + "seconds_length" is the length of the track in seconds + and "user_data" is forwarded to calls to select_track() + + track_len is the length of all tracks in seconds""" + + from audiotools.text import (LAB_PLAY_BUTTON, + METADATA_TRACK_NAME, + METADATA_ARTIST_NAME, + METADATA_ALBUM_NAME, + LAB_PLAY_TRACK, + LAB_PREVIOUS_BUTTON, + LAB_NEXT_BUTTON, + LAB_PLAY_STATUS, + LAB_PLAY_STATUS_1) + + self.player = player + + self.track_name = urwid.Text(u"") + self.album_name = urwid.Text(u"") + self.artist_name = urwid.Text(u"") + self.tracknum = urwid.Text(u"") + self.play_pause_button = MappedButton(LAB_PLAY_BUTTON, + on_press=self.play_pause, + key_map={'tab': 'right'}) + self.progress = AudioProgressBar(normal='pg normal', + complete='pg complete', + sample_rate=0, + current=0, + done=100) + + label_width = max([len(audiotools.display_unicode(s)) + for s in [METADATA_TRACK_NAME, + METADATA_ARTIST_NAME, + METADATA_ALBUM_NAME, + LAB_PLAY_TRACK]]) + 3 + + track_name_widget = urwid.Columns( + [('fixed', + label_width, + urwid.Text(('label', + u"%s : " % (METADATA_TRACK_NAME)), + align='right')), + ('weight', 1, self.track_name)]) + + artist_name_widget = urwid.Columns( + [('fixed', + label_width, + urwid.Text(('label', + u"%s : " % (METADATA_ARTIST_NAME)), + align='right')), + ('weight', 1, self.artist_name)]) + + album_name_widget = urwid.Columns( + [('fixed', + label_width, + urwid.Text(('label', + u"%s : " % (METADATA_ALBUM_NAME)), + align='right')), + ('weight', 1, self.album_name)]) + + track_number_widget = urwid.Columns( + [('fixed', + label_width, + urwid.Text(('label', + u"%s : " % (LAB_PLAY_TRACK)), + align='right')), + ('weight', 1, self.tracknum)]) + + header = urwid.Pile([track_name_widget, + artist_name_widget, + album_name_widget, + track_number_widget, + self.progress]) + + controls = urwid.Columns( + [("weight", 1, + urwid.Divider(u" ")), + ("fixed", 12, + MappedButton(LAB_PREVIOUS_BUTTON, + on_press=self.previous_track, + key_map={"tab":"right"})), + ("fixed", 12, + self.play_pause_button), + ("fixed", 12, + MappedButton(LAB_NEXT_BUTTON, + on_press=self.next_track, + key_map={"tab":"down"})), + ("weight", 1, + urwid.Divider(u" "))], + dividechars=4, + focus_column=2) + + self.track_group = [] + self.track_list_widget = urwid.ListBox( + [urwid.Columns( + [("weight", + 1, + MappedRadioButton(group=self.track_group, + label=track_label, + user_data=user_data, + on_state_change=self.select_track, + key_map={'tab': 'down'})), + ("fixed", + 6, + urwid.Text(u"%2.1d:%2.2d" % + (seconds_length / 60, + seconds_length % 60), + align="right"))]) + for (track_label, seconds_length, user_data) in tracks]) + + status = ((LAB_PLAY_STATUS if (len(tracks) > 1) else + LAB_PLAY_STATUS_1) % {"count": len(tracks), + "min": int(track_len) / 60, + "sec": int(track_len) % 60}) + + body = urwid.Pile( + [("fixed", 1, + urwid.Filler(controls)), + ("weight", 1, + BottomLineBox(self.track_list_widget, + title=status))]) + + urwid.Frame.__init__( + self, + body=body, + header=urwid.LineBox(header)) + + def select_track(self, radio_button, new_state, user_data, + auto_play=True): + #to be implemented by subclasses + + raise NotImplementedError() + + def next_track(self, user_data=None): + track_index = [g.state for g in self.track_group].index(True) + try: + self.track_group[track_index + 1].set_state(True) + self.track_list_widget.set_focus(track_index + 1, "above") + except IndexError: + if (len(self.track_group)): + self.track_group[0].set_state(True) + self.track_list_widget.set_focus(0, "above") + self.player.stop() + + def previous_track(self, user_data=None): + track_index = [g.state for g in self.track_group].index(True) + try: + if (track_index == 0): + raise IndexError() + else: + self.track_group[track_index - 1].set_state(True) + self.track_list_widget.set_focus(track_index - 1, "below") + except IndexError: pass - elif (state == self.APPLY_LIST_W_FIELDS): - if (i == 'tab'): - self.work_area.set_focus('body') - elif (state == self.APPLY_LIST): - if (i == 'tab'): - self.work_area.set_focus('body') - elif (state == self.EDITING_LIST_W_FIELDS): - if (i == 'tab'): - self.work_area.set_focus('footer') - elif (i == 'esc'): - for checkbox in self.selected_album_list.radios: - checkbox.set_state(False, do_callback=False) - self.work_area.set_footer(self.collapsed) - self.work_area.set_focus('body') - elif (state == self.EDITING_LIST_ONLY): - if ((i == 'tab') or (i == 'esc')): - self.work_area.set_focus('header') - elif (state == self.EDITING_FIELDS): - if (i == 'tab'): - self.work_area.set_focus('body') - elif (i == 'esc'): - for checkbox in self.selected_album_list.radios: - checkbox.set_state(False, do_callback=False) - self.work_area.set_footer(self.collapsed) - self.work_area.set_focus('body') - - def set_keys(self, keys): - """keys is a [(key, action), ...] list - where 'key' and 'action' are both strings""" - - if (len(keys) > 0): - text = [] - for (last, (key, action)) in audiotools.iter_last(iter(keys)): - text.append(('key', key)) - text.append(u" - " + action) - if (not last): - text.append(u" ") - - self.status.set_text(text) - else: - self.status.set_text(u"") - - def update_focus(self, widget, focus_part): - self.set_state_message(self.get_state()) - - def set_state_message(self, state): - if (state != self.UNKNOWN_STATE): - self.set_keys([ - #SELECTING_MATCH - [], - - #APPLY_LIST_W_FIELDS - [(u"tab", u"go to track list")], - - #APPLY_LIST - [(u"tab", u"go to track list")], - - #EDITING_LIST_W_FIELDS - [(u"esc", u"close fields"), - (u"tab", u"return to fields")], - - #EDITING_LIST_ONLY - [(u"esc/tab", u"return to control buttons")], - - #EDITING_FIELDS - [(u"esc", u"close fields"), - (u"tab", u"return to track list")] - ][state]) + + def update_metadata(self, track_name=None, + album_name=None, + artist_name=None, + track_number=None, + track_total=None, + pcm_frames=0, + channels=0, + sample_rate=0, + bits_per_sample=0): + """updates metadata fields with new values + for when a new track is opened""" + + from audiotools.text import (LAB_X_OF_Y, + LAB_TRACK_LENGTH) + + self.track_name.set_text(track_name if + track_name is not None + else u"") + self.album_name.set_text(album_name if + album_name is not None + else u"") + self.artist_name.set_text(artist_name if + artist_name is not None + else u"") + if (track_number is not None): + if (track_total is not None): + self.tracknum.set_text(LAB_X_OF_Y % + (track_number, + track_total)) + else: + self.tracknum.set_text(unicode(track_number)) else: - self.status.set_text(u"") + self.tracknum.set_text(u"") - def populated_metadata(self): - """yields a fully populated MetaData object per track - to be called once Urwid's main loop has completed""" + try: + seconds_length = pcm_frames / sample_rate + except ZeroDivisionError: + seconds_length = 0 + + self.progress.current = 0 + self.progress.done = pcm_frames + self.progress.sample_rate = sample_rate + + def update_status(self): + self.progress.set_completion(self.player.progress()[0]) + + def play_pause(self, user_data): + from audiotools.text import (LAB_PLAY_BUTTON, + LAB_PAUSE_BUTTON) + + self.player.toggle_play_pause() + if (self.play_pause_button.get_label() == LAB_PLAY_BUTTON): + self.play_pause_button.set_label(LAB_PAUSE_BUTTON) + elif (self.play_pause_button.get_label() == LAB_PAUSE_BUTTON): + self.play_pause_button.set_label(LAB_PLAY_BUTTON) + + def stop(self): + from audiotools.text import LAB_PLAY_BUTTON + + self.player.stop() + self.play_pause_button.set_label(LAB_PLAY_BUTTON) + + def handle_text(self, i): + if ((i == 'esc') or (i == 'q') or (i == 'Q')): + self.perform_exit() + elif ((i == 'n') or (i == 'N')): + self.next_track() + elif ((i == 'p') or (i == 'P')): + self.previous_track() + elif ((i == 's') or (i == 'S')): + self.stop() - return self.selected_album_list.albums[0].get_metadata() + def perform_exit(self, *args): + self.player.close() + raise urwid.ExitMainLoop() + + def timer(main_loop, playergui): + import time + + playergui.update_status() + main_loop.set_alarm_at(tm=time.time() + 0.5, + callback=timer, + user_data=playergui) + + def style(): + """returns a list of widget style tuples + for use with urwid.MainLoop""" + + return [('key', 'white', 'dark blue'), + ('label', 'default,bold', 'default'), + ('modified', 'default,bold', 'default', ''), + ('duplicate', 'light red', 'default'), + ('error', 'light red,bold', 'default'), + ('pg normal', 'white', 'black', 'standout'), + ('pg complete', 'white', 'dark blue'), + ('pg smooth', 'dark blue', 'black')] except ImportError: AVAILABLE = False -def select_metadata(metadata_choices, msg): - """queries the user for the best matching metadata to use""" +def show_available_formats(msg): + """given a Messenger object, + display all the available file formats""" + + from audiotools.text import (LAB_AVAILABLE_FORMATS, + LAB_OUTPUT_TYPE, + LAB_OUTPUT_TYPE_DESCRIPTION) + + msg.output(LAB_AVAILABLE_FORMATS) + msg.info(u"") + + msg.new_row() + msg.output_column(LAB_OUTPUT_TYPE, True) + msg.output_column(u" ") + msg.output_column(LAB_OUTPUT_TYPE_DESCRIPTION) + msg.divider_row([u"-", u" ", u"-"]) + for name in sorted(audiotools.TYPE_MAP.keys()): + msg.new_row() + if (name == audiotools.DEFAULT_TYPE): + msg.output_column(msg.ansi(name.decode('ascii'), + [msg.BOLD, + msg.UNDERLINE]), True) + else: + msg.output_column(name.decode('ascii'), True) + msg.output_column(u" : ") + msg.output_column(audiotools.TYPE_MAP[name].DESCRIPTION) + msg.output_rows() + + +def show_available_qualities(msg, audiotype): + """given a Messenger object and AudioFile class, + display all available quality types for that format""" + + from audiotools.text import (LAB_AVAILABLE_COMPRESSION_TYPES, + LAB_OUTPUT_QUALITY_DESCRIPTION, + LAB_OUTPUT_QUALITY, + ERR_NO_COMPRESSION_MODES) + + if (len(audiotype.COMPRESSION_MODES) > 1): + msg.info(LAB_AVAILABLE_COMPRESSION_TYPES % + (audiotype.NAME.decode('ascii'))) + msg.info(u"") + + msg.new_row() + msg.output_column(LAB_OUTPUT_QUALITY, True) + msg.output_column(u" ") + msg.output_column(LAB_OUTPUT_QUALITY_DESCRIPTION) + msg.divider_row([u"-", u" ", u"-"]) + for mode in audiotype.COMPRESSION_MODES: + msg.new_row() + if (mode == audiotools.__default_quality__(audiotype.NAME)): + msg.output_column(msg.ansi(mode.decode('ascii'), + [msg.BOLD, + msg.UNDERLINE]), True) + else: + msg.output_column(mode.decode('ascii'), True) + if (mode in audiotype.COMPRESSION_DESCRIPTIONS): + msg.output_column(u" : ") + else: + msg.output_column(u" ") + msg.output_column( + audiotype.COMPRESSION_DESCRIPTIONS.get(mode, u"")) + msg.info_rows() + else: + msg.info(ERR_NO_COMPRESSION_MODES % + (audiotype.NAME.decode('ascii'))) + + +def select_metadata(metadata_choices, msg, use_default=False): + """queries the user for the best matching metadata to use + returns a list of MetaData objects for the selected choice""" + #there must be at least one choice assert(len(metadata_choices) > 0) - if (len(metadata_choices) == 1): + + #all choices must have at least 1 track + assert(min(map(len, metadata_choices)) > 0) + + #and all choices must have the same number of tracks + assert(len(set(map(len, metadata_choices))) == 1) + + if ((len(metadata_choices) == 1) or use_default): return metadata_choices[0] else: choice = None while (choice not in range(0, len(metadata_choices))): + from audiotools.text import (LAB_SELECT_BEST_MATCH) for (i, choice) in enumerate(metadata_choices): msg.output(u"%d) %s" % (i + 1, choice[0].album_name)) try: - choice = int(raw_input("please select best match (1-%d) : " % - (len(metadata_choices)))) - 1 + choice = int(raw_input(u"%s (1-%d) : " % + (LAB_SELECT_BEST_MATCH, + len(metadata_choices)))) - 1 except ValueError: choice = None return metadata_choices[choice] +def process_output_options(metadata_choices, + input_filenames, + output_directory, + format_string, + output_class, + quality, + msg, + use_default=False): + """metadata_choices[c][t] + is a MetaData object for choice number "c" and track number "t" + all choices must have the same number of tracks + + input_filenames is a list of Filename objects for input files + the number of input files must equal the number of metadata objects + in each metadata choice + + output_directory is a string of the default output dir + + format_string is a UTF-8 encoded format string + + output_class is the default AudioFile-compatible class + + quality is a string of the default output quality to use + + msg is a Messenger object + + this may take user input from the prompt to select a MetaData choice + after which it yields (output_class, + output_filename, + output_quality, + output_metadata) tuple for each input file + + output_metadata is a reference to an object in metadata_choices + + may raise UnsupportedTracknameField or InvalidFilenameFormat + if the given format_string is invalid + + may raise DuplicateOutputFile + if the same output file is generated more than once + + may raise OutputFileIsInput + if an output file is the same as one of the given input_filenames""" + + #there must be at least one choice + assert(len(metadata_choices) > 0) + + #ensure input filename count is equal to metadata track count + assert(len(metadata_choices[0]) == len(input_filenames)) + + import os.path + + selected_metadata = select_metadata(metadata_choices, msg, use_default) + + #ensure no output paths overwrite input paths + #and that all output paths are distinct + __input__ = frozenset([f for f in input_filenames if f.disk_file()]) + __output__ = set([]) + output_filenames = [] + for (input_filename, metadata) in zip(input_filenames, selected_metadata): + output_filename = audiotools.Filename( + os.path.join(output_directory, + output_class.track_name(str(input_filename), + metadata, + format_string))) + if (output_filename in __input__): + raise audiotools.OutputFileIsInput(output_filename) + elif (output_filename in __output__): + raise audiotools.DuplicateOutputFile(output_filename) + else: + __output__.add(output_filename) + output_filenames.append(output_filename) + + for (output_filename, metadata) in zip(output_filenames, + selected_metadata): + yield (output_class, + output_filename, + quality, + metadata) + + +class PlayerTTY: + OUTPUT_FORMAT = (u"%(track_number)d/%(track_total)d " + + u"[%(sent_minutes)d:%(sent_seconds)2.2d / " + + u"%(total_minutes)d:%(total_seconds)2.2d] " + + u"%(channels)dch %(sample_rate)s " + + u"%(bits_per_sample)d-bit") + + def __init__(self, player): + self.player = player + self.track_number = 0 + self.track_total = 0 + self.channels = 0 + self.sample_rate = 0 + self.bits_per_sample = 0 + self.playing_finished = False + + def previous_track(self): + #implement this in subclass + #to call set_metadata() and have the player open the previous track + raise NotImplementedError() + + def next_track(self): + #implement this in subclass + #to call set_metadata() and have the player open the next track + #set playing_finished to True when all tracks have been exhausted + raise NotImplementedError() + + def set_metadata(self, track_number, + track_total, + channels, + sample_rate, + bits_per_sample): + self.track_number = track_number + self.track_total = track_total + self.channels = channels + self.sample_rate = sample_rate + self.bits_per_sample = bits_per_sample + + def toggle_play_pause(self): + self.player.toggle_play_pause() + + def stop(self): + self.player.stop() + + def run(self, msg, stdin): + """msg is a Messenger object + stdin is a file object for keyboard input + + returns 0 on a successful exit, 1 if there's an error""" + + import os + import tty + import termios + import select + + try: + original_terminal_settings = termios.tcgetattr(0) + except termios.error: + from .text import (ERR_TERMIOS_ERROR, ERR_TERMIOS_SUGGESTION) + msg.error(ERR_TERMIOS_ERROR) + msg.info(ERR_TERMIOS_SUGGESTION) + return 1 + + output_line_len = 0 + self.next_track() + + try: + tty.setcbreak(stdin.fileno()) + while (not self.playing_finished): + (frames_sent, frames_total) = self.progress() + output_line = self.progress_line(frames_sent, frames_total) + msg.ansi_clearline() + if (len(output_line) > output_line_len): + output_line_len = len(output_line) + msg.partial_output(output_line) + else: + msg.partial_output(output_line + + (u" " * (output_line_len - + len(output_line)))) + + (r_list, w_list, x_list) = select.select([stdin.fileno()], + [], [], 1) + if (len(r_list) > 0): + char = os.read(stdin.fileno(), 1) + if (((char == 'q') or + (char == 'Q') or + (char == '\x1B'))): + self.playing_finished = True + elif (char == ' '): + self.toggle_play_pause() + elif ((char == 'n') or + (char == 'N')): + self.next_track() + elif ((char == 'p') or + (char == 'P')): + self.previous_track() + elif ((char == 's') or + (char == 'S')): + self.stop() + else: + pass + + msg.ansi_clearline() + self.player.close() + return 0 + finally: + termios.tcsetattr(0, termios.TCSADRAIN, original_terminal_settings) + + def progress(self): + return self.player.progress() + + def progress_line(self, frames_sent, frames_total): + return (self.OUTPUT_FORMAT % + {"track_number": self.track_number, + "track_total": self.track_total, + "sent_minutes": (frames_sent / self.sample_rate) / 60, + "sent_seconds": (frames_sent / self.sample_rate) % 60, + "total_minutes": (frames_total / self.sample_rate) / 60, + "total_seconds": (frames_total / self.sample_rate) % 60, + "channels": self.channels, + "sample_rate": audiotools.khz(self.sample_rate), + "bits_per_sample": self.bits_per_sample}) + + def not_available_message(msg): """prints a message about lack of Urwid availability to a Messenger object""" - msg.error(u"urwid is required for interactive mode") - msg.output(u"Please download and install urwid " + - u"from http://excess.org/urwid/") - msg.output(u"or your system's package manager.") + from audiotools.text import (ERR_URWID_REQUIRED, + ERR_GET_URWID1, + ERR_GET_URWID2) + msg.error(ERR_URWID_REQUIRED) + msg.output(ERR_GET_URWID1) + msg.output(ERR_GET_URWID2) + + +def xargs_suggestion(args): + """converts arguments to xargs-compatible suggestion + and returns unicode string""" + + import os.path + + #All command-line arguments start with "-" + #but not everything that starts with "-" is a command-line argument. + #However, since this is just a suggestion, + #users can be expected to figure it out. + + return (u"xargs sh -c '%s %s \"%%@\" < /dev/tty'" % + (os.path.basename(args[0]).decode('utf-8'), + " ".join([arg.decode('utf-8') for arg in args[1:] + if arg.startswith("-")])))
View file
audiotools-2.19.tar.gz/audiotools/vorbis.py
Added
@@ -0,0 +1,659 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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) + + +class InvalidVorbis(InvalidFile): + pass + + +def verify_ogg_stream(stream): + """verifies an Ogg stream file object + + this file must be rewound to the start of a page + returns True if the file is valid + raises IOError or ValueError if there is some problem with the file + """ + + from . import verify + verify.ogg(stream) + return True + + +####################### +#Vorbis File +####################### + +class VorbisAudio(AudioFile): + """an Ogg Vorbis file""" + + from .text import (COMP_VORBIS_0, + COMP_VORBIS_10) + + SUFFIX = "ogg" + NAME = SUFFIX + DESCRIPTION = u"Ogg Vorbis" + DEFAULT_COMPRESSION = "3" + COMPRESSION_MODES = tuple([str(i) for i in range(0, 11)]) + COMPRESSION_DESCRIPTIONS = {"0": COMP_VORBIS_0, + "10": COMP_VORBIS_10} + BINARIES = ("oggenc", "oggdec") + REPLAYGAIN_BINARIES = ("vorbisgain", ) + + def __init__(self, filename): + """filename is a plain string""" + + AudioFile.__init__(self, filename) + self.__sample_rate__ = 0 + self.__channels__ = 0 + try: + self.__read_identification__() + except IOError, msg: + raise InvalidVorbis(str(msg)) + + def __read_identification__(self): + from .bitstream import BitstreamReader + + f = open(self.filename, "rb") + try: + ogg_reader = BitstreamReader(f, 1) + (magic_number, + version, + header_type, + granule_position, + self.__serial_number__, + page_sequence_number, + checksum, + segment_count) = ogg_reader.parse("4b 8u 8u 64S 32u 32u 32u 8u") + + if (magic_number != 'OggS'): + from .text import ERR_OGG_INVALID_MAGIC_NUMBER + raise InvalidFLAC(ERR_OGG_INVALID_MAGIC_NUMBER) + if (version != 0): + from .text import ERR_OGG_INVALID_VERSION + raise InvalidFLAC(ERR_OGG_INVALID_VERSION) + + segment_length = ogg_reader.read(8) + + (vorbis_type, + header, + version, + self.__channels__, + self.__sample_rate__, + maximum_bitrate, + nominal_bitrate, + minimum_bitrate, + blocksize0, + blocksize1, + framing) = ogg_reader.parse( + "8u 6b 32u 8u 32u 32u 32u 32u 4u 4u 1u") + + if (vorbis_type != 1): + from .text import ERR_VORBIS_INVALID_TYPE + raise InvalidVorbis(ERR_VORBIS_INVALID_TYPE) + if (header != 'vorbis'): + from .text import ERR_VORBIS_INVALID_HEADER + raise InvalidVorbis(ERR_VORBIS_INVALID_HEADER) + if (version != 0): + from .text import ERR_VORBIS_INVALID_VERSION + raise InvalidVorbis(ERR_VORBIS_INVALID_VERSION) + if (framing != 1): + from .text import ERR_VORBIS_INVALID_FRAMING_BIT + raise InvalidVorbis(ERR_VORBIS_INVALID_FRAMING_BIT) + finally: + f.close() + + def lossless(self): + """returns False""" + + return False + + def bits_per_sample(self): + """returns an integer number of bits-per-sample this track contains""" + + return 16 + + 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() == 1): + return ChannelMask.from_fields( + front_center=True) + elif (self.channels() == 2): + return ChannelMask.from_fields( + front_left=True, front_right=True) + elif (self.channels() == 3): + return ChannelMask.from_fields( + front_left=True, front_right=True, + front_center=True) + elif (self.channels() == 4): + return ChannelMask.from_fields( + front_left=True, front_right=True, + back_left=True, back_right=True) + elif (self.channels() == 5): + return ChannelMask.from_fields( + front_left=True, front_right=True, + front_center=True, + back_left=True, back_right=True) + elif (self.channels() == 6): + return ChannelMask.from_fields( + front_left=True, front_right=True, + front_center=True, + back_left=True, back_right=True, + low_frequency=True) + elif (self.channels() == 7): + return ChannelMask.from_fields( + front_left=True, front_right=True, + front_center=True, + side_left=True, side_right=True, + back_center=True, low_frequency=True) + elif (self.channels() == 8): + return ChannelMask.from_fields( + front_left=True, front_right=True, + side_left=True, side_right=True, + back_left=True, back_right=True, + front_center=True, low_frequency=True) + else: + return ChannelMask(0) + + def total_frames(self): + """returns the total PCM frames of the track as an integer""" + + from .bitstream import BitstreamReader + + pcm_samples = 0 + end_of_stream = 0 + try: + ogg_stream = BitstreamReader(file(self.filename, "rb"), 1) + while (end_of_stream == 0): + (magic_number, + version, + end_of_stream, + granule_position, + page_segment_count) = ogg_stream.parse( + "4b 8u 1p 1p 1u 5p 64S 32p 32p 32p 8u") + ogg_stream.skip_bytes(sum([ogg_stream.read(8) for i in + xrange(page_segment_count)])) + + if ((magic_number != "OggS") or (version != 0)): + return 0 + if (granule_position >= 0): + pcm_samples = granule_position + + ogg_stream.close() + return pcm_samples + except IOError: + return 0 + + def sample_rate(self): + """returns the rate of the track's audio as an integer number of Hz""" + + return self.__sample_rate__ + + def to_pcm(self): + """returns a PCMReader object containing the track's PCM data""" + + from . import PCMReader + from . import BIN + import subprocess + import os + + sub = subprocess.Popen([BIN['oggdec'], '-Q', + '-b', str(16), + '-e', str(0), + '-s', str(1), + '-R', + '-o', '-', + self.filename], + stdout=subprocess.PIPE, + stderr=file(os.devnull, "a")) + + pcmreader = PCMReader(sub.stdout, + sample_rate=self.sample_rate(), + channels=self.channels(), + channel_mask=int(self.channel_mask()), + bits_per_sample=self.bits_per_sample(), + process=sub) + + if (self.channels() <= 2): + return pcmreader + elif (self.channels() <= 8): + #these mappings transform Vorbis order into ChannelMask order + from . import ReorderedPCMReader + + standard_channel_mask = self.channel_mask() + vorbis_channel_mask = VorbisChannelMask(self.channel_mask()) + return ReorderedPCMReader( + pcmreader, + [vorbis_channel_mask.channels().index(channel) for channel in + standard_channel_mask.channels()]) + else: + return pcmreader + + @classmethod + def from_pcm(cls, filename, pcmreader, compression=None): + """returns a PCMReader object containing the track's PCM data""" + + from . import transfer_framelist_data + from . import BIN + from . import ignore_sigint + from . import EncodingError + from . import DecodingError + from . import UnsupportedChannelMask + from . import __default_quality__ + import subprocess + import os + + if (((compression is None) or + (compression not in cls.COMPRESSION_MODES))): + compression = __default_quality__(cls.NAME) + + devnull = file(os.devnull, 'ab') + + sub = subprocess.Popen([BIN['oggenc'], '-Q', + '-r', + '-B', str(pcmreader.bits_per_sample), + '-C', str(pcmreader.channels), + '-R', str(pcmreader.sample_rate), + '--raw-endianness', str(0), + '-q', compression, + '-o', filename, '-'], + stdin=subprocess.PIPE, + stdout=devnull, + stderr=devnull, + preexec_fn=ignore_sigint) + + if ((pcmreader.channels <= 2) or (int(pcmreader.channel_mask) == 0)): + try: + transfer_framelist_data(pcmreader, sub.stdin.write) + except (IOError, ValueError), err: + sub.stdin.close() + sub.wait() + cls.__unlink__(filename) + raise EncodingError(str(err)) + except Exception, err: + sub.stdin.close() + sub.wait() + cls.__unlink__(filename) + raise err + + elif (pcmreader.channels <= 8): + if (int(pcmreader.channel_mask) in + (0x7, # FR, FC, FL + 0x33, # FR, FL, BR, BL + 0x37, # FR, FC, FL, BL, BR + 0x3f, # FR, FC, FL, BL, BR, LFE + 0x70f, # FL, FC, FR, SL, SR, BC, LFE + 0x63f)): # FL, FC, FR, SL, SR, BL, BR, LFE + + standard_channel_mask = ChannelMask(pcmreader.channel_mask) + vorbis_channel_mask = VorbisChannelMask(standard_channel_mask) + else: + raise UnsupportedChannelMask(filename, + int(pcmreader.channel_mask)) + + try: + from . import ReorderedPCMReader + + transfer_framelist_data( + ReorderedPCMReader( + pcmreader, + [standard_channel_mask.channels().index(channel) + for channel in vorbis_channel_mask.channels()]), + sub.stdin.write) + except (IOError, ValueError), err: + sub.stdin.close() + sub.wait() + cls.__unlink__(filename) + raise EncodingError(str(err)) + except Exception, err: + sub.stdin.close() + sub.wait() + cls.__unlink__(filename) + raise err + + else: + raise UnsupportedChannelMask(filename, + int(pcmreader.channel_mask)) + + try: + pcmreader.close() + except DecodingError, err: + raise EncodingError(err.error_message) + + sub.stdin.close() + devnull.close() + + if (sub.wait() == 0): + return VorbisAudio(filename) + else: + raise EncodingError(u"unable to encode file with oggenc") + + def update_metadata(self, metadata): + """takes this track's current MetaData object + as returned by get_metadata() and sets this track's metadata + with any fields updated in that object + + raises IOError if unable to write the file + """ + + from .bitstream import BitstreamReader + from .bitstream import BitstreamRecorder + from .bitstream import BitstreamWriter + from .ogg import OggStreamWriter + from .ogg import OggStreamReader + from .ogg import read_ogg_packets_data + from . import iter_first + from .vorbiscomment import VorbisComment + + if (metadata is None): + return + + if (not isinstance(metadata, VorbisComment)): + from .text import ERR_FOREIGN_METADATA + raise ValueError(ERR_FOREIGN_METADATA) + + original_reader = BitstreamReader(open(self.filename, "rb"), 1) + original_ogg = OggStreamReader(original_reader) + original_serial_number = original_ogg.serial_number + original_packets = read_ogg_packets_data(original_reader) + + #save the current file's identification page/packet + #(the ID packet is always fixed size, and fits in one page) + identification_page = original_ogg.read_page() + + #discard the current file's comment packet + original_packets.next() + + #save the current file's setup packet + setup_packet = original_packets.next() + + #save all the subsequent Ogg pages + data_pages = list(original_ogg.pages()) + + del(original_ogg) + del(original_packets) + original_reader.close() + + updated_writer = BitstreamWriter(open(self.filename, "wb"), 1) + updated_ogg = OggStreamWriter(updated_writer, original_serial_number) + + #write the identification packet in its own page + updated_ogg.write_page(*identification_page) + + #write the new comment packet in its own page(s) + comment_writer = BitstreamRecorder(1) + comment_writer.build("8u 6b", (3, "vorbis")) + vendor_string = metadata.vendor_string.encode('utf-8') + comment_writer.build("32u %db" % (len(vendor_string)), + (len(vendor_string), vendor_string)) + comment_writer.write(32, len(metadata.comment_strings)) + for comment_string in metadata.comment_strings: + comment_string = comment_string.encode('utf-8') + comment_writer.build("32u %db" % (len(comment_string)), + (len(comment_string), comment_string)) + + comment_writer.build("1u a", (1,)) + + for (first_page, segments) in iter_first( + updated_ogg.segments_to_pages( + updated_ogg.packet_to_segments(comment_writer.data()))): + updated_ogg.write_page(0, segments, 0 if first_page else 1, 0, 0) + + #write the setup packet in its own page(s) + for (first_page, segments) in iter_first( + updated_ogg.segments_to_pages( + updated_ogg.packet_to_segments(setup_packet))): + updated_ogg.write_page(0, segments, 0 if first_page else 1, 0, 0) + + #write the subsequent Ogg pages + for page in data_pages: + updated_ogg.write_page(*page) + + def set_metadata(self, metadata): + """takes a MetaData object and sets this track's metadata + + this metadata includes track name, album name, and so on + raises IOError if unable to write the file""" + + from .vorbiscomment import VorbisComment + + if (metadata is not None): + metadata = VorbisComment.converted(metadata) + + old_metadata = self.get_metadata() + + metadata.vendor_string = old_metadata.vendor_string + + #remove REPLAYGAIN_* tags from new metadata (if any) + for key in [u"REPLAYGAIN_TRACK_GAIN", + u"REPLAYGAIN_TRACK_PEAK", + u"REPLAYGAIN_ALBUM_GAIN", + u"REPLAYGAIN_ALBUM_PEAK", + u"REPLAYGAIN_REFERENCE_LOUDNESS"]: + try: + metadata[key] = old_metadata[key] + except KeyError: + metadata[key] = [] + + self.update_metadata(metadata) + + def get_metadata(self): + """returns a MetaData object, or None + + raises IOError if unable to read the file""" + + from .bitstream import BitstreamReader + from .ogg import read_ogg_packets + from .vorbiscomment import VorbisComment + + packets = read_ogg_packets( + BitstreamReader(open(self.filename, "rb"), 1)) + + identification = packets.next() + comment = packets.next() + + (packet_type, packet_header) = comment.parse("8u 6b") + if ((packet_type != 3) or (packet_header != 'vorbis')): + return None + else: + vendor_string = \ + comment.read_bytes(comment.read(32)).decode('utf-8') + comment_strings = [ + comment.read_bytes(comment.read(32)).decode('utf-8') + for i in xrange(comment.read(32))] + if (comment.read(1) == 1): # framing bit + return VorbisComment(comment_strings, vendor_string) + else: + return None + + def delete_metadata(self): + """deletes the track's MetaData + + this removes or unsets tags as necessary in order to remove all data + raises IOError if unable to write the file""" + + from . import MetaData + + #the vorbis comment packet is required, + #so simply zero out its contents + self.set_metadata(MetaData()) + + @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 . import BIN + from . import open_files + import subprocess + import os + + track_names = [track.filename for track in + open_files(filenames) if + isinstance(track, cls)] + + if (progress is not None): + progress(0, 1) + + if ((len(track_names) > 0) and BIN.can_execute(BIN['vorbisgain'])): + devnull = file(os.devnull, 'ab') + + sub = subprocess.Popen([BIN['vorbisgain'], + '-q', '-a'] + track_names, + stdout=devnull, + stderr=devnull) + sub.wait() + devnull.close() + + if (progress is not None): + progress(1, 1) + + @classmethod + def can_add_replay_gain(cls, audiofiles): + """given a list of audiofiles, + returns True if this class can add ReplayGain to those files + returns False if not""" + + for audiofile in audiofiles: + if (not isinstance(audiofile, VorbisAudio)): + return False + else: + from . import BIN + + return BIN.can_execute(BIN['vorbisgain']) + + @classmethod + def supports_replay_gain(cls): + """returns True if this class supports ReplayGain""" + + return True + + @classmethod + def lossless_replay_gain(cls): + """returns True""" + + return True + + def replay_gain(self): + """returns a ReplayGain object of our ReplayGain values + + returns None if we have no values""" + + from . import ReplayGain + + vorbis_metadata = self.get_metadata() + + if ((vorbis_metadata is not None) and + (set(['REPLAYGAIN_TRACK_PEAK', + 'REPLAYGAIN_TRACK_GAIN', + 'REPLAYGAIN_ALBUM_PEAK', + 'REPLAYGAIN_ALBUM_GAIN']).issubset(vorbis_metadata.keys()))): + # we have ReplayGain data + try: + return ReplayGain( + vorbis_metadata['REPLAYGAIN_TRACK_GAIN'][0][0:-len(" dB")], + vorbis_metadata['REPLAYGAIN_TRACK_PEAK'][0], + vorbis_metadata['REPLAYGAIN_ALBUM_GAIN'][0][0:-len(" dB")], + vorbis_metadata['REPLAYGAIN_ALBUM_PEAK'][0]) + except (IndexError, ValueError): + return None + else: + return None + + 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""" + + #Ogg stream 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, err: + raise InvalidVorbis(str(err)) + try: + try: + result = verify_ogg_stream(f) + if (progress is not None): + progress(1, 1) + return result + except (IOError, ValueError), err: + raise InvalidVorbis(str(err)) + finally: + f.close() + + +class VorbisChannelMask(ChannelMask): + """the Vorbis-specific channel mapping""" + + def __repr__(self): + return "VorbisChannelMask(%s)" % \ + ",".join(["%s=%s" % (field, getattr(self, field)) + for field in self.SPEAKER_TO_MASK.keys() + if (getattr(self, field))]) + + def channels(self): + """returns a list of speaker strings this mask contains + + returned in the order in which they should appear + in the PCM stream + """ + + count = len(self) + if (count == 1): + return ["front_center"] + elif (count == 2): + return ["front_left", "front_right"] + elif (count == 3): + return ["front_left", "front_center", "front_right"] + elif (count == 4): + return ["front_left", "front_right", + "back_left", "back_right"] + elif (count == 5): + return ["front_left", "front_center", "front_right", + "back_left", "back_right"] + elif (count == 6): + return ["front_left", "front_center", "front_right", + "back_left", "back_right", "low_frequency"] + elif (count == 7): + return ["front_left", "front_center", "front_right", + "side_left", "side_right", "back_center", + "low_frequency"] + elif (count == 8): + return ["front_left", "front_center", "front_right", + "side_left", "side_right", + "back_left", "back_right", "low_frequency"] + else: + return []
View file
audiotools-2.19.tar.gz/audiotools/vorbiscomment.py
Added
@@ -0,0 +1,529 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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 . import MetaData +import re + + +class VorbisComment(MetaData): + ATTRIBUTE_MAP = {'track_name': u'TITLE', + 'track_number': u'TRACKNUMBER', + 'track_total': u'TRACKTOTAL', + 'album_name': u'ALBUM', + 'artist_name': u'ARTIST', + 'performer_name': u'PERFORMER', + 'composer_name': u'COMPOSER', + 'conductor_name': u'CONDUCTOR', + 'media': u'SOURCE MEDIUM', + 'ISRC': u'ISRC', + 'catalog': u'CATALOG', + 'copyright': u'COPYRIGHT', + 'publisher': u'PUBLISHER', + 'year': u'DATE', + 'album_number': u'DISCNUMBER', + 'album_total': u'DISCTOTAL', + 'comment': u'COMMENT'} + + ALIASES = {} + + for aliases in [frozenset([u'TRACKTOTAL', u'TOTALTRACKS']), + frozenset([u'DISCTOTAL', u'TOTALDISCS'])]: + for alias in aliases: + ALIASES[alias] = aliases + + def __init__(self, comment_strings, vendor_string): + """comment_strings is a list of unicode strings + + vendor_string is a unicode string""" + + self.__dict__["comment_strings"] = comment_strings + self.__dict__["vendor_string"] = vendor_string + + def keys(self): + return list(set([comment.split(u"=", 1)[0] + for comment in self.comment_strings + if (u"=" in comment)])) + + def values(self): + return [self[key] for key in self.keys()] + + def items(self): + return [(key, self[key]) for key in self.keys()] + + def __contains__(self, key): + matching_keys = self.ALIASES.get(key.upper(), frozenset([key.upper()])) + + return len([item_value for (item_key, item_value) in + [comment.split(u"=", 1) for comment in self.comment_strings + if (u"=" in comment)] + if (item_key.upper() in matching_keys)]) > 0 + + def __getitem__(self, key): + matching_keys = self.ALIASES.get(key.upper(), frozenset([key.upper()])) + + values = [item_value for (item_key, item_value) in + [comment.split(u"=", 1) for comment in self.comment_strings + if (u"=" in comment)] + if (item_key.upper() in matching_keys)] + + if (len(values) > 0): + return values + else: + raise KeyError(key) + + def __setitem__(self, key, values): + new_values = values[:] + new_comment_strings = [] + matching_keys = self.ALIASES.get(key.upper(), frozenset([key.upper()])) + + for comment in self.comment_strings: + if (u"=" in comment): + (c_key, c_value) = comment.split(u"=", 1) + if (c_key.upper() in matching_keys): + try: + #replace current value with newly set value + new_comment_strings.append( + u"%s=%s" % (c_key, new_values.pop(0))) + except IndexError: + #no more newly set values, so remove current value + continue + else: + #passthrough unmatching values + new_comment_strings.append(comment) + else: + #passthrough values with no "=" sign + new_comment_strings.append(comment) + + #append any leftover values + for new_value in new_values: + new_comment_strings.append(u"%s=%s" % (key.upper(), new_value)) + + self.__dict__["comment_strings"] = new_comment_strings + + def __repr__(self): + return "VorbisComment(%s, %s)" % \ + (repr(self.comment_strings), repr(self.vendor_string)) + + def __comment_name__(self): + return u"Vorbis Comment" + + def raw_info(self): + """returns a Unicode string of low-level MetaData information + + whereas __unicode__ is meant to contain complete information + at a very high level + raw_info() should be more developer-specific and with + very little adjustment or reordering to the data itself + """ + + from os import linesep + from . import display_unicode + + #align text strings on the "=" sign, if any + + if (len(self.comment_strings) > 0): + max_indent = max([len(display_unicode(comment.split(u"=", 1)[0])) + for comment in self.comment_strings + if u"=" in comment]) + + comment_strings = [] + for comment in self.comment_strings: + if (u"=" in comment): + comment_strings.append( + u" " * (max_indent - + len( + display_unicode(comment.split(u"=", 1)[0]))) + + comment) + else: + comment_strings.append(comment) + else: + comment_strings = 0 + + return linesep.decode('ascii').join( + [u"%s: %s" % (self.__comment_name__(), self.vendor_string)] + + comment_strings) + + def __getattr__(self, attr): + #returns the first matching key for the given attribute + #in our list of comment strings + + if ((attr == "track_number") or (attr == "album_number")): + try: + #get the TRACKNUMBER/DISCNUMBER values + #return the first value that contains an integer + for value in self[self.ATTRIBUTE_MAP[attr]]: + integer = re.search(r'\d+', value) + if (integer is not None): + return int(integer.group(0)) + else: + #otherwise, return None + return None + except KeyError: + #if no TRACKNUMBER/DISCNUMBER, return None + return None + elif ((attr == "track_total") or (attr == "album_total")): + try: + #get the TRACKTOTAL/DISCTOTAL values + #return the first value that contains an integer + for value in self[self.ATTRIBUTE_MAP[attr]]: + integer = re.search(r'\d+', value) + if (integer is not None): + return int(integer.group(0)) + except KeyError: + pass + + #if no TRACKTOTAL/DISCTOTAL, + #or none of them contain an integer, + #look for slashed TRACKNUMBER/DISCNUMBER values + try: + for value in self[{"track_total":u"TRACKNUMBER", + "album_total":u"DISCNUMBER"}[attr]]: + integer = re.search(r'/\D*(\d+)', value) + if (integer is not None): + return int(integer.group(1)) + except KeyError: + #no slashed TRACKNUMBER/DISCNUMBER values either + #so return None + return None + elif (attr in self.ATTRIBUTE_MAP): + #attribute is supported by VorbisComment + try: + #if present, return the first value + return self[self.ATTRIBUTE_MAP[attr]][0] + except KeyError: + #if not present, return None + return None + elif (attr in self.FIELDS): + #attribute is supported by MetaData + #but not supported by VorbisComment + return None + else: + #attribute is not MetaData-specific + try: + return self.__dict__[attr] + except KeyError: + raise AttributeError(attr) + + def __setattr__(self, attr, value): + #updates the first matching field for the given attribute + #in our list of comment strings + + if ((value is None) and (attr in self.FIELDS)): + #setting any value to None is equivilent to deleting it + #in this high-level implementation + delattr(self, attr) + elif ((attr == "track_number") or (attr == "album_number")): + key = self.ATTRIBUTE_MAP[attr] + try: + new_values = self[key] + for i in xrange(len(new_values)): + #look for the first TRACKNUMBER/DISCNUMBER field + #which contains an integer + if (re.search(r'\d+', new_values[i]) is not None): + #and replace the integer part of the field + new_values[i] = re.sub(r'\d+', + unicode(int(value)), + new_values[i], + 1) + + #then set the field to the new set of values + #(which may contain subsequent fields to leave as-is) + self[key] = new_values + break + else: + #no integer field with matching key + #so append new integer field + self[key] = self[key] + [unicode(int(value))] + except KeyError: + #no TRACKNUMBER/DISCNUMBER field + #so add a new one + self[key] = [unicode(int(value))] + elif ((attr == "track_total") or (attr == "album_total")): + key = self.ATTRIBUTE_MAP[attr] + try: + new_values = self[key] + for i in xrange(len(new_values)): + #look for the first TRACKTOTAL/DISCTOTAL field + #which contains an integer + if (re.search(r'\d+', new_values[i]) is not None): + #and replace the integer part of the field + new_values[i] = re.sub(r'\d+', + unicode(int(value)), + new_values[i], + 1) + self[key] = new_values + return + except KeyError: + new_values = [] + + #no TRACKTOTAL/DISCTOTAL field + #or none of them contain an integer, + #so look for slashed TRACKNUMBER/DISCNUMBER values + try: + slashed_key = {"track_total": u"TRACKNUMBER", + "album_total": u"DISCNUMBER"}[attr] + new_slashed_values = self[slashed_key] + for i in xrange(len(new_slashed_values)): + #look for the first TRACKNUMBER/DISCNUMBER field + #which contains a slashed value + if (re.search(r'/\D*\d+', + new_slashed_values[i]) is not None): + #and replace the slashed part of the field + new_slashed_values[i] = re.sub( + r'(/\D*)(\d+)', + u'\\g<1>' + unicode(int(value)), + new_slashed_values[i], + 1) + self[slashed_key] = new_slashed_values + return + except KeyError: + #no TRACKNUMBER/DISCNUMBER field found + pass + + #no TRACKTOTAL/DISCTOTAL fields + #or no integer values in those fields, + #and no slashed TRACKNUMBER/DISCNUMBER values + #or no integer values in those fields, + #so append a TRACKTOTAL/DISCTOTAL field + self[key] = new_values + [unicode(int(value))] + elif (attr in self.ATTRIBUTE_MAP.keys()): + key = self.ATTRIBUTE_MAP[attr] + try: + current_values = self[key] + #try to leave subsequent fields with the same key as-is + self[key] = [unicode(value)] + current_values[1:] + except KeyError: + #no current field with the same key, so add a new one + self[key] = [unicode(value)] + elif (attr in self.FIELDS): + #attribute is supported by MetaData + #but not supported by VorbisComment + #so ignore it + pass + else: + #attribute is not MetaData-specific, so set as-is + self.__dict__[attr] = value + + def __delattr__(self, attr): + #deletes all matching keys for the given attribute + #in our list of comment strings + + if ((attr == "track_number") or (attr == "album_number")): + key = self.ATTRIBUTE_MAP[attr] + try: + #convert the slashed side of TRACKNUMBER/DISCNUMBER fields + #to TRACKTOTAL/DISCKTOTAL fields + slashed_field = re.compile(r'/\s*(.*)') + + orphaned_totals = [match.group(1) for match in + [slashed_field.search(value) + for value in self[key]] + if match is not None] + + #remove any TRACKNUMBER/DISCNUMBER fields + self[key] = [] + + if (len(orphaned_totals) > 0): + total_key = {"track_number": u"TRACKTOTAL", + "album_number": u"DISCTOTAL"}[attr] + try: + #append new TRACKTOTAL/DISCTOTAL fields + self[total_key] = self[total_key] + orphaned_totals + except KeyError: + #no TRACKTOTAL/DISCTOTAL field, so add new ones + self[total_key] = orphaned_totals + except KeyError: + #no TRACKNUMBER/DISCNUMBER fields to remove + pass + elif ((attr == "track_total") or (attr == "album_total")): + slashed_key = {"track_total": u"TRACKNUMBER", + "album_total": u"DISCNUMBER"}[attr] + slashed_field = re.compile(r'(.*?)\s*/.*') + + def slash_filter(s): + match = slashed_field.match(s) + if (match is not None): + return match.group(1) + else: + return s + + #remove TRACKTOTAL/DISCTOTAL fields + self[self.ATTRIBUTE_MAP[attr]] = [] + + #preserve the non-slashed side of TRACKNUMBER/DISCNUMBER fields + try: + self[slashed_key] = map(slash_filter, self[slashed_key]) + except KeyError: + #no TRACKNUMBER/DISCNUMBER fields + pass + elif (attr in self.ATTRIBUTE_MAP): + #unlike __setattr_, which tries to preserve multiple instances + #of fields, __delattr__ wipes them all + #so that orphaned fields don't show up after deletion + self[self.ATTRIBUTE_MAP[attr]] = [] + elif (attr in self.FIELDS): + #attribute is part of MetaData + #but not supported by VorbisComment + pass + else: + try: + del(self.__dict__[attr]) + except KeyError: + raise AttributeError(attr) + + def __eq__(self, metadata): + if (isinstance(metadata, self.__class__)): + return self.comment_strings == metadata.comment_strings + else: + return MetaData.__eq__(self, metadata) + + @classmethod + def converted(cls, metadata): + """converts metadata from another class to VorbisComment""" + + from . import VERSION + + if (metadata is None): + return None + elif (isinstance(metadata, VorbisComment)): + return cls(metadata.comment_strings[:], + metadata.vendor_string) + elif (metadata.__class__.__name__ == 'FlacMetaData'): + if (metadata.has_block(4)): + vorbis_comment = metadata.get_block(4) + return cls(vorbis_comment.comment_strings[:], + vorbis_comment.vendor_string) + else: + return cls([], u"Python Audio Tools %s" % (VERSION)) + elif (metadata.__class__.__name__ in ('Flac_VORBISCOMMENT', + 'OpusTags')): + return cls(metadata.comment_strings[:], + metadata.vendor_string) + else: + comment_strings = [] + + for (attr, key) in cls.ATTRIBUTE_MAP.items(): + value = getattr(metadata, attr) + if (value is not None): + comment_strings.append(u"%s=%s" % (key, value)) + + return cls(comment_strings, u"Python Audio Tools %s" % (VERSION)) + + @classmethod + def supports_images(cls): + """returns False""" + + #There's actually a (proposed?) standard to add embedded covers + #to Vorbis Comments by base64 encoding them. + #This strikes me as messy and convoluted. + #In addition, I'd have to perform a special case of + #image extraction and re-insertion whenever converting + #to FlacMetaData. The whole thought gives me a headache. + + return False + + def images(self): + """returns a list of embedded Image objects""" + + return [] + + def clean(self, fixes_performed): + """returns a new MetaData object that's been cleaned of problems""" + + reverse_attr_map = {} + for (attr, key) in self.ATTRIBUTE_MAP.items(): + reverse_attr_map[key] = attr + if (key in self.ALIASES): + for alias in self.ALIASES[key]: + reverse_attr_map[alias] = attr + + cleaned_fields = [] + + for comment_string in self.comment_strings: + if (u"=" in comment_string): + (key, value) = comment_string.split(u"=", 1) + if (key.upper() in reverse_attr_map): + attr = reverse_attr_map[key.upper()] + #handle all text fields by stripping whitespace + if (len(value.strip()) == 0): + from .text import CLEAN_REMOVE_EMPTY_TAG + fixes_performed.append( + CLEAN_REMOVE_EMPTY_TAG % + {"field": key}) + else: + fix1 = value.rstrip() + if (fix1 != value): + from .text import CLEAN_REMOVE_TRAILING_WHITESPACE + fixes_performed.append( + CLEAN_REMOVE_TRAILING_WHITESPACE % + {"field": key}) + + fix2 = fix1.lstrip() + if (fix2 != fix1): + from .text import CLEAN_REMOVE_LEADING_WHITESPACE + fixes_performed.append( + CLEAN_REMOVE_LEADING_WHITESPACE % + {"field": key}) + + #integer fields also strip leading zeroes + if (((attr == "track_number") or + (attr == "album_number"))): + match = re.match(r'(.*?)\s*/\s*(.*)', fix2) + if (match is not None): + #fix whitespace/zeroes + #on either side of slash + fix3 = u"%s/%s" % ( + match.group(1).lstrip(u"0"), + match.group(2).lstrip(u"0")) + + if (fix3 != fix2): + from .text import ( + CLEAN_REMOVE_LEADING_WHITESPACE_ZEROES) + fixes_performed.append( + CLEAN_REMOVE_LEADING_WHITESPACE_ZEROES % + {"field": key}) + else: + #fix zeroes only + fix3 = fix2.lstrip(u"0") + + if (fix3 != fix2): + from .text import ( + CLEAN_REMOVE_LEADING_ZEROES) + fixes_performed.append( + CLEAN_REMOVE_LEADING_ZEROES % + {"field": key}) + elif ((attr == "track_total") or + (attr == "album_total")): + fix3 = fix2.lstrip(u"0") + if (fix3 != fix2): + from .text import CLEAN_REMOVE_LEADING_ZEROES + fixes_performed.append( + CLEAN_REMOVE_LEADING_ZEROES % + {"field": key}) + else: + fix3 = fix2 + + cleaned_fields.append(u"%s=%s" % (key, fix3)) + else: + cleaned_fields.append(comment_string) + else: + cleaned_fields.append(comment_string) + + return self.__class__(cleaned_fields, self.vendor_string)
View file
audiotools-2.19.tar.gz/audiotools/wav.py
Added
@@ -0,0 +1,1132 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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 . import (AudioFile, InvalidFile, PCMReader, WaveContainer) +from .pcm import FrameList +import struct + +####################### +#RIFF WAVE +####################### + + +class RIFF_Chunk: + """a raw chunk of RIFF WAVE data""" + + def __init__(self, chunk_id, chunk_size, chunk_data): + """chunk_id should be a binary string of ASCII + chunk_data should be a binary string of chunk data""" + + #FIXME - check chunk_id's validity + + self.id = chunk_id + self.__size__ = chunk_size + self.__data__ = chunk_data + + def __repr__(self): + return "RIFF_Chunk(%s)" % (repr(self.id)) + + def size(self): + """returns size of chunk in bytes + not including any spacer byte for odd-sized chunks""" + + return self.__size__ + + def total_size(self): + """returns the total size of the chunk + including the 8 byte ID/size and any padding byte""" + + if (self.__size__ % 2): + return 8 + self.__size__ + 1 + else: + return 8 + self.__size__ + + def data(self): + """returns chunk data as file-like object""" + + import cStringIO + + return cStringIO.StringIO(self.__data__) + + def verify(self): + """returns True if the chunk is sized properly""" + + return self.__size__ == len(self.__data__) + + def write(self, f): + """writes the entire chunk to the given output file object + returns size of entire chunk (including header and spacer) + in bytes""" + + f.write(self.id) + f.write(struct.pack("<I", self.__size__)) + f.write(self.__data__) + if (self.__size__ % 2): + f.write(chr(0)) + return self.total_size() + + +class RIFF_File_Chunk(RIFF_Chunk): + """a raw chunk of RIFF WAVE data taken from an existing file""" + + def __init__(self, chunk_id, chunk_size, wav_file, chunk_data_offset): + """chunk_id should be a binary string of ASCII + chunk_size is the size of the chunk in bytes + (not counting any spacer byte) + wav_file is the file this chunk belongs to + chunk_data_offset is the offset to the chunk's data bytes + (not including the 8 byte header)""" + + self.id = chunk_id + self.__size__ = chunk_size + self.__wav_file__ = wav_file + self.__offset__ = chunk_data_offset + + def __repr__(self): + return "RIFF_File_Chunk(%s)" % (repr(self.id)) + + def data(self): + """returns chunk data as file-like object""" + + self.__wav_file__.seek(self.__offset__) + + from . import LimitedFileReader + + return LimitedFileReader(self.__wav_file__, self.size()) + + def verify(self): + """returns True if the chunk is sized properly""" + + self.__wav_file__.seek(self.__offset__) + to_read = self.__size__ + while (to_read > 0): + s = self.__wav_file__.read(min(0x100000, to_read)) + if (len(s) == 0): + return False + else: + to_read -= len(s) + return True + + def write(self, f): + """writes the entire chunk to the given output file object + returns size of entire chunk (including header and spacer) + in bytes""" + + f.write(self.id) + f.write(struct.pack("<I", self.__size__)) + self.__wav_file__.seek(self.__offset__) + to_write = self.__size__ + while (to_write > 0): + s = self.__wav_file__.read(min(0x100000, to_write)) + f.write(s) + to_write -= len(s) + + if (self.__size__ % 2): + f.write(chr(0)) + return self.total_size() + + +def pad_data(pcm_frames, channels, bits_per_sample): + """returns True if the given stream combination + requires an extra padding byte at the end of the 'data' chunk""" + + return (pcm_frames * channels * (bits_per_sample / 8)) % 2 + + +def validate_header(header): + """given header string as returned by wave_header_footer(), + returns (total size, data size) + where total size is the size of the file in bytes + and data size is the size of the data chunk in bytes + (not including any padding byte) + + the size of the data chunk and of the total file should be validated + after the file has been completely written + such that len(header) + len(data chunk) + len(footer) = total size + + raises ValueError if the header is invalid + """ + + import cStringIO + from .bitstream import BitstreamReader + + header_size = len(header) + wave_file = BitstreamReader(cStringIO.StringIO(header), 1) + try: + #ensure header starts with RIFF<size>WAVE chunk + (riff, remaining_size, wave) = wave_file.parse("4b 32u 4b") + if (riff != "RIFF"): + from .text import ERR_WAV_NOT_WAVE + raise ValueError(ERR_WAV_NOT_WAVE) + elif (wave != "WAVE"): + from .text import ERR_WAV_INVALID_WAVE + raise ValueError(ERR_WAV_INVALID_WAVE) + else: + total_size = remaining_size + 8 + header_size -= 12 + + fmt_found = False + + while (header_size > 0): + #ensure each chunk header is valid + (chunk_id, chunk_size) = wave_file.parse("4b 32u") + if (not frozenset(chunk_id).issubset(WaveAudio.PRINTABLE_ASCII)): + from .text import ERR_WAV_INVALID_CHUNK + raise ValueError(ERR_WAV_INVALID_CHUNK) + else: + header_size -= 8 + + if (chunk_id == "fmt "): + if (not fmt_found): + #skip fmt chunk data when found + fmt_found = True + if (chunk_size % 2): + wave_file.skip_bytes(chunk_size + 1) + header_size -= (chunk_size + 1) + else: + wave_file.skip_bytes(chunk_size) + header_size -= chunk_size + else: + #ensure only one fmt chunk is found + from .text import ERR_WAV_MULTIPLE_FMT + raise ValueError(ERR_WAV_MULTIPLE_FMT) + elif (chunk_id == "data"): + if (not fmt_found): + #ensure at least one fmt chunk is found + from .text import ERR_WAV_PREMATURE_DATA + raise ValueError(ERR_WAV_PREMATURE_DATA) + elif (header_size > 0): + #ensure no data remains after data chunk header + from .text import ERR_WAV_HEADER_EXTRA_DATA + raise ValueError(ERR_WAV_HEADER_EXTRA_DATA % + (header_size)) + else: + return (total_size, chunk_size) + else: + #skip the full contents of non-audio chunks + if (chunk_size % 2): + wave_file.skip_bytes(chunk_size + 1) + header_size -= (chunk_size + 1) + else: + wave_file.skip_bytes(chunk_size) + header_size -= chunk_size + else: + #header parsed with no data chunks found + from .text import ERR_WAV_NO_DATA_CHUNK + raise ValueError(ERR_WAV_NO_DATA_CHUNK) + except IOError: + from .text import ERR_WAV_HEADER_IOERROR + raise ValueError(ERR_WAV_HEADER_IOERROR) + + +def validate_footer(footer, data_bytes_written): + """given a footer string as returned by wave_header_footer() + and PCM stream parameters, returns True if the footer is valid + + raises ValueError if the footer is invalid""" + + import cStringIO + from .bitstream import BitstreamReader + + total_size = len(footer) + wave_file = BitstreamReader(cStringIO.StringIO(footer), 1) + try: + #ensure footer is padded properly if necessary + #based on size of data bytes written + if (data_bytes_written % 2): + wave_file.skip_bytes(1) + total_size -= 1 + + while (total_size > 0): + (chunk_id, chunk_size) = wave_file.parse("4b 32u") + if (not frozenset(chunk_id).issubset(WaveAudio.PRINTABLE_ASCII)): + from .text import ERR_WAV_INVALID_CHUNK + raise ValueError(ERR_WAV_INVALID_CHUNK) + else: + total_size -= 8 + + if (chunk_id == "fmt "): + #ensure no fmt chunks are found + from .text import ERR_WAV_MULTIPLE_FMT + raise ValueError(ERR_WAV_MULTIPLE_FMT) + elif (chunk_id == "data"): + #ensure no data chunks are found + from .text import ERR_WAV_MULTIPLE_DATA + raise ValueError(ERR_WAV_MULTIPLE_DATA) + else: + #skip the full contents of non-audio chunks + if (chunk_size % 2): + wave_file.skip_bytes(chunk_size + 1) + total_size -= (chunk_size + 1) + else: + wave_file.skip_bytes(chunk_size) + total_size -= chunk_size + else: + return True + except IOError: + from .text import ERR_WAV_FOOTER_IOERROR + raise ValueError(ERR_WAV_FOOTER_IOERROR) + + +def parse_fmt(fmt): + """given a fmt block BitstreamReader (without the 8 byte header) + returns (channels, sample_rate, bits_per_sample, channel_mask) + where channel_mask is a ChannelMask object and the rest are ints + may raise ValueError if the fmt chunk is invalid + or IOError if an error occurs parsing the chunk""" + + from . import ChannelMask + + (compression, + channels, + sample_rate, + bytes_per_second, + block_align, + bits_per_sample) = fmt.parse("16u 16u 32u 32u 16u 16u") + + if (compression == 1): + #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 + if (channels == 1): + channel_mask = ChannelMask.from_fields( + front_center=True) + elif (channels == 2): + channel_mask = ChannelMask.from_fields( + front_left=True, front_right=True) + elif (channels == 3): + channel_mask = ChannelMask.from_fields( + front_left=True, front_right=True, + front_center=True) + elif (channels == 4): + channel_mask = ChannelMask.from_fields( + front_left=True, front_right=True, + back_left=True, back_right=True) + elif (channels == 5): + channel_mask = ChannelMask.from_fields( + front_left=True, front_right=True, + back_left=True, back_right=True, + front_center=True) + elif (channels == 6): + channel_mask = ChannelMask.from_fields( + front_left=True, front_right=True, + back_left=True, back_right=True, + front_center=True, low_frequency=True) + else: + channel_mask = ChannelMask(0) + + return (channels, sample_rate, bits_per_sample, channel_mask) + elif (compression == 0xFFFE): + (cb_size, + valid_bits_per_sample, + channel_mask, + sub_format) = fmt.parse("16u 16u 32u 16b") + if (sub_format != + ('\x01\x00\x00\x00\x00\x00\x10\x00' + + '\x80\x00\x00\xaa\x00\x38\x9b\x71')): + #FIXME + raise ValueError("invalid WAVE sub-format") + else: + channel_mask = ChannelMask(channel_mask) + + return (channels, sample_rate, bits_per_sample, channel_mask) + else: + #FIXME + raise ValueError("unsupported WAVE compression") + + +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 + + from . import __capped_stream_reader__ + from .bitstream import BitstreamReader + + #build a capped reader for the data chunk + wave_reader = BitstreamReader(wave_file, 1) + try: + (riff, wave) = wave_reader.parse("4b 32p 4b") + if (riff != 'RIFF'): + from .text import ERR_WAV_NOT_WAVE + raise InvalidWave(ERR_WAV_NOT_WAVE) + elif (wave != 'WAVE'): + from .text import ERR_WAV_INVALID_WAVE + raise InvalidWave(ERR_WAV_INVALID_WAVE) + + while (True): + (chunk_id, chunk_size) = wave_reader.parse("4b 32u") + if (chunk_id == 'data'): + self.wave = __capped_stream_reader__(self.file, chunk_size) + self.data_chunk_length = chunk_size + break + else: + wave_reader.skip_bytes(chunk_size) + if (chunk_size % 2): + wave_reader.skip(8) + + except IOError: + from .text import ERR_WAV_NO_DATA_CHUNK + raise InvalidWave(ERR_WAV_NO_DATA_CHUNK) + + def read(self, pcm_frames): + """try to read a pcm.FrameList with the given number of PCM frames""" + + #align bytes downward if an odd number is read in + bytes_per_frame = self.channels * (self.bits_per_sample / 8) + requested_frames = max(1, pcm_frames) + pcm_data = self.wave.read(requested_frames * bytes_per_frame) + if ((len(pcm_data) == 0) and (self.data_chunk_length > 0)): + from .text import ERR_WAV_TRUNCATED_DATA_CHUNK + raise IOError(ERR_WAV_TRUNCATED_DATA_CHUNK) + else: + self.data_chunk_length -= len(pcm_data) + + try: + return FrameList(pcm_data, + self.channels, + self.bits_per_sample, + False, + self.bits_per_sample != 8) + except ValueError: + from .text import ERR_WAV_TRUNCATED_DATA_CHUNK + raise IOError(ERR_WAV_TRUNCATED_DATA_CHUNK) + + def close(self): + """closes the stream for reading + + any subprocess is waited for also so for proper cleanup""" + + self.file.close() + if (self.process is not None): + if (self.process.wait() != 0): + from . import DecodingError + + 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 + + +class WaveAudio(WaveContainer): + """a waveform audio file""" + + SUFFIX = "wav" + NAME = SUFFIX + DESCRIPTION = u"Waveform Audio File Format" + + PRINTABLE_ASCII = frozenset([chr(i) for i in xrange(0x20, 0x7E + 1)]) + + def __init__(self, filename): + """filename is a plain string""" + + from . import ChannelMask + + AudioFile.__init__(self, filename) + + self.__channels__ = 0 + self.__sample_rate__ = 0 + self.__bits_per_sample__ = 0 + self.__data_size__ = 0 + self.__channel_mask__ = ChannelMask(0) + + from .bitstream import BitstreamReader + + fmt_read = data_read = False + + try: + for chunk in self.chunks(): + if (chunk.id == "fmt "): + try: + (self.__channels__, + self.__sample_rate__, + self.__bits_per_sample__, + self.__channel_mask__) = parse_fmt( + BitstreamReader(chunk.data(), 1)) + fmt_read = True + if (fmt_read and data_read): + break + except IOError: + continue + except ValueError, err: + raise InvalidWave(str(err)) + elif (chunk.id == "data"): + self.__data_size__ = chunk.size() + data_read = True + if (fmt_read and data_read): + break + except IOError: + #FIXME + raise InvalidWave("I/O error reading wave") + + def lossless(self): + """returns True""" + + return True + + def has_foreign_wave_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([c.id for c in self.chunks()]) + + 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""" + + from . import FRAMELIST_SIZE + from . import EncodingError + from . import DecodingError + from .bitstream import BitstreamWriter, format_size + + try: + f = file(filename, "wb") + wave = BitstreamWriter(f, 1) + except IOError, err: + raise EncodingError(str(err)) + + try: + total_size = 0 + data_size = 0 + + avg_bytes_per_second = (pcmreader.sample_rate * + pcmreader.channels * + (pcmreader.bits_per_sample / 8)) + block_align = (pcmreader.channels * + (pcmreader.bits_per_sample / 8)) + + #build a regular or extended fmt chunk + #based on the reader's attributes + if (((pcmreader.channels <= 2) and + (pcmreader.bits_per_sample <= 16))): + fmt = "16u 16u 32u 32u 16u 16u" + fmt_fields = (1, # compression code + pcmreader.channels, + pcmreader.sample_rate, + avg_bytes_per_second, + block_align, + pcmreader.bits_per_sample) + else: + if (pcmreader.channel_mask != 0): + channel_mask = pcmreader.channel_mask + else: + channel_mask = {1: 0x4, + 2: 0x3, + 3: 0x7, + 4: 0x33, + 5: 0x37, + 6: 0x3F}.get(pcmreader.channels, 0) + + fmt = "16u 16u 32u 32u 16u 16u" + "16u 16u 32u 16b" + fmt_fields = (0xFFFE, # compression code + pcmreader.channels, + pcmreader.sample_rate, + avg_bytes_per_second, + block_align, + pcmreader.bits_per_sample, + 22, # CB size + pcmreader.bits_per_sample, + channel_mask, + '\x01\x00\x00\x00\x00\x00\x10\x00' + + '\x80\x00\x00\xaa\x00\x38\x9b\x71' # sub format + ) + + #write out the basic headers first + #we'll be back later to clean up the sizes + wave.build("4b 32u 4b", ("RIFF", total_size, "WAVE")) + total_size += 4 + + wave.build("4b 32u", ('fmt ', format_size(fmt) / 8)) + total_size += format_size("4b 32u") / 8 + + wave.build(fmt, fmt_fields) + total_size += format_size(fmt) / 8 + + wave.build("4b 32u", ('data', data_size)) + total_size += format_size("4b 32u") / 8 + + #dump pcmreader's FrameLists into the file as little-endian + try: + framelist = pcmreader.read(FRAMELIST_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) + total_size += len(bytes) + data_size += len(bytes) + + framelist = pcmreader.read(FRAMELIST_SIZE) + except (IOError, ValueError), err: + cls.__unlink__(filename) + raise EncodingError(str(err)) + except Exception, err: + cls.__unlink__(filename) + raise err + + #handle odd-sized data chunks + if (data_size % 2): + wave.write(8, 0) + total_size += 1 + + #close the PCM reader and flush our output + try: + pcmreader.close() + except DecodingError, err: + cls.__unlink__(filename) + raise EncodingError(err.error_message) + f.flush() + + if (total_size < (2 ** 32)): + #go back to the beginning the rewrite the header + f.seek(0, 0) + wave.build("4b 32u 4b", ("RIFF", total_size, "WAVE")) + wave.build("4b 32u", ('fmt ', format_size(fmt) / 8)) + wave.build(fmt, fmt_fields) + wave.build("4b 32u", ('data', data_size)) + else: + import os + + os.unlink(filename) + #FIXME + raise EncodingError("PCM data too large for wave file") + + finally: + f.close() + + return WaveAudio(filename) + + def total_frames(self): + """returns the total PCM frames of the track as an integer""" + + return (self.__data_size__ / + (self.__bits_per_sample__ / 8) / + self.__channels__) + + def sample_rate(self): + """returns the rate of the track's audio as an integer number of Hz""" + + return self.__sample_rate__ + + 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.__bits_per_sample__ + + @classmethod + def can_add_replay_gain(cls, audiofiles): + """given a list of audiofiles, + returns True if this class can add ReplayGain to those files + returns False if not""" + + for audiofile in audiofiles: + if (not isinstance(audiofile, WaveAudio)): + return False + else: + return True + + @classmethod + def supports_replay_gain(cls): + """returns True if this class supports 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 . import transfer_data + from . import transfer_framelist_data + from . import open_files + from . import calculate_replay_gain + from .replaygain import ReplayGainReader + import tempfile + + wave_files = [track for track in open_files(filenames) if + isinstance(track, cls)] + + #then, apply those Gain and Peak values to our files + #rewriting the originals in the process + for (original_wave, + track_gain, + track_peak, + album_gain, + album_peak) in calculate_replay_gain(wave_files, progress): + temp_wav_file = tempfile.NamedTemporaryFile(suffix=".wav") + try: + (header, footer) = original_wave.wave_header_footer() + temp_wav_file.write(header) + replaygain_pcm = ReplayGainReader(original_wave.to_pcm(), + track_gain, track_peak) + transfer_framelist_data( + replaygain_pcm, + temp_wav_file.write, + signed=original_wave.bits_per_sample() > 8, + big_endian=False) + temp_wav_file.write(footer) + replaygain_pcm.close() + + 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 chunks(self): + """yields a set of RIFF_Chunk or RIFF_File_Chunk objects""" + + wave_file = file(self.filename, "rb") + try: + (riff, + total_size, + wave) = struct.unpack("<4sI4s", wave_file.read(12)) + except struct.error: + from .text import ERR_WAV_INVALID_WAVE + raise InvalidWave(ERR_WAV_INVALID_WAVE) + + if (riff != 'RIFF'): + from .text import ERR_WAV_NOT_WAVE + raise InvalidWave(ERR_WAV_NOT_WAVE) + elif (wave != 'WAVE'): + from .text import ERR_WAV_INVALID_WAVE + raise InvalidWave(ERR_WAV_INVALID_WAVE) + else: + total_size -= 4 + + while (total_size > 0): + #read the chunk header and ensure its validity + try: + (chunk_id, + chunk_size) = struct.unpack("<4sI", wave_file.read(8)) + except struct.error: + from .text import ERR_WAV_INVALID_WAVE + raise InvalidWave(ERR_WAV_INVALID_WAVE) + if (not frozenset(chunk_id).issubset(self.PRINTABLE_ASCII)): + from .text import ERR_WAV_INVALID_CHUNK + raise InvalidWave(ERR_WAV_INVALID_CHUNK) + else: + total_size -= 8 + + #yield RIFF_Chunk or RIFF_File_Chunk depending on chunk size + if (chunk_size >= 0x100000): + #if chunk is too large, yield a File_Chunk + yield RIFF_File_Chunk(chunk_id, + chunk_size, + file(self.filename, "rb"), + wave_file.tell()) + wave_file.seek(chunk_size, 1) + else: + #otherwise, yield a raw data Chunk + yield RIFF_Chunk(chunk_id, chunk_size, + wave_file.read(chunk_size)) + + if (chunk_size % 2): + if (len(wave_file.read(1)) < 1): + from .text import ERR_WAV_INVALID_CHUNK + raise InvalidWave(ERR_WAV_INVALID_CHUNK) + total_size -= (chunk_size + 1) + else: + total_size -= chunk_size + + @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 RIFF_Chunk-compatible objects + """ + + wave_file = file(filename, 'wb') + try: + total_size = 4 + + #write an unfinished header with a placeholder size + wave_file.write(struct.pack("<4sI4s", "RIFF", total_size, "WAVE")) + + #write the individual chunks + for chunk in chunk_iter: + total_size += chunk.write(wave_file) + + #once the chunks are done, go back and re-write the header + wave_file.seek(0, 0) + wave_file.write(struct.pack("<4sI4s", "RIFF", total_size, "WAVE")) + finally: + wave_file.close() + + def wave_header_footer(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.wave_header_footer() + >>> 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" + """ + + from .bitstream import BitstreamReader + from .bitstream import BitstreamRecorder + + head = BitstreamRecorder(1) + tail = BitstreamRecorder(1) + current_block = head + fmt_found = False + + wave_file = BitstreamReader(open(self.filename, 'rb'), 1) + try: + #transfer the 12-byte "RIFFsizeWAVE" header to head + (riff, size, wave) = wave_file.parse("4b 32u 4b") + if (riff != 'RIFF'): + from .text import ERR_WAV_NOT_WAVE + raise ValueError(ERR_WAV_NOT_WAVE) + elif (wave != 'WAVE'): + from .text import ERR_WAV_INVALID_WAVE + raise ValueError(ERR_WAV_INVALID_WAVE) + else: + current_block.build("4b 32u 4b", (riff, size, wave)) + total_size = size - 4 + + while (total_size > 0): + #transfer each chunk header + (chunk_id, chunk_size) = wave_file.parse("4b 32u") + if (not frozenset(chunk_id).issubset(self.PRINTABLE_ASCII)): + from .text import ERR_WAV_INVALID_CHUNK + raise ValueError(ERR_WAV_INVALID_CHUNK) + else: + current_block.build("4b 32u", (chunk_id, chunk_size)) + total_size -= 8 + + #and transfer the full content of non-audio chunks + if (chunk_id != "data"): + if (chunk_id == "fmt "): + if (not fmt_found): + fmt_found = True + else: + from .text import ERR_WAV_MULTIPLE_FMT + raise ValueError(ERR_WAV_MULTIPLE_FMT) + + if (chunk_size % 2): + current_block.write_bytes( + wave_file.read_bytes(chunk_size + 1)) + total_size -= (chunk_size + 1) + else: + current_block.write_bytes( + wave_file.read_bytes(chunk_size)) + total_size -= chunk_size + else: + wave_file.skip_bytes(chunk_size) + current_block = tail + + if (chunk_size % 2): + current_block.write_bytes(wave_file.read_bytes(1)) + total_size -= (chunk_size + 1) + else: + total_size -= chunk_size + + if (fmt_found): + return (head.data(), tail.data()) + else: + from .text import ERR_WAV_NO_FMT_CHUNK + return ValueError(ERR_WAV_NO_FMT_CHUNK) + finally: + wave_file.close() + + @classmethod + def from_wave(cls, filename, header, pcmreader, footer, compression=None): + """encodes a new file from wave data + + takes a filename string, header string, + PCMReader object, footer string + 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 + + header + pcm data + footer should always result + in the original wave file being restored + without need for any padding bytes + + may raise EncodingError if some problem occurs when + encoding the input file""" + + from . import (DecodingError, EncodingError, FRAMELIST_SIZE) + from struct import unpack + + #ensure header validates correctly + try: + (total_size, data_size) = validate_header(header) + except ValueError, err: + raise EncodingError(str(err)) + + try: + #write header to output file + f = open(filename, "wb") + f.write(header) + + #write PCM data to output file + data_bytes_written = 0 + signed = (pcmreader.bits_per_sample > 8) + s = pcmreader.read(FRAMELIST_SIZE).to_bytes(False, signed) + while (len(s) > 0): + data_bytes_written += len(s) + f.write(s) + s = pcmreader.read(FRAMELIST_SIZE).to_bytes(False, signed) + + #ensure output data size matches the "data" chunk's size + if (data_size != data_bytes_written): + cls.__unlink__(filename) + from .text import ERR_WAV_TRUNCATED_DATA_CHUNK + raise EncodingError(ERR_WAV_TRUNCATED_DATA_CHUNK) + + #ensure footer validates correctly + try: + validate_footer(footer, data_bytes_written) + #before writing it to disk + f.write(footer) + except ValueError, err: + cls.__unlink__(filename) + raise EncodingError(str(err)) + + f.close() + + #ensure total size is correct + if ((len(header) + data_size + len(footer)) != total_size): + cls.__unlink__(filename) + from .text import ERR_WAV_INVALID_SIZE + raise EncodingError(ERR_WAV_INVALID_SIZE) + + return cls(filename) + except IOError, err: + cls.__unlink__(filename) + raise EncodingError(str(err)) + except DecodingError, err: + cls.__unlink__(filename) + raise EncodingError(err.error_message) + + 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""" + + from . import CounterPCMReader + from . import transfer_framelist_data + from . import to_pcm_progress + + try: + (header, footer) = self.wave_header_footer() + except IOError, err: + raise InvalidWave(unicode(err)) + except ValueError, err: + raise InvalidWave(unicode(err)) + + #ensure header is valid + try: + (total_size, data_size) = validate_header(header) + except ValueError, err: + raise InvalidWave(unicode(err)) + + #ensure "data" chunk has all its data + counter = CounterPCMReader(to_pcm_progress(self, progress)) + try: + transfer_framelist_data(counter, lambda f: f) + except IOError: + from .text import ERR_WAV_TRUNCATED_DATA_CHUNK + raise InvalidWave(ERR_WAV_TRUNCATED_DATA_CHUNK) + + data_bytes_written = counter.bytes_written() + + #ensure output data size matches the "data" chunk's size + if (data_size != data_bytes_written): + from .text import ERR_WAV_TRUNCATED_DATA_CHUNK + raise InvalidWave(ERR_WAV_TRUNCATED_DATA_CHUNK) + + #ensure footer validates correctly + try: + validate_footer(footer, data_bytes_written) + except ValueError, err: + from .text import ERR_WAV_INVALID_SIZE + raise InvalidWave(ERR_WAV_INVALID_SIZE) + + #ensure total size is correct + if ((len(header) + data_size + len(footer)) != total_size): + from .text import ERR_WAV_INVALID_SIZE + raise InvalidWave(ERR_WAV_INVALID_SIZE) + + return True + + def clean(self, fixes_performed, output_filename=None): + """cleans the file of known data and metadata problems + + fixes_performed is a list-like object which is appended + with Unicode strings of fixed problems + + output_filename is an optional filename of the fixed file + if present, a new AudioFile is returned + otherwise, only a dry-run is performed and no new file is written + + raises IOError if unable to write the file or its metadata + raises ValueError if the file has errors of some sort + """ + + chunk_queue = [] + pending_data = None + + for chunk in self.chunks(): + if (chunk.id == "fmt "): + if ("fmt " in [c.id for c in chunk_queue]): + from .text import CLEAN_WAV_MULTIPLE_FMT_CHUNKS + fixes_performed.append(CLEAN_WAV_MULTIPLE_FMT_CHUNKS) + else: + chunk_queue.append(chunk) + if (pending_data is not None): + chunk_queue.append(pending_data) + pending_data = None + elif (chunk.id == "data"): + if ("fmt " not in [c.id for c in chunk_queue]): + from .text import CLEAN_WAV_REORDERED_DATA_CHUNK + fixes_performed.append(CLEAN_WAV_REORDERED_DATA_CHUNK) + pending_data = chunk + elif ("data" in [c.id for c in chunk_queue]): + from .text import CLEAN_WAV_MULTIPLE_DATA_CHUNKS + fixes_performed.append(CLEAN_WAV_MULTIPLE_DATA_CHUNKS) + else: + chunk_queue.append(chunk) + else: + chunk_queue.append(chunk) + + if (output_filename is not None): + WaveAudio.wave_from_chunks(output_filename, chunk_queue) + return WaveAudio(output_filename)
View file
audiotools-2.19.tar.gz/audiotools/wavpack.py
Added
@@ -0,0 +1,652 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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 . import WaveContainer, InvalidFile +from .ape import ApeTaggedAudio + + +class InvalidWavPack(InvalidFile): + pass + + +def __riff_chunk_ids__(data_size, data): + (riff, size, wave) = data.parse("4b 32u 4b") + if (riff != "RIFF"): + return + elif (wave != "WAVE"): + return + else: + data_size -= 12 + + while (data_size > 0): + (chunk_id, chunk_size) = data.parse("4b 32u") + data_size -= 8 + if ((chunk_size % 2) == 1): + chunk_size += 1 + yield chunk_id + if (chunk_id != 'data'): + data.skip_bytes(chunk_size) + data_size -= chunk_size + + +####################### +#WavPack +####################### + + +class WavPackAudio(ApeTaggedAudio, WaveContainer): + """a WavPack audio file""" + + from .text import (COMP_WAVPACK_VERYFAST, + COMP_WAVPACK_VERYHIGH) + + SUFFIX = "wv" + NAME = SUFFIX + DESCRIPTION = u"WavPack" + DEFAULT_COMPRESSION = "standard" + COMPRESSION_MODES = ("veryfast", "fast", "standard", "high", "veryhigh") + COMPRESSION_DESCRIPTIONS = {"veryfast": COMP_WAVPACK_VERYFAST, + "veryhigh": COMP_WAVPACK_VERYHIGH} + + BITS_PER_SAMPLE = (8, 16, 24, 32) + SAMPLING_RATE = (6000, 8000, 9600, 11025, + 12000, 16000, 22050, 24000, + 32000, 44100, 48000, 64000, + 88200, 96000, 192000, 0) + + __options__ = {"veryfast": {"block_size": 44100, + "joint_stereo": True, + "false_stereo": True, + "wasted_bits": True, + "correlation_passes": 1}, + "fast": {"block_size": 44100, + "joint_stereo": True, + "false_stereo": True, + "wasted_bits": True, + "correlation_passes": 2}, + "standard": {"block_size": 44100, + "joint_stereo": True, + "false_stereo": True, + "wasted_bits": True, + "correlation_passes": 5}, + "high": {"block_size": 44100, + "joint_stereo": True, + "false_stereo": True, + "wasted_bits": True, + "correlation_passes": 10}, + "veryhigh": {"block_size": 44100, + "joint_stereo": True, + "false_stereo": True, + "wasted_bits": True, + "correlation_passes": 16}} + + def __init__(self, filename): + """filename is a plain string""" + + self.filename = filename + self.__samplerate__ = 0 + self.__channels__ = 0 + self.__bitspersample__ = 0 + self.__total_frames__ = 0 + + try: + self.__read_info__() + except IOError, msg: + raise InvalidWavPack(str(msg)) + + def lossless(self): + """returns True""" + + return True + + def channel_mask(self): + """returns a ChannelMask object of this track's channel layout""" + + return self.__channel_mask__ + + def get_metadata(self): + """returns a MetaData object, or None + + raises IOError if unable to read the file""" + + metadata = ApeTaggedAudio.get_metadata(self) + if (metadata is not None): + metadata.frame_count = self.total_frames() + return metadata + + def has_foreign_wave_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""" + + for (sub_header, nondecoder, data_size, data) in self.sub_blocks(): + if ((sub_header == 1) and nondecoder): + if (set(__riff_chunk_ids__(data_size, + data)) != set(['fmt ', 'data'])): + return True + elif ((sub_header == 2) and nondecoder): + return True + else: + return False + + def wave_header_footer(self): + """returns (header, footer) tuple of strings + containing all data before and after the PCM stream + + may raise ValueError if there's a problem with + the header or footer data + may raise IOError if there's a problem reading + header or footer data from the file + """ + + head = None + tail = None + + for (sub_block_id, nondecoder, data_size, data) in self.sub_blocks(): + if ((sub_block_id == 1) and nondecoder): + head = data.read_bytes(data_size) + elif ((sub_block_id == 2) and nondecoder): + tail = data.read_bytes(data_size) + + if (head is not None): + return (head, tail if tail is not None else "") + else: + raise ValueError("no wave header found") + + @classmethod + def from_wave(cls, filename, header, pcmreader, footer, compression=None, + encoding_function=None): + """encodes a new file from wave data + + takes a filename string, header string, + PCMReader object, footer string + 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 + + header + pcm data + footer should always result + in the original wave file being restored + without need for any padding bytes + + may raise EncodingError if some problem occurs when + encoding the input file""" + + from .encoders import encode_wavpack + from . import BufferedPCMReader + from . import CounterPCMReader + from .wav import (validate_header, validate_footer) + from . import EncodingError + from . import __default_quality__ + + if (((compression is None) or + (compression not in cls.COMPRESSION_MODES))): + compression = __default_quality__(cls.NAME) + + #ensure header is valid + try: + (total_size, data_size) = validate_header(header) + except ValueError, err: + raise EncodingError(str(err)) + + counter = CounterPCMReader(pcmreader) + + try: + (encode_wavpack if encoding_function is None + else encoding_function)(filename, + BufferedPCMReader(counter), + wave_header=header, + wave_footer=footer, + **cls.__options__[compression]) + + data_bytes_written = counter.bytes_written() + + #ensure output data size matches the "data" chunk's size + if (data_size != data_bytes_written): + from .text import ERR_WAV_TRUNCATED_DATA_CHUNK + raise EncodingError(ERR_WAV_TRUNCATED_DATA_CHUNK) + + #ensure footer validates correctly + try: + validate_footer(footer, data_bytes_written) + except ValueError, err: + raise EncodingError(str(err)) + + #ensure total size is correct + if ((len(header) + data_size + len(footer)) != total_size): + from .text import ERR_WAV_INVALID_SIZE + raise EncodingError(ERR_WAV_INVALID_SIZE) + + return cls(filename) + except (ValueError, IOError), msg: + cls.__unlink__(filename) + raise EncodingError(str(msg)) + except Exception, err: + cls.__unlink__(filename) + raise err + + def blocks(self, reader=None): + """yields (length, reader) tuples of WavPack frames + + length is the total length of all the substreams + reader is a BitstreamReader which can be parsed + """ + + def blocks_iter(reader): + try: + while (True): + (wvpk, block_size) = reader.parse("4b 32u 192p") + if (wvpk == 'wvpk'): + yield (block_size - 24, + reader.substream(block_size - 24)) + else: + return + except IOError: + return + + if (reader is None): + from .bitstream import BitstreamReader + + reader = BitstreamReader(file(self.filename), 1) + try: + for block in blocks_iter(reader): + yield block + finally: + reader.close() + else: + for block in blocks_iter(reader): + yield block + + def sub_blocks(self, reader=None): + """yields (function, nondecoder, data_size, data) tuples + + function is an integer + nondecoder is a boolean indicating non-decoder data + data is a BitstreamReader which can be parsed + """ + + for (block_size, block_data) in self.blocks(reader): + while (block_size > 0): + (metadata_function, + nondecoder_data, + actual_size_1_less, + large_block) = block_data.parse("5u 1u 1u 1u") + + if (large_block): + sub_block_size = block_data.read(24) + block_size -= 4 + else: + sub_block_size = block_data.read(8) + block_size -= 2 + + if (actual_size_1_less): + yield (metadata_function, + nondecoder_data, + sub_block_size * 2 - 1, + block_data.substream(sub_block_size * 2 - 1)) + block_data.skip(8) + else: + yield (metadata_function, + nondecoder_data, + sub_block_size * 2, + block_data.substream(sub_block_size * 2)) + + block_size -= sub_block_size * 2 + + def __read_info__(self): + from .bitstream import BitstreamReader + from . import ChannelMask + + reader = BitstreamReader(file(self.filename, "rb"), 1) + reader.mark() + try: + (block_id, + total_samples, + bits_per_sample, + mono_output, + initial_block, + final_block, + sample_rate) = reader.parse( + "4b 64p 32u 64p 2u 1u 8p 1u 1u 5p 5p 4u 37p") + + if (block_id != 'wvpk'): + from .text import ERR_WAVPACK_INVALID_HEADER + raise InvalidWavPack(ERR_WAVPACK_INVALID_HEADER) + + if (sample_rate != 0xF): + self.__samplerate__ = WavPackAudio.SAMPLING_RATE[sample_rate] + else: + #if unknown, pull from SAMPLE_RATE sub-block + for (block_id, + nondecoder, + data_size, + data) in self.sub_blocks(reader): + if ((block_id == 0x7) and nondecoder): + self.__samplerate__ = data.read(data_size * 8) + break + else: + #no SAMPLE RATE sub-block found + #so pull info from FMT chunk + reader.rewind() + (self.__samplerate__,) = self.fmt_chunk(reader).parse( + "32p 32u") + + self.__bitspersample__ = [8, 16, 24, 32][bits_per_sample] + self.__total_frames__ = total_samples + + if (initial_block and final_block): + if (mono_output): + self.__channels__ = 1 + self.__channel_mask__ = ChannelMask(0x4) + else: + self.__channels__ = 2 + self.__channel_mask__ = ChannelMask(0x3) + else: + #if not mono or stereo, pull from CHANNEL INFO sub-block + reader.rewind() + for (block_id, + nondecoder, + data_size, + data) in self.sub_blocks(reader): + if ((block_id == 0xD) and not nondecoder): + self.__channels__ = data.read(8) + self.__channel_mask__ = ChannelMask( + data.read((data_size - 1) * 8)) + break + else: + #no CHANNEL INFO sub-block found + #so pull info from FMT chunk + reader.rewind() + fmt = self.fmt_chunk(reader) + compression_code = fmt.read(16) + self.__channels__ = fmt.read(16) + if (compression_code == 1): + #this is theoretically possible + #with very old .wav files, + #but shouldn't happen in practice + self.__channel_mask__ = \ + {1: ChannelMask.from_fields(front_center=True), + 2: ChannelMask.from_fields(front_left=True, + front_right=True), + 3: ChannelMask.from_fields(front_left=True, + front_right=True, + front_center=True), + 4: ChannelMask.from_fields(front_left=True, + front_right=True, + back_left=True, + back_right=True), + 5: ChannelMask.from_fields(front_left=True, + front_right=True, + back_left=True, + back_right=True, + front_center=True), + 6: ChannelMask.from_fields(front_left=True, + front_right=True, + back_left=True, + back_right=True, + front_center=True, + low_frequency=True) + }.get(self.__channels__, ChannelMask(0)) + elif (compression_code == 0xFFFE): + fmt.skip(128) + mask = fmt.read(32) + self.__channel_mask__ = ChannelMask(mask) + else: + from .text import ERR_WAVPACK_UNSUPPORTED_FMT + raise InvalidWavPack(ERR_WAVPACK_UNSUPPORTED_FMT) + + finally: + reader.unmark() + reader.close() + + def bits_per_sample(self): + """returns an integer number of bits-per-sample this track contains""" + + return self.__bitspersample__ + + def channels(self): + """returns an integer number of channels this track contains""" + + return self.__channels__ + + def total_frames(self): + """returns the total PCM frames of the track as an integer""" + + return self.__total_frames__ + + def sample_rate(self): + """returns the rate of the track's audio as an integer number of Hz""" + + return self.__samplerate__ + + @classmethod + def from_pcm(cls, filename, pcmreader, compression=None, + encoding_function=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 WavPackAudio object""" + + from .encoders import encode_wavpack + from . import BufferedPCMReader + from . import EncodingError + from . import __default_quality__ + + if (((compression is None) or + (compression not in cls.COMPRESSION_MODES))): + compression = __default_quality__(cls.NAME) + + try: + (encode_wavpack if encoding_function is None + else encoding_function)(filename, + BufferedPCMReader(pcmreader), + **cls.__options__[compression]) + + return cls(filename) + except (ValueError, IOError), msg: + cls.__unlink__(filename) + raise EncodingError(str(msg)) + except Exception, err: + cls.__unlink__(filename) + raise err + + def to_pcm(self): + """returns a PCMReader object containing the track's PCM data""" + + from . import decoders + from . import PCMReaderError + + try: + return decoders.WavPackDecoder(self.filename) + except (IOError, ValueError), msg: + return PCMReaderError(error_message=str(msg), + sample_rate=self.__samplerate__, + channels=self.__channels__, + channel_mask=int(self.channel_mask()), + bits_per_sample=self.__bitspersample__) + + def fmt_chunk(self, reader=None): + """returns the 'fmt' chunk as a BitstreamReader""" + + for (block_id, + nondecoder, + data_size, + data) in self.sub_blocks(reader): + if ((block_id == 1) and nondecoder): + (riff, wave) = data.parse("4b 32p 4b") + if ((riff != 'RIFF') or (wave != 'WAVE')): + from .text import ERR_WAVPACK_INVALID_FMT + raise InvalidWavPack(ERR_WAVPACK_INVALID_FMT) + else: + while (True): + (chunk_id, chunk_size) = data.parse("4b 32u") + if (chunk_id == 'fmt '): + return data.substream(chunk_size) + elif (chunk_id == 'data'): + from .text import ERR_WAVPACK_INVALID_FMT + raise InvalidWavPack(ERR_WAVPACK_INVALID_FMT) + else: + data.skip_bytes(chunk_size) + else: + from .text import ERR_WAVPACK_NO_FMT + raise InvalidWavPack(ERR_WAVPACK_NO_FMT) + + @classmethod + def can_add_replay_gain(cls, audiofiles): + """given a list of audiofiles, + returns True if this class can add ReplayGain to those files + returns False if not""" + + for audiofile in audiofiles: + if (not isinstance(audiofile, WavPackAudio)): + return False + else: + return True + + @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 . import open_files + from . import calculate_replay_gain + from .ape import ApeTagItem + from .ape import ApeTag + + tracks = [track for track in open_files(filenames) if + isinstance(track, cls)] + + if (len(tracks) > 0): + for (track, + track_gain, + track_peak, + album_gain, + album_peak) in calculate_replay_gain(tracks, progress): + metadata = track.get_metadata() + if (metadata is None): + metadata = ApeTag([]) + + metadata["replaygain_track_gain"] = ApeTagItem.string( + "replaygain_track_gain", + u"%+1.2f dB" % (track_gain)) + metadata["replaygain_track_peak"] = ApeTagItem.string( + "replaygain_track_peak", + u"%1.6f" % (track_peak)) + metadata["replaygain_album_gain"] = ApeTagItem.string( + "replaygain_album_gain", + u"%+1.2f dB" % (album_gain)) + metadata["replaygain_album_peak"] = ApeTagItem.string( + "replaygain_album_peak", + u"%1.6f" % (album_peak)) + + track.update_metadata(metadata) + + @classmethod + def supports_replay_gain(cls): + """returns True if this class supports ReplayGain""" + + return True + + @classmethod + def lossless_replay_gain(cls): + """returns True""" + + return True + + def replay_gain(self): + """returns a ReplayGain object of our ReplayGain values + + returns None if we have no values""" + + from . import ReplayGain + + metadata = self.get_metadata() + if (metadata is None): + return None + + if (set(['replaygain_track_gain', 'replaygain_track_peak', + 'replaygain_album_gain', 'replaygain_album_peak']).issubset( + metadata.keys())): # we have ReplayGain data + try: + return ReplayGain( + unicode(metadata['replaygain_track_gain'])[0:-len(" dB")], + unicode(metadata['replaygain_track_peak']), + unicode(metadata['replaygain_album_gain'])[0:-len(" dB")], + unicode(metadata['replaygain_album_peak'])) + except ValueError: + return None + else: + return None + + def get_cuesheet(self): + """returns the embedded Cuesheet-compatible object, or None + + raises IOError if a problem occurs when reading the file""" + + import cue + + metadata = self.get_metadata() + + if ((metadata is not None) and ('Cuesheet' in metadata.keys())): + try: + return cue.parse( + cue.tokens( + unicode(metadata['Cuesheet']).encode('utf-8', + 'replace'))) + except cue.CueException: + #unlike FLAC, just because a cuesheet is embedded + #does not mean it is compliant + return None + else: + return None + + def set_cuesheet(self, cuesheet): + """imports cuesheet data from a Cuesheet-compatible object + + this are objects with catalog(), ISRCs(), indexes(), and pcm_lengths() + methods. Raises IOError if an error occurs setting the cuesheet""" + + import os.path + import cue + from . import MetaData + from .ape import ApeTag + + if (cuesheet is None): + return + + metadata = self.get_metadata() + if (metadata is None): + metadata = ApeTag.converted(MetaData()) + + metadata['Cuesheet'] = ApeTag.ITEM.string( + 'Cuesheet', + cue.Cuesheet.file( + cuesheet, + os.path.basename(self.filename)).decode('ascii', 'replace')) + self.update_metadata(metadata)
View file
audiotools-2.18.tar.gz/cd2track -> audiotools-2.19.tar.gz/cd2track
Changed
@@ -22,22 +22,29 @@ import os import audiotools import audiotools.ui -import gettext +import termios +import audiotools.text as _ + + +def limit(l, indexes): + """given a list and set of indexes (starting from 1) + returns a new list with only those items""" + + return [item for (index, item) in enumerate(l) + if ((index + 1) in indexes)] -gettext.install("audiotools", unicode=True) if (__name__ == '__main__'): parser = audiotools.OptionParser( - usage=_(u"%prog [options] [track #] [track #] ..."), + usage=_.USAGE_CD2TRACK, version="Python Audio Tools %s" % (audiotools.VERSION)) parser.add_option( - '-V', '--verbose', - action='store', - dest='verbosity', - choices=audiotools.VERBOSITY_LEVELS, - default=audiotools.DEFAULT_VERBOSITY, - help=_(u'the verbosity level to execute at')) + '-I', '--interactive', + action='store_true', + default=False, + dest='interactive', + help=_.OPT_INTERACTIVE_OPTIONS) parser.add_option( '-c', '--cdrom', action='store', @@ -48,46 +55,47 @@ '-s', '--speed', action='store', type='int', dest='speed') - # parser.add_option( - # '--accuraterip', - # action='store_true', - # default=False, - # dest='accuraterip', - # help=_(u'attempt to fetch AccurateRip information')) + parser.add_option( + '-V', '--verbose', + action='store', + dest='verbosity', + choices=audiotools.VERBOSITY_LEVELS, + default=audiotools.DEFAULT_VERBOSITY, + help=_.OPT_VERBOSE) - conversion = audiotools.OptionGroup(parser, _(u"Extraction Options")) + conversion = audiotools.OptionGroup(parser, _.OPT_CAT_EXTRACTION) conversion.add_option( '-t', '--type', action='store', dest='type', - choices=audiotools.TYPE_MAP.keys(), + choices=sorted(audiotools.TYPE_MAP.keys() + ['help']), default=audiotools.DEFAULT_TYPE, - help=_(u'the type of audio track to create')) + help=_.OPT_TYPE) conversion.add_option( '-q', '--quality', action='store', type='string', dest='quality', - help=_(u'the quality to store audio tracks at')) + help=_.OPT_QUALITY) conversion.add_option( "-d", "--dir", action='store', default='.', dest='dir', - help=_(u"the directory to store extracted audio tracks")) + help=_.OPT_DIR) conversion.add_option( '--format', action='store', type='string', - default=None, + default=audiotools.FILENAME_FORMAT, dest='format', - help=_(u'the format string for new filenames')) + help=_.OPT_FORMAT) parser.add_option_group(conversion) - lookup = audiotools.OptionGroup(parser, _(u"CD Lookup Options")) + lookup = audiotools.OptionGroup(parser, _.OPT_CAT_CD_LOOKUP) lookup.add_option( '--musicbrainz-server', action='store', @@ -103,7 +111,7 @@ '--no-musicbrainz', action='store_false', dest='use_musicbrainz', default=audiotools.MUSICBRAINZ_SERVICE, - help='do not query MusicBrainz for metadata') + help=_.OPT_NO_MUSICBRAINZ) lookup.add_option( '--freedb-server', action='store', @@ -119,24 +127,16 @@ '--no-freedb', action='store_false', dest='use_freedb', default=audiotools.FREEDB_SERVICE, - help='do not query FreeDB for metadata') - - lookup.add_option( - '-I', '--interactive', - action='store_true', - default=False, - dest='interactive', - help=_(u'edit metadata in interactive mode')) + help=_.OPT_NO_FREEDB) lookup.add_option( '-D', '--default', dest='use_default', action='store_true', default=False, - help=_(u'when multiple choices are available, ' + - u'select the first one automatically')) + help=_.OPT_DEFAULT) parser.add_option_group(lookup) - metadata = audiotools.OptionGroup(parser, _(u"Metadata Options")) + metadata = audiotools.OptionGroup(parser, _.OPT_CAT_METADATA) metadata.add_option( '--album-number', @@ -144,8 +144,7 @@ action='store', type='int', default=0, - help=_(u'the album number of this CD, ' + - u'if it is one of a series of albums')) + help=_.OPT_ALBUM_NUMBER) metadata.add_option( '--album-total', @@ -153,8 +152,7 @@ action='store', type='int', default=0, - help=_(u'the total albums of this CD\'s set, ' + - u'if it is one of a series of albums')) + help=_.OPT_ALBUM_TOTAL) #if adding ReplayGain is a lossless process #(i.e. added as tags rather than modifying track data) @@ -165,17 +163,18 @@ '--replay-gain', action='store_true', dest='add_replay_gain', - help=_(u'add ReplayGain metadata to newly created tracks')) + help=_.OPT_REPLAY_GAIN) metadata.add_option( '--no-replay-gain', action='store_false', dest='add_replay_gain', - help=_(u'do not add ReplayGain metadata in newly created tracks')) + help=_.OPT_NO_REPLAY_GAIN) parser.add_option_group(metadata) (options, args) = parser.parse_args() + msg = audiotools.Messenger("cd2track", options) #ensure interactive mode is available, if selected @@ -183,43 +182,23 @@ audiotools.ui.not_available_message(msg) sys.exit(1) - #get the AudioFile class we are converted to - AudioType = audiotools.TYPE_MAP[options.type] - - if (options.add_replay_gain is None): - options.add_replay_gain = AudioType.lossless_replay_gain() + #get the default AudioFile class we are converted to + if (options.type == 'help'): + audiotools.ui.show_available_formats(msg) + sys.exit(0) + else: + AudioType = audiotools.TYPE_MAP[options.type] #ensure the selected compression is compatible with that class if (options.quality == 'help'): - if (len(AudioType.COMPRESSION_MODES) > 1): - msg.info(_(u"Available compression types for %s:") % \ - (AudioType.NAME)) - for mode in AudioType.COMPRESSION_MODES: - msg.new_row() - if (mode == audiotools.__default_quality__(AudioType.NAME)): - msg.output_column(msg.ansi(mode.decode('ascii'), - [msg.BOLD, - msg.UNDERLINE]), True) - else: - msg.output_column(mode.decode('ascii'), True) - if (mode in AudioType.COMPRESSION_DESCRIPTIONS): - msg.output_column(u" : ") - else: - msg.output_column(u" ") - msg.output_column( - AudioType.COMPRESSION_DESCRIPTIONS.get(mode, u"")) - msg.info_rows() - else: - msg.error(_(u"Audio type %s has no compression modes") % \ - (AudioType.NAME)) + audiotools.ui.show_available_qualities(msg, AudioType) sys.exit(0) elif (options.quality is None): options.quality = audiotools.__default_quality__(AudioType.NAME) elif (options.quality not in AudioType.COMPRESSION_MODES): - msg.error(_(u"\"%(quality)s\" is not a supported " + - u"compression mode for type \"%(type)s\"") % \ - {"quality": options.quality, - "type": AudioType.NAME}) + msg.error(_.ERR_UNSUPPORTED_COMPRESSION_MODE % + {"quality": options.quality, + "type": AudioType.NAME}) sys.exit(1) quality = options.quality @@ -228,12 +207,14 @@ try: cdda = audiotools.CDDA(options.cdrom, options.speed) except IOError, err: - msg.error(unicode(err) + _(u". Is that an audio cd ?")) + msg.error(unicode(err) + _.ERR_INVALID_CDDA) sys.exit(-1) if (len(cdda) == 0): - msg.error(_(u"No CD in drive")) + msg.error(_.ERR_NO_CDDA) sys.exit(1) + else: + cd_track_count = len(cdda) #use CDDA object to query metadata services for metadata choices metadata_choices = cdda.metadata_lookup( @@ -255,121 +236,141 @@ for m in c: m.album_total = options.album_total - #decide which metadata to use to tag extracted tracks - if (options.interactive): - #pick choice using interactive widget - metadata_widget = audiotools.ui.MetaDataFiller(metadata_choices) - loop = audiotools.ui.urwid.MainLoop( - metadata_widget, - [('key', 'white', 'dark blue')], - unhandled_input=metadata_widget.handle_text) - loop.run() - - track_metadatas = dict([(m.track_number, m) for m in - metadata_widget.populated_metadata()]) - else: - if ((len(metadata_choices) == 1) or options.use_default): - #use default choice - track_metadatas = dict([(m.track_number, m) for m in - metadata_choices[0]]) - else: - #pick choice using raw stdin/stdout - track_metadatas = \ - dict([(m.track_number, m) for m in - audiotools.ui.select_metadata(metadata_choices, msg)]) - + #determine tracks to be ripped if (len(args) == 0): - to_rip = [track for track in cdda] + tracks_to_rip = set(range(1, len(cdda) + 1)) else: - to_rip = [] + tracks_to_rip = set([]) for arg in args: try: - to_rip.append(cdda[int(arg)]) - except IndexError: - continue + tracks_to_rip.add(int(arg)) except ValueError: continue - to_rip.sort(lambda x, y: cmp(x.track_number, y.track_number)) - encoded = [] - rip_log = {} - for cd_track in to_rip: - basename = "track%2d%2.2d" % (options.album_number, - cd_track.track_number) + if (len(limit(range(cd_track_count), tracks_to_rip)) == 0): + #no tracks selected to rip, so do nothing + sys.exit(0) - try: - metadata = track_metadatas.get(cd_track.track_number, None) + #decide which metadata and output options use when extracting tracks + if (options.interactive): + #pick options using interactive widget + output_widget = audiotools.ui.OutputFiller( + track_labels=limit([_.LAB_TRACK_X_OF_Y % (i + 1, cd_track_count) + for i in xrange(cd_track_count)], + tracks_to_rip), + metadata_choices=[limit(c, tracks_to_rip) + for c in metadata_choices], + input_filenames=limit( + [audiotools.Filename("track%2.2d.cdda.wav" % (i + 1)) + for i in xrange(cd_track_count)], + tracks_to_rip), + output_directory=options.dir, + format_string=options.format, + output_class=AudioType, + quality=quality, + completion_label=_.LAB_CD2TRACK_APPLY) - filename = os.path.join( - base_directory, - AudioType.track_name(file_path=basename, - track_metadata=metadata, - format=options.format)) + loop = audiotools.ui.urwid.MainLoop( + output_widget, + audiotools.ui.style(), + unhandled_input=output_widget.handle_text, + pop_ups=True) + try: + loop.run() + msg.ansi_clearscreen() + except (termios.error, IOError): + msg.error(_.ERR_TERMIOS_ERROR) + msg.info(_.ERR_TERMIOS_SUGGESTION) + msg.info(audiotools.ui.xargs_suggestion(sys.argv)) + sys.exit(1) - try: - audiotools.make_dirs(filename) - except OSError, err: - msg.os_error(err) - sys.exit(1) - - progress = audiotools.SingleProgressDisplay( - msg, msg.filename(filename)) - - track = AudioType.from_pcm( - filename, - audiotools.PCMReaderProgress( - cd_track, - cd_track.length() * (44100 / 75), - progress.update), - quality) - - track.set_metadata(metadata) - progress.clear() - - encoded.append(track) - - rip_log[cd_track.track_number] = cd_track.rip_log - # if (options.accuraterip): - # if (db_entry is not None): - # for entry in db_entry[cd_track.track_number - 1]: - # if (entry.crc == int(cd_track.accuraterip_crc)): - # rip_log[cd_track.track_number]["accuraterip"] = \ - # _(u"confidence %d") % (entry.confidence) - # break - # else: - # rip_log[cd_track.track_number]["accuraterip"] = \ - # _(u"unverified") - # else: - # rip_log[cd_track.track_number]["accuraterip"] = \ - # _(u"unknown") - - msg.info(_(u"track %(track_number)2.2d -> %(filename)s") % \ - {"track_number": cd_track.track_number, - "filename": msg.filename(track.filename)}) + if (not output_widget.cancelled()): + output_tracks = list(output_widget.output_tracks()) + else: + sys.exit(0) + else: + #pick options without using GUI + try: + output_tracks = list( + audiotools.ui.process_output_options( + metadata_choices=[limit(c, tracks_to_rip) + for c in metadata_choices], + input_filenames=limit( + [audiotools.Filename("track%2.2d.cdda.wav" % (i + 1)) + for i in xrange(cd_track_count)], + tracks_to_rip), + output_directory=options.dir, + format_string=options.format, + output_class=AudioType, + quality=options.quality, + msg=msg, + use_default=options.use_default)) except audiotools.UnsupportedTracknameField, err: err.error_msg(msg) sys.exit(1) - except KeyError: - continue - except audiotools.InvalidFormat, err: + except (audiotools.InvalidFilenameFormat, + audiotools.OutputFileIsInput, + audiotools.DuplicateOutputFile), err: msg.error(unicode(err)) sys.exit(1) + + #perform actual ripping of tracks from CDDA + encoded = [] + rip_log = {} + for (i, (cd_track, + (output_class, + output_filename, + output_quality, + output_metadata))) in enumerate(zip(limit(cdda, + tracks_to_rip), + output_tracks)): + try: + audiotools.make_dirs(str(output_filename)) + except OSError, err: + msg.os_error(err) + sys.exit(1) + + progress = audiotools.SingleProgressDisplay( + msg, unicode(output_filename)) + + try: + encoded.append( + output_class.from_pcm( + str(output_filename), + audiotools.PCMReaderProgress( + cd_track, + cd_track.length() * (44100 / 75), + progress.update), + output_quality)) except audiotools.EncodingError, err: - msg.error(_(u"Unable to write \"%s\"") % (msg.filename(filename))) + msg.error(_.ERR_ENCODING_ERROR % (output_filename,)) sys.exit(1) + encoded[-1].set_metadata(output_metadata) + progress.clear() + + rip_log[cd_track.track_number] = cd_track.rip_log + + msg.info( + audiotools.output_progress( + _.LAB_CD2TRACK_PROGRESS % + {"track_number": cd_track.track_number, + "filename": output_filename}, i + 1, len(output_tracks))) + cdda.close() - if (audiotools.ADD_REPLAYGAIN and - options.add_replay_gain and - AudioType.can_add_replay_gain()): + #add ReplayGain to ripped tracks, if necessary + if ((audiotools.ADD_REPLAYGAIN and + (options.add_replay_gain if (options.add_replay_gain is not None) + else output_class.lossless_replay_gain()) and + output_class.can_add_replay_gain(encoded))): rg_progress = audiotools.ReplayGainProgressDisplay( - msg, AudioType.lossless_replay_gain()) + msg, output_class.lossless_replay_gain()) rg_progress.initial_message() try: #all audio files must belong to the same album, by definition - AudioType.add_replay_gain([f.filename for f in encoded], - rg_progress.update) + output_class.add_replay_gain([f.filename for f in encoded], + rg_progress.update) except ValueError, err: rg_progress.clear() msg.error(unicode(err)) @@ -377,14 +378,12 @@ rg_progress.final_message() #display ripping log - msg.output(_("Rip log : ")) + msg.output(_.LAB_CD2TRACK_LOG) msg.new_row() headers = [u"track", u"rderr", u"skip", u"atom", u"edge", u"drop", u"dup", u"drift"] keys = ["rderr", "skip", "atom", "edge", "drop", "dup", "drift"] - # if (options.accuraterip): - # headers.append(u" AccurateRip") - # keys.append("accuraterip") + dividers = [] for header in headers: msg.output_column(header, True)
View file
audiotools-2.18.tar.gz/cdinfo -> audiotools-2.19.tar.gz/cdinfo
Changed
@@ -19,15 +19,14 @@ import os.path -import audiotools import sys -import gettext +import audiotools +import audiotools.text as _ -gettext.install("audiotools", unicode=True) if (__name__ == '__main__'): parser = audiotools.OptionParser( - usage=_(u"%prog [options]"), + usage=_.USAGE_CDINFO, version="Python Audio Tools %s" % (audiotools.VERSION)) parser.add_option('-c', '--cdrom', action='store', @@ -35,7 +34,7 @@ default=audiotools.DEFAULT_CDROM) (options, args) = parser.parse_args() - msg = audiotools.Messenger("cd2xmcd", options) + msg = audiotools.Messenger("cd2info", options) try: cdda = audiotools.CDDA(options.cdrom, perform_logging=False) @@ -44,34 +43,35 @@ sys.exit(1) if (len(cdda) == 255): - msg.error(_(u"No CD in drive")) + msg.error(_.ERR_NO_CDDA) sys.exit(1) elif (len(cdda) < 1): - msg.error(_(u"No audio tracks found on CD")) + msg.error(_.ERR_NO_EMPTY_CDDA) sys.exit(1) tracks = list(cdda) msg.new_row() - msg.output_column(_(u"Total Tracks"), True) - msg.output_column(_(u" : ")) + msg.output_column(_.LAB_TOTAL_TRACKS, True) + msg.output_column(u" : ") msg.output_column(unicode(len(cdda))) msg.new_row() - msg.output_column(_(u"Total Length"), True) - msg.output_column(_(u" : ")) - msg.output_column(_(u"%2d:%2.2d (%d frames)" % (cdda.length() / 75 / 60, - cdda.length() / 75 % 60, - cdda.length()))) + msg.output_column(_.LAB_TOTAL_LENGTH, True) + msg.output_column(u" : ") + msg.output_column( + _.LAB_TRACK_LENGTH_FRAMES % (cdda.length() / 75 / 60, + cdda.length() / 75 % 60, + cdda.length())) msg.new_row() - msg.output_column(_(u"FreeDB disc ID"), True) - msg.output_column(_(u" : ")) + msg.output_column(_.LAB_FREEDB_ID, True) + msg.output_column(u" : ") msg.output_column(str(cdda.freedb_disc_id()).decode('ascii')) msg.new_row() - msg.output_column(_(u"MusicBrainz disc ID"), True) - msg.output_column(_(u" : ")) + msg.output_column(_.LAB_MUSICBRAINZ_ID, True) + msg.output_column(u" : ") msg.output_column(str(cdda.musicbrainz_disc_id()).decode('ascii')) @@ -79,29 +79,33 @@ msg.output_rows() msg.new_row() - msg.output_column(_(u"#"), True) + msg.output_column(u"#", True) msg.output_column(u" ") - msg.output_column(_(u"Length")) + msg.output_column(_.LAB_CDINFO_LENGTH) msg.output_column(u" ") - msg.output_column(_(u"Frames")) + msg.output_column(_.LAB_CDINFO_FRAMES) msg.output_column(u" ") - msg.output_column(_(u"Offset")) + msg.output_column(_.LAB_CDINFO_OFFSET) msg.new_row() - msg.output_column(_(u"--")) + msg.output_column(u"--") msg.output_column(u" ") - msg.output_column(_(u"------")) + msg.output_column( + u"-" * len(audiotools.display_unicode(_.LAB_CDINFO_LENGTH))) msg.output_column(u" ") - msg.output_column(_(u"------")) + msg.output_column( + u"-" * len(audiotools.display_unicode(_.LAB_CDINFO_FRAMES))) msg.output_column(u" ") - msg.output_column(_(u"------")) + msg.output_column( + u"-" * len(audiotools.display_unicode(_.LAB_CDINFO_OFFSET))) for track in tracks: msg.new_row() msg.output_column(unicode(track.track_number), True) msg.output_column(u" ") - msg.output_column(u"%d:%2.2d" % (track.length() / 75 / 60, - track.length() / 75 % 60), True) + msg.output_column( + _.LAB_TRACK_LENGTH % (track.length() / 75 / 60, + track.length() / 75 % 60), True) msg.output_column(u" ") msg.output_column(unicode(track.length())) msg.output_column(u" ")
View file
audiotools-2.18.tar.gz/cdplay -> audiotools-2.19.tar.gz/cdplay
Changed
@@ -17,370 +17,125 @@ #along with this program; if not, write to the Free Software #Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import audiotools -import audiotools.player import sys -import gettext import time import select import os import tty import termios import cStringIO +import audiotools +import audiotools.ui +import audiotools.player +import audiotools.text as _ -gettext.install("audiotools", unicode=True) - -try: - import urwid - - class MappedButton(urwid.Button): - def __init__(self, label, on_press=None, user_data=None, - key_map={}): - urwid.Button.__init__(self, label=label, - on_press=on_press, - user_data=user_data) - self.__key_map__ = key_map - - def keypress(self, size, key): - return urwid.Button.keypress(self, - size, - self.__key_map__.get(key, key)) - - class MappedRadioButton(urwid.RadioButton): - def __init__(self, group, label, state='first True', - on_state_change=None, user_data=None, - key_map={}): - urwid.RadioButton.__init__(self, group=group, - label=label, - state=state, - on_state_change=on_state_change, - user_data=user_data) - self.__key_map__ = key_map - - def keypress(self, size, key): - return urwid.RadioButton.keypress(self, - size, - self.__key_map__.get(key, key)) - - class AudioProgressBar(urwid.ProgressBar): - def __init__(self, normal, complete, sample_rate, current=0, done=100, - satt=None): - urwid.ProgressBar.__init__(self, - normal=normal, - complete=complete, - current=current, - done=done, - satt=satt) - self.sample_rate = sample_rate - - def render(self, size, focus=False): - """ - Render the progress bar. - """ - - #leeched from the orignal implementation - #only the txt formatting differs - - (maxcol,) = size +if (audiotools.ui.AVAILABLE): + urwid = audiotools.ui.urwid - try: - txt = urwid.Text("%d:%2.2d" % \ - ((self.current / self.sample_rate) / 60, - (self.current / self.sample_rate) % 60), - 'center', 'clip') - except ZeroDivisionError: - txt = urwid.Text("0:00", 'center', 'clip') - c = txt.render((maxcol,)) - - cf = float(self.current) * maxcol / self.done - ccol = int(cf) - cs = 0 - if self.satt is not None: - cs = int((cf - ccol) * 8) - if ccol < 0 or (ccol == 0 and cs == 0): - c._attr = [[(self.normal, maxcol)]] - elif ccol >= maxcol: - c._attr = [[(self.complete, maxcol)]] - elif cs and c._text[0][ccol] == " ": - t = c._text[0] - cenc = self.eighths[cs].encode("utf-8") - c._text[0] = t[:ccol] + cenc + t[ccol + 1:] - a = [] - if ccol > 0: - a.append((self.complete, ccol)) - a.append((self.satt, len(cenc))) - if maxcol - ccol - 1 > 0: - a.append((self.normal, maxcol - ccol - 1)) - c._attr = [a] - c._cs = [[(None, len(c._text[0]))]] - else: - c._attr = [[(self.complete, ccol), - (self.normal, maxcol - ccol)]] - return c - - class CDplayGUI(urwid.Frame): + class CDplayGUI(audiotools.ui.PlayerGUI): def __init__(self, cdda, metadata, track_list, audio_output): self.cdda = cdda - self.metadata = metadata - self.track_list = track_list - self.audio_output = audio_output - self.player = audiotools.player.CDPlayer( - cdda=cdda, - audio_output=audio_output, - next_track_callback=self.next_track) - - self.track_name = urwid.Text("") - self.album_name = urwid.Text("") - self.artist_name = urwid.Text("") - self.tracknum = urwid.Text("") - self.length = urwid.Text("", align='right') - self.channels = urwid.Text("2ch", align='right') - self.sample_rate = urwid.Text("44100Hz", align='right') - self.bits_per_sample = urwid.Text("16bps", align='right') - self.play_pause_button = MappedButton("Play", - on_press=self.play_pause, - key_map={'tab': 'right'}) - self.progress = AudioProgressBar(normal='pg normal', - complete='pg complete', - sample_rate=0, - current=0, - done=100) - self.progress.sample_rate = 44100 - - self.track_group = [] - - track_len = cdda.length() / 75 - - self.status = urwid.Text( - "%(count)d tracks, %(min)d:%(sec)2.2d minutes" % { - "count": len(self.track_list), - "min": int(track_len) / 60, - "sec": int(track_len) % 60}, - align='center') - - header = urwid.Pile([ - urwid.Columns([ - ('fixed', 9, urwid.Text(('header', u"Name : "), - align='right')), - ('weight', 1, self.track_name)]), - urwid.Columns([ - ('fixed', 9, urwid.Text(('header', u"Album : "), - align='right')), - ('weight', 1, self.album_name)]), - urwid.Columns([ - ('fixed', 9, urwid.Text(('header', u"Artist : "), - align='right')), - ('weight', 1, self.artist_name)]), - urwid.Columns([ - ('fixed', 9, urwid.Text(('header', u"Track : "), - align='right')), - ('fixed', 7, self.tracknum), - ('fixed', 7, self.length), - ('fixed', 5, self.channels), - ('fixed', 10, self.sample_rate), - ('fixed', 7, self.bits_per_sample)]), - self.progress]) - - controls = urwid.GridFlow([ - MappedButton("Prev", on_press=self.previous_track, - key_map={'tab': 'right'}), - self.play_pause_button, - MappedButton("Next", on_press=self.next_track, - key_map={'tab': 'down'})], - 9, 4, 1, 'center') - controls.set_focus(1) - - def track_label(label, track_number): - if (len(label) > 0): - return label - else: - return "track %2.2d" % (track_number) - - track_list = urwid.ListBox([ - MappedRadioButton( - group=self.track_group, - label=track_label( - metadata[track.track_number].track_name, - track.track_number), - user_data=(track, - metadata[track.track_number].track_name, - metadata[track.track_number].artist_name, - metadata[track.track_number].album_name), - on_state_change=self.select_track, - key_map={'tab': 'down'}) - for track in self.track_list]) - body = urwid.Pile([('fixed', 2, urwid.Filler(urwid.Pile([ - controls, urwid.Divider(div_char='-')]))), - ('weight', 1, track_list)]) - footer = urwid.Pile([urwid.Divider(div_char='-'), - self.status]) - - urwid.Frame.__init__(self, body=body, header=header, footer=footer) - - def next_track(self, user_data=None): - track_index = [g.state for g in self.track_group].index(True) - try: - self.track_group[track_index + 1].set_state(True) - except IndexError: - pass - def previous_track(self, user_data=None): - track_index = [g.state for g in self.track_group].index(True) - try: - if (track_index == 0): - raise IndexError() - else: - self.track_group[track_index - 1].set_state(True) - except IndexError: - pass + audiotools.ui.PlayerGUI.__init__( + self, + audiotools.player.CDPlayer( + cdda=cdda, + audio_output=audio_output, + next_track_callback=self.next_track), + [(metadata[track.track_number].track_name + if (metadata[track.track_number].track_name is not None) + else (u"track %2.2d" % (track.track_number)), + track.length() / 75, + (track, metadata[track.track_number])) + for track in track_list], + cdda.length() / 75) def select_track(self, radio_button, new_state, user_data, auto_play=True): - if (new_state == True): - (track, - track_name, - artist_name, - album_name) = user_data - self.track_name.set_text(track_name) - self.album_name.set_text(album_name) - self.artist_name.set_text(artist_name) - self.tracknum.set_text(u"%d / %d" % \ - (track.track_number, - len(self.cdda))) - - track_length = track.length() / 75 - self.length.set_text("%d:%2.2d" % (int(track_length) / 60, - int(track_length) % 60)) - - self.progress.current = 0 - self.progress.done = track.length() * (44100 / 75) + if (new_state): + (track, metadata) = user_data + + if (metadata is not None): + self.update_metadata( + track_name=metadata.track_name, + album_name=metadata.album_name, + artist_name=metadata.artist_name, + track_number=track.track_number, + track_total=len(self.cdda), + pcm_frames=track.length() * 588, + channels=2, + sample_rate=44100, + bits_per_sample=16) + else: + self.update_metadata( + track_total=len(self.cdda), + pcm_frames=track.length() * 588, + channels=2, + sample_rate=44100, + bits_per_sample=16) self.player.open(track.track_number) if (auto_play): self.player.play() - self.play_pause_button.set_label("Pause") - - def update_status(self): - self.progress.set_completion(self.player.progress()[0]) - - def play_pause(self, user_data): - self.player.toggle_play_pause() - if (self.play_pause_button.get_label() == "Play"): - self.play_pause_button.set_label("Pause") - elif (self.play_pause_button.get_label() == "Pause"): - self.play_pause_button.set_label("Play") - - def handle_text(self, i): - if ((i == 'esc') or (i == 'q') or (i == 'Q')): - self.perform_exit() - elif ((i == 'n') or (i == 'N')): - self.next_track() - elif ((i == 'p') or (i == 'P')): - self.previous_track() - - def perform_exit(self, *args): - self.player.close() - raise urwid.ExitMainLoop() - - def timer(main_loop, cdplay): - cdplay.update_status() - loop.set_alarm_at(tm=time.time() + 1, - callback=timer, - user_data=cdplay) + self.play_pause_button.set_label(_.LAB_PAUSE_BUTTON) interactive_available = True -except ImportError: +else: interactive_available = False -class CDplayTTY: - OUTPUT_FORMAT = (u"%(track_number)d/%(track_total)d " + - u"[%(sent_minutes)d:%(sent_seconds)2.2d / " + - u"%(total_minutes)d:%(total_seconds)2.2d] " + - u"%(channels)dch %(sample_rate)dHz " + - u"%(bits_per_sample)d-bit") - +class CDplayTTY(audiotools.ui.PlayerTTY): def __init__(self, cdda, track_list, audio_output): - self.track_list = track_list - self.player = audiotools.player.CDPlayer( - cdda=cdda, - audio_output=audio_output, - next_track_callback=self.next_track) self.track_index = -1 - self.current_track = None - self.seconds_total = 0 - self.track_number = 0 - self.track_total = len(self.track_list) - - def quit(self): - if (self.current_track is not None): - self.current_track = None - self.player.close() - - def toggle_play_pause(self): - if (self.current_track is not None): - self.player.toggle_play_pause() - - def play(self): - self.next_track() + self.track_list = track_list + self.cdda = cdda + audiotools.ui.PlayerTTY.__init__( + self, + audiotools.player.CDPlayer( + cdda=cdda, + audio_output=audio_output, + next_track_callback=self.next_track)) def previous_track(self): if (self.track_index > 0): self.track_index -= 1 - self.current_track = self.track_list[self.track_index] - self.track_number = self.track_index + 1 - self.player.open(self.current_track.track_number) + current_track = self.track_list[self.track_index] + self.set_metadata( + track_number=self.track_index + 1, + track_total=len(self.cdda), + channels=2, + sample_rate=44100, + bits_per_sample=16) + self.player.open(current_track.track_number) self.player.play() def next_track(self): try: self.track_index += 1 - self.current_track = self.track_list[self.track_index] - self.track_number = self.track_index + 1 - self.player.open(self.current_track.track_number) + current_track = self.track_list[self.track_index] + self.set_metadata( + track_number=self.track_index + 1, + track_total=len(self.cdda), + channels=2, + sample_rate=44100, + bits_per_sample=16) + self.player.open(current_track.track_number) self.player.play() except IndexError: - self.current_track = None - self.player.close() - - def progress(self): - return self.player.progress() - - def progress_line(self): - return (self.OUTPUT_FORMAT % - {"track_number": self.track_number, - "track_total": self.track_total, - "sent_minutes": - (frames_sent / 44100) / 60, - "sent_seconds": - (frames_sent / 44100) % 60, - "total_minutes": - (frames_total / 44100) / 60, - "total_seconds": - (frames_total / 44100) % 60, - "channels": 2, - "sample_rate": 44100, - "bits_per_sample": 16}) + self.playing_finished = True + if (__name__ == '__main__'): parser = audiotools.OptionParser( - usage=_(u"%prog [track 1] [track 2] ..."), + usage=_.USAGE_CDPLAY, version="Python Audio Tools %s" % (audiotools.VERSION)) parser.add_option( - '-V', '--verbose', - action='store', - dest='verbosity', - choices=audiotools.VERBOSITY_LEVELS, - default=audiotools.DEFAULT_VERBOSITY, - help=_(u'the verbosity level to execute at')) - - parser.add_option( '-I', '--interactive', action='store_true', default=False, dest='interactive', - help=_(u'run in interactive mode')) + help=_.OPT_INTERACTIVE_PLAY) players_map = dict([(player.NAME, player) for player in audiotools.player.AUDIO_OUTPUT]) @@ -389,13 +144,14 @@ '-o', '--output', action='store', dest='output', - choices=players_map.keys(), + choices=[player.NAME for player in audiotools.player.AUDIO_OUTPUT + if player.available()], default=[player.NAME for player in audiotools.player.AUDIO_OUTPUT if player.available()][0], - help=_(u"the method to play audio (choose from: %s)") % \ - u", ".join([u"\"%s\"" % (player.NAME.decode('ascii')) - for player in audiotools.player.AUDIO_OUTPUT - if player.available()])) + help=(_.OPT_OUTPUT_PLAY % + u", ".join([u"\"%s\"" % (player.NAME.decode('ascii')) + for player in audiotools.player.AUDIO_OUTPUT + if player.available()]))) parser.add_option( '-c', '--cdrom', action='store', @@ -406,7 +162,15 @@ '--shuffle', action='store_true', dest='shuffle', default=False, help='shuffle tracks') - lookup = audiotools.OptionGroup(parser, _(u"CD Lookup Options")) + parser.add_option( + '-V', '--verbose', + action='store', + dest='verbosity', + choices=audiotools.VERBOSITY_LEVELS, + default=audiotools.DEFAULT_VERBOSITY, + help=_.OPT_VERBOSE) + + lookup = audiotools.OptionGroup(parser, _.OPT_CAT_CD_LOOKUP) lookup.add_option( '--musicbrainz-server', action='store', @@ -422,7 +186,7 @@ '--no-musicbrainz', action='store_false', dest='use_musicbrainz', default=audiotools.MUSICBRAINZ_SERVICE, - help='do not query MusicBrainz for metadata') + help=_.OPT_NO_MUSICBRAINZ) lookup.add_option( '--freedb-server', action='store', @@ -438,7 +202,7 @@ '--no-freedb', action='store_false', dest='use_freedb', default=audiotools.FREEDB_SERVICE, - help='do not query FreeDB for metadata') + help=_.OPT_NO_FREEDB) parser.add_option_group(lookup) @@ -446,20 +210,19 @@ msg = audiotools.Messenger("trackplay", options) if (options.interactive and (not interactive_available)): - msg.error(_(u"urwid is required for interactive mode")) - msg.output(_(u"Please download and install urwid " + - u"from http://excess.org/urwid/")) - msg.output(_(u"or your system's package manager.")) + msg.error(_.ERR_URWID_REQUIRED) + msg.output(_.ERR_GET_URWID1) + msg.output(_.ERR_GET_URWID2) sys.exit(1) try: cdda = audiotools.CDDA(options.cdrom, perform_logging=False) except IOError, err: - msg.error(unicode(err) + _(u". Is that an audio cd ?")) + msg.error(unicode(err) + _.ERR_INVALID_CDDA) sys.exit(-1) if (len(cdda) == 0): - msg.error(_(u"No CD in drive")) + msg.error(ERR_NO_CDDA) sys.exit(1) if (len(args) == 0): @@ -480,14 +243,15 @@ if (options.interactive): #a track_number -> MetaData dictionary #where track_number typically starts from 1 - metadata = dict([(m.track_number, m) for m in - cdda.metadata_lookup( - musicbrainz_server=options.musicbrainz_server, - musicbrainz_port=options.musicbrainz_port, - freedb_server=options.freedb_server, - freedb_port=options.freedb_port, - use_musicbrainz=options.use_musicbrainz, - use_freedb=options.use_freedb)[0]]) + metadata = dict( + [(m.track_number, m) for m in + cdda.metadata_lookup( + musicbrainz_server=options.musicbrainz_server, + musicbrainz_port=options.musicbrainz_port, + freedb_server=options.freedb_server, + freedb_port=options.freedb_port, + use_musicbrainz=options.use_musicbrainz, + use_freedb=options.use_freedb)[0]]) cdplay = CDplayGUI(cdda=cdda, metadata=metadata, @@ -496,63 +260,26 @@ if (len(cdda) > 0): cdplay.select_track(None, True, (tracks[0], - metadata[1].track_name, - metadata[1].artist_name, - metadata[1].album_name), + metadata[tracks[0].track_number]), False) loop = urwid.MainLoop(cdplay, - [('header', 'default,bold', 'default', ''), - ('pg normal', 'white', 'black', 'standout'), - ('pg complete', 'white', 'dark blue'), - ('pg smooth', 'dark blue', 'black')], + audiotools.ui.style(), unhandled_input=cdplay.handle_text) loop.set_alarm_at(tm=time.time() + 1, - callback=timer, + callback=audiotools.ui.timer, user_data=cdplay) - loop.run() + try: + loop.run() + msg.ansi_clearscreen() + except (termios.error, IOError): + msg.error(_.ERR_TERMIOS_ERROR) + msg.info(_.ERR_TERMIOS_SUGGESTION) + msg.info(audiotools.ui.xargs_suggestion(sys.argv)) + sys.exit(1) else: trackplay = CDplayTTY(cdda=cdda, track_list=tracks, audio_output=players_map[options.output]()) - trackplay.play() - output_line_len = 0 - - original_terminal_settings = termios.tcgetattr(0) - try: - tty.setcbreak(sys.stdin.fileno()) - while (trackplay.current_track is not None): - (frames_sent, frames_total) = trackplay.progress() - output_line = trackplay.progress_line() - msg.ansi_clearline() - if (len(output_line) > output_line_len): - output_line_len = len(output_line) - msg.partial_output(output_line) - else: - msg.partial_output(output_line + - (u" " * (output_line_len - - len(output_line)))) - - (r_list, w_list, x_list) = select.select([sys.stdin.fileno()], - [], [], 1) - if (len(r_list) > 0): - char = os.read(sys.stdin.fileno(), 1) - if ((char == 'q') or - (char == 'Q') or - (char == '\x1B')): - trackplay.quit() - elif (char == ' '): - trackplay.toggle_play_pause() - elif ((char == 'n') or - (char == 'N')): - trackplay.next_track() - elif ((char == 'p') or - (char == 'P')): - trackplay.previous_track() - else: - pass - - msg.ansi_clearline() - finally: - termios.tcsetattr(0, termios.TCSADRAIN, original_terminal_settings) + sys.exit(trackplay.run(msg, sys.stdin))
View file
audiotools-2.18.tar.gz/coverdump -> audiotools-2.19.tar.gz/coverdump
Changed
@@ -18,63 +18,66 @@ #Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import audiotools import sys import os import os.path -import gettext - -gettext.install("audiotools", unicode=True) +import audiotools +import audiotools.text as _ FILENAME_TYPES = ("front_cover", "back_cover", "leaflet", "media", "other") if (__name__ == '__main__'): parser = audiotools.OptionParser( - usage=_(u'%prog [-d directory] <track>'), + usage=_.USAGE_COVERDUMP, version="Python Audio Tools %s" % (audiotools.VERSION)) - parser.add_option('-V', '--verbose', - action='store', - dest='verbosity', - choices=audiotools.VERBOSITY_LEVELS, - default=audiotools.DEFAULT_VERBOSITY, - help=_(u'the verbosity level to execute at')) - parser.add_option('-d', '--dir', action='store', type='string', dest='dir', default='.', - help=_(u'the directory to store extracted images')) + help=_.OPT_DIR_IMAGES) parser.add_option('-p', '--prefix', action='store', dest='prefix', default="", - help=_(u'add a prefix to the output image')) + help=_.OPT_PREFIX) + + parser.add_option('-V', '--verbose', + action='store', + dest='verbosity', + choices=audiotools.VERBOSITY_LEVELS, + default=audiotools.DEFAULT_VERBOSITY, + help=_.OPT_VERBOSE) (options, args) = parser.parse_args() msg = audiotools.Messenger("coverdump", options) if (len(args) != 1): - msg.error(_(u"You must specify exactly 1 supported audio file")) + msg.error(_.ERR_1_FILE_REQUIRED) sys.exit(1) + else: + input_filename = audiotools.Filename(args[0]) try: - audiofile = audiotools.open(args[0]) + audiofile = audiotools.open(str(input_filename)) except audiotools.UnsupportedFile: - msg.error(_(u"%s file format not supported") % msg.filename(args[0])) + msg.error(_.ERR_1_FILE_REQUIRED) sys.exit(1) except IOError: - msg.error(_(u"Unable to open \"%s\"") % (msg.filename(args[0]))) + msg.error(_.ERR_OPEN_IOERROR % (audiotools.Filename(args[0]),)) sys.exit(1) metadata = audiofile.get_metadata() if (metadata is not None): + #divide images by type (front cover, leaflet page, etc.) image_types = {} for image in metadata.images(): image_types.setdefault(image.type, []).append(image) + #build a set of (Image, Filename) tuples to be extracted + output_images = [] for (type, images) in image_types.items(): if (len(images) != 1): FILE_TEMPLATE = \ @@ -84,25 +87,43 @@ "%(prefix)s%(filename)s.%(suffix)s" for (i, image) in enumerate(images): - output_filename = os.path.join( - options.dir, - FILE_TEMPLATE % {"prefix": options.prefix, - "filename": FILENAME_TYPES[image.type], - "filenum": i + 1, - "suffix": image.suffix()}) - try: - audiotools.make_dirs(output_filename) - f = open(output_filename, "wb") - f.write(image.data) - f.close() - msg.info( - _(u"%(source)s -> %(destination)s") % - {"source": msg.filename(audiofile.filename), - "destination": msg.filename(output_filename)}) - except IOError, e: - msg.error(_(u"Unable to write \"%s\"") % - msg.filename(output_filename)) - sys.exit(1) - except OSError, e: - msg.os_error(e) - sys.exit(1) + output_images.append( + (image, + audiotools.Filename( + os.path.join( + options.dir, + FILE_TEMPLATE % { + "prefix": options.prefix, + "filename": FILENAME_TYPES[image.type], + "filenum": i + 1, + "suffix": image.suffix()})))) + + #ensure our input file isn't the same + #as any of the proposed files to extract + #(this sounds crazy, + # but there's no technical reason one's audio file + # can't be named "front_cover.jpg" + # even if it's not a JPEG + # so we have to be sure it's not going to be overwritten) + if (input_filename in + [output_filename for + (image, output_filename) in output_images]): + msg.error(_.ERR_OUTPUT_IS_INPUT % (input_filename,)) + sys.exit(1) + + #finally, write actual image data to disk if possible + for (image, output_filename) in output_images: + try: + audiotools.make_dirs(str(output_filename)) + f = open(str(output_filename), "wb") + f.write(image.data) + f.close() + msg.info(_.LAB_ENCODE % + {"source": input_filename, + "destination": output_filename}) + except IOError, e: + msg.error(_.ERR_ENCODING_ERROR % (output_filename,)) + sys.exit(1) + except OSError, e: + msg.os_error(e) + sys.exit(1)
View file
audiotools-2.19.tar.gz/covertag
Added
@@ -0,0 +1,169 @@ +#!/usr/bin/python + +#Audio Tools, a module and set of tools for manipulating audio data +#Copyright (C) 2007-2012 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 + + +import sys +import os.path +import audiotools +import audiotools.ui +import audiotools.text as _ + + +IMAGE_TYPE_ORDER = [0, 2, 1, 3, 4] + + +#tries to return a populated Image object of the appropriate type +#raises InvalidImage if something goes wrong during opening or parsing +def get_image(filename, type): + try: + return audiotools.Image.new(open(filename, 'rb').read(), u'', type) + except IOError: + raise audiotools.InvalidImage(_.ERR_OPEN_IOERROR % + (audiotools.Filename(filename),)) + + +if (__name__ == '__main__'): + parser = audiotools.OptionParser( + usage=_.USAGE_COVERTAG, + version="Python Audio Tools %s" % (audiotools.VERSION)) + + parser.add_option( + '-r', '--replace', + action='store_true', + default=False, + dest='replace', + help=_.OPT_TRACKTAG_REPLACE) + + #FIXME - add -I/--interactive mode + #which should use a proper GUI, if available, + #so one can see added images directly + + parser.add_option( + '-V', '--verbose', + action='store', + dest='verbosity', + choices=audiotools.VERBOSITY_LEVELS, + default=audiotools.DEFAULT_VERBOSITY, + help=_.OPT_VERBOSE) + + img_group = audiotools.OptionGroup(parser, _.OPT_CAT_IMAGE) + + for (option, destination, helptext) in [ + ('--front-cover', 'front_cover', + _.OPT_TRACKTAG_FRONT_COVER), + ('--back-cover', 'back_cover', + _.OPT_TRACKTAG_BACK_COVER), + ('--leaflet', 'leaflet', + _.OPT_TRACKTAG_LEAFLET), + ('--media', 'media', + _.OPT_TRACKTAG_MEDIA), + ('--other-image', 'other_image', + _.OPT_TRACKTAG_OTHER_IMAGE)]: + img_group.add_option( + option, + action='append', + type='string', + dest=destination, + metavar='FILENAME', + help=helptext) + + parser.add_option_group(img_group) + + (options, args) = parser.parse_args() + msg = audiotools.Messenger("covertag", options) + + #open our set of input files for tagging + try: + audiofiles = audiotools.open_files(args, + messenger=msg, + no_duplicates=True) + except audiotools.DuplicateFile, err: + msg.error(_.ERR_DUPLICATE_FILE % (err.filename,)) + sys.exit(1) + + #open images for addition + #to avoid reading the same images multiple times + images = {} + try: + if (options.front_cover is not None): + for path in options.front_cover: + images.setdefault(0, []).append(get_image(path, 0)) + + if (options.leaflet is not None): + for path in options.leaflet: + images.setdefault(2, []).append(get_image(path, 2)) + + if (options.back_cover is not None): + for path in options.back_cover: + images.setdefault(1, []).append(get_image(path, 1)) + + if (options.media is not None): + for path in options.media: + images.setdefault(3, []).append(get_image(path, 3)) + + if (options.other_image is not None): + for path in options.other_image: + images.setdefault(4, []).append(get_image(path, 4)) + except audiotools.InvalidImage, err: + msg.error(unicode(err)) + sys.exit(1) + + for track in audiofiles: + #get metadata from each audio file + metadata = track.get_metadata() + + #if metadata is present + if (metadata is not None): + if (metadata.supports_images()): + #if --replace indicated, remove old images + if (options.replace): + for i in metadata.images(): + metadata.delete_image(i) + + #add images to metadata object in order + for t in IMAGE_TYPE_ORDER: + for i in images.get(t, []): + metadata.add_image(i) + + #call update_metadata() to update track's metadata + try: + track.update_metadata(metadata) + except IOError, err: + msg.error(_.ERR_ENCODING_ERROR % + (audiotools.Filename(track.filename),)) + sys.exit(1) + else: + #metadata doesn't support images, so do nothing + pass + else: + #if no metadata is present, construct new MetaData object + metadata = audiotools.MetaData() + + #add images to metadata object in order + for t in IMAGE_TYPE_ORDER: + for i in images.get(t, []): + metadata.add_image(i) + + #call set_metadata() to update track's metadata + try: + track.set_metadata(metadata) + except IOError, err: + msg.error(_.ERR_ENCODING_ERROR % + (audiotools.Filename(track.filename),)) + sys.exit(1)
View file
audiotools-2.18.tar.gz/coverview -> audiotools-2.19.tar.gz/coverview
Changed
@@ -17,21 +17,18 @@ #along with this program; if not, write to the Free Software #Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import audiotools import sys import os import os.path import cStringIO -import gettext - -gettext.install("audiotools", unicode=True) +import audiotools +import audiotools.text as _ #returns the dimensions of image_surface scaled to match the #display surface size, but without changing its aspect ratio def image_size(display_surface, image_surface): - display_ratio = float(display_surface[0]) / \ - float(display_surface[1]) + display_ratio = float(display_surface[0]) / float(display_surface[1]) image_ratio = float(image_surface[0]) / float(image_surface[1]) @@ -91,9 +88,9 @@ (self.full_pixbuf.get_width(), self.full_pixbuf.get_height())) - if ((self.scaled_thumbnail is None) or - (self.scaled_thumbnail.get_width() != image_width) or - (self.scaled_thumbnail.get_height() != image_height)): + if (((self.scaled_thumbnail is None) or + (self.scaled_thumbnail.get_width() != image_width) or + (self.scaled_thumbnail.get_height() != image_height))): self.scaled_thumbnail = self.full_pixbuf.scale_simple( image_width, image_height, @@ -121,7 +118,8 @@ str, str, str, str) self.list_widget = gtk.TreeView(self.list) - self.list_widget.append_column(gtk.TreeViewColumn( + self.list_widget.append_column( + gtk.TreeViewColumn( None, gtk.CellRendererText(), text=0)) self.list_widget.set_headers_visible(False) list_scroll = gtk.ScrolledWindow() @@ -171,13 +169,13 @@ image_info_bar.pack_start(self.size_bits) image_info_bar.pack_start(gtk.VSeparator(), - expand=False, fill=False, padding=3) + expand=False, fill=False, padding=3) image_info_bar.pack_start(self.size_pixels) image_info_bar.pack_start(gtk.VSeparator(), - expand=False, fill=False, padding=3) + expand=False, fill=False, padding=3) image_info_bar.pack_start(self.bits) image_info_bar.pack_start(gtk.VSeparator(), - expand=False, fill=False, padding=3) + expand=False, fill=False, padding=3) image_info_bar.pack_start(self.type) self.statusbar = gtk.Statusbar() @@ -191,19 +189,18 @@ table.attach(self.statusbar, 0, 2, 5, 6, yoptions=0) self.file_dialog = gtk.FileChooserDialog( - title="Choose an audio file", + title=_.LAB_CHOOSE_FILE, action=gtk.FILE_CHOOSER_ACTION_OPEN, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) self.about_dialog = gtk.AboutDialog() - self.about_dialog.set_program_name("coverview") + self.about_dialog.set_program_name(u"coverview") self.about_dialog.set_version(audiotools.VERSION) - self.about_dialog.set_copyright("(c) Brian Langenberger") - self.about_dialog.set_comments( - "A viewer for displaying images embedded in audio files.") - self.about_dialog.set_website("http://audiotools.sourceforge.net") + self.about_dialog.set_copyright(u"(c) Brian Langenberger") + self.about_dialog.set_comments(_.LAB_COVERVIEW_ABOUT) + self.about_dialog.set_website(_.LAB_AUDIOTOOLS_URL) self.window.add(table) self.window.set_default_size(800, 600) @@ -223,21 +220,21 @@ except audiotools.UnsupportedFile: error = gtk.MessageDialog( type=gtk.MESSAGE_ERROR, - message_format="Unsupported File '%s'" % (filename), + message_format=_.ERR_UNSUPPORTED_FILE % (filename), buttons=gtk.BUTTONS_OK) error.run() error.destroy() except audiotools.InvalidFile: error = gtk.MessageDialog( type=gtk.MESSAGE_ERROR, - message_format="Invalid File '%s'" % (filename), + message_format=_.ERR_INVALID_FILE % (filename), buttons=gtk.BUTTONS_OK) error.run() error.destroy() except IOError: error = gtk.MessageDialog( type=gtk.MESSAGE_ERROR, - message_format="I/O Error Reading '%s'" % (filename), + message_format=_.ERR_OPEN_IOERROR % (filename), buttons=gtk.BUTTONS_OK) error.run() error.destroy() @@ -289,12 +286,16 @@ first_image = None for image in images: tree_iter = self.list.append() - self.list.set(tree_iter, 0, image.type_string()) - self.list.set(tree_iter, 1, get_pixbuf(image.data)) - self.list.set(tree_iter, 2, u"%d bytes" % (len(image.data))) - self.list.set(tree_iter, 3, u"%d \u00D7 %d" % (image.width, - image.height)) - self.list.set(tree_iter, 4, u"%d bits" % (image.color_depth)) + self.list.set(tree_iter, 0, + image.type_string()) + self.list.set(tree_iter, 1, + get_pixbuf(image.data)) + self.list.set(tree_iter, 2, + _.LAB_BYTE_SIZE % (len(image.data))) + self.list.set(tree_iter, 3, + _.LAB_DIMENSIONS % (image.width, image.height)) + self.list.set(tree_iter, 4, + _.LAB_BITS_PER_PIXEL % (image.color_depth)) self.list.set(tree_iter, 5, image.mime_type.decode('ascii')) if (first_image is None): @@ -387,10 +388,11 @@ else: at_image = image.audiotools self.path.config(text=image.path) - self.size_bits.config(text=u"%d bytes" % (len(at_image.data))) - self.size_pixels.config(text=u"%d \u00D7 %d" % (at_image.width, - at_image.height)) - self.bits.config(text="%d bits" % (at_image.color_depth)) + self.size_bits.config(text=_.LAB_BYTE_SIZE % (len(at_image.data))) + self.size_pixels.config(text=_.LAB_DIMENSIONS % (at_image.width, + at_image.height)) + self.bits.config( + text=_.LAB_BITS_PER_PIXEL % (at_image.color_depth)) self.type.config(text=at_image.mime_type) def clear_image_data(self): @@ -406,17 +408,18 @@ Label(self, text="coverview", font=("Helvetica", 48)).pack(side=TOP) - Label(self, - text=("A viewer for displaying images" + - "embedded in audio files.")).pack(side=TOP) + text="Python Audio Tools %s" % + (audiotools.VERSION)).pack(side=TOP) Label(self, - text="Part of Python Audio Tools").pack(side=TOP) - Label(self, - text="Copyright 2008-2012 by Brian Langenberger", + text="(c) by Brian Langenberger", font=("Helvetica", 10)).pack(side=TOP) + Label(self, + text=_.LAB_COVERVIEW_ABOUT).pack(side=TOP) + Label(self, + text=_.LAB_AUDIOTOOLS_URL).pack(side=TOP) - close = Button(self, text="Close") + close = Button(self, text=_.LAB_CLOSE) close.bind(sequence="<ButtonRelease-1>", func=self.close) close.pack(side=BOTTOM) @@ -480,18 +483,18 @@ def open(self, *args): new_file = tkFileDialog.askopenfilename( parent=self.master, - title="select an audio file") + title=_.LAB_CHOOSE_FILE) if (len(new_file) > 0): try: self.set_metadata(new_file, audiotools.open(new_file).get_metadata()) except audiotools.UnsupportedFile: - self.show_error("Unsupported File '%s'" % (new_file)) + self.show_error(_.ERR_UNSUPPORTED_FILE % (new_file)) except audiotools.InvalidFile: - self.show_error("Invalid File '%s'" % (new_file)) + self.show_error(_.ERR_INVALID_FILE % (new_file)) except IOError, err: - self.show_error("I/O Error Reading '%s'" % (new_file)) + self.show_error(_.ERR_OPEN_IOERROR % (new_file)) def quit(self, *args): self.master.quit() @@ -590,7 +593,7 @@ if (__name__ == '__main__'): parser = audiotools.OptionParser( - usage=_(u'%prog [OPTIONS] [track]'), + usage=_.USAGE_COVERVIEW, version="Python Audio Tools %s" % (audiotools.VERSION)) if (GTK_AVAILABLE): @@ -598,14 +601,14 @@ dest="no_gtk", action='store_true', default=False, - help=_(u"don't use PyGTK for GUI")) + help=_.OPT_NO_GTK) if (TKINTER_AVAILABLE): parser.add_option('--no-tkinter', dest="no_tkinter", action='store_true', default=False, - help=_(u"don't use Tkinter for GUI")) + help=_.OPT_NO_TKINTER) (options, args) = parser.parse_args() messenger = audiotools.Messenger("coverview", None) @@ -618,14 +621,16 @@ try: main_gtk(audiotools.open(args[0])) except audiotools.UnsupportedFile: - messenger.error(_(u"Specified file is unsupported")) + messenger.error(_.ERR_UNSUPPORTED_FILE % + (audiotools.Filename(args[0]),)) sys.exit(1) except audiotools.InvalidFile: - messenger.error(_(u"Specified file is invalid")) + messenger.error(_.ERR_INVALID_FILE % + (audiotools.Filename(args[0]),)) sys.exit(1) except IOError: - messenger.error(_(u"Unable to open \"%s\"") % - (messenger.filename(args[0]))) + messenger.error(_.ERR_OPEN_IOERROR % + (audiotools.Filename(args[0]),)) sys.exit(1) else: main_gtk(None) @@ -634,17 +639,19 @@ try: main_tkinter(audiotools.open(args[0])) except audiotools.UnsupportedFile: - messenger.error(_(u"Specified file is unsupported")) + messenger.error(_.ERR_UNSUPPORTED_FILE % + (audiotools.Filename(args[0]),)) sys.exit(1) except audiotools.InvalidFile: - messenger.error(_(u"Specified file is invalid")) + messenger.error(_.ERR_INVALID_FILE % + (audiotools.Filename(args[0]),)) sys.exit(1) except IOError: - messenger.error(_(u"Unable to open \"%s\"") % - (messenger.filename(args[0]))) + messenger.error(_.ERR_OPEN_IOERROR % + (audiotools.Filename(args[0]),)) sys.exit(1) else: main_tkinter(None) else: - messenger.error(_("Neither PyGTK nor Tkinter is available")) + messenger.error(_.ERR_NO_GUI) sys.exit(1)
View file
audiotools-2.18.tar.gz/docs/BUILD -> audiotools-2.19.tar.gz/docs/BUILD
Changed
@@ -7,8 +7,6 @@ http://www.tug.org/texlive/ * sphinx-build http://sphinx.pocoo.org/ -* m4 - http://www.gnu.org/software/m4/ * fig2dev http://www.xfig.org/ * gnuplot
View file
audiotools-2.18.tar.gz/docs/Makefile -> audiotools-2.19.tar.gz/docs/Makefile
Changed
@@ -7,6 +7,7 @@ cdinfo.1 \ cdplay.1 \ coverdump.1 \ +covertag.1 \ coverview.1 \ dvda2track.1 \ dvdainfo.1 \ @@ -30,6 +31,7 @@ cdinfo.xml \ cdplay.xml \ coverdump.xml \ +covertag.xml \ coverview.xml \ dvda2track.xml \ dvdainfo.xml \
View file
audiotools-2.18.tar.gz/docs/audiotools-config.xml -> audiotools-2.19.tar.gz/docs/audiotools-config.xml
Changed
@@ -15,31 +15,10 @@ </description> <options> <option short="h" long="help">show a list of options and exit</option> + <option short="I" long="interactive">edit options interactively</option> <option short="V" long="verbose" arg="verbosity"> The level of output to display. - Choose between 'normal', 'quiet' and 'debug. - </option> - <option long="format" arg="string"> - The format string to use for new filenames. - Template fields are replaced with metadata values when - new tracks are created. All other text is left as-is. - </option> - </options> - <options category="system"> - <option short="c" long="cdrom" name="cdrom"> - the default CD-ROM drive to use - </option> - <option long="cdrom-read-offset" name="samples"> - the CD-ROM read offset, in PCM samples - </option> - <option long="cdrom-write-offset" name="samples"> - the CD-ROM write offset, in PCM samples - </option> - <option long="fs-encoding" name="encoding"> - the encoding of filenames on the system, such as "UTF-8" - </option> - <option long="io-encoding" name="encoding"> - the encoding of text sent to the screen, such as "UTF-8" + Choose between 'normal', 'quiet' and 'debug'. </option> </options> <options category="transcoding"> @@ -54,32 +33,39 @@ The default quality level for a given audio type. For a list of available quality levels, try: -q help </option> + <option long="format" arg="string"> + The format string to use for new filenames. + Template fields are replaced with metadata values when + new tracks are created. All other text is left as-is. + </option> <option short="j" long="joint" name="processes"> the default maximum number of processes to execute at one time </option> - </options> - <options category="thumbnail"> - <option long="thumbnail-format" arg="type"> - the format to use for thumbnails, such as "jpeg" or "png" - </option> - <option long="thumbnail-size" arg="int"> - The maximum size of each thumbnail, in pixels squared. - For example, specifying 100 results in thumbnails a maximum of - 100x100 pixels large. + <option long="replay-gain" arg="yes/no"> + whether to add ReplayGain metadata by default </option> </options> - <options category="freedb"> - <option long="use-freedb" arg="yes/no"> - whether to use the FreeDB service for CD lookups by default + <options category="id3"> + <option long="id3v2-version" arg="version"> + Which version to use for newly created ID3v2 tags. + Choose from "id3v2.2", "id3v2.3", "id3v2.4", or "none". + All three ID3v2 tag versions are roughly equivalent in terms + of supported features, but "id3v2.2" and "id3v2.3" have + the widest level of support. + If "none" is given, new MP3 files will not be given ID3v2 tags. </option> - <option long="freedb-server" arg="hostname"> - the default FreeDB server to use + <option long="id3v2-pad" arg="true / false"> + Whether to pad ID3v2 numerical fields to 2 digits. + For example, if "true", track number 1 will be stored as "01" + in ID3v2 tags. If false, it will be stored as "1". </option> - <option long="freedb-port" arg="port"> - the default FreeDB port number to use + <option long="id3v1-version" arg="version"> + Which version to use for newly created ID3v1 tags. + Choose from "id3v1.1" or "none". + If "none" is given, new MP3 files will not be given ID3v1 tags. </option> </options> - <options category="musicbrainz"> + <options category="CD lookup"> <option long="use-musicbrainz" arg="yes/no"> whether to use the MusicBrainz service for CD lookups by default </option> @@ -89,30 +75,31 @@ <option long="musicbrainz-port" arg="port"> the default MusicBrainz port number to use </option> - </options> - <options category="id3"> - <option long="id3v2-version" arg="version"> - Which version to use for newly created ID3v2 tags. - Choose from "id3v2.2", "id3v2.3", "id3v2.4", or "none". - All three ID3v2 tag versions are roughly equivalent in terms - of supported features, but "id3v2.2" and "id3v2.3" have - the widest level of support. - If "none" is given, new MP3 files will not be given ID3v2 tags. + <option long="use-freedb" arg="yes/no"> + whether to use the FreeDB service for CD lookups by default </option> - <option long="id3v1-version" arg="version"> - Which version to use for newly created ID3v1 tags. - Choose from "id3v1.1" or "none". - If "none" is given, new MP3 files will not be given ID3v1 tags. + <option long="freedb-server" arg="hostname"> + the default FreeDB server to use </option> - <option long="id3v2-pad" arg="true / false"> - Whether to pad ID3v2 numerical fields to 2 digits. - For example, if "true", track number 1 will be stored as "01" - in ID3v2 tags. If false, it will be stored as "1". + <option long="freedb-port" arg="port"> + the default FreeDB port number to use </option> </options> - <options category="ReplayGain"> - <option long="replay-gain" arg="yes/no"> - whether to add ReplayGain metadata by default + <options category="system"> + <option short="c" long="cdrom" name="cdrom"> + the default CD-ROM drive to use + </option> + <option long="cdrom-read-offset" name="samples"> + the CD-ROM read offset, in PCM samples + </option> + <option long="cdrom-write-offset" name="samples"> + the CD-ROM write offset, in PCM samples + </option> + <option long="fs-encoding" name="encoding"> + the encoding of filenames on the system, such as "UTF-8" + </option> + <option long="io-encoding" name="encoding"> + the encoding of text sent to the screen, such as "UTF-8" </option> </options> <options category="binaries">
View file
audiotools-2.18.tar.gz/docs/cd2track.xml -> audiotools-2.19.tar.gz/docs/cd2track.xml
Changed
@@ -14,9 +14,9 @@ </description> <options> <option short="h" long="help">show a list of options and exit</option> - <option short="V" long="verbose" arg="verbosity"> - The level of output to display. - Choose between 'normal', 'quiet' and 'debug. + <option short="I" long="interactive"> + edit metadata and encoding options in interactive mode + prior to splitting file </option> <option short="c" long="cdrom" arg="cdrom"> the CD-ROM device to extract audio from @@ -24,6 +24,10 @@ <option short="s" long="speed" arg="speed"> the speed to extract audio data at </option> + <option short="V" long="verbose" arg="verbosity"> + The level of output to display. + Choose between 'normal', 'quiet' and 'debug'. + </option> </options> <options category="extraction"> <option short="t" long="type" arg="type"> @@ -66,9 +70,6 @@ <option long="no-freedb"> don't query FreeDB for metadata </option> - <option short="I" long="interactive"> - edit metadata in interactive mode prior to splitting file - </option> <option short="D" long="default"> When multiple metadata choices are available, select the first one automatically.
View file
audiotools-2.18.tar.gz/docs/cdinfo.xml -> audiotools-2.19.tar.gz/docs/cdinfo.xml
Changed
@@ -15,10 +15,6 @@ </description> <options> <option short="h" long="help">show a list of options and exit</option> - <option short="V" long="verbose" arg="verbosity"> - The level of output to display. - Choose between 'normal', 'quiet' and 'debug. - </option> <option short="c" long="cdrom" arg="cdrom"> the CD-ROM device to read CD information from </option>
View file
audiotools-2.18.tar.gz/docs/cdplay.xml -> audiotools-2.19.tar.gz/docs/cdplay.xml
Changed
@@ -12,10 +12,6 @@ </description> <options> <option short="h" long="help">show a list of options and exit</option> - <option short="V" long="verbose" arg="verbosity"> - The level of output to display. - Choose between 'normal', 'quiet' and 'debug. - </option> <option short="I" long="interactive"> run in interactive mode; this provides a text-based GUI and allows one to operate @@ -24,16 +20,16 @@ <option short="o" long="output" arg="system"> the output system to use, such as "PulseAudio" or "OSS" </option> - <option short="x" long="XMCD" arg="filename"> - FreeDB XMCD or MusicBrainz XML metadata file for displaying - disc information in interactive mode - </option> <option short="c" long="cdrom" arg="cdrom"> the CD-ROM device to play audio from </option> <option long="shuffle"> shuffle tracks randomly before playback </option> + <option short="V" long="verbose" arg="verbosity"> + The level of output to display. + Choose between 'normal', 'quiet' and 'debug. + </option> </options> <options category="CD lookup"> <option long="musicbrainz-server" arg="hostname">
View file
audiotools-2.18.tar.gz/docs/coverdump.xml -> audiotools-2.19.tar.gz/docs/coverdump.xml
Changed
@@ -12,10 +12,6 @@ </description> <options> <option short="h" long="help">show a list of options and exit</option> - <option short="V" long="verbose" arg="verbosity"> - The level of output to display. - Choose between 'normal', 'quiet' and 'debug. - </option> <option short="d" long="dir" arg="directory"> the target directory for the extracted image files; if none is given, the current working directory is used @@ -23,6 +19,10 @@ <option short="p" long="prefix" arg="prefix"> a string to prefix to each extracted image file </option> + <option short="V" long="verbose" arg="verbosity"> + The level of output to display. + Choose between 'normal', 'quiet' and 'debug'. + </option> </options> <examples> <example>
View file
audiotools-2.19.tar.gz/docs/covertag.xml
Added
@@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="utf-8"?> +<manpage> + <utility>covertag</utility> + <author>Brian Langenberger</author> + <section>1</section> + <name>update audio file image metadata</name> + <title>Audio File Image Tagger</title> + <synopsis>[OPTIONS] <track 1> [track 2] ...</synopsis> + <description> + covertag takes image files and a list of audio files + and updates those files with the new artwork. + </description> + <options> + <option short="h" long="help">show a list of options and exit</option> + <option short="r" long="replace"> + this options erases all existing images and replaces them + with any specified files + </option> + <option short="V" long="verbose" arg="verbosity"> + The level of output to display. + Choose between 'normal', 'quiet' and 'debug'. + </option> + </options> + <options category="image"> + <option long="front-cover" arg="filename"> + an image file of the album's front cover + </option> + <option long="back-cover" arg="filename"> + an image file of the album's back cover + </option> + <option long="leaflet" arg="filename"> + an image file of one of the album's leaflet pages + </option> + <option long="media" arg="filename"> + an image file of the album's media + </option> + <option long="other-image" arg="filename"> + an image file related to the album + </option> + </options> + <examples> + <example> + <description> + Replace all images in track.mp3 with a PNG file + </description> + <command> + covertag -r --front-cover=front.png track.mp3 + </command> + </example> + <example> + <description> + Add several JPEG images to track.flac + </description> + <command> + covertag --front-cover=front.jpg --back-cover=back.jpg + --leaflet=page1.jpg --leaflet=page2.jpg --leaflet=page3.jpg + track.flac + </command> + </example> + <example> + <description> + Remove all cover art from track.wv + </description> + <command> + covertag -r track.wv + </command> + </example> + </examples> +</manpage>
View file
audiotools-2.18.tar.gz/docs/coverview.xml -> audiotools-2.19.tar.gz/docs/coverview.xml
Changed
@@ -11,10 +11,6 @@ </description> <options> <option short="h" long="help">show a list of options and exit</option> - <option short="V" long="verbose" arg="verbosity"> - The level of output to display. - Choose between 'normal', 'quiet' and 'debug. - </option> <option long="no-gtk"> disable PyGTK-based GUI, if available </option>
View file
audiotools-2.18.tar.gz/docs/dvda2track.xml -> audiotools-2.19.tar.gz/docs/dvda2track.xml
Changed
@@ -14,9 +14,9 @@ </description> <options> <option short="h" long="help">show a list of options and exit</option> - <option short="V" long="verbose" arg="verbosity"> - The level of output to display. - Choose between 'normal', 'quiet' and 'debug. + <option short="I" long="interactive"> + edit metadata and encoding options in interactive mode + prior to extracting files </option> <option short="c" long="cdrom" arg="cdrom"> the DVD-ROM device to extract audio from @@ -28,6 +28,10 @@ the title number to retrieve tracks from, starting from 1; the number of titles can be determined from the dvdainfo(1) utility </option> + <option short="V" long="verbose" arg="verbosity"> + The level of output to display. + Choose between 'normal', 'quiet' and 'debug'. + </option> </options> <options category="extraction"> <option short="t" long="type" arg="type"> @@ -70,9 +74,6 @@ <option long="no-freedb"> don't query FreeDB for metadata </option> - <option short="I" long="interactive"> - edit metadata in interactive mode prior to splitting file - </option> <option short="D" long="default"> When multiple metadata choices are available, select the first one automatically.
View file
audiotools-2.18.tar.gz/docs/programming/source/audiotools.rst -> audiotools-2.19.tar.gz/docs/programming/source/audiotools.rst
Changed
@@ -33,6 +33,7 @@ MP3Audio MPEG-1 Layer 3 MP2Audio MPEG-1 Layer 2 OggFlacAudio Ogg Free Lossless Audio Codec + OpusAudio Opus Audio Codec ShortenAudio Shorten SpeexAudio Ogg Speex VorbisAudio Ogg Vorbis @@ -104,6 +105,16 @@ this is set to the user's CPU count. If neither is available, this is set to 1. +.. function:: file_type(file) + + Given a seekable file object rewound to the file's start, + returns an :class:`AudioFile`-compatible class of the stream's + detected type, or ``None`` if the stream's type is unknown. + + The :class:`AudioFile` class may not be available for use + and so its :meth:`AudioFile.has_binaries` classmethod + may need to be checked separately. + .. function:: open(filename) Opens the given filename string and returns an :class:`AudioFile`-compatible @@ -112,17 +123,30 @@ not supported. Raises :exc:`IOError` if the file cannot be opened at all. -.. function:: open_files(filenames[, sorted[, messenger]]) +.. function:: open_files(filenames[, sorted][, messenger][, no_duplicates][, warn_duplicates][, opened_files]) Given a list of filename strings, returns a list of - :class:`AudioFile`-compatible objects which can be successfully opened. + :class:`AudioFile`-compatible objects which are successfully opened. By default, they are returned sorted by album number and track number. + If ``sorted`` is ``False``, they are returned in the same order as they appear in the filenames list. + If ``messenger`` is given, use that :class:`Messenger` object to for warnings if files cannot be opened. Otherwise, such warnings are sent to stdout. + If ``no_duplicates`` is ``True``, attempting to open + the same file twice raises a :exc:`DuplicateFile` exception. + + If ``no_duplicates`` is ``False`` and ``warn_duplicates`` is ``True``, + attempting to open the same file twice results in a + warning to ``messenger``, if present. + + ``opened_files``, if present, is a set of previously opened + :class:`Filename` objects for the purpose of detecting duplicates. + Any opened files are added to that set. + .. function:: open_directory(directory[, sorted[, messenger]]) Given a root directory, returns an iterator of all the @@ -153,9 +177,9 @@ ``from_function`` with an integer argument (presumably a string) until that object's length is 0. - >>> infile = open("input.txt","r") - >>> outfile = open("output.txt","w") - >>> transfer_data(infile.read,outfile.write) + >>> infile = open("input.txt", "r") + >>> outfile = open("output.txt", "w") + >>> transfer_data(infile.read, outfile.write) >>> infile.close() >>> outfile.close() @@ -203,13 +227,6 @@ each limited to the given lengths. The original pcmreader is closed upon the iterator's completion. -.. function:: applicable_replay_gain(audiofiles) - - Takes a list of :class:`AudioFile`-compatible objects. - Returns ``True`` if ReplayGain can be applied to those files - based on their sample rate, number of channels, and so forth. - Returns ``False`` if not. - .. function:: calculate_replay_gain(audiofiles) Takes a list of :class:`AudioFile`-compatible objects. @@ -239,6 +256,46 @@ If ``progress`` is ``None``, the audiofile's PCM stream is returned as-is. +Filename Objects +---------------- + +.. class:: Filename(filename) + + :class:`Filename` is a file which may or may not exist on disk. + ``filename`` is a raw string of the actual filename. + Filename objects are immutable and hashable, + which means they can be used as dictionary keys + or placed in sets. + + The purpose of Filename objects is for easier + conversion of raw string filename paths to Unicode, + and to make it easier to detect filenames + which point to the same file on disk. + + The former case is used by utilities to display + output about file operations in progress. + The latter case is for utilities + which need to avoid overwriting input files + with output files. + +.. function:: Filename.__str__() + + Returns the raw string of the actual filename after + being normalized. + +.. function:: Filename.__unicode__() + + Returns a Unicode string of the filename after being decoded + through :attr:`FS_ENCODING`. + +.. function:: Filename.__eq__(filename) + + Filename objects which exist on disk hash and compare equally + if their device ID and inode number values match + (the ``st_dev`` and ``st_ino`` fields according to stat(2)). + Filename objects which don't exist on disk hash and compare + equally if their filename string matches. + AudioFile Objects ----------------- @@ -264,6 +321,11 @@ function for inferring the file format from its name. However, it need not be unique among all formats. +.. attribute:: AudioFile.DESCRIPTION + + A longer, descriptive name for the audio type as a Unicode string. + This is meant to be human-readable. + .. attribute:: AudioFile.COMPRESSION_MODES A tuple of valid compression level strings, for use with the @@ -279,7 +341,7 @@ .. attribute:: AudioFile.COMPRESSION_DESCRIPTIONS - A dict of compression descriptions, as unicode strings. + A dict of compression descriptions, as Unicode strings. The key is a valid compression mode string. Not all compression modes need have a description; some may be left blank. @@ -298,14 +360,6 @@ This tuple may be empty if the format requires no binaries or has no ReplayGain support. -.. classmethod:: AudioFile.is_type(file) - - Takes a file-like object with :meth:`read` and :meth:`seek` methods - that's reset to the beginning of the stream. - Returns ``True`` if the file is determined to be of the same type - as this particular :class:`AudioFile` implementation. - Returns ``False`` if not. - .. method:: AudioFile.bits_per_sample() Returns the number of bits-per-sample in this audio file as a positive @@ -541,11 +595,9 @@ that is called as needed during ReplayGain application to indicate progress - identical to the argument used by :meth:`convert`. -.. classmethod:: AudioFile.can_add_replay_gain() +.. classmethod:: AudioFile.supports_replay_gain() - Returns ``True`` if this audio class supports ReplayGain - and we have the necessary binaries to apply it. - Returns ``False`` if not. + Returns ``True`` if this class supports ReplayGain metadata. .. classmethod:: AudioFile.lossless_replay_gain() @@ -554,6 +606,12 @@ Returns ``False`` if applying metadata modifies the audio file data itself. +.. classmethod:: AudioFile.can_add_replay_gain(audiofiles) + + Given a list of :class:`AudioFile` objects, + returns ``True`` if this class can run :meth:`AudioFile.add_replay_gain` + on those objects, ``False`` if not. + .. method:: AudioFile.replay_gain() Returns this audio file's ReplayGain values as a @@ -578,7 +636,7 @@ Cleans the audio file of known data and metadata problems. ``fixes_performed`` is a list-like object which is appended - with unicode strings of the fixes performed. + with Unicode strings of the fixes performed. ``output_filename`` is an optional string in which the fixed audio file is placed. @@ -607,27 +665,32 @@ .. class:: WaveContainer -.. method:: WaveContainer.to_wave(wave_filename[, progress]) +.. method:: WaveContainer.has_foreign_wave_chunks() + + Returns ``True`` if our object has non-audio RIFF WAVE chunks. - Creates a Wave file with the given filename string - from our data, with any stored chunks intact. - ``progress``, if given, functions identically to the - :meth:`AudioFile.convert` method. - May raise :exc:`EndodingError` if some problem occurs during encoding. +.. method:: WaveContainer.wave_header_footer() -.. classmethod:: WaveContainer.from_wave(filename, wave_filename[, compression[, progress]]) + Returns ``(header, footer)`` tuple of strings + where ``header`` is everything before the PCM data + and ``footer`` is everything after the PCM data. - Like :meth:`AudioFile.from_pcm`, creates a file with our class - at the given ``filename`` string, from the given ``wave_filename`` - string and returns a new object of our class. - ``compression`` is an optional compression level string - and ``progress`` functions identically to that of - :meth:`AudioFile.convert`. - May raise :exc:`EndodingError` if some problem occurs during encoding. + May raise :exc:`ValueError` if there's a problem + with the header or footer data, such as invalid chunk IDs. + May raise :exc:`IOError` if there's a problem + reading the header or footer data from the file. -.. method:: WaveContainer.has_foreign_riff_chunks() +.. classmethod:: WaveContainer.from_wave(filename, header, pcmreader, footer[, compression]) - Returns ``True`` if our object has non-audio RIFF WAVE chunks. + Encodes a new file from wave data. + ``header`` and ``footer`` are binary strings as returned by a + :meth:`WaveContainer.wave_header_footer` method, + ``pcmreader`` is a :class:`PCMReader` object + and ``compression`` is a binary string. + + Returns a new :class:`AudioFile`-compatible object + or raises :exc:`EncodingError` if some error occurs when + encoding the file. AiffContainer Objects ^^^^^^^^^^^^^^^^^^^^^ @@ -642,32 +705,37 @@ .. class:: AiffContainer -.. method:: AiffContainer.to_aiff(aiff_filename[, progress]) +.. method:: AiffContainer.has_foreign_aiff_chunks() - Creates an AIFF file with the given filename string - from our data, with any stored chunks intact. - ``progress``, if given, functions identically to the - :meth:`AudioFile.convert` method. - May raise :exc:`EndodingError` if some problem occurs during encoding. + Returns ``True`` if our object has non-audio AIFF chunks. -.. classmethod:: AiffContainer.from_aiff(filename, aiff_filename[, compression[, progress]]) +.. method:: AiffContainer.aiff_header_footer() - Like :meth:`AudioFile.from_pcm`, creates a file with our class - at the given ``filename`` string, from the given ``aiff_filename`` - string and returns a new object of our class. - ``compression`` is an optional compression level string - and ``progress`` functions identically to that of - :meth:`AudioFile.convert`. - May raise :exc:`EndodingError` if some problem occurs during encoding. + Returns ``(header, footer)`` tuple of strings + where ``header`` is everything before the PCM data + and ``footer`` is everything after the PCM data. -.. method:: AiffContainer.has_foreign_aiff_chunks() + May raise :exc:`ValueError` if there's a problem + with the header or footer data, such as invalid chunk IDs. + May raise :exc:`IOError` if there's a problem + reading the header or footer data from the file. - Returns ``True`` if our object has non-audio AIFF chunks. +.. classmethod:: AiffContainer.from_aiff(filename, header, pcmreader, footer[, compression]) + + Encodes a new file from wave data. + ``header`` and ``footer`` are binary strings as returned by a + :meth:`AiffContainer.aiff_header_footer` method, + ``pcmreader`` is a :class:`PCMReader` object + and ``compression`` is a binary string. + + Returns a new :class:`AudioFile`-compatible object + or raises :exc:`EncodingError` if some error occurs when + encoding the file. MetaData Objects ---------------- -.. class:: MetaData([track_name[, track_number[, track_total[, album_name[, artist_name[, performer_name[, composer_name[, conductor_name[, media[, ISRC[, catalog[, copyright[, publisher[, year[, data[, album_number[, album_total[, comment[, images]]]]]]]]]]]]]]]]]]]) +.. class:: MetaData([track_name][, track_number][, track_total][, album_name][, artist_name][, performer_name][, composer_name][, conductor_name][, media][, ISRC][, catalog][, copyright][, publisher][, year][, data][, album_number][, album_total][, comment][, images]) The :class:`MetaData` class represents an :class:`AudioFile`'s non-technical metadata. @@ -685,6 +753,43 @@ The ``images`` argument, if given, should be an iterable collection of :class:`Image`-compatible objects. + MetaData attributes may be ``None``, + which indicates the low-level implementation has + no corresponding entry. + For instance, ID3v2.3 tags use the ``"TALB"`` frame + to indicate the track's album name. + If that frame is present, an :class:`audiotools.ID3v23Comment` + MetaData object will have an ``album_name`` field containing + a Unicode string of its value. + If that frame is not present in the ID3v2.3 tag, + its ``album_name`` field will be ``None``. + + For example, to access a track's album name field: + + >>> metadata = track.get_metadata() + >>> metadata.album_name + u"Album Name" + + To change a track's album name field: + + >>> metadata = track.get_metadata() + >>> metadata.album_name = u"Updated Album Name" + >>> track.update_metadata(metadata) # because metadata comes from track's get_metadata() method, one can use update_metadata() + + To delete a track's album name field: + + >>> metadata = track.get_metadata() + >>> del(metadata.album_name) + >>> track.update_metadata(metadata) + + Or to replace a track's entire set of metadata: + + >>> metadata = MetaData(track_name=u"Track Name", + ... album_name=u"Updated Album Name", + ... track_number=1, + ... track_total=3) + >>> track.set_metadata(metadata) # because metadata is built from scratch, one must use set_metadata() + .. data:: MetaData.track_name This individual track's name as a Unicode string. @@ -758,18 +863,12 @@ .. method:: MetaData.filled_fields() Yields an ``(attr, value)`` tuple per non-blank :class:`MetaData` field. - Non-blank fields are text fields with a length greater than 0, - or numerical fields with a value greater than 0. - Note that text fields containing nothing but whitespace - are treated as non-blank. + Non-blank fields are those with a value other than ``None``. .. method:: MetaData.empty_fields() Yields an ``(attr, value)`` tuple per blank :class:`MetaData` field. - Blank fields are text fields with a length equal to 0, - or numerical fields with a value equal to 0. - Note that text fields containing nothing but whitespace - are treated as non-blank. + Blank fields are those with a value of ``None``. .. classmethod:: MetaData.converted(metadata) @@ -850,11 +949,11 @@ ``fixes_performed`` is a list object with an append method. Text descriptions of the fixes performed are appended - to that list as unicode strings. + to that list as Unicode strings. .. method:: MetaData.raw_info() - Returns a unicode string of raw metadata information + Returns a Unicode string of raw metadata information with as little filtering as possible. This is meant to be useful for debugging purposes. @@ -873,114 +972,6 @@ Returns a single :class:`MetaData` object containing all the fields that are consistent across this object's collection of MetaData. -AlbumMetaDataFile Objects -------------------------- - -.. deprecated:: 2.18 - Use :func:`metadata_lookup` instead. - -.. class:: AlbumMetaDataFile(album_name, artist_name, year, catalog, extra, track_metadata) - - This is an abstract parent class to :class:`audiotools.XMCD` and - :class:`audiotools.MusicBrainzReleaseXML`. - It represents a collection of album metadata as generated - by the FreeDB or MusicBrainz services. - Modifying fields within an :class:`AlbumMetaDataFile`-compatible - object will modify its underlying representation and those - changes will be present when :meth:`to_string` is called - on the updated object. - Note that :class:`audiotools.XMCD` doesn't support the `catalog` - field while :class:`audiotools.MusicBrainzReleaseXML` doesn't - support the `extra` fields. - -.. data:: AlbumMetaDataFile.album_name - - The album's name as a Unicode string. - -.. data:: AlbumMetaDataFile.artist_name - - The album's artist's name as a Unicode string. - -.. data:: AlbumMetaDataFile.year - - The album's release year as a Unicode string. - -.. data:: AlbumMetaDataFile.catalog - - The album's catalog number as a Unicode string. - -.. data:: AlbumMetaDataFile.extra - - The album's extra information as a Unicode string. - -.. method:: AlbumMetaDataFile.__len__() - - The total number of tracks on the album. - -.. method:: AlbumMetaDataFile.to_string() - - Returns the on-disk representation of the file as a binary string. - -.. classmethod:: AlbumMetaDataFile.from_string(string) - - Given a binary string, returns an :class:`AlbumMetaDataFile` object - of the same class. - Raises :exc:`MetaDataFileException` if a parsing error occurs. - -.. method:: AlbumMetaDataFile.get_track(index) - - Given a track index (starting from 0), returns a - (`track_name`, `track_artist`, `track_extra`) tuple of Unicode strings. - Raises :exc:`IndexError` if the requested track is out-of-bounds. - -.. method:: AlbumMetaDataFile.set_track(index, track_name, track_artist, track_extra) - - Given a track index (starting from 0) and a set of Unicode strings, - sets the appropriate track information. - Raises :exc:`IndexError` if the requested track is out-of-bounds. - -.. classmethod:: AlbumMetaDataFile.from_tracks(tracks) - - Given a set of :class:`AudioFile` objects, returns an - :class:`AlbumMetaDataFile` object of the same class. - All files are presumed to be from the same album. - -.. classmethod:: AlbumMetaDataFile.from_cuesheet(cuesheet, total_frames, sample_rate[, metadata]) - - Given a Cuesheet-compatible object with :meth:`catalog`, - :meth:`IRSCs`, :meth:`indexes` and :meth:`pcm_lengths` methods; - `total_frames` and `sample_rate` integers; and an optional - :class:`MetaData` object of the entire album's metadata, - returns an :class:`AlbumMetaDataFile` object of the same class - constructed from that data. - -.. method:: AlbumMetaDataFile.track_metadata(track_number) - - Given a `track_number` (starting from 1), returns a - :class:`MetaData` object of that track's metadata. - - Raises :exc:`IndexError` if the track is out-of-bounds. - -.. method:: AlbumMetaDataFile.get(track_number, default) - - Given a `track_number` (starting from 1), returns a - :class:`MetaData` object of that track's metadata, - or returns `default` if that track is not present. - -.. method:: AlbumMetaDataFile.track_metadatas() - - Returns an iterator over all the :class:`MetaData` objects - in this file. - -.. method:: AlbumMetaDataFile.metadata() - - Returns a single :class:`MetaData` object of all consistent fields - in this file. - For example, if `album_name` is the same in all MetaData objects, - the returned object will have that `album_name` value. - If `track_name` differs, the returned object have a blank - `track_name` field. - Image Objects ------------- @@ -1051,12 +1042,6 @@ Raises :exc:`InvalidImage` If unable to determine the image type from the data string. -.. method:: Image.thumbnail(width, height, format) - - Given width and height integers and a format string (such as ``"JPEG"``) - returns a new :class:`Image` object resized to those dimensions - while retaining its original aspect ratio. - ReplayGain Objects ------------------ @@ -1115,14 +1100,19 @@ The number of bits-per-sample in this audio stream as a positive integer. -.. method:: PCMReader.read(bytes) +.. method:: PCMReader.read(pcm_frames) - Try to read a :class:`pcm.FrameList` object of size ``bytes``, if possible. - This method is *not* guaranteed to read that amount of bytes. + Try to read a :class:`pcm.FrameList` object with the given + number of PCM frames, if possible. + This method is *not* guaranteed to read that amount of frames. It may return less, particularly at the end of an audio stream. It may even return FrameLists larger than requested. However, it must always return a non-empty FrameList until the end of the PCM stream is reached. + + Once the end of the stream is reached, subsequent calls + will return empty FrameLists. + May raise :exc:`IOError` if there is a problem reading the source file, or :exc:`ValueError` if the source file has some sort of error. @@ -1132,6 +1122,10 @@ Closes the audio stream. If any subprocesses were used for audio decoding, they will also be closed and waited for their process to finish. + + Subsequent calls to :meth:`PCMReader.read` will + raise :exc:`ValueError` exceptions once the stream is closed. + May raise a :exc:`DecodingError`, typically indicating that a helper subprocess used for decoding has exited with an error. @@ -1192,6 +1186,19 @@ This method functions the same as the :meth:`PCMReader.close` method. +RemaskedPCMReader Objects +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. class:: RemaskedPCMReader(pcmreader, channel_count, channel_mask) + + This class wraps around an existing :class:`PCMReader` object + and constructs a new :class:`PCMReader` with the given + channel count and mask. + + Channels common to ``pcmreader`` and the given channel mask + are output by calls to :meth:`RemaskedPCMReader.read` + while missing channels are populated with silence. + BufferedPCMReader Objects ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1200,7 +1207,7 @@ This class wraps around an existing :class:`PCMReader` object. Its calls to :meth:`read` are guaranteed to return :class:`pcm.FrameList` objects as close to the requested amount - of bytes as possible without going over by buffering data + of PCM frames as possible without going over by buffering data internally. The reason such behavior is not required is that we often @@ -1209,6 +1216,23 @@ But on occasions when we need :class:`pcm.FrameList` objects to be of a particular size, this class can accomplish that. +CounterPCMReader Objects +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. class:: CounterPCMReader(pcmreader) + + This class wraps around an existing :class:`PCMReader` object + and keeps track of the number of bytes and frames written + upon each call to ``read``. + +.. attribute:: CounterPCMReader.frames_written + + The number of PCM frames written thus far. + +.. method:: CounterPCMReader.bytes_written() + + The number of bytes written thus far. + ReorderedPCMReader Objects ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1232,16 +1256,12 @@ .. class:: PCMCat(pcmreaders) - This class wraps around an iterable group of :class:`PCMReader` objects + This class wraps around a list of :class:`PCMReader` objects and concatenates their output into a single output stream. -.. warning:: - - :class:`PCMCat` does not check that its input :class:`PCMReader` objects - all have the same sample rate, channels, channel mask or bits-per-sample. - Mixing incompatible readers is likely to trigger undesirable behavior - from any sort of processing - which often assumes data will be in a - consistent format. + If any of the readers has different attributes + from the first reader in the stream, :exc:`ValueError` is raised + at init-time. PCMReaderWindow Objects ^^^^^^^^^^^^^^^^^^^^^^^ @@ -1528,9 +1548,10 @@ This class is used to access a DVD-Audio. It contains a collection of titlesets. - Each titleset contains a list of :class:`DVDATitle` objects, - and each :class:`DVDATitle` contains a list of - :class:`DVDATrack` objects. + Each titleset contains a list of + :class:`audiotools.dvda.DVDATitle` objects, + and each :class:`audiotools.dvda.DVDATitle` contains a list of + :class:`audiotools.dvda.DVDATrack` objects. ``audio_ts_path`` is the path to the DVD-Audio's ``AUDIO_TS`` directory, such as ``/media/cdrom/AUDIO_TS``. ``device`` is the path to the DVD-Audio's mount device, @@ -1549,122 +1570,6 @@ performed automatically if supported on the user's platform. Otherwise, the files are assumed to be unprotected. -DVDATitle Objects -^^^^^^^^^^^^^^^^^ - -.. class:: DVDATitle(dvdaudio, titleset, title, pts_length, tracks) - - This class represents a single DVD-Audio title. - ``dvdaudio`` is a :class:`DVDAudio` object. - ``titleset`` and ``title`` are integers indicating - this title's position in the DVD-Audio - both offset from 0. - ``pts_length`` is the the total length of the title in - PTS ticks (there are 90000 PTS ticks per second). - ``tracks`` is a list of :class:`DVDATrack` objects. - - It is rarely instantiated directly; one usually - retrieves titles from the parent :class:`DVDAudio` object. - -.. data:: DVDATitle.dvdaudio - - The parent :class:`DVDAudio` object. - -.. data:: DVDATitle.titleset - - An integer of this title's titleset, offset from 0. - -.. data:: DVDATitle.title - - An integer of this title's position within the titleset, offset from 0. - -.. data:: DVDATitle.pts_length - - The length of this title in PTS ticks. - -.. data:: DVDATitle.tracks - - A list of :class:`DVDATrack` objects. - -.. method:: DVDATitle.info() - - Returns a (``sample_rate``, ``channels``, ``channel_mask``, - ``bits_per_sample``, ``type``) tuple of integers. - ``type`` is ``0xA0`` if the title is a PCM stream, - or ``0xA1`` if the title is an MLP stream. - -.. method:: DVDATitle.to_pcm() - - Returns a :class:`PCMReader`-compatible object of this title's - entire data stream, which must be split into tracks - using its ``next_track()`` method to indicate the length of - the next track in the title. - - >>> dvda = DVDAudio("/dev/cdrom") - >>> title = dvda[0][0] # get the first title from the first titleset - >>> pcm = title.to_pcm() - >>> for track in title: - ... pcm.next_track(track.pts_length) - ... extracted = AudioFile.from_pcm(path, pcm) - - -DVDATrack Objects -^^^^^^^^^^^^^^^^^ - -.. class:: DVDATrack(dvdaudio, titleset, title, track, first_pts, pts_length, first_sector, last_sector) - - This class represents a single DVD-Audio track. - ``dvdaudio`` is a :class:`DVDAudio` object. - ``titleset``, ``title`` and ``track`` are integers indicating - this track's position in the DVD-Audio - all offset from 0. - ``first_pts`` is the track's first PTS value. - ``pts_length`` is the the total length of the track in PTS ticks. - ``first_sector`` and ``last_sector`` indicate the range of - sectors this track occupies. - - It is also rarely instantiated directly; - one usually retrieves tracks from the parent - :class:`DVDATitle` object. - -.. data:: DVDATrack.dvdaudio - - The parent :class:`DVDAudio` object. - -.. data:: DVDATrack.titleset - - An integer of this tracks's titleset, offset from 0. - -.. data:: DVDATrack.title - - An integer of this track's position within the titleset, offset from 0. - -.. data:: DVDATrack.track - - An integer of this track's position within the title, offset from 0. - -.. data:: DVDATrack.first_pts - - The track's first PTS index. - -.. data:: DVDATrack.pts_length - - The length of this track in PTS ticks. - -.. data:: DVDATrack.first_sector - - The first sector this track occupies. - -.. warning:: - - The track is *not* guaranteed to start at the beginning of - its first sector. - Although it begins within that sector, the track's start may be - offset some arbitrary number of bytes from the sector's start. - -.. data:: DVDATrack.last_sector - - The last sector this track occupies. - - ExecQueue Objects ----------------- @@ -1723,7 +1628,7 @@ This class runs multiple jobs in parallel and displays their progress output to the given :class:`ProgressDisplay` object. - The optional ``total_progress_message`` argument is a unicode string + The optional ``total_progress_message`` argument is a Unicode string which displays an additional progress bar of the queue's total progress. .. attribute:: ExecProgressQueue.results @@ -1751,14 +1656,14 @@ The executed function can then call that ``progress`` function at regular intervals to indicate its progress. - If given, ``progress_text`` is a unicode string to be displayed + If given, ``progress_text`` is a Unicode string to be displayed while the function is being executed. ``completion_output`` is displayed once the executed function is completed. - It can be either a unicode string or a function whose argument + It can be either a Unicode string or a function whose argument is the returned result of the executed function and which must - output either a unicode string or ``None``. + output either a Unicode string or ``None``. If ``None``, no output text is generated for the completed job. .. method:: ExecProgressQueue.run([max_processes]) @@ -1884,10 +1789,6 @@ >>> m.usage(u"<arg1> <arg2> <arg3>") *** Usage: audiotools <arg1> <arg2> <arg3> -.. method:: Messenger.filename(string) - - Takes a raw filename string and converts it to a Unicode string. - .. method:: Messenger.new_row() This method begins the process of creating aligned table data output. @@ -2058,7 +1959,7 @@ Adds a row of output to be displayed with progress indicated. ``row_id`` should be a unique identifier, typically an int. - ``output_line`` should be a unicode string indicating what + ``output_line`` should be a Unicode string indicating what we're displaying the progress of. .. method:: ProgressDisplay.update_row(row_id, current, total) @@ -2090,7 +1991,7 @@ This is a subclass of :class:`ProgressDisplay` used for generating only a single line of progress output. - As such, one only specifies a single row of unicode ``progress_text`` + As such, one only specifies a single row of Unicode ``progress_text`` at initialization time and can avoid the row management functions entirely. @@ -2137,7 +2038,7 @@ This is used by :class:`ProgressDisplay` and its subclasses for actual output generation. - ``row_id`` is a unique identifier and ``output_line`` is a unicode string. + ``row_id`` is a unique identifier and ``output_line`` is a Unicode string. It is not typically instantiated directly. .. method:: ProgressRow.update(current, total) @@ -2146,7 +2047,7 @@ .. method:: ProgressRow.unicode(width) - Returns the output line and its current progress as a unicode string, + Returns the output line and its current progress as a Unicode string, formatted to the given width in onscreen characters. Screen width can be determined from the :meth:`Messenger.terminal_size` method. @@ -2154,11 +2055,11 @@ display_unicode Objects ^^^^^^^^^^^^^^^^^^^^^^^ -This class is for displaying portions of a unicode string to +This class is for displaying portions of a Unicode string to the screen. The reason this is needed is because not all Unicode characters are the same width. -So, for example, if one wishes to display a portion of a unicode string to +So, for example, if one wishes to display a portion of a Unicode string to a screen that's 80 ASCII characters wide, one can't simply perform: >>> messenger.output(unicode_string[0:80]) @@ -2208,3 +2109,95 @@ u'\u30a1\u30a2\u30a3\u30a4\u30a5\u30a6\u30a7\u30a8\u30a9\u30aa\u30ab\u30ac\u30ad\u30ae\u30af\u30b0\u30b1\u30b2\u30b3\u30b4' >>> print repr(unicode(tail)) u'\u30b5\u30b6\u30b7\u30b8\u30b9' + +Exceptions +---------- + +.. exception:: UnknownAudioType + + Raised by :func:`filename_to_type` if the file's suffix is unknown. + +.. exception:: AmbiguousAudioType + + Raised by :func:`filename_to_type` if the file's suffix + applies to more than one audio class. + +.. exception:: DecodingError + + Raised by :class:`PCMReader`'s .close() method if + a helper subprocess exits with an error, + typically indicating a problem decoding the file. + +.. exception:: DuplicateFile + + Raised by :func:`open_files` if the same file + is included more than once and ``no_duplicates`` is indicated. + +.. exception:: DuplicateOutputFile + + Raised by :func:`audiotools.ui.process_output_options` + if the same output file is generated more than once. + +.. exception:: EncodingError + + Raised by :meth:`AudioFile.from_pcm` and :meth:`AudioFile.convert` + if an error occurs when encoding an input file. + This includes errors from the input stream, + a problem writing the output file in the given location, + or EncodingError subclasses such as + :exc:`UnsupportedBitsPerSample` if the input stream + is formatted in a way the output class does not support. + +.. exception:: InvalidFile + + Raised by :meth:`AudioFile.__init__` if the file + is invalid in some way. + +.. exception:: InvalidFilenameFormat + + Raised by :meth:`AudioFile.track_name` if the format string + contains broken fields. + +.. exception:: InvalidImage + + Raised by :meth:`Image.new` if the image cannot be parsed correctly. + +.. exception:: OutputFileIsInput + + Raised by :func:`process_output_options` if an output file + is the same as any of the input files. + +.. exception:: SheetException + + A parent exception of :exc:`audiotools.cue.CueException` + and :exc:`audiotools.toc.TOCException`, + to be raised by :func:`read_sheet` if a .toc or .cue file + is unable to be parsed correctly. + +.. exception:: UnsupportedBitsPerSample + + Subclass of :exc:`EncodingError`, indicating + the input stream's bits-per-sample is not supported + by the output class. + +.. exception:: UnsupportedChannelCount + + Subclass of :exc:`EncodingError`, indicating + the input stream's channel count is not supported + by the output class. + +.. exception:: UnsupportedChannelMask + + Subclass of :exc:`EncodingError`, indicating + the input stream's channel mask is not supported + by the output class. + +.. exception:: UnsupportedFile + + Raised by :func:`open` if the given file is not something + identifiable, or we do not have the installed binaries to support. + +.. exception:: UnsupportedTracknameField + + Raised by :meth:`AudioFile.track_name` if a track name + field is not supported.
View file
audiotools-2.18.tar.gz/docs/programming/source/audiotools_cue.rst -> audiotools-2.19.tar.gz/docs/programming/source/audiotools_cue.rst
Changed
@@ -48,9 +48,10 @@ Each index is a tuple of CD sectors corresponding to a track's offset on disk. -.. method:: Cuesheet.pcm_lengths(total_length) +.. method:: Cuesheet.pcm_lengths(total_length, sample_rate) - Takes the total length of the entire CD in PCM frames. + Takes the total length of the entire CD, in PCM frames, + and the sample rate of the stream, in Hz. Returns a list of PCM frame lengths for all audio tracks within the cuesheet. This list of lengths can be used to split a single CD image
View file
audiotools-2.19.tar.gz/docs/programming/source/audiotools_pcmconverter.rst
Added
@@ -0,0 +1,209 @@ +:mod:`audiotools.pcmconverter` --- the PCM Conversion Module +============================================================ + +.. module:: audiotools.pcmconverter + :synopsis: a Module for Converting PCM Streams + + +The :mod:`audiotools.pcmconverter` module contains +:class:`audiotools.PCMReader` wrapper classes +for converting streams to different sample rates, +channel counts, channel assignments and so on. + +These classes are combined by :class:`audiotools.PCMConverter` +as needed to modify a stream from one format to another. + + +Averager Objects +---------------- + +.. class:: Averager(pcmreader) + + This class takes a :class:`audiotools.PCMReader`-compatible + object and constructs a new :class:`audiotools.PCMReader`-compatible + whose channels have been averaged together into a single channel. + +.. data:: Averager.sample_rate + + The sample rate of this audio stream, in Hz, as a positive integer. + +.. data:: Averager.channels + + The number of channels of this audio stream, which is always 1. + +.. data:: Averager.channel_mask + + The channel mask of this audio stream, which is always ``0x4``. + +.. data:: Averager.bits_per_sample + + The number of bits-per-sample in this audio stream as a positive integer. + +.. method:: Averager.read(pcm_frames) + + Try to read a :class:`audiotools.pcm.FrameList` object with the given + number of PCM frames, if possible. + This method is *not* guaranteed to read that amount of frames. + It may return less, particularly at the end of an audio stream. + It may even return FrameLists larger than requested. + However, it must always return a non-empty FrameList until the + end of the PCM stream is reached. + May raise :exc:`IOError` if there is a problem reading the + source file, or :exc:`ValueError` if the source file has + some sort of error. + +.. method:: Averager.close() + + Closes the audio stream. + If any subprocesses were used for audio decoding, they will also be + closed and waited for their process to finish. + May raise a :exc:`DecodingError`, typically indicating that + a helper subprocess used for decoding has exited with an error. + +BPSConverter Objects +-------------------- + +.. class:: BPSConveter(pcmreader, bits_per_sample) + + This class takes a :class:`audiotools.PCMReader`-compatible + object and new ``bits_per_sample`` integer, + and constructs a new :class:`audiotools.PCMReader`-compatible + object with that amount of bits-per-sample + by truncating or extending bits to each sample as needed. + +.. data:: BPSConverter.sample_rate + + The sample rate of this audio stream, in Hz, as a positive integer. + +.. data:: BPSConverter.channels + + The number of channels in this audio stream as a positive integer. + +.. data:: BPSConverter.channel_mask + + The channel mask of this audio stream as a non-negative integer. + +.. data:: BPSConverter.bits_per_sample + + The number of bits-per-sample in this audio stream as + indicated at init-time. + +.. method:: BPSConverter.read(pcm_frames) + + Try to read a :class:`audiotools.pcm.FrameList` object with the given + number of PCM frames, if possible. + This method is *not* guaranteed to read that amount of frames. + It may return less, particularly at the end of an audio stream. + It may even return FrameLists larger than requested. + However, it must always return a non-empty FrameList until the + end of the PCM stream is reached. + May raise :exc:`IOError` if there is a problem reading the + source file, or :exc:`ValueError` if the source file has + some sort of error. + +.. method:: BPSConverter.close() + + Closes the audio stream. + If any subprocesses were used for audio decoding, they will also be + closed and waited for their process to finish. + May raise a :exc:`DecodingError`, typically indicating that + a helper subprocess used for decoding has exited with an error. + +Downmixer Objects +----------------- + +.. class:: Downmixer(pcmreader) + + This class takes a :class:`audiotools.PCMReader`-compatible + object, presumably with more than two channels, and + constructs a :class:`audiotools.PCMReader`-compatible object + with only two channels mixed in Dolby Pro Logic format + such that a rear channel can be restored. + + If the stream has fewer than 5.1 channels, those channels + are padded with silence. + Additional channels beyond 5.1 are ignored. + +.. data:: Downmixer.sample_rate + + The sample rate of this audio stream, in Hz, as a positive integer. + +.. data:: Downmixer.channels + + The number of channels in this audio stream, which is always 2. + +.. data:: Downmixer.channel_mask + + The channel mask of this audio stream, which is always ``0x3``. + +.. data:: Downmixer.bits_per_sample + + The number of bits-per-sample in this audio stream as a positive integer. + +.. method:: Downmixer.read(pcm_frames) + + Try to read a :class:`audiotools.pcm.FrameList` object with the given + number of PCM frames, if possible. + This method is *not* guaranteed to read that amount of frames. + It may return less, particularly at the end of an audio stream. + It may even return FrameLists larger than requested. + However, it must always return a non-empty FrameList until the + end of the PCM stream is reached. + May raise :exc:`IOError` if there is a problem reading the + source file, or :exc:`ValueError` if the source file has + some sort of error. + +.. method:: Downmixer.close() + + Closes the audio stream. + If any subprocesses were used for audio decoding, they will also be + closed and waited for their process to finish. + May raise a :exc:`DecodingError`, typically indicating that + a helper subprocess used for decoding has exited with an error. + +Resampler Objects +----------------- + +.. class:: Resampler(pcmreader, sample_rate) + + This class takes a :class:`audiotools.PCMReader`-compatible object + and new ``sample_rate`` integer, and constructs a new + :class:`audiotools.PCMReader`-compatible object with that sample rate. + +.. data:: Resampler.sample_rate + + The sample rate of this audio stream, in Hz, + as given at init-time. + +.. data:: Resampler.channels + + The number of channels in this audio stream as a positive integer. + +.. data:: Resampler.channel_mask + + The channel mask of this audio stream as a non-negative integer. + +.. data:: Resampler.bits_per_sample + + The number of bits-per-sample in this audio stream as a positive integer. + +.. method:: Resampler.read(pcm_frames) + + Try to read a :class:`audiotools.pcm.FrameList` object with the given + number of PCM frames, if possible. + This method is *not* guaranteed to read that amount of frames. + It may return less, particularly at the end of an audio stream. + It may even return FrameLists larger than requested. + However, it must always return a non-empty FrameList until the + end of the PCM stream is reached. + May raise :exc:`IOError` if there is a problem reading the + source file, or :exc:`ValueError` if the source file has + some sort of error. + +.. method:: Resampler.close() + + Closes the audio stream. + If any subprocesses were used for audio decoding, they will also be + closed and waited for their process to finish. + May raise a :exc:`DecodingError`, typically indicating that + a helper subprocess used for decoding has exited with an error.
View file
audiotools-2.18.tar.gz/docs/programming/source/audiotools_player.rst -> audiotools-2.19.tar.gz/docs/programming/source/audiotools_player.rst
Changed
@@ -19,7 +19,7 @@ ----------------- ----------------- PulseAudioOutput PulseAudio_ OSSAudioOutput OSS_ -PortAudioOutput PortAudio_ +CoreAudioOutput CoreAudio NULLAudioOutput No output ================= ================= @@ -166,7 +166,7 @@ Why not simply have the :meth:`play` method perform PCM conversion itself instead of shifting it to :meth:`framelist_converter`? The reason is that conversion may be a relatively time-consuming task. - By shifting that process into a subthread, there's less chance + By shifting that process into a sub-thread, there's less chance that performing that work will cause playing to stutter while it completes.
View file
audiotools-2.18.tar.gz/docs/programming/source/audiotools_replaygain.rst -> audiotools-2.19.tar.gz/docs/programming/source/audiotools_replaygain.rst
Changed
@@ -20,20 +20,21 @@ the given ``sample_rate``. Raises :exc:`ValueError` if the sample rate is not supported. -.. method:: Replaygain.update(frame_list) +.. method:: ReplayGain.title_gain(pcmreader) - Takes a :class:`pcm.FrameList` object and updates our ongoing - ReplayGain calculation. - Raises :exc:`ValueError` if some error occurs during calculation. - -.. method:: ReplayGain.title_gain() - - Returns a pair of floats. + Given a :class:`audiotools.PCMReader`-compatible object, + calculates its ReplayGain values and returns a pair of floats. The first is the calculated gain value since our last call to :meth:`title_gain`. The second is the calculated peak value since our last call to :meth:`title_gain`. + May raise :exc:`ValueError` if the stream's sample rate + doesn't match the rate given at init-time, + the stream's number of channels is more than 2, + the stream doesn't contain enough samples + or the calculation generates some other error. + .. method:: ReplayGain.album_gain() Returns a pair of floats.
View file
audiotools-2.18.tar.gz/docs/programming/source/audiotools_toc.rst -> audiotools-2.19.tar.gz/docs/programming/source/audiotools_toc.rst
Changed
@@ -40,9 +40,10 @@ Each index is a tuple of CD sectors corresponding to a track's offset on disk. -.. method:: TOCFile.pcm_lengths(total_length) +.. method:: TOCFile.pcm_lengths(total_length, sample_rate) - Takes the total length of the entire CD in PCM frames. + Takes the total length of the entire CD, in PCM frames, + and the sample rate of the stream, in Hz. Returns a list of PCM frame lengths for all audio tracks within the TOC file. This list of lengths can be used to split a single CD image
View file
audiotools-2.19.tar.gz/docs/programming/source/audiotools_ui.rst
Added
@@ -0,0 +1,575 @@ +:mod:`audiotools.ui` --- Reusable Python Audio Tools GUI Widgets +================================================================ + +This module contains a collection of reusable Urwid widgets +and helper functions for processing user input +and generating helper output in a consistent way. + +.. function:: show_available_formats(messenger) + + Given a :class:`audiotools.Messenger` object, + displays all the available file formats + one can select with a utility's ``-t`` argument + in a user-friendly way. + +.. function:: show_available_qualities(messenger, audio_type) + + Given a :class:`audiotools.Messenger` object + and an :class:`audiotools.AudioFile` subclass, + displays all the available output qualities + for that audio type one can select with a utility's ``-q`` argument. + +.. function:: select_metadata(metadata_choices, messenger, [use_default]) + + Given ``metadata_choices[choice][track]`` where each choice + contains a list of :class:`audiotools.MetaData` objects + and all choices must have the same number of objects, + along with a :class:`audiotools.Messenger` object, + queries the user for which metadata choice to use + and returns that list of :class:`audiotools.MetaData` objects. + + If ``use_default`` is ``True``, this simply returns the metadata + for the first choice. + +.. function:: process_output_options(metadata_choices, input_filenames, output_directory, format_string, output_class, quality, messenger, [use_default]) + + This function is for processing the input and output options + for utilities such as ``track2track`` and ``cd2track``. + + ``metadata_choices[choice][track]`` is a list of + :class:`audiotools.MetaData` objects + and all choices must have the same number of objects. + + ``input_filenames`` is a list of :class:`audiotools.Filename` objects + where the number of filenames must match the number of + :class:`audiotools.MetaData` objects for each choice + in ``metadata_choices``. + + ``output_directory`` is a string to the output directory, + indicated by a utility's ``-d`` option. + + ``format_string`` is a UTF-8 encoded plain string of the + output file format, indicated by a utility's ``--format`` option. + + ``output_class`` is an :class:`audiotools.AudioFile` class, + indicated by a utility's ``-t`` option. + + ``quality`` is a string if the output quality to use, + indicated by a utility's ``-q`` option. + + ``messenger`` is an :class:`audiotools.Messenger` object + used for displaying output. + + ``use_default``, if indicated, is applied to this function's call to + :func:`select_metadata` if necessary. + + Yields a ``(output_class, output_filename, output_quality, output_metadata)`` + tuple for each output file where ``output_class`` is an + :class:`audiotools.AudioFile` object, + ``output_filename`` is a :class:`audiotools.Filename` object, + ``output_quality`` is a compression string, + and ``output_metadata`` is a :class:`audiotools.MetaData` object. + + Raises :exc:`audiotools.UnsupportedTracknameField` or + :exc:`audiotools.InvalidFilenameFormat` if the ``format_string`` + option is invalid. + + Raises :exc:`audiotools.DuplicateOutputFile` if the + same output filename is generated more than once. + + Raises :exc:`audiotools.OutputFileIsInput` if + one of the output files is the same as any of the input files. + +.. function:: not_available_message(messenger) + + Given a :class:`audiotools.Messenger` object, + displays a message about Urwid being unavailable + and offers a suggestion on how to obtain it. + +.. function:: xargs_suggestion(args) + + Given a list of argument strings (such as from ``sys.argv``) + returns a Unicode string indicating how one might + call the given program using ``xargs``. + +PlayerTTY Objects +----------------- + +.. class:: PlayerTTY(player) + + This is a base class for implementing the user interface + for TTY-based audio players. + + ``player`` is a :class:`audiotools.player.Player`-compatible + object. + +.. method:: PlayerTTY.next_track() + + Stop playing the current track and begin playing the next one. + This must be implemented in a subclass. + +.. method:: PlayerTTY.previous_track() + + Stop playing the current track and begin playing the previous one. + This must be implemented in a subclass. + +.. method:: PlayerTTY.set_metadata(track_number, track_total, channels, sample_rate, bits_per_sample) + + Typically called by :meth:`PlayerTTY.next_track` and + :meth:`PlayerTTY.previous_track`, this sets the current metadata + to the given values for displaying to the user. + +.. method:: PlayerTTY.toggle_play_pause() + + Calls :meth:`audiotools.player.Player.toggle_play_pause` + on the internal :class:`audiotools.player.Player` object + to suspend or resume output. + +.. method:: PlayerTTY.stop() + + Calls :meth:`audiotools.player.Player.stop` + on the internal :class:`audiotools.player.Player` object + to stop playing the current file completely. + +.. method:: PlayerTTY.progress() + + Returns the values from :meth:`audiotools.player.Player.progress` + which indicate the current status of the playing file. + +.. method:: PlayerTTY.progress_line(frames_sent, frames_total) + + Given the amount of PCM frames sent and total number of PCM frames + as integers, returns a Unicode string of the current progress + to be displayed to the user. + +.. method:: PlayerTTY.run(messenger, stdin) + + Given a :class:`audiotools.Messenger` object + and ``sys.stdin`` file object, + this runs the player's output loop + until the user indicates it should exit or the input is exhausted. + + Returns 0 on a successful exit, 1 if it exits with an error. + +.. data:: AVAILABLE + + ``True`` if Urwid is available and is of a sufficiently high version. + ``False`` if not. + +Urwid Widgets +------------- + +If Urwid is available, the following classes will be in this +module for use by utilities to generate interactive modes. +If not, the classes will not be defined. + +OutputFiller Objects +^^^^^^^^^^^^^^^^^^^^ + +.. class:: OutputFiller(track_labels, metadata_choices, input_filenames, output_directory, format_string, output_class, quality, [completion_label]) + + This is an Urwid Frame subclass for populating track data + and options for multiple output file utilities such + as ``track2track`` and ``cd2track``. + + ``track_labels`` is a list of Unicode strings, one per track + + ``metadata_choices[choice][track]`` is a list of + :class:`audiotools.MetaData` objects per choice, one per track + + ``input_filenames`` is a list of :class:`audiotools.Filename` objects, + one per track. + + ``output_directory`` is a string to the output directory, + indicated by a utility's ``-d`` option. + + ``format_string`` is a UTF-8 encoded plain string of the + output file format, indicated by a utility's ``--format`` option. + + ``output_class`` is an :class:`audiotools.AudioFile` class, + indicated by a utility's ``-t`` option. + + ``quality`` is a string if the output quality to use, + indicated by a utility's ``-q`` option. + + ``completion_label`` is an optional Unicode string + to display in the widget's "apply" button + used to complete the operation. + + This widget is typically executed as follows: + + >>> widget = audiotools.ui.OutputFiller(...) # populate widget with metadata and command-line options + >>> loop = urwid.MainLoop(widget, + ... audiotools.ui.style(), + ... unhandled_input=widget.handle_text, + ... pop_ups=True) + >>> loop.run() + >>> if (not widget.cancelled()): + ... # do work here + ... else: + ... # exit + +.. method:: OutputFiller.output_tracks() + + Yields a ``(output_class, output_filename, output_quality, output_metadata)`` + tuple for each output file where ``output_class`` is an + :class:`audiotools.AudioFile` object, + ``output_filename`` is a :class:`audiotools.Filename` object, + ``output_quality`` is a compression string, + and ``output_metadata`` is a :class:`audiotools.MetaData` object. + +.. note:: + + This method returns freshly-created :class:`audiotools.MetaData` objects, + whereas :func:`process_output_options` resuses the same objects + passed to it. + + Because :class:`OutputFiller` may modify input metadata, + we don't want to risk modifying objects used elsewhere. + +.. method:: OutputFiller.output_directory() + + Returns the currently selected output directory as a plain string. + +.. method:: OutputFiller.format_string() + + Returns the current format string as a plain, UTF-8 encoded string. + +.. method:: OutputFiller.output_class() + + Returns the current :class:`audiotools.AudioFile`-compatible + output class. + +.. method:: OutputFiller.quality() + + Returns the current output quality as a plain string. + +SingleOutputFiller Objects +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. class:: SingleOutputFiller(track_label, metadata_choices, input_filenames, output_file, output_class, quality, [completion_label]) + + This is an Urwid Frame subclass for populating track data + and options for a single output track utilities such as ``trackcat``. + + ``track_label`` is a Unicode string. + + ``metadata_choices[choice]`` is a list of :class:`audiotools.MetaData` + objects for all possible choices for the given track. + + ``input_filenames`` is a list or set of :class:`audiotools.Filename` + objects for all input files, which may include cuesheets + or other auxiliary data. + + ``output_file`` is a plain string of the default output filename. + + ``output_class`` is an :class:`audiotools.AudioFile` class, + indicated by a utility's ``-t`` option. + + ``quality`` is a string if the output quality to use, + indicated by a utility's ``-q`` option. + + ``completion_label`` is an optional Unicode string + to display in the widget's "apply" button + used to complete the operation. + + This widget is typically executed as follows: + + >>> widget = audiotools.ui.SingleOutputFiller(...) # populate widget with metadata and command-line options + >>> loop = urwid.MainLoop(widget, + ... audiotools.ui.style(), + ... unhandled_input=widget.handle_text, + ... pop_ups=True) + >>> loop.run() + >>> if (not widget.cancelled()): + ... # do work here + ... else: + ... # exit + +.. method:: SingleOutputFiller.output_track() + + Returns ``(output_class, output_filename, output_quality, output_metadata)`` + tuple to apply to the single output track. + ``output_class`` is an :class:`audiotools.AudioFile` object, + ``output_filename`` is a :class:`audiotools.Filename` object, + ``output_quality`` is a compression string, + and ``output_metadata`` is a :class:`audiotools.MetaData` object. + +MetaDataFiller Objects +^^^^^^^^^^^^^^^^^^^^^^ + +.. class:: MetaDataFiller(track_labels, metadata_choices, status) + + This is an Urwid Pile subclass for selecting and editing + a single set of metadata from multiple choices. + It is used by :class:`OutputFiller` and :class:`SingleOutputFiller` + as necessary to allow the user to edit metadata + when setting options. + + ``track_labels`` is a list of Unicode strings, one per track. + + ``metadata_choices[choice][track]`` is a list of + :class:`audiotools.MetaData` objects per choice, one per track + + ``status`` is an :class:`urwid.Text` object + to display status text such as key shortcuts. + +.. method:: MetaDataFiller.select_previous_item() + + Selects the previous item in the current set of metadata, + such as the previous track or the previous field, + depending on how the data is swiveled. + +.. method:: MetaDataFiller.select_next_item() + + Selects the next item in the current set of metadata, + such as the next track or the next field, + depending on how the data is swiveled. + +.. method:: MetaDataFiller.populated_metadata() + + Yields a new, populated :class:`audiotools.MetaData` object + per track, depending on the current selection and its values. + +MetaDataEditor Objects +^^^^^^^^^^^^^^^^^^^^^^ + +.. class:: MetaDataEditor(tracks, [on_text_change], [on_swivel_change]) + + This is an Urwid Frame subclass for editing a single set of metadata + across multiple tracks. + + ``tracks`` is a list of ``(id, label, metadata)`` tuples + in the order they are to be displayed + with ``id`` is some unique, hashable ID value, + ``label`` is a Unicode string, + and ``metadata`` is an :class:`audiotools.MetaData` object, or ``None``. + + ``on_text_change(widget, new_value)`` + is a callback for when any text field is modified. + + ``on_swivel_change(widget, selected, swivel)`` + is a callback for when tracks and fields are swapped. + +.. method:: MetaDataEditor.select_previous_item() + + Selects the previous item in the current set of metadata, + such as the previous track or the previous field, + depending on how the data is swiveled. + +.. method:: MetaDataEditor.select_next_item() + + Selects the next item in the current set of metadata, + such as the next track or the next field, + depending on how the data is swiveled. + +.. method:: MetaDataEditor.metadata() + + Yields a ``(track_id, metadata)`` tuple per edited track + where ``track_id`` is the unique, hashable value + entered at init-time, and ``metadata`` is a newly created + :class:`audiotools.MetaData` object. + +BottomLineBox Objects +^^^^^^^^^^^^^^^^^^^^^ + +.. class:: BottomLineBox(original_widget, [title], [tlcorner], [tline], [lline], [trcorner], [blcorner], [rline], [bline], [bcorner]) + + This is an Urwid LineBox subclass which places its title + at the bottom instead of the top. + +SelectOne Objects +^^^^^^^^^^^^^^^^^ + +.. class:: SelectOne(items, [selected_value], [on_change], [user_data], [label]) + + This is an Urwid PopUpLauncher subclass designed to work + as an HTML-style <SELECT> dropdown. + + ``items`` is a list of ``(label, value)`` tuples + where ``label`` is a Unicode string and ``value`` + is any object with an ``__eq__`` method. + + ``selected_value`` indicates which object in items + is currently selected. + + ``on_change(new_value, [user_data])`` is a callback + which is called whenever the selected item is changed + where ``new_value`` is the value from the ``item`` tuple. + + ``user_data`` is an object passed to the ``on_change`` callback. + + ``label`` is a Unicode label string for the selection box. + +.. method:: SelectOne.make_selection(label, value) + + Given a Unicode ``label`` and ``value`` object, + sets the selection to the given values. + +.. method:: SelectOne.selection() + + Returns the selected ``value`` object. + +.. method:: SelectOne.set_items(items, selected_value) + + Replaces all the items in the dropdown with new values. + ``items`` is a list of ``(label, value)`` tuples + where ``label`` is a Unicode string and ``value`` + is any object with an ``__eq__`` method. + + ``selected_value`` indicates which object in items + is currently selected. + +SelectDirectory Objects +^^^^^^^^^^^^^^^^^^^^^^^ + +.. class:: SelectDirectory(initial_directory, [on_change], [user_data]) + + This is an Urwid Columns subclass consisting of an + editable output directory box and a directory tree + browser button. + + ``initial_directory`` is a plain string of the starting directory. + + ``on_change(widget, new_directory, user_data)`` is a callback + which is called whenever the directory is changed. + + ``user_data`` is passed to the ``on_change`` callback. + +.. method:: SelectDirectory.get_directory() + + Returns the currently selected directory as a plain string. + +EditFilename Objects +^^^^^^^^^^^^^^^^^^^^ + +.. class:: EditFilename(initial_filename) + + This is an Urwid Edit subclass for editing a single output filename. + + ``initial_filename`` is a plain string of the starting filename. + +.. method:: EditFilename.get_filename() + + Returns the edited filename as a plain string. + +.. method:: EditFilename.set_filename(filename) + + Updates the field's value to the given filename, + which is a plain string. + +OutputOptions Objects +^^^^^^^^^^^^^^^^^^^^^ + +.. class:: OutputOptions(output_directory, format_string, audio_class, quality, input_filenames, metadatas, [extra_widgets]) + + This is an Urwid Pile subclass for populating output options, + including an output file previewer so one can see the results + of changing the directory and format string in real-time. + + ``output_directory`` is a string to the output directory, + indicated by a utility's ``-d`` option. + + ``format_string`` is a UTF-8 encoded plain string of the + output file format, indicated by a utility's ``--format`` option. + + ``output_class`` is an :class:`audiotools.AudioFile` class, + indicated by a utility's ``-t`` option. + + ``quality`` is a string if the output quality to use, + indicated by a utility's ``-q`` option. + + ``input_filenames`` is a list of :class:`audiotools.Filename` objects, + one per input track. + + ``metadatas`` is a list of :class:`audiotools.Metadata` objects, + oner per input track. + + ``extra_widgets`` is a list of additional Urwid widgets + to append to the pile. + +.. method:: OutputOptions.set_metadatas(metadatas) + + ``metadatas`` is a list of :class:`audiotools.MetaData` objects + (which may be ``None``), one per input track. + +.. method:: OutputOptions.selected_options() + + Returns ``(output_class, output_quality, [output filenames])`` + tuple where ``output_class`` is an :class:`audiotools.AudioFile`, + ``output_quality`` is a plain string + and ``output_filenames`` is a list of :class:`audiotools.Filename` + objects, one per input filename. + +SingleOutputOptions Objects +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. class:: SingleOutputOptions(output_filename, audio_class, quality) + + This is an Urwid ListBox subclass for populating the options + of a single output audio file. + + ``output_filename`` is a plain string of the default + output filename. + + ``audio_class`` is an :class:`audiotools.AudioFile` object. + + ``quality`` is a plain string of the default output quality. + +.. method:: SingleOutputOptions.selected_options() + + Returns ``(output_class, output_quality, output filename)`` + where ``output_class`` is an :class:`audiotools.AudioFile`, + ``output_quality`` is a plain string + and ``output_filename`` is a :class:`audiotools.Filename` object. + +PlayerGUI Objects +^^^^^^^^^^^^^^^^^ + +.. class:: PlayerGUI(player, tracks, total_length) + + This is an Urwid Frame subclass for implementing + an interactive audio player. + It cannot be instantiated directly; + a subclass must implement its ``select_track`` method + to determine what to play next. + + ``player`` is a :class:`audiotools.player.Player`-compatible + object. + + ``tracks`` is a list of ``(track_name, seconds_length, user_data)`` + tuples where ``track_name`` is a Unicode string, + ``seconds_length`` is the length of the track in seconds + and ``user_data`` is some Python object. + + ``total_length`` is the length of all tracks in seconds. + + This widget is typically used as follows: + + >>> player = PlayerGUISubclass(...) # instantiate PlayerGUI subclass widget with input + >>> loop = urwid.MainLoop(widget, # setup MainLoop to execute widget + ... audiotools.ui.style(), + ... unhandled_input=player.handle_text) + >>> loop.set_alarm_at(tm=time.time() + 1, # set timer to update player's progress bar + ... callback=audiotools.ui.timer, + ... user_data=player) + >>> loop.run() + +.. method:: PlayerGUI.select_track(radio_button, new_state, user_data, [auto_play]) + + Begins playing the selected audio track. + This must be implemented by a subclass. + +Extra Urwid Functions +^^^^^^^^^^^^^^^^^^^^^ + +.. function:: timer(main_loop, playergui) + + Updates the status of the given :class:`PlayerGUI` object + at regular intervals so that its progress bar moves properly. + +.. function:: style() + + Returns a list of widget style tuples + for use with Urwid's ``MainLoop`` object + in order to style all interactive modes consistently.
View file
audiotools-2.18.tar.gz/docs/programming/source/conf.py -> audiotools-2.19.tar.gz/docs/programming/source/conf.py
Changed
@@ -45,9 +45,9 @@ # built documents. # # The short X.Y version. -version = '2.18' +version = '2.19' # The full version, including alpha/beta/rc tags. -release = '2.18' +release = '2.19' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages.
View file
audiotools-2.18.tar.gz/docs/programming/source/index.rst -> audiotools-2.19.tar.gz/docs/programming/source/index.rst
Changed
@@ -14,11 +14,12 @@ audiotools.rst audiotools_pcm.rst audiotools_bitstream.rst - audiotools_resample.rst + audiotools_pcmconverter.rst audiotools_replaygain.rst audiotools_cdio.rst audiotools_cue.rst audiotools_toc.rst + audiotools_ui.rst audiotools_player.rst metadata.rst
View file
audiotools-2.18.tar.gz/docs/programming/source/metadata.rst -> audiotools-2.19.tar.gz/docs/programming/source/metadata.rst
Changed
@@ -25,7 +25,7 @@ >>> tag.track_name u'Track Title' >>> tag['Title'] - ApeTagItem(0,False,'Title','Track Title') + ApeTagItem(0, False, 'Title', 'Track Title') >>> tag['Title'] = ApeTagItem(0, False, 'Title', u'New Title'.encode('utf-8')) >>> tag.track_name u'New Title' @@ -62,8 +62,7 @@ >>> tag = ApeTag([ApeTagItem(0, False, 'Track', u'1'.encode('utf-8'))]) >>> tag.track_number 1 - >>> tag.track_total - 0 + >>> tag.track_total # None >>> tag = ApeTag([ApeTagItem(0, False, 'Track', u'2/3'.encode('utf-8'))]) >>> tag.track_number 2 @@ -99,7 +98,7 @@ ``read_only`` is a boolean set to ``True`` if the tag-item is read-only. ``key`` is an ASCII string. - ``data`` is a regular Python string (not unicode). + ``data`` is a regular Python string (not Unicode). .. method:: ApeTagItem.build() @@ -119,7 +118,7 @@ .. classmethod:: ApeTagItem.string(key, unicode) - A convenience classmethod which takes a key string and value unicode + A convenience classmethod which takes a key string and value Unicode and returns a populated :class:`ApeTagItem` object of the appropriate type. @@ -188,7 +187,7 @@ .. method:: FlacMetaData.raw_info() - Returns this metadata as a human-readable unicode string. + Returns this metadata as a human-readable Unicode string. .. method:: FlacMetaData.size() @@ -215,7 +214,7 @@ .. method:: Flac_STREAMINFO.raw_info() - Returns this metadata block as a human-readable unicode string. + Returns this metadata block as a human-readable Unicode string. .. classmethod:: Flac_STREAMINFO.parse(reader) @@ -249,7 +248,7 @@ .. method:: Flac_PADDING.raw_info() - Returns this metadata block as a human-readable unicode string. + Returns this metadata block as a human-readable Unicode string. .. classmethod:: Flac_PADDING.parse(reader) @@ -284,7 +283,7 @@ .. method:: Flac_APPLICATION.raw_info() - Returns this metadata block as a human-readable unicode string. + Returns this metadata block as a human-readable Unicode string. .. classmethod:: Flac_APPLICATION.parse(reader) @@ -320,7 +319,7 @@ .. method:: Flac_SEEKTABLE.raw_info() - Returns this metadata block as a human-readable unicode string. + Returns this metadata block as a human-readable Unicode string. .. classmethod:: Flac_SEEKTABLE.parse(reader) @@ -343,15 +342,17 @@ and ``byte offset`` / ``PCM frame count`` values reordered to be incrementing. Any fixes performed are appended to the ``fixes_performed`` - list as unicode strings. + list as Unicode strings. VORBISCOMMENT ^^^^^^^^^^^^^ .. class:: Flac_VORBISCOMMENT(comment_strings, vendor_string) - ``comment_strings`` is a list of unicode strings - and ``vendor_string`` is a unicode string. + ``comment_strings`` is a list of Unicode strings + and ``vendor_string`` is a Unicode string. + + See :class:`VorbisComment` on how to map strings to attributes. >>> Flac_VORBISCOMMENT([u"TITLE=Foo", u"ARTIST=Bar"], u"Python Audio Tools") @@ -365,7 +366,7 @@ .. method:: Flac_VORBISCOMMENT.raw_info() - Returns this metadata block as a human-readable unicode string. + Returns this metadata block as a human-readable Unicode string. .. classmethod:: Flac_VORBISCOMMENT.parse(reader) @@ -407,7 +408,7 @@ .. method:: Flac_CUESHEET.raw_info() - Returns this metadata block as a human-readable unicode string. + Returns this metadata block as a human-readable Unicode string. .. classmethod:: Flac_CUESHEET.parse(reader) @@ -444,10 +445,13 @@ Returns a list of ``(start, end)`` integer tuples indicating all the index points in the cuesheet. -.. method:: Flac_CUESHEET.pcm_lengths(total_length) +.. method:: Flac_CUESHEET.pcm_lengths(total_length, sample_rate) - Given a total length of the file in PCM frames, - returns a list of PCM frame lengths for each track in the cuesheet. + Given a total length of the file, in PCM frames, + and the sample rate of the stream, in Hz. + Returns a list of PCM frame lengths for each track in the cuesheet. + This list of lengths can be used to split a single CD image + file into several individual tracks. .. class:: Flac_CUESHEET_track(offset, number, ISRC, track_type, pre_emphasis, index_points) @@ -464,7 +468,7 @@ .. method:: Flac_CUESHEET_track.raw_info() - Returns this cuesheet track as a human-readable unicode string. + Returns this cuesheet track as a human-readable Unicode string. .. classmethod:: Flac_CUESHEET_track.parse(reader) @@ -524,7 +528,7 @@ 20 Publisher/Studio logotype == =================================== - ``mime_type`` and ``description`` are unicode strings. + ``mime_type`` and ``description`` are Unicode strings. ``width`` and ``height`` are integer number of pixels. ``color_depth`` is an integer number of bits per pixel. ``color_count`` is an integer number of colors for images @@ -544,7 +548,7 @@ .. method:: Flac_IMAGE.raw_info() - Returns this metadata block as a human-readable unicode string. + Returns this metadata block as a human-readable Unicode string. .. classmethod:: Flac_IMAGE.parse(reader) @@ -573,25 +577,42 @@ metadata fields cleaned up according to the metrics of the contained raw image data. Any fixes are appended to the ``fixes_performed`` list - as unicode strings. + as Unicode strings. ID3v1 ----- -.. class:: ID3v1Comment(track_name, artist_name, album_name, year, comment, track_number, genre) +.. class:: ID3v1Comment([track_name][, artist_name][, album_name][, year][, comment][, track_number][, genre]) + + All fields are binary strings with a fixed length: - All fields except track_number and genre are binary strings. + ============= ====== + field length + ------------- ------ + track_name 30 + artist_name 30 + album_name 30 + year 4 + comment 28 + track_number 1 + genre 1 + ============= ====== - >>> tag = ID3v1Comment(u'Track Title', u'', u'', u'', u'', 1, 0) + >>> tag = ID3v1Comment(track_name='Track Title' + chr(0) * 19, # note the NULL byte padding + ... track_number=chr(1)) >>> tag.track_name u'Track Title' >>> tag.track_name = u'New Track Name' >>> tag.track_name u'New Track Name' + Undefined fields are filled with NULL bytes. + Attempting to initialize a field with an incorrect size + will raise a :exc:`ValueError`. + .. method:: ID3v1Comment.raw_info() - Returns this metadata as a human-readable unicode string. + Returns this metadata as a human-readable Unicode string. .. classmethod:: ID3v1Comment.parse(mp3_file) @@ -666,7 +687,7 @@ .. method:: ID3v22Frame.raw_info() - Returns this frame as a human-readable unicode string. + Returns this frame as a human-readable Unicode string. .. classmethod:: ID3v22Frame.parse(frame_id, frame_size, reader) @@ -690,7 +711,7 @@ Returns a new :class:`ID3v22Frame` object that's been cleaned of any problems. Any fixes performed are appended to ``fixes_performed`` - as unicode strings. + as Unicode strings. ID3v2.2 Text Frames ^^^^^^^^^^^^^^^^^^^ @@ -728,7 +749,7 @@ .. classmethod:: ID3v22_T__Frame.converted(frame_id, unicode_string) - Given a 3 byte frame ID and unicode string, + Given a 3 byte frame ID and Unicode string, returns a new :class:`ID3v22_T__Frame` object. .. class:: ID3v22_TXX_Frame(encoding, description, data) @@ -772,7 +793,7 @@ .. classmethod:: ID3v22_COM_Frame.converted(frame_id, unicode_string) - Given a 3 byte ``"COM"`` frame ID and unicode string, + Given a 3 byte ``"COM"`` frame ID and Unicode string, returns a new :class:`ID3v22_COM_Frame` object. ID3v2.2 PIC Frame @@ -893,7 +914,7 @@ .. method:: ID3v23_Frame.raw_info() - Returns this frame as a human-readable unicode string. + Returns this frame as a human-readable Unicode string. .. classmethod:: ID3v23_Frame.parse(frame_id, frame_size, reader) @@ -917,7 +938,7 @@ Returns a new :class:`ID3v23_Frame` object that's been cleaned of any problems. Any fixes performed are appended to ``fixes_performed`` - as unicode strings. + as Unicode strings. ID3v2.3 Text Frames ^^^^^^^^^^^^^^^^^^^ @@ -955,7 +976,7 @@ .. classmethod:: ID3v23_T___Frame.converted(frame_id, unicode_string) - Given a 4 byte frame ID and unicode string, + Given a 4 byte frame ID and Unicode string, returns a new :class:`ID3v23_T___Frame` object. .. class:: ID3v23_TXXX_Frame(encoding, description, data) @@ -999,7 +1020,7 @@ .. classmethod:: ID3v23_COMM_Frame.converted(frame_id, unicode_string) - Given a 4 byte ``"COMM"`` frame ID and unicode string, + Given a 4 byte ``"COMM"`` frame ID and Unicode string, returns a new :class:`ID3v23_COMM_Frame` object. ID3v2.3 APIC Frame @@ -1110,7 +1131,7 @@ .. method:: ID3v24_Frame.raw_info() - Returns this frame as a human-readable unicode string. + Returns this frame as a human-readable Unicode string. .. classmethod:: ID3v24_Frame.parse(frame_id, frame_size, reader) @@ -1134,7 +1155,7 @@ Returns a new :class:`ID3v24_Frame` object that's been cleaned of any problems. Any fixes performed are appended to ``fixes_performed`` - as unicode strings. + as Unicode strings. ID3v2.4 Text Frames ^^^^^^^^^^^^^^^^^^^ @@ -1174,7 +1195,7 @@ .. classmethod:: ID3v24_T___Frame.converted(frame_id, unicode_string) - Given a 4 byte frame ID and unicode string, + Given a 4 byte frame ID and Unicode string, returns a new :class:`ID3v24_T___Frame` object. .. class:: ID3v24_TXXX_Frame(encoding, description, data) @@ -1220,7 +1241,7 @@ .. classmethod:: ID3v24_COMM_Frame.converted(frame_id, unicode_string) - Given a 4 byte ``"COMM"`` frame ID and unicode string, + Given a 4 byte ``"COMM"`` frame ID and Unicode string, returns a new :class:`ID3v23_COMM_Frame` object. ID3v2.4 APIC Frame @@ -1369,7 +1390,7 @@ .. method:: M4A_META_Atom.raw_info() - Returns atom data as a human-readable unicode string. + Returns atom data as a human-readable Unicode string. .. classmethod:: M4A_META_Atom.parse(name, data_size, reader, parsers) @@ -1409,7 +1430,7 @@ .. method:: M4A_Leaf_Atom.raw_info() - Returns atom data as a human-readable unicode string. + Returns atom data as a human-readable Unicode string. .. method:: M4A_Leaf_Atom.parse(name, size, reader, parsers) @@ -1445,7 +1466,7 @@ .. method:: M4A_Tree_Atom.raw_info() - Returns atom data as a human-readable unicode string. + Returns atom data as a human-readable Unicode string. .. method:: M4A_Tree_Atom.parse(name, data_size, reader, parsers) @@ -1519,7 +1540,7 @@ .. method:: M4A_ILST_Leaf_Atom.__unicode__() - Returns the unicode value of this leaf's first ``data`` child atom. + Returns the Unicode value of this leaf's first ``data`` child atom. >>> nam = M4A_ILST_Leaf_Atom("\xa9nam", ... [M4A_ILST_Unicode_Data_Atom(0, 1, "Track Name")]) @@ -1548,7 +1569,7 @@ ^^^^^^^^^^^^^^^^^^^^^^^^^^ This atom is a subclass of :class:`M4A_Leaf_Atom` -specialized for holding unicode data. +specialized for holding Unicode data. .. class:: M4A_ILST_Unicode_Data_Atom(type, flags, data) @@ -1559,7 +1580,7 @@ .. method:: M4A_ILST_Unicode_Data_Atom.__unicode__() - Returns the unicode value of this atom's data. + Returns the Unicode value of this atom's data. >>> nam = M4A_ILST_Unicode_Data_Atom(0, 1, "Track Name") >>> unicode(nam) @@ -1615,8 +1636,8 @@ Returns this atom's ``disc_total`` value. - >>> trkn = M4A_ILST_DISK_Data_Atom(3, 4) - >>> trkn.total() + >>> disk = M4A_ILST_DISK_Data_Atom(3, 4) + >>> disk.total() 4 M4A ILST Cover Art Atom @@ -1642,8 +1663,8 @@ This is a VorbisComment_ tag used by FLAC, Ogg FLAC, Ogg Vorbis, Ogg Speex and other formats in the Ogg family. - ``comment_strings`` is a list of unicode strings. - ``vendor_string`` is a unicode string. + ``comment_strings`` is a list of Unicode strings. + ``vendor_string`` is a Unicode string. Once initialized, :class:`VorbisComment` can be manipulated like a regular Python dict in addition to its standard :class:`audiotools.MetaData` methods. @@ -1691,6 +1712,14 @@ >>> tag.track_name u'Title1' +Opus Tags +--------- + +.. class:: OpusTags(comment_strings, vendor_string) + + OpusTags is a subclass of :class:`VorbisComment` + which supports the same field/metadata mapping. + .. _APEv2: http://wiki.hydrogenaudio.org/index.php?title=APEv2 .. _ID3v1: http://www.id3.org/ID3v1
View file
audiotools-2.18.tar.gz/docs/reference/Makefile -> audiotools-2.19.tar.gz/docs/reference/Makefile
Changed
@@ -2,10 +2,14 @@ ID3v22 = \ figures/id3v22/header.pdf \ figures/id3v22/frame.pdf \ +figures/id3v22/frames.pdf \ figures/id3v22/com.pdf \ +figures/id3v22/com-example.pdf \ figures/id3v22/geo.pdf \ figures/id3v22/pic.pdf \ +figures/id3v22/pic-example.pdf \ figures/id3v22/t__.pdf \ +figures/id3v22/t__-example.pdf \ figures/id3v22/txx.pdf \ figures/id3v22/ult.pdf \ figures/id3v22/w__.pdf \ @@ -14,10 +18,14 @@ ID3v23 = \ figures/id3v23/header.pdf \ figures/id3v23/frame.pdf \ +figures/id3v23/frames.pdf \ figures/id3v23/apic.pdf \ +figures/id3v23/apic-example.pdf \ figures/id3v23/comm.pdf \ +figures/id3v23/comm-example.pdf \ figures/id3v23/geob.pdf \ figures/id3v23/t___.pdf \ +figures/id3v23/t___-example.pdf \ figures/id3v23/txxx.pdf \ figures/id3v23/uslt.pdf \ figures/id3v23/w___.pdf \ @@ -26,72 +34,96 @@ ID3v24 = \ figures/id3v24/header.pdf \ figures/id3v24/frame.pdf \ +figures/id3v24/frames.pdf \ figures/id3v24/apic.pdf \ +figures/id3v24/apic-example.pdf \ figures/id3v24/comm.pdf \ +figures/id3v24/comm-example.pdf \ figures/id3v24/geob.pdf \ +figures/id3v24/size.pdf \ figures/id3v24/t___.pdf \ +figures/id3v24/t___-example.pdf \ figures/id3v24/txxx.pdf \ figures/id3v24/uslt.pdf \ figures/id3v24/w___.pdf \ figures/id3v24/wxxx.pdf FLAC = \ -figures/flac/application.pdf \ -figures/flac/block_header.pdf \ -figures/flac/constant.pdf \ -figures/flac/cuesheet.pdf \ -figures/flac/fixed.pdf \ -figures/flac/fixed2.pdf \ -figures/flac/fixed-parse.pdf \ -figures/flac/frame.pdf \ -figures/flac/frames.pdf \ -figures/flac/header-example.pdf \ -figures/flac/lag0.pdf \ -figures/flac/lag1.pdf \ -figures/flac/lag2.pdf \ -figures/flac/lag3.pdf \ -figures/flac/lpc.pdf \ -figures/flac/lpc2.pdf \ -figures/flac/lpc-parse.pdf \ -figures/flac/lpc-parse2.pdf \ -figures/flac/metadata.pdf \ -figures/flac/picture.pdf \ -figures/flac/read_utf8.pdf \ -figures/flac/residual.pdf \ -figures/flac/residual-example1.pdf \ -figures/flac/residual-example2.pdf \ -figures/flac/residual-example3.pdf \ -figures/flac/residual-example4.pdf \ -figures/flac/residual-example5.pdf \ -figures/flac/residual-parse.pdf \ -figures/flac/seektable.pdf \ -figures/flac/stream.pdf \ -figures/flac/stream2.pdf \ -figures/flac/stream3.pdf \ -figures/flac/streaminfo.pdf \ -figures/flac/subframes.pdf \ -figures/flac/tukey.pdf \ -figures/flac/utf8.pdf \ -figures/flac/verbatim.pdf \ -figures/flac/vorbiscomment.pdf \ -figures/flac/write_utf8.pdf \ +flac/metadata.tex \ +flac/decode.tex \ +flac/encode.tex \ +flac/encode/fixed.tex \ +flac/encode/residual.tex \ +flac/encode/lpc.tex \ +flac/figures/application.pdf \ +flac/figures/block_header.pdf \ +flac/figures/constant.pdf \ +flac/figures/cuesheet.pdf \ +flac/figures/cuesheet-example.pdf \ +flac/figures/fixed.pdf \ +flac/figures/fixed2.pdf \ +flac/figures/fixed-enc-example.pdf \ +flac/figures/fixed-parse.pdf \ +flac/figures/frame.pdf \ +flac/figures/frames.pdf \ +flac/figures/header-example.pdf \ +flac/figures/lag0.pdf \ +flac/figures/lag1.pdf \ +flac/figures/lag2.pdf \ +flac/figures/lag3.pdf \ +flac/figures/lpc.pdf \ +flac/figures/lpc2.pdf \ +flac/figures/lpc-parse.pdf \ +flac/figures/lpc-parse2.pdf \ +flac/figures/metadata.pdf \ +flac/figures/metadata-blocks.pdf \ +flac/figures/padding.pdf \ +flac/figures/picture.pdf \ +flac/figures/picture-example.pdf \ +flac/figures/read_utf8.pdf \ +flac/figures/residual.pdf \ +flac/figures/residual-example1.pdf \ +flac/figures/residual-example2.pdf \ +flac/figures/residual-example3.pdf \ +flac/figures/residual-example4.pdf \ +flac/figures/residual-example5.pdf \ +flac/figures/residual-parse.pdf \ +flac/figures/residuals-enc-example.pdf \ +flac/figures/seektable.pdf \ +flac/figures/seektable-example.pdf \ +flac/figures/stream.pdf \ +flac/figures/stream2.pdf \ +flac/figures/stream3.pdf \ +flac/figures/streaminfo.pdf \ +flac/figures/streaminfo-example.pdf \ +flac/figures/subframes.pdf \ +flac/figures/tukey.pdf \ +flac/figures/utf8.pdf \ +flac/figures/verbatim.pdf \ +flac/figures/vorbiscomment.pdf \ +flac/figures/vorbiscomment-example.pdf \ +flac/figures/write_utf8.pdf \ figures/oggflac_stream.pdf M4A = figures/m4a/quicktime.pdf \ +figures/m4a/alb-example.pdf \ figures/m4a/atom.pdf \ figures/m4a/atom2.pdf \ figures/m4a/atoms.pdf \ figures/m4a/data.pdf \ figures/m4a/disk.pdf \ +figures/m4a/disk-example.pdf \ figures/m4a/dref.pdf \ figures/m4a/ftyp.pdf \ figures/m4a/hdlr.pdf \ figures/m4a/mdhd.pdf \ figures/m4a/meta.pdf \ figures/m4a/meta_atoms.pdf \ +figures/m4a/meta-example.pdf \ figures/m4a/mp4a.pdf \ figures/m4a/mvhd.pdf \ +figures/m4a/nam-example.pdf \ figures/m4a/smhd.pdf \ figures/m4a/stco.pdf \ figures/m4a/stsc.pdf \ @@ -99,66 +131,92 @@ figures/m4a/stsz.pdf \ figures/m4a/stts.pdf \ figures/m4a/tkhd.pdf \ -figures/m4a/trkn.pdf - -WAVPACK = figures/wavpack/block_channels.pdf \ -figures/wavpack/block_header.pdf \ -figures/wavpack/block_header2.pdf \ -figures/wavpack/block_header_parse.pdf \ -figures/wavpack/channel_info.pdf \ -figures/wavpack/decoding_params.pdf \ -figures/wavpack/decorr_samples.pdf \ -figures/wavpack/decorr_samples_encode.pdf \ -figures/wavpack/decorr_samples_parse.pdf \ -figures/wavpack/decorr_terms.pdf \ -figures/wavpack/decorr_weights.pdf \ -figures/wavpack/decorr_weights_parse.pdf \ -figures/wavpack/decorrelation0.pdf \ -figures/wavpack/decorrelation1.pdf \ -figures/wavpack/decorrelation2.pdf \ -figures/wavpack/decorrelation3.pdf \ -figures/wavpack/decorrelation4.pdf \ -figures/wavpack/decorrelation5.pdf \ -figures/wavpack/entropy_vars.pdf \ -figures/wavpack/entropy_vars_parse.pdf \ -figures/wavpack/extended_integers.pdf \ -figures/wavpack/md5sum.pdf \ -figures/wavpack/pcm_sandwich.pdf \ -figures/wavpack/residuals.pdf \ -figures/wavpack/residuals_parse.pdf \ -figures/wavpack/residuals_parse2.pdf \ -figures/wavpack/sample_rate.pdf \ -figures/wavpack/stream.pdf \ -figures/wavpack/subblock.pdf \ -figures/wavpack/subblock_header.pdf \ -figures/wavpack/subblock_parse.pdf \ -figures/wavpack/terms_parse.pdf \ -figures/wavpack/typical_block.pdf - -SHORTEN = figures/shorten/block1.pdf \ -figures/shorten/block2.pdf \ -figures/shorten/filetype.pdf \ -figures/shorten/header_parse.pdf \ -figures/shorten/qlpc.pdf \ -figures/shorten/qlpc1.pdf \ -figures/shorten/stream.pdf \ -figures/shorten/verbatim.pdf - -ALAC = figures/alac/alac_atom.pdf \ -figures/alac/alac-atom-parse.pdf \ -figures/alac/atom.pdf \ -figures/alac/atoms.pdf \ -figures/alac/interlaced_frame.pdf \ -figures/alac/mdhd.pdf \ -figures/alac/mdhd-parse.pdf \ -figures/alac/noninterlaced_frame.pdf \ -figures/alac/residual-build.pdf \ -figures/alac/residual-parse.pdf \ -figures/alac/stream.pdf \ -figures/alac/subframe-build.pdf \ -figures/alac/subframe-parse.pdf \ -figures/alac/subframe_header.pdf \ -figures/alac/uncompressed_frame.pdf +figures/m4a/trkn.pdf \ +figures/m4a/trkn-example.pdf + + +WAVPACK = \ +wavpack/decode.tex \ +wavpack/decode/terms.tex \ +wavpack/decode/weights.tex \ +wavpack/decode/samples.tex \ +wavpack/decode/entropy.tex \ +wavpack/decode/bitstream.tex \ +wavpack/decode/decorrelation.tex \ +wavpack/encode.tex \ +wavpack/encode/correlation.tex \ +wavpack/encode/terms.tex \ +wavpack/encode/weights.tex \ +wavpack/encode/samples.tex \ +wavpack/encode/entropy.tex \ +wavpack/encode/bitstream.tex \ +wavpack/figures/block_channels.pdf \ +wavpack/figures/block_header.pdf \ +wavpack/figures/block_header2.pdf \ +wavpack/figures/block_header_parse.pdf \ +wavpack/figures/channel_info.pdf \ +wavpack/figures/decoding_params.pdf \ +wavpack/figures/decorr_samples.pdf \ +wavpack/figures/decorr_samples_encode.pdf \ +wavpack/figures/decorr_samples_parse.pdf \ +wavpack/figures/decorr_terms.pdf \ +wavpack/figures/decorr_weights.pdf \ +wavpack/figures/decorr_weights_parse.pdf \ +wavpack/figures/decorrelation0.pdf \ +wavpack/figures/decorrelation1.pdf \ +wavpack/figures/decorrelation2.pdf \ +wavpack/figures/decorrelation3.pdf \ +wavpack/figures/decorrelation4.pdf \ +wavpack/figures/decorrelation5.pdf \ +wavpack/figures/entropy_vars.pdf \ +wavpack/figures/entropy_vars_parse.pdf \ +wavpack/figures/extended_integers.pdf \ +wavpack/figures/md5sum.pdf \ +wavpack/figures/pcm_sandwich.pdf \ +wavpack/figures/residuals.pdf \ +wavpack/figures/residuals_parse.pdf \ +wavpack/figures/residuals_parse2.pdf \ +wavpack/figures/sample_rate.pdf \ +wavpack/figures/stream.pdf \ +wavpack/figures/subblock.pdf \ +wavpack/figures/subblock_header.pdf \ +wavpack/figures/subblock_parse.pdf \ +wavpack/figures/terms_parse.pdf \ +wavpack/figures/typical_block.pdf + +SHORTEN = \ +shorten/decode.tex \ +shorten/encode.tex \ +shorten/figures/block1.pdf \ +shorten/figures/block2.pdf \ +shorten/figures/filetype.pdf \ +shorten/figures/header_parse.pdf \ +shorten/figures/qlpc.pdf \ +shorten/figures/qlpc1.pdf \ +shorten/figures/stream.pdf \ +shorten/figures/verbatim.pdf + +ALAC = \ +alac/decode.tex \ +alac/encode.tex \ +alac/encode/atoms.tex \ +alac/encode/lpc.tex \ +alac/encode/residual.tex \ +alac/figures/alac_atom.pdf \ +alac/figures/alac-atom-parse.pdf \ +alac/figures/atom.pdf \ +alac/figures/atoms.pdf \ +alac/figures/interlaced_frame.pdf \ +alac/figures/mdhd.pdf \ +alac/figures/mdhd-parse.pdf \ +alac/figures/noninterlaced_frame.pdf \ +alac/figures/residual-build.pdf \ +alac/figures/residual-parse.pdf \ +alac/figures/stream.pdf \ +alac/figures/subframe-build.pdf \ +alac/figures/subframe-parse.pdf \ +alac/figures/subframe_header.pdf \ +alac/figures/uncompressed_frame.pdf MLP = figures/mlp/channel_parameters.pdf \ figures/mlp/codebook1.pdf \ @@ -218,16 +276,20 @@ figures/aobpcm/24bps_5ch.pdf \ figures/aobpcm/24bps_6ch.pdf -VORBIS = figures/ogg_packets.pdf \ -figures/ogg_stream.pdf \ +OGG = figures/ogg/packets.pdf \ +figures/ogg/stream.pdf + +VORBIS = figures/vorbis/stream.pdf \ figures/vorbis/codebooks.pdf \ figures/vorbis/comment.pdf \ +figures/vorbis/comment-example.pdf \ figures/vorbis/float32.pdf \ figures/vorbis/huffman_example1.pdf \ figures/vorbis/huffman_example2.pdf \ figures/vorbis/huffman_example3.pdf \ figures/vorbis/huffman_example4.pdf \ figures/vorbis/identification.pdf \ +figures/vorbis/identification_example.pdf \ figures/vorbis/ordered_entries.pdf \ figures/vorbis/setup_packet.pdf @@ -241,7 +303,9 @@ figures/mp3/granule.pdf \ figures/mp3/id3v1.pdf \ figures/mp3/id3v11.pdf \ +figures/mp3/id3v11-example.pdf \ figures/mp3/id3v2_stream.pdf \ +figures/mp3/id3v2_stream-example.pdf \ figures/mp3/main_data.pdf \ figures/mp3/scalefactors.pdf \ figures/mp3/side_data_1ch.pdf \ @@ -264,11 +328,21 @@ APE = \ figures/ape/stream.pdf \ figures/ape/descriptor.pdf \ +figures/ape/descriptor-example.pdf \ figures/ape/header.pdf \ -figures/ape/apev2_item.pdf \ -figures/ape/apev2_tag.pdf \ -figures/ape/apev2_tagheader.pdf \ -figures/ape/apev2_flags.pdf +figures/ape/header-example.pdf + +APEV2 = \ +figures/apev2/footer-example.pdf \ +figures/apev2/footer-flags.pdf \ +figures/apev2/full-example.pdf \ +figures/apev2/header.pdf \ +figures/apev2/header-flags.pdf \ +figures/apev2/item.pdf \ +figures/apev2/item-example1.pdf \ +figures/apev2/item-example2.pdf \ +figures/apev2/item-example3.pdf \ +figures/apev2/tag.pdf MUSEPACK = \ figures/musepack/sv7_stream.pdf \ @@ -295,8 +369,10 @@ $(SHORTEN) \ $(FLAC) \ $(WAVPACK) \ +$(APEV2) \ $(APE) \ $(MP3) \ +$(OGG) \ $(ID3v22) \ $(ID3v23) \ $(ID3v24) \ @@ -319,6 +395,7 @@ shorten.tex \ flac.tex \ wavpack.tex \ +apev2.tex \ ape.tex \ mp3.tex \ m4a.tex \ @@ -340,7 +417,7 @@ clean: rm -f *.toc *.aux *.log *.out $(FIGURES) - rm -f figures/pcm.fig figures/flac/tukey.fig + rm -f figures/pcm.fig flac/figures/tukey.fig rm -f audiotools-a4.tex audiotools-letter.tex audioformats-letter.pdf: audioformats-letter.tex $(CHAPTERS) $(FIGURES) @@ -354,41 +431,64 @@ mp3_decode.pdf: mp3_decode.tex $(MP3) python pdflatexbuild.py -halt-on-error $< -flac-codec.tex: flac-codec.m4 header.m4 footer.m4 - m4 -D PAPERSIZE=letterpaper $< > $@ +flac-codec.tex: flac-codec.template header.tex footer.tex + python simple-template.py -D PAPERSIZE=letterpaper $< > $@ flac-codec.pdf: flac-codec.tex flac.tex $(FLAC) python pdflatexbuild.py -halt-on-error $< -alac-codec.tex: alac-codec.m4 header.m4 footer.m4 - m4 -D PAPERSIZE=letterpaper $< > $@ +alac-codec.tex: alac-codec.template header.tex footer.tex + python simple-template.py -D PAPERSIZE=letterpaper $< > $@ alac-codec.pdf: alac-codec.tex alac.tex $(ALAC) python pdflatexbuild.py -halt-on-error $< -wavpack-codec.tex: wavpack-codec.m4 header.m4 footer.m4 - m4 -D PAPERSIZE=letterpaper $< > $@ +wavpack-codec.tex: wavpack-codec.template header.tex footer.tex + python simple-template.py -D PAPERSIZE=letterpaper $< > $@ wavpack-codec.pdf: wavpack-codec.tex wavpack.tex $(WAVPACK) python pdflatexbuild.py -halt-on-error $< -shorten-codec.tex: shorten-codec.m4 header.m4 footer.m4 - m4 -D PAPERSIZE=letterpaper $< > $@ +shorten-codec.tex: shorten-codec.template header.tex footer.tex + python simple-template.py -D PAPERSIZE=letterpaper $< > $@ shorten-codec.pdf: shorten-codec.tex shorten.tex $(SHORTEN) python pdflatexbuild.py -halt-on-error $< -dvda-codec.tex: dvda-codec.m4 header.m4 footer.m4 - m4 -D PAPERSIZE=letterpaper $< > $@ +dvda-codec.tex: dvda-codec.template header.tex footer.tex + python simple-template.py -D PAPERSIZE=letterpaper $< > $@ dvda-codec.pdf: dvda-codec.tex dvda2.tex $(DVDA) python pdflatexbuild.py -halt-on-error $< -audioformats-letter.tex: audioformats.m4 header.m4 footer.m4 - m4 -D PAPERSIZE=letterpaper $< > $@ +mp3-codec.tex: mp3-codec.template header.tex footer.tex + python simple-template.py -D PAPERSIZE=letterpaper $< > $@ + +mp3-codec.pdf: mp3-codec.tex mp3.tex $(MP3) $(ID3v22) $(ID3v23) $(ID3v24) + +ogg-codec.tex: ogg-codec.template header.tex footer.tex + python simple-template.py -D PAPERSIZE=lettepaper $< > $@ -audioformats-a4.tex: audioformats.m4 header.m4 footer.m4 - m4 -D PAPERSIZE=a4paper $< > $@ +ogg-codec.pdf: ogg-codec.tex ogg.tex $(OGG) + python pdflatexbuild.py -halt-on-error $< + +vorbis-codec.tex: vorbis-codec.template header.tex footer.tex + python simple-template.py -D PAPERSIZE=letterpaper $< > $@ + +vorbis-codec.pdf: vorbis-codec.tex vorbis.tex $(VORBIS) + python pdflatexbuild.py -halt-on-error $< + +ape-codec.tex: ape-codec.template header.tex footer.tex + python simple-template.py -D PAPERSIZE=letterpaper $< > $@ + +ape-codec.pdf: ape-codec.tex ape.tex $(APE) + python pdflatexbuild.py -halt-on-error $< + +audioformats-letter.tex: audioformats.template header.tex footer.tex + python simple-template.py -D PAPERSIZE=letterpaper $< > $@ + +audioformats-a4.tex: audioformats.template header.tex footer.tex + python simple-template.py -D PAPERSIZE=a4paper $< > $@ figures/pcm.pdf: figures/pcm.fig fig2dev -Z 3 -L pdf $< $@ @@ -396,22 +496,22 @@ figures/pcm.fig: figures/pcm.plot ./figures/pcm.plot > $@ -figures/flac/tukey.pdf: figures/flac/tukey.fig +flac/figures/tukey.pdf: flac/figures/tukey.fig fig2dev -Z 3 -L pdf $< $@ -figures/flac/tukey.fig: figures/flac/tukey.plot - ./figures/flac/tukey.plot > $@ +flac/figures/tukey.fig: flac/figures/tukey.plot + ./flac/figures/tukey.plot > $@ -figures/flac/lag0.pdf: figures/flac/lag0.fig +flac/figures/lag0.pdf: flac/figures/lag0.fig fig2dev -Z 3.5 -L pdf $< $@ -figures/flac/lag1.pdf: figures/flac/lag1.fig +flac/figures/lag1.pdf: flac/figures/lag1.fig fig2dev -Z 3.5 -L pdf $< $@ -figures/flac/lag2.pdf: figures/flac/lag2.fig +flac/figures/lag2.pdf: flac/figures/lag2.fig fig2dev -Z 3.5 -L pdf $< $@ -figures/flac/lag3.pdf: figures/flac/lag3.fig +flac/figures/lag3.pdf: flac/figures/lag3.fig fig2dev -Z 3.5 -L pdf $< $@ figures/freedb/sequence.pdf: figures/freedb/sequence.fig @@ -426,7 +526,7 @@ figures/m4a/meta_atoms.pdf: figures/m4a/meta_atoms.fig fig2dev -Z 3 -L pdf $< $@ -figures/alac/atoms.pdf: figures/alac/atoms.fig +alac/figures/atoms.pdf: alac/figures/atoms.fig fig2dev -Z 5 -L pdf $< $@ figures/bitstream_bigendian.pdf: figures/bitstream_bigendian.fig @@ -441,7 +541,7 @@ figures/vorbis/ordered_entries.pdf: figures/vorbis/ordered_entries.dot dot -Tpdf $< -o $@ -.SUFFIXES: .pdf .bdx .bpx .dot +.SUFFIXES: .pdf .bdx .bpx .bypx .dot .bdx.pdf: ./bitdiagram.py -i $< -o $@ @@ -449,6 +549,9 @@ .bpx.pdf: ./bitparse.py -i $< -o $@ +.bypx.pdf: + ./byteparse.py -i $< -o $@ + .dot.pdf: dot -Tpdf $< -o $@ @@ -464,80 +567,92 @@ figures/m4a/data.pdf: figures/m4a/data.bdx ./bitdiagram.py -w 306 -i $< -o $@ -figures/alac/atom.pdf: figures/m4a/atom.bdx +alac/figures/atom.pdf: figures/m4a/atom.bdx ./bitdiagram.py -w 324 -i $< -o $@ -figures/alac/alac-atom-parse.pdf: figures/alac/alac-atom-parse.bpx +alac/figures/alac-atom-parse.pdf: alac/figures/alac-atom-parse.bpx ./bitparse.py -w 480 -b 24 -i $< -o $@ -figures/alac/mdhd.pdf: figures/m4a/mdhd.bdx +alac/figures/mdhd.pdf: figures/m4a/mdhd.bdx ./bitdiagram.py -i $< -o $@ -# figures/alac/subframe-parse.pdf: figures/alac/subframe-parse.bpx +# alac/figures/subframe-parse.pdf: alac/figures/subframe-parse.bpx # ./bitparse.py -w 500 -b 24 -i $< -o $@ -figures/alac/subframe-parse.pdf: figures/alac/subframe-parse.bpx +alac/figures/subframe-parse.pdf: alac/figures/subframe-parse.bpx ./bitparse.py -w 432 -b 16 -i $< -o $@ -figures/alac/residual-parse.pdf: figures/alac/residual-parse.bpx +alac/figures/residual-parse.pdf: alac/figures/residual-parse.bpx ./bitparse.py -w 490 -b 24 -i $< -o $@ -figures/alac/residual-build.pdf: figures/alac/residual-build.bpx +alac/figures/residual-build.pdf: alac/figures/residual-build.bpx ./bitparse.py -w 490 -b 24 -i $< -o $@ -# figures/wavpack/block_header_parse.pdf: figures/wavpack/block_header_parse.bpx +# wavpack/figures/block_header_parse.pdf: wavpack/figures/block_header_parse.bpx # ./bitparse.py -w 480 -b 24 -i $< -o $@ -figures/wavpack/decorrelation0.fig: figures/wavpack/decorrelation1.plot figures/wavpack/decorr_pass0.dat - ./figures/wavpack/decorrelation0.plot > $@ +wavpack/figures/decorrelation0.fig: wavpack/figures/decorrelation1.plot wavpack/figures/decorr_pass0.dat + ./wavpack/figures/decorrelation0.plot > $@ -figures/wavpack/decorrelation0.pdf: figures/wavpack/decorrelation0.fig +wavpack/figures/decorrelation0.pdf: wavpack/figures/decorrelation0.fig fig2dev -Z 3 -L pdf $< $@ -figures/wavpack/decorrelation1.fig: figures/wavpack/decorrelation1.plot figures/wavpack/decorr_pass1.dat - ./figures/wavpack/decorrelation1.plot > $@ +wavpack/figures/decorrelation1.fig: wavpack/figures/decorrelation1.plot wavpack/figures/decorr_pass1.dat + ./wavpack/figures/decorrelation1.plot > $@ -figures/wavpack/decorrelation1.pdf: figures/wavpack/decorrelation1.fig +wavpack/figures/decorrelation1.pdf: wavpack/figures/decorrelation1.fig fig2dev -Z 3 -L pdf $< $@ -figures/wavpack/decorrelation2.fig: figures/wavpack/decorrelation2.plot - ./figures/wavpack/decorrelation2.plot > $@ +wavpack/figures/decorrelation2.fig: wavpack/figures/decorrelation2.plot + ./wavpack/figures/decorrelation2.plot > $@ -figures/wavpack/decorrelation2.pdf: figures/wavpack/decorrelation2.fig +wavpack/figures/decorrelation2.pdf: wavpack/figures/decorrelation2.fig fig2dev -Z 3 -L pdf $< $@ -figures/wavpack/decorrelation3.fig: figures/wavpack/decorrelation3.plot - ./figures/wavpack/decorrelation3.plot > $@ +wavpack/figures/decorrelation3.fig: wavpack/figures/decorrelation3.plot + ./wavpack/figures/decorrelation3.plot > $@ -figures/wavpack/decorrelation3.pdf: figures/wavpack/decorrelation3.fig +wavpack/figures/decorrelation3.pdf: wavpack/figures/decorrelation3.fig fig2dev -Z 3 -L pdf $< $@ -figures/wavpack/decorrelation4.fig: figures/wavpack/decorrelation4.plot - ./figures/wavpack/decorrelation4.plot > $@ +wavpack/figures/decorrelation4.fig: wavpack/figures/decorrelation4.plot + ./wavpack/figures/decorrelation4.plot > $@ -figures/wavpack/decorrelation4.pdf: figures/wavpack/decorrelation4.fig +wavpack/figures/decorrelation4.pdf: wavpack/figures/decorrelation4.fig fig2dev -Z 3 -L pdf $< $@ -figures/wavpack/decorrelation5.fig: figures/wavpack/decorrelation5.plot - ./figures/wavpack/decorrelation5.plot > $@ +wavpack/figures/decorrelation5.fig: wavpack/figures/decorrelation5.plot + ./wavpack/figures/decorrelation5.plot > $@ -figures/wavpack/decorrelation5.pdf: figures/wavpack/decorrelation5.fig +wavpack/figures/decorrelation5.pdf: wavpack/figures/decorrelation5.fig fig2dev -Z 3 -L pdf $< $@ -figures/wavpack/residuals_parse.pdf: figures/wavpack/residuals_parse.bpx +wavpack/figures/residuals_parse.pdf: wavpack/figures/residuals_parse.bpx ./bitparse.py -i $< -o $@ -b 24 -w 480 -figures/flac/utf8.pdf: figures/flac/utf8.bpx +wavpack/figures/block_header_parse.pdf: wavpack/figures/block_header_parse.bpx + ./bitparse.py -i $< -o $@ -b 24 -w 500 + +flac/figures/utf8.pdf: flac/figures/utf8.bpx ./bitparse.py -i $< -o $@ -b 8 -w 170 -figures/flac/lpc-parse2.pdf: figures/flac/lpc-parse2.bpx +flac/figures/lpc-parse2.pdf: flac/figures/lpc-parse2.bpx ./bitparse.py -i $< -o $@ -b 24 -w 480 -figures/shorten/filetype.pdf: figures/shorten/filetype.bpx +shorten/figures/filetype.pdf: shorten/figures/filetype.bpx ./bitparse.py -w 140 -b 7 -i $< -o $@ -basics-info.tex: basics-info.m4 header.m4 footer.m4 +basics-info.tex: basics-info.m4 header.tex footer.tex m4 -D PAPERSIZE=letterpaper $< > $@ basics-info.pdf: basics-info.tex basics.tex $(BASICS) python pdflatexbuild.py -halt-on-error $< + +flac/figures/cuesheet-example.pdf: flac/figures/cuesheet-example.bypx + ./byteparse.py --no-ascii --digits-per-row 20 -i $< -o $@ + +figures/apev2/header-flags.pdf: figures/apev2/header-flags.bpx + ./bitparse.py --bit-width=50 --bits-per-row=8 -i $< -o $@ + +figures/apev2/footer-flags.pdf: figures/apev2/footer-flags.bpx + ./bitparse.py --bit-width=50 --bits-per-row=8 -i $< -o $@
View file
audiotools-2.19.tar.gz/docs/reference/alac
Added
+(directory)
View file
audiotools-2.19.tar.gz/docs/reference/alac-codec.template
Added
@@ -0,0 +1,3 @@ +<<file:header.tex>> +\include{alac} +<<file:footer.tex>>
View file
audiotools-2.18.tar.gz/docs/reference/alac.tex -> audiotools-2.19.tar.gz/docs/reference/alac.tex
Changed
@@ -14,2466 +14,8 @@ \pageref{m4a}. The key difference is the contents of its \texttt{mdat} atom. -\section{ALAC Decoding} -\begin{wrapfigure}[6]{r}{1.5in} -\includegraphics{figures/alac/atoms.pdf} -\end{wrapfigure} -The basic process for decoding an ALAC file is as follows: -\par -\begin{wrapfigure}[6]{l}{4.5in} -{\relsize{-1} -\ALGORITHM{an ALAC encoded file}{PCM samples} -\hyperref[alac:read_alac_atom]{read \texttt{alac} atom to obtain decoding parameters}\; -\hyperref[alac:read_mdhd_atom]{read \texttt{mdhd} atom to obtain $PCM~frame~count$}\; -seek to \texttt{mdat} atom's data\; -\While{$PCM~frame~count > 0$}{ - \hyperref[alac:decode_frameset]{decode ALAC frameset to 1 or more PCM frames}\; - deduct ALAC frameset's samples from stream's $PCM~frame~count$\; - return decoded PCM frames\; -} -\EALGORITHM -} -\par -Seeking to a particular atom within the ALAC file is a recursive -process. -Each ALAC atom is laid out as follows: -\vskip .1in -\includegraphics{figures/alac/atom.pdf} -\vskip .1in -where \VAR{atom length} is the full size of the atom in bytes, -including the 8 byte atom header. -\VAR{atom type} is an ASCII string -\VAR{atom data} is a binary blob of data -which may contain one or more sub-atoms. -\end{wrapfigure} +\input{alac/decode} \clearpage -\subsection{Parsing the alac Atom} -\label{alac:read_alac_atom} -The \texttt{stsd} atom contains a single \texttt{alac} atom -which contains an \texttt{alac} sub-atom of its own. -\begin{figure}[h] -\includegraphics{figures/alac/alac_atom.pdf} -\end{figure} -\par -\noindent -Many of these fields appear redundant between the outer \texttt{alac} atom -and the inner sub-atom. -However, for proper decoding, one must ignore the outer atom entirely -and use only the parameters from the innermost \texttt{alac} atom. - -Of these, we'll be interested in \VAR{samples per frame}, -\VAR{bits per sample}, \VAR{history multiplier}, \VAR{initial history}, -\VAR{maximum K}, \VAR{channels} and \VAR{sample rate}. -The others can safely be ignored. - -\clearpage - -For example, given the bytes: -\par -\begin{figure}[h] -\includegraphics{figures/alac/alac-atom-parse.pdf} -\end{figure} -\begin{tabular}{rcrcl} -$alac~length$ & $\leftarrow$ & \texttt{00000024} & = & 36 \\ -$alac$ & $\leftarrow$ & \texttt{616C6163} & = & \texttt{"alac"} \\ -$padding$ & $\leftarrow$ & \texttt{00000000} & = & 0 \\ -samples per frame & $\leftarrow$ & \texttt{00001000} & = & 4096 \\ -compatible version & $\leftarrow$ & \texttt{00} & = & 0 \\ -bits per sample & $\leftarrow$ & \texttt{10} & = & 16 \\ -history multiplier & $\leftarrow$ & \texttt{28} & = & 40 \\ -initial history & $\leftarrow$ & \texttt{0A} & = & 10 \\ -maximum K & $\leftarrow$ & \texttt{0E} & = & 14 \\ -channels & $\leftarrow$ & \texttt{02} & = & 2 \\ -max run & $\leftarrow$ & \texttt{FF} & = & 255 \\ -max coded frame size & $\leftarrow$ & \texttt{24} & = & 36 bytes \\ -bitrate & $\leftarrow$ & \texttt{0AC4} & = & 2756 \\ -sample rate & $\leftarrow$ & \texttt{0000AC44} & = & 44100 Hz\\ -\end{tabular} - -\clearpage - -\subsection{Parsing the mdhd atom} -\label{alac:read_mdhd_atom} -\begin{figure}[h] -\includegraphics{figures/alac/mdhd.pdf} -\end{figure} -\par -\noindent -\VAR{version} indicates whether the Mac UTC date fields are 32 or 64 bit. -These date fields are seconds since the Macintosh Epoch, -which is 00:00:00, January 1st, 1904.\footnote{Why 1904? - It's the first leap year of the 20th century.} -To convert the Macintosh Epoch to a Unix Epoch timestamp -(seconds since January 1st, 1970), one needs to subtract 24,107 days - -or \texttt{2082844800} seconds. -\par -\noindent -\VAR{track length} is the total length of the ALAC file, in PCM frames. -\par -\noindent -\VAR{language} is 3, 5 bit fields encoded as ISO 639-2. -Add 96 to each field to convert the value to ASCII. - -\clearpage - -For example, given the bytes: -\begin{figure}[h] -\includegraphics{figures/alac/mdhd-parse.pdf} -\end{figure} -\par -\noindent -\begin{tabular}{rcrcll} -created MAC UTC date & $\leftarrow$ & \texttt{CA6BF4A9} & = & 3396072617 \\ -modified MAC UTC date & $\leftarrow$ & \texttt{CA6BFF5E} & = & 3396075358 \\ -sample rate & $\leftarrow$ & \texttt{0000AC44} & = & 44100 Hz \\ -PCM frame count & $\leftarrow$ & \texttt{8EACE80} & = & 149606016 & \texttt{56m 32s} \\ -$\text{language}_0$ & $\leftarrow$ & \texttt{15} & = & 21 & + 96 = `\texttt{u}'\\ -$\text{language}_1$ & $\leftarrow$ & \texttt{0E} & = & 14 & + 96 = `\texttt{n}'\\ -$\text{language}_2$ & $\leftarrow$ & \texttt{04} & = & 4 & + 96 = `\texttt{d}'\\ -\end{tabular} -\vskip .15in -\par -\noindent -Note that the language field is typically \texttt{und}, -meaning ``undetermined''. - -\clearpage - -\subsection{Decoding ALAC Frameset} -\label{alac:decode_frameset} -ALAC framesets contain multiple frames, -each of which contains 1 or 2 subframes. -\par -\noindent -\ALGORITHM{\texttt{mdat} atom data, decoding parameters from \texttt{alac} atom}{decoded PCM frames} -\SetKwData{CHANNELS}{channels} -$\CHANNELS \leftarrow$ (\READ 3 unsigned bits) + 1\; -\While{$\CHANNELS \neq 8$}{ - \hyperref[alac:decode_frame]{decode ALAC frame to 1 or 2 \CHANNELS of PCM data}\; - $\CHANNELS \leftarrow$ (\READ 3 unsigned bits) + 1\; -} -byte-align file stream\; -\Return all frames' channels as PCM frames\; -\EALGORITHM -\begin{figure}[h] -\includegraphics{figures/alac/stream.pdf} -\end{figure} - -\clearpage - -\subsection{Decoding ALAC Frame} -\label{alac:decode_frame} -{\relsize{-1} -\ALGORITHM{\texttt{mdat} atom data, channel count, decoding parameters from \texttt{alac} atom}{1 or 2 decoded channels of PCM data} -\SetKwData{CHANNELCOUNT}{channel count} -\SetKwData{SAMPLECOUNT}{sample count} -\SetKwData{SAMPLESPERFRAME}{samples per frame} -\SetKwData{BPS}{bits per sample} -\SetKwData{SAMPLESIZE}{sample size} -\SetKwData{HASSAMPLECOUNT}{has sample count} -\SetKwData{UNCOMPRESSEDLSBS}{uncompressed LSBs} -\SetKwData{NOTCOMPRESSED}{not compressed} -\SetKwData{INTERLACINGSHIFT}{interlacing shift} -\SetKwData{INTERLACINGLEFTWEIGHT}{interlacing leftweight} -\SetKwData{SUBFRAMEHEADER}{subframe header} -\SetKwData{QLPSHIFTNEEDED}{QLP shift needed} -\SetKwData{COEFF}{QLP coefficient} -\SetKwData{INITHISTORY}{initial history} -\SetKwData{MAXIMUMK}{maximum K} -\SetKwData{RESIDUALS}{residual block} -\SetKwData{LSB}{LSB} -\SetKwData{SUBFRAME}{subframe} -\SetKwData{CHANNEL}{channel} -\SetKw{AND}{and} -\SKIP 16 bits\; -$\HASSAMPLECOUNT \leftarrow$ \READ 1 unsigned bit\; -$\UNCOMPRESSEDLSBS \leftarrow$ \READ 2 unsigned bits\; -$\NOTCOMPRESSED \leftarrow$ \READ 1 unsigned bit\; -\uIf{$\HASSAMPLECOUNT = 0$}{ - \SAMPLECOUNT $\leftarrow$ \SAMPLESPERFRAME from \texttt{alac} atom\; -} -\lElse{\SAMPLECOUNT $\leftarrow$ \READ 32 unsigned bits\;} -\eIf(\tcc*[f]{raw, uncompressed frame}){$\NOTCOMPRESSED = 1$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}\SAMPLECOUNT}{ - \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ - $\text{\CHANNEL}_{c~i} \leftarrow$ \READ (\BPS) signed bits\; - } - } -}(\tcc*[f]{compressed frame}){ - $\SAMPLESIZE \leftarrow \BPS - (\UNCOMPRESSEDLSBS \times 8) + (\text{\CHANNELCOUNT} - 1)$\; - \INTERLACINGSHIFT $\leftarrow$ \READ 8 unsigned bits\; - \INTERLACINGLEFTWEIGHT $\leftarrow$ \READ 8 unsigned bits\; - \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ - $\left.\begin{tabular}{r} - $\text{\QLPSHIFTNEEDED}_c$ \\ - $\text{\COEFF}_c$ \\ -\end{tabular}\right\rbrace \leftarrow$ \hyperref[alac:read_subframe_header]{read subframe header}\; - } - \If{$\UNCOMPRESSEDLSBS > 0$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}\SAMPLECOUNT}{ - \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ - $\text{\LSB}_{c~i} \leftarrow$ \READ ($\UNCOMPRESSEDLSBS \times 8$) unsigned bits\; - } - } - } - \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ - $\text{\RESIDUALS}_c \leftarrow$ \hyperref[alac:read_residual_block]{read residual block} using $\left\lbrace\begin{tabular}{ll} - \INITHISTORY & from \texttt{alac} atom \\ - \MAXIMUMK & from \texttt{alac} atom \\ - \SAMPLESIZE \\ - \SAMPLECOUNT \\ -\end{tabular}\right.$\; - $\text{\SUBFRAME}_c \leftarrow$ \hyperref[alac:decode_subframe]{decode subframe using} $\left\lbrace\begin{tabular}{l} - $\text{\QLPSHIFTNEEDED}_c$ \\ - $\text{\COEFF}_c$ \\ - $\text{\RESIDUALS}_c$ \\ - \SAMPLESIZE \\ - \SAMPLECOUNT \\ -\end{tabular}\right.$\; - } - \uIf{$(\CHANNELCOUNT = 2)$ \AND $(\INTERLACINGLEFTWEIGHT > 0)$}{ - \hyperref[alac:decorrelate_channels]{decorrelate $\text{\SUBFRAME}_0$ and $\text{\SUBFRAME}_1$ to $\text{\CHANNEL}_0$ and $\text{\CHANNEL}_1$}\newline according to \INTERLACINGSHIFT and \INTERLACINGLEFTWEIGHT\; - } - \lElse{\For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ - $\text{\CHANNEL}_c \leftarrow \text{\SUBFRAME}_c$\; - } - } - \If(\tcc*[f]{prepend any LSBs to each sample}){$\UNCOMPRESSEDLSBS > 0$}{ - \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ - \For{$i \leftarrow 0$ \emph{\KwTo}\SAMPLECOUNT}{ - $\text{\CHANNEL}_{c~i} \leftarrow (\text{\CHANNEL}_{c~i} \times 2 ^ {\UNCOMPRESSEDLSBS \times 8}) + \text{\LSB}_{c~i}$\; - } - } - } -} -\Return \CHANNEL\; -\EALGORITHM -} - -\clearpage - -\subsection{Reading Subframe Header} -\label{alac:read_subframe_header} -{\relsize{-1} -\ALGORITHM{\texttt{mdat} atom data}{QLP shift needed, QLP coefficient list} -\SetKw{OR}{or} -\SetKwData{PREDICTIONTYPE}{prediction type} -\SetKwData{QLPSHIFTNEEDED}{QLP shift needed} -\SetKwData{RICEMODIFIER}{Rice modifier} -\SetKwData{COEFFCOUNT}{coefficient count} -\SetKwData{COEFF}{QLP coefficient} -$\PREDICTIONTYPE \leftarrow$ \READ 4 unsigned bits\; -\ASSERT $\PREDICTIONTYPE = 0$\; -$\QLPSHIFTNEEDED \leftarrow$ \READ 4 unsigned bits\; -$\RICEMODIFIER \leftarrow$ \READ 3 unsigned bits\tcc*[r]{unused} -$\COEFFCOUNT \leftarrow$ \READ 5 unsigned bits\; -\ASSERT $(\COEFFCOUNT = 4)$ \OR $(\COEFFCOUNT = 8)$\; -\For{$i \leftarrow 0$ \emph{\KwTo}\COEFFCOUNT}{ - $\text{\COEFF}_i \leftarrow$ \READ 16 signed bits\; -} -\Return $\left\lbrace\begin{tabular}{l} -$\text{\QLPSHIFTNEEDED}$ \\ -$\text{\COEFF}$ \\ -\end{tabular}\right.$\; -\EALGORITHM -} -\begin{figure}[h] -\includegraphics{figures/alac/subframe_header.pdf} -\end{figure} -\par -\noindent -For example, given the bytes on the opposite page, -our frame and subframe headers are: -\begin{table}[h] -{\relsize{-1} -\begin{tabular}{rclcl} -\multicolumn{5}{l}{frame header:} \\ -\textsf{channels} & $\leftarrow$ & \texttt{0} (+1) &=& 2 \\ -\textsf{has sample count} & $\leftarrow$ & \texttt{1} \\ -\textsf{uncompressed LSBs} & $\leftarrow$ & \texttt{0} \\ -\textsf{not compressed} & $\leftarrow$ & \texttt{0} \\ -\textsf{sample count} & $\leftarrow$ & \texttt{0x19} &=& 25 \\ -\textsf{interlacing shift} & $\leftarrow$ & \texttt{2} \\ -\textsf{interlacing leftweight} & $\leftarrow$ & \texttt{2} \\ -\hline -\multicolumn{5}{l}{subframe header 0:} \\ -$\textsf{prediction type}_0$ & $\leftarrow$ & \texttt{0} \\ -$\textsf{QLP shift needed}_0$ & $\leftarrow$ & \texttt{9} \\ -$\textsf{Rice modifier}_0$ & $\leftarrow$ & \texttt{4} \\ -$\textsf{coefficient count}_0$ & $\leftarrow$ & \texttt{4} \\ -$\textsf{coefficient}_{0~0}$ & $\leftarrow$ & \texttt{0x05A6} &=& 1446 \\ -$\textsf{coefficient}_{0~1}$ & $\leftarrow$ & \texttt{0xF943} &=& -1725 \\ -$\textsf{coefficient}_{0~2}$ & $\leftarrow$ & \texttt{0x0430} &=& 1072 \\ -$\textsf{coefficient}_{0~3}$ & $\leftarrow$ & \texttt{0xFECF} &=& -305 \\ -\hline -\multicolumn{5}{l}{subframe header 1:} \\ -$\textsf{prediction type}_1$ & $\leftarrow$ & \texttt{0} \\ -$\textsf{QLP shift needed}_1$ & $\leftarrow$ & \texttt{9} \\ -$\textsf{Rice modifier}_1$ & $\leftarrow$ & \texttt{4} \\ -$\textsf{coefficient count}_1$ & $\leftarrow$ & \texttt{4} \\ -$\textsf{coefficient}_{1~0}$ & $\leftarrow$ & \texttt{0x0587} &=& 1415 \\ -$\textsf{coefficient}_{1~1}$ & $\leftarrow$ & \texttt{0xF987} &=& -1657 \\ -$\textsf{coefficient}_{1~2}$ & $\leftarrow$ & \texttt{0x03F3} &=& 1011 \\ -$\textsf{coefficient}_{1~3}$ & $\leftarrow$ & \texttt{0xFEE5} &=& -283 \\ -\end{tabular} -} -\end{table} - -\clearpage - -\begin{figure}[h] -\includegraphics{figures/alac/subframe-parse.pdf} -\caption{mdat atom bytes} -\end{figure} - -\clearpage - -\subsection{Reading Residual Block} -\label{alac:read_residual_block} -\ALGORITHM{\texttt{mdat} atom data, initial history and maximum K from \texttt{alac} atom, sample count, sample size}{a decoded list of signed residuals} -\SetKwData{SAMPLECOUNT}{sample count} -\SetKwData{INITHISTORY}{initial history} -\SetKwData{MAXIMUMK}{maximum K} -\SetKwData{SIGN}{sign modifier} -\SetKwData{HISTORY}{history} -\SetKwData{HISTORYMULT}{history multiplier} -\SetKwData{ZEROES}{zero residuals count} -\SetKw{AND}{and} -\SetKwFunction{MIN}{min} -\SetKwFunction{READRESIDUAL}{read residual} -\SetKwData{RESIDUAL}{residual} -$\HISTORY \leftarrow \INITHISTORY$\; -\SIGN $\leftarrow 0$\; -\For{$i \leftarrow 0$ \emph{\KwTo}\SAMPLECOUNT}{ - $\kappa \leftarrow \MIN(\lfloor\log_2(\HISTORY \div 2 ^ 9 + 3)\rfloor~,~\MAXIMUMK)$\; - $u_i \leftarrow \READRESIDUAL(\kappa~,~\text{sample size}) + \SIGN$\; - \SIGN $\leftarrow 0$\; - \BlankLine - \eIf(\tcc*[f]{apply sign bit to unsigned value}){$(u_i \bmod 0) = 0$}{ - $\text{\RESIDUAL}_i \leftarrow u_i \div 2$\; - }{ - $\text{\RESIDUAL}_i \leftarrow -((u_i + 1) \div 2)$\; - } - \BlankLine - \eIf(\tcc*[f]{update history}){$u_i \leq 65535$}{ - \HISTORY $\leftarrow \HISTORY + (u_i \times \HISTORYMULT) - \left\lfloor\frac{\HISTORY \times \HISTORYMULT}{2 ^ 9}\right\rfloor$\; - \If{$\HISTORY < 128$ \AND $(i + 1) < \SAMPLECOUNT$}{ - \tcc{generate run of 0 residuals if history gets too small} - $\kappa \leftarrow \MIN(7 - \lfloor\log_2(\HISTORY)\rfloor + ((\HISTORY + 16) \div 64)~,~\MAXIMUMK)$\; - \ZEROES $\leftarrow \READRESIDUAL(\kappa~,~16)$\; - \For{$j \leftarrow 0$ \emph{\KwTo}\ZEROES}{ - $\text{\RESIDUAL}_{i + j + 1} \leftarrow 0$\; - } - $i \leftarrow i + j$\; - \HISTORY $\leftarrow 0$\; - \If{$\ZEROES \leq 65535$}{ - \SIGN $\leftarrow 1$\; - } - } - }{ - \HISTORY $\leftarrow 65535$\; - } -} -\Return \RESIDUAL\; -\EALGORITHM - -\clearpage - -\subsubsection{Reading Residual} -\ALGORITHM{\texttt{mdat} atom data, $\kappa$, sample size}{an unsigned residual} -\SetKwData{MSB}{MSB} -\SetKwData{LSB}{LSB} -\SetKw{UNREAD}{unread} -\MSB $\leftarrow$ \UNARY with stop bit 0, to a maximum of 8 bits\; -\uIf{9, \texttt{1} bits encountered}{ - \Return \READ (sample size) unsigned bits\; -} -\uElseIf{$\kappa = 0$}{ - \Return \MSB\; -} -\Else{ - \LSB $\leftarrow$ \READ $\kappa$ unsigned bits\; - \uIf{$\LSB > 1$}{ - \Return $\MSB \times (2 ^ \kappa - 1) + (\LSB - 1)$\; - } - \uElseIf{$\LSB = 1$}{ - \UNREAD single \texttt{1} bit back into stream\; - \Return $\MSB \times (2 ^ \kappa - 1)$\; - } - \Else{ - \UNREAD single \texttt{0} bit back into stream\; - \Return $\MSB \times (2 ^ \kappa - 1)$\; - } -} -\EALGORITHM - -\begin{landscape} -\subsubsection{Residual Decoding Example} -For this example, \VAR{initial history} (from the \texttt{alac} atom) is 10. -\par -\begin{figure}[h] -\includegraphics{figures/alac/residual-parse.pdf} -\end{figure} -\par -\noindent -Note how unreading a bit when $i = 1$ means that $\text{LSB}_1$'s 3rd bit -(a \texttt{1} in this case) is also $\text{MSB}_2$'s 1st bit. -This is signified by $\text{}_1 \leftrightarrow \text{}_2$ -since the same bit is in both fields. - -\clearpage - -\begin{table}[h] -{\relsize{-1} -\renewcommand{\arraystretch}{1.25} -\begin{tabular}{r||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}} -$i$ & \kappa & \textsf{MSB}_i & \textsf{LSB}_i & \textsf{unsigned}_i & -\textsf{residual}_i & \textsf{history}_{i + 1} \\ -\hline -0 & -\lfloor\log_2(10 \div 2 ^ 9 + 3)\rfloor = 1 & -9 & & 64 & -64 \div 2 = 32 & -10 + (64 \times 40) - \left\lfloor\frac{10 \times 40}{2 ^ 9}\right\rfloor = 2570 -\\ -1 & -\lfloor\log_2(2570 \div 2 ^ 9 + 3)\rfloor = 3 & -2 & *1 & 2 \times (2 ^ 3 - 1) = 14 & -14 \div 2 = 7 & -2570 + (14 \times 40) - \left\lfloor\frac{2570 \times 40}{2 ^ 9}\right\rfloor = 2930 -\\ -2 & -\lfloor\log_2(2930 \div 2 ^ 9 + 3)\rfloor = 3 & -1 & 2 & 1 \times (2 ^ 3 - 1) + (2 - 1) = 8 & -8 \div 2 = 4 & -2930 + (8 \times 40) - \left\lfloor\frac{2930 \times 40}{2 ^ 9}\right\rfloor = 3022 -\\ -3 & -\lfloor\log_2(3022 \div 2 ^ 9 + 3)\rfloor = 3 & -0 & 5 & 0 \times (2 ^ 3 - 1) + (5 - 1) = 4 & -4 \div 2 = 2 & -3022 + (4 \times 40) - \left\lfloor\frac{3022 \times 40}{2 ^ 9}\right\rfloor = 2946 -\\ -4 & -\lfloor\log_2(2946 \div 2 ^ 9 + 3)\rfloor = 3 & -0 & 2 & 0 \times (2 ^ 3 - 1) + (2 - 1) = 1 & --((1 + 1) \div 2) = -1 & -2946 + (1 \times 40) - \left\lfloor\frac{2946 \times 40}{2 ^ 9}\right\rfloor = 2756 -\\ -5 & -\lfloor\log_2(2756 \div 2 ^ 9 + 3)\rfloor = 3 & -0 & 2 & 0 \times (2 ^ 3 - 1) + (2 - 1) = 1 & --((1 + 1) \div 2) = -1 & -2756 + (1 \times 40) - \left\lfloor\frac{2756 \times 40}{2 ^ 9}\right\rfloor = 2581 -\\ -6 & -\lfloor\log_2(2581 \div 2 ^ 9 + 3)\rfloor = 3 & -0 & 2 & 0 \times (2 ^ 3 - 1) + (2 - 1) = 1 & --((1 + 1) \div 2) = -1 & -2581 + (1 \times 40) - \left\lfloor\frac{2581 \times 40}{2 ^ 9}\right\rfloor = 2420 -\\ -7 & -\lfloor\log_2(2420 \div 2 ^ 9 + 3)\rfloor = 2 & -2 & 2 & 2 \times (2 ^ 2 - 1) + (2 - 1) = 7 & --((7 + 1) \div 2) = -4 & -2420 + (7 \times 40) - \left\lfloor\frac{2420 \times 40}{2 ^ 9}\right\rfloor = 2511 -\\ -8 & -\lfloor\log_2(2511 \div 2 ^ 9 + 3)\rfloor = 2 & -0 & 2 & 0 \times (2 ^ 2 - 1) + (2 - 1) = 1 & --((1 + 1) \div 2) = -1 & -2511 + (1 \times 40) - \left\lfloor\frac{2511 \times 40}{2 ^ 9}\right\rfloor = 2355 -\\ -9 & -\lfloor\log_2(2355 \div 2 ^ 9 + 3)\rfloor = 2 & -2 & 2 & 2 \times (2 ^ 2 - 1) + (2 - 1) = 7 & --((7 + 1) \div 2) = -4 & -2355 + (7 \times 40) - \left\lfloor\frac{2355 \times 40}{2 ^ 9}\right\rfloor = 2452 -\\ -10 & -\lfloor\log_2(2452 \div 2 ^ 9 + 3)\rfloor = 2 & -1 & *1 & 1 \times (2 ^ 2 - 1) = 3 & --((3 + 1) \div 2) = -2 & -2452 + (3 \times 40) - \left\lfloor\frac{2452 \times 40}{2 ^ 9}\right\rfloor = 2381 -\\ -11 & -\lfloor\log_2(2381 \div 2 ^ 9 + 3)\rfloor = 2 & -1 & 3 & 1 \times (2 ^ 2 - 1) + (3 - 1) = 5 & --((5 + 1) \div 2) = -3 & -2381 + (5 \times 40) - \left\lfloor\frac{2381 \times 40}{2 ^ 9}\right\rfloor = 2395 -\\ -12 & -\lfloor\log_2(2395 \div 2 ^ 9 + 3)\rfloor = 2 & -0 & 2 & 0 \times (2 ^ 2 - 1) + (2 - 1) = 1 & --((1 + 1) \div 2) = -1 & -2395 + (1 \times 40) - \left\lfloor\frac{2395 \times 40}{2 ^ 9}\right\rfloor = 2248 -\\ -13 & -\lfloor\log_2(2248 \div 2 ^ 9 + 3)\rfloor = 2 & -0 & 2 & 0 \times (2 ^ 2 - 1) + (2 - 1) = 1 & --((1 + 1) \div 2) = -1 & -2248 + (1 \times 40) - \left\lfloor\frac{2248 \times 40}{2 ^ 9}\right\rfloor = 2113 -\\ -14 & -\lfloor\log_2(2113 \div 2 ^ 9 + 3)\rfloor = 2 & -0 & 2 & 0 \times (2 ^ 2 - 1) + (2 - 1) = 1 & --((1 + 1) \div 2) = -1 & -2113 + (1 \times 40) - \left\lfloor\frac{2113 \times 40}{2 ^ 9}\right\rfloor = 1988 -\\ -15 & -\lfloor\log_2(1988 \div 2 ^ 9 + 3)\rfloor = 2 & -0 & 3 & 0 \times (2 ^ 2 - 1) + (3 - 1) = 2 & -2 \div 2 = 1 & -1988 + (2 \times 40) - \left\lfloor\frac{1988 \times 40}{2 ^ 9}\right\rfloor = 1913 -\\ -16 & -\lfloor\log_2(1913 \div 2 ^ 9 + 3)\rfloor = 2 & -0 & *0 & 0 \times (2 ^ 2 - 1) = 0 & -0 \div 2 = 0 & -1913 + (0 \times 40) - \left\lfloor\frac{1913 \times 40}{2 ^ 9}\right\rfloor = 1764 -\\ -17 & -\lfloor\log_2(1764 \div 2 ^ 9 + 3)\rfloor = 2 & -0 & *1 & 0 \times (2 ^ 2 - 1) = 0 & -0 \div 2 = 0 & -1764 + (0 \times 40) - \left\lfloor\frac{1764 \times 40}{2 ^ 9}\right\rfloor = 1627 -\\ -18 & -\lfloor\log_2(1627 \div 2 ^ 9 + 3)\rfloor = 2 & -2 & *1 & 2 \times (2 ^ 2 - 1) = 6 & -6 \div 2 = 3 & -1627 + (6 \times 40) - \left\lfloor\frac{1627 \times 40}{2 ^ 9}\right\rfloor = 1740 -\\ -19 & -\lfloor\log_2(1740 \div 2 ^ 9 + 3)\rfloor = 2 & -1 & 2 & 1 \times (2 ^ 2 - 1) + (2 - 1) = 4 & -4 \div 2 = 2 & -1740 + (4 \times 40) - \left\lfloor\frac{1740 \times 40}{2 ^ 9}\right\rfloor = 1765 -\\ -20 & -\lfloor\log_2(1765 \div 2 ^ 9 + 3)\rfloor = 2 & -0 & 3 & 0 \times (2 ^ 2 - 1) + (3 - 1) = 2 & -2 \div 2 = 1 & -1765 + (2 \times 40) - \left\lfloor\frac{1765 \times 40}{2 ^ 9}\right\rfloor = 1708 -\\ -21 & -\lfloor\log_2(1708 \div 2 ^ 9 + 3)\rfloor = 2 & -2 & 3 & 2 \times (2 ^ 2 - 1) + (3 - 1) = 8 & -8 \div 2 = 4 & -1708 + (8 \times 40) - \left\lfloor\frac{1708 \times 40}{2 ^ 9}\right\rfloor = 1895 -\\ -22 & -\lfloor\log_2(1895 \div 2 ^ 9 + 3)\rfloor = 2 & -0 & 3 & 0 \times (2 ^ 2 - 1) + (3 - 1) = 2 & -2 \div 2 = 1 & -1895 + (2 \times 40) - \left\lfloor\frac{1895 \times 40}{2 ^ 9}\right\rfloor = 1827 -\\ -23 & -\lfloor\log_2(1827 \div 2 ^ 9 + 3)\rfloor = 2 & -2 & *1 & 2 \times (2 ^ 2 - 1) = 6 & -6 \div 2 = 3 & -1827 + (6 \times 40) - \left\lfloor\frac{1827 \times 40}{2 ^ 9}\right\rfloor = 1925 -\\ -24 & -\lfloor\log_2(1925 \div 2 ^ 9 + 3)\rfloor = 2 & -1 & 2 & 1 \times (2 ^ 2 - 1) + (2 - 1) = 4 & -4 \div 2 = 2 & -1925 + (4 \times 40) - \left\lfloor\frac{1925 \times 40}{2 ^ 9}\right\rfloor = 1935 -\\ -\end{tabular} -\renewcommand{\arraystretch}{1.0} -} -\end{table} -\end{landscape} - -\clearpage - -\subsection{Decoding Subframe} -\label{alac:decode_subframe} -{\relsize{-1} -\ALGORITHM{sample count, sample size, QLP coefficients, QLP shift needed, signed residuals}{a list of signed subframe samples} -\SetKwData{RESIDUAL}{residual} -\SetKwData{SAMPLE}{sample} -\SetKwData{SAMPLESIZE}{sample size} -\SetKwData{COEFFCOUNT}{coefficient count} -\SetKwData{SAMPLECOUNT}{sample count} -\SetKwData{BASE}{base sample} -\SetKwData{QLPSUM}{QLP sum} -\SetKwData{QLPCOEFF}{QLP coefficient} -\SetKwData{QLPSHIFT}{QLP shift needed} -\SetKwData{DIFF}{diff} -\SetKwData{SSIGN}{sign} -\SetKw{BREAK}{break} -\SetKwData{ORIGSIGN}{original sign} -\SetKwFunction{TRUNCATE}{truncate} -\SetKwFunction{SIGN}{sign} -$\text{\SAMPLE}_0 \leftarrow \text{\RESIDUAL}_0$\tcc*[r]{first sample always copied verbatim} -\eIf{$\COEFFCOUNT < 31$}{ - \For{$i \leftarrow 1$ \emph{\KwTo}$\text{\COEFFCOUNT} + 1$}{ - $\text{\SAMPLE}_i \leftarrow \TRUNCATE(\text{\RESIDUAL}_{i} + \text{\SAMPLE}_{i - 1}~,~\SAMPLESIZE)$\; - } - \BlankLine - \For{i $\leftarrow \text{\COEFFCOUNT} + 1$ \emph{\KwTo}$\text{\SAMPLECOUNT}$}{ - $\text{\BASE}_i \leftarrow \text{\SAMPLE}_{i - \COEFFCOUNT - 1}$\; - $\text{\QLPSUM}_i \leftarrow \overset{\COEFFCOUNT - 1}{\underset{j = 0}{\sum}} \text{\QLPCOEFF}_j \times (\text{\SAMPLE}_{i - j - 1} - \text{\BASE}_i)$\; - $\text{\SAMPLE}_i \leftarrow \TRUNCATE\left(\left\lfloor\frac{\text{\QLPSUM}_i + 2 ^ \text{\QLPSHIFT - 1}}{2 ^ \text{\QLPSHIFT}}\right\rfloor + \text{\RESIDUAL}_i + \text{\BASE}_i~,~\SAMPLESIZE\right)$\; - \BlankLine - \uIf(\tcc*[f]{modify QLP coefficients}){$\text{\RESIDUAL}_i > 0$}{ - \For{$j \leftarrow 0$ \emph{\KwTo}$\text{\COEFFCOUNT}$}{ - $\DIFF \leftarrow \text{\BASE}_i - \text{\SAMPLE}_{i - \COEFFCOUNT + j}$\; - $\SSIGN \leftarrow \SIGN(\DIFF)$\; - $\text{\QLPCOEFF}_{\COEFFCOUNT - j - 1} \leftarrow \text{\QLPCOEFF}_{\COEFFCOUNT - j - 1} - \SSIGN$\; - $\text{\RESIDUAL}_i \leftarrow \text{\RESIDUAL}_i - \left\lfloor\frac{\DIFF \times \SSIGN}{2 ^ \text{\QLPSHIFT}}\right\rfloor \times (j + 1)$\; - \If{$\text{\RESIDUAL}_i \leq 0$}{ - \BREAK\; - } - } - } - \ElseIf{$\text{\RESIDUAL}_i < 0$}{ - \For{$j \leftarrow 0$ \emph{\KwTo}$\text{\COEFFCOUNT}$}{ - $\DIFF \leftarrow \text{\BASE}_i - \text{\SAMPLE}_{i - \COEFFCOUNT + j}$\; - $\SSIGN \leftarrow \SIGN(\DIFF)$\; - $\text{\QLPCOEFF}_{\COEFFCOUNT - j - 1} \leftarrow \text{\QLPCOEFF}_{\COEFFCOUNT - j - 1} + \SSIGN$\; - $\text{\RESIDUAL}_i \leftarrow \text{\RESIDUAL}_i - \left\lfloor\frac{\DIFF \times -\SSIGN}{2 ^ \text{\QLPSHIFT}}\right\rfloor \times (j + 1)$\; - \If{$\text{\RESIDUAL}_i \geq 0$}{ - \BREAK\; - } - } - } - } -}{ - \For{$i \leftarrow 1$ \emph{\KwTo}\SAMPLECOUNT}{ - $\text{\SAMPLE}_i \leftarrow \TRUNCATE(\text{\RESIDUAL}_i + \text{\SAMPLE}_{i - 1}~,~\SAMPLESIZE)$\; - } -} -\Return \SAMPLE\; -\EALGORITHM -} - -\subsubsection{The \texttt{truncate} Function} -{\relsize{-1} - \ALGORITHM{a signed sample, the maximum size of the sample in bits}{a truncated signed sample} - \SetKw{BAND}{bitwise and} - \SetKwData{SAMPLE}{sample} - \SetKwData{BITS}{bits} - \SetKwData{TRUNCATED}{truncated} - $\TRUNCATED \leftarrow \SAMPLE~\BAND~(2 ^ {\BITS} - 1)$\; - \eIf(\tcc*[f]{apply sign bit}){$(\TRUNCATED~\BAND~2 ^ {\BITS - 1}) \neq 0$}{ - \Return $\TRUNCATED - 2 ^ {\BITS}$\; - }{ - \Return \TRUNCATED\; - } - \EALGORITHM -} - -\clearpage - -\subsubsection{The \texttt{sign} Function} -{\relsize{-1} -\begin{equation*} -\texttt{sign}(x) = -\begin{cases} -\texttt{ 1} & \text{if } x > 0 \\ -\texttt{ 0} & \text{if } x = 0 \\ -\texttt{-1} & \text{if } x < 0 -\end{cases} -\end{equation*} -} - -\subsubsection{Decoding Subframe Example} -Given the residuals -\texttt{32, 7, 4, 2, -1, -1, -1, -4, -1, -4, -2}, -the QLP coefficients -\texttt{1446, -1725, 1072, -305} -and a QLP shift needed value of \texttt{9}, -the subframe samples are calculated as follows: -\begin{table}[h] -{\relsize{-1} -\begin{tabular}{r||r|r|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}} -$i$ & $\textsf{residual}_i$ & $\textsf{base}_i$ & \textsf{QLP sum}_i & \textsf{sample}_i & \textsf{QLP coefficient}_{(i + 1)~j} \\ -\hline -0 & 32 & & & 32 \\ -1 & 7 & & & 7 + 32 = 39 \\ -2 & 4 & & & 4 + 39 = 43 \\ -3 & 2 & & & 2 + 43 = 45 \\ -4 & -1 & & & -1 + 45 = 44 \\ -\hline -5 & -1 & 32 & 1446 \times (44 - 32) \texttt{ +} & \lfloor(4584 + 2 ^ 8) \div 2 ^ 9\rfloor - 1 + 32 = 40 & 1446 \\ -& & & -1725 \times (45 - 32) \texttt{ +}& & -1725 \\ -& & & 1072 \times (43 - 32) \texttt{ +} & & 1072 \\ -& & & -305 \times (39 - 32) \texttt{~~} & & -305 - 1 = -306 \\ -\hline -6 & -1 & 39 & 1446 \times (40 - 39) \texttt{ +} & \lfloor(-1971 + 2 ^ 8) \div 2 ^ 9\rfloor - 1 + 39 = 34 & 1446 \\ -& & & -1725 \times (44 - 39) \texttt{ +} & & -1725 \\ -& & & 1072 \times (45 - 39) \texttt{ +} & & 1072 \\ -& & & -306 \times (43 - 39) \texttt{~~} & & -306 - 1 = -307 \\ -\hline -7 & -4 & 43 & 1446 \times (34 - 43) \texttt{ +} & \lfloor(-7381 + 2 ^ 8) \div 2 ^ 9\rfloor - 4 + 43 = 25 & 1446 \\ -& & & -1725 \times (40 - 43) \texttt{ +} & & -1725 + 1 = -1724 \\ -& & & 1072 \times (44 - 43) \texttt{ +} & & 1072 - 1 = 1071 \\ -& & & -307 \times (45 - 43) \texttt{~~} & & -307 - 1 = -308 \\ -\hline -8 & -1 & 45 & 1446 \times (25 - 45) \texttt{ +} & \lfloor(-15003 + 2 ^ 8) \div 2 ^ 9\rfloor - 1 + 45 = 15 & 1446 \\ -& & & -1724 \times (34 - 45) \texttt{ +} & & -1724 \\ -& & & 1071 \times (40 - 45) \texttt{ +} & & 1071 \\ -& & & -308 \times (44 - 45) \texttt{~~} & & -308 + 1 = -307 \\ -\hline -9 & -4 & 44 & 1446 \times (15 - 44) \texttt{ +} & \lfloor(-18660 + 2 ^ 8) \div 2 ^ 9\rfloor - 4 + 44 = 4 & 1446 \\ -& & & -1724 \times (25 - 44) \texttt{ +} & & -1724 + 1 = -1723 \\ -& & & 1071 \times (34 - 44) \texttt{ +} & & 1071 + 1 = 1072 \\ -& & & -307 \times (40 - 44) \texttt{~~} & & -307 + 1 = -306 \\ -\hline -10 & -2 & 40 & 1446 \times (4 - 40) \texttt{ +} & \lfloor(-23225 + 2 ^ 8) \div 2 ^ 9\rfloor - 2 + 40 = -7 & 1446 \\ -& & & -1723 \times (15 - 40) \texttt{ +} & & -1723 \\ -& & & 1072 \times (25 - 40) \texttt{ +} & & 1072 + 1 = 1073 \\ -& & & -306 \times (34 - 40) \texttt{~~} & & -306 + 1 = -305 \\ -\hline -\end{tabular} -} -\end{table} - -Although some steps have been omitted for brevity, -what's important to note is how the base sample -is removed prior to $\textsf{QLP sum}_i$ calculation, -how it is re-added during $\textsf{sample}_i$ calculation -and how the next sample's \textsf{QLP coefficient} values are shifted. - -\clearpage - -\subsection{Channel Decorrelation} -\label{alac:decorrelate_channels} -\ALGORITHM{$\text{subframe samples}_0$, $\text{subframe samples}_1$, interlacing shift and interlacing leftweight from frame header}{left and right channels} -\SetKwData{LEFTWEIGHT}{interlacing leftweight} -\SetKwData{SAMPLECOUNT}{sample count} -\SetKwData{SUBFRAME}{subframe} -\SetKwData{LEFT}{left} -\SetKwData{RIGHT}{right} -\For{$i \leftarrow 0$ \emph{\KwTo}\SAMPLECOUNT}{ - $\text{\RIGHT}_i \leftarrow \text{\SUBFRAME}_{0~i} - \left\lfloor\frac{\text{\SUBFRAME}_{1~i} \times \text{\LEFTWEIGHT}}{2 ^ \text{interlacing shift}}\right\rfloor$\; - $\text{\LEFT}_i \leftarrow \text{\SUBFRAME}_{1~i} + \text{\RIGHT}_i$ -} -\Return $(\LEFT~,~\RIGHT)$\; -\EALGORITHM -For example, given the $\textsf{subframe}_0$ samples of 14, 15, 19, 17, 18; -the $\textsf{subframe}_1$ samples of 16, 17, 26, 25, 24, -an \VAR{interlacing shift} value of 2 and an \VAR{interlacing leftweight} -values of 3, we calculate output samples as follows: -\begin{table}[h] -\begin{tabular}{|c||>{$}r<{$}|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|} -\hline -$i$ & \textsf{subframe}_{0~i} & \textsf{subframe}_{1~i} & \textsf{right}_i & \textsf{left}_i \\ -\hline -0 & 14 & 16 & 14 - \lfloor(16 \times 3) \div 2^2\rfloor = \textbf{2} & 16 + \textbf{2} = \textbf{18} \\ -1 & 15 & 17 & 15 - \lfloor(17 \times 3) \div 2^2\rfloor = \textbf{3} & 17 + \textbf{3} = \textbf{20} \\ -2 & 19 & 26 & 19 - \lfloor(26 \times 3) \div 2^2\rfloor = \textbf{0} & 26 + \textbf{0} = \textbf{26} \\ -3 & 17 & 25 & 17 - \lfloor(25 \times 3) \div 2^2\rfloor = \textbf{-1} & 25 + \textbf{-1} = \textbf{24} \\ -4 & 18 & 24 & 18 - \lfloor(24 \times 3) \div 2^2\rfloor = \textbf{0} & 24 + \textbf{0} = \textbf{24} \\ -\hline -\end{tabular} -\end{table} - -\subsection{Channel Assignment} -\begin{table}[h] -\begin{tabular}{r|l} -channels & assignment \\ -\hline -1 & mono \\ -2 & left, right \\ -3 & center, left, right \\ -4 & center, left, right, center surround \\ -5 & center, left, right, left surround, right surround \\ -6 & center, left, right, left surround, right surround, LFE \\ -7 & center, left, right, left surround, right surround, center surround, LFE \\ -8 & center, left center, right center, left, right, left surround, right surround, LFE \\ -\end{tabular} -\end{table} - -\clearpage - -\section{ALAC Encoding} - -To encode an ALAC file, we need a stream of PCM sample integers -along with that stream's sample rate, bits-per-sample and number of -channels. -We'll start by encoding all of the non-audio ALAC atoms, -most of which are contained within the \ATOM{moov} atom. -There's over twenty atoms in a typical ALAC file, -most of which are packed with seemingly redundant or -nonessential data, -so it will take awhile before we can move on to the actual -audio encoding process. - -Remember, all of an ALAC's fields are big-endian. - -\subsection{ALAC Atoms} -\begin{wrapfigure}[6]{r}{1.5in} -\includegraphics{figures/alac/atoms.pdf} -\end{wrapfigure} -We'll encode our ALAC file in iTunes order, which means -it contains the \ATOM{ftyp}, \ATOM{moov}, \ATOM{free} and -\ATOM{mdat} atoms, in that order. - -\subsubsection{the ftyp Atom} - -\begin{table}[h] -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & 32 \\ -atom type & 32 & `ftyp' (\texttt{0x66747970}) \\ -\hline -major brand & 32 & `M4A ' (\texttt{0x4d344120}) \\ -major brand version & 32 & \texttt{0} \\ -compatible brand & 32 & `M4A ' (\texttt{0x4d344120}) \\ -compatible brand & 32 & `mp42' (\texttt{0x6d703432}) \\ -compatible brand & 32 & `isom' (\texttt{0x69736f6d}) \\ -compatible brand & 32 & \texttt{0x00000000} \\ -\hline -\end{tabular} -\end{table} - -\subsubsection{the moov Atom} - -\begin{table}[h] -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & \ATOM{mvhd} size + \ATOM{trak} size + \ATOM{udta} size + 8 \\ -atom type & 32 & `moov' (\texttt{0x6d6f6f76}) \\ -\hline -\ATOM{mvhd} atom & \ATOM{mvhd} size & \ATOM{mvhd} data \\ -\ATOM{trak} atom & \ATOM{trak} size & \ATOM{trak} data \\ -\ATOM{udta} atom & \ATOM{udta} size & \ATOM{udta} data \\ -\hline -\end{tabular} -\end{table} - -\clearpage - -\subsubsection{the mvhd Atom} - -\begin{table}[h] -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & 108/120 \\ -atom type & 32 & `mvhd' (\texttt{0x6d766864}) \\ -\hline -version & 8 & \texttt{0x00} \\ -flags & 24 & \texttt{0x000000} \\ -created date & 32/64 & creation date as Mac UTC \\ -modified date & 32/64 & modification date as Mac UTC \\ -time scale & 32 & sample rate \\ -duration & 32/64 & total PCM frames \\ -playback speed & 32 & \texttt{0x10000} \\ -user volume & 16 & \texttt{0x100} \\ -padding & 80 & \texttt{0x00000000000000000000} \\ -window geometry matrix a & 32 & \texttt{0x10000} \\ -window geometry matrix b & 32 & \texttt{0} \\ -window geometry matrix u & 32 & \texttt{0} \\ -window geometry matrix c & 32 & \texttt{0} \\ -window geometry matrix d & 32 & \texttt{0x10000} \\ -window geometry matrix v & 32 & \texttt{0} \\ -window geometry matrix x & 32 & \texttt{0} \\ -window geometry matrix y & 32 & \texttt{0} \\ -window geometry matrix w & 32 & \texttt{0x40000000} \\ -QuickTime preview & 64 & \texttt{0} \\ -QuickTime still poster & 32 & \texttt{0} \\ -QuickTime selection time & 64 & \texttt{0} \\ -QuickTime current time & 32 & \texttt{0} \\ -next track ID & 32 & \texttt{2} \\ -\hline -\end{tabular} -\end{table} - -If \VAR{version} is 0, \VAR{created date}, \VAR{modified date} and -\VAR{duration} are 32 bit fields. -Otherwise, they are 64 bit fields. -The \VAR{created date} and \VAR{modified date} are seconds -since the Macintosh Epoch, which is 00:00:00, January 1st, 1904.\footnote{Why 1904? It's the first leap year of the 20th century.} -To convert a Unix Epoch timestamp (seconds since January 1st, 1970) to -a Macintosh Epoch, one needs to add 24,107 days - -or \texttt{2082844800} seconds. - -\clearpage - -\subsubsection{the trak Atom} -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & \ATOM{tkhd} size + \ATOM{mdia} size + 8 \\ -atom type & 32 & `trak' (\texttt{0x7472616b}) \\ -\hline -\ATOM{tkhd} atom & \ATOM{tkhd} size & \ATOM{tkhd} data \\ -\ATOM{mdia} atom & \ATOM{mdia} size & \ATOM{mdia} data \\ -\hline -\end{tabular} - -\subsubsection{the tkhd Atom} - -\begin{table}[h] -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & 92/104 \\ -atom type & 32 & `tkhd' (\texttt{0x746b6864}) \\ -\hline -version & 8 & \texttt{0x00} \\ -padding & 20 & \texttt{0x000000} \\ -track in poster & 1 & \texttt{0} \\ -track in preview & 1 & \texttt{1} \\ -track in movie & 1 & \texttt{1} \\ -track enabled & 1 & \texttt{1} \\ -created date & 32/64 & creation date as Mac UTC \\ -modified date & 32/64 & modification date as Mac UTC \\ -track ID & 32 & \texttt{1} \\ -padding & 32 & \texttt{0x00000000} \\ -duration & 32/64 & total PCM frames \\ -padding & 64 & \texttt{0x0000000000000000} \\ -video layer & 16 & \texttt{0} \\ -QuickTime alternate & 16 & \texttt{0} \\ -volume & 16 & \texttt{0x1000} \\ -padding & 16 & \texttt{0x0000} \\ -video geometry matrix a & 32 & \texttt{0x10000} \\ -video geometry matrix b & 32 & \texttt{0} \\ -video geometry matrix u & 32 & \texttt{0} \\ -video geometry matrix c & 32 & \texttt{0} \\ -video geometry matrix d & 32 & \texttt{0x10000} \\ -video geometry matrix v & 32 & \texttt{0} \\ -video geometry matrix x & 32 & \texttt{0} \\ -video geometry matrix y & 32 & \texttt{0} \\ -video geometry matrix w & 32 & \texttt{0x40000000} \\ -video width & 32 & \texttt{0} \\ -video height & 32 & \texttt{0} \\ -\hline -\end{tabular} -\end{table} - -\clearpage - -\subsubsection{the mdia Atom} - -\begin{table}[h] -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & \ATOM{mdhd} size + \ATOM{hdlr} size + \ATOM{minf} size + 8 \\ -atom type & 32 & `mdia' (\texttt{0x6d646961}) \\ -\hline -\ATOM{mdhd} atom & \ATOM{mdhd} size & \ATOM{mdhd} data \\ -\ATOM{hdlr} atom & \ATOM{hdlr} size & \ATOM{hdlr} data \\ -\ATOM{minf} atom & \ATOM{minf} size & \ATOM{minf} data \\ -\hline -\end{tabular} -\end{table} - -\subsubsection{the mdhd Atom} - -\begin{table}[h] -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & 32/44 \\ -atom type & 32 & `mdhd' (\texttt{0x6d646864}) \\ -\hline -version & 8 & \texttt{0x00} \\ -flags & 24 & \texttt{0x000000} \\ -created date & 32/64 & creation date as Mac UTC \\ -modified date & 32/64 & modification date as Mac UTC \\ -time scale & 32 & sample rate \\ -duration & 32/64 & total PCM frames \\ -padding & 1 & \texttt{0} \\ -language & 5 & \\ -language & 5 & language value as ISO 639-2 \\ -language & 5 & \\ -QuickTime quality & 16 & \texttt{0} \\ -\hline -\end{tabular} -\end{table} -Note the three, 5-bit \VAR{language} fields. -By adding 0x60 to each value and converting the result to ASCII characters, -the result is an \href{http://www.loc.gov/standards/iso639-2/}{ISO 639-2} -string of the file's language representation. -For example, given the values \texttt{0x15}, \texttt{0x0E} and \texttt{0x04}: -\begin{align*} -\text{language}_0 &= \texttt{0x15} + \texttt{0x60} = \texttt{0x75} = \texttt{u} \\ -\text{language}_1 &= \texttt{0x0E} + \texttt{0x60} = \texttt{0x6E} = \texttt{n} \\ -\text{language}_2 &= \texttt{0x04} + \texttt{0x60} = \texttt{0x64} = \texttt{d} -\end{align*} -Which is the code `\texttt{und}', meaning `undetermined' - which is typical. - -\clearpage - -\subsubsection{the hdlr Atom} -\label{alac_hdlr} -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & 33 + component \\ -atom type & 32 & `hdlr' (\texttt{0x68646c72}) \\ -\hline -version & 8 & \texttt{0x00} \\ -flags & 24 & \texttt{0x000000} \\ -QuickTime type & 32 & \texttt{0x00000000} \\ -QuickTime subtype & 32 & `soun' (\texttt{0x736f756e}) \\ -QuickTime manufacturer & 32 & \texttt{0x00000000} \\ -QuickTime component reserved flags & 32 & \texttt{0x00000000} \\ -QuickTime component reserved flags mask & 32 & \texttt{0x00000000} \\ -component name length & 8 & \texttt{0x00} \\ -component name & component name length $\times$ 8 & \\ -\hline -\end{tabular} - - -\subsubsection{the minf Atom} -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & \ATOM{smhd} size + \ATOM{dinf} size + \ATOM{stbl} size + 8 \\ -atom type & 32 & `minf' (\texttt{0x6d696e66}) \\ -\hline -\ATOM{smhd} atom & \ATOM{smhd} size & \ATOM{smhd} data \\ -\ATOM{dinf} atom & \ATOM{dinf} size & \ATOM{dinf} data \\ -\ATOM{stbl} atom & \ATOM{stbl} size & \ATOM{stbl} data \\ -\hline -\end{tabular} - -\subsubsection{the smhd Atom} -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & 16 \\ -atom type & 32 & `smhd' (\texttt{0x736d6864}) \\ -\hline -version & 8 & \texttt{0x00} \\ -flags & 24 & \texttt{0x000000} \\ -audio balance & 16 & \texttt{0x0000} \\ -padding & 16 & \texttt{0x0000} \\ -\hline -\end{tabular} - -\subsubsection{the dinf Atom} -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & \ATOM{dref} size + 8 \\ -atom type & 32 & `dinf' (\texttt{0x64696e66}) \\ -\hline -\ATOM{dref} atom & \ATOM{dref} size & \ATOM{dref} data \\ -\hline -\end{tabular} - -\clearpage - -\subsubsection{the dref Atom} - -\begin{table}[h] -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & 28 \\ -atom type & 32 & `dref' (\texttt{0x64726566}) \\ -\hline -version & 8 & \texttt{0x00} \\ -flags & 24 & \texttt{0x000000} \\ -number of references & 32 & \texttt{1} \\ -\hline -\hline -reference atom size & 32 & \texttt{12} \\ -reference atom type & 32 & `url ' (\texttt{0x75726c20}) \\ -reference atom data & 32 & \texttt{0x00000001} \\ -\hline -\end{tabular} -\end{table} - -\subsubsection{the stbl Atom} - -\begin{table}[h] -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & \ATOM{stsd} size + \ATOM{stts} size + \ATOM{stsc} size + \\ -& & \ATOM{stsz} size + \ATOM{stco} size + 8 \\ -atom type & 32 & `stbl' (\texttt{0x7374626c}) \\ -\hline -\ATOM{stsd} atom & \ATOM{stsd} size & \ATOM{stsd} data \\ -\ATOM{stts} atom & \ATOM{stts} size & \ATOM{stts} data \\ -\ATOM{stsc} atom & \ATOM{stsc} size & \ATOM{stsc} data \\ -\ATOM{stsz} atom & \ATOM{stsz} size & \ATOM{stsz} data \\ -\ATOM{stco} atom & \ATOM{stco} size & \ATOM{stco} data \\ -\hline -\end{tabular} -\end{table} - -\subsubsection{the stsd Atom} - -\begin{table}[h] -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & \ATOM{alac} size + 16 \\ -atom type & 32 & `stsd' (\texttt{0x73747364}) \\ -\hline -version & 8 & \texttt{0x00} \\ -flags & 24 & \texttt{0x000000} \\ -number of descriptions & 32 & \texttt{1} \\ -\hline -\ATOM{alac} atom & \ATOM{alac} size & \ATOM{alac} data \\ -\hline -\end{tabular} -\end{table} - -\clearpage - -\subsubsection{the alac Atom} - -\begin{table}[h] -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & 72 \\ -atom type & 32 & `alac' (\texttt{0x616c6163}) \\ -\hline -reserved & 48 & \texttt{0x000000000000} \\ -reference index & 16 & \texttt{1} \\ -version & 16 & \texttt{0} \\ -revision level & 16 & \texttt{0} \\ -vendor & 32 & \texttt{0x00000000} \\ -channels & 16 & channel count \\ -bits per sample & 16 & bits per sample \\ -compression ID & 16 & \texttt{0} \\ -audio packet size & 16 & \texttt{0} \\ -sample rate & 32 & \texttt{0xAC440000} \\ -\hline -\hline -atom length & 32 & 36 \\ -atom type & 32 & `alac' (\texttt{0x616c6163}) \\ -\hline -padding & 32 & \texttt{0x00000000} \\ -max samples per frame & 32 & largest number of PCM frames per ALAC frame \\ -padding & 8 & \texttt{0x00} \\ -sample size & 8 & bits per sample \\ -history multiplier & 8 & \texttt{40} \\ -initial history & 8 & \texttt{10} \\ -maximum K & 8 & \texttt{14} \\ -channels & 8 & channel count \\ -unknown & 16 & \texttt{0x00FF} \\ -max coded frame size & 32 & largest ALAC frame size, in bytes \\ -bitrate & 32 & $((\text{\ATOM{mdat} size} \times 8 ) \div (\text{total PCM frames} \div \text{sample rate}))$ \\ -sample rate & 32 & sample rate \\ -\hline -\end{tabular} -\end{table} -The \VAR{history multiplier}, \VAR{initial history} and \VAR{maximum K} -values are encode-time options, typically set to 40, 10 and 14, -respectively. - -Note that the \VAR{bitrate} field can't be known in advance; -we must fill that value with 0 for now and then -return to this atom once encoding is completed -and its size has been determined. - -\clearpage - -\subsubsection{the stts Atom} - -\begin{table}[h] -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & number of times $\times$ 8 + 16\\ -atom type & 32 & `stts' (\texttt{0x73747473}) \\ -\hline -version & 8 & \texttt{0x00} \\ -flags & 24 & \texttt{0x000000} \\ -number of times & 32 & \\ -\hline -frame count 1 & 32 & number of occurrences \\ -frame duration 1 & 32 & PCM frame count \\ -\hline -\multicolumn{3}{|c|}{...} \\ -\hline -\end{tabular} -\end{table} -This atom keeps track of how many different sizes of ALAC frames -occur in the ALAC file, in PCM frames. -It will typically have only two ``times'', the block size we're -using for most of our samples and the final block size for -any remaining samples. - -For example, let's imagine encoding a 1 minute audio file -at 44100Hz with a block size of 4096 frames. -This file has a total of 2,646,000 PCM frames ($60 \times 44100 = 2646000$). -2,646,000 PCM frames divided by a 4096 block size means -we have 645 ALAC frames of size 4096, and 1 ALAC frame of size 4080. - -Therefore: -\begin{align*} -\text{number of times} &= 2 \\ -\text{frame count}_1 &= 645 \\ -\text{frame duration}_1 &= 4096 \\ -\text{frame count}_2 &= 1 \\ -\text{frame duration}_2 &= 4080 -\end{align*} - -\subsubsection{the stsc Atom} - -\begin{table}[h] -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & entries $\times$ 12 + 16 \\ -atom type & 32 & `stsc' (\texttt{0x73747363}) \\ -\hline -version & 8 & \texttt{0x00} \\ -flags & 24 & \texttt{0x000000} \\ -number of entries & 32 & \\ -\hline -first chunk & 32 & \\ -ALAC frames per chunk & 32 & \\ -description index & 32 & \texttt{1} \\ -\hline -\multicolumn{3}{|c|}{...} \\ -\hline -\end{tabular} -\end{table} - -This atom stores how many ALAC frames are in a given ``chunk''. -In this instance a ``chunk'' represents an entry in -the \ATOM{stco} atom table, used for seeking backwards and forwards -through the file. -\VAR{First chunk} is the starting offset of its frames-per-chunk -value, beginning at 1. - -As an example, let's take a one minute, 44100Hz audio file -that's been broken into 130 chunks -(each with an entry in the \ATOM{stco} atom). -Its \ATOM{stsc} entries would typically be: -\begin{align*} -\text{first chunk}_1 &= 1 \\ -\text{frames per chunk}_1 &= 5 \\ -\text{first chunk}_2 &= 130 \\ -\text{frames per chunk}_2 &= 1 -\end{align*} -What this means is that chunks 1 through 129 have 5 ALAC frames each -while chunk 130 has 1 ALAC frame. -This is a total of 646 ALAC frames, which matches the contents of -the \ATOM{stts} atom. - -\subsubsection{the stsz Atom} - -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & sizes $\times$ 4 + 20 \\ -atom type & 32 & `stsz' (\texttt{0x7374737a}) \\ -\hline -version & 8 & \texttt{0x00} \\ -flags & 24 & \texttt{0x000000} \\ -block byte size & 32 & \texttt{0x00000000} \\ -number of sizes & 32 & \\ -\hline -frame size & 32 & \\ -\hline -\multicolumn{3}{|c|}{...} \\ -\hline -\end{tabular} - -This atom is a list of ALAC frame sizes, each in bytes. -For example, our 646 frame file would have 646 corresponding -\ATOM{stsz} entries. - -\subsubsection{the stco Atom} - -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & offset $\times$ 4 + 16 \\ -atom type & 32 & `stco' (\texttt{0x7374636f}) \\ -\hline -version & 8 & \texttt{0x00} \\ -flags & 24 & \texttt{0x000000} \\ -number of offsets & 32 & \\ -\hline -frame offset & 32 & \\ -\hline -\multicolumn{3}{|c|}{...} \\ -\hline -\end{tabular} - -This atom is a list of absolute file offsets for each chunk, where -each chunk is typically 5 ALAC frames large. - -\clearpage - -\subsubsection{the udta Atom} - -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & \ATOM{meta} size + 8 \\ -atom type & 32 & `udta' (\texttt{0x75647461}) \\ -\hline -\ATOM{meta} atom & \ATOM{meta} size & \ATOM{meta} data \\ -\hline -\end{tabular} - -\subsubsection{the meta Atom} - -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & \ATOM{hdlr} size + \ATOM{ilst} size + \ATOM{free} size + 12 \\ -atom type & 32 & `meta' (\texttt{0x6d657461}) \\ -\hline -version & 8 & \texttt{0x00} \\ -flags & 24 & \texttt{0x000000} \\ -\hline -\ATOM{hdlr} atom & \ATOM{hdlr} size & \ATOM{hdlr} data \\ -\ATOM{ilst} atom & \ATOM{ilst} size & \ATOM{ilst} data \\ -\ATOM{free} atom & \ATOM{free} size & \ATOM{free} data \\ -\hline -\end{tabular} - -\subsubsection{the hdlr atom (revisited)} - -\begin{tabular}{|l|r|l|} -\hline -Field & Size & Value \\ -\hline -atom length & 32 & 34 \\ -atom type & 32 & `hdlr' (\texttt{0x68646c72}) \\ -\hline -version & 8 & \texttt{0x00} \\ -flags & 24 & \texttt{0x000000} \\ -QuickTime type & 32 & \texttt{0x00000000} \\ -QuickTime subtype & 32 & `mdir' (\texttt{0x6d646972}) \\ -QuickTime manufacturer & 32 & `appl' (\texttt{0x6170706c}) \\ -QuickTime component reserved flags & 32 & \texttt{0x00000000} \\ -QuickTime component reserved flags mask & 32 & \texttt{0x00000000} \\ -component name length & 8 & \texttt{0x00} \\ -component name & 0 & \\ -\hline -\end{tabular} - -This atom is laid out identically to the ALAC file's primary -\ATOM{hdlr} atom (described on page \pageref{alac_hdlr}). -The only difference is the contents of its fields. - -\subsubsection{the ilst Atom} - -This atom is a collection of \ATOM{data} sub-atoms -and is described on page \pageref{m4a_meta}. - -\subsubsection{the free Atom} - -These atoms are simple collection of NULL bytes which can easily be -resized to make room for other atoms without rewriting the entire file. - -\clearpage - -\subsection{Encoding mdat Atom} -\ALGORITHM{PCM frames, various encoding parameters: -\newline -\begin{tabular}{rl} -parameter & typical value \\ -\hline -block size & 4096 \\ -initial history & 40 \\ -history multiplier & 10 \\ -maximum K & 14 \\ -interlacing shift & 2 \\ -minimum interlacing leftweight & 0 \\ -maximum interlacing leftweight & 4 \\ -\end{tabular} -}{an encoded \texttt{mdat} atom} -\SetKwData{BLOCKSIZE}{block size} -$0 \rightarrow$ \WRITE 32 unsigned bits\tcc*[r]{placeholder length} -$\texttt{"mdat"} \rightarrow$ \WRITE 4 bytes\; -\While{PCM frames remain}{ - take \BLOCKSIZE PCM frames from the input\; - \hyperref[alac:encode_frameset]{write PCM frames to frameset}\; -} -return to start of \texttt{mdat} atom and write actual length\; -\EALGORITHM -\begin{figure}[h] -\includegraphics{figures/alac/stream.pdf} -\end{figure} - -\clearpage - -\subsection{Encoding Frameset} -\label{alac:encode_frameset} -{\relsize{-2} -\ALGORITHM{1 or more channels of PCM frames}{1 or more ALAC frames as a frameset} -\SetKwData{CHANCOUNT}{channel count} -\SetKwData{FRAMEDATA}{frame channels} -\Switch{\CHANCOUNT}{ - \uCase{1}{ - \hyperref[alac:encode_frame]{encode mono as 1 channel frame}\; - } - \uCase{2}{ - \hyperref[alac:encode_frame]{encode left,right as 2 channel frame}\; - } - \uCase{3}{ - \hyperref[alac:encode_frame]{encode center as 1 channel frame}\; - \hyperref[alac:encode_frame]{encode left,right as 2 channel frame}\; - } - \uCase{4}{ - \hyperref[alac:encode_frame]{encode center as 1 channel frame}\; - \hyperref[alac:encode_frame]{encode left,right as 2 channel frame}\; - \hyperref[alac:encode_frame]{encode center surround as 1 channel frame}\; - } - \uCase{5}{ - \hyperref[alac:encode_frame]{encode center as 1 channel frame}\; - \hyperref[alac:encode_frame]{encode left,right as 2 channel frame}\; - \hyperref[alac:encode_frame]{encode left surround,right surround as 2 channel frame}\; - } - \uCase{6}{ - \hyperref[alac:encode_frame]{encode center as 1 channel frame}\; - \hyperref[alac:encode_frame]{encode left,right as 2 channel frame}\; - \hyperref[alac:encode_frame]{encode left surround,right surround as 2 channel frame}\; - \hyperref[alac:encode_frame]{encode LFE as 1 channel frame}\; - } - \uCase{7}{ - \hyperref[alac:encode_frame]{encode center as 1 channel frame}\; - \hyperref[alac:encode_frame]{encode left,right as 2 channel frame}\; - \hyperref[alac:encode_frame]{encode left surround,right surround as 2 channel frame}\; - \hyperref[alac:encode_frame]{encode center surround as 1 channel frame}\; - \hyperref[alac:encode_frame]{encode LFE as 1 channel frame}\; - } - \Case{8}{ - \hyperref[alac:encode_frame]{encode center as 1 channel frame}\; - \hyperref[alac:encode_frame]{encode left center,right center as 2 channel frame}\; - \hyperref[alac:encode_frame]{encode left,right as 2 channel frame}\; - \hyperref[alac:encode_frame]{encode left surround,right surround as 1 channel frame}\; - \hyperref[alac:encode_frame]{encode LFE as 1 channel frame}\; - } - $7 \rightarrow$ \WRITE 3 unsigned bits\; - byte align output stream\; -} -\Return encoded frameset\; -\EALGORITHM -} - -\subsubsection{Channel Assignment} -\begin{tabular}{r|l} -channels & assignment \\ -\hline -1 & mono \\ -2 & left, right \\ -3 & center, left, right \\ -4 & center, left, right, center surround \\ -5 & center, left, right, left surround, right surround \\ -6 & center, left, right, left surround, right surround, LFE \\ -7 & center, left, right, left surround, right surround, center surround, LFE \\ -8 & center, left center, right center, left, right, left surround, right surround, LFE \\ -\end{tabular} - -\clearpage - -\subsection{Encoding Frame} -\label{alac:encode_frame} -{\relsize{-1} -\ALGORITHM{1 or 2 channels of PCM data, encoding parameters}{a compressed or uncompressed ALAC frame} -\SetKwData{PCMCOUNT}{PCM frame count} -\SetKwData{CHANCOUNT}{channel count} -\SetKwData{COMPRESSED}{compressed frame} -\SetKwData{UNCOMPRESSED}{uncompressed frame} -\SetKwData{BPS}{bits per sample} -\SetKwFunction{LEN}{len} -$\CHANCOUNT - 1 \rightarrow$ \WRITE 3 unsigned bits\; -\eIf{$\text{\PCMCOUNT} \geq 10$}{ - $\UNCOMPRESSED \leftarrow$ \hyperref[alac:write_uncompressed_frame]{encode channel as uncompressed frame}\; - $\COMPRESSED \leftarrow$ \hyperref[alac:write_compressed_frame]{encode channels as compressed frame}\; - \uIf{residual overflow occurred}{ - \Return \UNCOMPRESSED\; - } - \uElseIf{$\LEN(\COMPRESSED) \geq \LEN(\UNCOMPRESSED)$}{ - \Return \UNCOMPRESSED\; - } - \Else{ - \Return \COMPRESSED\; - } -}{ - \Return \UNCOMPRESSED\; -} -\EALGORITHM -} - -\subsection{Encoding Uncompressed Frame} -\label{alac:write_uncompressed_frame} -{\relsize{-1} -\ALGORITHM{1 or 2 channels of PCM data, encoding parameters}{an uncompressed ALAC frame} -\SetKwData{PCMCOUNT}{PCM frame count} -\SetKwData{CHANCOUNT}{channel count} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{BPS}{bits per sample} -\SetKwData{CHANNEL}{channel} -$0 \rightarrow$ \WRITE 16 unsigned bits\tcc*[r]{unused} -\eIf{$\text{\PCMCOUNT} = \text{encoding parameter's \BLOCKSIZE}$}{ - $0 \rightarrow$ \WRITE 1 unsigned bit\; -}{ - $1 \rightarrow$ \WRITE 1 unsigned bit\; -} -$0 \rightarrow$ \WRITE 2 unsigned bits\tcc*[r]{uncompressed LSBs} -$1 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{not compressed} -\If{$\text{\PCMCOUNT} \neq \text{encoding parameter's \BLOCKSIZE}$}{ - $\PCMCOUNT \rightarrow$ \WRITE 32 unsigned bits\; -} -\For{$i \leftarrow 0$ \emph{\KwTo}\PCMCOUNT}{ - \For{$c \leftarrow 0$ \emph{\KwTo}\CHANCOUNT}{ - $\text{\CHANNEL}_{c~i} \rightarrow$ \WRITE (\BPS) signed bits\; - } -} -\Return uncompressed frame\; -\EALGORITHM -} - -\begin{figure}[h] - \includegraphics{figures/alac/uncompressed_frame.pdf} -\end{figure} - -\clearpage - -\subsection{Encoding Compressed Frame} -\label{alac:write_compressed_frame} -{\relsize{-1} -\ALGORITHM{1 or 2 channels of PCM data, encoding parameters}{a compressed ALAC frame, or a \textit{residual overflow} exception} -\SetKwData{PCMCOUNT}{PCM frame count} -\SetKwData{CHANCOUNT}{channel count} -\SetKwData{CHANNEL}{channel} -\SetKwData{FRAME}{frame} -\SetKwData{BPS}{bits per sample} -\SetKwData{HASLSBS}{uncompressed LSBs} -\SetKwData{MINWEIGHT}{minimum leftweight} -\SetKwData{MAXWEIGHT}{maximum leftweight} -\SetKwData{LSB}{LSB} -\eIf{$\text{\BPS} \leq 16$}{ - \HASLSBS $\leftarrow 0$\; -}(\tcc*[f]{extract uncompressed LSBs}){ - \HASLSBS $\leftarrow (\text{\BPS} - 16) \div 8$\; - \For{$i \leftarrow 0$ \emph{\KwTo}\PCMCOUNT}{ - \For{$c \leftarrow 0$ \emph{\KwTo}\CHANCOUNT}{ - $\text{\LSB}_{(i \times \CHANCOUNT) + c} \leftarrow \text{\CHANNEL}_{c~i} \bmod~2^{\text{\BPS} - 16}$\; - $\text{\CHANNEL}_{c~i} \leftarrow \left\lfloor\text{\CHANNEL}_{c~i} \div 2^{\text{\BPS} - 16}\right\rfloor$\; - } - } -} -\eIf{$\text{\CHANCOUNT} = 1$}{ - \Return \hyperref[alac:write_non_interlaced_frame]{non-interlaced frame for $\text{\CHANNEL}_0$}\; -}{ - \For{l $\leftarrow \MINWEIGHT$ \emph{\KwTo}$\text{\MAXWEIGHT} + 1$}{ - $\text{\FRAME}_l \leftarrow$ \hyperref[alac:write_interlaced_frame]{interlaced frame for $\text{\CHANNEL}_0~,~\text{\CHANNEL}_1$ with leftweight $l$}\; - } - \Return smallest $\text{\FRAME}_l$\; -} -\EALGORITHM -} - -\clearpage - -\subsection{Encoding Non-Interlaced Frame} -\label{alac:write_non_interlaced_frame} -{\relsize{-1} -\ALGORITHM{1 channel of PCM data, uncompressed LSBs, encoding parameters}{a compressed ALAC frame, or a \textit{residual overflow} exception} -\SetKwData{PCMCOUNT}{PCM frame count} -\SetKwData{CHANCOUNT}{channel count} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{BPS}{bits per sample} -\SetKwData{SAMPLESIZE}{sample size} -\SetKwData{QLPCOEFF}{QLP coefficient} -\SetKwData{RESIDUAL}{residual} -\SetKwData{CHANNEL}{channel} -\SetKwData{UNCOMPRESSEDLSB}{uncompressed LSBs} -\SetKwData{LSB}{LSB} -$0 \rightarrow$ \WRITE 16 unsigned bits\tcc*[r]{unused} -\eIf{$\text{\PCMCOUNT} \neq \text{encoding parameter's \BLOCKSIZE}$}{ - $1 \rightarrow$ \WRITE 1 unsigned bit\; -}{ - $0 \rightarrow$ \WRITE 1 unsigned bit\; -} -$\text{uncompressed LSBs} \rightarrow$ \WRITE 2 unsigned bits\; -$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{is compressed} -\If{$\text{\PCMCOUNT} \neq \text{encoding parameter's \BLOCKSIZE}$}{ - $\PCMCOUNT \rightarrow$ \WRITE 32 unsigned bits\; -} -$0 \rightarrow$ \WRITE 8 unsigned bits\tcc*[r]{interlacing shift} -$0 \rightarrow$ \WRITE 8 unsigned bits\tcc*[r]{interlacing leftweight} -\SAMPLESIZE $\leftarrow \text{\BPS} - (\text{uncompressed LSBs} \times 8)$\; -\hyperref[alac:compute_qlp_coeffs]{compute $\text{\QLPCOEFF}_0$ and $\text{\RESIDUAL}_0$ for $\text{\CHANNEL}_0$ with \SAMPLESIZE}\; -\hyperref[alac:write_subframe_header]{write subframe header with $\text{\QLPCOEFF}_0$}\; -\If{$\text{\UNCOMPRESSEDLSB} > 0$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}\PCMCOUNT}{ - $\text{\LSB}_i \rightarrow$ \WRITE $(\text{\UNCOMPRESSEDLSB} \times 8)$ unsigned bits\; - } -} -\hyperref[alac:write_residuals]{write residual block $\text{\RESIDUAL}_0$}\; -\BlankLine -\Return non-interlaced ALAC frame\; -\EALGORITHM -} - -\begin{figure}[h] - \includegraphics{figures/alac/noninterlaced_frame.pdf} -\end{figure} - -\clearpage - -\subsection{Encoding Interlaced Frame} -\label{alac:write_interlaced_frame} -{\relsize{-1} -\ALGORITHM{2 channels of PCM data, interlacing shift, interlacing leftweight, uncompressed LSBs, encoding parameters}{a compressed ALAC frame, or a \textit{residual overflow} exception} -\SetKwData{PCMCOUNT}{PCM frame count} -\SetKwData{CHANCOUNT}{channel count} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{BPS}{bits per sample} -\SetKwData{SAMPLESIZE}{sample size} -\SetKwData{UNCOMPRESSEDLSB}{uncompressed LSBs} -\SetKwData{INTERLACINGSHIFT}{interlacing shift} -\SetKwData{INTERLACINGLEFTWEIGHT}{interlacing leftweight} -\SetKwData{CHANNEL}{channel} -\SetKwData{CORRELATED}{correlated} -\SetKwData{QLPCOEFF}{QLP coefficient} -\SetKwData{RESIDUAL}{residual} -$0 \rightarrow$ \WRITE 16 unsigned bits\tcc*[r]{unused} -\eIf{$\text{\PCMCOUNT} \neq \text{encoding parameter's \BLOCKSIZE}$}{ - $1 \rightarrow$ \WRITE 1 unsigned bit\; -}{ - $0 \rightarrow$ \WRITE 1 unsigned bit\; -} -$\text{\UNCOMPRESSEDLSB} \rightarrow$ \WRITE 2 unsigned bits\; -$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{is compressed} -\If{$\text{\PCMCOUNT} \neq \text{encoding parameter's \BLOCKSIZE}$}{ - $\text{\PCMCOUNT} \rightarrow$ \WRITE 32 unsigned bits\; -} -$\text{\INTERLACINGSHIFT} \rightarrow$ \WRITE 8 unsigned bits\; -$\text{\INTERLACINGLEFTWEIGHT} \rightarrow$ \WRITE 8 unsigned bits\; -\BlankLine -\hyperref[alac:correlate_channels]{correlate $(\text{\CHANNEL}_0~,~\text{\CHANNEL}_1)$ to $(\text{\CORRELATED}_0~,~\text{\CORRELATED}_1)$}\newline with \INTERLACINGSHIFT and \INTERLACINGLEFTWEIGHT\; -\BlankLine -\SAMPLESIZE $\leftarrow \text{\BPS} - (\text{uncompressed LSBs} \times 8) + 1$\; -\hyperref[alac:compute_qlp_coeffs]{compute $\text{\QLPCOEFF}_0$ and $\text{\RESIDUAL}_0$ for $\text{\CORRELATED}_0$ with \SAMPLESIZE}\; -\hyperref[alac:compute_qlp_coeffs]{compute $\text{\QLPCOEFF}_1$ and $\text{\RESIDUAL}_1$ for $\text{\CORRELATED}_1$ with \SAMPLESIZE}\; -\hyperref[alac:write_subframe_header]{write subframe header with $\text{\QLPCOEFF}_0$}\; -\hyperref[alac:write_subframe_header]{write subframe header with $\text{\QLPCOEFF}_1$}\; -\BlankLine -\If{$\text{\UNCOMPRESSEDLSB} > 0$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}\PCMCOUNT}{ - $\text{LSB}_i \rightarrow$ \WRITE $(\text{\UNCOMPRESSEDLSB} \times 8)$ unsigned bits\; - } -} -\BlankLine -\hyperref[alac:write_residuals]{write residual block $\text{\RESIDUAL}_0$}\; -\hyperref[alac:write_residuals]{write residual block $\text{\RESIDUAL}_1$}\; -\BlankLine -\Return interlaced ALAC frame\; -\EALGORITHM -} - -\begin{figure}[h] - \includegraphics{figures/alac/interlaced_frame.pdf} -\end{figure} - -\clearpage - -\subsubsection{Correlating Channels} -\label{alac:correlate_channels} -{\relsize{-1} -\ALGORITHM{2 channels of PCM data, interlacing shift, interlacing leftweight}{2 correlated channels of PCM data} -\SetKwData{PCMCOUNT}{PCM frame count} -\SetKwData{LEFTWEIGHT}{interlacing leftweight} -\SetKwData{SHIFT}{interlacing shift} -\SetKwData{CORRELATED}{correlated} -\SetKwData{CHANNEL}{channel} -\eIf{$\text{\LEFTWEIGHT} > 0$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}\PCMCOUNT}{ - $\text{\CORRELATED}_{0~i} \leftarrow \text{\CHANNEL}_{1~i} + \left\lfloor\frac{(\text{\CHANNEL}_{0~i} - \text{\CHANNEL}_{1~i}) \times \text{\LEFTWEIGHT}}{2 ^ \text{\SHIFT}}\right\rfloor$\; - $\text{\CORRELATED}_{1~i} \leftarrow \text{\CHANNEL}_{0~i} - \text{\CHANNEL}_{1~i}$\; - } -}{ - \For{$i \leftarrow 0$ \emph{\KwTo}\PCMCOUNT}{ - $\text{\CORRELATED}_{0~i} \leftarrow \text{\CHANNEL}_{0~i}$\; - $\text{\CORRELATED}_{1~i} \leftarrow \text{\CHANNEL}_{1~i}$\; - } -} -\EALGORITHM -\par -\noindent -For example, given an \VAR{interlacing shift} value of 2 and an -\VAR{interlacing leftweight} value of 3: -\par -\noindent -{\relsize{-1} -\begin{tabular}{r||r|r||>{$}r<{$}|>{$}r<{$}|} -$i$ & $\textsf{channel}_{0~i}$ & $\textsf{channel}_{1~i}$ & \textsf{correlated}_{0~i} & \textsf{correlated}_{1~i} \\ -\hline -0 & 18 & 2 & 2 + \lfloor((18 - 2) \times 3) \div 2 ^ 2\rfloor = 14 & 18 - 2 = 16 \\ -1 & 20 & 3 & 3 + \lfloor((20 - 3) \times 3) \div 2 ^ 2\rfloor = 15 & 20 - 3 = 17 \\ -2 & 26 & 0 & 0 + \lfloor((26 - 0) \times 3) \div 2 ^ 2\rfloor = 19 & 26 - 0 = 26 \\ -3 & 24 & -1 & -1 + \lfloor((24 + 1) \times 3) \div 2 ^ 2\rfloor = 17 & 24 + 1 = 25 \\ -4 & 24 & 0 & 0 + \lfloor((24 - 0) \times 3) \div 2 ^ 2\rfloor = 18 & 24 - 0 = 24 \\ -\end{tabular} -} -} - -\clearpage - -\subsection{Computing QLP Coefficients and Residual} -\label{alac:compute_qlp_coeffs} -{\relsize{-1} -\ALGORITHM{a list of signed PCM samples, sample size, encoding parameters}{a list of 4 or 8 signed QLP coefficients, a block of residual data; or a \textit{residual overflow} exception} -\SetKwData{SAMPLES}{subframe samples} -\SetKwData{WINDOWED}{windowed} -\SetKwData{AUTOCORRELATION}{autocorrelated} -\SetKwData{LPCOEFF}{LP coefficient} -\SetKwData{QLPCOEFF}{QLP coefficient} -\SetKwData{SAMPLESIZE}{sample size} -\SetKwData{RESIDUAL}{residual} -\SetKwData{RESIDUALBLOCK}{residual block} -$\WINDOWED \leftarrow$ \hyperref[alac:window]{window signed integer \SAMPLES}\; -$\AUTOCORRELATION \leftarrow$ \hyperref[alac:autocorrelate]{autocorrelate \WINDOWED}\; -\eIf{$\text{\AUTOCORRELATION}_0 \neq 0.0$}{ - $\LPCOEFF \leftarrow$ \hyperref[alac:compute_lp_coeffs]{compute LP coefficients from \AUTOCORRELATION}\; - $\text{\QLPCOEFF}_3 \leftarrow$ \hyperref[alac:quantize_lp_coeffs]{quantize $\text{\LPCOEFF}_3$ at order 4}\; - $\text{\QLPCOEFF}_7 \leftarrow$ \hyperref[alac:quantize_lp_coeffs]{quantize $\text{\LPCOEFF}_7$ at order 8}\; - $\text{\RESIDUAL}_3 \leftarrow$ \hyperref[alac:calculate_residuals]{calculate residuals from $\text{\QLPCOEFF}_3$ and \SAMPLES}\; - $\text{\RESIDUAL}_7 \leftarrow$ \hyperref[alac:calculate_residuals]{calculate residuals from $\text{\QLPCOEFF}_7$ and \SAMPLES}\; - $\text{\RESIDUALBLOCK}_3 \leftarrow$ \hyperref[alac:write_residuals]{encode residual block from $\text{\RESIDUAL}_3$ with \SAMPLESIZE}\; - $\text{\RESIDUALBLOCK}_7 \leftarrow$ \hyperref[alac:write_residuals]{encode residual block from $\text{\RESIDUAL}_7$ with \SAMPLESIZE}\; - \eIf{$\LEN(\text{\RESIDUALBLOCK}_3) < (\LEN(\text{\RESIDUALBLOCK}_7) + 64~bits)$}{ - \Return ($\text{\QLPCOEFF}_3~,~\text{\RESIDUALBLOCK}_3$)\; - }{ - \Return ($\text{\QLPCOEFF}_7~,~\text{\RESIDUALBLOCK}_7$)\; - } -}(\tcc*[f]{all samples are 0}){ - \QLPCOEFF $\leftarrow$ \texttt{[0, 0, 0, 0]}\; - $\text{\RESIDUAL} \leftarrow$ \hyperref[alac:calculate_residuals]{calculate residuals from $\text{\QLPCOEFF}$ and \SAMPLES}\; - $\text{\RESIDUALBLOCK} \leftarrow$ \hyperref[alac:write_residuals]{encode residual block from $\text{\RESIDUAL}$ with \SAMPLESIZE}\; - \Return ($\text{\QLPCOEFF}~,~\text{\RESIDUALBLOCK}$)\; -} -\EALGORITHM -} - -\subsubsection{Windowing the Input Samples} -\label{alac:window} -{\relsize{-1} -\ALGORITHM{a list of signed input sample integers}{a list of signed windowed samples as floats} -\SetKwFunction{TUKEY}{tukey} -\SetKwData{SAMPLECOUNT}{sample count} -\SetKwData{WINDOWED}{windowed} -\SetKwData{SAMPLE}{sample} -\For{$i \leftarrow 0$ \emph{\KwTo}\SAMPLECOUNT}{ - $\text{\WINDOWED}_i = \text{\SAMPLE}_i \times \TUKEY(i)$\; -} -\Return \WINDOWED\; -\EALGORITHM -\par -\noindent -where the \VAR{Tukey} function is defined as: -\begin{equation*} -tukey(n) = -\begin{cases} -\frac{1}{2} \times \left[1 + cos\left(\pi \times \left(\frac{2 \times n}{\alpha \times (N - 1)} - 1 \right)\right)\right] & \text{ if } 0 \leq n \leq \frac{\alpha \times (N - 1)}{2} \\ -1 & \text{ if } \frac{\alpha \times (N - 1)}{2} \leq n \leq (N - 1) \times (1 - \frac{\alpha}{2}) \\ -\frac{1}{2} \times \left[1 + cos\left(\pi \times \left(\frac{2 \times n}{\alpha \times (N - 1)} - \frac{2}{\alpha} + 1 \right)\right)\right] & \text{ if } (N - 1) \times (1 - \frac{\alpha}{2}) \leq n \leq (N - 1) \\ -\end{cases} -\end{equation*} -\par -\noindent -$N$ is the total number of input samples and $\alpha$ is $\nicefrac{1}{2}$. -\par -\noindent -{\relsize{-2} -\begin{tabular}{r|rcrcr} -$i$ & $\textsf{sample}_i$ & & \texttt{tukey}($i$) & & $\textsf{windowed}_i$ \\ -\hline -0 & \texttt{0} & $\times$ & \texttt{0.00} & = & \texttt{0.00} \\ -1 & \texttt{16} & $\times$ & \texttt{0.19} & = & \texttt{3.01} \\ -2 & \texttt{31} & $\times$ & \texttt{0.61} & = & \texttt{18.95} \\ -3 & \texttt{44} & $\times$ & \texttt{0.95} & = & \texttt{41.82} \\ -4 & \texttt{54} & $\times$ & \texttt{1.00} & = & \texttt{54.00} \\ -5 & \texttt{61} & $\times$ & \texttt{1.00} & = & \texttt{61.00} \\ -6 & \texttt{64} & $\times$ & \texttt{1.00} & = & \texttt{64.00} \\ -7 & \texttt{63} & $\times$ & \texttt{1.00} & = & \texttt{63.00} \\ -8 & \texttt{58} & $\times$ & \texttt{1.00} & = & \texttt{58.00} \\ -9 & \texttt{49} & $\times$ & \texttt{1.00} & = & \texttt{49.00} \\ -10 & \texttt{38} & $\times$ & \texttt{1.00} & = & \texttt{38.00} \\ -11 & \texttt{24} & $\times$ & \texttt{0.95} & = & \texttt{22.81} \\ -12 & \texttt{8} & $\times$ & \texttt{0.61} & = & \texttt{4.89} \\ -13 & \texttt{-8} & $\times$ & \texttt{0.19} & = & \texttt{-1.51} \\ -14 & \texttt{-24} & $\times$ & \texttt{0.00} & = & \texttt{0.00} \\ -\end{tabular} -} -} - -\clearpage - -\subsubsection{Autocorrelating Windowed Samples} -\label{alac:autocorrelate} -{\relsize{-1} -\ALGORITHM{a list of signed windowed samples}{a list of signed autocorrelation values} -\SetKwData{LAG}{lag} -\SetKwData{AUTOCORRELATION}{autocorrelated} -\SetKwData{TOTALSAMPLES}{total samples} -\SetKwData{WINDOWED}{windowed} -\For{$\LAG \leftarrow 0$ \emph{\KwTo}9}{ - $\text{\AUTOCORRELATION}_{\text{\LAG}} = \overset{\text{\TOTALSAMPLES} - \text{\LAG} - 1}{\underset{i = 0}{\sum}}\text{\WINDOWED}_i \times \text{\WINDOWED}_{i + \text{\LAG}}$\; -} -\Return \AUTOCORRELATION\; -\EALGORITHM -} - -\subsubsection{Autocorrelation Example} -{\relsize{-1} -\begin{multicols}{2} -\begin{tabular}{rrrrr} - \texttt{0.00} & $\times$ & \texttt{0.00} & $=$ & \texttt{0.00} \\ - \texttt{3.01} & $\times$ & \texttt{3.01} & $=$ & \texttt{9.07} \\ - \texttt{18.95} & $\times$ & \texttt{18.95} & $=$ & \texttt{359.07} \\ - \texttt{41.82} & $\times$ & \texttt{41.82} & $=$ & \texttt{1749.02} \\ - \texttt{54.00} & $\times$ & \texttt{54.00} & $=$ & \texttt{2916.00} \\ - \texttt{61.00} & $\times$ & \texttt{61.00} & $=$ & \texttt{3721.00} \\ - \texttt{64.00} & $\times$ & \texttt{64.00} & $=$ & \texttt{4096.00} \\ - \texttt{63.00} & $\times$ & \texttt{63.00} & $=$ & \texttt{3969.00} \\ - \texttt{58.00} & $\times$ & \texttt{58.00} & $=$ & \texttt{3364.00} \\ - \texttt{49.00} & $\times$ & \texttt{49.00} & $=$ & \texttt{2401.00} \\ - \texttt{38.00} & $\times$ & \texttt{38.00} & $=$ & \texttt{1444.00} \\ - \texttt{22.81} & $\times$ & \texttt{22.81} & $=$ & \texttt{520.37} \\ - \texttt{4.89} & $\times$ & \texttt{4.89} & $=$ & \texttt{23.91} \\ - \texttt{-1.51} & $\times$ & \texttt{-1.51} & $=$ & \texttt{2.27} \\ - \texttt{0.00} & $\times$ & \texttt{0.00} & $=$ & \texttt{0.00} \\ - \hline - \multicolumn{3}{r}{$\textsf{autocorrelation}_0$} & $=$ & \texttt{24574.71} \\ -\end{tabular} -\par -\begin{tabular}{rrrrr} - \texttt{0.00} & $\times$ & \texttt{3.01} & $=$ & \texttt{0.00} \\ - \texttt{3.01} & $\times$ & \texttt{18.95} & $=$ & \texttt{57.08} \\ - \texttt{18.95} & $\times$ & \texttt{41.82} & $=$ & \texttt{792.48} \\ - \texttt{41.82} & $\times$ & \texttt{54.00} & $=$ & \texttt{2258.35} \\ - \texttt{54.00} & $\times$ & \texttt{61.00} & $=$ & \texttt{3294.00} \\ - \texttt{61.00} & $\times$ & \texttt{64.00} & $=$ & \texttt{3904.00} \\ - \texttt{64.00} & $\times$ & \texttt{63.00} & $=$ & \texttt{4032.00} \\ - \texttt{63.00} & $\times$ & \texttt{58.00} & $=$ & \texttt{3654.00} \\ - \texttt{58.00} & $\times$ & \texttt{49.00} & $=$ & \texttt{2842.00} \\ - \texttt{49.00} & $\times$ & \texttt{38.00} & $=$ & \texttt{1862.00} \\ - \texttt{38.00} & $\times$ & \texttt{22.81} & $=$ & \texttt{866.84} \\ - \texttt{22.81} & $\times$ & \texttt{4.89} & $=$ & \texttt{111.55} \\ - \texttt{4.89} & $\times$ & \texttt{-1.51} & $=$ & \texttt{-7.36} \\ - \texttt{-1.51} & $\times$ & \texttt{0.00} & $=$ & \texttt{0.00} \\ - \hline - \multicolumn{3}{r}{$\textsf{autocorrelation}_1$} & $=$ & \texttt{23666.93} \\ -\end{tabular} -\par -\begin{tabular}{rrrrr} - \texttt{0.00} & $\times$ & \texttt{18.95} & $=$ & \texttt{0.00} \\ - \texttt{3.01} & $\times$ & \texttt{41.82} & $=$ & \texttt{125.97} \\ - \texttt{18.95} & $\times$ & \texttt{54.00} & $=$ & \texttt{1023.25} \\ - \texttt{41.82} & $\times$ & \texttt{61.00} & $=$ & \texttt{2551.10} \\ - \texttt{54.00} & $\times$ & \texttt{64.00} & $=$ & \texttt{3456.00} \\ - \texttt{61.00} & $\times$ & \texttt{63.00} & $=$ & \texttt{3843.00} \\ - \texttt{64.00} & $\times$ & \texttt{58.00} & $=$ & \texttt{3712.00} \\ - \texttt{63.00} & $\times$ & \texttt{49.00} & $=$ & \texttt{3087.00} \\ - \texttt{58.00} & $\times$ & \texttt{38.00} & $=$ & \texttt{2204.00} \\ - \texttt{49.00} & $\times$ & \texttt{22.81} & $=$ & \texttt{1117.77} \\ - \texttt{38.00} & $\times$ & \texttt{4.89} & $=$ & \texttt{185.82} \\ - \texttt{22.81} & $\times$ & \texttt{-1.51} & $=$ & \texttt{-34.36} \\ - \texttt{4.89} & $\times$ & \texttt{0.00} & $=$ & \texttt{0.00} \\ - \hline - \multicolumn{3}{r}{$\textsf{autocorrelation}_2$} & $=$ & \texttt{21271.56} \\ -\end{tabular} -\par -\begin{tabular}{rrrrr} - \texttt{0.00} & $\times$ & \texttt{41.82} & $=$ & \texttt{0.00} \\ - \texttt{3.01} & $\times$ & \texttt{54.00} & $=$ & \texttt{162.65} \\ - \texttt{18.95} & $\times$ & \texttt{61.00} & $=$ & \texttt{1155.89} \\ - \texttt{41.82} & $\times$ & \texttt{64.00} & $=$ & \texttt{2676.56} \\ - \texttt{54.00} & $\times$ & \texttt{63.00} & $=$ & \texttt{3402.00} \\ - \texttt{61.00} & $\times$ & \texttt{58.00} & $=$ & \texttt{3538.00} \\ - \texttt{64.00} & $\times$ & \texttt{49.00} & $=$ & \texttt{3136.00} \\ - \texttt{63.00} & $\times$ & \texttt{38.00} & $=$ & \texttt{2394.00} \\ - \texttt{58.00} & $\times$ & \texttt{22.81} & $=$ & \texttt{1323.07} \\ - \texttt{49.00} & $\times$ & \texttt{4.89} & $=$ & \texttt{239.61} \\ - \texttt{38.00} & $\times$ & \texttt{-1.51} & $=$ & \texttt{-57.23} \\ - \texttt{22.81} & $\times$ & \texttt{0.00} & $=$ & \texttt{0.00} \\ - \hline - \multicolumn{3}{r}{$\textsf{autocorrelation}_3$} & $=$ & \texttt{17970.57} \\ -\end{tabular} -\par -\begin{tabular}{rrrrr} - \texttt{0.00} & $\times$ & \texttt{54.00} & $=$ & \texttt{0.00} \\ - \texttt{3.01} & $\times$ & \texttt{61.00} & $=$ & \texttt{183.74} \\ - \texttt{18.95} & $\times$ & \texttt{64.00} & $=$ & \texttt{1212.74} \\ - \texttt{41.82} & $\times$ & \texttt{63.00} & $=$ & \texttt{2634.74} \\ - \texttt{54.00} & $\times$ & \texttt{58.00} & $=$ & \texttt{3132.00} \\ - \texttt{61.00} & $\times$ & \texttt{49.00} & $=$ & \texttt{2989.00} \\ - \texttt{64.00} & $\times$ & \texttt{38.00} & $=$ & \texttt{2432.00} \\ - \texttt{63.00} & $\times$ & \texttt{22.81} & $=$ & \texttt{1437.13} \\ - \texttt{58.00} & $\times$ & \texttt{4.89} & $=$ & \texttt{283.62} \\ - \texttt{49.00} & $\times$ & \texttt{-1.51} & $=$ & \texttt{-73.80} \\ - \texttt{38.00} & $\times$ & \texttt{0.00} & $=$ & \texttt{0.00} \\ - \hline - \multicolumn{3}{r}{$\textsf{autocorrelation}_4$} & $=$ & \texttt{14231.18} \\ -\end{tabular} -\end{multicols} -} - -\clearpage - -\subsubsection{LP Coefficient Calculation} -\label{alac:compute_lp_coeffs} -{\relsize{-1} -\ALGORITHM{a list of autocorrelation floats}{a list of LP coefficient lists} -\SetKwData{LPCOEFF}{LP coefficient} -\SetKwData{ERROR}{error} -\SetKwData{AUTOCORRELATION}{autocorrelation} -\begin{tabular}{rcl} -$\kappa_0$ &$\leftarrow$ & $ \AUTOCORRELATION_1 \div \AUTOCORRELATION_0$ \\ -$\LPCOEFF_{0~0}$ &$\leftarrow$ & $ \kappa_0$ \\ -$\ERROR_0$ &$\leftarrow$ & $ \AUTOCORRELATION_0 \times (1 - {\kappa_0} ^ 2)$ \\ -\end{tabular}\; -\For{$i \leftarrow 1$ \emph{\KwTo}8}{ - \tcc{"zip" all of the previous row's LP coefficients - \newline - and the reversed autocorrelation values from 1 to i + 1 - \newline - into ($c$,$a$) pairs - \newline - $q_i$ is $\AUTOCORRELATION_{i + 1}$ minus the sum of those multiplied ($c$,$a$) pairs} - $q_i \leftarrow \AUTOCORRELATION_{i + 1}$\; - \For{$j \leftarrow 0$ \emph{\KwTo}i}{ - $q_i \leftarrow q_i - (\LPCOEFF_{(i - 1)~j} \times \AUTOCORRELATION_{i - j})$\; - } - \BlankLine - \tcc{"zip" all of the previous row's LP coefficients - \newline - and the previous row's LP coefficients reversed - \newline - into ($c$,$r$) pairs} - $\kappa_i = q_i \div \ERROR_{i - 1}$\; - \For{$j \leftarrow 0$ \emph{\KwTo}i}{ - \tcc{then build a new coefficient list of $c - (\kappa_i * r)$ for each ($c$,$r$) pair} - $\LPCOEFF_{i~j} \leftarrow \LPCOEFF_{(i - 1)~j} - (\kappa_i \times \LPCOEFF_{(i - 1)~(i - j - 1)})$\; - } - $\text{\LPCOEFF}_{i~i} \leftarrow \kappa_i$\tcc*[r]{and append $\kappa_i$ as the final coefficient in that list} - \BlankLine - $\ERROR_i \leftarrow \ERROR_{i - 1} \times (1 - {\kappa_i}^2)$\; -} -\Return \LPCOEFF\; -\EALGORITHM -} - -\begin{landscape} - -\subsubsection{LP Coefficient Calculation Example} -\begin{table}[h] -{\relsize{-1} -\begin{tabular}{r|r} -$i$ & $\textsf{autocorrelation}_i$ \\ -\hline -0 & \texttt{24598.25} \\ -1 & \texttt{23694.34} \\ -2 & \texttt{21304.57} \\ -3 & \texttt{18007.86} \\ -4 & \texttt{14270.30} \\ -\end{tabular} -} -\end{table} - -\begin{table}[h] -{\relsize{-1} -\renewcommand{\arraystretch}{1.45} -\begin{tabular}{|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|} -\hline -k_0 & -\multicolumn{4}{>{$}l<{$}|}{\texttt{23694.34} \div \texttt{24598.25} = \texttt{0.96}} \\ -\textsf{LP coefficient}_{0~0} & \texttt{\color{blue}0.96} & & & \\ -\textsf{error}_0 & -\multicolumn{4}{>{$}l<{$}|}{\texttt{24598.25} \times (1 - \texttt{0.96} ^ 2) = \texttt{1774.62}} \\ -\hline -q_1 & \multicolumn{4}{>{$}l<{$}|}{\texttt{21304.57} - (\texttt{0.96} \times \texttt{23694.34}) = \texttt{-1519.07}} \\ -k_1 & \multicolumn{4}{>{$}l<{$}|}{\texttt{-1519.07} \div \texttt{1774.62} = \texttt{-0.86}} \\ -\textsf{LP coefficient}_{1~i} & -\texttt{0.96} -(\texttt{-0.86} \times \texttt{0.96}) = \texttt{\color{blue}1.79} & -\texttt{\color{blue}-0.86} & & \\ -\textsf{error}_1 & \multicolumn{4}{>{$}l<{$}|}{\texttt{1774.62} \times (1 - \texttt{-0.86} ^ 2) = \texttt{474.30}} \\ -\hline -q_2 & \multicolumn{4}{>{$}l<{$}|}{\texttt{18007.86} - (\texttt{1.79} \times \texttt{21304.57} + \texttt{-0.86} \times \texttt{23694.34}) = \texttt{201.96}} \\ -k_2 & \multicolumn{4}{>{$}l<{$}|}{\texttt{201.96} \div \texttt{474.30} = \texttt{0.43}} \\ -\textsf{LP coefficient}_{2~i} & -\texttt{1.79} -(\texttt{0.43} \times \texttt{-0.86}) = \texttt{\color{blue}2.15} & -\texttt{-0.86} -(\texttt{0.43} \times \texttt{1.79}) = \texttt{\color{blue}-1.62} & -\texttt{\color{blue}0.43} & \\ -\textsf{error}_2 & \multicolumn{4}{>{$}l<{$}|}{\texttt{474.30} \times (1 - \texttt{0.43} ^ 2) = \texttt{388.31}} \\ -\hline -q_3 & \multicolumn{4}{>{$}l<{$}|}{\texttt{14270.30} - (\texttt{2.15} \times \texttt{18007.86} + \texttt{-1.62} \times \texttt{21304.57} + \texttt{0.43} \times \texttt{23694.34}) = \texttt{-122.06}} \\ -k_3 & \multicolumn{4}{>{$}l<{$}|}{\texttt{-122.06} \div \texttt{388.31} = \texttt{-0.31}} \\ -\textsf{LP coefficient}_{3~i} & -\texttt{2.15} -(\texttt{-0.31} \times \texttt{0.43}) = \texttt{\color{blue}2.29} & -\texttt{-1.62} -(\texttt{-0.31} \times \texttt{-1.62}) = \texttt{\color{blue}-2.13} & -\texttt{0.43} -(\texttt{-0.31} \times \texttt{2.15}) = \texttt{\color{blue}1.10} & -\texttt{\color{blue}-0.31} \\ -\textsf{error}_3 & \multicolumn{4}{>{$}l<{$}|}{\texttt{388.31} \times (1 - \texttt{-0.31} ^ 2) = \texttt{349.94}} \\ -\hline -\end{tabular} -\renewcommand{\arraystretch}{1.0} -} -\end{table} - -\end{landscape} - -\subsubsection{Quantizing LP Coefficients} -\label{alac:quantize_lp_coeffs} -\ALGORITHM{LP coefficients, an order value of 4 or 8}{QLP coefficients as a list of signed integers} -\SetKwData{ORDER}{order} -\SetKwFunction{MIN}{min} -\SetKwFunction{MAX}{max} -\SetKwFunction{ROUND}{round} -\SetKwData{QLPMIN}{QLP min} -\SetKwData{QLPMAX}{QLP max} -\SetKwData{LPCOEFF}{LP coefficient} -\SetKwData{QLPCOEFF}{QLP coefficient} -\tcc{QLP min and max are the smallest and largest QLP coefficients that fit in a signed field that's 16 bits wide} -$\QLPMIN \leftarrow 2 ^ \text{15} - 1$\; -$\QLPMAX \leftarrow -(2 ^ \text{15})$\; -$e \leftarrow 0.0$\; -\For{$i \leftarrow 0$ \emph{\KwTo}\ORDER}{ - $e \leftarrow e + \text{\LPCOEFF}_{\ORDER - 1~i} \times 2 ^ 9$\; - $\text{\QLPCOEFF}_i \leftarrow \MIN(\MAX(\ROUND(e)~,~\text{\QLPMIN})~,~\text{\QLPMAX})$\; - $e \leftarrow e - \text{\QLPCOEFF}_i$\; -} -\Return \QLPCOEFF\; -\EALGORITHM - -\clearpage - -\subsubsection{Quantizing Coefficients Example} -\begin{align*} -e &\leftarrow \texttt{0.00} + \texttt{2.29} \times 2 ^ 9 = \texttt{1170.49} \\ -\textsf{QLP coefficient}_0 &\leftarrow \texttt{round}(\texttt{1170.49}) = \texttt{\color{blue}1170} \\ -e &\leftarrow \texttt{1170.49} - 1170 = \texttt{0.49} \\ -e &\leftarrow \texttt{0.49} + \texttt{-2.13} \times 2 ^ 9 = \texttt{-1087.81} \\ -\textsf{QLP coefficient}_1 &\leftarrow \texttt{round}(\texttt{-1087.81}) = \texttt{\color{blue}-1088} \\ -e &\leftarrow \texttt{-1087.81} - -1088 = \texttt{0.19} \\ -e &\leftarrow \texttt{0.19} + \texttt{1.10} \times 2 ^ 9 = \texttt{564.59} \\ -\textsf{QLP coefficient}_2 &\leftarrow\texttt{round}(\texttt{564.59}) = \texttt{\color{blue}565} \\ -e &\leftarrow \texttt{564.59} - 565 = \texttt{-0.41} \\ -e &\leftarrow \texttt{-0.41} + \texttt{-0.31} \times 2 ^ 9 = \texttt{-161.35} \\ -\textsf{QLP coefficient}_3 &\leftarrow \texttt{round}(\texttt{-161.35}) = \texttt{\color{blue}-161} \\ -e &\leftarrow \texttt{-161.35} - -161 = \texttt{-0.35} \\ -\end{align*} - -\clearpage - -\subsection{Computing Residual Values} -\label{alac:calculate_residuals} -{\relsize{-1} -\ALGORITHM{a list of signed PCM samples, 4 or 8 QLP coefficients, sample size}{a list of signed residual values} -\SetKwFunction{SIGN}{sign} -\SetKw{BREAK}{break} -\SetKwData{SAMPLESIZE}{sample size} -\SetKwData{SAMPLECOUNT}{sample count} -\SetKwData{COEFFCOUNT}{coefficient count} -\SetKwData{COEFF}{QLP coefficient} -\SetKwData{ERROR}{error} -\SetKwData{RESIDUAL}{residual} -\SetKwData{SAMPLE}{sample} -\SetKwData{BASESAMPLE}{base sample} -\SetKwData{QLPSUM}{QLP sum} -\SetKwData{DIFF}{diff} -\SetKwData{SSIGN}{sign} -\SetKwFunction{TRUNCATE}{truncate} -$\text{\RESIDUAL}_0 \leftarrow \text{\SAMPLE}_0$\tcc*[r]{first sample always copied verbatim} -\eIf{$\COEFFCOUNT < 31$}{ - \For{$i \leftarrow 1$ \emph{\KwTo}$\COEFFCOUNT + 1$}{ - $\text{\RESIDUAL}_i \leftarrow \TRUNCATE(\text{\SAMPLE}_i - \text{\SAMPLE}_{i - 1}~,~\SAMPLESIZE)$\; - } - \For{i $\leftarrow \text{\COEFFCOUNT} + 1$ \emph{\KwTo}\SAMPLECOUNT}{ - $\text{\BASESAMPLE}_i \leftarrow \text{\SAMPLE}_{i - \COEFFCOUNT - 1}$\; - $\text{\QLPSUM}_i \leftarrow \overset{\COEFFCOUNT - 1}{\underset{j = 0}{\sum}} \text{\COEFF}_j \times (\text{\SAMPLE}_{i - j - 1} - \text{\BASESAMPLE}_i)$\; - $\text{\ERROR} \leftarrow \TRUNCATE\left(\text{\SAMPLE}_i - \text{\BASESAMPLE}_i - \left\lfloor\frac{\text{\QLPSUM}_i + 2 ^ 8}{2 ^ 9}\right\rfloor~,~\SAMPLESIZE \right)$\; - $\text{\RESIDUAL}_i \leftarrow \text{\ERROR}$\; - \BlankLine - \uIf(\tcc*[f]{modify QLP coefficients}){$\text{\ERROR} > 0$}{ - \For{$j \leftarrow 0$ \emph{\KwTo}$\text{\COEFFCOUNT}$}{ - $\DIFF \leftarrow \text{\BASESAMPLE}_i - \text{\SAMPLE}_{i - \COEFFCOUNT + j}$\; - $\SSIGN \leftarrow \SIGN(\DIFF)$\; - $\text{\COEFF}_{\COEFFCOUNT - j - 1} \leftarrow \text{\COEFF}_{\COEFFCOUNT - j - 1} - \SSIGN$\; - $\text{\ERROR} \leftarrow \text{\ERROR} - \left\lfloor\frac{\DIFF \times \SSIGN}{2 ^ 9}\right\rfloor \times (j + 1)$\; - \If{$\text{\ERROR} \leq 0$}{ - \BREAK\; - } - } - } - \ElseIf{$\text{\ERROR} < 0$}{ - \For{$j \leftarrow 0$ \emph{\KwTo}$\text{\COEFFCOUNT}$}{ - $\DIFF \leftarrow \text{\BASESAMPLE}_i - \text{\SAMPLE}_{i - \COEFFCOUNT + j}$\; - $\SSIGN \leftarrow \SIGN(\DIFF)$\; - $\text{\COEFF}_{\COEFFCOUNT - j - 1} \leftarrow \text{\COEFF}_{\COEFFCOUNT - j - 1} + \SSIGN$\; - $\text{\ERROR} \leftarrow \text{\ERROR} - \left\lfloor\frac{\DIFF \times -\SSIGN}{2 ^ 9}\right\rfloor \times (j + 1)$\; - \If{$\text{\ERROR} \geq 0$}{ - \BREAK\; - } - } - } - } -}{ - \For{$i \leftarrow 1$ \emph{\KwTo}\SAMPLECOUNT}{ - $\text{\RESIDUAL}_i \leftarrow \TRUNCATE(\text{\SAMPLE}_i - \text{\SAMPLE}_{i - 1}~,~\SAMPLESIZE)$\; - } -} -\Return \RESIDUAL\; -\EALGORITHM -} -\subsubsection{The \texttt{truncate} Function} -{\relsize{-1} - \ALGORITHM{a signed sample, the maximum size of the sample in bits}{a truncated signed sample} - \SetKw{BAND}{bitwise and} - \SetKwData{SAMPLE}{sample} - \SetKwData{BITS}{bits} - \SetKwData{TRUNCATED}{truncated} - $\TRUNCATED \leftarrow \SAMPLE~\BAND~(2 ^ {\BITS} - 1)$\; - \eIf(\tcc*[f]{apply sign bit}){$(\TRUNCATED~\BAND~2 ^ {\BITS - 1}) \neq 0$}{ - \Return $\TRUNCATED - 2 ^ {\BITS}$\; - }{ - \Return \TRUNCATED\; - } - \EALGORITHM -} - -\clearpage - -{\relsize{-1} -\begin{equation*} -\texttt{sign}(x) = -\begin{cases} -\texttt{ 1} & \text{if } x > 0 \\ -\texttt{ 0} & \text{if } x = 0 \\ -\texttt{-1} & \text{if } x < 0 -\end{cases} -\end{equation*} -} - -\subsubsection{Computing Residuals Example} -{\relsize{-2} -Given the samples -\texttt{0, 16, 32, 44, 54, 61, 64, 63, 58, 49, 38, 24, 8, -8, -24}, -and the QLP coefficients -\texttt{1170, -1088, 565, -161}, -the subframe's residuals are calculate as follows: -\par -\vskip .15in -\noindent -\begin{tabular}{r||r|r|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}} -$i$ & $\textsf{sample}_i$ & $\textsf{base}_i$ & \textsf{QLP sum}_i & \textsf{residual}_i & \textsf{QLP coefficient}_{(i + 1)~j} \\ -\hline -0 & 0 & & & 0 \\ -1 & 16 & & & 16 - 0 = 16 \\ -2 & 32 & & & 32 - 16 = 16 \\ -3 & 44 & & & 44 - 32 = 12 \\ -4 & 54 & & & 54 - 44 = 10 \\ -\hline -5 & 61 & 0 & 1170 \times (54 - 0) \texttt{ +} & 61 - (\lfloor(30812 + 2 ^ 8) \div 2 ^ 9\rfloor + 0) = 1 & 1170 + 1 = 1171 \\ -& & & -1088 \times (44 - 0) \texttt{ +} & & -1088 + 1 = -1087 \\ -& & & 565 \times (32 - 0) \texttt{ +} & & 565 + 1 = 566 \\ -& & & -161 \times (16 - 0) \texttt{~~} & & -161 + 1 = -160 \\ -\hline -6 & 64 & 16 & 1171 \times (61 - 16) \texttt{ +} & 64 - (\lfloor(24677 + 2 ^ 8) \div 2 ^ 9\rfloor + 16) = 0 & 1171 \\ -& & & -1087 \times (54 - 16) \texttt{ +} & & -1087 \\ -& & & 566 \times (44 - 16) \texttt{ +} & & 566 \\ -& & & -160 \times (32 - 16) \texttt{~~} & & -160 \\ -\hline -7 & 63 & 32 & 1171 \times (64 - 32) \texttt{ +} & 63 - (\lfloor(16481 + 2 ^ 8) \div 2 ^ 9\rfloor + 32) = -1 & 1171 \\ -& & & -1087 \times (61 - 32) \texttt{ +} & & -1087 \\ -& & & 566 \times (54 - 32) \texttt{ +} & & 566 \\ -& & & -160 \times (44 - 32) \texttt{~~} & & -160 - 1 = -159 \\ -\hline -8 & 58 & 44 & 1171 \times (63 - 44) \texttt{ +} & 58 - (\lfloor(8521 + 2 ^ 8) \div 2 ^ 9\rfloor + 44) = -3 & 1171 \\ -& & & -1087 \times (64 - 44) \texttt{ +} & & -1087 \\ -& & & 566 \times (61 - 44) \texttt{ +} & & 565 \\ -& & & -161 \times (54 - 44) \texttt{~~} & & -161 - 1 = -160 \\ -\hline -9 & 49 & 54 & 1171 \times (58 - 54) \texttt{ +} & 49 - (\lfloor(-583 + 2 ^ 8) \div 2 ^ 9\rfloor + 54) = -4 & 1171 \\ -& & & -1087 \times (63 - 54) \texttt{ +} & & -1088 \\ -& & & 565 \times (64 - 54) \texttt{ +} & & -1087 - 1 = -1086 \\ -& & & -162 \times (61 - 54) \texttt{~~} & & -162 - 1 = -161 \\ -\hline -10 & 38 & 61 & 1171 \times (49 - 61) \texttt{ +} & 38 - (\lfloor(-10149 + 2 ^ 8) \div 2 ^ 9\rfloor + 61) = -3 & 1171 \\ -& & & -1088 \times (58 - 61) \texttt{ +} & & -1088 \\ -& & & 564 \times (63 - 61) \texttt{ +} & & 563 \\ -& & & -163 \times (64 - 61) \texttt{~~} & & -163 - 1 = -162 \\ -\hline -11 & 24 & 64 & 1171 \times (38 - 64) \texttt{ +} & 24 - (\lfloor(-17340 + 2 ^ 8) \div 2 ^ 9\rfloor + 64) = -6 & 1171 \\ -& & & -1088 \times (49 - 64) \texttt{ +} & & -1087 \\ -& & & 563 \times (58 - 64) \texttt{ +} & & -1088 + 1 = -1089 \\ -& & & -164 \times (63 - 64) \texttt{~~} & & -164 + 1 = -165 \\ -\hline -12 & 8 & 63 & 1171 \times (24 - 63) \texttt{ +} & 8 - (\lfloor(-25575 + 2 ^ 8) \div 2 ^ 9\rfloor + 63) = -5 & 1171 \\ -& & & -1087 \times (38 - 63) \texttt{ +} & & -1086 \\ -& & & 564 \times (49 - 63) \texttt{ +} & & -1087 + 1 = -1088 \\ -& & & -163 \times (58 - 63) \texttt{~~} & & -163 + 1 = -164 \\ -\hline -13 & -8 & 58 & 1171 \times (8 - 58) \texttt{ +} & -8 - (\lfloor(-31468 + 2 ^ 8) \div 2 ^ 9\rfloor + 58) = -5 & 1171 \\ -& & & -1086 \times (24 - 58) \texttt{ +} & & -1085 \\ -& & & 565 \times (38 - 58) \texttt{ +} & & -1086 + 1 = -1087 \\ -& & & -162 \times (49 - 58) \texttt{~~} & & -162 + 1 = -163 \\ -\hline -14 & -24 & 49 & 1171 \times (-8 - 49) \texttt{ +} & -24 - (\lfloor(-34641 + 2 ^ 8) \div 2 ^ 9\rfloor + 49) = -5 & 1171 \\ -& & & -1085 \times (8 - 49) \texttt{ +} & & -1084 \\ -& & & 566 \times (24 - 49) \texttt{ +} & & -1085 + 1 = -1086 \\ -& & & -161 \times (38 - 49) \texttt{~~} & & -161 + 1 = -162 \\ -\hline -\end{tabular} -} - -\clearpage - -\subsection{Encoding Residual Block} -\label{alac:write_residuals} -\ALGORITHM{a list of signed residual values, sample size; initial history, history multiplier, maximum K from encoding options}{a block of residual data, or a \textit{residual overflow} exception} -\SetKwData{RESIDUAL}{residual} -\SetKwData{UNSIGNED}{unsigned} -\SetKwData{SAMPLESIZE}{sample size} -\SetKwData{HISTORY}{history} -\SetKwData{HISTORYMULT}{history multiplier} -\SetKwData{MAXIMUMK}{maximum K} -\SetKwData{SIGNMODIFIER}{sign modifier} -\SetKwData{ZEROES}{zeroes} -\SetKw{RAISE}{raise} -\SetKwFunction{MIN}{min} -\SetKwFunction{WRITERESIDUAL}{write residual} -\SetKw{AND}{and} -\HISTORY $\leftarrow$ initial history\; -\SIGNMODIFIER $\leftarrow 0$\; -$i \leftarrow 0$\; -\While{$i < \text{residual count}$}{ - \eIf(\tcc*[f]{add sign bit}){$\text{\RESIDUAL}_i \geq 0$}{ - $\text{\UNSIGNED}_i \leftarrow \text{\RESIDUAL}_i \times 2$\; - }{ - $\text{\UNSIGNED}_i \leftarrow (-\text{\RESIDUAL}_i \times 2) - 1$\; - } - \If{$\text{\UNSIGNED}_i \geq 2 ^ \text{\SAMPLESIZE}$}{ - \RAISE residual overflow exception\footnote{in this case, we should cease building a compressed frame and write an uncompressed frame instead}\; - } - $\kappa \leftarrow \MIN(\lfloor\log_2((\HISTORY \div 2 ^ 9) + 3)\rfloor~,~\MAXIMUMK)$\; - $\WRITERESIDUAL(\text{\UNSIGNED}_i - \SIGNMODIFIER~,~\kappa~,~\text{\SAMPLESIZE})$\; - $\SIGNMODIFIER \leftarrow 0$\; - \BlankLine - \eIf(\tcc*[f]{update history}){$\text{\UNSIGNED}_i \leq 65535$}{ - $\HISTORY \leftarrow \HISTORY + (\text{\UNSIGNED}_i \times \HISTORYMULT) - \left\lfloor\frac{\HISTORY \times \HISTORYMULT}{2 ^ 9}\right\rfloor$\; - $i \leftarrow i + 1$\; - \BlankLine - \If(\tcc*[f]{handle 0 residuals}){$\HISTORY < 128$ \AND $i < \text{residual count}$}{ - $\kappa \leftarrow \MIN(7 - \lfloor\log_2(\HISTORY)\rfloor + \lfloor(\HISTORY + 16) \div 2 ^ 6\rfloor~,~\MAXIMUMK)$\; - $\ZEROES \leftarrow 0$\; - \While{$i < \text{residual count}$ \AND $\text{\RESIDUAL}_i = 0$}{ - $\ZEROES \leftarrow \ZEROES + 1$\; - $i \leftarrow i + 1$\; - } - $\WRITERESIDUAL(\ZEROES~,~\kappa~,~16)$\; - \If{$\ZEROES < 65535$}{ - $\SIGNMODIFIER \leftarrow 1$\; - } - $\HISTORY \leftarrow 0$\; - } - }{ - $i \leftarrow i + 1$\; - $\HISTORY \leftarrow 65535$\; - } -} -\Return encoded residual block\; -\EALGORITHM - -\clearpage - -\subsubsection{Encoding Individual Residual} - -\ALGORITHM{an unsigned residual value, $\kappa$, sample size}{an individual encoded residual} -\SetKwData{UNSIGNED}{unsigned} -\SetKwData{MSB}{MSB} -\SetKwData{LSB}{LSB} -$\MSB \leftarrow \text{\UNSIGNED} \div 2 ^ \kappa - 1$\; -$\LSB \leftarrow \text{\UNSIGNED} \bmod~2 ^ \kappa - 1$\; -\eIf{$\MSB > 8$}{ - $\texttt{0x1FF} \rightarrow$ \WRITE 9 unsigned bits\; - $\text{\UNSIGNED} \rightarrow$ \WRITE (sample size) unsigned bits\; -}{ - $\MSB \rightarrow$ \WUNARY with stop bit 0\; - \If{$\kappa > 1$}{ - \eIf{$\LSB > 0$}{ - $\LSB + 1 \rightarrow$ \WRITE $\kappa$ unsigned bits\; - }{ - $0 \rightarrow$ \WRITE $(\kappa - 1)$ unsigned bits\; - } - } -} -\Return encoded residual data\; -\EALGORITHM - -\begin{landscape} - -\subsubsection{Residual Encoding Example} -\begin{table}[h] -{\relsize{-1} -\renewcommand{\arraystretch}{1.5} -\begin{tabular}{r||r|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}} -$i$ & $\textsf{residual}_i$ & \textsf{unsigned}_i & \kappa & \textsf{MSB}_i & \textsf{LSB}_i & \textsf{history}_{i + 1} \\ -\hline -0 & \texttt{0} & -\texttt{0} \times 2 = \texttt{0} & -\lfloor\log_2(\frac{\texttt{10}}{2 ^ 9} + 3)\rfloor = \texttt{1} & -0 \div 2 ^ 1 - 1 = 0 & - & -\texttt{10} + (\texttt{0} \times \texttt{40}) - \left\lfloor\frac{\texttt{10} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{10} \\ -& $zeroes = 0$ & -\multicolumn{2}{>{$}r<{$}|}{7 - \lfloor\log_2(10)\rfloor + \lfloor\frac{10 + 16}{2 ^ 6}\rfloor = 4} & -0 \div 2 ^ 4 - 1 = 0 & -0 \bmod~2 ^ 4 - 1 = 0 & -\texttt{0} \\ -1 & \texttt{16} & -\texttt{16} \times 2 = \texttt{32}\text{\symbolfootnotemark[2]} & -\lfloor\log_2(\frac{\texttt{0}}{2 ^ 9} + 3)\rfloor = \texttt{1} & -\multicolumn{2}{r|}{write \texttt{0x1FFF} in 9 unsigned bits} & -\texttt{0} + (\texttt{32} \times \texttt{40}) - \left\lfloor\frac{\texttt{0} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{1280} \\ -& & & & \multicolumn{2}{r|}{write \texttt{31} in 16 unsigned bits} & \\ -2 & \texttt{16} & -\texttt{16} \times 2 = \texttt{32} & -\lfloor\log_2(\frac{\texttt{1280}}{2 ^ 9} + 3)\rfloor = \texttt{2} & -\multicolumn{2}{r|}{write \texttt{0x1FFF} in 9 unsigned bits} & -\texttt{1280} + (\texttt{32} \times \texttt{40}) - \left\lfloor\frac{\texttt{1280} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{2460} \\ -& & & & \multicolumn{2}{r|}{write \texttt{32} in 16 unsigned bits} & \\ -3 & \texttt{12} & -\texttt{12} \times 2 = \texttt{24} & -\lfloor\log_2(\frac{\texttt{2460}}{2 ^ 9} + 3)\rfloor = \texttt{2} & -24 \div 2 ^ 2 - 1 = 8 & -24 \bmod~2 ^ 2 - 1 = 0 & -\texttt{2460} + (\texttt{24} \times \texttt{40}) - \left\lfloor\frac{\texttt{2460} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3228} \\ -4 & \texttt{10} & -\texttt{10} \times 2 = \texttt{20} & -\lfloor\log_2(\frac{\texttt{3228}}{2 ^ 9} + 3)\rfloor = \texttt{3} & -20 \div 2 ^ 3 - 1 = 2 & -20 \bmod~2 ^ 3 - 1 = 6 & -\texttt{3228} + (\texttt{20} \times \texttt{40}) - \left\lfloor\frac{\texttt{3228} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3776} \\ -5 & \texttt{1} & -\texttt{1} \times 2 = \texttt{2} & -\lfloor\log_2(\frac{\texttt{3776}}{2 ^ 9} + 3)\rfloor = \texttt{3} & -2 \div 2 ^ 3 - 1 = 0 & -2 \bmod~2 ^ 3 - 1 = 2 & -\texttt{3776} + (\texttt{2} \times \texttt{40}) - \left\lfloor\frac{\texttt{3776} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3561} \\ -6 & \texttt{0} & -\texttt{0} \times 2 = \texttt{0} & -\lfloor\log_2(\frac{\texttt{3561}}{2 ^ 9} + 3)\rfloor = \texttt{3} & -0 \div 2 ^ 3 - 1 = 0 & -0 \bmod~2 ^ 3 - 1 = 0 & -\texttt{3561} + (\texttt{0} \times \texttt{40}) - \left\lfloor\frac{\texttt{3561} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3283} \\ -7 & \texttt{-1} & -(\texttt{1} \times 2) - 1 = \texttt{1} & -\lfloor\log_2(\frac{\texttt{3283}}{2 ^ 9} + 3)\rfloor = \texttt{3} & -1 \div 2 ^ 3 - 1 = 0 & -1 \bmod~2 ^ 3 - 1 = 1 & -\texttt{3283} + (\texttt{1} \times \texttt{40}) - \left\lfloor\frac{\texttt{3283} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3067} \\ -8 & \texttt{-3} & -(\texttt{3} \times 2) - 1 = \texttt{5} & -\lfloor\log_2(\frac{\texttt{3067}}{2 ^ 9} + 3)\rfloor = \texttt{3} & -5 \div 2 ^ 3 - 1 = 0 & -5 \bmod~2 ^ 3 - 1 = 5 & -\texttt{3067} + (\texttt{5} \times \texttt{40}) - \left\lfloor\frac{\texttt{3067} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3028} \\ -9 & \texttt{-4} & -(\texttt{4} \times 2) - 1 = \texttt{7} & -\lfloor\log_2(\frac{\texttt{3028}}{2 ^ 9} + 3)\rfloor = \texttt{3} & -7 \div 2 ^ 3 - 1 = 1 & -7 \bmod~2 ^ 3 - 1 = 0 & -\texttt{3028} + (\texttt{7} \times \texttt{40}) - \left\lfloor\frac{\texttt{3028} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3072} \\ -10 & \texttt{-3} & -(\texttt{3} \times 2) - 1 = \texttt{5} & -\lfloor\log_2(\frac{\texttt{3072}}{2 ^ 9} + 3)\rfloor = \texttt{3} & -5 \div 2 ^ 3 - 1 = 0 & -5 \bmod~2 ^ 3 - 1 = 5 & -\texttt{3072} + (\texttt{5} \times \texttt{40}) - \left\lfloor\frac{\texttt{3072} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3032} \\ -11 & \texttt{-6} & -(\texttt{6} \times 2) - 1 = \texttt{11} & -\lfloor\log_2(\frac{\texttt{3032}}{2 ^ 9} + 3)\rfloor = \texttt{3} & -11 \div 2 ^ 3 - 1 = 1 & -11 \bmod~2 ^ 3 - 1 = 4 & -\texttt{3032} + (\texttt{11} \times \texttt{40}) - \left\lfloor\frac{\texttt{3032} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3236} \\ -12 & \texttt{-5} & -(\texttt{5} \times 2) - 1 = \texttt{9} & -\lfloor\log_2(\frac{\texttt{3236}}{2 ^ 9} + 3)\rfloor = \texttt{3} & -9 \div 2 ^ 3 - 1 = 1 & -9 \bmod~2 ^ 3 - 1 = 2 & -\texttt{3236} + (\texttt{9} \times \texttt{40}) - \left\lfloor\frac{\texttt{3236} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3344} \\ -13 & \texttt{-5} & -(\texttt{5} \times 2) - 1 = \texttt{9} & -\lfloor\log_2(\frac{\texttt{3344}}{2 ^ 9} + 3)\rfloor = \texttt{3} & -9 \div 2 ^ 3 - 1 = 1 & -9 \bmod~2 ^ 3 - 1 = 2 & -\texttt{3344} + (\texttt{9} \times \texttt{40}) - \left\lfloor\frac{\texttt{3344} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3443} \\ -14 & \texttt{-5} & -(\texttt{5} \times 2) - 1 = \texttt{9} & -\lfloor\log_2(\frac{\texttt{3443}}{2 ^ 9} + 3)\rfloor = \texttt{3} & -9 \div 2 ^ 3 - 1 = 1 & -9 \bmod~2 ^ 3 - 1 = 2 & -\texttt{3443} + (\texttt{9} \times \texttt{40}) - \left\lfloor\frac{\texttt{3443} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3535} \\ -\end{tabular} -\renewcommand{\arraystretch}{1.0} -} -\end{table} -\symbolfootnotetext[2]{written as $\texttt{32} - 1 = \texttt{31}$ due to sign modifier} - -\clearpage - -\begin{figure}[h] -\includegraphics{figures/alac/residual-build.pdf} -\end{figure} - -\end{landscape} - -\subsection{Writing Subframe Header} -\label{alac:write_subframe_header} -\ALGORITHM{4 or 8 signed QLP coefficients}{a subframe header} -\SetKwData{COEFFCOUNT}{coefficient count} -\SetKwData{COEFF}{QLP coefficient} -$0 \rightarrow$ \WRITE 4 unsigned bits\tcc*[r]{prediction type} -$9 \rightarrow$ \WRITE 4 unsigned bits\tcc*[r]{QLP shift needed} -$4 \rightarrow$ \WRITE 3 unsigned bits\tcc*[r]{Rice modifier} -$\text{\COEFFCOUNT} \rightarrow$ \WRITE 5 unsigned bits\; -\For{$i \leftarrow 0$ \emph{\KwTo}\COEFFCOUNT}{ - $\text{\COEFF}_i \rightarrow$ \WRITE 16 signed bits\; -} -\Return subframe header data\; -\EALGORITHM -\begin{figure}[h] -\includegraphics{figures/alac/subframe_header.pdf} -\end{figure} -\par -\noindent -For example, given the QLP coefficients -\texttt{1170, -1088, 565, -161}, -the subframe header is written as: -\begin{figure}[h] -\includegraphics{figures/alac/subframe-build.pdf} -\end{figure} +\input{alac/encode}
View file
audiotools-2.19.tar.gz/docs/reference/alac/decode.tex
Added
@@ -0,0 +1,796 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\section{ALAC Decoding} +\begin{wrapfigure}[6]{r}{1.5in} +\includegraphics{alac/figures/atoms.pdf} +\end{wrapfigure} +The basic process for decoding an ALAC file is as follows: +\par +\begin{wrapfigure}[6]{l}{4.5in} +{\relsize{-1} +\ALGORITHM{an ALAC encoded file}{PCM samples} +\hyperref[alac:read_alac_atom]{read \texttt{alac} atom to obtain decoding parameters}\; +\hyperref[alac:read_mdhd_atom]{read \texttt{mdhd} atom to obtain $PCM~frame~count$}\; +seek to \texttt{mdat} atom's data\; +\While{$PCM~frame~count > 0$}{ + \hyperref[alac:decode_frameset]{decode ALAC frameset to 1 or more PCM frames}\; + deduct ALAC frameset's samples from stream's $PCM~frame~count$\; + return decoded PCM frames\; +} +\EALGORITHM +} +\par +Seeking to a particular atom within the ALAC file is a recursive +process. +Each ALAC atom is laid out as follows: +\vskip .1in +\includegraphics{alac/figures/atom.pdf} +\vskip .1in +where \VAR{atom length} is the full size of the atom in bytes, +including the 8 byte atom header. +\VAR{atom type} is an ASCII string +\VAR{atom data} is a binary blob of data +which may contain one or more sub-atoms. +\end{wrapfigure} + +\clearpage + +\subsection{Parsing the alac Atom} +\label{alac:read_alac_atom} +The \texttt{stsd} atom contains a single \texttt{alac} atom +which contains an \texttt{alac} sub-atom of its own. +\begin{figure}[h] +\includegraphics{alac/figures/alac_atom.pdf} +\end{figure} +\par +\noindent +Many of these fields appear redundant between the outer \texttt{alac} atom +and the inner sub-atom. +However, for proper decoding, one must ignore the outer atom entirely +and use only the parameters from the innermost \texttt{alac} atom. + +Of these, we'll be interested in \VAR{samples per frame}, +\VAR{bits per sample}, \VAR{history multiplier}, \VAR{initial history}, +\VAR{maximum K}, \VAR{channels} and \VAR{sample rate}. +The others can safely be ignored. + +\clearpage + +For example, given the bytes: +\par +\begin{figure}[h] +\includegraphics{alac/figures/alac-atom-parse.pdf} +\end{figure} +\begin{tabular}{rcrcl} +$alac~length$ & $\leftarrow$ & \texttt{00000024} & = & 36 \\ +$alac$ & $\leftarrow$ & \texttt{616C6163} & = & \texttt{"alac"} \\ +$padding$ & $\leftarrow$ & \texttt{00000000} & = & 0 \\ +samples per frame & $\leftarrow$ & \texttt{00001000} & = & 4096 \\ +compatible version & $\leftarrow$ & \texttt{00} & = & 0 \\ +bits per sample & $\leftarrow$ & \texttt{10} & = & 16 \\ +history multiplier & $\leftarrow$ & \texttt{28} & = & 40 \\ +initial history & $\leftarrow$ & \texttt{0A} & = & 10 \\ +maximum K & $\leftarrow$ & \texttt{0E} & = & 14 \\ +channels & $\leftarrow$ & \texttt{02} & = & 2 \\ +max run & $\leftarrow$ & \texttt{FF} & = & 255 \\ +max coded frame size & $\leftarrow$ & \texttt{24} & = & 36 bytes \\ +bitrate & $\leftarrow$ & \texttt{0AC4} & = & 2756 \\ +sample rate & $\leftarrow$ & \texttt{0000AC44} & = & 44100 Hz\\ +\end{tabular} + +\clearpage + +\subsection{Parsing the mdhd atom} +\label{alac:read_mdhd_atom} +\begin{figure}[h] +\includegraphics{alac/figures/mdhd.pdf} +\end{figure} +\par +\noindent +\VAR{version} indicates whether the Mac UTC date fields are 32 or 64 bit. +These date fields are seconds since the Macintosh Epoch, +which is 00:00:00, January 1st, 1904.\footnote{Why 1904? + It's the first leap year of the 20th century.} +To convert the Macintosh Epoch to a Unix Epoch timestamp +(seconds since January 1st, 1970), one needs to subtract 24,107 days - +or \texttt{2082844800} seconds. +\par +\noindent +\VAR{track length} is the total length of the ALAC file, in PCM frames. +\par +\noindent +\VAR{language} is 3, 5 bit fields encoded as ISO 639-2. +Add 96 to each field to convert the value to ASCII. + +\clearpage + +For example, given the bytes: +\begin{figure}[h] +\includegraphics{alac/figures/mdhd-parse.pdf} +\end{figure} +\par +\noindent +\begin{tabular}{rcrcll} +created MAC UTC date & $\leftarrow$ & \texttt{CA6BF4A9} & = & 3396072617 \\ +modified MAC UTC date & $\leftarrow$ & \texttt{CA6BFF5E} & = & 3396075358 \\ +sample rate & $\leftarrow$ & \texttt{0000AC44} & = & 44100 Hz \\ +PCM frame count & $\leftarrow$ & \texttt{8EACE80} & = & 149606016 & \texttt{56m 32s} \\ +$\text{language}_0$ & $\leftarrow$ & \texttt{15} & = & 21 & + 96 = `\texttt{u}'\\ +$\text{language}_1$ & $\leftarrow$ & \texttt{0E} & = & 14 & + 96 = `\texttt{n}'\\ +$\text{language}_2$ & $\leftarrow$ & \texttt{04} & = & 4 & + 96 = `\texttt{d}'\\ +\end{tabular} +\vskip .15in +\par +\noindent +Note that the language field is typically \texttt{und}, +meaning ``undetermined''. + +\clearpage + +\subsection{Decoding ALAC Frameset} +\label{alac:decode_frameset} +ALAC framesets contain multiple frames, +each of which contains 1 or 2 subframes. +\par +\noindent +\ALGORITHM{\texttt{mdat} atom data, decoding parameters from \texttt{alac} atom}{decoded PCM frames} +\SetKwData{CHANNELS}{channels} +$\CHANNELS \leftarrow$ (\READ 3 unsigned bits) + 1\; +\While{$\CHANNELS \neq 8$}{ + \hyperref[alac:decode_frame]{decode ALAC frame to 1 or 2 \CHANNELS of PCM data}\; + $\CHANNELS \leftarrow$ (\READ 3 unsigned bits) + 1\; +} +byte-align file stream\; +\Return all frames' channels as PCM frames\; +\EALGORITHM +\begin{figure}[h] +\includegraphics{alac/figures/stream.pdf} +\end{figure} + +\clearpage + +\subsection{Decoding ALAC Frame} +\label{alac:decode_frame} +{\relsize{-2} +\ALGORITHM{\texttt{mdat} atom data, channel count, decoding parameters from \texttt{alac} atom}{1 or 2 decoded channels of PCM data} +\SetKwData{CHANNELCOUNT}{channel count} +\SetKwData{SAMPLECOUNT}{sample count} +\SetKwData{SAMPLESPERFRAME}{samples per frame} +\SetKwData{BPS}{bits per sample} +\SetKwData{SAMPLESIZE}{sample size} +\SetKwData{HASSAMPLECOUNT}{has sample count} +\SetKwData{UNCOMPRESSEDLSBS}{uncompressed LSBs} +\SetKwData{NOTCOMPRESSED}{not compressed} +\SetKwData{INTERLACINGSHIFT}{interlacing shift} +\SetKwData{INTERLACINGLEFTWEIGHT}{interlacing leftweight} +\SetKwData{SUBFRAMEHEADER}{subframe header} +\SetKwData{QLPSHIFTNEEDED}{QLP shift needed} +\SetKwData{COEFF}{QLP coefficient} +\SetKwData{INITHISTORY}{initial history} +\SetKwData{MAXIMUMK}{maximum K} +\SetKwData{RESIDUALS}{residual block} +\SetKwData{LSB}{LSB} +\SetKwData{SUBFRAME}{subframe} +\SetKwData{CHANNEL}{channel} +\SetKw{AND}{and} +\SKIP 16 bits\; +$\HASSAMPLECOUNT \leftarrow$ \READ 1 unsigned bit\; +$\UNCOMPRESSEDLSBS \leftarrow$ \READ 2 unsigned bits\; +$\NOTCOMPRESSED \leftarrow$ \READ 1 unsigned bit\; +\uIf{$\HASSAMPLECOUNT = 0$}{ + \SAMPLECOUNT $\leftarrow$ \SAMPLESPERFRAME from \texttt{alac} atom\; +} +\lElse{\SAMPLECOUNT $\leftarrow$ \READ 32 unsigned bits\;} +\eIf(\tcc*[f]{raw, uncompressed frame}){$\NOTCOMPRESSED = 1$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}\SAMPLECOUNT}{ + \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ + $\text{\CHANNEL}_{c~i} \leftarrow$ \READ (\BPS) signed bits\; + } + } +}(\tcc*[f]{compressed frame}){ + $\SAMPLESIZE \leftarrow \BPS - (\UNCOMPRESSEDLSBS \times 8) + (\text{\CHANNELCOUNT} - 1)$\; + \INTERLACINGSHIFT $\leftarrow$ \READ 8 unsigned bits\; + \INTERLACINGLEFTWEIGHT $\leftarrow$ \READ 8 unsigned bits\; + \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ + $\left.\begin{tabular}{r} + $\text{\QLPSHIFTNEEDED}_c$ \\ + $\text{\COEFF}_c$ \\ +\end{tabular}\right\rbrace \leftarrow$ \hyperref[alac:read_subframe_header]{read subframe header}\; + } + \If{$\UNCOMPRESSEDLSBS > 0$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}\SAMPLECOUNT}{ + \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ + $\text{\LSB}_{c~i} \leftarrow$ \READ ($\UNCOMPRESSEDLSBS \times 8$) unsigned bits\; + } + } + } + \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ + $\text{\RESIDUALS}_c \leftarrow$ \hyperref[alac:read_residual_block]{read residual block} using $\left\lbrace\begin{tabular}{ll} + \INITHISTORY & from \texttt{alac} atom \\ + \MAXIMUMK & from \texttt{alac} atom \\ + \SAMPLESIZE \\ + \SAMPLECOUNT \\ +\end{tabular}\right.$\; + $\text{\SUBFRAME}_c \leftarrow$ \hyperref[alac:decode_subframe]{decode subframe using} $\left\lbrace\begin{tabular}{l} + $\text{\QLPSHIFTNEEDED}_c$ \\ + $\text{\COEFF}_c$ \\ + $\text{\RESIDUALS}_c$ \\ + \SAMPLESIZE \\ + \SAMPLECOUNT \\ +\end{tabular}\right.$\; + } + \uIf{$(\CHANNELCOUNT = 2)$ \AND $(\INTERLACINGLEFTWEIGHT > 0)$}{ + $\left.\begin{tabular}{r} + $\text{\CHANNEL}_0$ \\ + $\text{\CHANNEL}_1$ \\ + \end{tabular}\right\rbrace \leftarrow$ + \hyperref[alac:decorrelate_channels]{decorrelate channels} + $\left\lbrace\begin{tabular}{l} + $\text{\SUBFRAME}_0$ \\ + $\text{\SUBFRAME}_1$ \\ + \INTERLACINGSHIFT \\ + \INTERLACINGLEFTWEIGHT \\ + \end{tabular}\right.$\; + } + \lElse{\For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ + $\text{\CHANNEL}_c \leftarrow \text{\SUBFRAME}_c$\; + } + } + \If(\tcc*[f]{prepend any LSBs to each sample}){$\UNCOMPRESSEDLSBS > 0$}{ + \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ + \For{$i \leftarrow 0$ \emph{\KwTo}\SAMPLECOUNT}{ + $\text{\CHANNEL}_{c~i} \leftarrow (\text{\CHANNEL}_{c~i} \times 2 ^ {\UNCOMPRESSEDLSBS \times 8}) + \text{\LSB}_{c~i}$\; + } + } + } +} +\Return \CHANNEL\; +\EALGORITHM +} + +\clearpage + +\subsection{Reading Subframe Header} +\label{alac:read_subframe_header} +{\relsize{-1} +\ALGORITHM{\texttt{mdat} atom data}{QLP shift needed, QLP coefficient list} +\SetKw{OR}{or} +\SetKwData{PREDICTIONTYPE}{prediction type} +\SetKwData{QLPSHIFTNEEDED}{QLP shift needed} +\SetKwData{RICEMODIFIER}{Rice modifier} +\SetKwData{COEFFCOUNT}{coefficient count} +\SetKwData{COEFF}{QLP coefficient} +$\PREDICTIONTYPE \leftarrow$ \READ 4 unsigned bits\; +\ASSERT $\PREDICTIONTYPE = 0$\; +$\QLPSHIFTNEEDED \leftarrow$ \READ 4 unsigned bits\; +$\RICEMODIFIER \leftarrow$ \READ 3 unsigned bits\tcc*[r]{unused} +$\COEFFCOUNT \leftarrow$ \READ 5 unsigned bits\; +\ASSERT $(\COEFFCOUNT = 4)$ \OR $(\COEFFCOUNT = 8)$\; +\For{$i \leftarrow 0$ \emph{\KwTo}\COEFFCOUNT}{ + $\text{\COEFF}_i \leftarrow$ \READ 16 signed bits\; +} +\Return $\left\lbrace\begin{tabular}{l} +$\text{\QLPSHIFTNEEDED}$ \\ +$\text{\COEFF}$ \\ +\end{tabular}\right.$\; +\EALGORITHM +} +\begin{figure}[h] +\includegraphics{alac/figures/subframe_header.pdf} +\end{figure} +\par +\noindent +For example, given the bytes on the opposite page, +our frame and subframe headers are: +\begin{table}[h] +{\relsize{-1} +\begin{tabular}{rclcl} +\multicolumn{5}{l}{frame header:} \\ +\textsf{channels} & $\leftarrow$ & \texttt{0} (+1) &=& 2 \\ +\textsf{has sample count} & $\leftarrow$ & \texttt{1} \\ +\textsf{uncompressed LSBs} & $\leftarrow$ & \texttt{0} \\ +\textsf{not compressed} & $\leftarrow$ & \texttt{0} \\ +\textsf{sample count} & $\leftarrow$ & \texttt{0x19} &=& 25 \\ +\textsf{interlacing shift} & $\leftarrow$ & \texttt{2} \\ +\textsf{interlacing leftweight} & $\leftarrow$ & \texttt{2} \\ +\hline +\multicolumn{5}{l}{subframe header 0:} \\ +$\textsf{prediction type}_0$ & $\leftarrow$ & \texttt{0} \\ +$\textsf{QLP shift needed}_0$ & $\leftarrow$ & \texttt{9} \\ +$\textsf{Rice modifier}_0$ & $\leftarrow$ & \texttt{4} \\ +$\textsf{coefficient count}_0$ & $\leftarrow$ & \texttt{4} \\ +$\textsf{coefficient}_{0~0}$ & $\leftarrow$ & \texttt{0x05A6} &=& 1446 \\ +$\textsf{coefficient}_{0~1}$ & $\leftarrow$ & \texttt{0xF943} &=& -1725 \\ +$\textsf{coefficient}_{0~2}$ & $\leftarrow$ & \texttt{0x0430} &=& 1072 \\ +$\textsf{coefficient}_{0~3}$ & $\leftarrow$ & \texttt{0xFECF} &=& -305 \\ +\hline +\multicolumn{5}{l}{subframe header 1:} \\ +$\textsf{prediction type}_1$ & $\leftarrow$ & \texttt{0} \\ +$\textsf{QLP shift needed}_1$ & $\leftarrow$ & \texttt{9} \\ +$\textsf{Rice modifier}_1$ & $\leftarrow$ & \texttt{4} \\ +$\textsf{coefficient count}_1$ & $\leftarrow$ & \texttt{4} \\ +$\textsf{coefficient}_{1~0}$ & $\leftarrow$ & \texttt{0x0587} &=& 1415 \\ +$\textsf{coefficient}_{1~1}$ & $\leftarrow$ & \texttt{0xF987} &=& -1657 \\ +$\textsf{coefficient}_{1~2}$ & $\leftarrow$ & \texttt{0x03F3} &=& 1011 \\ +$\textsf{coefficient}_{1~3}$ & $\leftarrow$ & \texttt{0xFEE5} &=& -283 \\ +\end{tabular} +} +\end{table} + +\clearpage + +\begin{figure}[h] +\includegraphics{alac/figures/subframe-parse.pdf} +\caption{mdat atom bytes} +\end{figure} + +\clearpage + +\subsection{Reading Residual Block} +\label{alac:read_residual_block} +\ALGORITHM{\texttt{mdat} atom data, initial history and maximum K from \texttt{alac} atom, sample count, sample size}{a decoded list of signed residuals} +\SetKwData{SAMPLECOUNT}{sample count} +\SetKwData{INITHISTORY}{initial history} +\SetKwData{MAXIMUMK}{maximum K} +\SetKwData{SIGN}{sign modifier} +\SetKwData{HISTORY}{history} +\SetKwData{HISTORYMULT}{history multiplier} +\SetKwData{ZEROES}{zero residuals count} +\SetKw{AND}{and} +\SetKwFunction{MIN}{min} +\SetKwFunction{READRESIDUAL}{read residual} +\SetKwData{RESIDUAL}{residual} +$\HISTORY \leftarrow \INITHISTORY$\; +\SIGN $\leftarrow 0$\; +\For{$i \leftarrow 0$ \emph{\KwTo}\SAMPLECOUNT}{ + $\kappa \leftarrow \MIN(\lfloor\log_2(\HISTORY \div 2 ^ 9 + 3)\rfloor~,~\MAXIMUMK)$\; + $u_i \leftarrow \READRESIDUAL(\kappa~,~\text{sample size}) + \SIGN$\; + \SIGN $\leftarrow 0$\; + \BlankLine + \eIf(\tcc*[f]{apply sign bit to unsigned value}){$(u_i \bmod 0) = 0$}{ + $\text{\RESIDUAL}_i \leftarrow u_i \div 2$\; + }{ + $\text{\RESIDUAL}_i \leftarrow -((u_i + 1) \div 2)$\; + } + \BlankLine + \eIf(\tcc*[f]{update history}){$u_i \leq 65535$}{ + \HISTORY $\leftarrow \HISTORY + (u_i \times \HISTORYMULT) - \left\lfloor\frac{\HISTORY \times \HISTORYMULT}{2 ^ 9}\right\rfloor$\; + \If{$\HISTORY < 128$ \AND $(i + 1) < \SAMPLECOUNT$}{ + \tcc{generate run of 0 residuals if history gets too small} + $\kappa \leftarrow \MIN(7 - \lfloor\log_2(\HISTORY)\rfloor + ((\HISTORY + 16) \div 64)~,~\MAXIMUMK)$\; + \ZEROES $\leftarrow \READRESIDUAL(\kappa~,~16)$\; + \For{$j \leftarrow 0$ \emph{\KwTo}\ZEROES}{ + $\text{\RESIDUAL}_{i + j + 1} \leftarrow 0$\; + } + $i \leftarrow i + j$\; + \HISTORY $\leftarrow 0$\; + \If{$\ZEROES \leq 65535$}{ + \SIGN $\leftarrow 1$\; + } + } + }{ + \HISTORY $\leftarrow 65535$\; + } +} +\Return \RESIDUAL\; +\EALGORITHM + +\clearpage + +\subsubsection{Reading Residual} +\ALGORITHM{\texttt{mdat} atom data, $\kappa$, sample size}{an unsigned residual} +\SetKwData{MSB}{MSB} +\SetKwData{LSB}{LSB} +\SetKw{UNREAD}{unread} +\MSB $\leftarrow$ \UNARY with stop bit 0, to a maximum of 8 bits\; +\uIf{9, \texttt{1} bits encountered}{ + \Return \READ (sample size) unsigned bits\; +} +\uElseIf{$\kappa = 0$}{ + \Return \MSB\; +} +\Else{ + \LSB $\leftarrow$ \READ $\kappa$ unsigned bits\; + \uIf{$\LSB > 1$}{ + \Return $\MSB \times (2 ^ \kappa - 1) + (\LSB - 1)$\; + } + \uElseIf{$\LSB = 1$}{ + \UNREAD single \texttt{1} bit back into stream\; + \Return $\MSB \times (2 ^ \kappa - 1)$\; + } + \Else{ + \UNREAD single \texttt{0} bit back into stream\; + \Return $\MSB \times (2 ^ \kappa - 1)$\; + } +} +\EALGORITHM + +\begin{landscape} +\subsubsection{Residual Decoding Example} +For this example, \VAR{initial history} (from the \texttt{alac} atom) is 10. +\par +\begin{figure}[h] +\includegraphics{alac/figures/residual-parse.pdf} +\end{figure} +\par +\noindent +Note how unreading a bit when $i = 1$ means that $\text{LSB}_1$'s 3rd bit +(a \texttt{1} in this case) is also $\text{MSB}_2$'s 1st bit. +This is signified by $\text{}_1 \leftrightarrow \text{}_2$ +since the same bit is in both fields. + +\clearpage + +\begin{table}[h] +{\relsize{-1} +\renewcommand{\arraystretch}{1.25} +\begin{tabular}{r||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}} +$i$ & \kappa & \textsf{MSB}_i & \textsf{LSB}_i & \textsf{unsigned}_i & +\textsf{residual}_i & \textsf{history}_{i + 1} \\ +\hline +0 & +\lfloor\log_2(10 \div 2 ^ 9 + 3)\rfloor = 1 & +9 & & 64 & +64 \div 2 = 32 & +10 + (64 \times 40) - \left\lfloor\frac{10 \times 40}{2 ^ 9}\right\rfloor = 2570 +\\ +1 & +\lfloor\log_2(2570 \div 2 ^ 9 + 3)\rfloor = 3 & +2 & *1 & 2 \times (2 ^ 3 - 1) = 14 & +14 \div 2 = 7 & +2570 + (14 \times 40) - \left\lfloor\frac{2570 \times 40}{2 ^ 9}\right\rfloor = 2930 +\\ +2 & +\lfloor\log_2(2930 \div 2 ^ 9 + 3)\rfloor = 3 & +1 & 2 & 1 \times (2 ^ 3 - 1) + (2 - 1) = 8 & +8 \div 2 = 4 & +2930 + (8 \times 40) - \left\lfloor\frac{2930 \times 40}{2 ^ 9}\right\rfloor = 3022 +\\ +3 & +\lfloor\log_2(3022 \div 2 ^ 9 + 3)\rfloor = 3 & +0 & 5 & 0 \times (2 ^ 3 - 1) + (5 - 1) = 4 & +4 \div 2 = 2 & +3022 + (4 \times 40) - \left\lfloor\frac{3022 \times 40}{2 ^ 9}\right\rfloor = 2946 +\\ +4 & +\lfloor\log_2(2946 \div 2 ^ 9 + 3)\rfloor = 3 & +0 & 2 & 0 \times (2 ^ 3 - 1) + (2 - 1) = 1 & +-((1 + 1) \div 2) = -1 & +2946 + (1 \times 40) - \left\lfloor\frac{2946 \times 40}{2 ^ 9}\right\rfloor = 2756 +\\ +5 & +\lfloor\log_2(2756 \div 2 ^ 9 + 3)\rfloor = 3 & +0 & 2 & 0 \times (2 ^ 3 - 1) + (2 - 1) = 1 & +-((1 + 1) \div 2) = -1 & +2756 + (1 \times 40) - \left\lfloor\frac{2756 \times 40}{2 ^ 9}\right\rfloor = 2581 +\\ +6 & +\lfloor\log_2(2581 \div 2 ^ 9 + 3)\rfloor = 3 & +0 & 2 & 0 \times (2 ^ 3 - 1) + (2 - 1) = 1 & +-((1 + 1) \div 2) = -1 & +2581 + (1 \times 40) - \left\lfloor\frac{2581 \times 40}{2 ^ 9}\right\rfloor = 2420 +\\ +7 & +\lfloor\log_2(2420 \div 2 ^ 9 + 3)\rfloor = 2 & +2 & 2 & 2 \times (2 ^ 2 - 1) + (2 - 1) = 7 & +-((7 + 1) \div 2) = -4 & +2420 + (7 \times 40) - \left\lfloor\frac{2420 \times 40}{2 ^ 9}\right\rfloor = 2511 +\\ +8 & +\lfloor\log_2(2511 \div 2 ^ 9 + 3)\rfloor = 2 & +0 & 2 & 0 \times (2 ^ 2 - 1) + (2 - 1) = 1 & +-((1 + 1) \div 2) = -1 & +2511 + (1 \times 40) - \left\lfloor\frac{2511 \times 40}{2 ^ 9}\right\rfloor = 2355 +\\ +9 & +\lfloor\log_2(2355 \div 2 ^ 9 + 3)\rfloor = 2 & +2 & 2 & 2 \times (2 ^ 2 - 1) + (2 - 1) = 7 & +-((7 + 1) \div 2) = -4 & +2355 + (7 \times 40) - \left\lfloor\frac{2355 \times 40}{2 ^ 9}\right\rfloor = 2452 +\\ +10 & +\lfloor\log_2(2452 \div 2 ^ 9 + 3)\rfloor = 2 & +1 & *1 & 1 \times (2 ^ 2 - 1) = 3 & +-((3 + 1) \div 2) = -2 & +2452 + (3 \times 40) - \left\lfloor\frac{2452 \times 40}{2 ^ 9}\right\rfloor = 2381 +\\ +11 & +\lfloor\log_2(2381 \div 2 ^ 9 + 3)\rfloor = 2 & +1 & 3 & 1 \times (2 ^ 2 - 1) + (3 - 1) = 5 & +-((5 + 1) \div 2) = -3 & +2381 + (5 \times 40) - \left\lfloor\frac{2381 \times 40}{2 ^ 9}\right\rfloor = 2395 +\\ +12 & +\lfloor\log_2(2395 \div 2 ^ 9 + 3)\rfloor = 2 & +0 & 2 & 0 \times (2 ^ 2 - 1) + (2 - 1) = 1 & +-((1 + 1) \div 2) = -1 & +2395 + (1 \times 40) - \left\lfloor\frac{2395 \times 40}{2 ^ 9}\right\rfloor = 2248 +\\ +13 & +\lfloor\log_2(2248 \div 2 ^ 9 + 3)\rfloor = 2 & +0 & 2 & 0 \times (2 ^ 2 - 1) + (2 - 1) = 1 & +-((1 + 1) \div 2) = -1 & +2248 + (1 \times 40) - \left\lfloor\frac{2248 \times 40}{2 ^ 9}\right\rfloor = 2113 +\\ +14 & +\lfloor\log_2(2113 \div 2 ^ 9 + 3)\rfloor = 2 & +0 & 2 & 0 \times (2 ^ 2 - 1) + (2 - 1) = 1 & +-((1 + 1) \div 2) = -1 & +2113 + (1 \times 40) - \left\lfloor\frac{2113 \times 40}{2 ^ 9}\right\rfloor = 1988 +\\ +15 & +\lfloor\log_2(1988 \div 2 ^ 9 + 3)\rfloor = 2 & +0 & 3 & 0 \times (2 ^ 2 - 1) + (3 - 1) = 2 & +2 \div 2 = 1 & +1988 + (2 \times 40) - \left\lfloor\frac{1988 \times 40}{2 ^ 9}\right\rfloor = 1913 +\\ +16 & +\lfloor\log_2(1913 \div 2 ^ 9 + 3)\rfloor = 2 & +0 & *0 & 0 \times (2 ^ 2 - 1) = 0 & +0 \div 2 = 0 & +1913 + (0 \times 40) - \left\lfloor\frac{1913 \times 40}{2 ^ 9}\right\rfloor = 1764 +\\ +17 & +\lfloor\log_2(1764 \div 2 ^ 9 + 3)\rfloor = 2 & +0 & *1 & 0 \times (2 ^ 2 - 1) = 0 & +0 \div 2 = 0 & +1764 + (0 \times 40) - \left\lfloor\frac{1764 \times 40}{2 ^ 9}\right\rfloor = 1627 +\\ +18 & +\lfloor\log_2(1627 \div 2 ^ 9 + 3)\rfloor = 2 & +2 & *1 & 2 \times (2 ^ 2 - 1) = 6 & +6 \div 2 = 3 & +1627 + (6 \times 40) - \left\lfloor\frac{1627 \times 40}{2 ^ 9}\right\rfloor = 1740 +\\ +19 & +\lfloor\log_2(1740 \div 2 ^ 9 + 3)\rfloor = 2 & +1 & 2 & 1 \times (2 ^ 2 - 1) + (2 - 1) = 4 & +4 \div 2 = 2 & +1740 + (4 \times 40) - \left\lfloor\frac{1740 \times 40}{2 ^ 9}\right\rfloor = 1765 +\\ +20 & +\lfloor\log_2(1765 \div 2 ^ 9 + 3)\rfloor = 2 & +0 & 3 & 0 \times (2 ^ 2 - 1) + (3 - 1) = 2 & +2 \div 2 = 1 & +1765 + (2 \times 40) - \left\lfloor\frac{1765 \times 40}{2 ^ 9}\right\rfloor = 1708 +\\ +21 & +\lfloor\log_2(1708 \div 2 ^ 9 + 3)\rfloor = 2 & +2 & 3 & 2 \times (2 ^ 2 - 1) + (3 - 1) = 8 & +8 \div 2 = 4 & +1708 + (8 \times 40) - \left\lfloor\frac{1708 \times 40}{2 ^ 9}\right\rfloor = 1895 +\\ +22 & +\lfloor\log_2(1895 \div 2 ^ 9 + 3)\rfloor = 2 & +0 & 3 & 0 \times (2 ^ 2 - 1) + (3 - 1) = 2 & +2 \div 2 = 1 & +1895 + (2 \times 40) - \left\lfloor\frac{1895 \times 40}{2 ^ 9}\right\rfloor = 1827 +\\ +23 & +\lfloor\log_2(1827 \div 2 ^ 9 + 3)\rfloor = 2 & +2 & *1 & 2 \times (2 ^ 2 - 1) = 6 & +6 \div 2 = 3 & +1827 + (6 \times 40) - \left\lfloor\frac{1827 \times 40}{2 ^ 9}\right\rfloor = 1925 +\\ +24 & +\lfloor\log_2(1925 \div 2 ^ 9 + 3)\rfloor = 2 & +1 & 2 & 1 \times (2 ^ 2 - 1) + (2 - 1) = 4 & +4 \div 2 = 2 & +1925 + (4 \times 40) - \left\lfloor\frac{1925 \times 40}{2 ^ 9}\right\rfloor = 1935 +\\ +\end{tabular} +\renewcommand{\arraystretch}{1.0} +} +\end{table} +\end{landscape} + +\clearpage + +\subsection{Decoding Subframe} +\label{alac:decode_subframe} +{\relsize{-1} +\ALGORITHM{sample count, sample size, QLP coefficients, QLP shift needed, signed residuals}{a list of signed subframe samples} +\SetKwData{RESIDUAL}{residual} +\SetKwData{SAMPLE}{sample} +\SetKwData{SAMPLESIZE}{sample size} +\SetKwData{COEFFCOUNT}{coefficient count} +\SetKwData{SAMPLECOUNT}{sample count} +\SetKwData{BASE}{base sample} +\SetKwData{QLPSUM}{QLP sum} +\SetKwData{QLPCOEFF}{QLP coefficient} +\SetKwData{QLPSHIFT}{QLP shift needed} +\SetKwData{DIFF}{diff} +\SetKwData{SSIGN}{sign} +\SetKw{BREAK}{break} +\SetKwData{ORIGSIGN}{original sign} +\SetKwFunction{TRUNCATE}{truncate} +\SetKwFunction{SIGN}{sign} +$\text{\SAMPLE}_0 \leftarrow \text{\RESIDUAL}_0$\tcc*[r]{first sample always copied verbatim} +\eIf{$\COEFFCOUNT < 31$}{ + \For{$i \leftarrow 1$ \emph{\KwTo}$\text{\COEFFCOUNT} + 1$}{ + $\text{\SAMPLE}_i \leftarrow \TRUNCATE(\text{\RESIDUAL}_{i} + \text{\SAMPLE}_{i - 1}~,~\SAMPLESIZE)$\; + } + \BlankLine + \For{i $\leftarrow \text{\COEFFCOUNT} + 1$ \emph{\KwTo}$\text{\SAMPLECOUNT}$}{ + $\text{\BASE}_i \leftarrow \text{\SAMPLE}_{i - \COEFFCOUNT - 1}$\; + $\text{\QLPSUM}_i \leftarrow \overset{\COEFFCOUNT - 1}{\underset{j = 0}{\sum}} \text{\QLPCOEFF}_j \times (\text{\SAMPLE}_{i - j - 1} - \text{\BASE}_i)$\; + $\text{\SAMPLE}_i \leftarrow \TRUNCATE\left(\left\lfloor\frac{\text{\QLPSUM}_i + 2 ^ \text{\QLPSHIFT - 1}}{2 ^ \text{\QLPSHIFT}}\right\rfloor + \text{\RESIDUAL}_i + \text{\BASE}_i~,~\SAMPLESIZE\right)$\; + \BlankLine + \uIf(\tcc*[f]{modify QLP coefficients}){$\text{\RESIDUAL}_i > 0$}{ + \For{$j \leftarrow 0$ \emph{\KwTo}$\text{\COEFFCOUNT}$}{ + $\DIFF \leftarrow \text{\BASE}_i - \text{\SAMPLE}_{i - \COEFFCOUNT + j}$\; + $\SSIGN \leftarrow \SIGN(\DIFF)$\; + $\text{\QLPCOEFF}_{\COEFFCOUNT - j - 1} \leftarrow \text{\QLPCOEFF}_{\COEFFCOUNT - j - 1} - \SSIGN$\; + $\text{\RESIDUAL}_i \leftarrow \text{\RESIDUAL}_i - \left\lfloor\frac{\DIFF \times \SSIGN}{2 ^ \text{\QLPSHIFT}}\right\rfloor \times (j + 1)$\; + \If{$\text{\RESIDUAL}_i \leq 0$}{ + \BREAK\; + } + } + } + \ElseIf{$\text{\RESIDUAL}_i < 0$}{ + \For{$j \leftarrow 0$ \emph{\KwTo}$\text{\COEFFCOUNT}$}{ + $\DIFF \leftarrow \text{\BASE}_i - \text{\SAMPLE}_{i - \COEFFCOUNT + j}$\; + $\SSIGN \leftarrow \SIGN(\DIFF)$\; + $\text{\QLPCOEFF}_{\COEFFCOUNT - j - 1} \leftarrow \text{\QLPCOEFF}_{\COEFFCOUNT - j - 1} + \SSIGN$\; + $\text{\RESIDUAL}_i \leftarrow \text{\RESIDUAL}_i - \left\lfloor\frac{\DIFF \times -\SSIGN}{2 ^ \text{\QLPSHIFT}}\right\rfloor \times (j + 1)$\; + \If{$\text{\RESIDUAL}_i \geq 0$}{ + \BREAK\; + } + } + } + } +}{ + \For{$i \leftarrow 1$ \emph{\KwTo}\SAMPLECOUNT}{ + $\text{\SAMPLE}_i \leftarrow \TRUNCATE(\text{\RESIDUAL}_i + \text{\SAMPLE}_{i - 1}~,~\SAMPLESIZE)$\; + } +} +\Return \SAMPLE\; +\EALGORITHM +} + +\subsubsection{The \texttt{truncate} Function} +{\relsize{-1} + \ALGORITHM{a signed sample, the maximum size of the sample in bits}{a truncated signed sample} + \SetKw{BAND}{bitwise and} + \SetKwData{SAMPLE}{sample} + \SetKwData{BITS}{bits} + \SetKwData{TRUNCATED}{truncated} + $\TRUNCATED \leftarrow \SAMPLE~\BAND~(2 ^ {\BITS} - 1)$\; + \eIf(\tcc*[f]{apply sign bit}){$(\TRUNCATED~\BAND~2 ^ {\BITS - 1}) \neq 0$}{ + \Return $\TRUNCATED - 2 ^ {\BITS}$\; + }{ + \Return \TRUNCATED\; + } + \EALGORITHM +} + +\clearpage + +\subsubsection{The \texttt{sign} Function} +{\relsize{-1} +\begin{equation*} +\texttt{sign}(x) = +\begin{cases} +\texttt{ 1} & \text{if } x > 0 \\ +\texttt{ 0} & \text{if } x = 0 \\ +\texttt{-1} & \text{if } x < 0 +\end{cases} +\end{equation*} +} + +\subsubsection{Decoding Subframe Example} +Given the residuals +\texttt{32, 7, 4, 2, -1, -1, -1, -4, -1, -4, -2}, +the QLP coefficients +\texttt{1446, -1725, 1072, -305} +and a QLP shift needed value of \texttt{9}, +the subframe samples are calculated as follows: +\begin{table}[h] +{\relsize{-1} +\begin{tabular}{r||r|r|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}} +$i$ & $\textsf{residual}_i$ & $\textsf{base}_i$ & \textsf{QLP sum}_i & \textsf{sample}_i & \textsf{QLP coefficient}_{(i + 1)~j} \\ +\hline +0 & 32 & & & 32 \\ +1 & 7 & & & 7 + 32 = 39 \\ +2 & 4 & & & 4 + 39 = 43 \\ +3 & 2 & & & 2 + 43 = 45 \\ +4 & -1 & & & -1 + 45 = 44 \\ +\hline +5 & -1 & 32 & 1446 \times (44 - 32) \texttt{ +} & \lfloor(4584 + 2 ^ 8) \div 2 ^ 9\rfloor - 1 + 32 = 40 & 1446 \\ +& & & -1725 \times (45 - 32) \texttt{ +}& & -1725 \\ +& & & 1072 \times (43 - 32) \texttt{ +} & & 1072 \\ +& & & -305 \times (39 - 32) \texttt{~~} & & -305 - 1 = -306 \\ +\hline +6 & -1 & 39 & 1446 \times (40 - 39) \texttt{ +} & \lfloor(-1971 + 2 ^ 8) \div 2 ^ 9\rfloor - 1 + 39 = 34 & 1446 \\ +& & & -1725 \times (44 - 39) \texttt{ +} & & -1725 \\ +& & & 1072 \times (45 - 39) \texttt{ +} & & 1072 \\ +& & & -306 \times (43 - 39) \texttt{~~} & & -306 - 1 = -307 \\ +\hline +7 & -4 & 43 & 1446 \times (34 - 43) \texttt{ +} & \lfloor(-7381 + 2 ^ 8) \div 2 ^ 9\rfloor - 4 + 43 = 25 & 1446 \\ +& & & -1725 \times (40 - 43) \texttt{ +} & & -1725 + 1 = -1724 \\ +& & & 1072 \times (44 - 43) \texttt{ +} & & 1072 - 1 = 1071 \\ +& & & -307 \times (45 - 43) \texttt{~~} & & -307 - 1 = -308 \\ +\hline +8 & -1 & 45 & 1446 \times (25 - 45) \texttt{ +} & \lfloor(-15003 + 2 ^ 8) \div 2 ^ 9\rfloor - 1 + 45 = 15 & 1446 \\ +& & & -1724 \times (34 - 45) \texttt{ +} & & -1724 \\ +& & & 1071 \times (40 - 45) \texttt{ +} & & 1071 \\ +& & & -308 \times (44 - 45) \texttt{~~} & & -308 + 1 = -307 \\ +\hline +9 & -4 & 44 & 1446 \times (15 - 44) \texttt{ +} & \lfloor(-18660 + 2 ^ 8) \div 2 ^ 9\rfloor - 4 + 44 = 4 & 1446 \\ +& & & -1724 \times (25 - 44) \texttt{ +} & & -1724 + 1 = -1723 \\ +& & & 1071 \times (34 - 44) \texttt{ +} & & 1071 + 1 = 1072 \\ +& & & -307 \times (40 - 44) \texttt{~~} & & -307 + 1 = -306 \\ +\hline +10 & -2 & 40 & 1446 \times (4 - 40) \texttt{ +} & \lfloor(-23225 + 2 ^ 8) \div 2 ^ 9\rfloor - 2 + 40 = -7 & 1446 \\ +& & & -1723 \times (15 - 40) \texttt{ +} & & -1723 \\ +& & & 1072 \times (25 - 40) \texttt{ +} & & 1072 + 1 = 1073 \\ +& & & -306 \times (34 - 40) \texttt{~~} & & -306 + 1 = -305 \\ +\hline +\end{tabular} +} +\end{table} + +Although some steps have been omitted for brevity, +what's important to note is how the base sample +is removed prior to $\textsf{QLP sum}_i$ calculation, +how it is re-added during $\textsf{sample}_i$ calculation +and how the next sample's \textsf{QLP coefficient} values are shifted. + +\clearpage + +\subsection{Channel Decorrelation} +\label{alac:decorrelate_channels} +\ALGORITHM{$\text{subframe samples}_0$, $\text{subframe samples}_1$, interlacing shift and interlacing leftweight from frame header}{left and right channels} +\SetKwData{LEFTWEIGHT}{interlacing leftweight} +\SetKwData{SAMPLECOUNT}{sample count} +\SetKwData{SUBFRAME}{subframe} +\SetKwData{LEFT}{left} +\SetKwData{RIGHT}{right} +\For{$i \leftarrow 0$ \emph{\KwTo}\SAMPLECOUNT}{ + $\text{\RIGHT}_i \leftarrow \text{\SUBFRAME}_{0~i} - \left\lfloor\frac{\text{\SUBFRAME}_{1~i} \times \text{\LEFTWEIGHT}}{2 ^ \text{interlacing shift}}\right\rfloor$\; + $\text{\LEFT}_i \leftarrow \text{\SUBFRAME}_{1~i} + \text{\RIGHT}_i$ +} +\Return $\left\lbrace\begin{tabular}{l} +\LEFT \\ +\RIGHT \\ +\end{tabular}\right.$ +\EALGORITHM +For example, given the $\textsf{subframe}_0$ samples of 14, 15, 19, 17, 18; +the $\textsf{subframe}_1$ samples of 16, 17, 26, 25, 24, +an \VAR{interlacing shift} value of 2 and an \VAR{interlacing leftweight} +values of 3, we calculate output samples as follows: +\begin{table}[h] +\begin{tabular}{|c||>{$}r<{$}|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|} +\hline +$i$ & \textsf{subframe}_{0~i} & \textsf{subframe}_{1~i} & \textsf{right}_i & \textsf{left}_i \\ +\hline +0 & 14 & 16 & 14 - \lfloor(16 \times 3) \div 2^2\rfloor = \textbf{2} & 16 + \textbf{2} = \textbf{18} \\ +1 & 15 & 17 & 15 - \lfloor(17 \times 3) \div 2^2\rfloor = \textbf{3} & 17 + \textbf{3} = \textbf{20} \\ +2 & 19 & 26 & 19 - \lfloor(26 \times 3) \div 2^2\rfloor = \textbf{0} & 26 + \textbf{0} = \textbf{26} \\ +3 & 17 & 25 & 17 - \lfloor(25 \times 3) \div 2^2\rfloor = \textbf{-1} & 25 + \textbf{-1} = \textbf{24} \\ +4 & 18 & 24 & 18 - \lfloor(24 \times 3) \div 2^2\rfloor = \textbf{0} & 24 + \textbf{0} = \textbf{24} \\ +\hline +\end{tabular} +\end{table} + +\subsection{Channel Assignment} +\begin{table}[h] +\begin{tabular}{r|l} +channels & assignment \\ +\hline +1 & mono \\ +2 & left, right \\ +3 & center, left, right \\ +4 & center, left, right, center surround \\ +5 & center, left, right, left surround, right surround \\ +6 & center, left, right, left surround, right surround, LFE \\ +7 & center, left, right, left surround, right surround, center surround, LFE \\ +8 & center, left center, right center, left, right, left surround, right surround, LFE \\ +\end{tabular} +\end{table}
View file
audiotools-2.19.tar.gz/docs/reference/alac/encode
Added
+(directory)
View file
audiotools-2.19.tar.gz/docs/reference/alac/encode.tex
Added
@@ -0,0 +1,464 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\section{ALAC Encoding} + +To encode an ALAC file, we need a stream of PCM sample integers +along with that stream's sample rate, bits-per-sample and number of +channels. +We'll start by encoding all of the non-audio ALAC atoms, +most of which are contained within the \ATOM{moov} atom. +There's over twenty atoms in a typical ALAC file, +most of which are packed with seemingly redundant or +nonessential data, +so it will take awhile before we can move on to the actual +audio encoding process. + +Remember, all of an ALAC's fields are big-endian. + +\input{alac/encode/atoms} + +\clearpage + +\subsection{Encoding mdat Atom} +\ALGORITHM{PCM frames, various encoding parameters: +\newline +\begin{tabular}{rl} +parameter & typical value \\ +\hline +block size & 4096 \\ +initial history & 40 \\ +history multiplier & 10 \\ +maximum K & 14 \\ +interlacing shift & 2 \\ +minimum interlacing leftweight & 0 \\ +maximum interlacing leftweight & 4 \\ +\end{tabular} +}{an encoded \texttt{mdat} atom} +\SetKwData{BLOCKSIZE}{block size} +$0 \rightarrow$ \WRITE 32 unsigned bits\tcc*[r]{placeholder length} +$\texttt{"mdat"} \rightarrow$ \WRITE 4 bytes\; +\While{PCM frames remain}{ + take \BLOCKSIZE PCM frames from the input\; + \hyperref[alac:encode_frameset]{write PCM frames to frameset}\; +} +return to start of \texttt{mdat} atom and write actual length\; +\EALGORITHM +\begin{figure}[h] +\includegraphics{alac/figures/stream.pdf} +\end{figure} + +\clearpage + +\subsection{Encoding Frameset} +\label{alac:encode_frameset} +{\relsize{-2} +\ALGORITHM{1 or more channels of PCM frames}{1 or more ALAC frames as a frameset} +\SetKwData{CHANCOUNT}{channel count} +\SetKwData{FRAMEDATA}{frame channels} +\Switch{\CHANCOUNT}{ + \uCase{1}{ + \hyperref[alac:encode_frame]{encode mono as 1 channel frame}\; + } + \uCase{2}{ + \hyperref[alac:encode_frame]{encode left,right as 2 channel frame}\; + } + \uCase{3}{ + \hyperref[alac:encode_frame]{encode center as 1 channel frame}\; + \hyperref[alac:encode_frame]{encode left,right as 2 channel frame}\; + } + \uCase{4}{ + \hyperref[alac:encode_frame]{encode center as 1 channel frame}\; + \hyperref[alac:encode_frame]{encode left,right as 2 channel frame}\; + \hyperref[alac:encode_frame]{encode center surround as 1 channel frame}\; + } + \uCase{5}{ + \hyperref[alac:encode_frame]{encode center as 1 channel frame}\; + \hyperref[alac:encode_frame]{encode left,right as 2 channel frame}\; + \hyperref[alac:encode_frame]{encode left surround,right surround as 2 channel frame}\; + } + \uCase{6}{ + \hyperref[alac:encode_frame]{encode center as 1 channel frame}\; + \hyperref[alac:encode_frame]{encode left,right as 2 channel frame}\; + \hyperref[alac:encode_frame]{encode left surround,right surround as 2 channel frame}\; + \hyperref[alac:encode_frame]{encode LFE as 1 channel frame}\; + } + \uCase{7}{ + \hyperref[alac:encode_frame]{encode center as 1 channel frame}\; + \hyperref[alac:encode_frame]{encode left,right as 2 channel frame}\; + \hyperref[alac:encode_frame]{encode left surround,right surround as 2 channel frame}\; + \hyperref[alac:encode_frame]{encode center surround as 1 channel frame}\; + \hyperref[alac:encode_frame]{encode LFE as 1 channel frame}\; + } + \Case{8}{ + \hyperref[alac:encode_frame]{encode center as 1 channel frame}\; + \hyperref[alac:encode_frame]{encode left center,right center as 2 channel frame}\; + \hyperref[alac:encode_frame]{encode left,right as 2 channel frame}\; + \hyperref[alac:encode_frame]{encode left surround,right surround as 1 channel frame}\; + \hyperref[alac:encode_frame]{encode LFE as 1 channel frame}\; + } + $7 \rightarrow$ \WRITE 3 unsigned bits\; + byte align output stream\; +} +\Return encoded frameset\; +\EALGORITHM +} + +\subsubsection{Channel Assignment} +\begin{tabular}{r|l} +channels & assignment \\ +\hline +1 & mono \\ +2 & left, right \\ +3 & center, left, right \\ +4 & center, left, right, center surround \\ +5 & center, left, right, left surround, right surround \\ +6 & center, left, right, left surround, right surround, LFE \\ +7 & center, left, right, left surround, right surround, center surround, LFE \\ +8 & center, left center, right center, left, right, left surround, right surround, LFE \\ +\end{tabular} + +\clearpage + +\subsection{Encoding Frame} +\label{alac:encode_frame} +{\relsize{-1} +\ALGORITHM{1 or 2 channels of PCM data, encoding parameters}{a compressed or uncompressed ALAC frame} +\SetKwData{PCMCOUNT}{PCM frame count} +\SetKwData{CHANCOUNT}{channel count} +\SetKwData{COMPRESSED}{compressed frame} +\SetKwData{UNCOMPRESSED}{uncompressed frame} +\SetKwData{BPS}{bits per sample} +\SetKwFunction{LEN}{len} +$\CHANCOUNT - 1 \rightarrow$ \WRITE 3 unsigned bits\; +\eIf{$\text{\PCMCOUNT} \geq 10$}{ + $\UNCOMPRESSED \leftarrow$ \hyperref[alac:write_uncompressed_frame]{encode channel as uncompressed frame}\; + $\COMPRESSED \leftarrow$ \hyperref[alac:write_compressed_frame]{encode channels as compressed frame}\; + \uIf{residual overflow occurred}{ + \Return \UNCOMPRESSED\; + } + \uElseIf{$\LEN(\COMPRESSED) \geq \LEN(\UNCOMPRESSED)$}{ + \Return \UNCOMPRESSED\; + } + \Else{ + \Return \COMPRESSED\; + } +}{ + \Return \UNCOMPRESSED\; +} +\EALGORITHM +} + +\subsection{Encoding Uncompressed Frame} +\label{alac:write_uncompressed_frame} +{\relsize{-1} +\ALGORITHM{1 or 2 channels of PCM data, encoding parameters}{an uncompressed ALAC frame} +\SetKwData{PCMCOUNT}{PCM frame count} +\SetKwData{CHANCOUNT}{channel count} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{BPS}{bits per sample} +\SetKwData{CHANNEL}{channel} +$0 \rightarrow$ \WRITE 16 unsigned bits\tcc*[r]{unused} +\eIf{$\text{\PCMCOUNT} = \text{encoding parameter's \BLOCKSIZE}$}{ + $0 \rightarrow$ \WRITE 1 unsigned bit\; +}{ + $1 \rightarrow$ \WRITE 1 unsigned bit\; +} +$0 \rightarrow$ \WRITE 2 unsigned bits\tcc*[r]{uncompressed LSBs} +$1 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{not compressed} +\If{$\text{\PCMCOUNT} \neq \text{encoding parameter's \BLOCKSIZE}$}{ + $\PCMCOUNT \rightarrow$ \WRITE 32 unsigned bits\; +} +\For{$i \leftarrow 0$ \emph{\KwTo}\PCMCOUNT}{ + \For{$c \leftarrow 0$ \emph{\KwTo}\CHANCOUNT}{ + $\text{\CHANNEL}_{c~i} \rightarrow$ \WRITE (\BPS) signed bits\; + } +} +\Return uncompressed frame\; +\EALGORITHM +} + +\begin{figure}[h] + \includegraphics{alac/figures/uncompressed_frame.pdf} +\end{figure} + +\clearpage + +\subsection{Encoding Compressed Frame} +\label{alac:write_compressed_frame} +{\relsize{-1} +\ALGORITHM{1 or 2 channels of PCM data, encoding parameters}{a compressed ALAC frame, or a \textit{residual overflow} exception} +\SetKwData{PCMCOUNT}{PCM frame count} +\SetKwData{CHANCOUNT}{channel count} +\SetKwData{CHANNEL}{channel} +\SetKwData{SHIFTED}{shifted} +\SetKwData{FRAME}{frame} +\SetKwData{BPS}{bits per sample} +\SetKwData{HASLSBS}{uncompressed LSBs} +\SetKwData{ISHIFT}{interlacing shift} +\SetKwData{MINWEIGHT}{minimum leftweight} +\SetKwData{MAXWEIGHT}{maximum leftweight} +\SetKwData{LSB}{LSB} +\eIf{$\text{\BPS} \leq 16$}{ + \HASLSBS $\leftarrow 0$\; + \LSB $\leftarrow$ \texttt{[]}\; + \For{$c \leftarrow 0$ \emph{\KwTo}\CHANCOUNT}{ + $\text{\SHIFTED}_c \leftarrow \text{\CHANNEL}_c$\; + } +}(\tcc*[f]{extract uncompressed LSBs}){ + \HASLSBS $\leftarrow (\text{\BPS} - 16) \div 8$\; + \For{$i \leftarrow 0$ \emph{\KwTo}\PCMCOUNT}{ + \For{$c \leftarrow 0$ \emph{\KwTo}\CHANCOUNT}{ + $\text{\LSB}_{(i \times \CHANCOUNT) + c} \leftarrow (\text{\CHANNEL}_{c~i}) \bmod~2^{\text{\BPS} - 16}$\; + $\text{\SHIFTED}_{c~i} \leftarrow \left\lfloor(\text{\CHANNEL}_{c~i}) \div 2^{\text{\BPS} - 16}\right\rfloor$\; + } + } +} +\eIf{$\text{\CHANCOUNT} = 1$}{ + \Return \hyperref[alac:write_non_interlaced_frame]{non-interlaced frame} + $\left\lbrace\begin{tabular}{l} + $\text{\SHIFTED}_0$ \\ + \HASLSBS \\ + \LSB \\ + \end{tabular}\right.$\; +}{ + \tcc{minimum/maximum leftweight, interlacing shift from encoding parameters} + \For{l $\leftarrow \MINWEIGHT$ \emph{\KwTo}$\text{\MAXWEIGHT} + 1$}{ + $\text{\FRAME}_l \leftarrow$ + \hyperref[alac:write_interlaced_frame]{interlaced frame} + $\left\lbrace\begin{tabular}{l} + $\text{\SHIFTED}_0$ \\ + $\text{\SHIFTED}_1$ \\ + \ISHIFT \\ + leftweight $l$ \\ + \HASLSBS \\ + \LSB \\ + \end{tabular}\right.$\; + } + \Return smallest $\text{\FRAME}_l$\; +} +\EALGORITHM +} + +\clearpage + +\subsection{Encoding Non-Interlaced Frame} +\label{alac:write_non_interlaced_frame} +{\relsize{-1} +\ALGORITHM{1 channel of PCM data, uncompressed LSBs, encoding parameters}{a compressed ALAC frame, or a \textit{residual overflow} exception} +\SetKwData{PCMCOUNT}{PCM frame count} +\SetKwData{CHANCOUNT}{channel count} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{BPS}{bits per sample} +\SetKwData{SAMPLESIZE}{sample size} +\SetKwData{QLPCOEFF}{QLP coefficient} +\SetKwData{RESIDUAL}{residual} +\SetKwData{CHANNEL}{channel} +\SetKwData{UNCOMPRESSEDLSB}{uncompressed LSBs} +\SetKwData{LSB}{LSB} +$0 \rightarrow$ \WRITE 16 unsigned bits\tcc*[r]{unused} +\eIf{$\text{\PCMCOUNT} \neq \text{encoding parameter's \BLOCKSIZE}$}{ + $1 \rightarrow$ \WRITE 1 unsigned bit\; +}{ + $0 \rightarrow$ \WRITE 1 unsigned bit\; +} +$\text{uncompressed LSBs} \rightarrow$ \WRITE 2 unsigned bits\; +$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{is compressed} +\If{$\text{\PCMCOUNT} \neq \text{encoding parameter's \BLOCKSIZE}$}{ + $\PCMCOUNT \rightarrow$ \WRITE 32 unsigned bits\; +} +$0 \rightarrow$ \WRITE 8 unsigned bits\tcc*[r]{interlacing shift} +$0 \rightarrow$ \WRITE 8 unsigned bits\tcc*[r]{interlacing leftweight} +\SAMPLESIZE $\leftarrow \text{\BPS} - (\text{uncompressed LSBs} \times 8)$\; +$\left.\begin{tabular}{r} +$\text{\QLPCOEFF}_0$ \\ +$\text{\RESIDUAL}_0$ \\ +\end{tabular}\right\rbrace \leftarrow$ +\hyperref[alac:compute_qlp_coeffs]{compute QLP coefficient} +$\left\lbrace\begin{tabular}{l} +$\text{\CHANNEL}_0$ \\ +\SAMPLESIZE \\ +\end{tabular}\right.$\; +\hyperref[alac:write_subframe_header]{write subframe header with $\text{\QLPCOEFF}_0$}\; +\If{$\text{\UNCOMPRESSEDLSB} > 0$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}\PCMCOUNT}{ + $\text{\LSB}_i \rightarrow$ \WRITE $(\text{\UNCOMPRESSEDLSB} \times 8)$ unsigned bits\; + } +} +\hyperref[alac:write_residuals]{write residual block $\text{\RESIDUAL}_0$}\; +\BlankLine +\Return non-interlaced ALAC frame\; +\EALGORITHM +} + +\begin{figure}[h] + \includegraphics{alac/figures/noninterlaced_frame.pdf} +\end{figure} + +\clearpage + +\subsection{Encoding Interlaced Frame} +\label{alac:write_interlaced_frame} +{\relsize{-1} +\ALGORITHM{2 channels of PCM data, interlacing shift, interlacing leftweight, uncompressed LSBs, encoding parameters}{a compressed ALAC frame, or a \textit{residual overflow} exception} +\SetKwData{PCMCOUNT}{PCM frame count} +\SetKwData{CHANCOUNT}{channel count} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{BPS}{bits per sample} +\SetKwData{SAMPLESIZE}{sample size} +\SetKwData{UNCOMPRESSEDLSB}{uncompressed LSBs} +\SetKwData{INTERLACINGSHIFT}{interlacing shift} +\SetKwData{INTERLACINGLEFTWEIGHT}{interlacing leftweight} +\SetKwData{CHANNEL}{channel} +\SetKwData{CORRELATED}{correlated} +\SetKwData{QLPCOEFF}{QLP coefficient} +\SetKwData{RESIDUAL}{residual} +$0 \rightarrow$ \WRITE 16 unsigned bits\tcc*[r]{unused} +\eIf{$\text{\PCMCOUNT} \neq \text{encoding parameter's \BLOCKSIZE}$}{ + $1 \rightarrow$ \WRITE 1 unsigned bit\; +}{ + $0 \rightarrow$ \WRITE 1 unsigned bit\; +} +$\text{\UNCOMPRESSEDLSB} \rightarrow$ \WRITE 2 unsigned bits\; +$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{is compressed} +\If{$\text{\PCMCOUNT} \neq \text{encoding parameter's \BLOCKSIZE}$}{ + $\text{\PCMCOUNT} \rightarrow$ \WRITE 32 unsigned bits\; +} +$\text{\INTERLACINGSHIFT} \rightarrow$ \WRITE 8 unsigned bits\; +$\text{\INTERLACINGLEFTWEIGHT} \rightarrow$ \WRITE 8 unsigned bits\; +\BlankLine +$\left.\begin{tabular}{r} +$\text{\CORRELATED}_0$ \\ +$\text{\CORRELATED}_1$ \\ +\end{tabular}\right\rbrace \leftarrow$ +\hyperref[alac:correlate_channels]{correlate channels} +$\left\lbrace\begin{tabular}{l} +$\text{\CHANNEL}_0$ \\ +$\text{\CHANNEL}_1$ \\ +\INTERLACINGSHIFT \\ +\INTERLACINGLEFTWEIGHT \\ +\end{tabular}\right.$\; +\BlankLine +\SAMPLESIZE $\leftarrow \text{\BPS} - (\text{uncompressed LSBs} \times 8) + 1$\; +$\left.\begin{tabular}{r} +$\text{\QLPCOEFF}_0$ \\ +$\text{\RESIDUAL}_0$ \\ +\end{tabular}\right\rbrace \leftarrow$ +\hyperref[alac:compute_qlp_coeffs]{compute QLP coefficient} +$\left\lbrace\begin{tabular}{l} +$\text{\CORRELATED}_0$ \\ +\SAMPLESIZE \\ +\end{tabular}\right.$\; +$\left.\begin{tabular}{r} +$\text{\QLPCOEFF}_1$ \\ +$\text{\RESIDUAL}_1$ \\ +\end{tabular}\right\rbrace \leftarrow$ +\hyperref[alac:compute_qlp_coeffs]{compute QLP coefficient} +$\left\lbrace\begin{tabular}{l} +$\text{\CORRELATED}_1$ \\ +\SAMPLESIZE \\ +\end{tabular}\right.$\; +\hyperref[alac:write_subframe_header]{write subframe header with $\text{\QLPCOEFF}_0$}\; +\hyperref[alac:write_subframe_header]{write subframe header with $\text{\QLPCOEFF}_1$}\; +\BlankLine +\If{$\text{\UNCOMPRESSEDLSB} > 0$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}\PCMCOUNT}{ + $\text{LSB}_i \rightarrow$ \WRITE $(\text{\UNCOMPRESSEDLSB} \times 8)$ unsigned bits\; + } +} +\BlankLine +\hyperref[alac:write_residuals]{write residual block $\text{\RESIDUAL}_0$}\; +\hyperref[alac:write_residuals]{write residual block $\text{\RESIDUAL}_1$}\; +\BlankLine +\Return interlaced ALAC frame\; +\EALGORITHM +} + +\clearpage + +\begin{figure}[h] + \includegraphics{alac/figures/interlaced_frame.pdf} +\end{figure} + +\subsubsection{Correlating Channels} +\label{alac:correlate_channels} +{\relsize{-1} +\ALGORITHM{2 channels of PCM data, interlacing shift, interlacing leftweight}{2 correlated channels of PCM data} +\SetKwData{PCMCOUNT}{PCM frame count} +\SetKwData{LEFTWEIGHT}{interlacing leftweight} +\SetKwData{SHIFT}{interlacing shift} +\SetKwData{CORRELATED}{correlated} +\SetKwData{CHANNEL}{channel} +\eIf{$\text{\LEFTWEIGHT} > 0$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}\PCMCOUNT}{ + $\text{\CORRELATED}_{0~i} \leftarrow \text{\CHANNEL}_{1~i} + \left\lfloor\frac{(\text{\CHANNEL}_{0~i} - \text{\CHANNEL}_{1~i}) \times \text{\LEFTWEIGHT}}{2 ^ \text{\SHIFT}}\right\rfloor$\; + $\text{\CORRELATED}_{1~i} \leftarrow \text{\CHANNEL}_{0~i} - \text{\CHANNEL}_{1~i}$\; + } +}{ + \For{$i \leftarrow 0$ \emph{\KwTo}\PCMCOUNT}{ + $\text{\CORRELATED}_{0~i} \leftarrow \text{\CHANNEL}_{0~i}$\; + $\text{\CORRELATED}_{1~i} \leftarrow \text{\CHANNEL}_{1~i}$\; + } +} +\Return $\left\lbrace\begin{tabular}{l} +$\text{\CORRELATED}_0$ \\ +$\text{\CORRELATED}_1$ \\ +\end{tabular}\right.$ +\EALGORITHM +\par +\noindent +For example, given an \VAR{interlacing shift} value of 2 and an +\VAR{interlacing leftweight} value of 3: +\par +\noindent +{\relsize{-1} +\begin{tabular}{r||r|r||>{$}r<{$}|>{$}r<{$}|} +$i$ & $\textsf{channel}_{0~i}$ & $\textsf{channel}_{1~i}$ & \textsf{correlated}_{0~i} & \textsf{correlated}_{1~i} \\ +\hline +0 & 18 & 2 & 2 + \lfloor((18 - 2) \times 3) \div 2 ^ 2\rfloor = 14 & 18 - 2 = 16 \\ +1 & 20 & 3 & 3 + \lfloor((20 - 3) \times 3) \div 2 ^ 2\rfloor = 15 & 20 - 3 = 17 \\ +2 & 26 & 0 & 0 + \lfloor((26 - 0) \times 3) \div 2 ^ 2\rfloor = 19 & 26 - 0 = 26 \\ +3 & 24 & -1 & -1 + \lfloor((24 + 1) \times 3) \div 2 ^ 2\rfloor = 17 & 24 + 1 = 25 \\ +4 & 24 & 0 & 0 + \lfloor((24 - 0) \times 3) \div 2 ^ 2\rfloor = 18 & 24 - 0 = 24 \\ +\end{tabular} +} +} + +\clearpage + +\input{alac/encode/lpc} + +\clearpage + +\input{alac/encode/residual} + +\subsection{Writing Subframe Header} +\label{alac:write_subframe_header} +\ALGORITHM{4 or 8 signed QLP coefficients}{a subframe header} +\SetKwData{COEFFCOUNT}{coefficient count} +\SetKwData{COEFF}{QLP coefficient} +$0 \rightarrow$ \WRITE 4 unsigned bits\tcc*[r]{prediction type} +$9 \rightarrow$ \WRITE 4 unsigned bits\tcc*[r]{QLP shift needed} +$4 \rightarrow$ \WRITE 3 unsigned bits\tcc*[r]{Rice modifier} +$\text{\COEFFCOUNT} \rightarrow$ \WRITE 5 unsigned bits\; +\For{$i \leftarrow 0$ \emph{\KwTo}\COEFFCOUNT}{ + $\text{\COEFF}_i \rightarrow$ \WRITE 16 signed bits\; +} +\Return subframe header data\; +\EALGORITHM +\begin{figure}[h] +\includegraphics{alac/figures/subframe_header.pdf} +\end{figure} +\par +\noindent +For example, given the QLP coefficients +\texttt{1170, -1088, 565, -161}, +the subframe header is written as: +\begin{figure}[h] +\includegraphics{alac/figures/subframe-build.pdf} +\end{figure}
View file
audiotools-2.19.tar.gz/docs/reference/alac/encode/atoms.tex
Added
@@ -0,0 +1,593 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\subsection{ALAC Atoms} +\begin{wrapfigure}[6]{r}{1.5in} +\includegraphics{alac/figures/atoms.pdf} +\end{wrapfigure} +We'll encode our ALAC file in iTunes order, which means +it contains the \ATOM{ftyp}, \ATOM{moov}, \ATOM{free} and +\ATOM{mdat} atoms, in that order. + +\subsubsection{the ftyp Atom} + +\begin{table}[h] +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & 32 \\ +atom type & 32 & `ftyp' (\texttt{0x66747970}) \\ +\hline +major brand & 32 & `M4A ' (\texttt{0x4d344120}) \\ +major brand version & 32 & \texttt{0} \\ +compatible brand & 32 & `M4A ' (\texttt{0x4d344120}) \\ +compatible brand & 32 & `mp42' (\texttt{0x6d703432}) \\ +compatible brand & 32 & `isom' (\texttt{0x69736f6d}) \\ +compatible brand & 32 & \texttt{0x00000000} \\ +\hline +\end{tabular} +\end{table} + +\subsubsection{the moov Atom} + +\begin{table}[h] +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & \ATOM{mvhd} size + \ATOM{trak} size + \ATOM{udta} size + 8 \\ +atom type & 32 & `moov' (\texttt{0x6d6f6f76}) \\ +\hline +\ATOM{mvhd} atom & \ATOM{mvhd} size & \ATOM{mvhd} data \\ +\ATOM{trak} atom & \ATOM{trak} size & \ATOM{trak} data \\ +\ATOM{udta} atom & \ATOM{udta} size & \ATOM{udta} data \\ +\hline +\end{tabular} +\end{table} + +\clearpage + +\subsubsection{the mvhd Atom} + +\begin{table}[h] +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & 108/120 \\ +atom type & 32 & `mvhd' (\texttt{0x6d766864}) \\ +\hline +version & 8 & \texttt{0x00} \\ +flags & 24 & \texttt{0x000000} \\ +created date & 32/64 & creation date as Mac UTC \\ +modified date & 32/64 & modification date as Mac UTC \\ +time scale & 32 & sample rate \\ +duration & 32/64 & total PCM frames \\ +playback speed & 32 & \texttt{0x10000} \\ +user volume & 16 & \texttt{0x100} \\ +padding & 80 & \texttt{0x00000000000000000000} \\ +window geometry matrix a & 32 & \texttt{0x10000} \\ +window geometry matrix b & 32 & \texttt{0} \\ +window geometry matrix u & 32 & \texttt{0} \\ +window geometry matrix c & 32 & \texttt{0} \\ +window geometry matrix d & 32 & \texttt{0x10000} \\ +window geometry matrix v & 32 & \texttt{0} \\ +window geometry matrix x & 32 & \texttt{0} \\ +window geometry matrix y & 32 & \texttt{0} \\ +window geometry matrix w & 32 & \texttt{0x40000000} \\ +QuickTime preview & 64 & \texttt{0} \\ +QuickTime still poster & 32 & \texttt{0} \\ +QuickTime selection time & 64 & \texttt{0} \\ +QuickTime current time & 32 & \texttt{0} \\ +next track ID & 32 & \texttt{2} \\ +\hline +\end{tabular} +\end{table} + +If \VAR{version} is 0, \VAR{created date}, \VAR{modified date} and +\VAR{duration} are 32 bit fields. +Otherwise, they are 64 bit fields. +The \VAR{created date} and \VAR{modified date} are seconds +since the Macintosh Epoch, which is 00:00:00, January 1st, 1904.\footnote{Why 1904? It's the first leap year of the 20th century.} +To convert a Unix Epoch timestamp (seconds since January 1st, 1970) to +a Macintosh Epoch, one needs to add 24,107 days - +or \texttt{2082844800} seconds. + +\clearpage + +\subsubsection{the trak Atom} +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & \ATOM{tkhd} size + \ATOM{mdia} size + 8 \\ +atom type & 32 & `trak' (\texttt{0x7472616b}) \\ +\hline +\ATOM{tkhd} atom & \ATOM{tkhd} size & \ATOM{tkhd} data \\ +\ATOM{mdia} atom & \ATOM{mdia} size & \ATOM{mdia} data \\ +\hline +\end{tabular} + +\subsubsection{the tkhd Atom} + +\begin{table}[h] +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & 92/104 \\ +atom type & 32 & `tkhd' (\texttt{0x746b6864}) \\ +\hline +version & 8 & \texttt{0x00} \\ +padding & 20 & \texttt{0x000000} \\ +track in poster & 1 & \texttt{0} \\ +track in preview & 1 & \texttt{1} \\ +track in movie & 1 & \texttt{1} \\ +track enabled & 1 & \texttt{1} \\ +created date & 32/64 & creation date as Mac UTC \\ +modified date & 32/64 & modification date as Mac UTC \\ +track ID & 32 & \texttt{1} \\ +padding & 32 & \texttt{0x00000000} \\ +duration & 32/64 & total PCM frames \\ +padding & 64 & \texttt{0x0000000000000000} \\ +video layer & 16 & \texttt{0} \\ +QuickTime alternate & 16 & \texttt{0} \\ +volume & 16 & \texttt{0x1000} \\ +padding & 16 & \texttt{0x0000} \\ +video geometry matrix a & 32 & \texttt{0x10000} \\ +video geometry matrix b & 32 & \texttt{0} \\ +video geometry matrix u & 32 & \texttt{0} \\ +video geometry matrix c & 32 & \texttt{0} \\ +video geometry matrix d & 32 & \texttt{0x10000} \\ +video geometry matrix v & 32 & \texttt{0} \\ +video geometry matrix x & 32 & \texttt{0} \\ +video geometry matrix y & 32 & \texttt{0} \\ +video geometry matrix w & 32 & \texttt{0x40000000} \\ +video width & 32 & \texttt{0} \\ +video height & 32 & \texttt{0} \\ +\hline +\end{tabular} +\end{table} + +\clearpage + +\subsubsection{the mdia Atom} + +\begin{table}[h] +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & \ATOM{mdhd} size + \ATOM{hdlr} size + \ATOM{minf} size + 8 \\ +atom type & 32 & `mdia' (\texttt{0x6d646961}) \\ +\hline +\ATOM{mdhd} atom & \ATOM{mdhd} size & \ATOM{mdhd} data \\ +\ATOM{hdlr} atom & \ATOM{hdlr} size & \ATOM{hdlr} data \\ +\ATOM{minf} atom & \ATOM{minf} size & \ATOM{minf} data \\ +\hline +\end{tabular} +\end{table} + +\subsubsection{the mdhd Atom} + +\begin{table}[h] +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & 32/44 \\ +atom type & 32 & `mdhd' (\texttt{0x6d646864}) \\ +\hline +version & 8 & \texttt{0x00} \\ +flags & 24 & \texttt{0x000000} \\ +created date & 32/64 & creation date as Mac UTC \\ +modified date & 32/64 & modification date as Mac UTC \\ +time scale & 32 & sample rate \\ +duration & 32/64 & total PCM frames \\ +padding & 1 & \texttt{0} \\ +language & 5 & \\ +language & 5 & language value as ISO 639-2 \\ +language & 5 & \\ +QuickTime quality & 16 & \texttt{0} \\ +\hline +\end{tabular} +\end{table} +Note the three, 5-bit \VAR{language} fields. +By adding 0x60 to each value and converting the result to ASCII characters, +the result is an \href{http://www.loc.gov/standards/iso639-2/}{ISO 639-2} +string of the file's language representation. +For example, given the values \texttt{0x15}, \texttt{0x0E} and \texttt{0x04}: +\begin{align*} +\text{language}_0 &= \texttt{0x15} + \texttt{0x60} = \texttt{0x75} = \texttt{u} \\ +\text{language}_1 &= \texttt{0x0E} + \texttt{0x60} = \texttt{0x6E} = \texttt{n} \\ +\text{language}_2 &= \texttt{0x04} + \texttt{0x60} = \texttt{0x64} = \texttt{d} +\end{align*} +Which is the code `\texttt{und}', meaning `undetermined' - which is typical. + +\clearpage + +\subsubsection{the hdlr Atom} +\label{alac_hdlr} +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & 33 + component \\ +atom type & 32 & `hdlr' (\texttt{0x68646c72}) \\ +\hline +version & 8 & \texttt{0x00} \\ +flags & 24 & \texttt{0x000000} \\ +QuickTime type & 32 & \texttt{0x00000000} \\ +QuickTime subtype & 32 & `soun' (\texttt{0x736f756e}) \\ +QuickTime manufacturer & 32 & \texttt{0x00000000} \\ +QuickTime component reserved flags & 32 & \texttt{0x00000000} \\ +QuickTime component reserved flags mask & 32 & \texttt{0x00000000} \\ +component name length & 8 & \texttt{0x00} \\ +component name & component name length $\times$ 8 & \\ +\hline +\end{tabular} + + +\subsubsection{the minf Atom} +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & \ATOM{smhd} size + \ATOM{dinf} size + \ATOM{stbl} size + 8 \\ +atom type & 32 & `minf' (\texttt{0x6d696e66}) \\ +\hline +\ATOM{smhd} atom & \ATOM{smhd} size & \ATOM{smhd} data \\ +\ATOM{dinf} atom & \ATOM{dinf} size & \ATOM{dinf} data \\ +\ATOM{stbl} atom & \ATOM{stbl} size & \ATOM{stbl} data \\ +\hline +\end{tabular} + +\subsubsection{the smhd Atom} +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & 16 \\ +atom type & 32 & `smhd' (\texttt{0x736d6864}) \\ +\hline +version & 8 & \texttt{0x00} \\ +flags & 24 & \texttt{0x000000} \\ +audio balance & 16 & \texttt{0x0000} \\ +padding & 16 & \texttt{0x0000} \\ +\hline +\end{tabular} + +\subsubsection{the dinf Atom} +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & \ATOM{dref} size + 8 \\ +atom type & 32 & `dinf' (\texttt{0x64696e66}) \\ +\hline +\ATOM{dref} atom & \ATOM{dref} size & \ATOM{dref} data \\ +\hline +\end{tabular} + +\clearpage + +\subsubsection{the dref Atom} + +\begin{table}[h] +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & 28 \\ +atom type & 32 & `dref' (\texttt{0x64726566}) \\ +\hline +version & 8 & \texttt{0x00} \\ +flags & 24 & \texttt{0x000000} \\ +number of references & 32 & \texttt{1} \\ +\hline +\hline +reference atom size & 32 & \texttt{12} \\ +reference atom type & 32 & `url ' (\texttt{0x75726c20}) \\ +reference atom data & 32 & \texttt{0x00000001} \\ +\hline +\end{tabular} +\end{table} + +\subsubsection{the stbl Atom} + +\begin{table}[h] +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & \ATOM{stsd} size + \ATOM{stts} size + \ATOM{stsc} size + \\ +& & \ATOM{stsz} size + \ATOM{stco} size + 8 \\ +atom type & 32 & `stbl' (\texttt{0x7374626c}) \\ +\hline +\ATOM{stsd} atom & \ATOM{stsd} size & \ATOM{stsd} data \\ +\ATOM{stts} atom & \ATOM{stts} size & \ATOM{stts} data \\ +\ATOM{stsc} atom & \ATOM{stsc} size & \ATOM{stsc} data \\ +\ATOM{stsz} atom & \ATOM{stsz} size & \ATOM{stsz} data \\ +\ATOM{stco} atom & \ATOM{stco} size & \ATOM{stco} data \\ +\hline +\end{tabular} +\end{table} + +\subsubsection{the stsd Atom} + +\begin{table}[h] +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & \ATOM{alac} size + 16 \\ +atom type & 32 & `stsd' (\texttt{0x73747364}) \\ +\hline +version & 8 & \texttt{0x00} \\ +flags & 24 & \texttt{0x000000} \\ +number of descriptions & 32 & \texttt{1} \\ +\hline +\ATOM{alac} atom & \ATOM{alac} size & \ATOM{alac} data \\ +\hline +\end{tabular} +\end{table} + +\clearpage + +\subsubsection{the alac Atom} + +\begin{table}[h] +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & 72 \\ +atom type & 32 & `alac' (\texttt{0x616c6163}) \\ +\hline +reserved & 48 & \texttt{0x000000000000} \\ +reference index & 16 & \texttt{1} \\ +version & 16 & \texttt{0} \\ +revision level & 16 & \texttt{0} \\ +vendor & 32 & \texttt{0x00000000} \\ +channels & 16 & channel count \\ +bits per sample & 16 & bits per sample \\ +compression ID & 16 & \texttt{0} \\ +audio packet size & 16 & \texttt{0} \\ +sample rate & 32 & \texttt{0xAC440000} \\ +\hline +\hline +atom length & 32 & 36 \\ +atom type & 32 & `alac' (\texttt{0x616c6163}) \\ +\hline +padding & 32 & \texttt{0x00000000} \\ +max samples per frame & 32 & largest number of PCM frames per ALAC frame \\ +padding & 8 & \texttt{0x00} \\ +sample size & 8 & bits per sample \\ +history multiplier & 8 & \texttt{40} \\ +initial history & 8 & \texttt{10} \\ +maximum K & 8 & \texttt{14} \\ +channels & 8 & channel count \\ +unknown & 16 & \texttt{0x00FF} \\ +max coded frame size & 32 & largest ALAC frame size, in bytes \\ +bitrate & 32 & $((\text{\ATOM{mdat} size} \times 8 ) \div (\text{total PCM frames} \div \text{sample rate}))$ \\ +sample rate & 32 & sample rate \\ +\hline +\end{tabular} +\end{table} +The \VAR{history multiplier}, \VAR{initial history} and \VAR{maximum K} +values are encode-time options, typically set to 40, 10 and 14, +respectively. + +Note that the \VAR{bitrate} field can't be known in advance; +we must fill that value with 0 for now and then +return to this atom once encoding is completed +and its size has been determined. + +\clearpage + +\subsubsection{the stts Atom} + +\begin{table}[h] +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & number of times $\times$ 8 + 16\\ +atom type & 32 & `stts' (\texttt{0x73747473}) \\ +\hline +version & 8 & \texttt{0x00} \\ +flags & 24 & \texttt{0x000000} \\ +number of times & 32 & \\ +\hline +frame count 1 & 32 & number of occurrences \\ +frame duration 1 & 32 & PCM frame count \\ +\hline +\multicolumn{3}{|c|}{...} \\ +\hline +\end{tabular} +\end{table} +This atom keeps track of how many different sizes of ALAC frames +occur in the ALAC file, in PCM frames. +It will typically have only two ``times'', the block size we're +using for most of our samples and the final block size for +any remaining samples. + +For example, let's imagine encoding a 1 minute audio file +at 44100Hz with a block size of 4096 frames. +This file has a total of 2,646,000 PCM frames ($60 \times 44100 = 2646000$). +2,646,000 PCM frames divided by a 4096 block size means +we have 645 ALAC frames of size 4096, and 1 ALAC frame of size 4080. + +Therefore: +\begin{align*} +\text{number of times} &= 2 \\ +\text{frame count}_1 &= 645 \\ +\text{frame duration}_1 &= 4096 \\ +\text{frame count}_2 &= 1 \\ +\text{frame duration}_2 &= 4080 +\end{align*} + +\subsubsection{the stsc Atom} + +\begin{table}[h] +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & entries $\times$ 12 + 16 \\ +atom type & 32 & `stsc' (\texttt{0x73747363}) \\ +\hline +version & 8 & \texttt{0x00} \\ +flags & 24 & \texttt{0x000000} \\ +number of entries & 32 & \\ +\hline +first chunk & 32 & \\ +ALAC frames per chunk & 32 & \\ +description index & 32 & \texttt{1} \\ +\hline +\multicolumn{3}{|c|}{...} \\ +\hline +\end{tabular} +\end{table} + +This atom stores how many ALAC frames are in a given ``chunk''. +In this instance a ``chunk'' represents an entry in +the \ATOM{stco} atom table, used for seeking backwards and forwards +through the file. +\VAR{First chunk} is the starting offset of its frames-per-chunk +value, beginning at 1. + +As an example, let's take a one minute, 44100Hz audio file +that's been broken into 130 chunks +(each with an entry in the \ATOM{stco} atom). +Its \ATOM{stsc} entries would typically be: +\begin{align*} +\text{first chunk}_1 &= 1 \\ +\text{frames per chunk}_1 &= 5 \\ +\text{first chunk}_2 &= 130 \\ +\text{frames per chunk}_2 &= 1 +\end{align*} +What this means is that chunks 1 through 129 have 5 ALAC frames each +while chunk 130 has 1 ALAC frame. +This is a total of 646 ALAC frames, which matches the contents of +the \ATOM{stts} atom. + +\subsubsection{the stsz Atom} + +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & sizes $\times$ 4 + 20 \\ +atom type & 32 & `stsz' (\texttt{0x7374737a}) \\ +\hline +version & 8 & \texttt{0x00} \\ +flags & 24 & \texttt{0x000000} \\ +block byte size & 32 & \texttt{0x00000000} \\ +number of sizes & 32 & \\ +\hline +frame size & 32 & \\ +\hline +\multicolumn{3}{|c|}{...} \\ +\hline +\end{tabular} + +This atom is a list of ALAC frame sizes, each in bytes. +For example, our 646 frame file would have 646 corresponding +\ATOM{stsz} entries. + +\subsubsection{the stco Atom} + +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & offset $\times$ 4 + 16 \\ +atom type & 32 & `stco' (\texttt{0x7374636f}) \\ +\hline +version & 8 & \texttt{0x00} \\ +flags & 24 & \texttt{0x000000} \\ +number of offsets & 32 & \\ +\hline +frame offset & 32 & \\ +\hline +\multicolumn{3}{|c|}{...} \\ +\hline +\end{tabular} + +This atom is a list of absolute file offsets for each chunk, where +each chunk is typically 5 ALAC frames large. + +\clearpage + +\subsubsection{the udta Atom} + +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & \ATOM{meta} size + 8 \\ +atom type & 32 & `udta' (\texttt{0x75647461}) \\ +\hline +\ATOM{meta} atom & \ATOM{meta} size & \ATOM{meta} data \\ +\hline +\end{tabular} + +\subsubsection{the meta Atom} + +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & \ATOM{hdlr} size + \ATOM{ilst} size + \ATOM{free} size + 12 \\ +atom type & 32 & `meta' (\texttt{0x6d657461}) \\ +\hline +version & 8 & \texttt{0x00} \\ +flags & 24 & \texttt{0x000000} \\ +\hline +\ATOM{hdlr} atom & \ATOM{hdlr} size & \ATOM{hdlr} data \\ +\ATOM{ilst} atom & \ATOM{ilst} size & \ATOM{ilst} data \\ +\ATOM{free} atom & \ATOM{free} size & \ATOM{free} data \\ +\hline +\end{tabular} + +\subsubsection{the hdlr atom (revisited)} + +\begin{tabular}{|l|r|l|} +\hline +Field & Size & Value \\ +\hline +atom length & 32 & 34 \\ +atom type & 32 & `hdlr' (\texttt{0x68646c72}) \\ +\hline +version & 8 & \texttt{0x00} \\ +flags & 24 & \texttt{0x000000} \\ +QuickTime type & 32 & \texttt{0x00000000} \\ +QuickTime subtype & 32 & `mdir' (\texttt{0x6d646972}) \\ +QuickTime manufacturer & 32 & `appl' (\texttt{0x6170706c}) \\ +QuickTime component reserved flags & 32 & \texttt{0x00000000} \\ +QuickTime component reserved flags mask & 32 & \texttt{0x00000000} \\ +component name length & 8 & \texttt{0x00} \\ +component name & 0 & \\ +\hline +\end{tabular} + +This atom is laid out identically to the ALAC file's primary +\ATOM{hdlr} atom (described on page \pageref{alac_hdlr}). +The only difference is the contents of its fields. + +\subsubsection{the ilst Atom} + +This atom is a collection of \ATOM{data} sub-atoms +and is described on page \pageref{m4a_meta}. + +\subsubsection{the free Atom} + +These atoms are simple collection of NULL bytes which can easily be +resized to make room for other atoms without rewriting the entire file.
View file
audiotools-2.19.tar.gz/docs/reference/alac/encode/lpc.tex
Added
@@ -0,0 +1,361 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\subsection{Computing QLP Coefficients and Residual} +\label{alac:compute_qlp_coeffs} +{\relsize{-2} +\ALGORITHM{a list of signed PCM samples, sample size, encoding parameters}{a list of 4 or 8 signed QLP coefficients, a block of residual data; or a \textit{residual overflow} exception} +\SetKwData{SAMPLES}{subframe samples} +\SetKwData{WINDOWED}{windowed} +\SetKwData{AUTOCORRELATION}{autocorrelated} +\SetKwData{LPCOEFF}{LP coefficient} +\SetKwData{QLPCOEFF}{QLP coefficient} +\SetKwData{SAMPLESIZE}{sample size} +\SetKwData{RESIDUAL}{residual} +\SetKwData{RESIDUALBLOCK}{residual block} +$\WINDOWED \leftarrow$ \hyperref[alac:window]{window signed integer \SAMPLES}\; +$\AUTOCORRELATION \leftarrow$ \hyperref[alac:autocorrelate]{autocorrelate \WINDOWED}\; +\eIf{$\text{\AUTOCORRELATION}_0 \neq 0.0$}{ + $\LPCOEFF \leftarrow$ \hyperref[alac:compute_lp_coeffs]{compute LP coefficients from \AUTOCORRELATION}\; + $\text{\QLPCOEFF}_3 \leftarrow$ \hyperref[alac:quantize_lp_coeffs]{quantize $\text{\LPCOEFF}_3$ at order 4}\; + $\text{\QLPCOEFF}_7 \leftarrow$ \hyperref[alac:quantize_lp_coeffs]{quantize $\text{\LPCOEFF}_7$ at order 8}\; + $\text{\RESIDUAL}_3 \leftarrow$ \hyperref[alac:calculate_residuals]{calculate residuals from $\text{\QLPCOEFF}_3$ and \SAMPLES}\; + $\text{\RESIDUAL}_7 \leftarrow$ \hyperref[alac:calculate_residuals]{calculate residuals from $\text{\QLPCOEFF}_7$ and \SAMPLES}\; + $\text{\RESIDUALBLOCK}_3 \leftarrow$ \hyperref[alac:write_residuals]{encode residual block from $\text{\RESIDUAL}_3$ with \SAMPLESIZE}\; + $\text{\RESIDUALBLOCK}_7 \leftarrow$ \hyperref[alac:write_residuals]{encode residual block from $\text{\RESIDUAL}_7$ with \SAMPLESIZE}\; + \eIf{$\LEN(\text{\RESIDUALBLOCK}_3) < (\LEN(\text{\RESIDUALBLOCK}_7) + 64~bits)$}{ + \Return $\left\lbrace\begin{tabular}{l} + $\text{\QLPCOEFF}_3$ \\ + $\text{\RESIDUALBLOCK}_3$ \\ + \end{tabular}\right.$\; + }{ + \Return $\left\lbrace\begin{tabular}{l} + $\text{\QLPCOEFF}_7$ \\ + $\text{\RESIDUALBLOCK}_7$ \\ + \end{tabular}\right.$\; + } +}(\tcc*[f]{all samples are 0}){ + \QLPCOEFF $\leftarrow$ \texttt{[0, 0, 0, 0]}\; + $\text{\RESIDUAL} \leftarrow$ \hyperref[alac:calculate_residuals]{calculate residuals from $\text{\QLPCOEFF}$ and \SAMPLES}\; + $\text{\RESIDUALBLOCK} \leftarrow$ \hyperref[alac:write_residuals]{encode residual block from $\text{\RESIDUAL}$ with \SAMPLESIZE}\; + \Return $\left\lbrace\begin{tabular}{l} + $\text{\QLPCOEFF}$ \\ + $\text{\RESIDUALBLOCK}$ \\ + \end{tabular}\right.$\; +} +\EALGORITHM +} + +\subsubsection{Windowing the Input Samples} +\label{alac:window} +{\relsize{-1} +\ALGORITHM{a list of signed input sample integers}{a list of signed windowed samples as floats} +\SetKwFunction{TUKEY}{tukey} +\SetKwData{SAMPLECOUNT}{sample count} +\SetKwData{WINDOWED}{windowed} +\SetKwData{SAMPLE}{sample} +\For{$i \leftarrow 0$ \emph{\KwTo}\SAMPLECOUNT}{ + $\text{\WINDOWED}_i = \text{\SAMPLE}_i \times \TUKEY(i)$\; +} +\Return \WINDOWED\; +\EALGORITHM +\par +\noindent +where the \VAR{Tukey} function is defined as: +\begin{equation*} +tukey(n) = +\begin{cases} +\frac{1}{2} \times \left[1 + cos\left(\pi \times \left(\frac{2 \times n}{\alpha \times (N - 1)} - 1 \right)\right)\right] & \text{ if } 0 \leq n \leq \frac{\alpha \times (N - 1)}{2} \\ +1 & \text{ if } \frac{\alpha \times (N - 1)}{2} \leq n \leq (N - 1) \times (1 - \frac{\alpha}{2}) \\ +\frac{1}{2} \times \left[1 + cos\left(\pi \times \left(\frac{2 \times n}{\alpha \times (N - 1)} - \frac{2}{\alpha} + 1 \right)\right)\right] & \text{ if } (N - 1) \times (1 - \frac{\alpha}{2}) \leq n \leq (N - 1) \\ +\end{cases} +\end{equation*} +\par +\noindent +$N$ is the total number of input samples and $\alpha$ is $\nicefrac{1}{2}$. +\par +\noindent +{\relsize{-2} +\begin{tabular}{r|rcrcr} +$i$ & $\textsf{sample}_i$ & & \texttt{tukey}($i$) & & $\textsf{windowed}_i$ \\ +\hline +0 & \texttt{0} & $\times$ & \texttt{0.00} & = & \texttt{0.00} \\ +1 & \texttt{16} & $\times$ & \texttt{0.19} & = & \texttt{3.01} \\ +2 & \texttt{31} & $\times$ & \texttt{0.61} & = & \texttt{18.95} \\ +3 & \texttt{44} & $\times$ & \texttt{0.95} & = & \texttt{41.82} \\ +4 & \texttt{54} & $\times$ & \texttt{1.00} & = & \texttt{54.00} \\ +5 & \texttt{61} & $\times$ & \texttt{1.00} & = & \texttt{61.00} \\ +6 & \texttt{64} & $\times$ & \texttt{1.00} & = & \texttt{64.00} \\ +7 & \texttt{63} & $\times$ & \texttt{1.00} & = & \texttt{63.00} \\ +8 & \texttt{58} & $\times$ & \texttt{1.00} & = & \texttt{58.00} \\ +9 & \texttt{49} & $\times$ & \texttt{1.00} & = & \texttt{49.00} \\ +10 & \texttt{38} & $\times$ & \texttt{1.00} & = & \texttt{38.00} \\ +11 & \texttt{24} & $\times$ & \texttt{0.95} & = & \texttt{22.81} \\ +12 & \texttt{8} & $\times$ & \texttt{0.61} & = & \texttt{4.89} \\ +13 & \texttt{-8} & $\times$ & \texttt{0.19} & = & \texttt{-1.51} \\ +14 & \texttt{-24} & $\times$ & \texttt{0.00} & = & \texttt{0.00} \\ +\end{tabular} +} +} + +\clearpage + +\subsubsection{Autocorrelating Windowed Samples} +\label{alac:autocorrelate} +{\relsize{-1} +\ALGORITHM{a list of signed windowed samples}{a list of signed autocorrelation values} +\SetKwData{LAG}{lag} +\SetKwData{AUTOCORRELATION}{autocorrelated} +\SetKwData{TOTALSAMPLES}{total samples} +\SetKwData{WINDOWED}{windowed} +\For{$\LAG \leftarrow 0$ \emph{\KwTo}9}{ + $\text{\AUTOCORRELATION}_{\text{\LAG}} = \overset{\text{\TOTALSAMPLES} - \text{\LAG} - 1}{\underset{i = 0}{\sum}}\text{\WINDOWED}_i \times \text{\WINDOWED}_{i + \text{\LAG}}$\; +} +\Return \AUTOCORRELATION\; +\EALGORITHM +} + +\subsubsection{Autocorrelation Example} +{\relsize{-1} +\begin{multicols}{2} +\begin{tabular}{rrrrr} + \texttt{0.00} & $\times$ & \texttt{0.00} & $=$ & \texttt{0.00} \\ + \texttt{3.01} & $\times$ & \texttt{3.01} & $=$ & \texttt{9.07} \\ + \texttt{18.95} & $\times$ & \texttt{18.95} & $=$ & \texttt{359.07} \\ + \texttt{41.82} & $\times$ & \texttt{41.82} & $=$ & \texttt{1749.02} \\ + \texttt{54.00} & $\times$ & \texttt{54.00} & $=$ & \texttt{2916.00} \\ + \texttt{61.00} & $\times$ & \texttt{61.00} & $=$ & \texttt{3721.00} \\ + \texttt{64.00} & $\times$ & \texttt{64.00} & $=$ & \texttt{4096.00} \\ + \texttt{63.00} & $\times$ & \texttt{63.00} & $=$ & \texttt{3969.00} \\ + \texttt{58.00} & $\times$ & \texttt{58.00} & $=$ & \texttt{3364.00} \\ + \texttt{49.00} & $\times$ & \texttt{49.00} & $=$ & \texttt{2401.00} \\ + \texttt{38.00} & $\times$ & \texttt{38.00} & $=$ & \texttt{1444.00} \\ + \texttt{22.81} & $\times$ & \texttt{22.81} & $=$ & \texttt{520.37} \\ + \texttt{4.89} & $\times$ & \texttt{4.89} & $=$ & \texttt{23.91} \\ + \texttt{-1.51} & $\times$ & \texttt{-1.51} & $=$ & \texttt{2.27} \\ + \texttt{0.00} & $\times$ & \texttt{0.00} & $=$ & \texttt{0.00} \\ + \hline + \multicolumn{3}{r}{$\textsf{autocorrelation}_0$} & $=$ & \texttt{24574.71} \\ +\end{tabular} +\par +\begin{tabular}{rrrrr} + \texttt{0.00} & $\times$ & \texttt{3.01} & $=$ & \texttt{0.00} \\ + \texttt{3.01} & $\times$ & \texttt{18.95} & $=$ & \texttt{57.08} \\ + \texttt{18.95} & $\times$ & \texttt{41.82} & $=$ & \texttt{792.48} \\ + \texttt{41.82} & $\times$ & \texttt{54.00} & $=$ & \texttt{2258.35} \\ + \texttt{54.00} & $\times$ & \texttt{61.00} & $=$ & \texttt{3294.00} \\ + \texttt{61.00} & $\times$ & \texttt{64.00} & $=$ & \texttt{3904.00} \\ + \texttt{64.00} & $\times$ & \texttt{63.00} & $=$ & \texttt{4032.00} \\ + \texttt{63.00} & $\times$ & \texttt{58.00} & $=$ & \texttt{3654.00} \\ + \texttt{58.00} & $\times$ & \texttt{49.00} & $=$ & \texttt{2842.00} \\ + \texttt{49.00} & $\times$ & \texttt{38.00} & $=$ & \texttt{1862.00} \\ + \texttt{38.00} & $\times$ & \texttt{22.81} & $=$ & \texttt{866.84} \\ + \texttt{22.81} & $\times$ & \texttt{4.89} & $=$ & \texttt{111.55} \\ + \texttt{4.89} & $\times$ & \texttt{-1.51} & $=$ & \texttt{-7.36} \\ + \texttt{-1.51} & $\times$ & \texttt{0.00} & $=$ & \texttt{0.00} \\ + \hline + \multicolumn{3}{r}{$\textsf{autocorrelation}_1$} & $=$ & \texttt{23666.93} \\ +\end{tabular} +\par +\begin{tabular}{rrrrr} + \texttt{0.00} & $\times$ & \texttt{18.95} & $=$ & \texttt{0.00} \\ + \texttt{3.01} & $\times$ & \texttt{41.82} & $=$ & \texttt{125.97} \\ + \texttt{18.95} & $\times$ & \texttt{54.00} & $=$ & \texttt{1023.25} \\ + \texttt{41.82} & $\times$ & \texttt{61.00} & $=$ & \texttt{2551.10} \\ + \texttt{54.00} & $\times$ & \texttt{64.00} & $=$ & \texttt{3456.00} \\ + \texttt{61.00} & $\times$ & \texttt{63.00} & $=$ & \texttt{3843.00} \\ + \texttt{64.00} & $\times$ & \texttt{58.00} & $=$ & \texttt{3712.00} \\ + \texttt{63.00} & $\times$ & \texttt{49.00} & $=$ & \texttt{3087.00} \\ + \texttt{58.00} & $\times$ & \texttt{38.00} & $=$ & \texttt{2204.00} \\ + \texttt{49.00} & $\times$ & \texttt{22.81} & $=$ & \texttt{1117.77} \\ + \texttt{38.00} & $\times$ & \texttt{4.89} & $=$ & \texttt{185.82} \\ + \texttt{22.81} & $\times$ & \texttt{-1.51} & $=$ & \texttt{-34.36} \\ + \texttt{4.89} & $\times$ & \texttt{0.00} & $=$ & \texttt{0.00} \\ + \hline + \multicolumn{3}{r}{$\textsf{autocorrelation}_2$} & $=$ & \texttt{21271.56} \\ +\end{tabular} +\par +\begin{tabular}{rrrrr} + \texttt{0.00} & $\times$ & \texttt{41.82} & $=$ & \texttt{0.00} \\ + \texttt{3.01} & $\times$ & \texttt{54.00} & $=$ & \texttt{162.65} \\ + \texttt{18.95} & $\times$ & \texttt{61.00} & $=$ & \texttt{1155.89} \\ + \texttt{41.82} & $\times$ & \texttt{64.00} & $=$ & \texttt{2676.56} \\ + \texttt{54.00} & $\times$ & \texttt{63.00} & $=$ & \texttt{3402.00} \\ + \texttt{61.00} & $\times$ & \texttt{58.00} & $=$ & \texttt{3538.00} \\ + \texttt{64.00} & $\times$ & \texttt{49.00} & $=$ & \texttt{3136.00} \\ + \texttt{63.00} & $\times$ & \texttt{38.00} & $=$ & \texttt{2394.00} \\ + \texttt{58.00} & $\times$ & \texttt{22.81} & $=$ & \texttt{1323.07} \\ + \texttt{49.00} & $\times$ & \texttt{4.89} & $=$ & \texttt{239.61} \\ + \texttt{38.00} & $\times$ & \texttt{-1.51} & $=$ & \texttt{-57.23} \\ + \texttt{22.81} & $\times$ & \texttt{0.00} & $=$ & \texttt{0.00} \\ + \hline + \multicolumn{3}{r}{$\textsf{autocorrelation}_3$} & $=$ & \texttt{17970.57} \\ +\end{tabular} +\par +\begin{tabular}{rrrrr} + \texttt{0.00} & $\times$ & \texttt{54.00} & $=$ & \texttt{0.00} \\ + \texttt{3.01} & $\times$ & \texttt{61.00} & $=$ & \texttt{183.74} \\ + \texttt{18.95} & $\times$ & \texttt{64.00} & $=$ & \texttt{1212.74} \\ + \texttt{41.82} & $\times$ & \texttt{63.00} & $=$ & \texttt{2634.74} \\ + \texttt{54.00} & $\times$ & \texttt{58.00} & $=$ & \texttt{3132.00} \\ + \texttt{61.00} & $\times$ & \texttt{49.00} & $=$ & \texttt{2989.00} \\ + \texttt{64.00} & $\times$ & \texttt{38.00} & $=$ & \texttt{2432.00} \\ + \texttt{63.00} & $\times$ & \texttt{22.81} & $=$ & \texttt{1437.13} \\ + \texttt{58.00} & $\times$ & \texttt{4.89} & $=$ & \texttt{283.62} \\ + \texttt{49.00} & $\times$ & \texttt{-1.51} & $=$ & \texttt{-73.80} \\ + \texttt{38.00} & $\times$ & \texttt{0.00} & $=$ & \texttt{0.00} \\ + \hline + \multicolumn{3}{r}{$\textsf{autocorrelation}_4$} & $=$ & \texttt{14231.18} \\ +\end{tabular} +\end{multicols} +} + +\clearpage + +\subsubsection{LP Coefficient Calculation} +\label{alac:compute_lp_coeffs} +{\relsize{-1} +\ALGORITHM{a list of autocorrelation floats}{a list of LP coefficient lists} +\SetKwData{LPCOEFF}{LP coefficient} +\SetKwData{ERROR}{error} +\SetKwData{AUTOCORRELATION}{autocorrelation} +\begin{tabular}{rcl} +$\kappa_0$ &$\leftarrow$ & $ \AUTOCORRELATION_1 \div \AUTOCORRELATION_0$ \\ +$\LPCOEFF_{0~0}$ &$\leftarrow$ & $ \kappa_0$ \\ +$\ERROR_0$ &$\leftarrow$ & $ \AUTOCORRELATION_0 \times (1 - {\kappa_0} ^ 2)$ \\ +\end{tabular}\; +\For{$i \leftarrow 1$ \emph{\KwTo}8}{ + \tcc{"zip" all of the previous row's LP coefficients + \newline + and the reversed autocorrelation values from 1 to i + 1 + \newline + into ($c$,$a$) pairs + \newline + $q_i$ is $\AUTOCORRELATION_{i + 1}$ minus the sum of those multiplied ($c$,$a$) pairs} + $q_i \leftarrow \AUTOCORRELATION_{i + 1}$\; + \For{$j \leftarrow 0$ \emph{\KwTo}i}{ + $q_i \leftarrow q_i - (\LPCOEFF_{(i - 1)~j} \times \AUTOCORRELATION_{i - j})$\; + } + \BlankLine + \tcc{"zip" all of the previous row's LP coefficients + \newline + and the previous row's LP coefficients reversed + \newline + into ($c$,$r$) pairs} + $\kappa_i = q_i \div \ERROR_{i - 1}$\; + \For{$j \leftarrow 0$ \emph{\KwTo}i}{ + \tcc{then build a new coefficient list of $c - (\kappa_i * r)$ for each ($c$,$r$) pair} + $\LPCOEFF_{i~j} \leftarrow \LPCOEFF_{(i - 1)~j} - (\kappa_i \times \LPCOEFF_{(i - 1)~(i - j - 1)})$\; + } + $\text{\LPCOEFF}_{i~i} \leftarrow \kappa_i$\tcc*[r]{and append $\kappa_i$ as the final coefficient in that list} + \BlankLine + $\ERROR_i \leftarrow \ERROR_{i - 1} \times (1 - {\kappa_i}^2)$\; +} +\Return \LPCOEFF\; +\EALGORITHM +} + +\begin{landscape} + +\subsubsection{LP Coefficient Calculation Example} +\begin{table}[h] +{\relsize{-1} +\begin{tabular}{r|r} +$i$ & $\textsf{autocorrelation}_i$ \\ +\hline +0 & \texttt{24598.25} \\ +1 & \texttt{23694.34} \\ +2 & \texttt{21304.57} \\ +3 & \texttt{18007.86} \\ +4 & \texttt{14270.30} \\ +\end{tabular} +} +\end{table} + +\begin{table}[h] +{\relsize{-1} +\renewcommand{\arraystretch}{1.45} +\begin{tabular}{|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|} +\hline +k_0 & +\multicolumn{4}{>{$}l<{$}|}{\texttt{23694.34} \div \texttt{24598.25} = \texttt{0.96}} \\ +\textsf{LP coefficient}_{0~0} & \texttt{\color{blue}0.96} & & & \\ +\textsf{error}_0 & +\multicolumn{4}{>{$}l<{$}|}{\texttt{24598.25} \times (1 - \texttt{0.96} ^ 2) = \texttt{1774.62}} \\ +\hline +q_1 & \multicolumn{4}{>{$}l<{$}|}{\texttt{21304.57} - (\texttt{0.96} \times \texttt{23694.34}) = \texttt{-1519.07}} \\ +k_1 & \multicolumn{4}{>{$}l<{$}|}{\texttt{-1519.07} \div \texttt{1774.62} = \texttt{-0.86}} \\ +\textsf{LP coefficient}_{1~i} & +\texttt{0.96} -(\texttt{-0.86} \times \texttt{0.96}) = \texttt{\color{blue}1.79} & +\texttt{\color{blue}-0.86} & & \\ +\textsf{error}_1 & \multicolumn{4}{>{$}l<{$}|}{\texttt{1774.62} \times (1 - \texttt{-0.86} ^ 2) = \texttt{474.30}} \\ +\hline +q_2 & \multicolumn{4}{>{$}l<{$}|}{\texttt{18007.86} - (\texttt{1.79} \times \texttt{21304.57} + \texttt{-0.86} \times \texttt{23694.34}) = \texttt{201.96}} \\ +k_2 & \multicolumn{4}{>{$}l<{$}|}{\texttt{201.96} \div \texttt{474.30} = \texttt{0.43}} \\ +\textsf{LP coefficient}_{2~i} & +\texttt{1.79} -(\texttt{0.43} \times \texttt{-0.86}) = \texttt{\color{blue}2.15} & +\texttt{-0.86} -(\texttt{0.43} \times \texttt{1.79}) = \texttt{\color{blue}-1.62} & +\texttt{\color{blue}0.43} & \\ +\textsf{error}_2 & \multicolumn{4}{>{$}l<{$}|}{\texttt{474.30} \times (1 - \texttt{0.43} ^ 2) = \texttt{388.31}} \\ +\hline +q_3 & \multicolumn{4}{>{$}l<{$}|}{\texttt{14270.30} - (\texttt{2.15} \times \texttt{18007.86} + \texttt{-1.62} \times \texttt{21304.57} + \texttt{0.43} \times \texttt{23694.34}) = \texttt{-122.06}} \\ +k_3 & \multicolumn{4}{>{$}l<{$}|}{\texttt{-122.06} \div \texttt{388.31} = \texttt{-0.31}} \\ +\textsf{LP coefficient}_{3~i} & +\texttt{2.15} -(\texttt{-0.31} \times \texttt{0.43}) = \texttt{\color{blue}2.29} & +\texttt{-1.62} -(\texttt{-0.31} \times \texttt{-1.62}) = \texttt{\color{blue}-2.13} & +\texttt{0.43} -(\texttt{-0.31} \times \texttt{2.15}) = \texttt{\color{blue}1.10} & +\texttt{\color{blue}-0.31} \\ +\textsf{error}_3 & \multicolumn{4}{>{$}l<{$}|}{\texttt{388.31} \times (1 - \texttt{-0.31} ^ 2) = \texttt{349.94}} \\ +\hline +\end{tabular} +\renewcommand{\arraystretch}{1.0} +} +\end{table} + +\end{landscape} + +\subsubsection{Quantizing LP Coefficients} +\label{alac:quantize_lp_coeffs} +\ALGORITHM{LP coefficients, an order value of 4 or 8}{QLP coefficients as a list of signed integers} +\SetKwData{ORDER}{order} +\SetKwFunction{MIN}{min} +\SetKwFunction{MAX}{max} +\SetKwFunction{ROUND}{round} +\SetKwData{QLPMIN}{QLP min} +\SetKwData{QLPMAX}{QLP max} +\SetKwData{LPCOEFF}{LP coefficient} +\SetKwData{QLPCOEFF}{QLP coefficient} +\tcc{QLP min and max are the smallest and largest QLP coefficients that fit in a signed field that's 16 bits wide} +$\QLPMIN \leftarrow 2 ^ \text{15} - 1$\; +$\QLPMAX \leftarrow -(2 ^ \text{15})$\; +$e \leftarrow 0.0$\; +\For{$i \leftarrow 0$ \emph{\KwTo}\ORDER}{ + $e \leftarrow e + \text{\LPCOEFF}_{\ORDER - 1~i} \times 2 ^ 9$\; + $\text{\QLPCOEFF}_i \leftarrow \MIN(\MAX(\ROUND(e)~,~\text{\QLPMIN})~,~\text{\QLPMAX})$\; + $e \leftarrow e - \text{\QLPCOEFF}_i$\; +} +\Return \QLPCOEFF\; +\EALGORITHM + +\clearpage + +\subsubsection{Quantizing Coefficients Example} +\begin{align*} +e &\leftarrow \texttt{0.00} + \texttt{2.29} \times 2 ^ 9 = \texttt{1170.49} \\ +\textsf{QLP coefficient}_0 &\leftarrow \texttt{round}(\texttt{1170.49}) = \texttt{\color{blue}1170} \\ +e &\leftarrow \texttt{1170.49} - 1170 = \texttt{0.49} \\ +e &\leftarrow \texttt{0.49} + \texttt{-2.13} \times 2 ^ 9 = \texttt{-1087.81} \\ +\textsf{QLP coefficient}_1 &\leftarrow \texttt{round}(\texttt{-1087.81}) = \texttt{\color{blue}-1088} \\ +e &\leftarrow \texttt{-1087.81} - -1088 = \texttt{0.19} \\ +e &\leftarrow \texttt{0.19} + \texttt{1.10} \times 2 ^ 9 = \texttt{564.59} \\ +\textsf{QLP coefficient}_2 &\leftarrow\texttt{round}(\texttt{564.59}) = \texttt{\color{blue}565} \\ +e &\leftarrow \texttt{564.59} - 565 = \texttt{-0.41} \\ +e &\leftarrow \texttt{-0.41} + \texttt{-0.31} \times 2 ^ 9 = \texttt{-161.35} \\ +\textsf{QLP coefficient}_3 &\leftarrow \texttt{round}(\texttt{-161.35}) = \texttt{\color{blue}-161} \\ +e &\leftarrow \texttt{-161.35} - -161 = \texttt{-0.35} \\ +\end{align*}
View file
audiotools-2.19.tar.gz/docs/reference/alac/encode/residual.tex
Added
@@ -0,0 +1,370 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\subsection{Computing Residual Values} +\label{alac:calculate_residuals} +{\relsize{-1} +\ALGORITHM{a list of signed PCM samples, 4 or 8 QLP coefficients, sample size}{a list of signed residual values} +\SetKwFunction{SIGN}{sign} +\SetKw{BREAK}{break} +\SetKwData{SAMPLESIZE}{sample size} +\SetKwData{SAMPLECOUNT}{sample count} +\SetKwData{COEFFCOUNT}{coefficient count} +\SetKwData{COEFF}{QLP coefficient} +\SetKwData{ERROR}{error} +\SetKwData{RESIDUAL}{residual} +\SetKwData{SAMPLE}{sample} +\SetKwData{BASESAMPLE}{base sample} +\SetKwData{QLPSUM}{QLP sum} +\SetKwData{DIFF}{diff} +\SetKwData{SSIGN}{sign} +\SetKwFunction{TRUNCATE}{truncate} +$\text{\RESIDUAL}_0 \leftarrow \text{\SAMPLE}_0$\tcc*[r]{first sample always copied verbatim} +\eIf{$\COEFFCOUNT < 31$}{ + \For{$i \leftarrow 1$ \emph{\KwTo}$\COEFFCOUNT + 1$}{ + $\text{\RESIDUAL}_i \leftarrow \TRUNCATE(\text{\SAMPLE}_i - \text{\SAMPLE}_{i - 1}~,~\SAMPLESIZE)$\; + } + \For{i $\leftarrow \text{\COEFFCOUNT} + 1$ \emph{\KwTo}\SAMPLECOUNT}{ + $\text{\BASESAMPLE}_i \leftarrow \text{\SAMPLE}_{i - \COEFFCOUNT - 1}$\; + $\text{\QLPSUM}_i \leftarrow \overset{\COEFFCOUNT - 1}{\underset{j = 0}{\sum}} \text{\COEFF}_j \times (\text{\SAMPLE}_{i - j - 1} - \text{\BASESAMPLE}_i)$\; + $\text{\ERROR} \leftarrow \TRUNCATE\left(\text{\SAMPLE}_i - \text{\BASESAMPLE}_i - \left\lfloor\frac{\text{\QLPSUM}_i + 2 ^ 8}{2 ^ 9}\right\rfloor~,~\SAMPLESIZE \right)$\; + $\text{\RESIDUAL}_i \leftarrow \text{\ERROR}$\; + \BlankLine + \uIf(\tcc*[f]{modify QLP coefficients}){$\text{\ERROR} > 0$}{ + \For{$j \leftarrow 0$ \emph{\KwTo}$\text{\COEFFCOUNT}$}{ + $\DIFF \leftarrow \text{\BASESAMPLE}_i - \text{\SAMPLE}_{i - \COEFFCOUNT + j}$\; + $\SSIGN \leftarrow \SIGN(\DIFF)$\; + $\text{\COEFF}_{\COEFFCOUNT - j - 1} \leftarrow \text{\COEFF}_{\COEFFCOUNT - j - 1} - \SSIGN$\; + $\text{\ERROR} \leftarrow \text{\ERROR} - \left\lfloor\frac{\DIFF \times \SSIGN}{2 ^ 9}\right\rfloor \times (j + 1)$\; + \If{$\text{\ERROR} \leq 0$}{ + \BREAK\; + } + } + } + \ElseIf{$\text{\ERROR} < 0$}{ + \For{$j \leftarrow 0$ \emph{\KwTo}$\text{\COEFFCOUNT}$}{ + $\DIFF \leftarrow \text{\BASESAMPLE}_i - \text{\SAMPLE}_{i - \COEFFCOUNT + j}$\; + $\SSIGN \leftarrow \SIGN(\DIFF)$\; + $\text{\COEFF}_{\COEFFCOUNT - j - 1} \leftarrow \text{\COEFF}_{\COEFFCOUNT - j - 1} + \SSIGN$\; + $\text{\ERROR} \leftarrow \text{\ERROR} - \left\lfloor\frac{\DIFF \times -\SSIGN}{2 ^ 9}\right\rfloor \times (j + 1)$\; + \If{$\text{\ERROR} \geq 0$}{ + \BREAK\; + } + } + } + } +}{ + \For{$i \leftarrow 1$ \emph{\KwTo}\SAMPLECOUNT}{ + $\text{\RESIDUAL}_i \leftarrow \TRUNCATE(\text{\SAMPLE}_i - \text{\SAMPLE}_{i - 1}~,~\SAMPLESIZE)$\; + } +} +\Return \RESIDUAL\; +\EALGORITHM +} +\subsubsection{The \texttt{truncate} Function} +{\relsize{-1} + \ALGORITHM{a signed sample, the maximum size of the sample in bits}{a truncated signed sample} + \SetKw{BAND}{bitwise and} + \SetKwData{SAMPLE}{sample} + \SetKwData{BITS}{bits} + \SetKwData{TRUNCATED}{truncated} + $\TRUNCATED \leftarrow \SAMPLE~\BAND~(2 ^ {\BITS} - 1)$\; + \eIf(\tcc*[f]{apply sign bit}){$(\TRUNCATED~\BAND~2 ^ {\BITS - 1}) \neq 0$}{ + \Return $\TRUNCATED - 2 ^ {\BITS}$\; + }{ + \Return \TRUNCATED\; + } + \EALGORITHM +} + +\clearpage + +{\relsize{-1} +\begin{equation*} +\texttt{sign}(x) = +\begin{cases} +\texttt{ 1} & \text{if } x > 0 \\ +\texttt{ 0} & \text{if } x = 0 \\ +\texttt{-1} & \text{if } x < 0 +\end{cases} +\end{equation*} +} + +\subsubsection{Computing Residuals Example} +{\relsize{-2} +Given the samples +\texttt{0, 16, 32, 44, 54, 61, 64, 63, 58, 49, 38, 24, 8, -8, -24}, +and the QLP coefficients +\texttt{1170, -1088, 565, -161}, +the subframe's residuals are calculate as follows: +\par +\vskip .15in +\noindent +\begin{tabular}{r||r|r|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}} +$i$ & $\textsf{sample}_i$ & $\textsf{base}_i$ & \textsf{QLP sum}_i & \textsf{residual}_i & \textsf{QLP coefficient}_{(i + 1)~j} \\ +\hline +0 & 0 & & & 0 \\ +1 & 16 & & & 16 - 0 = 16 \\ +2 & 32 & & & 32 - 16 = 16 \\ +3 & 44 & & & 44 - 32 = 12 \\ +4 & 54 & & & 54 - 44 = 10 \\ +\hline +5 & 61 & 0 & 1170 \times (54 - 0) \texttt{ +} & 61 - (\lfloor(30812 + 2 ^ 8) \div 2 ^ 9\rfloor + 0) = 1 & 1170 + 1 = 1171 \\ +& & & -1088 \times (44 - 0) \texttt{ +} & & -1088 + 1 = -1087 \\ +& & & 565 \times (32 - 0) \texttt{ +} & & 565 + 1 = 566 \\ +& & & -161 \times (16 - 0) \texttt{~~} & & -161 + 1 = -160 \\ +\hline +6 & 64 & 16 & 1171 \times (61 - 16) \texttt{ +} & 64 - (\lfloor(24677 + 2 ^ 8) \div 2 ^ 9\rfloor + 16) = 0 & 1171 \\ +& & & -1087 \times (54 - 16) \texttt{ +} & & -1087 \\ +& & & 566 \times (44 - 16) \texttt{ +} & & 566 \\ +& & & -160 \times (32 - 16) \texttt{~~} & & -160 \\ +\hline +7 & 63 & 32 & 1171 \times (64 - 32) \texttt{ +} & 63 - (\lfloor(16481 + 2 ^ 8) \div 2 ^ 9\rfloor + 32) = -1 & 1171 \\ +& & & -1087 \times (61 - 32) \texttt{ +} & & -1087 \\ +& & & 566 \times (54 - 32) \texttt{ +} & & 566 \\ +& & & -160 \times (44 - 32) \texttt{~~} & & -160 - 1 = -159 \\ +\hline +8 & 58 & 44 & 1171 \times (63 - 44) \texttt{ +} & 58 - (\lfloor(8521 + 2 ^ 8) \div 2 ^ 9\rfloor + 44) = -3 & 1171 \\ +& & & -1087 \times (64 - 44) \texttt{ +} & & -1087 \\ +& & & 566 \times (61 - 44) \texttt{ +} & & 565 \\ +& & & -161 \times (54 - 44) \texttt{~~} & & -161 - 1 = -160 \\ +\hline +9 & 49 & 54 & 1171 \times (58 - 54) \texttt{ +} & 49 - (\lfloor(-583 + 2 ^ 8) \div 2 ^ 9\rfloor + 54) = -4 & 1171 \\ +& & & -1087 \times (63 - 54) \texttt{ +} & & -1088 \\ +& & & 565 \times (64 - 54) \texttt{ +} & & -1087 - 1 = -1086 \\ +& & & -162 \times (61 - 54) \texttt{~~} & & -162 - 1 = -161 \\ +\hline +10 & 38 & 61 & 1171 \times (49 - 61) \texttt{ +} & 38 - (\lfloor(-10149 + 2 ^ 8) \div 2 ^ 9\rfloor + 61) = -3 & 1171 \\ +& & & -1088 \times (58 - 61) \texttt{ +} & & -1088 \\ +& & & 564 \times (63 - 61) \texttt{ +} & & 563 \\ +& & & -163 \times (64 - 61) \texttt{~~} & & -163 - 1 = -162 \\ +\hline +11 & 24 & 64 & 1171 \times (38 - 64) \texttt{ +} & 24 - (\lfloor(-17340 + 2 ^ 8) \div 2 ^ 9\rfloor + 64) = -6 & 1171 \\ +& & & -1088 \times (49 - 64) \texttt{ +} & & -1087 \\ +& & & 563 \times (58 - 64) \texttt{ +} & & -1088 + 1 = -1089 \\ +& & & -164 \times (63 - 64) \texttt{~~} & & -164 + 1 = -165 \\ +\hline +12 & 8 & 63 & 1171 \times (24 - 63) \texttt{ +} & 8 - (\lfloor(-25575 + 2 ^ 8) \div 2 ^ 9\rfloor + 63) = -5 & 1171 \\ +& & & -1087 \times (38 - 63) \texttt{ +} & & -1086 \\ +& & & 564 \times (49 - 63) \texttt{ +} & & -1087 + 1 = -1088 \\ +& & & -163 \times (58 - 63) \texttt{~~} & & -163 + 1 = -164 \\ +\hline +13 & -8 & 58 & 1171 \times (8 - 58) \texttt{ +} & -8 - (\lfloor(-31468 + 2 ^ 8) \div 2 ^ 9\rfloor + 58) = -5 & 1171 \\ +& & & -1086 \times (24 - 58) \texttt{ +} & & -1085 \\ +& & & 565 \times (38 - 58) \texttt{ +} & & -1086 + 1 = -1087 \\ +& & & -162 \times (49 - 58) \texttt{~~} & & -162 + 1 = -163 \\ +\hline +14 & -24 & 49 & 1171 \times (-8 - 49) \texttt{ +} & -24 - (\lfloor(-34641 + 2 ^ 8) \div 2 ^ 9\rfloor + 49) = -5 & 1171 \\ +& & & -1085 \times (8 - 49) \texttt{ +} & & -1084 \\ +& & & 566 \times (24 - 49) \texttt{ +} & & -1085 + 1 = -1086 \\ +& & & -161 \times (38 - 49) \texttt{~~} & & -161 + 1 = -162 \\ +\hline +\end{tabular} +} + +\clearpage + +\subsection{Encoding Residual Block} +\label{alac:write_residuals} +\ALGORITHM{a list of signed residual values, sample size; initial history, history multiplier, maximum K from encoding options}{a block of residual data, or a \textit{residual overflow} exception} +\SetKwData{RESIDUAL}{residual} +\SetKwData{UNSIGNED}{unsigned} +\SetKwData{SAMPLESIZE}{sample size} +\SetKwData{HISTORY}{history} +\SetKwData{HISTORYMULT}{history multiplier} +\SetKwData{MAXIMUMK}{maximum K} +\SetKwData{SIGNMODIFIER}{sign modifier} +\SetKwData{ZEROES}{zeroes} +\SetKw{RAISE}{raise} +\SetKwFunction{MIN}{min} +\SetKwFunction{WRITERESIDUAL}{write residual} +\SetKw{AND}{and} +\HISTORY $\leftarrow$ initial history\; +\SIGNMODIFIER $\leftarrow 0$\; +$i \leftarrow 0$\; +\While{$i < \text{residual count}$}{ + \eIf(\tcc*[f]{add sign bit}){$\text{\RESIDUAL}_i \geq 0$}{ + $\text{\UNSIGNED}_i \leftarrow \text{\RESIDUAL}_i \times 2$\; + }{ + $\text{\UNSIGNED}_i \leftarrow (-\text{\RESIDUAL}_i \times 2) - 1$\; + } + \If{$\text{\UNSIGNED}_i \geq 2 ^ \text{\SAMPLESIZE}$}{ + \RAISE residual overflow exception\footnote{in this case, we should cease building a compressed frame and write an uncompressed frame instead}\; + } + $\kappa \leftarrow \MIN(\lfloor\log_2((\HISTORY \div 2 ^ 9) + 3)\rfloor~,~\MAXIMUMK)$\; + $\WRITERESIDUAL(\text{\UNSIGNED}_i - \SIGNMODIFIER~,~\kappa~,~\text{\SAMPLESIZE})$\; + $\SIGNMODIFIER \leftarrow 0$\; + \BlankLine + \eIf(\tcc*[f]{update history}){$\text{\UNSIGNED}_i \leq 65535$}{ + $\HISTORY \leftarrow \HISTORY + (\text{\UNSIGNED}_i \times \HISTORYMULT) - \left\lfloor\frac{\HISTORY \times \HISTORYMULT}{2 ^ 9}\right\rfloor$\; + $i \leftarrow i + 1$\; + \BlankLine + \If(\tcc*[f]{handle 0 residuals}){$\HISTORY < 128$ \AND $i < \text{residual count}$}{ + $\kappa \leftarrow \MIN(7 - \lfloor\log_2(\HISTORY)\rfloor + \lfloor(\HISTORY + 16) \div 2 ^ 6\rfloor~,~\MAXIMUMK)$\; + $\ZEROES \leftarrow 0$\; + \While{$i < \text{residual count}$ \AND $\text{\RESIDUAL}_i = 0$}{ + $\ZEROES \leftarrow \ZEROES + 1$\; + $i \leftarrow i + 1$\; + } + $\WRITERESIDUAL(\ZEROES~,~\kappa~,~16)$\; + \If{$\ZEROES < 65535$}{ + $\SIGNMODIFIER \leftarrow 1$\; + } + $\HISTORY \leftarrow 0$\; + } + }{ + $i \leftarrow i + 1$\; + $\HISTORY \leftarrow 65535$\; + } +} +\Return encoded residual block\; +\EALGORITHM + +\clearpage + +\subsubsection{Encoding Individual Residual} + +\ALGORITHM{an unsigned residual value, $\kappa$, sample size}{an individual encoded residual} +\SetKwData{UNSIGNED}{unsigned} +\SetKwData{MSB}{MSB} +\SetKwData{LSB}{LSB} +$\MSB \leftarrow \text{\UNSIGNED} \div 2 ^ \kappa - 1$\; +$\LSB \leftarrow \text{\UNSIGNED} \bmod~2 ^ \kappa - 1$\; +\eIf{$\MSB > 8$}{ + $\texttt{0x1FF} \rightarrow$ \WRITE 9 unsigned bits\; + $\text{\UNSIGNED} \rightarrow$ \WRITE (sample size) unsigned bits\; +}{ + $\MSB \rightarrow$ \WUNARY with stop bit 0\; + \If{$\kappa > 1$}{ + \eIf{$\LSB > 0$}{ + $\LSB + 1 \rightarrow$ \WRITE $\kappa$ unsigned bits\; + }{ + $0 \rightarrow$ \WRITE $(\kappa - 1)$ unsigned bits\; + } + } +} +\Return encoded residual data\; +\EALGORITHM + +\begin{landscape} + +\subsubsection{Residual Encoding Example} +\begin{table}[h] +{\relsize{-1} +\renewcommand{\arraystretch}{1.5} +\begin{tabular}{r||r|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}} +$i$ & $\textsf{residual}_i$ & \textsf{unsigned}_i & \kappa & \textsf{MSB}_i & \textsf{LSB}_i & \textsf{history}_{i + 1} \\ +\hline +0 & \texttt{0} & +\texttt{0} \times 2 = \texttt{0} & +\lfloor\log_2(\frac{\texttt{10}}{2 ^ 9} + 3)\rfloor = \texttt{1} & +0 \div 2 ^ 1 - 1 = 0 & + & +\texttt{10} + (\texttt{0} \times \texttt{40}) - \left\lfloor\frac{\texttt{10} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{10} \\ +& $zeroes = 0$ & +\multicolumn{2}{>{$}r<{$}|}{7 - \lfloor\log_2(10)\rfloor + \lfloor\frac{10 + 16}{2 ^ 6}\rfloor = 4} & +0 \div 2 ^ 4 - 1 = 0 & +0 \bmod~2 ^ 4 - 1 = 0 & +\texttt{0} \\ +1 & \texttt{16} & +\texttt{16} \times 2 = \texttt{32}\text{\symbolfootnotemark[2]} & +\lfloor\log_2(\frac{\texttt{0}}{2 ^ 9} + 3)\rfloor = \texttt{1} & +\multicolumn{2}{r|}{write \texttt{0x1FFF} in 9 unsigned bits} & +\texttt{0} + (\texttt{32} \times \texttt{40}) - \left\lfloor\frac{\texttt{0} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{1280} \\ +& & & & \multicolumn{2}{r|}{write \texttt{31} in 16 unsigned bits} & \\ +2 & \texttt{16} & +\texttt{16} \times 2 = \texttt{32} & +\lfloor\log_2(\frac{\texttt{1280}}{2 ^ 9} + 3)\rfloor = \texttt{2} & +\multicolumn{2}{r|}{write \texttt{0x1FFF} in 9 unsigned bits} & +\texttt{1280} + (\texttt{32} \times \texttt{40}) - \left\lfloor\frac{\texttt{1280} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{2460} \\ +& & & & \multicolumn{2}{r|}{write \texttt{32} in 16 unsigned bits} & \\ +3 & \texttt{12} & +\texttt{12} \times 2 = \texttt{24} & +\lfloor\log_2(\frac{\texttt{2460}}{2 ^ 9} + 3)\rfloor = \texttt{2} & +24 \div 2 ^ 2 - 1 = 8 & +24 \bmod~2 ^ 2 - 1 = 0 & +\texttt{2460} + (\texttt{24} \times \texttt{40}) - \left\lfloor\frac{\texttt{2460} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3228} \\ +4 & \texttt{10} & +\texttt{10} \times 2 = \texttt{20} & +\lfloor\log_2(\frac{\texttt{3228}}{2 ^ 9} + 3)\rfloor = \texttt{3} & +20 \div 2 ^ 3 - 1 = 2 & +20 \bmod~2 ^ 3 - 1 = 6 & +\texttt{3228} + (\texttt{20} \times \texttt{40}) - \left\lfloor\frac{\texttt{3228} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3776} \\ +5 & \texttt{1} & +\texttt{1} \times 2 = \texttt{2} & +\lfloor\log_2(\frac{\texttt{3776}}{2 ^ 9} + 3)\rfloor = \texttt{3} & +2 \div 2 ^ 3 - 1 = 0 & +2 \bmod~2 ^ 3 - 1 = 2 & +\texttt{3776} + (\texttt{2} \times \texttt{40}) - \left\lfloor\frac{\texttt{3776} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3561} \\ +6 & \texttt{0} & +\texttt{0} \times 2 = \texttt{0} & +\lfloor\log_2(\frac{\texttt{3561}}{2 ^ 9} + 3)\rfloor = \texttt{3} & +0 \div 2 ^ 3 - 1 = 0 & +0 \bmod~2 ^ 3 - 1 = 0 & +\texttt{3561} + (\texttt{0} \times \texttt{40}) - \left\lfloor\frac{\texttt{3561} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3283} \\ +7 & \texttt{-1} & +(\texttt{1} \times 2) - 1 = \texttt{1} & +\lfloor\log_2(\frac{\texttt{3283}}{2 ^ 9} + 3)\rfloor = \texttt{3} & +1 \div 2 ^ 3 - 1 = 0 & +1 \bmod~2 ^ 3 - 1 = 1 & +\texttt{3283} + (\texttt{1} \times \texttt{40}) - \left\lfloor\frac{\texttt{3283} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3067} \\ +8 & \texttt{-3} & +(\texttt{3} \times 2) - 1 = \texttt{5} & +\lfloor\log_2(\frac{\texttt{3067}}{2 ^ 9} + 3)\rfloor = \texttt{3} & +5 \div 2 ^ 3 - 1 = 0 & +5 \bmod~2 ^ 3 - 1 = 5 & +\texttt{3067} + (\texttt{5} \times \texttt{40}) - \left\lfloor\frac{\texttt{3067} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3028} \\ +9 & \texttt{-4} & +(\texttt{4} \times 2) - 1 = \texttt{7} & +\lfloor\log_2(\frac{\texttt{3028}}{2 ^ 9} + 3)\rfloor = \texttt{3} & +7 \div 2 ^ 3 - 1 = 1 & +7 \bmod~2 ^ 3 - 1 = 0 & +\texttt{3028} + (\texttt{7} \times \texttt{40}) - \left\lfloor\frac{\texttt{3028} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3072} \\ +10 & \texttt{-3} & +(\texttt{3} \times 2) - 1 = \texttt{5} & +\lfloor\log_2(\frac{\texttt{3072}}{2 ^ 9} + 3)\rfloor = \texttt{3} & +5 \div 2 ^ 3 - 1 = 0 & +5 \bmod~2 ^ 3 - 1 = 5 & +\texttt{3072} + (\texttt{5} \times \texttt{40}) - \left\lfloor\frac{\texttt{3072} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3032} \\ +11 & \texttt{-6} & +(\texttt{6} \times 2) - 1 = \texttt{11} & +\lfloor\log_2(\frac{\texttt{3032}}{2 ^ 9} + 3)\rfloor = \texttt{3} & +11 \div 2 ^ 3 - 1 = 1 & +11 \bmod~2 ^ 3 - 1 = 4 & +\texttt{3032} + (\texttt{11} \times \texttt{40}) - \left\lfloor\frac{\texttt{3032} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3236} \\ +12 & \texttt{-5} & +(\texttt{5} \times 2) - 1 = \texttt{9} & +\lfloor\log_2(\frac{\texttt{3236}}{2 ^ 9} + 3)\rfloor = \texttt{3} & +9 \div 2 ^ 3 - 1 = 1 & +9 \bmod~2 ^ 3 - 1 = 2 & +\texttt{3236} + (\texttt{9} \times \texttt{40}) - \left\lfloor\frac{\texttt{3236} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3344} \\ +13 & \texttt{-5} & +(\texttt{5} \times 2) - 1 = \texttt{9} & +\lfloor\log_2(\frac{\texttt{3344}}{2 ^ 9} + 3)\rfloor = \texttt{3} & +9 \div 2 ^ 3 - 1 = 1 & +9 \bmod~2 ^ 3 - 1 = 2 & +\texttt{3344} + (\texttt{9} \times \texttt{40}) - \left\lfloor\frac{\texttt{3344} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3443} \\ +14 & \texttt{-5} & +(\texttt{5} \times 2) - 1 = \texttt{9} & +\lfloor\log_2(\frac{\texttt{3443}}{2 ^ 9} + 3)\rfloor = \texttt{3} & +9 \div 2 ^ 3 - 1 = 1 & +9 \bmod~2 ^ 3 - 1 = 2 & +\texttt{3443} + (\texttt{9} \times \texttt{40}) - \left\lfloor\frac{\texttt{3443} \times \texttt{40}}{2 ^ 9}\right\rfloor = \texttt{3535} \\ +\end{tabular} +\renewcommand{\arraystretch}{1.0} +} +\end{table} +\symbolfootnotetext[2]{written as $\texttt{32} - 1 = \texttt{31}$ due to sign modifier} + +\clearpage + +\begin{figure}[h] +\includegraphics{alac/figures/residual-build.pdf} +\end{figure} + +\end{landscape}
View file
audiotools-2.19.tar.gz/docs/reference/alac/figures
Added
+(directory)
View file
audiotools-2.19.tar.gz/docs/reference/alac/figures/alac-atom-parse.bpx
Changed
(renamed from docs/reference/figures/alac/alac-atom-parse.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/alac/figures/alac_atom.bdx
Changed
(renamed from docs/reference/figures/alac/alac_atom.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/alac/figures/atoms.fig
Changed
(renamed from docs/reference/figures/alac/atoms.fig)
View file
audiotools-2.19.tar.gz/docs/reference/alac/figures/interlaced_frame.bdx
Changed
(renamed from docs/reference/figures/alac/interlaced_frame.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/alac/figures/mdhd-parse.bpx
Changed
(renamed from docs/reference/figures/alac/mdhd-parse.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/alac/figures/noninterlaced_frame.bdx
Changed
(renamed from docs/reference/figures/alac/noninterlaced_frame.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/alac/figures/residual-build.bpx
Changed
(renamed from docs/reference/figures/alac/residual-build.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/alac/figures/residual-parse.bpx
Changed
(renamed from docs/reference/figures/alac/residual-parse.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/alac/figures/stream.bdx
Changed
(renamed from docs/reference/figures/alac/stream.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/alac/figures/subframe-build.bpx
Changed
(renamed from docs/reference/figures/alac/subframe-build.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/alac/figures/subframe-parse.bpx
Changed
(renamed from docs/reference/figures/alac/subframe-parse.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/alac/figures/subframe_header.bdx
Changed
(renamed from docs/reference/figures/alac/subframe_header.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/alac/figures/uncompressed_frame.bdx
Changed
(renamed from docs/reference/figures/alac/uncompressed_frame.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/ape-codec.template
Added
@@ -0,0 +1,3 @@ +<<file:header.tex>> +\include{ape} +<<file:footer.tex>>
View file
audiotools-2.18.tar.gz/docs/reference/ape.tex -> audiotools-2.19.tar.gz/docs/reference/ape.tex
Changed
@@ -13,123 +13,81 @@ than storing only the data it contains. All of its fields are little-endian. -\section{the Monkey's Audio File Stream} +\section{File Stream} \begin{figure}[h] \includegraphics{figures/ape/stream.pdf} \end{figure} -\section{the Monkey's Audio Descriptor} +\clearpage + +\section{Descriptor} \begin{figure}[h] \includegraphics{figures/ape/descriptor.pdf} \end{figure} \par \noindent -\VAR{Version} is the encoding software's version times 1000. -i.e. Monkey's Audio 3.99 = 3990 +\VAR{ID} is always \texttt{"MAC "} (note the trailing space). +\VAR{version} is the encoding software's version times 1000 +(e.g. Monkey's Audio 3.99 = 3990). +The 6 length fields are the lengths of the entire +\VAR{Descriptor}, \VAR{Header}, \VAR{Seektable}, +\VAR{Wave Header}, \VAR{Frames} and \VAR{Wave Footer} blocks, +in bytes. +%%FIXME - figure out what MD5 sum is the hash of -\section{the Monkey's Audio header} +\subsection{Descriptor Example} \begin{figure}[h] -\includegraphics{figures/ape/header.pdf} +\includegraphics{figures/ape/descriptor-example.pdf} \end{figure} -{\relsize{-2} -\begin{equation} -\text{Length in Seconds} = \frac{((\text{Total Frames} - 1) \times \text{Blocks Per Frame}) + \text{Final Frame Blocks}}{\text{Sample Rate}} -\end{equation} -} -\section{the APEv2 Tag} -\label{apev2} +\begin{table}[h] +\begin{tabular}{rl} +ID : & \texttt{"MAC "} \\ +version : & \texttt{3990} (3.990) \\ +descriptor length : & \texttt{52} (bytes)\\ +header length : & \texttt{24} (bytes)\\ +seektable length : & \texttt{4} (bytes) \\ +wave header length : & \texttt{44} (bytes) \\ +frames length : & \texttt{44} (bytes) \\ +wave footer length : & \texttt{0} \\ +MD5 sum : & \texttt{"41486031F2B6EBAA6F513E46C46396E6"} +\end{tabular} +\end{table} + +\clearpage + +\section{Header} \begin{figure}[h] - \includegraphics{figures/ape/apev2_tagheader.pdf} +\includegraphics{figures/ape/header.pdf} \end{figure} -All APEv2 tags have a footer, 32 bytes from the end of the file. -Some may also have an identically-formatted header. -\VAR{tag size} is the size of the entire tag, -including the footer but \textit{not} including any header. -\VAR{item count} is the total number of items in the tag. - +\par +\noindent +\VAR{compression level} indicates the level the file was compressed at. +%%FIXME - figure out format flags +\VAR{blocks per frame} is the number of PCM frames per APE frame, +not including the final frame. +\VAR{final frame blocks} is the number of PCM frames +in the final APE frame. +\VAR{total frames} is the total number of APE frames. +\VAR{bits per sample}, \VAR{channels} and \VAR{sample rate} +are stream parameters. +\subsection{Header Example} \begin{figure}[h] - \includegraphics{figures/ape/apev2_item.pdf} +\includegraphics{figures/ape/header-example.pdf} \end{figure} -\VAR{item encoding} is one of: \begin{table}[h] - \begin{tabular}{rl} - \texttt{0} & UTF-8 \\ - \texttt{1} & Binary \\ - \texttt{2} & External Link \\ - \texttt{3} & Reserved \\ - \end{tabular} +\begin{tabular}{rl} +compression level : & \texttt{2000} \\ +format flags : & \texttt{0} \\ +blocks per frame : & \texttt{73728} \\ +final frame blocks : & \texttt{25} \\ +total frames : & \texttt{1} \\ +bits per sample : & \texttt{16} \\ +channels : & \texttt{1} \\ +sample rate : & \texttt{44100} \\ +\end{tabular} \end{table} - -%% The APEv2 tag is a little-endian metadata tag appended to -%% Monkey's Audio files, among others. -%% \begin{figure}[h] -%% \includegraphics{figures/ape/apev2_tag.pdf} -%% \end{figure} -%% \par -%% \noindent -%% \VAR{Item Key} is an ASCII string from the range 0x20 to 0x7E. -%% \VAR{Item Value} is typically a UTF-8 encoded string, but may -%% also be binary depending on the Flags. - -%% \begin{multicols}{2} %% {\relsize{-2} -%% \begin{description} -%% \item[Abstract] Abstract -%% \item[Album] album name -%% \item[Artist] performing artist -%% \item[Bibliography] Bibliography/Discography -%% \item[Catalog] catalog number -%% \item[Comment] user comment -%% \item[Composer] original composer -%% \item[Conductor] conductor -%% \item[Copyright] copyright holder -%% \item[Debut album] debut album name -%% \item[Dummy] place holder -%% \item[EAN/UPC] EAN-13/UPC-A bar code identifier -%% \item[File] file location -%% \item[Genre] genre -%% \item[Index] indexes for quick access -%% \item[Introplay] characteristic part of piece for intro playing -%% \item[ISBN] ISBN number with check digit -%% \item[ISRC] International Standard Recording Number -%% \item[Language] used Language(s) for music/spoken words -%% \item[LC] Label Code -%% \item[Media] source media -%% \item[Publicationright] publication right holder -%% \item[Publisher] record label or publisher -%% \item[Record Date] record date -%% \item[Record Location] record location -%% \item[Related] location of related information -%% \item[Subtitle] track subtitle -%% \item[Title] track title -%% \item[Track] track number -%% \item[Year] release date -%% \end{description} +%% \begin{equation} +%% \text{Length in Seconds} = \frac{((\text{Total Frames} - 1) \times \text{Blocks Per Frame}) + \text{Final Frame Blocks}}{\text{Sample Rate}} +%% \end{equation} %% } -%% \end{multicols} - -%% \pagebreak - -%% \subsection{the APEv2 Tag Header/Footer} - -%% \par -%% \noindent -%% The format of the APEv2 header and footer are identical -%% except for the \VAR{Is Header} tag. -%% \VAR{Version} is typically 2000 (stored little-endian). -%% \VAR{Tag Size} is the size of the entire APEv2 tag, including the -%% footer but excluding the header. -%% \VAR{Item Count} is the number of individual tag items. - -%% \subsection{the APEv2 Flags} -%% \begin{figure}[h] -%% \includegraphics{figures/ape/apev2_flags.pdf} -%% \end{figure} -%% \par -%% \noindent -%% This flags field is used by both the APEv2 header/footer and the -%% individual tag items. -%% The \VAR{Encoding} field indicates the encoding of its value: - - -%% .
View file
audiotools-2.19.tar.gz/docs/reference/apev2.tex
Added
@@ -0,0 +1,165 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\chapter{APEv2 Tags} +\label{apev2} +APEv2 tags are a format-agnostic way of attaching metadata to audio files. +It is used by WavPack, Musepack, Monkey's Audio and others. +In most cases APEv2 tags are attached to the end of the file, +but it is possible to attach them to the beginning. + +\begin{figure}[h] + \includegraphics{figures/apev2/tag.pdf} +\end{figure} +\par +\noindent +When attached to the end of a file, the footer is required +and the header is optional. +When attached to the beginning of a file, the header is required +and the footer is optional. +Because the header and footer are fixed sizes, +this placement allows one to locate them the file +unambiguously. + +All fields within an APEv2 tag are little-endian. +All items within an APEv2 tag should be sorted +by ascending size. + +\clearpage + +\section{APEv2 Header / Footer} +\begin{figure}[h] + \includegraphics{figures/apev2/header.pdf} +\end{figure} +\par +\noindent +\VAR{tag size} is the size of the APEv2 tag including +any header, but \textit{not} including the footer. +\VAR{item count} is the number of APEv2 tag items. +\VAR{read only} indicates the APEv2 tag is read-only. +\VAR{item encoding} is not used by the header or footer. +\VAR{is header} indicates this is the header. +\VAR{contains no footer} indicates the tag contains no footer. +\VAR{contains a header} indicates the tag contains a header. + +\subsection{APEv2 Footer Example} +\begin{figure}[h] + \includegraphics{figures/apev2/footer-example.pdf} +\end{figure} +\begin{figure}[h] + \includegraphics{figures/apev2/footer-flags.pdf} + \caption{footer flags} +\end{figure} + +\clearpage + +\begin{tabular}{rl} +preamble : & \texttt{"APETAGEX"} \\ +version : & \texttt{2000} \\ +tag size : & \texttt{99} bytes \\ +item count : & \texttt{3} \\ +read only : & \texttt{0} \\ +item encoding : & \texttt{0} (unused) \\ +is header : & \texttt{0} \\ +contains no footer : & \texttt{0} \\ +contains a header : & \texttt{1} \\ +\end{tabular} +\par +\noindent +Because the tag size is 99 bytes, the full size of the tag +is 131 bytes (99 + 32 bytes for footer). +And, because \VAR{contains a header} is set, +the first 32 bytes of the tag contain an identically-formatted header. + +\begin{figure}[h] + \includegraphics{figures/apev2/full-example.pdf} + \caption{Complete APEv2 Tag} +\end{figure} +\begin{figure}[h] + \includegraphics{figures/apev2/header-flags.pdf} + \caption{header flags} +\end{figure} + +\clearpage + +\section{APEv2 Item} +\begin{figure}[h] + \includegraphics{figures/apev2/item.pdf} +\end{figure} +\par +\noindent +\VAR{read only} indicates the item is read-only. +\VAR{item encoding} is \texttt{0} for UTF-8, +\texttt{1} for binary information, +\texttt{2} for a UTF-8-encoded external URL +and \texttt{3} is undefined. +\VAR{is header}, \VAR{contains no footer} and \VAR{contains a header} +are unused by tag items. +\VAR{item key} is a NULL-terminated ASCII string +with values from \texttt{0x20} to \texttt{0x7E}, inclusive. +\begin{table}[h] +{\relsize{-1} +\begin{tabular}{rl} +item key & meaning \\ +\hline +\texttt{Title} & music piece title \\ +\texttt{Subtitle} & title when \texttt{Title} contains the work or additional sub title \\ +\texttt{Artist} & performing artist \\ +\texttt{Album} & album name \\ +\texttt{Publisher} & record label or publisher \\ +\texttt{Conductor} & conductor \\ +\texttt{Track} & track number or track number/total tracks number \\ +\texttt{Composer} & name of the original composer \\ +\texttt{Comment} & user comment(s) \\ +\texttt{Copyright} & copyright holder \\ +\texttt{EAN} / \texttt{UPC} & EAN-13/UPC-A bar code identifier \\ +\texttt{ISBN} & ISBN number with check digit \\ +\texttt{Catalog} & catalog number \\ +\texttt{Year} & release date \\ +\texttt{Record Date} & record date \\ +\texttt{Genre} & genre \\ +\texttt{Media} & source media number/total media number \\ +\texttt{ISRC} & International Standard Recording Number \\ +\texttt{Dummy} & placeholder binary data \\ +\texttt{Cover Art (Front)} & front cover binary data \\ +\texttt{Cover Art (Back)} & back cover binary data \\ +\texttt{Cuesheet} & cuesheet binary data \\ +\end{tabular} +} +\end{table} + +\clearpage + +\subsection{APEv2 Item Examples} +\begin{figure}[h] + \includegraphics{figures/apev2/item-example1.pdf} +\end{figure} +\par +\noindent +\begin{tabular}{rl} +item key : & \texttt{"Year"} \\ +item value : & \texttt{"2012"} \\ +\end{tabular} +\begin{figure}[h] + \includegraphics{figures/apev2/item-example2.pdf} +\end{figure} +\par +\noindent +\begin{tabular}{rl} +item key : & \texttt{"Artist"} \\ +item value : & \texttt{"Artist Name"} \\ +\end{tabular} +\begin{figure}[h] + \includegraphics{figures/apev2/item-example3.pdf} +\end{figure} +\par +\noindent +\begin{tabular}{rl} + item key : & \texttt{"Title"} \\ + item value : & \texttt{"Track Name"} \\ +\end{tabular}
View file
audiotools-2.19.tar.gz/docs/reference/audioformats.template
Added
@@ -0,0 +1,25 @@ +<<file:header.tex>> +\include{introduction} +\include{basics} +\include{wav} +\include{aiff} +\include{au} +\include{shorten} +\include{flac} +\include{wavpack} +\include{apev2} +\include{ape} +\include{mp3} +\include{m4a} +\include{alac} +\include{ogg} +\include{vorbis} +\include{oggflac} +\include{speex} +\include{musepack} +\include{dvda2} +\include{freedb} +\include{musicbrainz} +\include{musicbrainz_mmd} +\include{replaygain} +<<file:footer.tex>>
View file
audiotools-2.18.tar.gz/docs/reference/basics.tex -> audiotools-2.19.tar.gz/docs/reference/basics.tex
Changed
@@ -80,7 +80,7 @@ int y = 2; \end{Verbatim} Comparisons are performed using the usual math operators -$<$, $\leq$, $=$, $\geq$, $>$. So: +$<$, $\leq$, $=$, $\neq$, $\geq$, $>$. So: \par \noindent \begin{algorithm}[H] @@ -467,7 +467,7 @@ $\VALUE \rightarrow$ \WRITE $(n - 1)$ unsigned bits\; }(\tcc*[f]{negative value}){ $1 \rightarrow$ \WRITE 1 unsigned bit\; - $2 ^ {n - 1} + \VALUE \rightarrow$ \WRITE $(n - 1)$ unsigned bits\; + $(2 ^ {n - 1} + \VALUE) \rightarrow$ \WRITE $(n - 1)$ unsigned bits\; } \EALGORITHM \par @@ -507,7 +507,7 @@ $\VALUE \rightarrow$ \WRITE $(n - 1)$ unsigned bits\; $0 \rightarrow$ \WRITE 1 unsigned bit\; }(\tcc*[f]{negative value}){ - $2 ^ {n - 1} + \VALUE \rightarrow$ \WRITE $(n - 1)$ unsigned bits\; + $(2 ^ {n - 1} + \VALUE) \rightarrow$ \WRITE $(n - 1)$ unsigned bits\; $1 \rightarrow$ \WRITE 1 unsigned bit\; } \EALGORITHM @@ -572,7 +572,7 @@ \SetKwData{VALUE}{value} \SetKwData{STOPBIT}{stop bit} \eIf{$\STOPBIT = 0$}{ - $2 ^ \text{\VALUE} - 1 \rightarrow$ \WRITE \VALUE unsigned bits\; + $(2 ^ \text{\VALUE} - 1) \rightarrow$ \WRITE \VALUE unsigned bits\; }{ $0 \rightarrow$ \WRITE \VALUE unsigned bits\; }
View file
audiotools-2.18.tar.gz/docs/reference/bitdiagram.py -> audiotools-2.19.tar.gz/docs/reference/bitdiagram.py
Changed
@@ -155,7 +155,7 @@ if (self.style is not BLANK): if (self.style == SOLID): - pdf.setDash(1,0) + pdf.setDash() elif (self.style == DASHED): pdf.setDash(6,6) elif (self.style == DOTTED): @@ -280,7 +280,7 @@ def render(self, pdf): if (self.style is not BLANK): if (self.style == SOLID): - pdf.setDash(1,0) + pdf.setDash() elif (self.style == DASHED): pdf.setDash(6,6) elif (self.style == DOTTED):
View file
audiotools-2.18.tar.gz/docs/reference/bitparse.py -> audiotools-2.19.tar.gz/docs/reference/bitparse.py
Changed
@@ -14,7 +14,7 @@ sys.exit(1) #this size of an individual bit cell, in points -BIT_WIDTH = 20 +# BIT_WIDTH = 0 BIT_HEIGHT = 30 (BORDER_NONE, BORDER_LINE, BORDER_DOTTED) = range(3) @@ -89,6 +89,8 @@ self.ne = self.nw = self.se = self.sw = (0, 0) def set_w_offset(self, x): + global BIT_WIDTH + self.nw = (x, self.nw[1]) self.sw = (x, self.sw[1]) self.ne = (x + (self.size() * BIT_WIDTH), self.ne[1]) @@ -160,15 +162,18 @@ unicode(superscript)) #draw centered name, if any - if (self.name is not None): + if ((self.name is not None) and + (pdf.stringWidth(unicode(self.name), + "DejaVu", + 6) <= (self.pt_width() * 2))): pdf.setFont("DejaVu", 6) pdf.drawCentredString(self.nw[0] + (self.pt_width() / 2), - self.se[1] + 2, + self.se[1] + 2, unicode(self.name)) pdf.setStrokeColorRGB(0.0, 0.0, 0.0, 1.0) #draw top and bottom borders - pdf.setDash(1, 0) + pdf.setDash() pdf.line(self.nw[0], self.nw[1], self.ne[0], self.ne[1]) pdf.line(self.sw[0], self.sw[1], @@ -176,7 +181,7 @@ #draw left and right borders, if any if (self.w_border == BORDER_LINE): - pdf.setDash(1, 0) + pdf.setDash() pdf.line(self.nw[0], self.nw[1], self.sw[0], self.sw[1]) elif (self.w_border == BORDER_DOTTED): @@ -185,7 +190,7 @@ self.sw[0], self.sw[1]) if (self.e_border == BORDER_LINE): - pdf.setDash(1, 0) + pdf.setDash() pdf.line(self.ne[0], self.ne[1], self.se[0], self.se[1]) elif (self.e_border == BORDER_DOTTED): @@ -340,7 +345,7 @@ pdf.setStrokeColorRGB(0.0, 0.0, 0.0, 1.0) #drop top and bottom borders - pdf.setDash(1, 0) + pdf.setDash() pdf.line(self.nw[0], self.nw[1], self.ne[0], self.ne[1]) pdf.line(self.sw[0], self.sw[1], @@ -348,7 +353,7 @@ #draw left and right borders, if any if (self.w_border == BORDER_LINE): - pdf.setDash(1, 0) + pdf.setDash() pdf.line(self.nw[0], self.nw[1], self.sw[0], self.sw[1]) elif (self.w_border == BORDER_DOTTED): @@ -357,7 +362,7 @@ self.sw[0], self.sw[1]) if (self.e_border == BORDER_LINE): - pdf.setDash(1, 0) + pdf.setDash() pdf.line(self.ne[0], self.ne[1], self.se[0], self.se[1]) elif (self.e_border == BORDER_DOTTED): @@ -487,7 +492,7 @@ pdf.setStrokeColorRGB(0.0, 0.0, 0.0, 1.0) #draw top and bottom borders - pdf.setDash(1, 0) + pdf.setDash() pdf.line(self.nw[0], self.nw[1], self.ne[0], self.ne[1]) pdf.line(self.sw[0], self.sw[1], @@ -495,7 +500,7 @@ #draw left and right borders, if any if (self.w_border == BORDER_LINE): - pdf.setDash(1, 0) + pdf.setDash() pdf.line(self.nw[0], self.nw[1], self.sw[0], self.sw[1]) elif (self.w_border == BORDER_DOTTED): @@ -504,7 +509,7 @@ self.sw[0], self.sw[1]) if (self.e_border == BORDER_LINE): - pdf.setDash(1, 0) + pdf.setDash() pdf.line(self.ne[0], self.ne[1], self.se[0], self.se[1]) elif (self.e_border == BORDER_DOTTED): @@ -722,6 +727,8 @@ x_position = x_offset remaining_bits = bits_per_row else: + global BIT_WIDTH + #populate the chunk's east/west positions chunk.set_w_offset(x_position) @@ -853,7 +860,8 @@ if (part.nodeName == u'field'): bits.append(Bits( - name=part.childNodes[0].data.strip(), + name=(part.childNodes[0].data.strip() + if len(part.childNodes) else u""), bits=bits_converter(part.getAttribute(u"size"), part.getAttribute(u"value")), init=init_converter(part), @@ -862,7 +870,8 @@ background_color=background_color)) elif (part.nodeName == u'text'): bits.append(Text( - name=part.childNodes[0].data.strip(), + name=(part.childNodes[0].data.strip() + if len(part.childNodes) else u""), bit_count=int_converter(part.getAttribute(u"size")), e_border=get_border(part, u"border_e"), w_border=get_border(part, u"border_w"), @@ -870,7 +879,8 @@ elif (part.nodeName == u'bytes'): try: bits.append(Bytes( - name=part.childNodes[0].data.strip(), + name=(part.childNodes[0].data.strip() + if len(part.childNodes) else u""), bytes_list=byte_converter(part.getAttribute(u"value")), e_border=get_border(part, u"border_e"), w_border=get_border(part, u"border_w"), @@ -892,14 +902,20 @@ type='int', default=16) parser.add_option('-w','--width',dest='width', type='int', default=6 * 72, - help='digram width, in PostScript points') + help='diagram width, in PostScript points') parser.add_option('-t','--type',dest='type', choices=("pdf", "svg"), help="type of output file", default="pdf") + parser.add_option('--bit-width', dest='bit_width', + type='int', default=20, + help='width of each bit value') (options,args) = parser.parse_args() + global BIT_WIDTH + BIT_WIDTH = options.bit_width + x_offset = (options.width - (options.bits_per_row * BIT_WIDTH)) / 2 table = ChunkTable(list(align_rows(chunks_to_rows(
View file
audiotools-2.19.tar.gz/docs/reference/byteparse.py
Added
@@ -0,0 +1,487 @@ +#!/usr/bin/python + +import sys +import re + +try: + from reportlab.pdfgen import canvas + from reportlab.lib.units import inch + from reportlab.pdfbase.pdfmetrics import registerFont + from reportlab.pdfbase.ttfonts import TTFont +except ImportError: + print "*** ReportLab is required" + print "Please fetch the open-source version from http://www.reportlab.org" + sys.exit(1) + +HEX_WIDTH = 16 +HEX_HEIGHT = 20 +ASCII_WIDTH = 8 +ASCII_HEIGHT = 20 +FONT_SIZE = 10 +LABEL_FONT_SIZE = 6 +S_OFFSET = 10 +LABEL_S_OFFSET = 2 + +(BORDER_NONE, BORDER_LINE, BORDER_DOTTED) = range(3) + +class RGB_Color: + RGB = re.compile(r'^#([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})$') + RGBA = re.compile(r'^#([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})' + + r'([0-9A-Fa-f]{2})$') + + COLOR_TABLE = {u"red":(1.0, 0.0, 0.0), + u"orange":(1.0, 0.4, 0.0), + u"yellow":(1.0, 1.0, 0.0), + u"green":(0.0, 1.0, 0.0), + u"blue":(0.0, 0.0, 1.0), + u"aqua":(0.0, 1.0, 1.0), + u"black":(0.0, 0.0, 0.0), + u"fuchsia":(1.0, 0.0, 1.0), + u"gray":(0.5, 0.5, 0.5), + u"lime":(0.0, 1.0, 0.0), + u"maroon":(0.5, 0.0, 0.0), + u"navy":(0.0, 0.0, 0.5), + u"olive":(0.5, 0.5, 0.0), + u"purple":(0.5, 0.0, 0.5), + u"silver":(0.75, 0.75, 0.75), + u"teal":(0.0, 0.5, 0.5), + u"white":(1.0, 1.0, 1.0)} + + def __init__(self, red, green, blue, alpha=None): + """all should be floats between 0.0 and 1.0""" + + self.red = red + self.green = green + self.blue = blue + self.alpha = alpha + + @classmethod + def from_string(cls, s): + if (s in cls.COLOR_TABLE): + (r, g, b) = cls.COLOR_TABLE[s] + return cls(red=r, green=g, blue=b, alpha=1.0) + else: + rgb = cls.RGB.match(s) + if (rgb is not None): + return cls(red=int(rgb.group(1), 16) / 255.0, + green=int(rgb.group(2), 16) / 255.0, + blue=int(rgb.group(3), 16) / 255.0) + else: + rgba = cls.RGBA.match(s) + if (rgba is not None): + return cls(red=int(rgba.group(1), 16) / 255.0, + green=int(rgba.group(2), 16) / 255.0, + blue=int(rgba.group(3), 16) / 255.0, + alpha=int(rgba.group(4), 16) / 255.0) + else: + raise ValueError("invalid color string %s" % (repr(s))) + +class HexChunk: + def __init__(self, digits, label=None, background_color=None, + w_border=BORDER_NONE, e_border=BORDER_NONE): + """digits is a list of 2 byte unicode strings for each hex pair + background_color is an RGB_Color object + w_border and e_border are one of the BORDER_* enum values""" + + import string + + hexdigits = string.hexdigits.decode('ascii') + for pair in digits: + assert(len(pair) == 2) + if (pair != u" "): + assert(pair[0] in hexdigits) + assert(pair[1] in hexdigits) + + self.digits = digits + self.label = label + self.background_color = background_color + self.w_border = w_border + self.e_border = e_border + + #the chunk's location in the PDF, in x,y point pairs + self.ne = self.nw = self.se = self.sw = (0, 0) + + def __repr__(self): + return "HexChunk(%s)" % \ + ",".join(["%s=%s" % (attr, repr(getattr(self, attr))) + for attr in ["digits", + "label", + "background_color", + "w_border", + "e_border"]]) + + def size(self): + """returns the number of hex digits in the chunk""" + + return len(self.digits) + + def set_w_offset(self, w): + """given an west position in points, + calculates the x positions of all 4 corners""" + + self.nw = (w, self.nw[1]) + self.sw = (w, self.sw[1]) + self.ne = (w + (self.size() * HEX_WIDTH), self.ne[1]) + self.se = (w + (self.size() * HEX_WIDTH), self.se[1]) + + def set_s_offset(self, s): + """given an south position in points, + calculates the y positions of all 4 corners""" + + self.nw = (self.nw[0], s + HEX_HEIGHT) + self.sw = (self.sw[0], s) + self.ne = (self.ne[0], s + HEX_HEIGHT) + self.se = (self.se[0], s) + + def split(self, digits): + """returns 2 objects, the first containing up to "digits" + and the second containing the remainder""" + + return (HexChunk(digits=self.digits[0:digits], + label=self.label, + background_color=self.background_color, + w_border=self.w_border, + e_border=BORDER_NONE), + HexChunk(digits=self.digits[digits:], + label=self.label, + background_color=self.background_color, + w_border=BORDER_NONE, + e_border=self.e_border)) + + def pt_width(self): + return abs(self.nw[0] - self.ne[0]) + + def pt_height(self): + return abs(self.nw[1] - self.sw[1]) + + def pts_per_cell(self): + return self.pt_width() / float(len(self.digits)) + + def cells(self): + return iter(self.digits) + + def to_pdf(self, pdf): + pts_per_string = self.pts_per_cell() + pt_offset = self.nw[0] + (pts_per_string / 2) + + #draw background color, if any + if (self.background_color is not None): + pdf.setFillColorRGB(r=self.background_color.red, + g=self.background_color.green, + b=self.background_color.blue, + alpha=self.background_color.alpha) + pdf.rect(self.sw[0], self.sw[1], self.pt_width(), self.pt_height(), + stroke=0, fill=1) + + pdf.setFillColorRGB(0.0, 0.0, 0.0, 1.0) + pdf.setFont("Courier", FONT_SIZE) + for (i, s) in enumerate(self.cells()): + #draw individual cells + pdf.drawCentredString((i * pts_per_string) + pt_offset, + self.se[1] + S_OFFSET, + unicode(s)) + + #draw label, if any + if ((self.label is not None) and + (pdf.stringWidth(unicode(self.label), + "DejaVu", + LABEL_FONT_SIZE) <= (self.pt_width() * 2))): + pdf.setFont("DejaVu", LABEL_FONT_SIZE) + pdf.drawCentredString(self.nw[0] + (self.pt_width() / 2), + self.se[1] + LABEL_S_OFFSET, + unicode(self.label)) + + #draw top and bottom borders + pdf.setStrokeColorRGB(0.0, 0.0, 0.0, 1.0) + pdf.setDash() + pdf.line(self.nw[0], self.nw[1], + self.ne[0], self.ne[1]) + pdf.line(self.sw[0], self.sw[1], + self.se[0], self.se[1]) + + #draw left and right borders, if any + if (self.w_border == BORDER_LINE): + pdf.setDash() + pdf.line(self.nw[0], self.nw[1], + self.sw[0], self.sw[1]) + elif (self.w_border == BORDER_DOTTED): + pdf.setDash(1, 6) + pdf.line(self.nw[0], self.nw[1], + self.sw[0], self.sw[1]) + + if (self.e_border == BORDER_LINE): + pdf.setDash() + pdf.line(self.ne[0], self.ne[1], + self.se[0], self.se[1]) + elif (self.e_border == BORDER_DOTTED): + pdf.setDash(1, 6) + pdf.line(self.ne[0], self.ne[1], + self.se[0], self.se[1]) + + +class HexChunkTable: + def __init__(self, width, rows=None): + """width is the number of hex digits per row""" + + self.width = width + if (rows is None): + self.rows = [] # a list of HexChunk object lists per row + else: + self.rows = rows + + def size(self): + """returns the size of the largest row, in digits/chars""" + + return max([sum([col.size() for col in row]) + for row in self.rows if (len(row) > 0)]) + + def __repr__(self): + return "HexChunkTable(%s, %s)" % (repr(self.width), + repr(self.rows)) + + def add_value(self, digits, label=None, background_color=None): + """digits is a list of hex unicode strings + label is an optional unicode string + background_color is an optional RGB_Color object""" + + self.add_chunk(HexChunk(digits=digits, + label=label, + background_color=background_color, + w_border=BORDER_LINE, + e_border=BORDER_LINE)) + + def add_chunk(self, chunk): + """chunk is a Chunk object to be added""" + + if (len(self.rows) == 0): + #no current rows, so start a new one + self.rows.append([]) + self.add_chunk(chunk) + else: + remaining_space = (self.width - + sum([c.size() for c in self.rows[-1]])) + if (remaining_space == 0): + #last row is filled, so start a new one + self.rows.append([]) + self.add_chunk(chunk) + elif (chunk.size() > remaining_space): + #chunk is too big to fit into row, + #so split chunk and add as much as possible + (head, tail) = chunk.split(remaining_space) + self.rows[-1].append(head) + self.rows.append([]) + self.add_chunk(tail) + else: + #room remains in row, so add as-is + self.rows[-1].append(chunk) + + def pt_width(self): + return max([row[-1].ne[0] - row[0].nw[0] for row in self.rows]) + + def pt_height(self): + return sum([row[0].pt_height() for row in self.rows if len(row) > 0]) + + def set_w_offset(self, w): + for row in self.rows: + if (len(row) > 0): + offset = 0 + for col in row: + col.set_w_offset(offset + w) + offset += col.pt_width() + + def set_s_offset(self, s): + offset = 0 + for row in reversed(self.rows): + if (len(row) > 0): + for col in row: + col.set_s_offset(offset + s) + offset += row[0].pt_height() + + def to_pdf(self, pdf): + for row in self.rows: + for col in row: + col.to_pdf(pdf) + + +class ASCIIChunk(HexChunk): + def __init__(self, chars, background_color=None, + w_border=BORDER_NONE, e_border=BORDER_NONE): + + # asciidigits = frozenset([unichr(i) for i in range(0x20, 0x7F)]) + # for char in chars: + # assert(len(char) == 1) + # assert(char[0] in asciidigits) + + self.chars = chars + self.label = None + self.background_color = background_color + self.w_border = w_border + self.e_border = e_border + + #the chunk's location in the PDF, in x,y point pairs + self.ne = self.nw = self.se = self.sw = (0, 0) + + def __repr__(self): + return "ASCIIChunk(%s)" % \ + ",".join(["%s=%s" % (attr, repr(getattr(self, attr))) + for attr in ["chars", + "background_color", + "w_border", + "e_border"]]) + + def size(self): + """returns the number of ASCII characters in the chunk""" + + return len(self.chars) + + def set_w_offset(self, w): + """given an west position in points, + calculates the x positions of all 4 corners""" + + self.nw = (w, self.nw[1]) + self.sw = (w, self.sw[1]) + self.ne = (w + (self.size() * ASCII_WIDTH), self.ne[1]) + self.se = (w + (self.size() * ASCII_WIDTH), self.se[1]) + + def set_s_offset(self, s): + """given an south position in points, + calculates the y positions of all 4 corners""" + + self.nw = (self.nw[0], s + ASCII_HEIGHT) + self.sw = (self.sw[0], s) + self.ne = (self.ne[0], s + ASCII_HEIGHT) + self.se = (self.se[0], s) + + def pts_per_cell(self): + return self.pt_width() / float(len(self.chars)) + + def cells(self): + return iter(self.chars) + + def split(self, digits): + """returns 2 objects, the first containing up to "digits" + and the second containing the remainder""" + + return (ASCIIChunk(chars=self.chars[0:digits], + background_color=self.background_color, + w_border=self.w_border, + e_border=BORDER_NONE), + ASCIIChunk(chars=self.chars[digits:], + background_color=self.background_color, + w_border=BORDER_NONE, + e_border=self.e_border)) + +class ASCIIChunkTable(HexChunkTable): + def __repr__(self): + return "ASCIIChunkTable(%s, %s)" % (repr(self.width), + repr(self.rows)) + + def add_value(self, chars, background_color=None): + self.add_chunk(ASCIIChunk(chars=chars, + background_color=background_color, + w_border=BORDER_LINE, + e_border=BORDER_LINE)) + +def populate_tables(xml_filename, hex_table, ascii_table): + import xml.dom.minidom + + dom = xml.dom.minidom.parse(xml_filename) + struct = dom.getElementsByTagName(u"bytestruct")[0] + + for part in struct.childNodes: + if (part.nodeName == u"field"): + if (part.hasAttribute(u"background-color")): + background_color = RGB_Color.from_string( + part.getAttribute(u"background-color")) + else: + background_color = None + + if (part.hasAttribute(u"label")): + label = part.getAttribute(u"label") + else: + label = None + + hexvalue = part.childNodes[0].data.strip() + if (len(hexvalue) % 2): + raise ValueError("hex value must be divisible by 2") + else: + hex_digits = [] + ascii_digits = [] + while (len(hexvalue) > 0): + value = int(hexvalue[0:2], 16) + hexvalue = hexvalue[2:] + hex_digits.append(u"%2.2X" % (value)) + if (value in range(0x20, 0x7F)): + ascii_digits.append(unichr(value)) + else: + ascii_digits.append(u"\u00B7") + + hex_table.add_value(digits=hex_digits, + label=label, + background_color=background_color) + ascii_table.add_value(chars=ascii_digits, + background_color=background_color) + + +if (__name__ == "__main__"): + import optparse + + parser = optparse.OptionParser() + parser.add_option('-i','--input',dest='input',help='input XML file') + parser.add_option('-o','--output',dest='output',help='output file') + parser.add_option('-d', '--digits-per-row', dest='digits_per_row', + type='int', default=16) + parser.add_option('-w','--width',dest='width', + type='int', default=6 * 72, + help='digram width, in PostScript points') + parser.add_option( + '-s', '--space', dest='space', + type='int', default=30, + help='space between hex and ASCII sections, in PostScript points') + parser.add_option('--no-ascii', + dest='ascii', + action='store_false', + default=True) + parser.add_option('-t','--type',dest='type', + choices=("pdf",), + help="type of output file", + default="pdf") + + (options,args) = parser.parse_args() + + hex_table = HexChunkTable(options.digits_per_row) + ascii_table = ASCIIChunkTable(options.digits_per_row) + + populate_tables(options.input, hex_table, ascii_table) + + if (options.ascii): + diagram_width = ((hex_table.size() * HEX_WIDTH) + + options.space + + (ascii_table.size() * ASCII_WIDTH)) + else: + diagram_width = (hex_table.size() * HEX_WIDTH) + + x_offset = (options.width - diagram_width) / 2 + + hex_table.set_w_offset(x_offset) + hex_table.set_s_offset(0) + + if (options.ascii): + ascii_table.set_w_offset(x_offset + + hex_table.pt_width() + + options.space) + ascii_table.set_s_offset(0) + + if (options.type == 'pdf'): + registerFont(TTFont("DejaVu", "DejaVuSans.ttf")) + pdf = canvas.Canvas(options.output) + pdf.setPageSize((options.width, + hex_table.pt_height())) + hex_table.to_pdf(pdf) + if (options.ascii): + ascii_table.to_pdf(pdf) + pdf.showPage() + pdf.save() + else: + print >>sys.stderr,"unknown output type" + sys.exit(1)
View file
audiotools-2.19.tar.gz/docs/reference/dvda-codec.template
Added
@@ -0,0 +1,3 @@ +<<file:header.tex>> +\include{dvda2} +<<file:footer.tex>>
View file
audiotools-2.18.tar.gz/docs/reference/dvda2.tex -> audiotools-2.19.tar.gz/docs/reference/dvda2.tex
Changed
@@ -299,7 +299,11 @@ $\text{\SUBSTREAMCOUNT} \leftarrow$ \hyperref[mlp:readmajorsync]{read major sync}\; \ASSERT $(\SUBSTREAMCOUNT = 1)$ \OR $(\SUBSTREAMCOUNT = 2)$\; \For{$s \leftarrow 0$ \emph{\KwTo}\SUBSTREAMCOUNT}{ - $(\text{\CHECKDATA}_s~,~\text{\SUBSTREAMEND}_s) \leftarrow$ \hyperref[mlp:read_substream_info]{read substream info}\; + $\left.\begin{tabular}{r} + $\text{\CHECKDATA}_s$ \\ + $\text{\SUBSTREAMEND}_s$ \\ + \end{tabular}\right\rbrace \leftarrow$ + \hyperref[mlp:read_substream_info]{read substream info}\; } \eIf{$\text{\CHECKDATA}_0 = 1$}{ $\text{\SUBSTREAM}_0 \leftarrow$ \hyperref[mlp:decode_substream]{decode next $(\text{\SUBSTREAMEND}_0 - 2)$ substream bytes}\; @@ -320,7 +324,8 @@ $\text{\CRC}_1 \leftarrow$ \READ 8 unsigned bits\; \hyperref[mlp:verify_parity]{verify $\text{\PARITY}_1$, $\text{\CRC}_1$ of $\text{\SUBSTREAM}_1$}\; }{ - \hyperref[mlp:decode_substream]{decode next $(\text{\SUBSTREAMEND}_1 - \text{\SUBSTREAMEND}_0)$ bytes as $\text{\SUBSTREAM}_1$}\; + $\text{\SUBSTREAM}_1 \leftarrow$ + \hyperref[mlp:decode_substream]{decode next $(\text{\SUBSTREAMEND}_1 - \text{\SUBSTREAMEND}_0)$ bytes}\; } \hyperref[mlp:rematrixing]{rematrix channels using $\text{\RESTARTHEADER}_1$ and $\text{\DECODINGPARAMS}_1$}\; \hyperref[mlp:output_shifts]{apply output shifts using $\text{\DECODINGPARAMS}_1$}\; @@ -445,7 +450,10 @@ $\text{\CHECKDATA} \leftarrow$ \READ 1 unsigned bit\; \SKIP 1 bit\; $\text{\SUBSTREAMEND} \leftarrow (\text{\READ 12 unsigned bits}) \times 2$\; -\Return $(\text{\CHECKDATA}~,~\text{\SUBSTREAMEND})$\; +\Return $\left\lbrace\begin{tabular}{l} +\CHECKDATA \\ +\SUBSTREAMEND \\ +\end{tabular}\right.$\; \EALGORITHM } \begin{figure}[h] @@ -526,7 +534,11 @@ $\text{\DECODINGPARAMS}_s \leftarrow$ from previous block's decoding parameters in substream\; } \BlankLine -$(\text{\BYPASSEDLSB}_{s}~,~\text{\RESIDUAL}_s) \leftarrow$ \hyperref[mlp:read_residuals]{read residual data based on $\text{\DECODINGPARAMS}_s$}\; +$\left.\begin{tabular}{r} + $\text{\BYPASSEDLSB}_s$ \\ + $\text{\RESIDUAL}_s$ \\ +\end{tabular}\right\rbrace \leftarrow$ +\hyperref[mlp:read_residuals]{read residual data based on $\text{\DECODINGPARAMS}_s$}\; \BlankLine \For{$c \leftarrow \text{\MINCHANNEL}_s$ \emph{\KwTo}$\text{\MAXCHANNEL}_s$}{ $\text{\FILTERED}_{s~c} \leftarrow$ \hyperref[mlp:channel_filtering]{filter $\text{\RESIDUAL}_s$ using $\text{\FILTERPARAMS}_{s~c}$}\; @@ -1161,7 +1173,10 @@ } } \BlankLine -\Return $(\text{\BYPASSEDLSB}_{s}~,~\text{\RESIDUAL}_s)$\; +\Return $\left\lbrace\begin{tabular}{l} +$\text{\BYPASSEDLSB}_s$ \\ +$\text{\RESIDUAL}_s$ \\ +\end{tabular}\right.$\; \EALGORITHM } @@ -1283,10 +1298,14 @@ $v_i \leftarrow \MASK(\text{\SHIFTEDSUM}_i + \text{\RESIDUAL}_{s~c~i}~,~\text{\QUANTSIZE}_{s~c})$\; \BlankLine $\text{\FILTERED}_{s~c~i} \leftarrow v_i$\; - $\text{\FIRSTATE}_{s~c~\LEN(\text{\FIRSTATE})} \leftarrow v_i$\; - $\text{\IIRSTATE}_{s~c~\LEN(\text{\IIRSTATE})} \leftarrow v_i - \text{\SHIFTEDSUM}_i$\; + $\text{\FIRSTATE}'_{s~c~\LEN(\text{\FIRSTATE})} \leftarrow v_i$\; + $\text{\IIRSTATE}'_{s~c~\LEN(\text{\IIRSTATE})} \leftarrow v_i - \text{\SHIFTEDSUM}_i$\; } -\Return $(\text{\FILTERED}_{s~c}~,~\text{\FIRSTATE}_{s~c}~,~\text{\IIRSTATE}_{s~c})$\; +\Return $\left\lbrace\begin{tabular}{l} +$\text{\FILTERED}_{s~c}$ \\ +$\text{\FIRSTATE}'_{s~c}$ \\ +$\text{\IIRSTATE}'_{s~c}$ \\ +\end{tabular}\right.$\; \EALGORITHM } \vskip .25in @@ -1331,7 +1350,7 @@ $\text{\SHIFTED}_i \leftarrow \lfloor\text{\NOISEGENSEED}_s \div 2 ^ 7\rfloor \bmod{2 ^ {16}}$\; $\text{\NOISE}_{0~i} \leftarrow \CROP(\lfloor\text{\NOISEGENSEED}_s \div 2 ^ {15}\rfloor) \times 2 ^ {\text{\NOISESHIFT}_s}$\; $\text{\NOISE}_{1~i} \leftarrow \CROP(\text{\SHIFTED}_i) \times 2 ^ {\text{\NOISESHIFT}_s}$\; - $\text{\NOISEGENSEED}_s \leftarrow ((\text{\NOISEGENSEED}_s \times 2 ^ {16}) \bmod{2 ^ {32}}) \xor \text{\SHIFTED}_i \xor (\text{\SHIFTED}_i \times 2 ^ 5)$\; + $\text{\NOISEGENSEED}'_s \leftarrow ((\text{\NOISEGENSEED}_s \times 2 ^ {16}) \bmod{2 ^ {32}}) \xor \text{\SHIFTED}_i \xor (\text{\SHIFTED}_i \times 2 ^ 5)$\; } \BlankLine \For(\tcc*[f]{perform channel rematrixing}){$m \leftarrow 0$ \emph{\KwTo}$\text{\MATRIXCOUNT}_s$}{ @@ -1346,7 +1365,10 @@ $\text{\FILTERED}_{s~o~i} \leftarrow \MASK(\lfloor\text{\SUM}_i \div 2 ^ {14}\rfloor~,~\text{\QUANTSTEPSIZE}_{s~o}) + \text{\BYPASSEDLSB}_{s~m~i}$\; } } -\Return $(\text{\FILTERED}_{s}~,~\text{\NOISEGENSEED}_s)$\; +\Return $\left\lbrace\begin{tabular}{l} +$\text{\FILTERED}_s$ \\ +$\text{\NOISEGENSEED}'_s$ \\ +\end{tabular}\right.$\; \EALGORITHM } \vskip .25in
View file
audiotools-2.19.tar.gz/docs/reference/figures/ape/descriptor-example.bypx
Added
@@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="ID">4d414320</field> + <field label="version">960f0000</field> + <field label="descriptor length">34000000</field> + <field label="header length">18000000</field> + <field label="seektable length">04000000</field> + <field label="wave header length">2c000000</field> + <field label="frames length">2c00000000000000</field> + <field label="wave footer length">00000000</field> + <field label="MD5 sum">41486031f2b6ebaa6f513e46c46396e6</field> +</bytestruct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/ape/descriptor.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/ape/descriptor.bdx
Changed
@@ -1,21 +1,38 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="31" width=".5">ID (`MAC' 0x4D414320)</col> - <col start="32" end="63" width=".5">Version</col> + <col start="0" end="415" width=".125" id="descriptor">Descriptor</col> + <col start="416" end="607" width=".125">Header</col> + <col width=".125" style="dashed">Seektable</col> + <col width=".20" style="dashed">Wave Header</col> + <col width=".10">Frames</col> + <col width=".20" style="dashed">Wave Footer</col> + <col width=".125" style="dashed">APEv2 Tag</col> </row> + <spacer/> <row> - <col start="64" end="95" width=".333333">Descriptor Bytes</col> - <col start="96" end="127" width=".333333">Header Bytes</col> - <col start="128" end="159" width=".333333">Seektable Bytes</col> + <col start="0" end="31" width=".5" id="descriptor_s">ID (`MAC ')</col> + <col start="32" end="63" width=".5" id="descriptor_e">version (× 1000)</col> </row> <row> - <col start="160" end="191" width=".333333">Header Data Bytes</col> - <col start="192" end="223" width=".333333">Frame Data Bytes</col> - <col start="224" end="255" width=".333333">Frame Data Bytes (High)</col> + <col start="64" end="95" width=".333333">descriptor length (52)</col> + <col start="96" end="127" width=".333333">header length (24)</col> + <col start="128" end="159" width=".333333">seektable length</col> </row> <row> - <col start="256" end="287" width=".333333">Terminating Data Bytes</col> - <col start="288" end="415" width=".666666">MD5 Sum</col> + <col start="160" end="191" width=".333333">wave header length</col> + <col start="192" end="255" width=".333333">frames length</col> + <col start="256" end="287" width=".333333">wave footer length</col> </row> + <row> + <col start="288" end="415">MD5 Sum</col> + </row> + <line style="dotted"> + <start id="descriptor" corner="sw"/> + <end id="descriptor_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="descriptor" corner="se"/> + <end id="descriptor_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/ape/header-example.bypx
Added
@@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="compr.">d007</field> + <field label="flags">0000</field> + <field label="blocks per frame">00200100</field> + <field label="final frame blocks">19000000</field> + <field label="total frames">01000000</field> + <field label="bps">1000</field> + <field label="channels">0100</field> + <field label="sample rate">44ac0000</field> +</bytestruct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/ape/header.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/ape/header.bdx
Changed
@@ -1,19 +1,35 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="15" width=".5">Compression Level</col> - <col start="16" end="31" width=".5">Format Flags</col> + <col start="0" end="415" width=".125">Descriptor</col> + <col start="416" end="607" width=".125" id="header">Header</col> + <col width=".125" style="dashed">Seektable</col> + <col width=".20" style="dashed">Wave Header</col> + <col width=".10">Frames</col> + <col width=".20" style="dashed">Wave Footer</col> + <col width=".125" style="dashed">APEv2 Tag</col> </row> + <spacer/> <row> - <col start="32" end="63" width=".5">Blocks Per Frame</col> - <col start="64" end="95" width=".5">Final Frame Blocks</col> + <col start="416" end="431" width=".5" id="header_s">compression level</col> + <col start="432" end="447" width=".5" id="header_e">format flags</col> </row> <row> - <col start="96" end="127" width=".5">Total Frames</col> - <col start="128" end="143" width=".5">Bits Per Sample</col> + <col start="448" end="479" width=".333333">blocks per frame</col> + <col start="480" end="511" width=".333333">final frame blocks</col> + <col start="512" end="543" width=".333333">total frames</col> </row> <row> - <col start="144" end="159" width=".5">Channels</col> - <col start="160" end="191" width=".5">Sample Rate</col> + <col start="544" end="559" width=".333333">bits per sample</col> + <col start="560" end="575" width=".333333">channels</col> + <col start="576" end="607" width=".333333">sample rate</col> </row> + <line style="dotted"> + <start id="header" corner="sw"/> + <end id="header_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="header" corner="se"/> + <end id="header_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/ape/stream.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/ape/stream.bdx
Changed
@@ -4,10 +4,9 @@ <col start="0" end="415" width=".125">Descriptor</col> <col start="416" end="607" width=".125">Header</col> <col width=".125" style="dashed">Seektable</col> - <col width=".20" style="dashed">Header Data</col> - <col width=".10">Frame₁</col> - <col width=".10">Frame₂</col> - <col width=".10" style="dashed">...</col> - <col width=".125">APEv2 Tag</col> + <col width=".20" style="dashed">Wave Header</col> + <col width=".10">Frames</col> + <col width=".20" style="dashed">Wave Footer</col> + <col width=".125" style="dashed">APEv2 Tag</col> </row> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/apev2
Added
+(directory)
View file
audiotools-2.19.tar.gz/docs/reference/figures/apev2/footer-example.bypx
Added
@@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="preamble">4150455441474558</field> + <field label="version">d0070000</field> + <field label="tag size">63000000</field> + <field label="item count">03000000</field> + <field label="flags" background-color="#00FF0030">00000080</field> + <field label="reserved">0000000000000000</field> +</bytestruct>
View file
audiotools-2.19.tar.gz/docs/reference/figures/apev2/footer-flags.bpx
Added
@@ -0,0 +1,9 @@ +<?xml version="1.0" ?> +<struct endianness="little"> + <field size="1" value="0" background-color="#00FF0030">read only</field> + <field size="2" value="0" background-color="#00FF0030">encoding</field> + <field size="26" value="0" background-color="#00FF0030">undefined</field> + <field size="1" value="0" background-color="#00FF0030">is header</field> + <field size="1" value="0" background-color="#00FF0030">no footer</field> + <field size="1" value="1" background-color="#00FF0030">contains header</field> +</struct>
View file
audiotools-2.19.tar.gz/docs/reference/figures/apev2/full-example.bypx
Added
@@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="preamble">4150455441474558</field> + <field label="version">d0070000</field> + <field label="tag size">63000000</field> + <field label="item count">03000000</field> + <field label="flags" background-color="#0000FF30">0000000a</field> + <field label="reserved">0000000000000000</field> + <field label="tag items">04000000000000005965617200323031320b0000000000000041727469737400417274697374204e616d650a000000000000005469746c6500547261636b204e616d65</field> + <field label="preamble">4150455441474558</field> + <field label="version">d0070000</field> + <field label="tag size">63000000</field> + <field label="item count">03000000</field> + <field label="flags" background-color="#00FF0030">00000080</field> + <field label="reserved">0000000000000000</field> +</bytestruct>
View file
audiotools-2.19.tar.gz/docs/reference/figures/apev2/header-flags.bpx
Added
@@ -0,0 +1,9 @@ +<?xml version="1.0" ?> +<struct endianness="little"> + <field size="1" value="0" background-color="#0000FF30">read only</field> + <field size="2" value="0" background-color="#0000FF30">encoding</field> + <field size="26" value="0" background-color="#0000FF30">undefined</field> + <field size="1" value="1" background-color="#0000FF30">is header</field> + <field size="1" value="0" background-color="#0000FF30">no footer</field> + <field size="1" value="1" background-color="#0000FF30">contains header</field> +</struct>
View file
audiotools-2.19.tar.gz/docs/reference/figures/apev2/header.bdx
Added
@@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<diagram> + <row> + <col width=".5" start="0" end="63">preamble ('APETAGEX')</col> + <col width=".5" start="64" end="95">version (2000)</col> + </row> + <row> + <col width=".5" start="96" end="127">tag size</col> + <col width=".5" start="128" end="159">item count</col> + </row> + <row> + <col width=".5" start="160" end="191" id="flags">flags</col> + <col width=".5" start="192" end="255">reserved</col> + </row> + <spacer/> + <row> + <col start="160" end="160" width=".333333" id="flags_s">read only</col> + <col start="161" end="162" width=".333333">item encoding</col> + <col start="163" end="188" width=".333333" id="flags_e">undefined</col> + </row> + <row> + <col start="189" end="189" width=".333333">is header</col> + <col start="190" end="190" width=".333333">contains no footer</col> + <col start="191" end="191" width=".333333">contains a header</col> + </row> + <line style="dotted"> + <start id="flags" corner="sw"/> + <end id="flags_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="flags" corner="se"/> + <end id="flags_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/apev2/item-example1.bypx
Added
@@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="value size">04000000</field> + <field label="item flags">00000000</field> + <field label="item key">59656172</field> + <field label="NULL">00</field> + <field label="item value">32303132</field> +</bytestruct>
View file
audiotools-2.19.tar.gz/docs/reference/figures/apev2/item-example2.bypx
Added
@@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="value size">0b000000</field> + <field label="item flags">00000000</field> + <field label="item key">417274697374</field> + <field>00</field> + <field label="item value">417274697374204e616d65</field> +</bytestruct>
View file
audiotools-2.19.tar.gz/docs/reference/figures/apev2/item-example3.bypx
Added
@@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="value size">0a000000</field> + <field label="item flags">00000000</field> + <field label="item key">5469746c65</field> + <field label="NULL">00</field> + <field label="item value">547261636b204e616d65</field> +</bytestruct>
View file
audiotools-2.19.tar.gz/docs/reference/figures/apev2/item.bdx
Added
@@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<diagram> + <row> + <col style="dashed" end="255" start="0" width=".20">Header</col> + <col width=".20" id="item">Item₀</col> + <col width=".20">Item₁</col> + <col style="dashed" width=".20">...</col> + <col style="dashed" end="255" start="0" width=".20">Footer</col> + </row> + <spacer/> + <row> + <col width=".20" start="0" end="31" + id="item_s">value size</col> + <col width=".20" start="32" end="63" id="flags">item flags</col> + <col width=".25">item key</col> + <col width=".10" start="0" end="7">NULL</col> + <col width=".25" + start="(value size) bytes" + end="(value size) bytes" + id="item_e">item value</col> + </row> + <spacer/> + <row> + <col start="32" end="32" width=".333333" id="flags_s">read only</col> + <col start="33" end="34" width=".333333">item encoding</col> + <col start="35" end="60" width=".333333" id="flags_e">undefined</col> + </row> + <row> + <col start="61" end="61" width=".333333">is header</col> + <col start="62" end="62" width=".333333">contains no footer</col> + <col start="63" end="63" width=".333333">contains a header</col> + </row> + <line style="dotted"> + <start id="item" corner="sw"/> + <end id="item_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="item" corner="se"/> + <end id="item_e" corner="ne"/> + </line> + <line style="dotted"> + <start id="flags" corner="sw"/> + <end id="flags_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="flags" corner="se"/> + <end id="flags_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/apev2/tag.bdx
Added
@@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<diagram> + <row> + <col style="dashed" end="255" start="0" width=".20" id="header">Header</col> + <col width=".20">Item₀</col> + <col width=".20">Item₁</col> + <col style="dashed" width=".20">...</col> + <col style="dashed" end="255" start="0" width=".20" id="footer">Footer</col> + </row> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/id3v22/com-example.bypx
Added
@@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="frame ID">434f4d</field> + <field label="frame size">000011</field> + <field label="enc.">00</field> + <field label="language">656e67</field> + <field label="NULL">00</field> + <field label="comment text">636f6d6d656e742074657874</field> +</bytestruct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v22/com.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v22/com.bdx
Changed
@@ -1,8 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="23" width=".5">`COM' (0x434F4D)</col> - <col start="24" end="47" width=".5">size</col> + <col width=".111" id="frame">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="23" width=".5" id="frame_s">frame ID (`COM')</col> + <col start="24" end="47" width=".5" id="frame_e">frame size</col> </row> <row> <col start="48" end="55" width=".20">encoding</col> @@ -13,4 +25,12 @@ <row> <col>comment text</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/id3v22/frames.bdx
Added
@@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<diagram> + <row> + <col start="0" end="79" width=".40">Header</col> + <col start="80" width=".60" id="frames">ID3v2.2 Frames</col> + </row> + <spacer/> + <row> + <col width=".111" id="frames_s">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed" id="frames_e">...</col> + </row> + <spacer/> + <row> + <col start="0" end="23" width=".50" id="frame_s">frame ID</col> + <col start="24" end="47" width=".50" id="frame_e">frame size</col> + </row> + <row> + <col start="48">Frame Data</col> + </row> + <line style="dotted"> + <start id="frames" corner="sw"/> + <end id="frames_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frames" corner="se"/> + <end id="frames_e" corner="ne"/> + </line> + <line style="dotted"> + <start id="frames_s" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frames_s" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v22/geo.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v22/geo.bdx
Changed
@@ -1,8 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="23" width=".5">`GEO' (0x47454F)</col> - <col start="24" end="47" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111" id="frame">Frame₇</col> + <col width=".111" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="23" width=".5" id="frame_s">frame ID (`GEO')</col> + <col start="24" end="47" width=".5" id="frame_e">frame size</col> </row> <row> <col start="48" end="55" width=".25">text encoding</col> @@ -18,4 +30,12 @@ <row> <col>binary data</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v22/header.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v22/header.bdx
Changed
@@ -1,22 +1,36 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="23" width=".5">ID (`ID3' 0x494433)</col> - <col start="24" end="39" width=".5">version (0x0200)</col> + <col start="0" end="79" id="header" width=".40">Header</col> + <col start="80" width=".60">ID3 Frames</col> + </row> + <spacer/> + <row> + <col start="0" end="23" width=".40" id="header_s">ID (`ID3')</col> + <col start="24" end="31" width=".30">major version (2)</col> + <col start="32" end="39" width=".30" id="header_e">minor version (0)</col> </row> <row> <col start="40" end="40" width=".20">unsync</col> <col start="41" end="41" width=".20">compression</col> - <col start="42" end="47" width=".60">NULL (0x00)</col> + <col start="42" end="47" width=".60">NULL</col> </row> <row> - <col start="48" end="48" width=".08">0x00</col> - <col start="49" end="55" width=".17">size</col> - <col start="56" end="56" width=".08">0x00</col> - <col start="57" end="63" width=".17">size</col> - <col start="64" end="64" width=".08">0x00</col> - <col start="65" end="71" width=".17">size</col> - <col start="72" end="72" width=".08">0x00</col> - <col start="73" end="79" width=".17">size</col> + <col start="48" end="48" width=".08">0</col> + <col start="49" end="55" width=".17">size₃</col> + <col start="56" end="56" width=".08">0</col> + <col start="57" end="63" width=".17">size₂</col> + <col start="64" end="64" width=".08">0</col> + <col start="65" end="71" width=".17">size₁</col> + <col start="72" end="72" width=".08">0</col> + <col start="73" end="79" width=".17">size₀</col> </row> + <line style="dotted"> + <start id="header" corner="sw"/> + <end id="header_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="header" corner="se"/> + <end id="header_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/id3v22/pic-example.bypx
Added
@@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="frame ID">504943</field> + <field label="frame size">0000b0</field> + <field label="enc.">00</field> + <field label="image format">504e47</field> + <field label="type">03</field> + <field label="description">4465736372697074696f6e</field> + <field label="NULL">00</field> + <field label="picture data">89504e470d0a1a0a0000000d494844520000000a0000000c010300000061a5bee30000000467414d410000b18f0bfc610500000003504c5445ffffffa7c41bc8000000097048597300000b1300000b1301009a9c180000000774494d4507db071b0e0d06d746bcb00000000874455874436f6d6d656e7400f6cc96bf0000000b4944415408d76360200c00002400012e1e71a00000000049454e44ae426082</field> +</bytestruct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v22/pic.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v22/pic.bdx
Changed
@@ -1,8 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="23" width=".5">`PIC' (0x504943)</col> - <col start="24" end="47" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111" id="frame">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="23" width=".5" id="frame_s">frame ID (`PIC')</col> + <col start="24" end="47" width=".5" id="frame_e">frame size</col> </row> <row> <col start="48" end="55" width=".25">encoding</col> @@ -16,4 +28,12 @@ <row> <col style="dashed">picture data</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/id3v22/t__-example.bypx
Added
@@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="frame ID">545432</field> + <field label="frame size">000010</field> + <field label="enc.">00</field> + <field label="text">736f6d6520747261636b206e616d65</field> +</bytestruct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v22/t__.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v22/t__.bdx
Changed
@@ -1,11 +1,31 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="23" width=".5">`T__' (0x54____)</col> - <col start="24" end="47" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111" id="frame">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="23" width=".5" id="frame_s">frame ID (`T__')</col> + <col start="24" end="47" width=".5" id="frame_e">frame size</col> </row> <row> <col start="48" end="55" width=".25">encoding</col> <col start="56" width=".75" style="dashed">text</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v22/txx.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v22/txx.bdx
Changed
@@ -1,8 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="23" width=".5">`TXX' (0x545858)</col> - <col start="24" end="47" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111" id="frame">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="23" width=".5" id="frame_s">frame ID (`TXX')</col> + <col start="24" end="47" width=".5" id="frame_e">frame size</col> </row> <row> <col start="48" end="55" width=".25">encoding</col> @@ -12,4 +24,12 @@ <row> <col>text</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v22/ult.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v22/ult.bdx
Changed
@@ -1,8 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="23" width=".5">`ULT' (0x554C54)</col> - <col start="24" end="47" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111" id="frame">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="23" width=".5" id="frame_s">frame ID (`ULT')</col> + <col start="24" end="47" width=".5" id="frame_e">frame size</col> </row> <row> <col start="48" end="55" width=".20">encoding</col> @@ -13,4 +25,12 @@ <row> <col>lyrics text</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v22/w__.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v22/w__.bdx
Changed
@@ -1,10 +1,30 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="23" width=".5">`W__' (0x57____)</col> - <col start="24" end="47" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111" id="frame">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="23" width=".5" id="frame_s">frame ID (`W__')</col> + <col start="24" end="47" width=".5" id="frame_e">frame size</col> </row> <row> <col>URL</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v22/wxx.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v22/wxx.bdx
Changed
@@ -1,8 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="23" width=".5">`WXX' (0x575858)</col> - <col start="24" end="47" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111" id="frame">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="23" width=".5" id="frame_s">frame ID (`WXX')</col> + <col start="24" end="47" width=".5" id="frame_e">frame size</col> </row> <row> <col start="48" end="55" width=".25">encoding</col> @@ -12,4 +24,12 @@ <row> <col>URL</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/id3v23/apic-example.bypx
Added
@@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="frame ID">41504943</field> + <field label="frame size">000000B7</field> + <field label="flags">0000</field> + <field label="enc.">00</field> + <field label="MIME type">696D6167652F706E67</field> + <field label="NULL">00</field> + <field label="type">03</field> + <field label="description">4465736372697074696F6E</field> + <field label="NULL">00</field> + <field label="picture data">89504e470d0a1a0a0000000d494844520000000a0000000c010300000061a5bee30000000467414d410000b18f0bfc610500000003504c5445ffffffa7c41bc8000000097048597300000b1300000b1301009a9c180000000774494d4507db071b0e0d06d746bcb00000000874455874436f6d6d656e7400f6cc96bf0000000b4944415408d76360200c00002400012e1e71a00000000049454e44ae426082</field> +</bytestruct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v23/apic.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v23/apic.bdx
Changed
@@ -1,8 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="31" width=".5">`APIC' (0x41504943)</col> - <col start="32" end="63" width=".5">size</col> + <col width=".111" id="frame">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="31" width=".5" id="frame_s">frame ID (`APIC')</col> + <col start="32" end="63" width=".5" id="frame_e">frame size</col> </row> <row> <col start="64" end="64" width=".13">tag alter</col> @@ -27,4 +39,12 @@ <row> <col style="dashed">picture data</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/id3v23/comm-example.bypx
Added
@@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="frame ID">434f4d4d</field> + <field label="frame size">00000011</field> + <field label="flags">0000</field> + <field label="enc.">00</field> + <field label="language">656e67</field> + <field label="NULL">00</field> + <field label="comment text">436f6d6d656e742054657874</field> +</bytestruct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v23/comm.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v23/comm.bdx
Changed
@@ -1,8 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="31" width=".5">`COMM' (0x434F4D4D)</col> - <col start="32" end="63" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111" id="frame">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed" id="frames_e">...</col> + </row> + <spacer/> + <row> + <col start="0" end="31" width=".5" id="frame_s">frame ID (`COMM')</col> + <col start="32" end="63" width=".5" id="frame_e">frame size</col> </row> <row> <col start="64" end="64" width=".13">tag alter</col> @@ -23,4 +35,12 @@ <row> <col>comment text</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/id3v23/frames.bdx
Added
@@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<diagram> + <row> + <col start="0" end="79" width=".40">Header</col> + <col start="80" width=".60" id="frames">ID3v2.3 Frames</col> + </row> + <spacer/> + <row> + <col width=".111" id="frames_s">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed" id="frames_e">...</col> + </row> + <spacer/> + <row> + <col start="0" end="31" width=".5" id="frame_s">frame ID</col> + <col start="32" end="63" width=".5" id="frame_e">frame size</col> + </row> + <row> + <col start="64" end="64" width=".13">tag alter</col> + <col start="65" end="65" width=".13">file alter</col> + <col start="66" end="66" width=".13">read only</col> + <col start="67" end="71" width=".1">NULL</col> + <col start="72" end="72" width=".15">compression</col> + <col start="73" end="73" width=".13">encryption</col> + <col start="74" end="74" width=".13">grouping</col> + <col start="75" end="79" width=".1">NULL</col> + </row> + <row> + <col>Frame Data</col> + </row> + <line style="dotted"> + <start id="frames" corner="sw"/> + <end id="frames_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frames" corner="se"/> + <end id="frames_e" corner="ne"/> + </line> + <line style="dotted"> + <start id="frames_s" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frames_s" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v23/geob.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v23/geob.bdx
Changed
@@ -1,8 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="31" width=".5">`GEOB' (0x47454F42)</col> - <col start="32" end="63" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111" id="frame">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="31" width=".5" id="frame_s">frame ID (`GEOB')</col> + <col start="32" end="63" width=".5" id="frame_e">frame size</col> </row> <row> <col start="64" end="64" width=".13">tag alter</col> @@ -28,4 +40,12 @@ <row> <col>encapsulated object</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v23/header.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v23/header.bdx
Changed
@@ -1,24 +1,38 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="23" width=".5">ID (`ID3' 0x494433)</col> - <col start="24" end="39" width=".5">version (0x0300)</col> + <col start="0" end="79" id="header" width=".40">Header</col> + <col start="80" width=".60">ID3 Frames</col> + </row> + <spacer/> + <row> + <col start="0" end="23" width=".40" id="header_s">ID (`ID3')</col> + <col start="24" end="31" width=".30">major version (3)</col> + <col start="32" end="39" width=".30" id="header_e">minor version (0)</col> </row> <row> <col start="40" end="40" width=".17">unsync</col> <col start="41" end="41" width=".17">extended</col> <col start="42" end="42" width=".17">experimental</col> <col start="43" end="43" width=".17">footer</col> - <col start="44" end="47" width=".32">NULL (0x00)</col> + <col start="44" end="47" width=".32">NULL</col> </row> <row> - <col start="48" end="48" width=".08">0x00</col> - <col start="49" end="55" width=".17">size</col> - <col start="56" end="56" width=".08">0x00</col> - <col start="57" end="63" width=".17">size</col> - <col start="64" end="64" width=".08">0x00</col> - <col start="65" end="71" width=".17">size</col> - <col start="72" end="72" width=".08">0x00</col> - <col start="73" end="79" width=".17">size</col> + <col start="48" end="48" width=".08">0</col> + <col start="49" end="55" width=".17">size₃</col> + <col start="56" end="56" width=".08">0</col> + <col start="57" end="63" width=".17">size₂</col> + <col start="64" end="64" width=".08">0</col> + <col start="65" end="71" width=".17">size₁</col> + <col start="72" end="72" width=".08">0</col> + <col start="73" end="79" width=".17">size₀</col> </row> + <line style="dotted"> + <start id="header" corner="sw"/> + <end id="header_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="header" corner="se"/> + <end id="header_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/id3v23/t___-example.bypx
Added
@@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="frame ID">54495432</field> + <field label="frame size">0000000B</field> + <field label="flags">0000</field> + <field label="enc.">00</field> + <field label="text">547261636b204e616d65</field> +</bytestruct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v23/t___.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v23/t___.bdx
Changed
@@ -1,8 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="31" width=".5">`T___' (0x54______)</col> - <col start="32" end="63" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111" id="frame">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="31" width=".5" id="frame_s">frame ID (`T___')</col> + <col start="32" end="63" width=".5" id="frame_e">frame size</col> </row> <row> <col start="64" end="64" width=".13">tag alter</col> @@ -18,4 +30,12 @@ <col start="80" end="87" width=".25">encoding</col> <col start="88" width=".75">text</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v23/txxx.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v23/txxx.bdx
Changed
@@ -1,8 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="31" width=".5">`TXXX' (0x54585858)</col> - <col start="32" end="63" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111" id="frame">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="31" width=".5" id="frame_s">frame ID (`TXXX')</col> + <col start="32" end="63" width=".5" id="frame_e">frame size</col> </row> <row> <col start="64" end="64" width=".13">tag alter</col> @@ -22,4 +34,12 @@ <row> <col>text</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v23/uslt.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v23/uslt.bdx
Changed
@@ -1,26 +1,46 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="31" width=".5">`USLT' (0x55534C54)</col> - <col start="32" end="63" width=".5">Size</col> + <col width=".111">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111" id="frame">Frame₇</col> + <col width=".111" style="dashed">...</col> </row> + <spacer/> <row> - <col start="64" end="64" width=".13">Tag Alter</col> - <col start="65" end="65" width=".13">File Alter</col> - <col start="66" end="66" width=".13">Read Only</col> + <col start="0" end="31" width=".5" id="frame_s">frame ID (`USLT')</col> + <col start="32" end="63" width=".5" id="frame_e">frame size</col> + </row> + <row> + <col start="64" end="64" width=".13">tag alter</col> + <col start="65" end="65" width=".13">file alter</col> + <col start="66" end="66" width=".13">read only</col> <col start="67" end="71" width=".1">NULL</col> - <col start="72" end="72" width=".15">Compression</col> - <col start="73" end="73" width=".13">Encryption</col> - <col start="74" end="74" width=".13">Grouping</col> + <col start="72" end="72" width=".15">compression</col> + <col start="73" end="73" width=".13">encryption</col> + <col start="74" end="74" width=".13">grouping</col> <col start="75" end="79" width=".1">NULL</col> </row> <row> - <col start="80" end="87" width=".25">Encoding</col> - <col start="88" end="111" width=".25">Language</col> - <col start="112" width=".35">Description</col> + <col start="80" end="87" width=".25">encoding</col> + <col start="88" end="111" width=".25">language</col> + <col start="112" width=".35">description</col> <col start="0" end="7/15" width=".15">NULL</col> </row> <row> - <col>Lyrics Text</col> + <col>lyrics text</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v23/w___.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v23/w___.bdx
Changed
@@ -1,8 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="31" width=".5">`W___' (0x57______)</col> - <col start="32" end="63" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111" id="frame">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="31" width=".5" id="frame_s">frame ID (`W___')</col> + <col start="32" end="63" width=".5" id="frame_e">frame size</col> </row> <row> <col start="64" end="64" width=".13">tag alter</col> @@ -17,4 +29,12 @@ <row> <col start="80">URL</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v23/wxxx.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v23/wxxx.bdx
Changed
@@ -1,8 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="31" width=".5">`WXXX' (0x57585858)</col> - <col start="32" end="63" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111" id="frame">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="31" width=".5" id="frame_s">frame ID (`WXXX')</col> + <col start="32" end="63" width=".5" id="frame_e">frame size</col> </row> <row> <col start="64" end="64" width=".13">tag alter</col> @@ -22,4 +34,12 @@ <row> <col>URL</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/id3v24/apic-example.bypx
Added
@@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="frame ID">41504943</field> + <field label="frame size">00000137</field> + <field label="flags">0000</field> + <field label="enc.">00</field> + <field label="MIME type">696d6167652f706e67</field> + <field label="NULL">00</field> + <field label="type">03</field> + <field label="description">4465736372697074696f6e</field> + <field label="NULL">00</field> + <field label="picture data">89504e470d0a1a0a0000000d494844520000000a0000000c010300000061a5bee30000000467414d410000b18f0bfc610500000003504c5445ffffffa7c41bc8000000097048597300000b1300000b1301009a9c180000000774494d4507db071b0e0d06d746bcb00000000874455874436f6d6d656e7400f6cc96bf0000000b4944415408d76360200c00002400012e1e71a00000000049454e44ae426082</field> +</bytestruct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v24/apic.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v24/apic.bdx
Changed
@@ -1,20 +1,39 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="31" width=".5">`APIC' (0x41504943)</col> - <col start="32" end="63" width=".5">size</col> + <col width=".111" id="frame">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> </row> + <spacer/> <row> - <col start="64" end="64" width=".125">0x00</col> + <col width=".46" start="0" end="31" id="frame_s">frame ID (`APIC')</col> + <col start="32" end="32" width=".035">0</col> + <col start="33" end="39" width=".10">size₃</col> + <col start="40" end="40" width=".035">0</col> + <col start="41" end="47" width=".10">size₂</col> + <col start="48" end="48" width=".035">0</col> + <col start="49" end="55" width=".10">size₁</col> + <col start="56" end="56" width=".035">0</col> + <col start="57" end="63" width=".10" id="frame_e">size₀</col> + </row> + <row> + <col start="64" end="64" width=".125">NULL</col> <col start="65" end="65" width=".125">tag alter</col> <col start="66" end="66" width=".125">file alter</col> <col start="67" end="67" width=".125">read only</col> - <col start="68" end="71" width=".5">0x00</col> + <col start="68" end="71" width=".5">NULL</col> </row> <row> - <col start="72" end="72" width=".120">0x00</col> + <col start="72" end="72" width=".120">NULL</col> <col start="73" end="73" width=".125">grouping</col> - <col start="74" end="75" width=".145">0x00</col> + <col start="74" end="75" width=".145">NULL</col> <col start="76" end="76" width=".155">compression</col> <col start="77" end="77" width=".150">encryption</col> <col start="78" end="78" width=".150">unsync</col> @@ -33,4 +52,12 @@ <row> <col style="dashed">picture data</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/id3v24/comm-example.bypx
Added
@@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="frame ID">434f4d4d</field> + <field label="frame size">00000011</field> + <field label="flags">0000</field> + <field label="enc.">00</field> + <field label="language">656e67</field> + <field label="NULL">00</field> + <field label="comment text">436f6d6d656e742054657874</field> +</bytestruct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v24/comm.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v24/comm.bdx
Changed
@@ -1,20 +1,39 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="31" width=".5">`COMM' (0x434F4D4D)</col> - <col start="32" end="63" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111" id="frame">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> </row> + <spacer/> <row> - <col start="64" end="64" width=".125">0x00</col> + <col width=".46" start="0" end="31" id="frame_s">frame ID (`COMM')</col> + <col start="32" end="32" width=".035">0</col> + <col start="33" end="39" width=".10">size₃</col> + <col start="40" end="40" width=".035">0</col> + <col start="41" end="47" width=".10">size₂</col> + <col start="48" end="48" width=".035">0</col> + <col start="49" end="55" width=".10">size₁</col> + <col start="56" end="56" width=".035">0</col> + <col start="57" end="63" width=".10" id="frame_e">size₀</col> + </row> + <row> + <col start="64" end="64" width=".125">NULL</col> <col start="65" end="65" width=".125">tag alter</col> <col start="66" end="66" width=".125">file alter</col> <col start="67" end="67" width=".125">read only</col> - <col start="68" end="71" width=".5">0x00</col> + <col start="68" end="71" width=".5">NULL</col> </row> <row> - <col start="72" end="72" width=".120">0x00</col> + <col start="72" end="72" width=".120">NULL</col> <col start="73" end="73" width=".125">grouping</col> - <col start="74" end="75" width=".145">0x00</col> + <col start="74" end="75" width=".145">NULL</col> <col start="76" end="76" width=".155">compression</col> <col start="77" end="77" width=".150">encryption</col> <col start="78" end="78" width=".150">unsync</col> @@ -29,4 +48,12 @@ <row> <col>comment text</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/id3v24/frames.bdx
Added
@@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="utf-8"?> +<diagram> + <row> + <col start="0" end="79" width=".40">Header</col> + <col start="80" width=".60" id="frames">ID3v2.4 Frames</col> + </row> + <spacer/> + <row> + <col width=".111" id="frames_s">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed" id="frames_e">...</col> + </row> + <spacer/> + <row> + <col width=".46" start="0" end="31" id="frame_s">frame ID</col> + <col start="32" end="32" width=".035">0</col> + <col start="33" end="39" width=".10">size₃</col> + <col start="40" end="40" width=".035">0</col> + <col start="41" end="47" width=".10">size₂</col> + <col start="48" end="48" width=".035">0</col> + <col start="49" end="55" width=".10">size₁</col> + <col start="56" end="56" width=".035">0</col> + <col start="57" end="63" width=".10" id="frame_e">size₀</col> + </row> + <row> + <col start="64" end="64" width=".125">NULL</col> + <col start="65" end="65" width=".125">tag alter</col> + <col start="66" end="66" width=".125">file alter</col> + <col start="67" end="67" width=".125">read only</col> + <col start="68" end="71" width=".5">NULL</col> + </row> + <row> + <col start="72" end="72" width=".120">NULL</col> + <col start="73" end="73" width=".125">grouping</col> + <col start="74" end="75" width=".145">NULL</col> + <col start="76" end="76" width=".155">compression</col> + <col start="77" end="77" width=".150">encryption</col> + <col start="78" end="78" width=".150">unsync</col> + <col start="79" end="79" width=".155">data length</col> + </row> + <row> + <col>Frame Data</col> + </row> + <line style="dotted"> + <start id="frames" corner="sw"/> + <end id="frames_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frames" corner="se"/> + <end id="frames_e" corner="ne"/> + </line> + <line style="dotted"> + <start id="frames_s" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frames_s" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v24/geob.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v24/geob.bdx
Changed
@@ -1,20 +1,39 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="31" width=".5">`GEOB' (0x47454F42)</col> - <col start="32" end="63" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111" id="frame">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> </row> + <spacer/> <row> - <col start="64" end="64" width=".125">0x00</col> + <col width=".46" start="0" end="31" id="frame_s">frame ID (`GEOB')</col> + <col start="32" end="32" width=".035">0</col> + <col start="33" end="39" width=".10">size₃</col> + <col start="40" end="40" width=".035">0</col> + <col start="41" end="47" width=".10">size₂</col> + <col start="48" end="48" width=".035">0</col> + <col start="49" end="55" width=".10">size₁</col> + <col start="56" end="56" width=".035">0</col> + <col start="57" end="63" width=".10" id="frame_e">size₀</col> + </row> + <row> + <col start="64" end="64" width=".125">NULL</col> <col start="65" end="65" width=".125">tag alter</col> <col start="66" end="66" width=".125">file alter</col> <col start="67" end="67" width=".125">read only</col> - <col start="68" end="71" width=".5">0x00</col> + <col start="68" end="71" width=".5">NULL</col> </row> <row> - <col start="72" end="72" width=".120">0x00</col> + <col start="72" end="72" width=".120">NULL</col> <col start="73" end="73" width=".125">grouping</col> - <col start="74" end="75" width=".145">0x00</col> + <col start="74" end="75" width=".145">NULL</col> <col start="76" end="76" width=".155">compression</col> <col start="77" end="77" width=".150">encryption</col> <col start="78" end="78" width=".150">unsync</col> @@ -34,4 +53,12 @@ <row> <col>encapsulated object</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v24/header.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v24/header.bdx
Changed
@@ -1,8 +1,14 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="23" width=".5">ID (`ID3' 0x494433)</col> - <col start="24" end="39" width=".5">version (0x0400)</col> + <col start="0" end="79" id="header" width=".40">Header</col> + <col start="80" width=".60">ID3 Frames</col> + </row> + <spacer/> + <row> + <col start="0" end="23" width=".40" id="header_s">ID (`ID3')</col> + <col start="24" end="31" width=".30">major version (4)</col> + <col start="32" end="39" width=".30" id="header_e">minor version (0)</col> </row> <row> <col start="40" end="40" width=".17">unsync</col> @@ -12,13 +18,21 @@ <col start="44" end="47" width=".32">NULL (0x00)</col> </row> <row> - <col start="48" end="48" width=".08">0x00</col> - <col start="49" end="55" width=".17">size</col> - <col start="56" end="56" width=".08">0x00</col> - <col start="57" end="63" width=".17">size</col> - <col start="64" end="64" width=".08">0x00</col> - <col start="65" end="71" width=".17">size</col> - <col start="72" end="72" width=".08">0x00</col> - <col start="73" end="79" width=".17">size</col> + <col start="48" end="48" width=".08">0</col> + <col start="49" end="55" width=".17">size₃</col> + <col start="56" end="56" width=".08">0</col> + <col start="57" end="63" width=".17">size₂</col> + <col start="64" end="64" width=".08">0</col> + <col start="65" end="71" width=".17">size₁</col> + <col start="72" end="72" width=".08">0</col> + <col start="73" end="79" width=".17">size₀</col> </row> + <line style="dotted"> + <start id="header" corner="sw"/> + <end id="header_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="header" corner="se"/> + <end id="header_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/id3v24/size.bpx
Added
@@ -0,0 +1,11 @@ +<?xml version="1.0" ?> +<struct endianness="big"> + <field size="1" value="0">NULL</field> + <field size="7" value="0">size₃</field> + <field size="1" value="0">NULL</field> + <field size="7" value="0">size₂</field> + <field size="1" value="0">NULL</field> + <field size="7" value="1">size₁</field> + <field size="1" value="0">NULL</field> + <field size="7" value="0x37">size₀</field> +</struct>
View file
audiotools-2.19.tar.gz/docs/reference/figures/id3v24/t___-example.bypx
Added
@@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="frame ID">54495432</field> + <field label="frame size">0000000b</field> + <field label="flags">0000</field> + <field label="enc.">00</field> + <field label="text">547261636b204e616d65</field> +</bytestruct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v24/t___.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v24/t___.bdx
Changed
@@ -1,20 +1,39 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="31" width=".5">`T___' (0x54______)</col> - <col start="32" end="63" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111" id="frame">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> </row> + <spacer/> <row> - <col start="64" end="64" width=".125">0x00</col> + <col width=".46" start="0" end="31" id="frame_s">frame ID (`T___')</col> + <col start="32" end="32" width=".035">0</col> + <col start="33" end="39" width=".10">size₃</col> + <col start="40" end="40" width=".035">0</col> + <col start="41" end="47" width=".10">size₂</col> + <col start="48" end="48" width=".035">0</col> + <col start="49" end="55" width=".10">size₁</col> + <col start="56" end="56" width=".035">0</col> + <col start="57" end="63" width=".10" id="frame_e">size₀</col> + </row> + <row> + <col start="64" end="64" width=".125">NULL</col> <col start="65" end="65" width=".125">tag alter</col> <col start="66" end="66" width=".125">file alter</col> <col start="67" end="67" width=".125">read only</col> - <col start="68" end="71" width=".5">0x00</col> + <col start="68" end="71" width=".5">NULL</col> </row> <row> - <col start="72" end="72" width=".120">0x00</col> + <col start="72" end="72" width=".120">NULL</col> <col start="73" end="73" width=".125">grouping</col> - <col start="74" end="75" width=".145">0x00</col> + <col start="74" end="75" width=".145">NULL</col> <col start="76" end="76" width=".155">compression</col> <col start="77" end="77" width=".150">encryption</col> <col start="78" end="78" width=".150">unsync</col> @@ -24,4 +43,12 @@ <col start="80" end="87" width=".25">encoding</col> <col start="88" width=".75">text</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v24/txxx.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v24/txxx.bdx
Changed
@@ -1,20 +1,39 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="31" width=".5">`TXXX' (0x54585858)</col> - <col start="32" end="63" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111" id="frame">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> </row> + <spacer/> <row> - <col start="64" end="64" width=".125">0x00</col> + <col width=".46" start="0" end="31" id="frame_s">frame ID (`TXXX')</col> + <col start="32" end="32" width=".035">0</col> + <col start="33" end="39" width=".10">size₃</col> + <col start="40" end="40" width=".035">0</col> + <col start="41" end="47" width=".10">size₂</col> + <col start="48" end="48" width=".035">0</col> + <col start="49" end="55" width=".10">size₁</col> + <col start="56" end="56" width=".035">0</col> + <col start="57" end="63" width=".10" id="frame_e">size₀</col> + </row> + <row> + <col start="64" end="64" width=".125">NULL</col> <col start="65" end="65" width=".125">tag alter</col> <col start="66" end="66" width=".125">file alter</col> <col start="67" end="67" width=".125">read only</col> - <col start="68" end="71" width=".5">0x00</col> + <col start="68" end="71" width=".5">NULL</col> </row> <row> - <col start="72" end="72" width=".120">0x00</col> + <col start="72" end="72" width=".120">NULL</col> <col start="73" end="73" width=".125">grouping</col> - <col start="74" end="75" width=".145">0x00</col> + <col start="74" end="75" width=".145">NULL</col> <col start="76" end="76" width=".155">compression</col> <col start="77" end="77" width=".150">encryption</col> <col start="78" end="78" width=".150">unsync</col> @@ -28,4 +47,12 @@ <row> <col>text</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v24/uslt.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v24/uslt.bdx
Changed
@@ -1,20 +1,39 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="31" width=".5">`USLT' (0x55534C54)</col> - <col start="32" end="63" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111" id="frame">Frame₇</col> + <col width=".111" style="dashed">...</col> </row> + <spacer/> <row> - <col start="64" end="64" width=".125">0x00</col> + <col width=".46" start="0" end="31" id="frame_s">frame ID (`USLT')</col> + <col start="32" end="32" width=".035">0</col> + <col start="33" end="39" width=".10">size₃</col> + <col start="40" end="40" width=".035">0</col> + <col start="41" end="47" width=".10">size₂</col> + <col start="48" end="48" width=".035">0</col> + <col start="49" end="55" width=".10">size₁</col> + <col start="56" end="56" width=".035">0</col> + <col start="57" end="63" width=".10" id="frame_e">size₀</col> + </row> + <row> + <col start="64" end="64" width=".125">NULL</col> <col start="65" end="65" width=".125">tag alter</col> <col start="66" end="66" width=".125">file alter</col> <col start="67" end="67" width=".125">read only</col> - <col start="68" end="71" width=".5">0x00</col> + <col start="68" end="71" width=".5">NULL</col> </row> <row> - <col start="72" end="72" width=".120">0x00</col> + <col start="72" end="72" width=".120">NULL</col> <col start="73" end="73" width=".125">grouping</col> - <col start="74" end="75" width=".145">0x00</col> + <col start="74" end="75" width=".145">NULL</col> <col start="76" end="76" width=".155">compression</col> <col start="77" end="77" width=".150">encryption</col> <col start="78" end="78" width=".150">unsync</col> @@ -29,4 +48,12 @@ <row> <col>lyrics text</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v24/w___.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v24/w___.bdx
Changed
@@ -1,20 +1,39 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="31" width=".5">`W___' (0x57______)</col> - <col start="32" end="63" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111" id="frame">Frame₄</col> + <col width=".111">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> </row> + <spacer/> <row> - <col start="64" end="64" width=".125">0x00</col> + <col width=".46" start="0" end="31" id="frame_s">frame ID (`W___')</col> + <col start="32" end="32" width=".035">0</col> + <col start="33" end="39" width=".10">size₃</col> + <col start="40" end="40" width=".035">0</col> + <col start="41" end="47" width=".10">size₂</col> + <col start="48" end="48" width=".035">0</col> + <col start="49" end="55" width=".10">size₁</col> + <col start="56" end="56" width=".035">0</col> + <col start="57" end="63" width=".10" id="frame_e">size₀</col> + </row> + <row> + <col start="64" end="64" width=".125">NULL</col> <col start="65" end="65" width=".125">tag alter</col> <col start="66" end="66" width=".125">file alter</col> <col start="67" end="67" width=".125">read only</col> - <col start="68" end="71" width=".5">0x00</col> + <col start="68" end="71" width=".5">NULL</col> </row> <row> - <col start="72" end="72" width=".120">0x00</col> + <col start="72" end="72" width=".120">NULL</col> <col start="73" end="73" width=".125">grouping</col> - <col start="74" end="75" width=".145">0x00</col> + <col start="74" end="75" width=".145">NULL</col> <col start="76" end="76" width=".155">compression</col> <col start="77" end="77" width=".150">encryption</col> <col start="78" end="78" width=".150">unsync</col> @@ -23,4 +42,12 @@ <row> <col start="80">URL</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/id3v24/wxxx.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/id3v24/wxxx.bdx
Changed
@@ -1,20 +1,39 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="31" width=".5">`WXXX' (0x57585858)</col> - <col start="32" end="63" width=".5">size</col> + <col width=".111">Frame₀</col> + <col width=".111">Frame₁</col> + <col width=".111">Frame₂</col> + <col width=".111">Frame₃</col> + <col width=".111">Frame₄</col> + <col width=".111" id="frame">Frame₅</col> + <col width=".111">Frame₆</col> + <col width=".111">Frame₇</col> + <col width=".111" style="dashed">...</col> </row> + <spacer/> <row> - <col start="64" end="64" width=".125">0x00</col> + <col width=".46" start="0" end="31" id="frame_s">frame ID (`WXXX')</col> + <col start="32" end="32" width=".035">0</col> + <col start="33" end="39" width=".10">size₃</col> + <col start="40" end="40" width=".035">0</col> + <col start="41" end="47" width=".10">size₂</col> + <col start="48" end="48" width=".035">0</col> + <col start="49" end="55" width=".10">size₁</col> + <col start="56" end="56" width=".035">0</col> + <col start="57" end="63" width=".10" id="frame_e">size₀</col> + </row> + <row> + <col start="64" end="64" width=".125">NULL</col> <col start="65" end="65" width=".125">tag alter</col> <col start="66" end="66" width=".125">file alter</col> <col start="67" end="67" width=".125">read only</col> - <col start="68" end="71" width=".5">0x00</col> + <col start="68" end="71" width=".5">NULL</col> </row> <row> - <col start="72" end="72" width=".120">0x00</col> + <col start="72" end="72" width=".120">NULL</col> <col start="73" end="73" width=".125">grouping</col> - <col start="74" end="75" width=".145">0x00</col> + <col start="74" end="75" width=".145">NULL</col> <col start="76" end="76" width=".155">compression</col> <col start="77" end="77" width=".150">encryption</col> <col start="78" end="78" width=".150">unsync</col> @@ -28,4 +47,12 @@ <row> <col>URL</col> </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/m4a/alb-example.bypx
Added
@@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="alb size" background-color="#FF00FF20">00000022</field> + <field background-color="#FF00FF20">a9616c62</field> + <field label="data size" background-color="#FF00FF20">0000001a</field> + <field background-color="#FF00FF20">64617461</field> + <field label="type" background-color="#FF00FF20">00000001</field> + <field label="reserved" background-color="#FF00FF20">00000000</field> + <field label="data" background-color="#FF00FF20">416c62756d204e616d65</field> +</bytestruct>
View file
audiotools-2.19.tar.gz/docs/reference/figures/m4a/disk-example.bypx
Added
@@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="disk size" background-color="#0000FF20">0000001e</field> + <field background-color="#0000FF20">6469736b</field> + <field label="data size" background-color="#0000FF20">00000016</field> + <field background-color="#0000FF20">64617461</field> + <field label="type" background-color="#0000FF20">00000000</field> + <field label="reserved" background-color="#0000FF20">00000000</field> + <field label="NULL" background-color="#0000FF20">0000</field> + <field label="number" background-color="#0000FF20">0003</field> + <field label="total" background-color="#0000FF20">0004</field> +</bytestruct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/m4a/disk.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/m4a/disk.bdx
Changed
@@ -1,17 +1,18 @@ <?xml version="1.0" ?><diagram> <row> <col end="31" start="0" width=".25">disk length</col> - <col end="63" start="32" width=".25">`disk' (0x6469736B)</col> - <col id="data" start="64" style="dashed" width=".25">data</col> + <col end="63" start="32" width=".25">`disk'</col> + <col id="data" start="64" style="dashed" width=".50">data</col> </row> <row><blank/></row> <row> <col end="31" id="data_len" start="0" width=".25">data length</col> - <col end="63" start="32" width=".25">`data' (0x64617461)</col> - <col end="127" id="data_flags" start="64" width=".5">flags (0x00000000)</col> + <col end="63" start="32" width=".25">`data'</col> + <col start="64" end="95" width=".25">type (0)</col> + <col start="96" end="127" width=".25" id="data_flags">reserved (0)</col> </row> <row> - <col end="143" start="128" width=".333333">NULL (0x0000)</col> + <col end="143" start="128" width=".333333">NULL</col> <col end="159" start="144" width=".333333">disc number</col> <col end="175" start="160" width=".333333">total discs</col> </row>
View file
audiotools-2.18.tar.gz/docs/reference/figures/m4a/dref.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/m4a/dref.bdx
Changed
@@ -10,8 +10,8 @@ <col start="96" end="127" width=".5">number of references</col> </row> <row> - <col start="128" width=".333333" style="dashed">reference atom₁</col> - <col width=".333333" style="dashed">reference atom₂</col> + <col start="128" width=".333333" style="dashed">reference atom₀</col> + <col width=".333333" style="dashed">reference atom₁</col> <col width=".333333" style="dashed">...</col> </row> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/m4a/meta-example.bypx
Added
@@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="meta size">000001b3</field> + <field label="meta name">6d657461</field> + <field label="ver.">00</field> + <field label="flags">000000</field> + <field label="hdlr atom">0000002168646c7200000000000000006d6469726170706c000000000000000000</field> + <field label="ilst size">00000186</field> + <field label="ilst name">696c7374</field> + <field label="nam atom" background-color="#FF000020">00000022a96e616d0000001a646174610000000100000000547261636b204e616d65</field> + <field label="alb atom" background-color="#FF00FF20">00000022a9616c620000001a646174610000000100000000416c62756d204e616d65</field> + <field label="ART atom">00000023a94152540000001b646174610000000100000000417274697374204e616d65</field> + <field label="trkn atom" background-color="#00FF0020">0000002074726b6e000000186461746100000000000000000000000100020000</field> + <field label="cpil atom">000000196370696c0000001164617461000000150000000001</field> + <field label="too atom">00000035a9746f6f0000002d646174610000000100000000507974686f6e20417564696f20546f6f6c7320322e3139616c70686131</field> + <field label="day atom">0000001ca96461790000001464617461000000010000000032303132</field> + <field label="disk atom" background-color="#0000FF20">0000001e6469736b00000016646174610000000000000000000000030004</field> + <field label="covr atom">0000006f636f76720000006764617461000000000000000089504e470d0a1a0a0000000d494844520000000b0000000c080000000091c21842000000017352474200aece1ce9000000114944415408d763fccf00074c0c03cf060095c101177df59c730000000049454e44ae426082</field> +</bytestruct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/m4a/meta.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/m4a/meta.bdx
Changed
@@ -2,16 +2,70 @@ <diagram> <row> <col start="0" end="31" width=".5">meta length</col> - <col start="32" end="63" width=".5">`meta' (0x6D657461)</col> + <col start="32" end="63" width=".5">`meta'</col> </row> <row> <col start="64" end="71" width=".25">version</col> <col start="72" end="95" width=".75">flags (0x000000)</col> </row> <row> - <col start="96" width=".25">`hdlr' atom</col> - <col width=".25">`ilst' atom</col> - <col width=".25">`free' atom</col> - <col style="dashed" width=".25">...</col> + <col start="96" width=".333333">`hdlr' atom</col> + <col width=".333333" id="ilst">`ilst' atom</col> + <col width=".333333">`free' atom</col> </row> + <spacer/> + <row> + <col width=".5" id="ilst_s" start="0" end="31">ilst length</col> + <col width=".5" id="ilst_e" start="32" end="63">`ilst'</col> + </row> + <row> + <col width=".25" id="atom">Atom₀</col> + <col width=".25">Atom₀</col> + <col width=".25">Atom₀</col> + <col width=".25" style="dashed">...</col> + </row> + <spacer/> + <row> + <col width=".5" id="atom_s" start="0" end="31">atom length</col> + <col width=".5" id="atom_e" start="32" end="63">atom name</col> + </row> + <row> + <col id="data" start="64">Data Atom</col> + </row> + <spacer/> + <row> + <col start="0" end="31" width=".5" id="data_s">data length</col> + <col start="32" end="63" width=".5" id="data_e">`data'</col> + </row> + <row> + <col start="64" end="95" width=".5">type</col> + <col start="96" end="127" width=".5">reserved (0x00000000)</col> + </row> + <row> + <col start="128">data</col> + </row> + <line style="dotted"> + <start id="ilst" corner="sw"/> + <end id="ilst_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="ilst" corner="se"/> + <end id="ilst_e" corner="ne"/> + </line> + <line style="dotted"> + <start id="atom" corner="sw"/> + <end id="atom_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="atom" corner="se"/> + <end id="atom_e" corner="ne"/> + </line> + <line style="dotted"> + <start id="data" corner="sw"/> + <end id="data_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="data" corner="se"/> + <end id="data_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/m4a/nam-example.bypx
Added
@@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="nam size" background-color="#FF000020">00000022</field> + <field background-color="#FF000020">a96e616d</field> + <field label="data size" background-color="#FF000020">0000001a</field> + <field background-color="#FF000020">64617461</field> + <field label="type" background-color="#FF000020">00000001</field> + <field label="reserved" background-color="#FF000020">00000000</field> + <field label="data" background-color="#FF000020">547261636b204e616d65</field> +</bytestruct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/m4a/stco.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/m4a/stco.bdx
Changed
@@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="31" width=".5">stco Length</col> + <col start="0" end="31" width=".5">stco length</col> <col start="32" end="63" width=".5">`stco' (0x7374636F)</col> </row> <row> - <col start="64" end="71" width=".25">Version</col> - <col start="72" end="95" width=".25">Flags (0x000000)</col> - <col start="96" end="127" width=".5">Number of Offsets</col> + <col start="64" end="71" width=".25">version</col> + <col start="72" end="95" width=".25">flags (0x000000)</col> + <col start="96" end="127" width=".5">number of offsets</col> </row> <row> - <col start="128" end="159" width=".333333">Offset₁</col> - <col start="160" end="191" width=".333333">Offset₂</col> + <col start="128" end="159" width=".333333">offset₀</col> + <col start="160" end="191" width=".333333">offset₁</col> <col start="192" style="dashed" width=".333333">...</col> </row> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/m4a/stsc.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/m4a/stsc.bdx
Changed
@@ -10,16 +10,25 @@ <col start="96" end="127" width=".5">number of blocks</col> </row> <row> - <col start="128" end="159" width=".333333">first chunk₁</col> - <col start="160" end="191" width=".333333">samples per chunk₁</col> - <col start="192" end="223" width=".333333">sample duration index₁</col> + <col start="128" end="223" id="block" width=".25">Block₀</col> + <col start="224" end="319" width=".25">Block₁</col> + <col start="320" end="415" width=".25">Block₂</col> + <col style="dashed" width=".25">...</col> </row> + <spacer/> <row> - <col start="224" end="255" width=".333333">first chunk₂</col> - <col start="256" end="287" width=".333333">samples per chunk₂</col> - <col start="288" end="319" width=".333333">sample duration index₂</col> - </row> - <row> - <col start="320" style="dashed">...</col> + <col start="128" end="159" width=".333333" + id="block_s">first chunk</col> + <col start="160" end="191" width=".333333">samples per chunk</col> + <col start="192" end="223" width=".333333" + id="block_e">sample duration index</col> </row> + <line style="dotted"> + <start id="block" corner="sw"/> + <end id="block_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="block" corner="se"/> + <end id="block_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/m4a/stsd.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/m4a/stsd.bdx
Changed
@@ -10,8 +10,8 @@ <col start="96" end="127" width=".5">number of descriptions</col> </row> <row> - <col start="128" width=".333333" style="dashed">description atom₁</col> - <col width=".333333" style="dashed">description atom₂</col> + <col start="128" width=".333333" style="dashed">description atom₀</col> + <col width=".333333" style="dashed">description atom₁</col> <col width=".333333" style="dashed">...</col> </row> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/m4a/stsz.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/m4a/stsz.bdx
Changed
@@ -11,8 +11,8 @@ <col start="128" end="159" width=".35">number of block sizes</col> </row> <row> - <col start="160" end="191" width=".333333">block size₁</col> - <col start="192" end="223" width=".333333">block size₂</col> + <col start="160" end="191" width=".333333">block size₀</col> + <col start="192" end="223" width=".333333">block size₁</col> <col start="224" style="dashed" width=".333333">...</col> </row> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/m4a/stts.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/m4a/stts.bdx
Changed
@@ -10,14 +10,22 @@ <col start="96" end="127" width=".5">number of times</col> </row> <row> - <col start="128" end="159" width=".5">frame count₁</col> - <col start="160" end="191" width=".5">duration₁</col> + <col width=".25" id="time" start="128" end="191">Time₀</col> + <col width=".25" start="192" end="255">Time₁</col> + <col width=".25" start="256" end="319">Time₂</col> + <col width=".25" style="dashed">...</col> </row> + <spacer/> <row> - <col start="192" end="223" width=".5">frame count₂</col> - <col start="224" end="255" width=".5">duration₂</col> - </row> - <row> - <col start="256" style="dashed">...</col> + <col start="128" end="159" width=".5" id="time_s">frame count</col> + <col start="160" end="191" width=".5" id="time_e">duration</col> </row> + <line style="dotted"> + <start id="time" corner="sw"/> + <end id="time_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="time" corner="se"/> + <end id="time_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/m4a/trkn-example.bypx
Added
@@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="trkn size" background-color="#00FF0020">00000020</field> + <field background-color="#00FF0020">74726b6e</field> + <field label="data size" background-color="#00FF0020">00000018</field> + <field background-color="#00FF0020">64617461</field> + <field label="type" background-color="#00FF0020">00000000</field> + <field label="reserved" background-color="#00FF0020">00000000</field> + <field label="NULL" background-color="#00FF0020">0000</field> + <field label="number" background-color="#00FF0020">0001</field> + <field label="total" background-color="#00FF0020">0002</field> + <field label="NULL" background-color="#00FF0020">0000</field> +</bytestruct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/m4a/trkn.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/m4a/trkn.bdx
Changed
@@ -1,20 +1,21 @@ <?xml version="1.0" ?><diagram> <row> <col end="31" start="0" width=".25">trkn length</col> - <col end="63" start="32" width=".25">`trkn' (0x74726B6E)</col> - <col id="data" start="64" style="dashed" width=".25">data</col> + <col end="63" start="32" width=".25">`trkn'</col> + <col id="data" start="64" style="dashed" width=".50">data</col> </row> <row><blank/></row> <row> <col end="31" id="data_len" start="0" width=".25">data length</col> - <col end="63" start="32" width=".25">`data' (0x64617461)</col> - <col end="127" id="data_flags" start="64" width=".5">flags (0x00000000)</col> + <col end="63" start="32" width=".25">`data'</col> + <col start="64" end="95" width=".25">type (0)</col> + <col start="96" end="127" width=".25" id="data_flags">reserved (0)</col> </row> <row> - <col end="143" start="128" width=".25">NULL (0x0000)</col> + <col end="143" start="128" width=".25">NULL</col> <col end="159" start="144" width=".25">track number</col> <col end="175" start="160" width=".25">total tracks</col> - <col end="191" start="176" width=".25">NULL (0x0000)</col> + <col end="191" start="176" width=".25">NULL</col> </row> <line style="dotted"><start corner="sw" id="data"/><end corner="nw" id="data_len"/></line> <line style="dotted"><start corner="se" id="data"/><end corner="ne" id="data_flags"/></line>
View file
audiotools-2.18.tar.gz/docs/reference/figures/mp3/id3v1.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/mp3/id3v1.bdx
Changed
@@ -1,16 +1,18 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="23" width=".40">header (`TAG' 0x544147)</col> - <col start="24" end="263" width=".20">track title</col> - <col start="264" end="503" width=".20">artist name</col> - <col start="504" end="743" width=".20">album name</col> + <col start="0" end="23">header (`TAG')</col> </row> <row> - <col start="744" end="775" width=".20">year</col> - <col start="776" end="1015" width=".80">comment</col> + <col start="24" end="263" width=".50">track title</col> + <col start="264" end="503" width=".50">artist name</col> + </row> + <row> + <col start="504" end="743" width=".50">album name</col> + <col start="744" end="775" width=".50">year</col> </row> <row> - <col start="1016" end="1023">genre</col> + <col start="776" end="1015" width=".80">comment</col> + <col start="1016" end="1023" width=".20">genre</col> </row> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/mp3/id3v11-example.bypx
Added
@@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="header">544147</field> + <field label="track title">736f6d6520747261636b206e616d65000000000000000000000000000000</field> + <field label="artist name">6172746973742773206e616d650000000000000000000000000000000000</field> + <field label="album name">616c62756d207469746c6500000000000000000000000000000000000000</field> + <field label="year">32303132</field> + <field label="comment">61206c656e6774687920636f6d6d656e74206669656c640000000000</field> + <field label="NULL">00</field> + <field label="num">01</field> + <field label="genre">00</field> +</bytestruct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/mp3/id3v11.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/mp3/id3v11.bdx
Changed
@@ -1,18 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="23" width=".40">header (`TAG' 0x544147)</col> - <col start="24" end="263" width=".20">track title</col> - <col start="264" end="503" width=".20">artist name</col> - <col start="504" end="743" width=".20">album name</col> + <col start="0" end="23">header (`TAG')</col> + </row> + <row> + <col start="24" end="263" width=".50">track title</col> + <col start="264" end="503" width=".50">artist name</col> + </row> + <row> + <col start="504" end="743" width=".50">album name</col> + <col start="744" end="775" width=".50">year</col> </row> <row> - <col start="744" end="775" width=".20">year</col> <col start="776" end="999" width=".50">comment</col> <col start="1000" end="1007" width=".10">NULL</col> <col start="1008" end="1015" width=".20">track number</col> - </row> - <row> - <col start="1016" end="1023">genre</col> + <col start="1016" end="1023" width=".20">genre</col> </row> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/mp3/id3v2_stream-example.bpx
Added
@@ -0,0 +1,15 @@ +<?xml version="1.0" ?> +<struct endianness="big"> + <field size="24" value="0x494433">ID</field> + <field size="8" value="3">major version</field> + <field size="8" value="0">minor version</field> + <field size="8" value="0">flags</field> + <field size="1" value="0"/> + <field size="7" value="0">size₃</field> + <field size="1" value="0"/> + <field size="7" value="0">size₂</field> + <field size="1" value="0"/> + <field size="7" value="1">size₁</field> + <field size="1" value="0"/> + <field size="7" value="21">size₀</field> +</struct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/mp3/id3v2_stream.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/mp3/id3v2_stream.bdx
Changed
@@ -1,10 +1,34 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="79" width=".20">header</col> - <col width=".20">ID3v2 Frame₀</col> - <col width=".20">ID3v2 Frame₁</col> - <col width=".20" style="dashed">...</col> - <col width=".20" style="dashed">padding</col> + <col start="0" end="79" id="header" width=".40">Header</col> + <col start="80" width=".60">ID3 Frames</col> </row> + <spacer/> + <row> + <col start="0" end="23" width=".40" id="header_s">ID (`ID3')</col> + <col start="24" end="31" width=".30">major version</col> + <col start="32" end="39" width=".30" id="header_e">minor version</col> + </row> + <row> + <col start="40" end="47">version-specific flags</col> + </row> + <row> + <col start="48" end="48" width=".08">0</col> + <col start="49" end="55" width=".17">size₃</col> + <col start="56" end="56" width=".08">0</col> + <col start="57" end="63" width=".17">size₂</col> + <col start="64" end="64" width=".08">0</col> + <col start="65" end="71" width=".17">size₁</col> + <col start="72" end="72" width=".08">0</col> + <col start="73" end="79" width=".17">size₀</col> + </row> + <line style="dotted"> + <start id="header" corner="sw"/> + <end id="header_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="header" corner="se"/> + <end id="header_e" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/mp3/stream.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/mp3/stream.bdx
Changed
@@ -1,10 +1,10 @@ <?xml version="1.0" ?><diagram> <row> <col style="dashed" width=".20">ID3v2 Metadata</col> - <col id="frame" width=".20">MPEG Frame₁</col> - <col width=".20">MPEG Frame₂</col> + <col id="frame" width=".20">MPEG Frame₀</col> + <col width=".20">MPEG Frame₁</col> <col style="dashed" width=".20">...</col> - <col style="dashed" width=".20">ID3v1 Metadata</col> + <col style="dashed" width=".20" start="0" end="1023">ID3v1 Metadata</col> </row> <row><blank/></row> <row>
View file
audiotools-2.19.tar.gz/docs/reference/figures/ogg
Added
+(directory)
View file
audiotools-2.19.tar.gz/docs/reference/figures/ogg/packets.bdx
Changed
(renamed from docs/reference/figures/ogg_packets.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/figures/ogg/stream.bdx
Added
@@ -0,0 +1,45 @@ +<?xml version="1.0" ?><diagram> + <row> + <col id="oggpage" width=".25">Ogg Page₀</col> + <col width=".25">Ogg Page₁</col> + <col width=".25">Ogg Page₂</col> + <col style="dashed" width=".25">...</col> + </row> + <row><blank/></row> + <row> + <col end="31" id="magic" start="0">magic number `OggS'</col> + </row> + <row> + <col end="39" start="32" width=".333333">version (0)</col> + <col end="47" start="40" width=".333333">header type</col> + <col end="111" start="48" width=".333333">granule position</col> + </row> + <row> + <col end="143" start="112" width=".50">bitstream serial number</col> + <col end="175" start="144" width=".50">page sequence number</col> + </row> + <row> + <col end="207" start="176" width=".50">checksum</col> + <col end="215" start="208" width=".50">page segment count</col> + </row> + <row> + <col start="216" end="223" width=".25">segment length₀</col> + <col start="224" end="231" width=".25">segment length₁</col> + <col start="232" end="239" width=".25">segment length₂</col> + <col width=".25" style="dashed" >...</col> + </row> + <row> + <col width=".25" + start="segment length₀ bytes" + end="segment length₀ bytes">segment₀</col> + <col width=".25" + start="segment length₁ bytes" + end="segment length₁ bytes">segment₁</col> + <col width=".25" + start="segment length₂ bytes" + end="segment length₂ bytes">segment₂</col> + <col width=".25" style="dashed" >...</col> + </row> + <line style="dotted"><start corner="sw" id="oggpage"/><end corner="nw" id="magic"/></line> + <line style="dotted"><start corner="se" id="oggpage"/><end corner="ne" id="magic"/></line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/vorbis/comment-example.bypx
Added
@@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="type">03</field> + <field>766f72626973</field> + <field label="vendor string length">1d000000</field> + <field label="vendor string">586970682e4f7267206c6962566f726269732049203230303930373039</field> + <field label="comment count">02000000</field> + <field label="length₀">0b000000</field> + <field label="string₀">7469746c653d5469746c65</field> + <field label="length₁">0d000000</field> + <field label="string₁">6172746973743d417274697374</field> + <field label="framing">01</field> +</bytestruct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/vorbis/comment.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/vorbis/comment.bdx
Changed
@@ -1,30 +1,50 @@ -<?xml version="1.0" ?><diagram> +<?xml version="1.0" ?> +<diagram> + <row> + <col width=".15">Packet₀</col> + <col width=".20" id="packet1">Packet₁</col> + <col width=".35">Packet₂</col> + <col width=".12">Packet₃</col> + <col width=".18" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="7" width=".15" + id="comment_start">type (3)</col> + <col start="8" end="55" width=".85" + id="comment_end">header (`vorbis')</col> + </row> + <row> + <col start="56" end="87" width=".35">vendor string length</col> + <col width=".65" + start="(vendor string length) bytes" + end="(vendor string length) bytes">vendor string</col> + </row> + <row> + <col start="0" end="31">comment string count</col> + </row> <row> - <col end="7" start="0" width=".15">type (0x03)</col> - <col end="55" start="8" width=".47">header `vorbis' (0x766F72626973)</col> - <col id="data" start="56" width=".20">comment data</col> - <col end="0" start="0" width=".18">framing (1)</col> + <col start="0" end="31" width=".35">comment string length₀</col> + <col width=".65" + start="(comment string length₀) bytes" + end="(comment string length₀) bytes">comment string₀</col> </row> - <row><blank/></row> <row> - <col id="vendorstring" width=".17">vendor string</col> - <col end="31" start="0" width=".20">total comments</col> - <col width=".21">comment string₀</col> - <col id="comment" width=".21">comment string₁</col> - <col id="comments" style="dashed" width=".21">...</col> + <col start="0" end="31" width=".35">comment string length₁</col> + <col width=".65" + start="(comment string length₁) bytes" + end="(comment string length₁) bytes">comment string₁</col> </row> - <row><blank/></row> <row> - <col end="31" id="v_len" start="0" width=".25">vendor string length</col> - <col id="v_str" start="32" width=".20">vendor string</col> - <blank width=".05"/> - <col end="31" id="c_len" start="0" width=".30">comment string length</col> - <col id="c_str" start="0" width=".20">comment string</col> + <col style="dashed" width=".80">...</col> + <col start="0" end="0" width=".20">framing</col> </row> - <line style="dotted"><start corner="sw" id="vendorstring"/><end corner="nw" id="v_len"/></line> - <line style="dotted"><start corner="se" id="vendorstring"/><end corner="ne" id="v_str"/></line> - <line style="dotted"><start corner="sw" id="comment"/><end corner="nw" id="c_len"/></line> - <line style="dotted"><start corner="se" id="comment"/><end corner="ne" id="c_str"/></line> - <line style="dotted"><start corner="sw" id="data"/><end corner="nw" id="vendorstring"/></line> - <line style="dotted"><start corner="se" id="data"/><end corner="ne" id="comments"/></line> + <line style="dotted"> + <start id="packet1" corner="sw"/> + <end id="comment_start" corner="nw"/> + </line> + <line style="dotted"> + <start id="packet1" corner="se"/> + <end id="comment_end" corner="ne"/> + </line> </diagram>
View file
audiotools-2.18.tar.gz/docs/reference/figures/vorbis/identification.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/vorbis/identification.bdx
Changed
@@ -1,12 +1,22 @@ <?xml version="1.0" encoding="utf-8"?> <diagram> <row> - <col start="0" end="7" width=".15">type (0x01)</col> - <col start="8" end="55" width=".85">header `vorbis' (0x766F72626973)</col> + <col width=".15" id="packet0">Packet₀</col> + <col width=".20">Packet₁</col> + <col width=".35">Packet₂</col> + <col width=".12">Packet₃</col> + <col width=".18" style="dashed">...</col> </row> + <spacer/> <row> - <col start="56" end="87" width=".85">vorbis version (0x00000000)</col> - <col start="88" end="95" width=".15">channels</col> + <col start="0" end="7" width=".15" + id="id_start">type (1)</col> + <col start="8" end="55" width=".85" + id="id_end">header (`vorbis')</col> + </row> + <row> + <col start="56" end="87" width=".75">vorbis version (0)</col> + <col start="88" end="95" width=".25">channels</col> </row> <row> <col start="96" end="127" width=".5">sample rate</col> @@ -17,8 +27,16 @@ <col start="192" end="223" width=".5">minimum bitrate</col> </row> <row> - <col start="224" end="227" width="0.375">blocksize₀</col> - <col start="228" end="231" width="0.375">blocksize₁</col> + <col start="224" end="227" width="0.375">block size₀</col> + <col start="228" end="231" width="0.375">block size₁</col> <col start="232" end="232" width=".25">framing (1)</col> </row> + <line style="dotted"> + <start id="packet0" corner="sw"/> + <end id="id_start" corner="nw"/> + </line> + <line style="dotted"> + <start id="packet0" corner="se"/> + <end id="id_end" corner="ne"/> + </line> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/vorbis/identification_example.bpx
Added
@@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<struct endianness="little"> + <field size="8" value="1">packet type</field> + <bytes value="766f72626973">"vorbis"</bytes> + <field size="32" value="0">version</field> + <field size="8" value="2">channels</field> + <field size="32" value="44100">sample rate</field> + <field size="32" value="0">maximum bitrate</field> + <field size="32" value="112000">nominal bitrate</field> + <field size="32" value="0">minimum bitrate</field> + <field size="4" value="8">block size₀</field> + <field size="4" value="11">block size₁</field> + <field size="1" value="1">framing</field> +</struct>
View file
audiotools-2.18.tar.gz/docs/reference/figures/vorbis/setup_packet.bdx -> audiotools-2.19.tar.gz/docs/reference/figures/vorbis/setup_packet.bdx
Changed
@@ -1,26 +1,34 @@ <?xml version="1.0" ?> <diagram> <row> - <col start="0" end="7" width=".15">type (0x05)</col> - <col start="8" end="55" width=".85">header `vorbis' (0x766F72626973)</col> + <col width=".15">Packet₀</col> + <col width=".20">Packet₁</col> + <col width=".35" id="packet2">Packet₂</col> + <col width=".12">Packet₃</col> + <col width=".18" style="dashed">...</col> </row> + <spacer/> <row> - <col>codebooks</col> + <col start="0" end="7" width=".15" + id="setup_start">type (5)</col> + <col start="8" end="55" width=".85" + id="setup_end">header (`vorbis')</col> </row> <row> - <col>time domain transforms</col> - </row> - <row> - <col>floors</col> - </row> - <row> - <col>residues</col> - </row> - <row> - <col>mappings</col> - </row> - <row> - <col width=".75">modes</col> - <col width=".25" start="0" end="0">framing (1)</col> + <col width=".167">codebooks</col> + <col width=".166">transforms</col> + <col width=".141">floors</col> + <col width=".167">residues</col> + <col width=".166">mappings</col> + <col width=".141">modes</col> + <col width=".05" start="0" end="0">(1)</col> </row> + <line style="dotted"> + <start id="packet2" corner="sw"/> + <end id="setup_start" corner="nw"/> + </line> + <line style="dotted"> + <start id="packet2" corner="se"/> + <end id="setup_end" corner="ne"/> + </line> </diagram>
View file
audiotools-2.19.tar.gz/docs/reference/figures/vorbis/stream.bdx
Added
@@ -0,0 +1,35 @@ +<?xml version="1.0" ?> +<diagram> + <row> + <col width=".20" id="page0">Ogg Page₀</col> + <col width=".20" id="page1">Ogg Page₁</col> + <col width=".20" id="page2">Ogg Page₂</col> + <col width=".20" id="page3">Ogg Page₃</col> + <col width=".20" style="dashed">...</col> + </row> + <spacer/> + <row> + <col width=".20" id="packet0">Identification</col> + <col width=".15" id="packet1">Comment</col> + <col width=".20" id="packet2">Setup</col> + <col width=".15" id="packet3">Audio₀</col> + <col width=".15">Audio₁</col> + <col width=".15" style="dashed">...</col> + </row> + <line style="dotted"> + <start id="page0" corner="sw"/> + <end id="packet0" corner="nw"/> + </line> + <line style="dotted"> + <start id="page0" corner="se"/> + <end id="packet0" corner="ne"/> + </line> + <line style="dotted"> + <start id="page1" corner="se"/> + <end id="packet1" corner="ne"/> + </line> + <line style="dotted"> + <start id="page3" corner="se"/> + <end id="packet2" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/flac
Added
+(directory)
View file
audiotools-2.19.tar.gz/docs/reference/flac-codec.template
Added
@@ -0,0 +1,3 @@ +<<file:header.tex>> +\include{flac} +<<file:footer.tex>>
View file
audiotools-2.18.tar.gz/docs/reference/flac.tex -> audiotools-2.19.tar.gz/docs/reference/flac.tex
Changed
@@ -14,2554 +14,16 @@ Except for the contents of the VORBIS\_COMMENT metadata block, everything in FLAC is big-endian. -\section{the FLAC File Stream} -\begin{figure}[h] -\includegraphics{figures/flac/stream.pdf} -\end{figure} -\par -\noindent -\VAR{Last} is 0 when there are no additional metadata blocks and 1 when -it is the final block before the the audio frames. -\VAR{Block Length} is the size of the metadata block data to follow, -not including the header. -\begin{figure}[h] -\begin{tabular}{| r | l |} -\hline -Block Type & Block \\ -\hline -\texttt{0} & STREAMINFO \\ -\texttt{1} & PADDING \\ -\texttt{2} & APPLICATION \\ -\texttt{3} & SEEKTABLE \\ -\texttt{4} & VORBIS\_COMMENT \\ -\texttt{5} & CUESHEET \\ -\texttt{6} & PICTURE \\ -\texttt{7-126} & reserved \\ -\texttt{127} & invalid \\ -\hline -\end{tabular} -\end{figure} +%% \begin{figure}[h] +%% \includegraphics{flac/figures/stream.pdf} +%% \end{figure} -\pagebreak - -\section{FLAC Metadata Blocks} - -\subsection{STREAMINFO} -\begin{figure}[h] -\includegraphics{figures/flac/streaminfo.pdf} -\end{figure} - -\subsection{PADDING} - -PADDING is simply a block full of NULL (\texttt{0x00}) bytes. -Its purpose is to provide extra metadata space within the FLAC file. -By having a padding block, other metadata blocks can be grown or -shrunk without having to rewrite the entire FLAC file by removing or -adding space to the padding. - - -\subsection{APPLICATION} -\begin{figure}[h] -\includegraphics{figures/flac/application.pdf} -\end{figure} -\noindent -APPLICATION is a general-purpose metadata block used by a variety of -different programs. -Its contents are defined by the ASCII Application ID value. - -\subsection{SEEKTABLE} -\begin{figure}[h] -\includegraphics{figures/flac/seektable.pdf} -\end{figure} - -\pagebreak - -\subsection{VORBIS\_COMMENT} -\begin{figure}[h] -\includegraphics{figures/flac/vorbiscomment.pdf} -\end{figure} -\par -\noindent -The length fields are all little-endian. -The Vendor String and Comment Strings are all UTF-8 encoded. -Keys are not case-sensitive and may occur multiple times, -indicating multiple values for the same field. -For instance, a track with multiple artists may have -more than one \texttt{ARTIST}. - -\begin{multicols}{2} -{\relsize{-2} -\begin{description} -\item[ALBUM] album name -\item[ARTIST] artist name, band name, composer, author, etc. -\item[CATALOGNUMBER*] CD spine number -\item[COMPOSER*] the work's author -\item[CONDUCTOR*] performing ensemble's leader -\item[COPYRIGHT] copyright attribution -\item[DATE] recording date -\item[DESCRIPTION] a short description -\item[DISCNUMBER*] disc number for multi-volume work -\item[ENGINEER*] the recording masterer -\item[ENSEMBLE*] performing group -\item[GENRE] a short music genre label -\item[GUEST ARTIST*] collaborating artist -\item[ISRC] ISRC number for the track -\item[LICENSE] license information -\item[LOCATION] recording location -\item[OPUS*] number of the work -\item[ORGANIZATION] record label -\item[PART*] track's movement title -\item[PERFORMER] performer name, orchestra, actor, etc. -\item[PRODUCER*] person responsible for the project -\item[PRODUCTNUMBER*] UPC, EAN, or JAN code -\item[PUBLISHER*] album's publisher -\item[RELEASE DATE*] date the album was published -\item[REMIXER*] person who created the remix -\item[SOURCE ARTIST*] artist of the work being performed -\item[SOURCE MEDIUM*] CD, radio, cassette, vinyl LP, etc. -\item[SOURCE WORK*] a soundtrack's original work -\item[SPARS*] DDD, ADD, AAD, etc. -\item[SUBTITLE*] for multiple track names in a single file -\item[TITLE] track name -\item[TRACKNUMBER] track number -\item[VERSION] track version -\end{description} -} -\end{multicols} -\par -\noindent -Fields marked with * are proposed extension fields and not part of the official Vorbis comment specification. - -\pagebreak - -\subsection{CUESHEET} -\begin{figure}[h] -\includegraphics{figures/flac/cuesheet.pdf} -\end{figure} - -\subsection{PICTURE} -\begin{figure}[h] -\includegraphics{figures/flac/picture.pdf} -\end{figure} -\begin{tabular}{|r|l|} -\hline -Picture Type & Type \\ -\hline -0 & Other \\ -1 & 32x32 pixels `file icon' (PNG only) \\ -2 & Other file icon \\ -3 & Cover (front) \\ -4 & Cover (back) \\ -5 & Leaflet page \\ -6 & Media (e.g. label side of CD) \\ -7 & Lead artist / Lead performer / Soloist \\ -8 & Artist / Performer \\ -9 & Conductor \\ -10 & Band / Orchestra \\ -11 & Composer \\ -12 & Lyricist / Text writer \\ -13 & Recording location \\ -14 & During recording \\ -15 & During performance \\ -16 & Movie / Video screen capture \\ -17 & A bright colored fish \\ -18 & Illustration \\ -19 & Band / Artist logotype \\ -20 & Publisher / Studio logotype \\ -\hline -\end{tabular} - -\section{FLAC Decoding} - -The basic process for decoding a FLAC file is as follows: -\par -\noindent -\ALGORITHM{a FLAC encoded file}{PCM samples} -\SetKwData{HEADER}{file header} -\SetKwData{PCMCOUNT}{PCM frame count} -\SetKwData{MDSUM}{MD5 sum} -$\HEADER \leftarrow$ \READ 4 bytes\; -\ASSERT $\text{\HEADER} = \texttt{"fLaC"}$\; -\hyperref[flac:read_metadata]{get \PCMCOUNT and \MDSUM from STREAMINFO metadata block}\; -skip remaining metadata blocks\; -initialize stream MD5\; -\While{$\PCMCOUNT > 0$}{ - \hyperref[flac:decode_frame]{decode FLAC frame to 1 or more PCM frames}\; - deduct FLAC frame's block size from $\PCMCOUNT$\; - \hyperref[flac:update_md5]{update stream MD5 sum with decoded PCM frame data}\; - \Return decoded PCM frames\; -} -\ASSERT STREAMINFO \MDSUM = stream MD5 sum -\EALGORITHM -\begin{figure}[h] -\includegraphics{figures/flac/stream3.pdf} -\end{figure} -\par -All of the fields in the FLAC stream are big-endian.\footnote{Except -for the length fields in the VORBIS\_COMMENT metadata block. -However, this block is not needed for decoding. -} - -\clearpage - -\subsection{Reading Metadata Blocks} -\label{flac:read_metadata} -{\relsize{-1} -\ALGORITHM{the FLAC file stream}{STREAMINFO values used for decoding} -\SetKwData{LAST}{last} -\SetKwData{TYPE}{type} -\SetKwData{SIZE}{size} -\SetKwData{MINBLOCKSIZE}{minimum block size} -\SetKwData{MAXBLOCKSIZE}{maximum block size} -\SetKwData{MINFRAMESIZE}{minimum frame size} -\SetKwData{MAXFRAMESIZE}{maximum frame size} -\SetKwData{SAMPLERATE}{sample rate} -\SetKwData{CHANNELS}{channels} -\SetKwData{BPS}{bits per sample} -\SetKwData{TOTALFRAMES}{total PCM frames} -\SetKwData{MDSUM}{MD5 sum} -\SetKwData{STREAMINFO}{STREAMINFO} -\Repeat{$\LAST = 1$}{ - $\LAST \leftarrow$ \READ 1 unsigned bit\; - $\TYPE \leftarrow$ \READ 7 unsigned bits\; - $\SIZE \leftarrow$ \READ 24 unsigned bits\; - \eIf(\tcc*[f]{read STREAMINFO metadata block}){$\TYPE = 0$}{ - \begin{tabular}{rcl} - \MINBLOCKSIZE & $\leftarrow$ & \READ 16 unsigned bits\; \\ - \MAXBLOCKSIZE & $\leftarrow$ & \READ 16 unsigned bits\; \\ - \MINFRAMESIZE & $\leftarrow$ & \READ 24 unsigned bits\; \\ - \MAXFRAMESIZE & $\leftarrow$ & \READ 24 unsigned bits\; \\ - \SAMPLERATE & $\leftarrow$ & \READ 20 unsigned bits\; \\ - \CHANNELS & $\leftarrow$ & (\READ 3 unsigned bits) + 1\; \\ - \BPS & $\leftarrow$ & (\READ 5 unsigned bits) + 1\; \\ - \TOTALFRAMES & $\leftarrow$ & \READ 36 unsigned bits\; \\ - \MDSUM & $\leftarrow$ & \READ 16 bytes\; - \end{tabular} - }(\tcc*[f]{skip other metadata blocks}){ - \SKIP \SIZE bytes\; - } -} -\Return $\text{\STREAMINFO} \leftarrow \left\lbrace\begin{tabular}{l} -\MINBLOCKSIZE \\ -\MAXBLOCKSIZE \\ -\MINFRAMESIZE \\ -\MAXFRAMESIZE \\ -\SAMPLERATE \\ -\CHANNELS \\ -\BPS \\ -\TOTALFRAMES \\ -\MDSUM \\ -\end{tabular}\right.$ -\EALGORITHM -} -\begin{figure}[h] -\includegraphics{figures/flac/metadata.pdf} -\end{figure} - -\clearpage - -For example, given the metadata bytes: -\begin{figure}[h] -\includegraphics{figures/flac/block_header.pdf} -\end{figure} -\par -\noindent -\begin{tabular}{rcrcl} -\textsf{last} & $\leftarrow$ & \texttt{0x1} & = & is last metadata block \\ -\textsf{size} & $\leftarrow$ & \texttt{0x0} & = & METADATA block \\ -\textsf{type} & $\leftarrow$ & \texttt{0x22} & = & 34 bytes \\ -\textsf{minimum block size} & $\leftarrow$ & \texttt{0x0100} & = & 4096 samples \\ -\textsf{maximum block size} & $\leftarrow$ & \texttt{0x0100} & = & 4096 samples \\ -\textsf{minimum frame size} & $\leftarrow$ & \texttt{0x00000C} & = & 12 bytes \\ -\textsf{maximum frame size} & $\leftarrow$ & \texttt{0x00000C} & = & 12 bytes \\ -\textsf{sample rate} & $\leftarrow$ & \texttt{0xAC44} & = & 44100Hz \\ -\textsf{channels} & $\leftarrow$ & \texttt{0x1} & = & 1 (+ 1) = 2 \\ -\textsf{bits per sample} & $\leftarrow$ & \texttt{0xF} & = & 15 (+ 1) = 16 \\ -\textsf{total PCM frames} & $\leftarrow$ & \texttt{0x32} & = & 50 \\ -\textsf{MD5 sum} & $\leftarrow$ & \multicolumn{3}{l}{\texttt{6D0BB00954CEB7FBEE436BB55A8397A9}} \\ -\end{tabular} - -\clearpage - -\subsection{Decoding a FLAC Frame} -\label{flac:decode_frame} -\ALGORITHM{STREAMINFO values and the FLAC file stream}{decoded PCM samples} -\SetKwData{CHANNEL}{channel} -\SetKwData{CHANNELCOUNT}{channel count} -\hyperref[flac:read_frame_header]{read frame header to determine channel count, assignment and bits-per-sample}\; -\ForEach{\CHANNEL \IN \CHANNELCOUNT}{ - \hyperref[flac:decode_subframe]{decode subframe to PCM samples based on its effective bits-per-sample}\; -} -byte-align file stream\; -\hyperref[flac:verify_crc16]{verify frame's CRC-16 checksum}\; -\hyperref[flac:recombine_subframes]{recombine subframes based on the frame's channel assignment}\; -\Return samples\; -\EALGORITHM -\begin{figure}[h] -\includegraphics{figures/flac/frames.pdf} -\end{figure} - -\clearpage - -\subsection{Reading a FLAC Frame Header} -\label{flac:read_frame_header} -{\relsize{-1} -\ALGORITHM{STREAMINFO values and the FLAC file stream}{stream information and subframe decoding parameters} -\SetKwData{SYNCCODE}{sync code} -\SetKwData{BLOCKINGSTRATEGY}{blocking strategy} -\SetKwData{ENCODEDBLOCKSIZE}{encoded block size} -\SetKwData{ENCODEDSAMPLERATE}{encoded sample rate} -\SetKwData{ENCODEDCHANNELS}{encoded channels} -\SetKwData{ENCODEDBPS}{encoded bits per sample} -\SetKwData{FRAMENUMBER}{frame number} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{SAMPLERATE}{sample rate} -\SetKwData{BPS}{bits per sample} -\SetKwData{CHANNELCOUNT}{channel count} -\SetKwData{CRC}{CRC-8} -\SetKwData{FRAMEHEADER}{frame header} -\begin{tabular}{rcl} -\SYNCCODE & $\leftarrow$ & \READ 14 unsigned bits\; \\ -& & \ASSERT $\text{\SYNCCODE} = \texttt{0x3FFE}$\; \\ -& & \SKIP 1 bit\; \\ -\BLOCKINGSTRATEGY & $\leftarrow$ & \READ 1 unsigned bit\; \\ -\ENCODEDBLOCKSIZE & $\leftarrow$ & \READ 4 unsigned bits\; \\ -\ENCODEDSAMPLERATE & $\leftarrow$ & \READ 4 unsigned bits\; \\ -\ENCODEDCHANNELS & $\leftarrow$ & \READ 4 unsigned bits\; \\ -\ENCODEDBPS & $\leftarrow$ & \READ 3 unsigned bits\; \\ -& & \SKIP 1 bit\; \\ -\FRAMENUMBER & $\leftarrow$ & \READ UTF-8 value\; \\ -\BLOCKSIZE & $\leftarrow$ & decode \ENCODEDBLOCKSIZE\; \\ -\SAMPLERATE & $\leftarrow$ & decode \ENCODEDSAMPLERATE\; \\ -\BPS & $\leftarrow$ & decode \ENCODEDBPS\; \\ -\CHANNELCOUNT & $\leftarrow$ & decode \ENCODEDCHANNELS\; \\ -\CRC & $\leftarrow$ & \READ 8 unsigned bits\; \\ -& & \hyperref[flac:verify_crc8]{verify \CRC}\; \\ -\end{tabular}\; -\BlankLine -\Return $\text{\FRAMEHEADER} \leftarrow \left\lbrace\begin{tabular}{l} -\BLOCKSIZE \\ -\SAMPLERATE \\ -\BPS \\ -\ENCODEDCHANNELS \\ -\CHANNELCOUNT \\ -\end{tabular}\right.$\; -\EALGORITHM -} - -\subsubsection{Reading UTF-8 Frame Number} -{\relsize{-1} -\ALGORITHM{FLAC file stream}{UTF-8 value as unsigned integer} -\SetKwData{TOTALBYTES}{total bytes} -\SetKwData{VALUE}{value} -\SetKwData{CONTINUATIONHEADER}{continuation header} -\SetKwData{CONTINUATIONBITS}{continuation bits} -$\TOTALBYTES \leftarrow$ \UNARY with stop bit 0\; -$\VALUE \leftarrow$ \READ (7 - $\TOTALBYTES$) unsigned bits\; -\While{$\TOTALBYTES > 0$}{ - $\CONTINUATIONHEADER \leftarrow$ \READ 2 unsigned bits\; - \ASSERT $\CONTINUATIONHEADER = 2$\; - $\CONTINUATIONBITS \leftarrow$ \READ 6 unsigned bits\; - $\VALUE \leftarrow (\VALUE \times 2 ^ 6) + \CONTINUATIONBITS$\; - $\TOTALBYTES \leftarrow \TOTALBYTES - 1$\; -} -\Return \VALUE\; -\EALGORITHM -} -For example, given the UTF-8 bytes \texttt{E1 82 84}: -\par -\begin{wrapfigure}[5]{l}{2.375in} -\includegraphics{figures/flac/utf8.pdf} -\end{wrapfigure} -\begin{align*} -\text{UTF-8 value} &= \texttt{0001 000010 000100} \\ -&= \texttt{0001 0000 1000 0100} \\ -&= \texttt{0x1084} \\ -&= 4228 -\end{align*} - -\clearpage - -\subsubsection{Decoding Block Size} -{\relsize{-1} -\begin{tabular}{rl||rl} -encoded & block size (in samples) & -encoded & block size \\ -\hline -\texttt{0000} & maximum block size from STREAMINFO & -\texttt{1000} & 256 \\ -\texttt{0001} & 192 & -\texttt{1001} & 512 \\ -\texttt{0010} & 576 & -\texttt{1010} & 1024 \\ -\texttt{0011} & 1152 & -\texttt{1011} & 2048 \\ -\texttt{0100} & 2304 & -\texttt{1100} & 4096 \\ -\texttt{0101} & 4608 & -\texttt{1101} & 8192 \\ -\texttt{0110} & (\textbf{read} 8 unsigned bits) + 1 & -\texttt{1110} & 16384 \\ -\texttt{0111} & (\textbf{read} 16 unsigned bits) + 1 & -\texttt{1111} & 32768 \\ -\end{tabular} -} - -\subsubsection{Decoding Sample Rate} -{\relsize{-1} -\begin{tabular}{rl||rl} -encoded & sample rate (in Hz) & -encoded & sample rate \\ -\hline -\texttt{0000} & from STREAMINFO & -\texttt{1000} & 32000 \\ -\texttt{0001} & 88200 & -\texttt{1001} & 44100 \\ -\texttt{0010} & 176400 & -\texttt{1010} & 48000 \\ -\texttt{0011} & 192000 & -\texttt{1011} & 96000 \\ -\texttt{0100} & 8000 & -\texttt{1100} & (\textbf{read} 8 unsigned bits) $\times$ 1000 \\ -\texttt{0101} & 16000 & -\texttt{1101} & \textbf{read} 16 unsigned bits \\ -\texttt{0110} & 22050 & -\texttt{1110} & (\textbf{read} 16 unsigned bits) $\times$ 10 \\ -\texttt{0111} & 24000 & -\texttt{1111} & invalid \\ -\end{tabular} -} - -\subsubsection{Decoding Bits per Sample} -{\relsize{-1} -\begin{tabular}{rl||rl} -encoded & bits-per-sample & -encoded & bits-per-sample \\ -\hline -\texttt{000} & from STREAMINFO & -\texttt{100} & 16 \\ -\texttt{001} & 8 & -\texttt{101} & 20 \\ -\texttt{010} & 12 & -\texttt{110} & 24 \\ -\texttt{011} & invalid & -\texttt{111} & invalid \\ -\end{tabular} -} - -\subsubsection{Decoding Channel Count and Assignment} -{\relsize{-1} -\begin{tabular}{rrl} -& channel & \\ -encoded & count & channel assignment \\ -\hline -\texttt{0000} & 1 & front Center \\ -\texttt{0001} & 2 & front Left, front Right \\ -\texttt{0010} & 3 & front Left, front Right, front Center \\ -\texttt{0011} & 4 & front Left, front Right, back Left, back Right \\ -\texttt{0100} & 5 & fL, fR, fC, back/surround left, back/surround right \\ -\texttt{0101} & 6 & fL, fR, fC, LFE, back/surround left, back/surround right \\ -\texttt{0110} & 7 & undefined \\ -\texttt{0111} & 8 & undefined \\ -\texttt{1000} & 2 & front Left, Difference \\ -\texttt{1001} & 2 & Difference, front Right \\ -\texttt{1010} & 2 & Mid, Side \\ -\texttt{1011} & & reserved \\ -\texttt{1100} & & reserved \\ -\texttt{1101} & & reserved \\ -\texttt{1110} & & reserved \\ -\texttt{1111} & & reserved \\ -\end{tabular} -} - -\subsubsection{Frame Header Decoding Example} -\begin{figure}[h] -\includegraphics{figures/flac/header-example.pdf} -\end{figure} -{\relsize{-1} -\begin{tabular}{rcl} -\textsf{sync code} & = & \texttt{0x3FFE} \\ -\textsf{encoded block size} & = & \texttt{1100b} \\ -\textsf{encoded sample rate} & = & \texttt{1001b} \\ -\textsf{encoded channels} & = & \texttt{0001b} \\ -\textsf{encoded bps} & = & \texttt{100b} \\ -\textsf{frame number} & = & 0 \\ -\textsf{block size} & = & 4096 samples \\ -\textsf{sample rate} & = & 44100Hz \\ -\textsf{bits per sample} & = & 16 \\ -\textsf{channel count} & = & 2 \\ -\textsf{channel assignment} & = & front left, front right -\end{tabular} -} -\subsubsection{Calculating Frame Header CRC-8} -\label{flac:verify_crc8} -Given a header byte and previous CRC-8 checksum, -or 0 as an initial value: -\begin{equation*} -\text{checksum}_i = \texttt{CRC8}(byte\xor\text{checksum}_{i - 1}) -\end{equation*} -\begin{table}[h] -{\relsize{-3}\ttfamily -\begin{tabular}{|r||r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} -\hline - & 0x?0 & 0x?1 & 0x?2 & 0x?3 & 0x?4 & 0x?5 & 0x?6 & 0x?7 & 0x?8 & 0x?9 & 0x?A & 0x?B & 0x?C & 0x?D & 0x?E & 0x?F \\ -\hline -0x0? & 0x00 & 0x07 & 0x0E & 0x09 & 0x1C & 0x1B & 0x12 & 0x15 & 0x38 & 0x3F & 0x36 & 0x31 & 0x24 & 0x23 & 0x2A & 0x2D \\ -0x1? & 0x70 & 0x77 & 0x7E & 0x79 & 0x6C & 0x6B & 0x62 & 0x65 & 0x48 & 0x4F & 0x46 & 0x41 & 0x54 & 0x53 & 0x5A & 0x5D \\ -0x2? & 0xE0 & 0xE7 & 0xEE & 0xE9 & 0xFC & 0xFB & 0xF2 & 0xF5 & 0xD8 & 0xDF & 0xD6 & 0xD1 & 0xC4 & 0xC3 & 0xCA & 0xCD \\ -0x3? & 0x90 & 0x97 & 0x9E & 0x99 & 0x8C & 0x8B & 0x82 & 0x85 & 0xA8 & 0xAF & 0xA6 & 0xA1 & 0xB4 & 0xB3 & 0xBA & 0xBD \\ -0x4? & 0xC7 & 0xC0 & 0xC9 & 0xCE & 0xDB & 0xDC & 0xD5 & 0xD2 & 0xFF & 0xF8 & 0xF1 & 0xF6 & 0xE3 & 0xE4 & 0xED & 0xEA \\ -0x5? & 0xB7 & 0xB0 & 0xB9 & 0xBE & 0xAB & 0xAC & 0xA5 & 0xA2 & 0x8F & 0x88 & 0x81 & 0x86 & 0x93 & 0x94 & 0x9D & 0x9A \\ -0x6? & 0x27 & 0x20 & 0x29 & 0x2E & 0x3B & 0x3C & 0x35 & 0x32 & 0x1F & 0x18 & 0x11 & 0x16 & 0x03 & 0x04 & 0x0D & 0x0A \\ -0x7? & 0x57 & 0x50 & 0x59 & 0x5E & 0x4B & 0x4C & 0x45 & 0x42 & 0x6F & 0x68 & 0x61 & 0x66 & 0x73 & 0x74 & 0x7D & 0x7A \\ -0x8? & 0x89 & 0x8E & 0x87 & 0x80 & 0x95 & 0x92 & 0x9B & 0x9C & 0xB1 & 0xB6 & 0xBF & 0xB8 & 0xAD & 0xAA & 0xA3 & 0xA4 \\ -0x9? & 0xF9 & 0xFE & 0xF7 & 0xF0 & 0xE5 & 0xE2 & 0xEB & 0xEC & 0xC1 & 0xC6 & 0xCF & 0xC8 & 0xDD & 0xDA & 0xD3 & 0xD4 \\ -0xA? & 0x69 & 0x6E & 0x67 & 0x60 & 0x75 & 0x72 & 0x7B & 0x7C & 0x51 & 0x56 & 0x5F & 0x58 & 0x4D & 0x4A & 0x43 & 0x44 \\ -0xB? & 0x19 & 0x1E & 0x17 & 0x10 & 0x05 & 0x02 & 0x0B & 0x0C & 0x21 & 0x26 & 0x2F & 0x28 & 0x3D & 0x3A & 0x33 & 0x34 \\ -0xC? & 0x4E & 0x49 & 0x40 & 0x47 & 0x52 & 0x55 & 0x5C & 0x5B & 0x76 & 0x71 & 0x78 & 0x7F & 0x6A & 0x6D & 0x64 & 0x63 \\ -0xD? & 0x3E & 0x39 & 0x30 & 0x37 & 0x22 & 0x25 & 0x2C & 0x2B & 0x06 & 0x01 & 0x08 & 0x0F & 0x1A & 0x1D & 0x14 & 0x13 \\ -0xE? & 0xAE & 0xA9 & 0xA0 & 0xA7 & 0xB2 & 0xB5 & 0xBC & 0xBB & 0x96 & 0x91 & 0x98 & 0x9F & 0x8A & 0x8D & 0x84 & 0x83 \\ -0xF? & 0xDE & 0xD9 & 0xD0 & 0xD7 & 0xC2 & 0xC5 & 0xCC & 0xCB & 0xE6 & 0xE1 & 0xE8 & 0xEF & 0xFA & 0xFD & 0xF4 & 0xF3 \\ -\hline -\end{tabular} -} -\end{table} -\begin{align*} -\text{checksum}_0 = \texttt{CRC8}(\texttt{FF}\xor\texttt{00}) = \texttt{F3} & & -\text{checksum}_3 = \texttt{CRC8}(\texttt{18}\xor\texttt{E6}) = \texttt{F4} \\ -\text{checksum}_1 = \texttt{CRC8}(\texttt{F8}\xor\texttt{F3}) = \texttt{31} & & -\text{checksum}_4 = \texttt{CRC8}(\texttt{00}\xor\texttt{F4}) = \texttt{C2} \\ -\text{checksum}_2 = \texttt{CRC8}(\texttt{C9}\xor\texttt{31}) = \texttt{E6} & & -\text{checksum}_5 = \texttt{CRC8}(\texttt{C2}\xor\texttt{C2}) = \texttt{00} \\ -\end{align*} -Note that the final checksum (including the CRC-8 byte itself) -should always be 0. - - -\clearpage - -\subsection{Decoding a FLAC Subframe} -\label{flac:decode_subframe} -{\relsize{-1} -\ALGORITHM{the frame's block size and bits per sample, the subframe's channel assignment and the FLAC file stream}{decoded signed PCM samples} -\SetKwData{TYPEORDER}{type/order} -\SetKwData{WASTEDBPS}{wasted BPS} -\SetKwData{EFFECTIVEBPS}{subframe's BPS} -\SetKwData{DIFFERENCE}{difference} -\SetKwData{SIDE}{side} -\SetKwData{ORDER}{order} -\SetKwData{SAMPLE}{sample} -\SetKwData{BLOCKSIZE}{block size} -\SKIP 1 bit\; -$\TYPEORDER \leftarrow$ \READ 6 unsigned bits\; -\eIf(\tcc*[f]{account for wasted bits}){$((\READ~\textnormal{1 unsigned bit}) = 1)$}{ - $\WASTEDBPS \leftarrow$ (\UNARY with stop bit 1) + 1\; - \eIf{subframe's channel assignment is \DIFFERENCE or \SIDE}{ - $\EFFECTIVEBPS \leftarrow \text{frame header's bits per sample} - \WASTEDBPS + 1$\; - }{ - $\EFFECTIVEBPS \leftarrow \text{frame header's bits per sample} - \WASTEDBPS$\; - } -}{ - $\WASTEDBPS \leftarrow 0$\; - \eIf{subframe's channel assignment is \DIFFERENCE or \SIDE}{ - $\EFFECTIVEBPS \leftarrow \text{frame header's bits per sample} + 1$\; - }{ - $\EFFECTIVEBPS \leftarrow \text{frame header's bits per sample}$\; - } -} -\uIf{$\TYPEORDER = 0$}{ - \SAMPLE $\leftarrow$ \hyperref[flac:decode_constant]{decode CONSTANT subframe with \BLOCKSIZE, \EFFECTIVEBPS}\; -} -\uElseIf{$\TYPEORDER = 1$} { - \SAMPLE $\leftarrow$ \hyperref[flac:decode_verbatim]{decode VERBATIM subframe with \BLOCKSIZE, \EFFECTIVEBPS}\; -} -\uElseIf{$8 \leq \TYPEORDER \leq 12$} { - $\ORDER \leftarrow \TYPEORDER - 8$\; - \SAMPLE $\leftarrow$ \hyperref[flac:decode_fixed]{decode FIXED subframe with \BLOCKSIZE, \EFFECTIVEBPS, \ORDER}\; -} -\uElseIf{$32 \leq \TYPEORDER \leq 63$} { - $\ORDER \leftarrow \TYPEORDER - 31$\; - \SAMPLE $\leftarrow$ \hyperref[flac:decode_lpc]{decode LPC subframe with \BLOCKSIZE, \EFFECTIVEBPS, \ORDER}\; -} -\Else { - undefined subframe type error\; -} -\If(\tcc*[f]{prepend any wasted bits to each sample}){$\WASTEDBPS > 0$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\SAMPLE}_i \leftarrow \text{\SAMPLE}_i \times 2 ^ {\WASTEDBPS}$\; - } -} -\Return \SAMPLE\; -\EALGORITHM -} -\begin{figure}[h] -\includegraphics{figures/flac/subframes.pdf} -\end{figure} - -\clearpage - -\subsubsection{Decoding CONSTANT Subframe} -\label{flac:decode_constant} -\ALGORITHM{the frame's block size, the subframe's bits per sample}{decoded signed PCM samples} -\SetKwData{CONSTANT}{constant} -\SetKwData{BPS}{subframe's BPS} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{SAMPLE}{sample} -$\CONSTANT \leftarrow$ \READ (\BPS) signed bits\; -\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\SAMPLE}_i \leftarrow \CONSTANT$\; -} -\Return \SAMPLE\; -\EALGORITHM -\begin{figure}[h] - \includegraphics{figures/flac/constant.pdf} -\end{figure} - -\subsubsection{Decoding VERBATIM Subframe} -\label{flac:decode_verbatim} -\ALGORITHM{the frame's block size, the subframe's bits per sample}{decoded signed PCM samples} -\SetKwData{BPS}{bits per sample} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{SAMPLE}{sample} -\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\SAMPLE}_i \leftarrow $ \READ (\BPS) signed bits\; -} -\Return \SAMPLE\; -\EALGORITHM -\begin{figure}[h] -\includegraphics{figures/flac/verbatim.pdf} -\end{figure} - -\clearpage - -\subsubsection{Decoding FIXED Subframe} -\label{flac:decode_fixed} -{\relsize{-1} -\ALGORITHM{the frame's block size, the subframe's bits per sample and predictor order}{decoded signed PCM samples} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{BPS}{subframe's BPS} -\SetKwData{ORDER}{order} -\SetKwData{SAMPLE}{sample} -\SetKwData{RESIDUAL}{residual} -\For(\tcc*[f]{warm-up samples}){$i \leftarrow 0$ \emph{\KwTo}\ORDER}{ - $\text{\SAMPLE}_i \leftarrow $ \READ (\BPS) signed bits\; -} -\BlankLine -$\RESIDUAL \leftarrow$ \hyperref[flac:decode_residual]{read residual block with frame's \BLOCKSIZE and subframe's \ORDER}\; -\BlankLine -\Switch{\ORDER}{ - \uCase{0} { - \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\SAMPLE}_i \leftarrow \text{\RESIDUAL}_i$ - } - } - \uCase{1} { - \For{$i \leftarrow 1$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\SAMPLE}_i \leftarrow \text{\SAMPLE}_{i - 1} + \text{\RESIDUAL}_{i - 1}$ - } - } - \uCase{2} { - \For{$i \leftarrow 2$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\SAMPLE}_i \leftarrow (2 \times \text{\SAMPLE}_{i - 1}) - \text{\SAMPLE}_{i - 2} + \text{\RESIDUAL}_{i - 2}$ - } - } - \uCase{3} { - \For{$i \leftarrow 3$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\SAMPLE}_i \leftarrow (3 \times \text{\SAMPLE}_{i - 1}) - (3 \times \text{\SAMPLE}_{i - 2}) + \text{\SAMPLE}_{i - 3} + \text{\RESIDUAL}_{i - 3}$ - } - } - \Case{4} { - \For{$i \leftarrow 4$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\SAMPLE}_i \leftarrow (4 \times \text{\SAMPLE}_{i - 1}) - (6 \times \text{\SAMPLE}_{i - 2}) + (4 \times \text{\SAMPLE}_{i - 3}) - \text{\SAMPLE}_{i - 4} + \text{\RESIDUAL}_{i - 4}$ - } - } -} -\Return \SAMPLE\; -\EALGORITHM -} -\begin{figure}[h] -\includegraphics{figures/flac/fixed.pdf} -\end{figure} - -\clearpage - -\subsubsection{FIXED Subframe Decoding Example} - -Given the subframe bytes of a 16 bits per sample stream\footnote{Decoding the residual block is explained on page \pageref{flac:decode_residual}}: -\begin{figure}[h] -\includegraphics{figures/flac/fixed-parse.pdf} -\end{figure} -\begin{align*} -\text{subframe type} &\leftarrow \text{FIXED} \\ -\text{subframe order} &\leftarrow 1 \\ -\text{wasted BPS} &\leftarrow 0 \\ -\text{warm-up sample}_0 &\leftarrow 37 \\ -\end{align*} -\begin{center} -\begin{tabular}{r||r|>{$}r<{$}} -$i$ & $\textsf{residual}_{i - 1}$ & \textsf{sample}_i \\ -\hline -0 & & 37 \\ -1 & -2 & 37 - 2 = 35 \\ -2 & 3 & 35 + 3 = 38 \\ -3 & -1 & 38 - 1 = 37 \\ -4 & -5 & 37 - 5 = 32 \\ -5 & 1 & 32 + 1 = 33 \\ -6 & -5 & 33 - 5 = 27 \\ -7 & 4 & 27 + 4 = 31 \\ -8 & -2 & 31 - 2 = 29 \\ -9 & -3 & 29 - 3 = 26 \\ -10 & 1 & 26 + 1 = 27 \\ -\end{tabular} -\end{center} - -\clearpage - -\subsubsection{Decoding LPC Subframe} -\label{flac:decode_lpc} -{\relsize{-1} -\ALGORITHM{the frame's block size and predictor order, the subframe's bits per sample}{decoded signed PCM samples} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{BPS}{subframe's BPS} -\SetKwData{ORDER}{order} -\SetKwData{QLPPREC}{QLP precision} -\SetKwData{QLPSHIFT}{QLP shift needed} -\SetKwData{COEFF}{QLP coefficient} -\SetKwData{RESIDUAL}{residual} -\SetKwData{SAMPLE}{sample} -\SetKwFunction{MAX}{max} -\For(\tcc*[f]{warm-up samples}){$i \leftarrow 0$ \emph{\KwTo}\ORDER}{ - $\text{\SAMPLE}_i \leftarrow $ \READ (\BPS) signed bits\; -} -$\QLPPREC \leftarrow$ (\READ 4 unsigned bits) + 1\; -$\QLPSHIFT \leftarrow \MAX(\text{\READ 5 signed bits}~,~0)$\footnote{negative shifts are no-ops in the decoder}\; -\For{$i \leftarrow 0$ \emph{\KwTo}\ORDER}{ - $\text{\COEFF}_i \leftarrow$ \READ (\QLPPREC) signed bits\; -} -\BlankLine -$\RESIDUAL \leftarrow$ \hyperref[flac:decode_residual]{read residual block with frame's \BLOCKSIZE and subframe's \ORDER}\; -\BlankLine -\For{$i \leftarrow \ORDER$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\SAMPLE}_i \leftarrow \left\lfloor \frac{\overset{\ORDER - 1}{\underset{j = 0}{\sum}} - \text{\COEFF}_j \times \text{\SAMPLE}_{i - j - 1} } {2 ^ \text{\QLPSHIFT}}\right\rfloor + \text{\RESIDUAL}_{i - \ORDER}$\; -} -\Return \SAMPLE\; -\EALGORITHM -} -\begin{figure}[h] -\includegraphics{figures/flac/lpc.pdf} -\end{figure} - -\subsubsection{LPC Subframe Decoding Example} -\begin{table}[h] -{\relsize{-1} -\begin{tabular}{rcl} - subframe type & $\leftarrow$ & LPC \\ - subframe order & $\leftarrow$ & 3 \\ - wasted BPS & $\leftarrow$ & 0 \\ - QLP precision & $\leftarrow$ & 12 \\ - QLP shift needed & $\leftarrow$ & 10 \\ -\end{tabular} -\begin{tabular}{r|rrr} - $i$ & $\textsf{sample}_i$ & $\textsf{QLP coefficient}_i$ & $\textsf{residual}_i$ \\ -\hline -0 & 43 & 1451 & 4 \\ -1 & 48 & -323 & 0 \\ -2 & 50 & -110 & 1 \\ -3 & & & -2 \\ -4 & & & -3 \\ -\end{tabular} -} -\end{table} - -\clearpage - -\begin{figure}[h] -\includegraphics{figures/flac/lpc-parse.pdf} -\end{figure} -{\relsize{-1} -\begin{align*} -\text{sample}_0 &\leftarrow 43 \\ -\text{sample}_1 &\leftarrow 48 \\ -\text{sample}_2 &\leftarrow 50 \\ -\text{sample}_3 &\leftarrow \left\lfloor\frac{(1451 \times 50) + (-323 \times 48) + (-110 \times 43)}{2 ^ {10}}\right\rfloor + 4 = \left\lfloor\frac{52316}{1024}\right\rfloor + 4 = \textbf{55} \\ -\text{sample}_4 &\leftarrow \left\lfloor\frac{(1451 \times 55) + (-323 \times 50) + (-110 \times 48)}{2 ^ {10}}\right\rfloor + 0 = \left\lfloor\frac{58375}{1024}\right\rfloor + 0 = \textbf{57} \\ -\text{sample}_5 &\leftarrow \left\lfloor\frac{(1451 \times 57) + (-323 \times 55) + (-110 \times 50)}{2 ^ {10}}\right\rfloor + 1 = \left\lfloor\frac{59442}{1024}\right\rfloor + 1 = \textbf{59} \\ -\text{sample}_6 &\leftarrow \left\lfloor\frac{(1451 \times 59) + (-323 \times 57) + (-110 \times 55)}{2 ^ {10}}\right\rfloor - 2 = \left\lfloor\frac{61148}{1024}\right\rfloor - 2 = \textbf{57} \\ -\text{sample}_7 &\leftarrow \left\lfloor\frac{(1451 \times 57) + (-323 \times 59) + (-110 \times 57)}{2 ^ {10}}\right\rfloor - 3 = \left\lfloor\frac{57380}{1024}\right\rfloor - 3 = \textbf{53} \\ -\end{align*} -} - -\clearpage - -\subsubsection{Decoding Residual Block} -\label{flac:decode_residual} -{\relsize{-1} -\ALGORITHM{the frame's block size and predictor order}{decoded signed residual values} -\SetKwData{CODING}{coding method} -\SetKwData{PORDER}{partition order} -\SetKwData{PCOUNT}{partition residual count} -\SetKwData{ORDER}{predictor order} -\SetKwData{RESIDUAL}{residual} -\SetKwData{RICE}{Rice} -\SetKwData{ESCAPE}{escape code} -\SetKwData{MSB}{MSB} -\SetKwData{LSB}{LSB} -\SetKwData{UNSIGNED}{unsigned} -\SetKw{AND}{and} -\SetKw{OR}{or} -$\CODING \leftarrow$ \READ 2 unsigned bits\; -$\PORDER \leftarrow$ \READ 4 unsigned bits\; -$i \leftarrow 0$\; -\BlankLine -\For(\tcc*[f]{read residual partitions}){$p \leftarrow 0$ \emph{\KwTo}$2 ^ {\text{\PORDER}}$}{ - \uIf{$\text{\CODING} = 0$}{ - $\text{\RICE}_p \leftarrow$ \READ 4 unsigned bits\; - } - \uElseIf{$\text{\CODING} = 1$}{ - $\text{\RICE}_p \leftarrow$ \READ 5 unsigned bits\; - } - \Else{ - undefined residual coding method error\; - } - \eIf{p = 0}{ - $\text{\PCOUNT}_p \leftarrow \lfloor\text{block size} \div 2 ^ {\PORDER}\rfloor - \text{\ORDER}$ - }{ - $\text{\PCOUNT}_p \leftarrow \lfloor\text{block size} \div 2 ^ {\PORDER}\rfloor$ - } - \eIf{$((\text{\CODING} = 0)~\text{\AND}~(\text{\RICE}_p = 15))$ \OR - $((\text{\CODING} = 1)~\text{\AND}~(\text{\RICE}_p = 31))$}{ - $\text{\ESCAPE}_p \leftarrow$ \READ 5 unsigned bits\; - \For{$j \leftarrow 0$ \emph{\KwTo}$\text{\PCOUNT}_p$}{ - $\text{\RESIDUAL}_i \leftarrow$ \READ $\text{\ESCAPE}_p$ signed bits\; - $i \leftarrow i + 1$\; - } - }{ - \For{$j \leftarrow 0$ \emph{\KwTo}$\text{\PCOUNT}_p$} { - $\text{\MSB}_i \leftarrow$ \UNARY with stop bit 1\; - $\text{\LSB}_i \leftarrow$ \READ $\text{\RICE}_p$ unsigned bits\; - $\text{\UNSIGNED}_i \leftarrow \text{\MSB}_i \times 2 ^ {\text{\RICE}_p} + \text{\LSB}_i$\; - \eIf(\tcc*[f]{apply sign bit}){$(\text{\UNSIGNED}_i \bmod 2) = 0$}{ - $\text{\RESIDUAL}_i \leftarrow \text{\UNSIGNED}_i \div 2$\; - }{ - $\text{\RESIDUAL}_i \leftarrow -\lfloor \text{\UNSIGNED}_i \div 2 \rfloor - 1$\; - } - $i \leftarrow i + 1$\; - } - } -} -\Return \RESIDUAL\; -\EALGORITHM -} - -\clearpage - -\begin{figure}[h] -\includegraphics{figures/flac/residual.pdf} -\end{figure} -\par -\noindent -As an example, we'll decode 10 residual values from the following bytes: -\begin{figure}[h] -\includegraphics{figures/flac/residual-parse.pdf} -\end{figure} -\par -\noindent -{\relsize{-1} -\begin{tabular}{rcl|rcl} -\textsf{coding method} & $\leftarrow$ & 0 \\ -\textsf{partition order} & $\leftarrow$ & 0 \\ -\textsf{Rice parameter} & $\leftarrow$ & 2 \\ -\hline -$\textsf{MSB}_0$ & $\leftarrow$ & 0 & -$\textsf{MSB}_5$ & $\leftarrow$ & 2 \\ -$\textsf{LSB}_0$ & $\leftarrow$ & 3 & -$\textsf{LSB}_5$ & $\leftarrow$ & 1 \\ -$\textsf{unsigned}_0$ & $\leftarrow$ & $0 \times 2 ^ 2 + 3 = 3$ & -$\textsf{unsigned}_5$ & $\leftarrow$ & $2 \times 2 ^ 2 + 1 = 9$ \\ -$\textsf{residual}_0$ & $\leftarrow$ & $-\lfloor 3 \div 2\rfloor - 1 = -2$ & -$\textsf{residual}_5$ & $\leftarrow$ & $-\lfloor 9 \div 2\rfloor - 1 = -5$ \\ -\hline -$\textsf{MSB}_1$ & $\leftarrow$ & 1 & -$\textsf{MSB}_6$ & $\leftarrow$ & 2 \\ -$\textsf{LSB}_1$ & $\leftarrow$ & 2 & -$\textsf{LSB}_6$ & $\leftarrow$ & 0 \\ -$\textsf{unsigned}_1$ & $\leftarrow$ & $1 \times 2 ^ 2 + 2 = 6$ & -$\textsf{unsigned}_6$ & $\leftarrow$ & $2 \times 2 ^ 2 + 0 = 8$ \\ -$\textsf{residual}_1$ & $\leftarrow$ & $6 \div 2 = 3$ & -$\textsf{residual}_6$ & $\leftarrow$ & $8 \div 2 = 4$ \\ -\hline -$\textsf{MSB}_2$ & $\leftarrow$ & 0 & -$\textsf{MSB}_7$ & $\leftarrow$ & 0 \\ -$\textsf{LSB}_2$ & $\leftarrow$ & 1 & -$\textsf{LSB}_7$ & $\leftarrow$ & 3 \\ -$\textsf{unsigned}_2$ & $\leftarrow$ & $0 \times 2 ^ 2 + 1 = 1$ & -$\textsf{unsigned}_7$ & $\leftarrow$ & $0 \times 2 ^ 2 + 3 = 3$ \\ -$\textsf{residual}_2$ & $\leftarrow$ & $-\lfloor 1 \div 2\rfloor - 1 = -1$ & -$\textsf{residual}_7$ & $\leftarrow$ & $-\lfloor 3 \div 2\rfloor - 1 = -2$ \\ -\hline -$\textsf{MSB}_3$ & $\leftarrow$ & 2 & -$\textsf{MSB}_8$ & $\leftarrow$ & 1 \\ -$\textsf{LSB}_3$ & $\leftarrow$ & 1 & -$\textsf{LSB}_8$ & $\leftarrow$ & 1 \\ -$\textsf{unsigned}_3$ & $\leftarrow$ & $2 \times 2 ^ 2 + 1 = 9$ & -$\textsf{unsigned}_8$ & $\leftarrow$ & $1 \times 2 ^ 2 + 1 = 5$ \\ -$\textsf{residual}_3$ & $\leftarrow$ & $-\lfloor 9 \div 2\rfloor - 1 = -5$ & -$\textsf{residual}_8$ & $\leftarrow$ & $-\lfloor 5 \div 2\rfloor - 1 = -3$ \\ -\hline -$\textsf{MSB}_4$ & $\leftarrow$ & 0 & -$\textsf{MSB}_9$ & $\leftarrow$ & 0 \\ -$\textsf{LSB}_4$ & $\leftarrow$ & 2 & -$\textsf{LSB}_9$ & $\leftarrow$ & 2 \\ -$\textsf{unsigned}_4$ & $\leftarrow$ & $0 \times 2 ^ 2 + 2 = 2$ & -$\textsf{unsigned}_9$ & $\leftarrow$ & $0 \times 2 ^ 2 + 2 = 2$ \\ -$\textsf{residual}_4$ & $\leftarrow$ & $2 \div 2 = 1$ & -$\textsf{residual}_9$ & $\leftarrow$ & $2 \div 2 = 1$ \\ -\end{tabular} -} -\par -\noindent -\vskip .25in -for a final set of residuals: -2, 3, -1, -5, 1, -5, 4, -2, -3 and 1. - -\clearpage - -\subsection{Calculating Frame CRC-16} -\label{flac:verify_crc16} -CRC-16 is used to checksum the entire FLAC frame, including the header -and any padding bits after the final subframe. -Given a byte of input and the previous CRC-16 checksum, -or 0 as an initial value, the current checksum can be calculated as follows: -\begin{equation} -\text{checksum}_i = \texttt{CRC16}(byte\xor(\text{checksum}_{i - 1} \gg 8 ))\xor(\text{checksum}_{i - 1} \ll 8) -\end{equation} -\par -\noindent -and the checksum is always truncated to 16-bits. -\begin{table}[h] -{\relsize{-3}\ttfamily -\begin{tabular}{|r||r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} -\hline - & 0x?0 & 0x?1 & 0x?2 & 0x?3 & 0x?4 & 0x?5 & 0x?6 & 0x?7 & 0x?8 & 0x?9 & 0x?A & 0x?B & 0x?C & 0x?D & 0x?E & 0x?F \\ -\hline -0x0? & 0000 & 8005 & 800f & 000a & 801b & 001e & 0014 & 8011 & 8033 & 0036 & 003c & 8039 & 0028 & 802d & 8027 & 0022 \\ -0x1? & 8063 & 0066 & 006c & 8069 & 0078 & 807d & 8077 & 0072 & 0050 & 8055 & 805f & 005a & 804b & 004e & 0044 & 8041 \\ -0x2? & 80c3 & 00c6 & 00cc & 80c9 & 00d8 & 80dd & 80d7 & 00d2 & 00f0 & 80f5 & 80ff & 00fa & 80eb & 00ee & 00e4 & 80e1 \\ -0x3? & 00a0 & 80a5 & 80af & 00aa & 80bb & 00be & 00b4 & 80b1 & 8093 & 0096 & 009c & 8099 & 0088 & 808d & 8087 & 0082 \\ -0x4? & 8183 & 0186 & 018c & 8189 & 0198 & 819d & 8197 & 0192 & 01b0 & 81b5 & 81bf & 01ba & 81ab & 01ae & 01a4 & 81a1 \\ -0x5? & 01e0 & 81e5 & 81ef & 01ea & 81fb & 01fe & 01f4 & 81f1 & 81d3 & 01d6 & 01dc & 81d9 & 01c8 & 81cd & 81c7 & 01c2 \\ -0x6? & 0140 & 8145 & 814f & 014a & 815b & 015e & 0154 & 8151 & 8173 & 0176 & 017c & 8179 & 0168 & 816d & 8167 & 0162 \\ -0x7? & 8123 & 0126 & 012c & 8129 & 0138 & 813d & 8137 & 0132 & 0110 & 8115 & 811f & 011a & 810b & 010e & 0104 & 8101 \\ -0x8? & 8303 & 0306 & 030c & 8309 & 0318 & 831d & 8317 & 0312 & 0330 & 8335 & 833f & 033a & 832b & 032e & 0324 & 8321 \\ -0x9? & 0360 & 8365 & 836f & 036a & 837b & 037e & 0374 & 8371 & 8353 & 0356 & 035c & 8359 & 0348 & 834d & 8347 & 0342 \\ -0xA? & 03c0 & 83c5 & 83cf & 03ca & 83db & 03de & 03d4 & 83d1 & 83f3 & 03f6 & 03fc & 83f9 & 03e8 & 83ed & 83e7 & 03e2 \\ -0xB? & 83a3 & 03a6 & 03ac & 83a9 & 03b8 & 83bd & 83b7 & 03b2 & 0390 & 8395 & 839f & 039a & 838b & 038e & 0384 & 8381 \\ -0xC? & 0280 & 8285 & 828f & 028a & 829b & 029e & 0294 & 8291 & 82b3 & 02b6 & 02bc & 82b9 & 02a8 & 82ad & 82a7 & 02a2 \\ -0xD? & 82e3 & 02e6 & 02ec & 82e9 & 02f8 & 82fd & 82f7 & 02f2 & 02d0 & 82d5 & 82df & 02da & 82cb & 02ce & 02c4 & 82c1 \\ -0xE? & 8243 & 0246 & 024c & 8249 & 0258 & 825d & 8257 & 0252 & 0270 & 8275 & 827f & 027a & 826b & 026e & 0264 & 8261 \\ -0xF? & 0220 & 8225 & 822f & 022a & 823b & 023e & 0234 & 8231 & 8213 & 0216 & 021c & 8219 & 0208 & 820d & 8207 & 0202 \\ -\hline -\end{tabular} -} -\end{table} -\par -\noindent -For example, given the frame bytes: -\texttt{FF F8 CC 1C 00 C0 EB 00 00 00 00 00 00 00 00}, -the frame's CRC-16 can be calculated: -{\relsize{-2} -\begin{align*} -\CRCSIXTEEN{0}{0xFF}{0x0000}{0xFF}{0x0000}{0x0202} \\ -\CRCSIXTEEN{1}{0xF8}{0x0202}{0xFA}{0x0200}{0x001C} \\ -\CRCSIXTEEN{2}{0xCC}{0x001C}{0xCC}{0x1C00}{0x1EA8} \\ -\CRCSIXTEEN{3}{0x1C}{0x1EA8}{0x02}{0xA800}{0x280F} \\ -\CRCSIXTEEN{4}{0x00}{0x280F}{0x28}{0x0F00}{0x0FF0} \\ -\CRCSIXTEEN{5}{0xC0}{0x0FF0}{0xCF}{0xF000}{0xF2A2} \\ -\CRCSIXTEEN{6}{0xEB}{0xF2A2}{0x19}{0xA200}{0x2255} \\ -\CRCSIXTEEN{7}{0x00}{0x2255}{0x22}{0x5500}{0x55CC} \\ -\CRCSIXTEEN{8}{0x00}{0x55CC}{0x55}{0xCC00}{0xCDFE} \\ -\CRCSIXTEEN{9}{0x00}{0xCDFE}{0xCD}{0xFE00}{0x7CAD} \\ -\CRCSIXTEEN{10}{0x00}{0x7CAD}{0x7C}{0xAD00}{0x2C0B} \\ -\CRCSIXTEEN{11}{0x00}{0x2C0B}{0x2C}{0x0B00}{0x8BEB} \\ -\CRCSIXTEEN{12}{0x00}{0x8BEB}{0x8B}{0xEB00}{0xE83A} \\ -\CRCSIXTEEN{13}{0x00}{0xE83A}{0xE8}{0x3A00}{0x3870} \\ -\CRCSIXTEEN{14}{0x00}{0x3870}{0x38}{0x7000}{0xF093} \\ -\intertext{Thus, the next two bytes after the final subframe should be -\texttt{0xF0} and \texttt{0x93}. -Again, when the checksum bytes are run through the checksumming procedure:} -\CRCSIXTEEN{15}{0xF0}{0xF093}{0x00}{0x9300}{0x9300} \\ -\CRCSIXTEEN{16}{0x93}{0x9300}{0x00}{0x0000}{0x0000} -\end{align*} -the result will also always be 0, just as in the CRC-8. -} - -\clearpage - -\subsection{Recombining Subframes} -\label{flac:recombine_subframes} -\ALGORITHM{the frame's block size and channel assignment, a set of decoded subframe samples\footnote{$\textsf{subframe}_{x~y}$ indicates the $y$th sample in subframe $x$}}{a list of signed PCM frames per channel} -\SetKwData{ENCODEDCHANNELS}{encoded channels} -\SetKwData{CHANNELCOUNT}{channel count} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{SUBFRAME}{subframe} -\SetKwData{CHANNEL}{channel} -\uIf(\tcc*[f]{independent}){$0 \leq \ENCODEDCHANNELS \leq 7$}{ - \CHANNELCOUNT $\leftarrow \ENCODEDCHANNELS + 1$\; - \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ - \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\CHANNEL}_{c~i} \leftarrow \text{\SUBFRAME}_{c~i}$ - } - } -} -\uElseIf(\tcc*[f]{left-difference}){$\ENCODEDCHANNELS = 8$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\CHANNEL}_{0~i} \leftarrow \text{\SUBFRAME}_{0~i}$\; - $\text{\CHANNEL}_{1~i} \leftarrow \text{\SUBFRAME}_{0~i} - \text{\SUBFRAME}_{1~i}$\; - } -} -\uElseIf(\tcc*[f]{difference-right}){$\ENCODEDCHANNELS = 9$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\CHANNEL}_{0~i} \leftarrow \text{\SUBFRAME}_{0~i} + \text{\SUBFRAME}_{1~i}$\; - $\text{\CHANNEL}_{1~i} \leftarrow \text{\SUBFRAME}_{1~i}$\; - } -} -\ElseIf(\tcc*[f]{mid-side}){$\ENCODEDCHANNELS = 10$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\CHANNEL}_{0~i} \leftarrow \lfloor(((\text{\SUBFRAME}_{0~i} \times 2) + (\text{\SUBFRAME}_{1~i} \bmod 2)) + \text{\SUBFRAME}_{1~i}) \div 2\rfloor$\; - $\text{\CHANNEL}_{1~i} \leftarrow \lfloor(((\text{\SUBFRAME}_{0~i} \times 2) + (\text{\SUBFRAME}_{1~i} \bmod 2)) - \text{\SUBFRAME}_{1~i}) \div 2\rfloor$\; - } -} -\Return \CHANNEL\; -\EALGORITHM - -\clearpage - -\subsection{Updating Stream MD5 Sum} -\label{flac:update_md5} -\ALGORITHM{the frame's signed PCM samples\footnote{$channel_{c~i}$ indicates the $i$th sample in channel $c$}}{the stream's updated MD5 sum} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{CHANNELCOUNT}{channel count} -\SetKwData{CHANNEL}{channel} -\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ - \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ - bytes $\leftarrow \text{\CHANNEL}_{c~i}$ as signed, little-endian bytes\; - update stream's MD5 sum with bytes\; - } -} -\Return stream's MD5 sum\; -\EALGORITHM -\vskip .25in -\par -\noindent -For example, given a 16 bits per sample stream with the signed sample values: -\begin{table}[h] -\begin{tabular}{r|rr} -$i$ & $\textsf{channel}_{0~i}$ & $\textsf{channel}_{1~i}$ \\ -\hline -0 & 1 & -1 \\ -1 & 2 & -2 \\ -2 & 3 & -3 \\ -\end{tabular} -\end{table} -\par -\noindent -are translated to the bytes: -\begin{table}[h] -\begin{tabular}{r|rr} -$i$ & $\textsf{channel}_{0~i}$ & $\textsf{channel}_{1~i}$ \\ -\hline -0 & \texttt{01 00} & \texttt{FF FF} \\ -1 & \texttt{02 00} & \texttt{FE FF} \\ -2 & \texttt{03 00} & \texttt{FD FF} \\ -\end{tabular} -\end{table} -\par -\noindent -and combined as: -\vskip .15in -\par -\noindent -\texttt{01 00 FF FF 02 00 FE FF 03 00 FD FF} -\vskip .15in -\par -\noindent -whose MD5 sum is: -\vskip .15in -\par -\noindent -\texttt{E7482f6462B27EE04EADC079291C79E9} - -\clearpage - -\section{FLAC Encoding} - -The basic process for encoding a FLAC file is as follows: -\par -\noindent -\ALGORITHM{PCM frames, various encoding parameters: -\newline -{\relsize{-1} -\begin{tabular}{rll} -parameter & possible values & typical values \\ -\hline -block size & a positive number of PCM frames & 1152 or 4096 \\ -maximum LPC order & integer between 0 and 32, inclusive & 0, 6, 8 or 12 \\ -minimum partition order & integer between 0 and 16, inclusive & 0 \\ -maximum partition order & integer between 0 and 16, inclusive & 3, 4, 5 or 6 \\ -maximum Rice parameter & 14 if bits-per-sample $\leq 16$, otherwise 30 & \\ -try mid-side & true or false & \\ -try adaptive mid-side & true or false & \\ -QLP precision & $\begin{cases} -7 & \text{ if } 0 < \text{block size} \leq 192 \\ -8 & \text{ if } 192 < \text{block size} \leq 384 \\ -9 & \text{ if } 384 < \text{block size} \leq 576 \\ -10 & \text{ if } 576 < \text{block size} \leq 1152 \\ -11 & \text{ if } 1152 < \text{block size} \leq 2304 \\ -12 & \text{ if } 2304 < \text{block size} \leq 4608 \\ -13 & \text{ if } \text{block size} > 4608 \\ -\end{cases}$ & \\ -exhaustive model search & true or false & \\ -\end{tabular} -} -}{an encoded FLAC file} -\SetKwData{BLOCKSIZE}{block size} -$\texttt{"fLaC"} \rightarrow$ \WRITE 4 bytes\; -\hyperref[flac:write_placeholder_blocks]{write placeholder STREAMINFO metadata block}\; -write PADDING metadata block\; -initialize stream's MD5 sum\; -\While{PCM frames remain}{ - take \BLOCKSIZE PCM frames from the input\; - \hyperref[flac:update_md5_w]{update the stream's MD5 sum with that PCM data}\; - \hyperref[flac:encode_frame]{encode a FLAC frame from PCM frames using the given encoding parameters}\; - update STREAMINFO's values from the FLAC frame\; -} -return to the start of the file and rewrite the STREAMINFO metadata block\; -\EALGORITHM -\begin{figure}[h] -\includegraphics{figures/flac/stream3.pdf} -\end{figure} -\par -\noindent -All of the fields in the FLAC stream are big-endian. - -\clearpage - -\subsection{Writing Placeholder Metadata Blocks} -\label{flac:write_placeholder_blocks} -\ALGORITHM{input stream's attributes, a default block size}{1 or more metadata blocks to the FLAC file stream} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{SAMPLERATE}{sample rate} -\SetKwData{CHANNELS}{channel count} -\SetKwData{BPS}{bits per sample} -$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{is last block} -$0 \rightarrow$ \WRITE 7 unsigned bits\tcc*[r]{STREAMINFO type} -$34 \rightarrow$ \WRITE 24 unsigned bits\tcc*[r]{STREAMINFO size} -$\BLOCKSIZE \rightarrow$ \WRITE 16 unsigned bits\tcc*[r]{minimum block size} -$\BLOCKSIZE \rightarrow$ \WRITE 16 unsigned bits\tcc*[r]{maximum block size} -$0 \rightarrow$ \WRITE 24 unsigned bits\tcc*[r]{minimum frame size} -$0 \rightarrow$ \WRITE 24 unsigned bits\tcc*[r]{maximum frame size} -$\SAMPLERATE \rightarrow$ \WRITE 20 unsigned bits\; -$\CHANNELS - 1 \rightarrow$ \WRITE 3 unsigned bits\; -$\BPS - 1 \rightarrow$ \WRITE 5 unsigned bits\; -$0 \rightarrow$ \WRITE 36 unsigned bits\tcc*[r]{total PCM frames} -$0 \rightarrow$ \WRITE 16 bytes\tcc*[r]{stream's MD5 sum} -\BlankLine -\BlankLine -$1 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{is last block} -$1 \rightarrow$ \WRITE 7 unsigned bits\tcc*[r]{PADDING type} -$4096 \rightarrow$ \WRITE 24 unsigned bits\tcc*[r]{PADDING size} -$0 \rightarrow$ \WRITE 4096 bytes\tcc*[r]{PADDING's data} -\EALGORITHM -\par -\noindent -PADDING can be some size other than 4096 bytes. -One simply wants to leave enough room for a VORBIS\_COMMENT block, -SEEKTABLE and so forth. -Other fields such as the minimum/maximum frame size -and the stream's final MD5 sum can't be known in advance; -we'll need to return to this block once encoding is finished -in order to populate them. -\begin{figure}[h] -\includegraphics{figures/flac/metadata.pdf} -\end{figure} - - -\clearpage - -\subsection{Updating Stream MD5 Sum} -\label{flac:update_md5_w} -\ALGORITHM{the frame's signed PCM input samples\footnote{$channel_{c~i}$ indicates the $i$th sample in channel $c$}}{the stream's updated MD5 sum} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{CHANNELCOUNT}{channel count} -\SetKwData{CHANNEL}{channel} -\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ - \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ - bytes $\leftarrow \text{\CHANNEL}_{c~i}$ as signed, little-endian bytes\; - update stream's MD5 sum with bytes\; - } -} -\Return stream's MD5 sum\; -\EALGORITHM -\par -\noindent -For example, given a 16 bits per sample stream with the signed sample values: -\begin{table}[h] -\begin{tabular}{r|rr} -$i$ & $\textsf{channel}_0$ & $\textsf{channel}_1$ \\ -\hline -0 & 1 & -1 \\ -1 & 2 & -2 \\ -2 & 3 & -3 \\ -\end{tabular} -\end{table} -\par -\noindent -are translated to the bytes: -\begin{table}[h] -\begin{tabular}{r|rr} -$i$ & $\textsf{channel}_0$ & $\textsf{channel}_1$ \\ -\hline -0 & \texttt{01 00} & \texttt{FF FF} \\ -1 & \texttt{02 00} & \texttt{FE FF} \\ -2 & \texttt{03 00} & \texttt{FD FF} \\ -\end{tabular} -\end{table} -\par -\noindent -and combined as: -\vskip .15in -\par -\noindent -\texttt{01 00 FF FF 02 00 FE FF 03 00 FD FF} -\vskip .15in -\par -\noindent -whose MD5 sum is: -\vskip .15in -\par -\noindent -\texttt{E7482f6462B27EE04EADC079291C79E9} -\vskip .25in -\par -This process is identical to the MD5 sum calculation performed -during FLAC decoding, but performed in the opposite order. - -\clearpage - -\subsection{Encoding a FLAC Frame} -\label{flac:encode_frame} -{\relsize{-1} -\ALGORITHM{up to ``block size'' number of PCM frames, encoding parameters}{a single FLAC frame} -\SetKw{AND}{and} -\SetKw{OR}{or} -\SetKwData{CHANNELCOUNT}{channel count} -\SetKwData{BPS}{bits per sample} -\SetKwData{CHANNEL}{channel} -\SetKwData{AVERAGE}{average} -\SetKwData{DIFFERENCE}{difference} -\SetKwData{LEFTS}{left subframe} -\SetKwData{RIGHTS}{right subframe} -\SetKwData{AVGS}{average subframe} -\SetKwData{DIFFS}{difference subframe} -\SetKwData{IBITS}{independent} -\SetKwData{LDBITS}{left/difference} -\SetKwData{DRBITS}{difference/right} -\SetKwData{ADBITS}{average/difference} -\SetKwData{SUBFRAME}{subframe} -\SetKwFunction{LEN}{len} -\SetKwFunction{MIN}{min} -\SetKwFunction{BUILDSUBFRAME}{build subframe} -\SetKwFunction{CALCMIDSIDE}{calculate mid-side} -\eIf{$\CHANNELCOUNT = 2$ \AND (try mid-side \OR try adaptive mid-side)}{ - \begin{tabular}{rcl} - $(\AVERAGE~,~\DIFFERENCE)$ & $\leftarrow$ & \hyperref[flac:calc_midside]{calculate mid-side of $\text{\CHANNEL}_0$ and $\text{\CHANNEL}_1$} \\ - \LEFTS & $\leftarrow$ & \hyperref[flac:encode_subframe]{encode $\text{\CHANNEL}_0$ as subframe at $\BPS$} \\ - \RIGHTS & $\leftarrow$ & \hyperref[flac:encode_subframe]{encode $\text{\CHANNEL}_1$ as subframe at $\BPS$} \\ - \AVGS & $\leftarrow$ & \hyperref[flac:encode_subframe]{encode $\text{\AVERAGE}$ as subframe at $\BPS$} \\ - \DIFFS & $\leftarrow$ & \hyperref[flac:encode_subframe]{encode $\text{\DIFFERENCE}$ as subframe at $(\BPS + 1)$} \\ - \IBITS & $\leftarrow$ & $\LEN(\LEFTS) + \LEN(\RIGHTS)$ \\ - \LDBITS & $\leftarrow$ & $\LEN(\LEFTS) + \LEN(\DIFFS)$ \\ - \DRBITS & $\leftarrow$ & $\LEN(\DIFFS) + \LEN(\RIGHTS)$ \\ - \ADBITS & $\leftarrow$ & $\LEN(\AVGS) + \LEN(\DIFFS)$ \\ - \end{tabular}\; - \BlankLine - \uIf{try mid-side}{ - \uIf{$\IBITS < \MIN(\LDBITS~,~\DRBITS~,~\ADBITS)$}{ - \hyperref[flac:write_frame_header]{write frame header with channel assignment \texttt{0x1}}\; - write \LEFTS\; - write \RIGHTS\; - } - \uElseIf{$\LDBITS < \MIN(\DRBITS~,~\ADBITS)$}{ - \hyperref[flac:write_frame_header]{write frame header with channel assignment \texttt{0x8}}\; - write \LEFTS\; - write \DIFFS\; - } - \uElseIf{$\DRBITS < \ADBITS$}{ - \hyperref[flac:write_frame_header]{write frame header with channel assignment \texttt{0x9}}\; - write \DIFFS\; - write \RIGHTS\; - } - \Else{ - \hyperref[flac:write_frame_header]{write frame header with channel assignment \texttt{0xA}}\; - write \AVGS\; - write \DIFFS\; - } - }\uElseIf{$\IBITS < \ADBITS$}{ - \hyperref[flac:write_frame_header]{write frame header with channel assignment \texttt{0x1}}\; - write \LEFTS\; - write \RIGHTS\; - } - \Else{ - \hyperref[flac:write_frame_header]{write frame header with channel assignment \texttt{0xA}}\; - write \AVGS\; - write \DIFFS\; - } -}(\tcc*[f]{store subframes independently}){ - \hyperref[flac:write_frame_header]{write frame header with channel assignment $\CHANNELCOUNT - 1$}\; - \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ - $\text{\SUBFRAME}_c \leftarrow $ \hyperref[flac:encode_subframe]{encode $\text{\CHANNEL}_c$ as subframe at $\BPS$}\; - write $\text{\SUBFRAME}_c$\; - } -} -byte align the stream\; -\hyperref[flac:calculate_crc16]{write frame's CRC-16 checksum}\; -\EALGORITHM -} - -\clearpage - -\subsubsection{Calculating Mid-Side} -\label{flac:calc_midside} -\ALGORITHM{block size, 2 channels of PCM data}{2 channels stored as average / difference} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{CHANNEL}{channel} -\SetKwData{AVERAGE}{average} -\SetKwData{DIFFERENCE}{difference} -\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\AVERAGE}_i \leftarrow \lfloor (\text{\CHANNEL}_{0~i} + \text{\CHANNEL}_{1~i}) \div 2\rfloor$\; - $\text{\DIFFERENCE}_i \leftarrow \text{\CHANNEL}_{0~i} - \text{\CHANNEL}_{1~i}$\; -} -\Return $(\text{\AVERAGE}~,~\text{\DIFFERENCE})$\; -\EALGORITHM -\begin{align*} -\intertext{For example, given the input samples:} -\textsf{channel}_{0~0} &\leftarrow 10 \\ -\textsf{channel}_{1~0} &\leftarrow 15 -\intertext{Our average and difference samples are:} -\textsf{average}_0 &\leftarrow \left\lfloor\frac{10 + 15}{2}\right\rfloor = 12 \\ -\textsf{difference}_0 &\leftarrow 10 - 15 = -5 -\intertext{Note that the \textsf{difference} channel is identical -for left-difference, difference-right and mid-side channel assignments. -For example, when recombined from left-difference\footnotemark:} -\textsf{sample}_0 &\leftarrow 10 \\ -\textsf{sample}_1 &\leftarrow 10 - (-5) = 15 -\intertext{difference-right:} -\textsf{sample}_0 &\leftarrow -5 + 15 = 10 \\ -\textsf{sample}_1 &\leftarrow 15 -\intertext{and mid-side:} -\textsf{sample}_0 &\leftarrow \lfloor(((12 \times 2) + (-5 \bmod 2)) + -5) \div 2\rfloor = \lfloor((24 + 1 - 5) \div 2\rfloor = 10 \\ -\textsf{sample}_1 &\leftarrow \lfloor(((12 \times 2) + (-5 \bmod 2)) - -5) \div 2\rfloor = \lfloor((24 + 1 + 5) \div 2\rfloor = 15 -\end{align*} -\footnotetext{See the recombining subframes algorithms on page -\pageref{flac:recombine_subframes}.} - -\clearpage - -\subsubsection{Writing Frame Header} -\label{flac:write_frame_header} -{\relsize{-1} -\ALGORITHM{the frame's channel assignment, the input stream's parameters}{a FLAC frame header} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{SAMPLERATE}{sample rate} -\SetKwData{FRAMENUMBER}{frame number} -\SetKwData{EBLOCKSIZE}{encoded block size} -\SetKwData{ESAMPLERATE}{encoded sample rate} -\SetKwData{EBPS}{encoded bits per sample} -\SetKwData{ASSIGNMENT}{channel assignment} -\SetKwData{CRC}{CRC-8} -\SetKw{OR}{or} -$\texttt{0x3FFE} \rightarrow$ \WRITE 14 unsigned bits\tcc*[r]{sync code} -$0 \rightarrow$ \WRITE 1 unsigned bit\; -$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{blocking strategy} -$\EBLOCKSIZE \rightarrow$ \WRITE 4 unsigned bits\; -$\ESAMPLERATE \rightarrow$ \WRITE 4 unsigned bits\; -$\ASSIGNMENT \rightarrow$ \WRITE 4 unsigned bits\; -$\EBPS \rightarrow$ \WRITE 3 unsigned bits\; -$0 \rightarrow$ \WRITE 1 unsigned bit\; -$\FRAMENUMBER \rightarrow$ \WRITE \hyperref[flac:write_utf8]{as UTF-8 encoded value}\; -\uIf{$\EBLOCKSIZE = 6$}{ - $(\BLOCKSIZE - 1) \rightarrow$ \WRITE 8 unsigned bits\; -} -\ElseIf{$\EBLOCKSIZE = 7$}{ - $\BLOCKSIZE - 1 \rightarrow$ \WRITE 16 unsigned bits\; -} -\uIf{$\ESAMPLERATE = 12$}{ - $\SAMPLERATE \div 1000 \rightarrow$ \WRITE 8 unsigned bits\; -} -\uElseIf{$\ESAMPLERATE = 13$}{ - $\SAMPLERATE \rightarrow$ \WRITE 16 unsigned bits\; -} -\ElseIf{$\ESAMPLERATE = 14$}{ - $\SAMPLERATE \div 10 \rightarrow$ \WRITE 16 unsigned bits\; -} -$\CRC \leftarrow$ \hyperref[flac:calculate_crc8]{calculate frame header's CRC-8}\; -$\CRC \rightarrow$ \WRITE 8 unsigned bits\; -\EALGORITHM -} - -\subsubsection{Encoding Block Size} -{\relsize{-1} -\ALGORITHM{block size in samples}{encoded block size as 4 bit value} -\SetKwData{BLOCKSIZE}{block size} -\Switch{\BLOCKSIZE}{ -\lCase{192}{\Return 1}\; -\lCase{256}{\Return 8}\; -\lCase{512}{\Return 9}\; -\lCase{576}{\Return 2}\; -\lCase{1024}{\Return 10}\; -\lCase{1152}{\Return 3}\; -\lCase{2048}{\Return 11}\; -\lCase{2304}{\Return 4}\; -\lCase{4096}{\Return 12}\; -\lCase{4608}{\Return 5}\; -\lCase{8192}{\Return 13}\; -\lCase{16384}{\Return 14}\; -\lCase{32768}{\Return 15}\; -\Other{ - \lIf{$\BLOCKSIZE \leq 256$}{\Return 6}\; - \lElseIf{$\BLOCKSIZE \leq 65536$}{\Return 7}\; - \lElse{\Return 0} -} -} -\EALGORITHM -} - -\clearpage - -\subsubsection{Encoding Sample Rate} -{\relsize{-1} -\ALGORITHM{sample rate in Hz}{encoded sample rate as 4 bit value} -\SetKw{AND}{and} -\SetKwData{SAMPLERATE}{sample rate} -\Switch{\SAMPLERATE}{ -\lCase{8000}{\Return 4}\; -\lCase{16000}{\Return 5}\; -\lCase{22050}{\Return 6}\; -\lCase{24000}{\Return 7}\; -\lCase{32000}{\Return 8}\; -\lCase{44100}{\Return 9}\; -\lCase{48000}{\Return 10}\; -\lCase{88200}{\Return 1}\; -\lCase{96000}{\Return 11}\; -\lCase{176400}{\Return 2}\; -\lCase{192000}{\Return 3}\; -\Other{ - \lIf{$(\SAMPLERATE \bmod 1000 = 0)$ \AND $(\SAMPLERATE \leq 255000)$}{\Return 12}\; - \lElseIf{$(\SAMPLERATE \bmod 10 = 0)$ \AND $(\SAMPLERATE \leq 655350)$}{\Return 14}\; - \lElseIf{$\SAMPLERATE \leq 65535$}{\Return 13}\; - \lElse{\Return 0} -} -} -\EALGORITHM -} -\subsubsection{Encoding Bits Per Sample} -{\relsize{-1} -\ALGORITHM{bits per sample}{encoded bits per sample as 3 bit value} -\SetKwData{BPS}{bits per sample} -\Switch{\BPS}{ -\lCase{8}{\Return 1}\; -\lCase{12}{\Return 2}\; -\lCase{16}{\Return 4}\; -\lCase{20}{\Return 5}\; -\lCase{24}{\Return 6}\; -\lOther{\Return 0}\; -} -\EALGORITHM -} -\begin{figure}[h] -\includegraphics{figures/flac/frames.pdf} -\end{figure} - -\clearpage - -\subsubsection{Encoding UTF-8 Frame Number} -\label{flac:write_utf8} -{\relsize{-1} -\ALGORITHM{value as unsigned integer}{1 or more UTF-8 bytes} -\SetKwData{VALUE}{value} -\SetKwData{TOTALBYTES}{total bytes} -\SetKwData{SHIFT}{shift} -\eIf{$\VALUE \leq 127$}{ - $\VALUE \rightarrow$ \WRITE 8 unsigned bits\; -}{ - \uIf{$\VALUE \leq 2047$}{ - $\TOTALBYTES \leftarrow 2$\; - } - \uElseIf{$\VALUE \leq 65535$}{ - $\TOTALBYTES \leftarrow 3$\; - } - \uElseIf{$\VALUE \leq 2097151$}{ - $\TOTALBYTES \leftarrow 4$\; - } - \uElseIf{$\VALUE \leq 67108863$}{ - $\TOTALBYTES \leftarrow 5$\; - } - \ElseIf{$\VALUE \leq 2147483647$}{ - $\TOTALBYTES \leftarrow 6$\; - } - $\SHIFT \leftarrow (\TOTALBYTES - 1) \times 6$\; - $\TOTALBYTES \rightarrow$ \WUNARY with stop bit 0\; - $\lfloor \text{\VALUE} \div 2 ^ \text{\SHIFT} \rfloor \rightarrow$ \WRITE $(7 - \TOTALBYTES)$ unsigned bits\tcc*[r]{initial value} - $\SHIFT \leftarrow \SHIFT - 6$\; - \While{$\SHIFT \geq 0$}{ - $2 \rightarrow$ \WRITE 2 unsigned bits\tcc*[r]{continuation header} - $\lfloor \VALUE \div 2 ^ \text{\SHIFT} \rfloor \bmod 64 \rightarrow$ \WRITE 6 unsigned bits\tcc*[r]{continuation bits} - $\SHIFT \leftarrow \SHIFT - 6$\; - } -} -\EALGORITHM -} -\par -\noindent -For example, encoding the frame number 4228 in UTF-8: -\par -\noindent -\begin{wrapfigure}[10]{r}{2.375in} -\includegraphics{figures/flac/utf8.pdf} -\end{wrapfigure} -\begin{align*} -\textsf{total bytes} &\leftarrow 3 \\ -\textsf{shift} &\leftarrow 12 \\ -& 3 \rightarrow \textbf{write unary} \text{ with stop bit 1} \\ -& 1 \rightarrow \textbf{write} \text{ in 4 unsigned bits} \\ -\textsf{shift} &\leftarrow 12 - 6 = 6 \\ -& 2 \rightarrow \textbf{write} \text{ in 2 unsigned bits} \\ -& 2 \rightarrow \textbf{write} \text{ in 6 unsigned bits} \\ -\textsf{shift} &\leftarrow 6 - 6 = 0 \\ -& 2 \rightarrow \textbf{write} \text{ in 2 unsigned bits} \\ -& 4 \rightarrow \textbf{write} \text{ in 6 unsigned bits} -\end{align*} - -\clearpage - -\subsubsection{Calculating CRC-8} -\label{flac:calculate_crc8} -Given a header byte and previous CRC-8 checksum, -or 0 as an initial value: -\begin{equation*} -\text{checksum}_i = \text{CRC8}(byte\xor\text{checksum}_{i - 1}) -\end{equation*} -\begin{table}[h] -{\relsize{-3}\ttfamily -\begin{tabular}{|r||r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} -\hline - & 0x?0 & 0x?1 & 0x?2 & 0x?3 & 0x?4 & 0x?5 & 0x?6 & 0x?7 & 0x?8 & 0x?9 & 0x?A & 0x?B & 0x?C & 0x?D & 0x?E & 0x?F \\ -\hline -0x0? & 0x00 & 0x07 & 0x0E & 0x09 & 0x1C & 0x1B & 0x12 & 0x15 & 0x38 & 0x3F & 0x36 & 0x31 & 0x24 & 0x23 & 0x2A & 0x2D \\ -0x1? & 0x70 & 0x77 & 0x7E & 0x79 & 0x6C & 0x6B & 0x62 & 0x65 & 0x48 & 0x4F & 0x46 & 0x41 & 0x54 & 0x53 & 0x5A & 0x5D \\ -0x2? & 0xE0 & 0xE7 & 0xEE & 0xE9 & 0xFC & 0xFB & 0xF2 & 0xF5 & 0xD8 & 0xDF & 0xD6 & 0xD1 & 0xC4 & 0xC3 & 0xCA & 0xCD \\ -0x3? & 0x90 & 0x97 & 0x9E & 0x99 & 0x8C & 0x8B & 0x82 & 0x85 & 0xA8 & 0xAF & 0xA6 & 0xA1 & 0xB4 & 0xB3 & 0xBA & 0xBD \\ -0x4? & 0xC7 & 0xC0 & 0xC9 & 0xCE & 0xDB & 0xDC & 0xD5 & 0xD2 & 0xFF & 0xF8 & 0xF1 & 0xF6 & 0xE3 & 0xE4 & 0xED & 0xEA \\ -0x5? & 0xB7 & 0xB0 & 0xB9 & 0xBE & 0xAB & 0xAC & 0xA5 & 0xA2 & 0x8F & 0x88 & 0x81 & 0x86 & 0x93 & 0x94 & 0x9D & 0x9A \\ -0x6? & 0x27 & 0x20 & 0x29 & 0x2E & 0x3B & 0x3C & 0x35 & 0x32 & 0x1F & 0x18 & 0x11 & 0x16 & 0x03 & 0x04 & 0x0D & 0x0A \\ -0x7? & 0x57 & 0x50 & 0x59 & 0x5E & 0x4B & 0x4C & 0x45 & 0x42 & 0x6F & 0x68 & 0x61 & 0x66 & 0x73 & 0x74 & 0x7D & 0x7A \\ -0x8? & 0x89 & 0x8E & 0x87 & 0x80 & 0x95 & 0x92 & 0x9B & 0x9C & 0xB1 & 0xB6 & 0xBF & 0xB8 & 0xAD & 0xAA & 0xA3 & 0xA4 \\ -0x9? & 0xF9 & 0xFE & 0xF7 & 0xF0 & 0xE5 & 0xE2 & 0xEB & 0xEC & 0xC1 & 0xC6 & 0xCF & 0xC8 & 0xDD & 0xDA & 0xD3 & 0xD4 \\ -0xA? & 0x69 & 0x6E & 0x67 & 0x60 & 0x75 & 0x72 & 0x7B & 0x7C & 0x51 & 0x56 & 0x5F & 0x58 & 0x4D & 0x4A & 0x43 & 0x44 \\ -0xB? & 0x19 & 0x1E & 0x17 & 0x10 & 0x05 & 0x02 & 0x0B & 0x0C & 0x21 & 0x26 & 0x2F & 0x28 & 0x3D & 0x3A & 0x33 & 0x34 \\ -0xC? & 0x4E & 0x49 & 0x40 & 0x47 & 0x52 & 0x55 & 0x5C & 0x5B & 0x76 & 0x71 & 0x78 & 0x7F & 0x6A & 0x6D & 0x64 & 0x63 \\ -0xD? & 0x3E & 0x39 & 0x30 & 0x37 & 0x22 & 0x25 & 0x2C & 0x2B & 0x06 & 0x01 & 0x08 & 0x0F & 0x1A & 0x1D & 0x14 & 0x13 \\ -0xE? & 0xAE & 0xA9 & 0xA0 & 0xA7 & 0xB2 & 0xB5 & 0xBC & 0xBB & 0x96 & 0x91 & 0x98 & 0x9F & 0x8A & 0x8D & 0x84 & 0x83 \\ -0xF? & 0xDE & 0xD9 & 0xD0 & 0xD7 & 0xC2 & 0xC5 & 0xCC & 0xCB & 0xE6 & 0xE1 & 0xE8 & 0xEF & 0xFA & 0xFD & 0xF4 & 0xF3 \\ -\hline -\end{tabular} -} -\end{table} - -\subsubsection{Frame Header Encoding Example} -Given a frame header with the following attributes: -\begin{table}[h] -\begin{tabular}{rl} -block size : & 4096 PCM frames \\ -sample rate : & 44100 Hz \\ -channel assignment : & 1 (2 channels stored independently) \\ -bits per sample : & 16 \\ -frame number : & 0 -\end{tabular} -\end{table} -\par -\noindent -we generate the following frame header bytes: -\begin{figure}[h] -\includegraphics{figures/flac/header-example.pdf} -\end{figure} -\par -\noindent -Note how the CRC-8 is calculated from the preceding 5 header bytes: -\begin{align*} -\text{checksum}_0 = \text{CRC8}(\texttt{FF}\xor\texttt{00}) = \texttt{F3} & & -\text{checksum}_3 = \text{CRC8}(\texttt{18}\xor\texttt{E6}) = \texttt{F4} \\ -\text{checksum}_1 = \text{CRC8}(\texttt{F8}\xor\texttt{F3}) = \texttt{31} & & -\text{checksum}_4 = \text{CRC8}(\texttt{00}\xor\texttt{F4}) = \texttt{C2} \\ -\text{checksum}_2 = \text{CRC8}(\texttt{C9}\xor\texttt{31}) = \texttt{E6} \\ -\end{align*} - -\clearpage - -\subsection{Encoding a FLAC Subframe} -\label{flac:encode_subframe} -{\relsize{-1} -\ALGORITHM{block size, signed subframe samples, subframe's bits per sample}{a FLAC subframe} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{SAMPLE}{sample} -\SetKwData{WASTEDBPS}{wasted BPS} -\SetKwData{BPS}{subframe's BPS} -\SetKwData{CONSTANT}{CONSTANT subframe} -\SetKwData{FIXED}{FIXED subframe} -\SetKwData{VERBATIM}{VERBATIM subframe} -\SetKwData{LPCPARAMS}{LPC parameters} -\SetKwData{LPC}{LPC subframe} -\SetKwFunction{LEN}{len} -\SetKwFunction{MIN}{min} -\eIf{all samples are the same}{ - \Return \hyperref[flac:encode_constant_subframe]{\CONSTANT using $\text{\SAMPLE}_0$}\; -}{ - $\WASTEDBPS \leftarrow$ \hyperref[flac:calculate_wasted_bps]{calculate wasted bits per sample for $\text{\SAMPLE}$}\; - \If{$\WASTEDBPS > 0$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\SAMPLE}_i \leftarrow \text{\SAMPLE}_i \div 2 ^ \text{\WASTEDBPS}$\; - } - $\BPS \leftarrow \BPS - \WASTEDBPS$\; - } - $\VERBATIM \leftarrow$ \hyperref[flac:encode_verbatim_subframe]{build VERBATIM subframe from \SAMPLE}\; - \BlankLine - $\FIXED \leftarrow$ \hyperref[flac:encode_fixed_subframe]{build FIXED subframe from \SAMPLE}\; - \BlankLine - \eIf(\tcc*[f]{from encoding parameters}){maximum LPC order $ > 0$}{ - $\LPCPARAMS \leftarrow$ \hyperref[flac:compute_lpc_params]{compute best LPC parameters from \SAMPLE}\; - $\LPC \leftarrow$ \hyperref[flac:encode_lpc_subframe]{build LPC subframe from \LPCPARAMS}\; - \BlankLine - \uIf{$\LEN(\VERBATIM) \leq \MIN(\LEN(\LPC)~,~\LEN(\FIXED))$}{ - \Return \VERBATIM\; - } - \uElseIf{$\LEN(\FIXED) \leq \LEN(\LPC)$}{ - \Return \FIXED\; - } - \Else{ - \Return \LPC\; - } - }{ - \eIf{$\LEN(\VERBATIM) \leq \LEN(\FIXED)$}{ - \Return \VERBATIM\; - }{ - \Return \FIXED\; - } - } -} -\EALGORITHM -} - -\subsubsection{Calculating Wasted Bits Per Sample} -\label{flac:calculate_wasted_bps} -{\relsize{-1} - \ALGORITHM{a list of signed PCM samples}{an unsigned integer} - \SetKwData{WASTEDBPS}{wasted bps} - \SetKwData{SAMPLE}{sample} - \SetKwData{BLOCKSIZE}{block size} - \SetKwFunction{MIN}{min} - \SetKwFunction{WASTED}{wasted} - $\text{\WASTEDBPS} \leftarrow \infty$\tcc*[r]{maximum unsigned integer} - \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\WASTEDBPS} \leftarrow \MIN(\WASTED(\text{\SAMPLE}_i)~,~\text{\WASTEDBPS})$\; - } - \eIf(\tcc*[f]{all samples are 0}){$\WASTEDBPS = \infty$}{ - \Return 0\; - }{ - \Return \WASTEDBPS\; - } - \EALGORITHM - where the \texttt{wasted} function is defined as: - \begin{equation*} - \texttt{wasted}(x) = - \begin{cases} - \infty & \text{if } x = 0 \\ - 0 & \text{if } x \bmod 2 = 1 \\ - 1 + \texttt{wasted}(x \div 2) & \text{if } x \bmod 2 = 0 \\ - \end{cases} - \end{equation*} -} - -\clearpage - -\subsection{Encoding a CONSTANT Subframe} -\label{flac:encode_constant_subframe} -{\relsize{-1} -\ALGORITHM{signed subframe sample, subframe's bits per sample}{a CONSTANT subframe} -\SetKwData{SAMPLE}{sample} -\SetKwData{BPS}{subframe's BPS} -$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{pad} -$0 \rightarrow$ \WRITE 6 unsigned bits\tcc*[r]{subframe type} -$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{no wasted BPS} -$\text{\SAMPLE} \rightarrow$ \WRITE $\text{\BPS}$ signed bits\; -\Return a CONSTANT subframe\; -\EALGORITHM -} -\begin{figure}[h] - \includegraphics{figures/flac/constant.pdf} -\end{figure} - -\subsection{Encoding a VERBATIM Subframe} -\label{flac:encode_verbatim_subframe} -{\relsize{-1} -\ALGORITHM{signed subframe samples, subframe's bits per sample}{a VERBATIM subframe} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{SAMPLE}{sample} -\SetKwData{BPS}{subframe's BPS} -$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{pad} -$1 \rightarrow$ \WRITE 6 unsigned bits\tcc*[r]{subframe type} -$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{no wasted BPS} -\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\SAMPLE}_i \rightarrow$ \WRITE $(\BPS)$ signed bits\; -} -\Return a VERBATIM subframe\; -\EALGORITHM -} -\begin{figure}[h] - \includegraphics{figures/flac/verbatim.pdf} -\end{figure} - -\clearpage - -\subsection{Encoding a FIXED Subframe} -\label{flac:encode_fixed_subframe} -{\relsize{-1} -\ALGORITHM{signed subframe samples, subframe's bits per sample, wasted BPS}{a FIXED subframe} -\SetKwData{BPS}{subframe's BPS} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{RESIDUAL}{residual} -\SetKwData{SAMPLE}{sample} -\SetKwData{ERROR}{total error} -\SetKwData{ORDER}{order} -\SetKwData{WASTEDBPS}{wasted BPS} -\tcc{first decide which FIXED subframe order to use} -\For(\tcc*[f]{order 0}){$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\RESIDUAL}_{0~i} \leftarrow \text{\SAMPLE}_i$\; -} -$\text{\ERROR}_0 \leftarrow \overset{\BLOCKSIZE - 1}{\underset{i = 4}{\sum}}|\text{\RESIDUAL}_{0~i}|$\; -\BlankLine -\eIf{$\BLOCKSIZE > 4$}{ -\For(\tcc*[f]{order 1-4}){$\ORDER \leftarrow 1$ \emph{\KwTo}5}{ - \For{$i \leftarrow 0$ \emph{\KwTo}$\BLOCKSIZE - \ORDER$}{ - $\text{\RESIDUAL}_{\ORDER~i} \leftarrow \text{\RESIDUAL}_{(\ORDER - 1)~(i + 1)} - \text{\RESIDUAL}_{(\ORDER - 1)~i}$\; - } - $\text{\ERROR}_{\ORDER} \leftarrow \overset{\BLOCKSIZE - \ORDER - 1}{\underset{i = 4 - \ORDER}{\sum}}|\text{\RESIDUAL}_{\ORDER~i}|$\; -} -\BlankLine -choose subframe \ORDER such that $\text{\ERROR}_{\ORDER}$ is smallest\; -}{ -use subframe \ORDER 0\; -} -\BlankLine -\tcc{then return a FIXED subframe with that order} -$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{pad} -$1 \rightarrow$ \WRITE 3 unsigned bits\tcc*[r]{subframe type} -$\text{\ORDER} \rightarrow$ \WRITE 3 unsigned bits\; -\eIf{$\WASTEDBPS > 0$}{ - $1 \rightarrow$ \WRITE 1 unsigned bit\; - $\text{\WASTEDBPS} - 1 \rightarrow$ \WUNARY with stop bit 1\; -}{ - $0 \rightarrow$ \WRITE 1 unsigned bit\; -} -\For(\tcc*[f]{warm-up samples}){$i \leftarrow 0$ \emph{\KwTo}\ORDER}{ - $\text{\SAMPLE}_i \rightarrow$ \WRITE $(\BPS)$ signed bits\; -} -\hyperref[flac:write_residual_block]{write encoded residual block based on $\text{\RESIDUAL}_{\ORDER}$'s signed values}\; -\BlankLine -\Return a FIXED subframe\; -\EALGORITHM -} -\begin{figure}[h] - \includegraphics{figures/flac/fixed.pdf} -\end{figure} - -\clearpage - -\subsubsection{FIXED Subframe Calculation Example} - -Given the subframe samples: \texttt{18, 20, 26, 24, 24, 23, 21, 24, 23, 20}: -\begin{table}[h] -\begin{tabular}{r|r|r|r|r|r} -& \textsf{order} 0 & \textsf{order} 1 & \textsf{order} 2 & \textsf{order} 3 & \textsf{order} 4 \\ -\hline -$\textsf{residual}_{o~0}$ & \texttt{\color{gray}18} & \texttt{\color{gray}2} & \texttt{\color{gray}4} & \texttt{\color{gray}-12} & \texttt{22} \\ -$\textsf{residual}_{o~1}$ & \texttt{\color{gray}20} & \texttt{\color{gray}6} & \texttt{\color{gray}-8} & \texttt{10} & \texttt{-13} \\ -$\textsf{residual}_{o~2}$ & \texttt{\color{gray}26} & \texttt{\color{gray}-2} & \texttt{2} & \texttt{-3} & \texttt{3} \\ -$\textsf{residual}_{o~3}$ & \texttt{\color{gray}24} & \texttt{0} & \texttt{-1} & \texttt{0} & \texttt{6} \\ -$\textsf{residual}_{o~4}$ & \texttt{24} & \texttt{-1} & \texttt{-1} & \texttt{6} & \texttt{-15} \\ -$\textsf{residual}_{o~5}$ & \texttt{23} & \texttt{-2} & \texttt{5} & \texttt{-9} & \texttt{11} \\ -$\textsf{residual}_{o~6}$ & \texttt{21} & \texttt{3} & \texttt{-4} & \texttt{2} \\ -$\textsf{residual}_{o~7}$ & \texttt{24} & \texttt{-1} & \texttt{-2} \\ -$\textsf{residual}_{o~8}$ & \texttt{23} & \texttt{-3} \\ -$\textsf{residual}_{o~9}$ & \texttt{20} \\ -\hline -$\textsf{total error}_{o}$ & \texttt{135} & \texttt{10} & \texttt{15} & \texttt{30} & \texttt{70} \\ -\end{tabular} -\end{table} -\par -\noindent -Note how the total number of residuals equals the -total number of samples minus the subframe's order, -to account for the warm-up samples. -Also note that if you remove the first $4 - \textsf{order}$ residuals -and sum the absolute value of the remaining residuals, -the result is the \VAR{total error} value -used when calculating the best FIXED subframe order. - -\clearpage - -\subsection{Residual Encoding} -\label{flac:write_residual_block} -{\relsize{-1} -\ALGORITHM{a set of signed residual values, the subframe's block size and predictor order, minimum and maximum partition order from encoding parameters}{an encoded block of residuals} -\SetKwData{ORDER}{predictor order} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{PAORDER}{partition order} -\SetKwData{PSIZE}{partition size} -\SetKwData{PSUM}{partition sum} -\SetKwData{RICE}{Rice} -\SetKwData{CODING}{coding method} -\SetKwData{UNSIGNED}{unsigned} -\SetKwData{PARTITION}{partition} -\SetKwData{PLEN}{partition length} -\SetKwData{MSB}{MSB} -\SetKwData{LSB}{LSB} -\SetKwFunction{SUM}{sum} -\SetKwFunction{MAX}{max} -\SetKw{BREAK}{break} -\tcc{calculate best partition order and Rice parameters for that order} -\For{$o \leftarrow \text{minimum partition order}$ \emph{\KwTo}(maximum partition order + 1)}{ - \eIf{$(\BLOCKSIZE \bmod 2^{o}) = 0$}{ - $(\text{\RICE}_o~,~\text{\PARTITION}_o~,~\text{\PSIZE}_o) \leftarrow$ \hyperref[flac:write_residual_partition]{encode residual partition(s) with order $o$}\; - }{ - \BREAK\; - } -} -\BlankLine -choose partition order $o$ such that $\PSIZE_{o}$ is smallest\; -\BlankLine -\eIf{$\MAX(\text{\RICE}_{o}) > 14$}{ - $\CODING \leftarrow 1$\; -}{ - $\CODING \leftarrow 0$\; -} -\BlankLine -\tcc{write 1 or more residual partitions to residual block} -$\CODING \rightarrow$ \WRITE 2 unsigned bits\; -$o \rightarrow$ \WRITE 4 unsigned bits\; -\For{$p \leftarrow 0$ \emph{\KwTo}$2 ^ {o}$} { - \eIf{$\CODING = 0$}{ - $\text{\RICE}_{o~p} \rightarrow$ \WRITE 4 unsigned bits\; - }{ - $\text{\RICE}_{o~p} \rightarrow$ \WRITE 5 unsigned bits\; - } - \BlankLine - \eIf{$p = 0$}{ - $\text{\PLEN}_{o~0} \leftarrow \BLOCKSIZE \div 2 ^ {o} - \ORDER$\; - }{ - $\text{\PLEN}_{o~p} \leftarrow \BLOCKSIZE \div 2 ^ {o}$\; - } - \BlankLine - \For(\tcc*[f]{write residual partition}){$i \leftarrow 0$ \emph{\KwTo}$\text{\PLEN}_{o~p}$}{ - \eIf{$\text{\PARTITION}_{o~p~i} \geq 0$}{ - $\text{\UNSIGNED}_i \leftarrow \text{\PARTITION}_{o~p~i} \times 2$\; - }{ - $\text{\UNSIGNED}_i \leftarrow (-\text{\PARTITION}_{o~p~i} - 1) \times 2 + 1$\; - } - $\text{\MSB}_i \leftarrow \lfloor \text{\UNSIGNED}_i \div 2 ^ \text{\RICE} \rfloor$\; - $\text{\LSB}_i \leftarrow \text{\UNSIGNED}_i - (\text{\MSB}_i \times 2 ^ \text{\RICE})$\; - $\text{\MSB}_i \rightarrow$ \WUNARY with stop bit 1\; - $\text{\LSB}_i \rightarrow$ \WRITE $\text{\RICE}$ unsigned bits\; - } -} -\Return encoded residual block\; -\EALGORITHM -} - -\clearpage - -\subsubsection{Encoding Partitions} -\label{flac:write_residual_partition} -{\relsize{-1} -\ALGORITHM{partition order $o$, predictor order, residual values, block size, maximum Rice parameter}{Rice parameter, residual partitions, total estimated size} -\SetKwData{ORDER}{predictor order} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{PSIZE}{partition size} -\SetKwData{PLEN}{plength} -\SetKwData{PARTITION}{partition} -\SetKwData{RESIDUAL}{residual} -\SetKwData{PSUM}{partition sum} -\SetKwData{RICE}{Rice} -\SetKwData{MAXPARAMETER}{maximum Rice parameter} -\SetKw{BREAK}{break} -$\text{\PSIZE}_o \leftarrow 0$\; -\BlankLine -\For(\tcc*[f]{split residuals into partitions}){$p \leftarrow 0$ \emph{\KwTo}$2 ^ {o}$}{ - \eIf{$p = 0$}{ - $\text{\PLEN}_{o~0} \leftarrow \BLOCKSIZE \div 2 ^ {o} - \ORDER$\; - }{ - $\text{\PLEN}_{o~p} \leftarrow \BLOCKSIZE \div 2 ^ {o}$\; - } - $\text{\PARTITION}_{o~p} \leftarrow$ get next $\text{\PLEN}_{o~p}$ values from \RESIDUAL\; - \BlankLine - $\text{\PSUM}_{o~p} \leftarrow \overset{\text{\PLEN}_{o~p} - 1}{\underset{i = 0}{\sum}} |\text{\PARTITION}_{o~p~i}|$\; - \BlankLine - $\text{\RICE}_{o~p} \leftarrow 0$\tcc*[r]{compute best Rice parameter for partition} - \While{$\text{\PLEN}_{o~p} \times 2 ^ {\text{\RICE}_{o~p}} < \text{\PSUM}_{o~p}$}{ - \eIf{$\text{\RICE}_{o~p} < \MAXPARAMETER$}{ - $\text{\RICE}_{o~p} \leftarrow \text{\RICE}_{o~p} + 1$\; - }{ - \BREAK\; - } - } - \BlankLine - \eIf(\tcc*[f]{add estimated size of partition to total size}){$\text{\RICE}_{o~p} > 0$}{ - $\text{\PSIZE}_o \leftarrow \text{\PSIZE}_o + 4 + ((1 + \text{\RICE}_{o~p}) \times \text{\PLEN}_{o~p}) + \left\lfloor\frac{\text{\PSUM}_{o~p}}{2 ^ {\text{\RICE}_{o~p} - 1}}\right\rfloor - \left\lfloor\frac{\text{\PLEN}_{o~p}}{2}\right\rfloor$\; - }{ - $\text{\PSIZE}_o \leftarrow \text{\PSIZE}_o + 4 + \text{\PLEN}_{o~p} + (\text{\PSUM}_{o~p} \times 2) - \left\lfloor\frac{\text{\PLEN}_{o~p}}{2}\right\rfloor$\; - } -} -\BlankLine -\Return $(\text{\RICE}_o~,~\text{\PARTITION}_o~,~\text{\PSIZE}_o)$\; -\EALGORITHM -} - -\begin{figure}[h] -\includegraphics{figures/flac/residual.pdf} -\end{figure} - -\clearpage - -\subsubsection{Residual Encoding Example} -Given a block size of 10 and the residuals \texttt{2, 6, -2, 0, -1, -2, 3, -1, -3}: -\begin{align*} -\intertext{for $\text{partition order} = 0$:} -\textsf{partition}_{0~0} &\leftarrow \texttt{[2, 6, -2, 0, -1, -2, 3, -1, -3]} \\ -\text{partition sum}_{0~0} &\leftarrow 2 + 6 + 2 + 0 + 1 + 2 + 3 + 1 + 3 = 20 \\ -\textsf{Rice}_{0~0} &\leftarrow 1~~(9 \times 2 ^ 0 < 20~,~9 \times 2 ^ 1 < 20~,~9 \times 2 ^ 2 > 20) \\ -\intertext{which is encoded to $encoded~residuals_{0~0}$: -\newline -\includegraphics{figures/flac/residual-example1.pdf}} -\intertext{for partition order (porder) = 1:} -\textsf{partition}_{1~0} &\leftarrow \texttt{[2, 6, -2, 0]} \\ -\text{partition sum}_{1~0} &\leftarrow 2 + 6 + 2 + 0 = 10 \\ -\textsf{Rice}_{1~0} &\leftarrow 1~~(4 \times 2 ^ 0 < 10~,~4 \times 2 ^ 1 < 10~,~4 \times 2 ^ 2 > 10) \\ -\intertext{which is encoded to $encoded~residuals_{1~0}$: -\newline -\includegraphics{figures/flac/residual-example2.pdf}} -\textsf{partition}_{1~1} &\leftarrow \texttt{[-1, -2, 3, -1, -3]} \\ -\text{partition sum}_{1~1} &\leftarrow 1 + 2 + 3 + 1 + 3 = 10 \\ -\textsf{Rice}_{1~1} &\leftarrow 1~~(4 \times 2 ^ 0 < 10~,~4 \times 2 ^ 1 < 10~,~4 \times 2 ^ 2 > 10) \\ -\intertext{which is encoded to $encoded~residuals_{1~1}$: -\newline -\includegraphics{figures/flac/residual-example3.pdf}} -\end{align*} -\par -\noindent -Since partition order 0's 33 bits, + 4 bits for one partition header, -is smaller than partition order 1's 17 bits + 16 bits + 8 bits -for two partition headers, the ideal partition order for these residuals is 0. - -\clearpage - -The 33 bit partition is packaged into a complete residual block -in which: -\newline -\begin{tabular}{rl} -$partition_{0~0}$ & $\leftarrow$ \texttt{2, 6, -2, 0, -1, -2, 3, -1, -3} \\ -$Rice_{0~0}$ & $\leftarrow 1$ \\ -partition order & $\leftarrow 0$ \\ -coding method & $\leftarrow 0$ \\ -\end{tabular} -\begin{figure}[h] -\includegraphics{figures/flac/residual-example4.pdf} -\end{figure} -\par -Finally, we package these residuals into a FIXED subframe in which: -\newline -\begin{tabular}{rl} -predictor order & $\leftarrow 1$ \\ -$\text{warm-up sample}_0$ & $\leftarrow $ 18 \\ -\end{tabular} -\begin{figure}[h] -\includegraphics{figures/flac/residual-example5.pdf} -\end{figure} -\par -\noindent -Reducing our 10, 16-bit samples from a total of 160 bits -down to only 67 bits - or about 40\% of their original size. - -\clearpage - -\subsection{Computing Best LPC Parameters} -\label{flac:compute_lpc_params} -{\relsize{-1} -\ALGORITHM{signed subframe samples, encoding parameters}{LPC parameters} -\SetKwFunction{LEN}{len} -\SetKw{AND}{and} -\SetKw{NOT}{not} -\SetKwData{SAMPLE}{sample} -\SetKwData{WINDOWED}{windowed} -\SetKwData{AUTOCORRELATIONS}{autocorrelation} -\SetKwData{LPCOEFFS}{LP coefficients} -\SetKwData{ERRORS}{error} -\SetKwData{ORDER}{predictor order} -\SetKwData{QLPCOEFFS}{QLP coefficients} -\SetKwData{QLPPRECISION}{QLP precision} -\SetKwData{QLPSHIFT}{QLP shift needed} -\SetKwData{BESTLPCPARAMS}{best LPC parameters} -\SetKwData{LPCPARAMS}{LPC parameters} -\SetKwData{LPCDATA}{LPC subframe data} -\tcc{windowed sample count equals subframe sample count} -$\WINDOWED \leftarrow$ \hyperref[flac:window]{window \SAMPLE}\; -\BlankLine -\tcc{autocorrelation value count equals the maximum LPC order + 1} -$\AUTOCORRELATIONS \leftarrow$ \hyperref[flac:autocorrelate]{autocorrelate \WINDOWED}\; -\BlankLine -\eIf{$\LEN(\AUTOCORRELATIONS) > 1$ \AND \AUTOCORRELATIONS aren't all 0.0}{ - $(\LPCOEFFS~,~\ERRORS)\leftarrow$ \hyperref[flac:compute_lp_coeffs]{compute LP coefficients from \AUTOCORRELATIONS}\; - \BlankLine - \eIf(\tcc*[f]{from encoding parameters}){\NOT exhaustive model search}{ - \tcc{estimate which set of LP coefficients is the smallest - and return those} - $o \leftarrow$ \hyperref[flac:estimate_best_order]{estimate best order from \ERRORS, sample count and bits per sample}\; - $\text{\LPCPARAMS}_o \leftarrow$ \hyperref[flac:quantize_lp_coeffs]{quantize \LPCOEFFS at order $o$}\; - \Return $\text{\LPCPARAMS}_o$\; - }{ - \tcc{build a complete LPC subframe from each set of LP coefficients - and return the parameters of the one which is smallest} - \For{$o \leftarrow 1$ \emph{\KwTo}maximum LPC order + 1}{ - $\text{\LPCPARAMS}_o \leftarrow$ \hyperref[flac:quantize_lp_coeffs]{quantize \LPCOEFFS at order $o$}\; - $\text{\LPCDATA}_o \leftarrow$ \hyperref[flac:encode_lpc_subframe]{build LPC subframe from $\text{\LPCPARAMS}_o$}\; - } - choose predictor order $o$ whose $\text{\LPCDATA}_o$ block is smallest\; - \BlankLine - \Return $\text{\LPCPARAMS}_o$\; - } -}{ - \tcc{all samples are 0, so return very basic coefficients} - \Return $\text{\LPCPARAMS} \leftarrow \left\lbrace\begin{tabular}{rcl} - \ORDER & $\leftarrow$ & 0 \\ - \QLPCOEFFS & $\leftarrow$ & \texttt{[0]} \\ - \QLPPRECISION & $\leftarrow$ & 2 \\ - \QLPSHIFT & $\leftarrow$ & 0 \\ - \end{tabular}\right.$\; -} -\EALGORITHM -} - -\clearpage - - -\subsubsection{Windowing the Input Samples} -\label{flac:window} -\ALGORITHM{a list of signed input sample integers}{a list of signed windowed samples as floats} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{SAMPLE}{sample} -\SetKwData{WINDOWED}{windowed} -\SetKwFunction{TUKEY}{tukey} -\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\WINDOWED}_i = \text{\SAMPLE}_i \times \TUKEY(i)$\; -} -\Return \WINDOWED\; -\EALGORITHM -\par -\noindent -The \VAR{Tukey} function is defined as: -\begin{equation*} -\texttt{tukey}(n) = -\begin{cases} -\frac{1}{2} \times \left[1 + cos\left(\pi \times \left(\frac{2 \times n}{\alpha \times (N - 1)} - 1 \right)\right)\right] & \text{ if } 0 \leq n \leq \frac{\alpha \times (N - 1)}{2} \\ -1 & \text{ if } \frac{\alpha \times (N - 1)}{2} \leq n \leq (N - 1) \times (1 - \frac{\alpha}{2}) \\ -\frac{1}{2} \times \left[1 + cos\left(\pi \times \left(\frac{2 \times n}{\alpha \times (N - 1)} - \frac{2}{\alpha} + 1 \right)\right)\right] & \text{ if } (N - 1) \times (1 - \frac{\alpha}{2}) \leq n \leq (N - 1) \\ -\end{cases} -\end{equation*} -\par -\noindent -Where $N$ is the total number of samples and $\alpha$ is $\nicefrac{1}{2}$. -\par -\noindent -\begin{wrapfigure}[5]{r}{3in} -\includegraphics{figures/flac/tukey.pdf} -\end{wrapfigure} -\begin{table}[h] -\begin{tabular}{r|r|r|r} -$i$ & $\textsf{sample}_i$ & $\texttt{tukey}(i)$ & $\textsf{windowed}_i$ \\ -\hline -0 & 18 & 0 & 0.0 \\ -1 & 20 & .41 & 8.2 \\ -2 & 26 & .97 & 25.2 \\ -3 & 24 & 1 & 24.0 \\ -4 & 24 & 1 & 24.0 \\ -5 & 23 & 1 & 23.0 \\ -6 & 21 & 1 & 21.0 \\ -7 & 24 & .97 & 23.3 \\ -8 & 23 & .41 & 9.4 \\ -9 & 20 & 0 & 0.0 \\ -\end{tabular} -\end{table} - -\clearpage - -\subsubsection{Performing Autocorrelation} -\label{flac:autocorrelate} -\ALGORITHM{a list of signed windowed samples, the maximum LPC order}{a list of signed autocorrelation values} -\SetKwData{MAXLPCORDER}{max LPC order} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{WINDOWED}{windowed} -\SetKwData{AUTOCORRELATED}{autocorrelated} -\For{$l \leftarrow 0$ \emph{\KwTo}$(\text{\MAXLPCORDER} + 1)$}{ - $\text{\AUTOCORRELATED}_{l} = \overset{\text{\BLOCKSIZE} - l - 1}{\underset{i = 0}{\sum}}\text{\WINDOWED}_i \times \text{\WINDOWED}_{i + l}$\; -} -\EALGORITHM -For example, given the windowed samples: -\texttt{0.0, 8.2, 25.2, 24.0, 24.0, 23.0, 21.0, 23.3, 9.4, 0.0} -and a maximum LPC order of 3: -\begin{figure}[h] -\subfloat{ - {\relsize{-2} - \begin{tabular}{rrrrr} - \texttt{0.0} & $\times$ & \texttt{0.0} & $=$ & \texttt{0.00} \\ - \texttt{8.2} & $\times$ & \texttt{8.2} & $=$ & \texttt{67.24} \\ - \texttt{25.2} & $\times$ & \texttt{25.2} & $=$ & \texttt{635.04} \\ - \texttt{24.0} & $\times$ & \texttt{24.0} & $=$ & \texttt{576.00} \\ - \texttt{24.0} & $\times$ & \texttt{24.0} & $=$ & \texttt{576.00} \\ - \texttt{23.0} & $\times$ & \texttt{23.0} & $=$ & \texttt{529.00} \\ - \texttt{21.0} & $\times$ & \texttt{21.0} & $=$ & \texttt{441.00} \\ - \texttt{23.3} & $\times$ & \texttt{23.3} & $=$ & \texttt{542.89} \\ - \texttt{9.4} & $\times$ & \texttt{9.4} & $=$ & \texttt{88.36} \\ - \texttt{0.0} & $\times$ & \texttt{0.0} & $=$ & \texttt{0.00} \\ - \hline - \multicolumn{3}{r}{$\textsf{autocorrelated}_0$} & $=$ & \texttt{3455.53} \\ - \end{tabular} - } -} -\includegraphics{figures/flac/lag0.pdf} - -\subfloat{ - {\relsize{-2} - \begin{tabular}{rrrrr} - \texttt{0.0} & $\times$ & \texttt{8.2} & $=$ & \texttt{0.00} \\ - \texttt{8.2} & $\times$ & \texttt{25.2} & $=$ & \texttt{206.64} \\ - \texttt{25.2} & $\times$ & \texttt{24.0} & $=$ & \texttt{604.80} \\ - \texttt{24.0} & $\times$ & \texttt{24.0} & $=$ & \texttt{576.00} \\ - \texttt{24.0} & $\times$ & \texttt{23.0} & $=$ & \texttt{552.00} \\ - \texttt{23.0} & $\times$ & \texttt{21.0} & $=$ & \texttt{483.00} \\ - \texttt{21.0} & $\times$ & \texttt{23.3} & $=$ & \texttt{489.30} \\ - \texttt{23.3} & $\times$ & \texttt{9.4} & $=$ & \texttt{219.02} \\ - \texttt{9.4} & $\times$ & \texttt{0.0} & $=$ & \texttt{0.00} \\ - \hline - \multicolumn{3}{r}{$\textsf{autocorrelated}_1$} & $=$ & \texttt{3130.76} \\ - \end{tabular} - } -} -\includegraphics{figures/flac/lag1.pdf} - -\subfloat{ - {\relsize{-2} - \begin{tabular}{rrrrr} - \texttt{0.0} & $\times$ & \texttt{25.2} & $=$ & \texttt{0.00} \\ - \texttt{8.2} & $\times$ & \texttt{24.0} & $=$ & \texttt{196.80} \\ - \texttt{25.2} & $\times$ & \texttt{24.0} & $=$ & \texttt{604.80} \\ - \texttt{24.0} & $\times$ & \texttt{23.0} & $=$ & \texttt{552.00} \\ - \texttt{24.0} & $\times$ & \texttt{21.0} & $=$ & \texttt{504.00} \\ - \texttt{23.0} & $\times$ & \texttt{23.3} & $=$ & \texttt{535.90} \\ - \texttt{21.0} & $\times$ & \texttt{9.4} & $=$ & \texttt{197.40} \\ - \texttt{23.3} & $\times$ & \texttt{0.0} & $=$ & \texttt{0.00} \\ - \hline - \multicolumn{3}{r}{$\textsf{autocorrelated}_2$} & $=$ & \texttt{2590.90} \\ - \end{tabular} - } -} -\includegraphics{figures/flac/lag2.pdf} - -\subfloat{ - {\relsize{-2} - \begin{tabular}{rrrrr} - \texttt{0.0} & $\times$ & \texttt{24.0} & $=$ & \texttt{0.00} \\ - \texttt{8.2} & $\times$ & \texttt{24.0} & $=$ & \texttt{196.80} \\ - \texttt{25.2} & $\times$ & \texttt{23.0} & $=$ & \texttt{579.60} \\ - \texttt{24.0} & $\times$ & \texttt{21.0} & $=$ & \texttt{504.00} \\ - \texttt{24.0} & $\times$ & \texttt{23.3} & $=$ & \texttt{559.20} \\ - \texttt{23.0} & $\times$ & \texttt{9.4} & $=$ & \texttt{216.20} \\ - \texttt{21.0} & $\times$ & \texttt{0.0} & $=$ & \texttt{0.00} \\ - \hline - \multicolumn{3}{r}{$\textsf{autocorrelated}_3$} & $=$ & \texttt{2055.80} \\ - \end{tabular} - } -} -\includegraphics{figures/flac/lag3.pdf} -\end{figure} -\par -\noindent -Note that the total number of autocorrelation values equals -the maximum LPC order + 1. - -\clearpage - -\subsubsection{LP Coefficient Calculation} -\label{flac:compute_lp_coeffs} -{\relsize{-1} -\ALGORITHM{a list of autocorrelation floats, the maximum LPC order}{a list of LP coefficient lists, a list of error values} -\SetKwData{MAXLPCORDER}{max LPC order} -\SetKwData{LPCOEFF}{LP coefficient} -\SetKwData{ERROR}{error} -\SetKwData{AUTOCORRELATION}{autocorrelated} -\begin{tabular}{rcl} -$\kappa_0$ &$\leftarrow$ & $ \AUTOCORRELATION_1 \div \AUTOCORRELATION_0$ \\ -$\LPCOEFF_{0~0}$ &$\leftarrow$ & $ \kappa_0$ \\ -$\ERROR_0$ &$\leftarrow$ & $ \AUTOCORRELATION_0 \times (1 - {\kappa_0} ^ 2)$ \\ -\end{tabular}\; -\For{$i \leftarrow 1$ \emph{\KwTo}\MAXLPCORDER}{ - \tcc{"zip" all of the previous row's LP coefficients - \newline - and the reversed autocorrelation values from 1 to i + 1 - \newline - into ($c$,$a$) pairs - \newline - $q_i$ is $\AUTOCORRELATION_{i + 1}$ minus the sum of those multiplied ($c$,$a$) pairs} - $q_i \leftarrow \AUTOCORRELATION_{i + 1}$\; - \For{$j \leftarrow 0$ \emph{\KwTo}i}{ - $q_i \leftarrow q_i - (\LPCOEFF_{(i - 1)~j} \times \AUTOCORRELATION_{i - j})$\; - } - \BlankLine - \tcc{"zip" all of the previous row's LP coefficients - \newline - and the previous row's LP coefficients reversed - \newline - into ($c$,$r$) pairs} - $\kappa_i = q_i \div \ERROR_{i - 1}$\; - \For{$j \leftarrow 0$ \emph{\KwTo}i}{ - \tcc{then build a new coefficient list of $c - (\kappa_i * r)$ for each ($c$,$r$) pair} - $\LPCOEFF_{i~j} \leftarrow \LPCOEFF_{(i - 1)~j} - (\kappa_i \times \LPCOEFF_{(i - 1)~(i - j - 1)})$\; - } - $\text{\LPCOEFF}_{i~i} \leftarrow \kappa_i$\tcc*[r]{and append $\kappa_i$ as the final coefficient in that list} - \BlankLine - $\ERROR_i \leftarrow \ERROR_{i - 1} \times (1 - {\kappa_i}^2)$\; -} -\Return $(\LPCOEFF~,~\ERROR)$\; -\EALGORITHM -} -\par -\noindent -Given a a maximum LPC order of 3 and 4 autocorrelation values: -{\relsize{-1} -\begin{align*} -\kappa_0 &\leftarrow \textsf{autocorrelation}_1 \div \textsf{autocorrelation}_0 \\ -\textsf{LP coefficient}_{0~0} &\leftarrow \kappa_0 \\ -\textsf{error}_0 &\leftarrow \textsf{autocorrelation}_0 \times (1 - {\kappa_0} ^ 2) \\ -i &= 1 \\ -q_1 &\leftarrow \textsf{autocorrelation}_2 - (\textsf{LP coefficient}_{0~0} \times \textsf{autocorrelation}_{1}) \\ -\kappa_1 &\leftarrow q_1 \div error_0 \\ -\textsf{LP coefficient}_{1~0} &\leftarrow \textsf{LP coefficient}_{0~0} - (\kappa_1 \times \textsf{LP coefficient}_{0~0}) \\ -\textsf{LP coefficient}_{1~1} &\leftarrow \kappa_1 \\ -\textsf{error}_1 &\leftarrow \textsf{error}_0 \times (1 - {\kappa_1} ^ 2) \\ -i &= 2 \\ -q_2 &\leftarrow \textsf{autocorrelation}_3 - (\textsf{LP coefficient}_{1~0} \times \textsf{autocorrelation}_{2} + \textsf{LP coefficient}_{1~1} \times \textsf{autocorrelation}_{1}) \\ -\kappa_2 &\leftarrow q_2 \div \textsf{error}_1 \\ -\textsf{LP coefficient}_{2~0} &\leftarrow \textsf{LP coefficient}_{1~0} - (\kappa_2 \times \textsf{LP coefficient}_{1~1}) \\ -\textsf{LP coefficient}_{2~1} &\leftarrow \textsf{LP coefficient}_{1~1} - (\kappa_2 \times \textsf{LP coefficient}_{1~0}) \\ -\textsf{LP coefficient}_{2~2} &\leftarrow \kappa_2 \\ -\textsf{error}_2 &\leftarrow \textsf{error}_1 \times (1 - {\kappa_2} ^ 2) \\ -\end{align*} -} - -\clearpage - -Performing this calculation with the autocorrelation values -\texttt{3455.53, 3130.76, 2590.90, 2055.80}: -{\relsize{-1} -\begin{align*} -\kappa_0 &\leftarrow 3130.76 \div 3455.53 = 0.906 \\ -\textsf{LP coefficient}_{0~0} &\leftarrow 0.906 \\ -\textsf{error}_0 &\leftarrow 3455.53 \times (1 - {0.906} ^ 2) = 619.107 \\ -i &= 1 \\ -q_1 &\leftarrow 2590.90 - (0.906 \times 3130.76) = -245.569 \\ -\kappa_1 &\leftarrow -245.569 \div 619.107 = -0.397 \\ -\textsf{LP coefficient}_{1~0} &\leftarrow 0.906 - (-0.397 \times 0.906) = 1.266 \\ -\textsf{LP coefficient}_{1~1} &\leftarrow -0.397 \\ -\textsf{error}_1 &\leftarrow 619.107 \times (1 - {-0.397} ^ 2) = 521.530 \\ -i &= 2 \\ -q_2 &\leftarrow 2055.80 - (1.266 \times 2590.90 + -0.397 \times 3130.76) = 18.632 \\ -\kappa_2 &\leftarrow 18.632 \div 521.53 = 0.036 \\ -\textsf{LP coefficient}_{2~0} &\leftarrow 1.266 - (0.036 \times -0.397) = 1.28 \\ -\textsf{LP coefficient}_{2~1} &\leftarrow -0.397 - (0.036 \times 1.266) = -0.443 \\ -\textsf{LP coefficient}_{2~2} &\leftarrow 0.036 \\ -\textsf{error}_2 &\leftarrow 521.53 \times (1 - {0.036} ^ 2) = 520.854 \\ -\end{align*} -} -\par -\noindent -With the final result of: -\begin{table}[h] -\begin{tabular}{r|rrr} -order & \multicolumn{3}{c}{\textsf{LP coefficients}} \\ -\hline -1 & \texttt{0.906} \\ -2 & \texttt{1.266} & \texttt{-0.397} \\ -3 & \texttt{1.280} & \texttt{-0.443} & \texttt{0.036} \\ -\end{tabular} -\end{table} -\par -\noindent -and \textsf{error} values: \texttt{619.107, 521.530, 520.854} - -\clearpage - -\subsubsection{Estimating Best Order} -\label{flac:estimate_best_order} -\ALGORITHM{floating point error values, block size, bits per sample,\newline QLP precision and maximum LPC order from encoding parameters}{the best estimated order value to use} -\SetKwData{ORDER}{predictor order} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{MAXLPCORDER}{max LPC order} -\SetKwData{HEADERBITS}{header bits} -\SetKwData{BSIZE}{best subframe bits} -\SetKwData{QLPPREC}{QLP precision} -\SetKwData{BPS}{bits per sample} -\SetKwData{BPR}{bits per residual} -\SetKwData{ERROR}{error} -\SetKwData{ERRORSCALE}{error scale} -\SetKwData{SUBFRAMEBITS}{subframe bits} -\SetKwFunction{MAX}{max} -\ERRORSCALE $\leftarrow \frac{\log_e(2) ^ 2}{\text{\BLOCKSIZE} \times 2}$\; -\BSIZE $\leftarrow$ maximum floating point\; -\For{$i \leftarrow 0$ \emph{\KwTo}\MAXLPCORDER}{ - $o \leftarrow i + 1$\tcc*[r]{current order} - \uIf{$\text{\ERROR}_i > 0.0$}{ - $\text{\HEADERBITS}_o \leftarrow \ORDER \times (\text{\BPS} + \text{\QLPPREC})$\; - $\text{\BPR}_o \leftarrow \MAX\left(\frac{\log_e(\text{\ERROR}_i \times \text{\ERRORSCALE})}{\log_e(2) \times 2}~,~0.0\right)$\; - $\text{\SUBFRAMEBITS}_o \leftarrow \text{\HEADERBITS}_o + \text{\BPR}_o \times (\text{\BLOCKSIZE} - \text{\ORDER})$\; - } - \ElseIf{$\text{\ERROR}_i = 0.0$}{ - \Return $o$\; - } -} -\BlankLine -\Return $o$ such as $\text{\SUBFRAMEBITS}_o$ is smallest\; -\EALGORITHM - -\clearpage - -\subsubsection{Quantizing LP Coefficients} -\label{flac:quantize_lp_coeffs} -\ALGORITHM{LP coefficients, a positive order value,\newline QLP precision from encoding parameters}{QLP coefficients as a list of integers,\newline QLP shift needed as a non-negative integer} -\SetKwData{LPCOEFF}{LP coefficient} -\SetKwData{QLPSHIFT}{QLP shift} -\SetKwData{QLPPREC}{QLP precision} -\SetKwData{ORDER}{order} -\SetKwData{QLPMAX}{QLP max} -\SetKwData{QLPMIN}{QLP min} -\SetKwData{ERROR}{error} -\SetKwData{QLPCOEFF}{QLP coefficient} -\SetKwFunction{MIN}{min} -\SetKwFunction{MAX}{max} -\SetKwFunction{ROUND}{round} -$l \leftarrow $ maximum $|c|$ for $c$ in $\text{\LPCOEFF}_{(\ORDER - 1)~0}$ to $\text{\LPCOEFF}_{(\ORDER - 1)~\ORDER}$\; -$\QLPSHIFT \leftarrow (\text{\QLPPREC} - 1) - (\lfloor \log_2(l) \rfloor - 1) - 1$\; -\uIf(\tcc*[f]{must fit into signed 5 bit field}){$\QLPSHIFT > 2 ^ 4 - 1$}{ - $\QLPSHIFT \leftarrow 2 ^ 4 - 1$\; -} -\ElseIf{$\QLPSHIFT < -(2 ^ 4)$}{ - \Return error\tcc*[r]{too much shift required for coefficients} -} - -\BlankLine -\tcc{QLP min and max are the smallest and largest QLP coefficients that fit in a signed field that's "QLP precision" bits wide} -$\QLPMAX \leftarrow 2 ^ {\text{\QLPPREC} - 1} - 1$\; -$\QLPMIN \leftarrow -(2 ^ {\text{\QLPPREC} - 1})$\; -$\ERROR \leftarrow 0.0$\; -\eIf{$\text{\QLPSHIFT} \geq 0$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}\ORDER}{ - $\ERROR \leftarrow \ERROR + \text{\LPCOEFF}_{\ORDER - 1~i} \times 2 ^ \text{\QLPSHIFT}$\; - $\text{\QLPCOEFF}_i \leftarrow \MIN(\MAX(\ROUND(\ERROR)~,~\text{\QLPMIN})~,~\text{\QLPMAX})$\; - $\ERROR \leftarrow \ERROR - \text{\QLPCOEFF}_i$\; - } - \Return $(\QLPCOEFF~,~\QLPSHIFT)$\; -}(\tcc*[f]{negative shifts are not allowed, so shrink coefficients}){ - \For{$i \leftarrow 0$ \emph{\KwTo}\ORDER}{ - $\ERROR \leftarrow \ERROR + \text{\LPCOEFF}_{\ORDER - 1~i} \div 2 ^ {-\text{\QLPSHIFT}}$\; - $\text{\QLPCOEFF}_i \leftarrow \MIN(\MAX(\ROUND(\ERROR)~,~\text{\QLPMIN})~,~\text{\QLPMAX})$\; - $\ERROR \leftarrow \ERROR - \text{\QLPCOEFF}_i$\; - } - \Return $(\QLPCOEFF~,~0)$\; -} -\EALGORITHM - -\clearpage - -For example, given the $\textsf{LP coefficient}_3$ \texttt{[1.280, -0.443, 0.036]}, -an \textsf{order} of \texttt{3} and a \textsf{QLP precision} \texttt{12}: -\begin{align*} -l &\leftarrow 1.280 \\ -\textsf{QLP shift} &\leftarrow 12 - \lfloor \log_2(1.280) \rfloor - 2 = 10 \\ -\textsf{QLP max} &\leftarrow 2047 \\ -\textsf{QLP min} &\leftarrow -2048 \\ -\textsf{error} &\leftarrow 0.0 \\ -i &= 0 \\ -\textsf{error} &\leftarrow 0.0 + 1.280 \times 2 ^ {10} = 1310.72 \\ -\textsf{QLP coefficient}_0 &\leftarrow 1311 \\ -\textsf{error} &\leftarrow 1310.72 - 1311 = -0.28 \\ -i &= 1 \\ -\textsf{error} &\leftarrow -0.28 + -0.443 \times 2 ^ {10} = -453.912 \\ -\textsf{QLP coefficient}_1 &\leftarrow -454 \\ -\textsf{error} &\leftarrow -453.912 - -454 = 0.088 \\ -i &= 2 \\ -\textsf{error} &\leftarrow 0.088 + 0.036 \times 2 ^ {10} = 36.952 \\ -\textsf{QLP coefficient}_2 &\leftarrow 37 \\ -\textsf{error} &\leftarrow 36.952 - 37 = -0.048 \\ -\end{align*} -\par -\noindent -Resulting in the QLP coefficients \texttt{1311, -454, 37} -and a QLP shift of \texttt{10}. -These values, in addition to QLP precision, -are inserted directly into a desired QLP subframe header -and are also used to calculate its residuals. - -\clearpage - -\subsection{Encoding an LPC Subframe} -\label{flac:encode_lpc_subframe} -\ALGORITHM{signed subframe samples, subframe order, QLP coefficients, QLP precision, QLP shift needed, subframe's bits per sample, wasted BPS}{an LPC subframe} -\SetKwData{ORDER}{predictor order} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{WASTEDBPS}{wasted BPS} -\SetKwData{BPS}{bits per sample} -\SetKwData{SAMPLE}{sample} -\SetKwData{RESIDUAL}{residual} -\SetKwData{QLPPREC}{QLP precision} -\SetKwData{QLPSHIFT}{QLP shift} -\SetKwData{QLPCOEFF}{QLP coefficient} -$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{pad} -$1 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{subframe type} -$(\text{\ORDER} - 1) \rightarrow$ \WRITE 5 unsigned bits\; -\eIf{$\WASTEDBPS > 0$}{ - $1 \rightarrow$ \WRITE 1 unsigned bit\; - $\text{\WASTEDBPS} - 1 \rightarrow$ \WUNARY with stop bit 1\; -}{ - $0 \rightarrow$ \WRITE 1 unsigned bit\; -} -\For(\tcc*[f]{warm-up samples}){$i \leftarrow 0$ \emph{\KwTo}\ORDER}{ - $\text{\SAMPLE}_i \rightarrow$ \WRITE $(\BPS)$ signed bits\; -} -$\text{\QLPPREC} - 1 \rightarrow$ \WRITE 4 unsigned bits\; -$\text{\QLPSHIFT} \rightarrow$ \WRITE 5 signed bits\; -\For(\tcc*[f]{QLP coefficients}){$i \leftarrow 0$ \emph{\KwTo}\ORDER}{ - $\text{\QLPCOEFF}_i \rightarrow$ \WRITE $(\QLPPREC)$ signed bits\; -} -\BlankLine -\For(\tcc*[f]{calculate signed residuals}){$i \leftarrow 0$ \emph{\KwTo}$\BLOCKSIZE - \ORDER$}{ - $\text{\RESIDUAL}_i \leftarrow \text{\SAMPLE}_{i + \ORDER} - \left \lfloor \frac{\overset{\ORDER - 1}{\underset{j = 0}{\sum}} \text{\QLPCOEFF}_j \times \text{\SAMPLE}_{i + \ORDER - j - 1} }{2 ^ \text{\QLPSHIFT}} \right \rfloor$ -} -write encoded residual block based on signed residual values\; -\Return LPC subframe\; -\EALGORITHM -\begin{figure}[h] -\includegraphics{figures/flac/lpc2.pdf} -\end{figure} +\input{flac/metadata} \clearpage -\subsubsection{LPC Subframe Residuals Calculation Example} -\begin{tabular}{rl} -\textsf{samples} : & \texttt{18, 20, 26, 24, 24, 23, 21, 24, 23, 20} \\ -\textsf{order} : & \texttt{3} \\ -\textsf{QLP precision} : &\texttt{12} \\ -\textsf{QLP shift} : & \texttt{10} \\ -\text{QLP coefficients} : & \texttt{1311, -454, 37} \\ -\end{tabular} -\newline -\begin{align*} -\textsf{residual}_0 &\leftarrow 24 - \left\lfloor\frac{1311 \times 26 + -454 \times 20 + 37 \times 18}{2 ^ {10}}\right\rfloor = 24 - \left\lfloor\frac{25672}{1024}\right\rfloor = 24 - 25 = -1 \\ -\textsf{residual}_1 &\leftarrow 24 - \left\lfloor\frac{1311 \times 24 + -454 \times 26 + 37 \times 20}{2 ^ {10}}\right\rfloor = 24 - \left\lfloor\frac{20400}{1024}\right\rfloor = 24 - 19 = 5 \\ -\textsf{residual}_2 &\leftarrow 23 - \left\lfloor\frac{1311 \times 24 + -454 \times 24 + 37 \times 26}{2 ^ {10}}\right\rfloor = 23 - \left\lfloor\frac{21530}{1024}\right\rfloor = 23 - 21 = 2 \\ -\textsf{residual}_3 &\leftarrow 21 - \left\lfloor\frac{1311 \times 23 + -454 \times 24 + 37 \times 24}{2 ^ {10}}\right\rfloor = 21 - \left\lfloor\frac{20145}{1024}\right\rfloor = 21 - 19 = 2 \\ -\textsf{residual}_4 &\leftarrow 24 - \left\lfloor\frac{1311 \times 21 + -454 \times 23 + 37 \times 24}{2 ^ {10}}\right\rfloor = 24 - \left\lfloor\frac{17977}{1024}\right\rfloor = 24 - 17 = 7 \\ -\textsf{residual}_5 &\leftarrow 23 - \left\lfloor\frac{1311 \times 24 + -454 \times 21 + 37 \times 23}{2 ^ {10}}\right\rfloor = 23 - \left\lfloor\frac{22781}{1024}\right\rfloor = 23 - 22 = 1 \\ -\textsf{residual}_6 &\leftarrow 20 - \left\lfloor\frac{1311 \times 23 + -454 \times 24 + 37 \times 21}{2 ^ {10}}\right\rfloor = 20 - \left\lfloor\frac{20034}{1024}\right\rfloor = 20 - 19 = 1 -\end{align*} -Leading to a final set of 7 residual values: \texttt{-1, 5, 2, 2, 7, 1, 1}. -Encoding them to a residual block, our final LPC subframe is: -\begin{figure}[h] -\includegraphics{figures/flac/lpc-parse2.pdf} -\end{figure} +\input{flac/decode} \clearpage -\subsection{Calculating Frame CRC-16} -\label{flac:calculate_crc16} -CRC-16 is used to checksum the entire FLAC frame, including the header -and any padding bits after the final subframe. -Given a byte of input and the previous CRC-16 checksum, -or 0 as an initial value, the current checksum can be calculated as follows: -\begin{equation} -\text{checksum}_i = \texttt{CRC16}(byte\xor(\text{checksum}_{i - 1} \gg 8 ))\xor(\text{checksum}_{i - 1} \ll 8) -\end{equation} -\par -\noindent -and the checksum is always truncated to 16-bits. -\begin{table}[h] -{\relsize{-3}\ttfamily -\begin{tabular}{|r||r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} -\hline - & 0x?0 & 0x?1 & 0x?2 & 0x?3 & 0x?4 & 0x?5 & 0x?6 & 0x?7 & 0x?8 & 0x?9 & 0x?A & 0x?B & 0x?C & 0x?D & 0x?E & 0x?F \\ -\hline -0x0? & 0000 & 8005 & 800f & 000a & 801b & 001e & 0014 & 8011 & 8033 & 0036 & 003c & 8039 & 0028 & 802d & 8027 & 0022 \\ -0x1? & 8063 & 0066 & 006c & 8069 & 0078 & 807d & 8077 & 0072 & 0050 & 8055 & 805f & 005a & 804b & 004e & 0044 & 8041 \\ -0x2? & 80c3 & 00c6 & 00cc & 80c9 & 00d8 & 80dd & 80d7 & 00d2 & 00f0 & 80f5 & 80ff & 00fa & 80eb & 00ee & 00e4 & 80e1 \\ -0x3? & 00a0 & 80a5 & 80af & 00aa & 80bb & 00be & 00b4 & 80b1 & 8093 & 0096 & 009c & 8099 & 0088 & 808d & 8087 & 0082 \\ -0x4? & 8183 & 0186 & 018c & 8189 & 0198 & 819d & 8197 & 0192 & 01b0 & 81b5 & 81bf & 01ba & 81ab & 01ae & 01a4 & 81a1 \\ -0x5? & 01e0 & 81e5 & 81ef & 01ea & 81fb & 01fe & 01f4 & 81f1 & 81d3 & 01d6 & 01dc & 81d9 & 01c8 & 81cd & 81c7 & 01c2 \\ -0x6? & 0140 & 8145 & 814f & 014a & 815b & 015e & 0154 & 8151 & 8173 & 0176 & 017c & 8179 & 0168 & 816d & 8167 & 0162 \\ -0x7? & 8123 & 0126 & 012c & 8129 & 0138 & 813d & 8137 & 0132 & 0110 & 8115 & 811f & 011a & 810b & 010e & 0104 & 8101 \\ -0x8? & 8303 & 0306 & 030c & 8309 & 0318 & 831d & 8317 & 0312 & 0330 & 8335 & 833f & 033a & 832b & 032e & 0324 & 8321 \\ -0x9? & 0360 & 8365 & 836f & 036a & 837b & 037e & 0374 & 8371 & 8353 & 0356 & 035c & 8359 & 0348 & 834d & 8347 & 0342 \\ -0xA? & 03c0 & 83c5 & 83cf & 03ca & 83db & 03de & 03d4 & 83d1 & 83f3 & 03f6 & 03fc & 83f9 & 03e8 & 83ed & 83e7 & 03e2 \\ -0xB? & 83a3 & 03a6 & 03ac & 83a9 & 03b8 & 83bd & 83b7 & 03b2 & 0390 & 8395 & 839f & 039a & 838b & 038e & 0384 & 8381 \\ -0xC? & 0280 & 8285 & 828f & 028a & 829b & 029e & 0294 & 8291 & 82b3 & 02b6 & 02bc & 82b9 & 02a8 & 82ad & 82a7 & 02a2 \\ -0xD? & 82e3 & 02e6 & 02ec & 82e9 & 02f8 & 82fd & 82f7 & 02f2 & 02d0 & 82d5 & 82df & 02da & 82cb & 02ce & 02c4 & 82c1 \\ -0xE? & 8243 & 0246 & 024c & 8249 & 0258 & 825d & 8257 & 0252 & 0270 & 8275 & 827f & 027a & 826b & 026e & 0264 & 8261 \\ -0xF? & 0220 & 8225 & 822f & 022a & 823b & 023e & 0234 & 8231 & 8213 & 0216 & 021c & 8219 & 0208 & 820d & 8207 & 0202 \\ -\hline -\end{tabular} -} -\end{table} -\par -\noindent -For example, given the frame bytes: -\texttt{FF F8 CC 1C 00 C0 EB 00 00 00 00 00 00 00 00}, -the frame's CRC-16 can be calculated: -{\relsize{-2} -\begin{align*} -\CRCSIXTEEN{0}{0xFF}{0x0000}{0xFF}{0x0000}{0x0202} \\ -\CRCSIXTEEN{1}{0xF8}{0x0202}{0xFA}{0x0200}{0x001C} \\ -\CRCSIXTEEN{2}{0xCC}{0x001C}{0xCC}{0x1C00}{0x1EA8} \\ -\CRCSIXTEEN{3}{0x1C}{0x1EA8}{0x02}{0xA800}{0x280F} \\ -\CRCSIXTEEN{4}{0x00}{0x280F}{0x28}{0x0F00}{0x0FF0} \\ -\CRCSIXTEEN{5}{0xC0}{0x0FF0}{0xCF}{0xF000}{0xF2A2} \\ -\CRCSIXTEEN{6}{0xEB}{0xF2A2}{0x19}{0xA200}{0x2255} \\ -\CRCSIXTEEN{7}{0x00}{0x2255}{0x22}{0x5500}{0x55CC} \\ -\CRCSIXTEEN{8}{0x00}{0x55CC}{0x55}{0xCC00}{0xCDFE} \\ -\CRCSIXTEEN{9}{0x00}{0xCDFE}{0xCD}{0xFE00}{0x7CAD} \\ -\CRCSIXTEEN{10}{0x00}{0x7CAD}{0x7C}{0xAD00}{0x2C0B} \\ -\CRCSIXTEEN{11}{0x00}{0x2C0B}{0x2C}{0x0B00}{0x8BEB} \\ -\CRCSIXTEEN{12}{0x00}{0x8BEB}{0x8B}{0xEB00}{0xE83A} \\ -\CRCSIXTEEN{13}{0x00}{0xE83A}{0xE8}{0x3A00}{0x3870} \\ -\CRCSIXTEEN{14}{0x00}{0x3870}{0x38}{0x7000}{0xF093} \\ -\intertext{Thus, the next two bytes after the final subframe should be -\texttt{0xF0} and \texttt{0x93}. -Again, when the checksum bytes are run through the checksumming procedure:} -\CRCSIXTEEN{15}{0xF0}{0xF093}{0x00}{0x9300}{0x9300} \\ -\CRCSIXTEEN{16}{0x93}{0x9300}{0x00}{0x0000}{0x0000} -\end{align*} -the result will also always be 0, just as in the CRC-8. -} +\input{flac/encode}
View file
audiotools-2.19.tar.gz/docs/reference/flac/decode.tex
Added
@@ -0,0 +1,881 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\section{FLAC Decoding} + +The basic process for decoding a FLAC file is as follows: +\par +\noindent +\ALGORITHM{a FLAC encoded file}{PCM samples} +\SetKwData{HEADER}{file header} +\SetKwData{PCMCOUNT}{PCM frame count} +\SetKwData{MDSUM}{MD5 sum} +$\HEADER \leftarrow$ \READ 4 bytes\; +\ASSERT $\text{\HEADER} = \texttt{"fLaC"}$\; +\hyperref[flac:read_metadata]{get \PCMCOUNT and \MDSUM from STREAMINFO metadata block}\; +skip remaining metadata blocks\; +initialize stream MD5\; +\While{$\PCMCOUNT > 0$}{ + \hyperref[flac:decode_frame]{decode FLAC frame to 1 or more PCM frames}\; + deduct FLAC frame's block size from $\PCMCOUNT$\; + \hyperref[flac:update_md5]{update stream MD5 sum with decoded PCM frame data}\; + \Return decoded PCM frames\; +} +\ASSERT STREAMINFO \MDSUM = stream MD5 sum +\EALGORITHM +\begin{figure}[h] +\includegraphics{flac/figures/stream3.pdf} +\end{figure} +\par +All of the fields in the FLAC stream are big-endian.\footnote{Except +for the length fields in the VORBIS\_COMMENT metadata block. +However, this block is not needed for decoding. +} + +\clearpage + +\subsection{Reading Metadata Blocks} +\label{flac:read_metadata} +{\relsize{-1} +\ALGORITHM{the FLAC file stream}{STREAMINFO values used for decoding} +\SetKwData{LAST}{last} +\SetKwData{TYPE}{type} +\SetKwData{SIZE}{size} +\SetKwData{MINBLOCKSIZE}{minimum block size} +\SetKwData{MAXBLOCKSIZE}{maximum block size} +\SetKwData{MINFRAMESIZE}{minimum frame size} +\SetKwData{MAXFRAMESIZE}{maximum frame size} +\SetKwData{SAMPLERATE}{sample rate} +\SetKwData{CHANNELS}{channels} +\SetKwData{BPS}{bits per sample} +\SetKwData{TOTALFRAMES}{total PCM frames} +\SetKwData{MDSUM}{MD5 sum} +\SetKwData{STREAMINFO}{STREAMINFO} +\Repeat{$\LAST = 1$}{ + $\LAST \leftarrow$ \READ 1 unsigned bit\; + $\TYPE \leftarrow$ \READ 7 unsigned bits\; + $\SIZE \leftarrow$ \READ 24 unsigned bits\; + \eIf(\tcc*[f]{read STREAMINFO metadata block}){$\TYPE = 0$}{ + \begin{tabular}{rcl} + \MINBLOCKSIZE & $\leftarrow$ & \READ 16 unsigned bits\; \\ + \MAXBLOCKSIZE & $\leftarrow$ & \READ 16 unsigned bits\; \\ + \MINFRAMESIZE & $\leftarrow$ & \READ 24 unsigned bits\; \\ + \MAXFRAMESIZE & $\leftarrow$ & \READ 24 unsigned bits\; \\ + \SAMPLERATE & $\leftarrow$ & \READ 20 unsigned bits\; \\ + \CHANNELS & $\leftarrow$ & (\READ 3 unsigned bits) + 1\; \\ + \BPS & $\leftarrow$ & (\READ 5 unsigned bits) + 1\; \\ + \TOTALFRAMES & $\leftarrow$ & \READ 36 unsigned bits\; \\ + \MDSUM & $\leftarrow$ & \READ 16 bytes\; + \end{tabular} + }(\tcc*[f]{skip other metadata blocks}){ + \SKIP \SIZE bytes\; + } +} +\Return $\text{\STREAMINFO} \leftarrow \left\lbrace\begin{tabular}{l} +\MINBLOCKSIZE \\ +\MAXBLOCKSIZE \\ +\MINFRAMESIZE \\ +\MAXFRAMESIZE \\ +\SAMPLERATE \\ +\CHANNELS \\ +\BPS \\ +\TOTALFRAMES \\ +\MDSUM \\ +\end{tabular}\right.$ +\EALGORITHM +} +\begin{figure}[h] +\includegraphics{flac/figures/metadata.pdf} +\end{figure} + +\clearpage + +For example, given the metadata bytes: +\begin{figure}[h] +\includegraphics{flac/figures/block_header.pdf} +\end{figure} +\par +\noindent +\begin{tabular}{rcrcl} +\textsf{last} & $\leftarrow$ & \texttt{0x1} & = & is last metadata block \\ +\textsf{size} & $\leftarrow$ & \texttt{0x0} & = & METADATA block \\ +\textsf{type} & $\leftarrow$ & \texttt{0x22} & = & 34 bytes \\ +\textsf{minimum block size} & $\leftarrow$ & \texttt{0x0100} & = & 4096 samples \\ +\textsf{maximum block size} & $\leftarrow$ & \texttt{0x0100} & = & 4096 samples \\ +\textsf{minimum frame size} & $\leftarrow$ & \texttt{0x00000C} & = & 12 bytes \\ +\textsf{maximum frame size} & $\leftarrow$ & \texttt{0x00000C} & = & 12 bytes \\ +\textsf{sample rate} & $\leftarrow$ & \texttt{0xAC44} & = & 44100Hz \\ +\textsf{channels} & $\leftarrow$ & \texttt{0x1} & = & 1 (+ 1) = 2 \\ +\textsf{bits per sample} & $\leftarrow$ & \texttt{0xF} & = & 15 (+ 1) = 16 \\ +\textsf{total PCM frames} & $\leftarrow$ & \texttt{0x32} & = & 50 \\ +\textsf{MD5 sum} & $\leftarrow$ & \multicolumn{3}{l}{\texttt{6D0BB00954CEB7FBEE436BB55A8397A9}} \\ +\end{tabular} + +\clearpage + +\subsection{Decoding a FLAC Frame} +\label{flac:decode_frame} +\ALGORITHM{STREAMINFO values and the FLAC file stream}{decoded PCM samples} +\SetKwData{CHANNEL}{channel} +\SetKwData{CHANNELCOUNT}{channel count} +\hyperref[flac:read_frame_header]{read frame header to determine channel count, assignment and bits-per-sample}\; +\ForEach{\CHANNEL \IN \CHANNELCOUNT}{ + \hyperref[flac:decode_subframe]{decode subframe to PCM samples based on its effective bits-per-sample}\; +} +byte-align file stream\; +\hyperref[flac:verify_crc16]{verify frame's CRC-16 checksum}\; +\hyperref[flac:recombine_subframes]{recombine subframes based on the frame's channel assignment}\; +\Return samples\; +\EALGORITHM +\begin{figure}[h] +\includegraphics{flac/figures/frames.pdf} +\end{figure} + +\clearpage + +\subsection{Reading a FLAC Frame Header} +\label{flac:read_frame_header} +{\relsize{-1} +\ALGORITHM{STREAMINFO values and the FLAC file stream}{stream information and subframe decoding parameters} +\SetKwData{SYNCCODE}{sync code} +\SetKwData{BLOCKINGSTRATEGY}{blocking strategy} +\SetKwData{ENCODEDBLOCKSIZE}{encoded block size} +\SetKwData{ENCODEDSAMPLERATE}{encoded sample rate} +\SetKwData{ENCODEDCHANNELS}{encoded channels} +\SetKwData{ENCODEDBPS}{encoded bits per sample} +\SetKwData{FRAMENUMBER}{frame number} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{SAMPLERATE}{sample rate} +\SetKwData{BPS}{bits per sample} +\SetKwData{CHANNELCOUNT}{channel count} +\SetKwData{CRC}{CRC-8} +\SetKwData{FRAMEHEADER}{frame header} +\begin{tabular}{rcl} +\SYNCCODE & $\leftarrow$ & \READ 14 unsigned bits\; \\ +& & \ASSERT $\text{\SYNCCODE} = \texttt{0x3FFE}$\; \\ +& & \SKIP 1 bit\; \\ +\BLOCKINGSTRATEGY & $\leftarrow$ & \READ 1 unsigned bit\; \\ +\ENCODEDBLOCKSIZE & $\leftarrow$ & \READ 4 unsigned bits\; \\ +\ENCODEDSAMPLERATE & $\leftarrow$ & \READ 4 unsigned bits\; \\ +\ENCODEDCHANNELS & $\leftarrow$ & \READ 4 unsigned bits\; \\ +\ENCODEDBPS & $\leftarrow$ & \READ 3 unsigned bits\; \\ +& & \SKIP 1 bit\; \\ +\FRAMENUMBER & $\leftarrow$ & \READ UTF-8 value\; \\ +\BLOCKSIZE & $\leftarrow$ & decode \ENCODEDBLOCKSIZE\; \\ +\SAMPLERATE & $\leftarrow$ & decode \ENCODEDSAMPLERATE\; \\ +\BPS & $\leftarrow$ & decode \ENCODEDBPS\; \\ +\CHANNELCOUNT & $\leftarrow$ & decode \ENCODEDCHANNELS\; \\ +\CRC & $\leftarrow$ & \READ 8 unsigned bits\; \\ +& & \hyperref[flac:verify_crc8]{verify \CRC}\; \\ +\end{tabular}\; +\BlankLine +\Return $\text{\FRAMEHEADER} \leftarrow \left\lbrace\begin{tabular}{l} +\BLOCKSIZE \\ +\SAMPLERATE \\ +\BPS \\ +\ENCODEDCHANNELS \\ +\CHANNELCOUNT \\ +\end{tabular}\right.$\; +\EALGORITHM +} + +\subsubsection{Reading UTF-8 Frame Number} +{\relsize{-1} +\ALGORITHM{FLAC file stream}{UTF-8 value as unsigned integer} +\SetKwData{TOTALBYTES}{total bytes} +\SetKwData{VALUE}{value} +\SetKwData{CONTINUATIONHEADER}{continuation header} +\SetKwData{CONTINUATIONBITS}{continuation bits} +$\TOTALBYTES \leftarrow$ \UNARY with stop bit 0\; +$\VALUE \leftarrow$ \READ (7 - $\TOTALBYTES$) unsigned bits\; +\While{$\TOTALBYTES > 0$}{ + $\CONTINUATIONHEADER \leftarrow$ \READ 2 unsigned bits\; + \ASSERT $\CONTINUATIONHEADER = 2$\; + $\CONTINUATIONBITS \leftarrow$ \READ 6 unsigned bits\; + $\VALUE \leftarrow (\VALUE \times 2 ^ 6) + \CONTINUATIONBITS$\; + $\TOTALBYTES \leftarrow \TOTALBYTES - 1$\; +} +\Return \VALUE\; +\EALGORITHM +} +For example, given the UTF-8 bytes \texttt{E1 82 84}: +\par +\begin{wrapfigure}[5]{l}{2.375in} +\includegraphics{flac/figures/utf8.pdf} +\end{wrapfigure} +\begin{align*} +\text{UTF-8 value} &= \texttt{0001 000010 000100} \\ +&= \texttt{0001 0000 1000 0100} \\ +&= \texttt{0x1084} \\ +&= 4228 +\end{align*} + +\clearpage + +\subsubsection{Decoding Block Size} +{\relsize{-1} +\begin{tabular}{rl||rl} +encoded & block size (in samples) & +encoded & block size \\ +\hline +\texttt{0000} & maximum block size from STREAMINFO & +\texttt{1000} & 256 \\ +\texttt{0001} & 192 & +\texttt{1001} & 512 \\ +\texttt{0010} & 576 & +\texttt{1010} & 1024 \\ +\texttt{0011} & 1152 & +\texttt{1011} & 2048 \\ +\texttt{0100} & 2304 & +\texttt{1100} & 4096 \\ +\texttt{0101} & 4608 & +\texttt{1101} & 8192 \\ +\texttt{0110} & (\textbf{read} 8 unsigned bits) + 1 & +\texttt{1110} & 16384 \\ +\texttt{0111} & (\textbf{read} 16 unsigned bits) + 1 & +\texttt{1111} & 32768 \\ +\end{tabular} +} + +\subsubsection{Decoding Sample Rate} +{\relsize{-1} +\begin{tabular}{rl||rl} +encoded & sample rate (in Hz) & +encoded & sample rate \\ +\hline +\texttt{0000} & from STREAMINFO & +\texttt{1000} & 32000 \\ +\texttt{0001} & 88200 & +\texttt{1001} & 44100 \\ +\texttt{0010} & 176400 & +\texttt{1010} & 48000 \\ +\texttt{0011} & 192000 & +\texttt{1011} & 96000 \\ +\texttt{0100} & 8000 & +\texttt{1100} & (\textbf{read} 8 unsigned bits) $\times$ 1000 \\ +\texttt{0101} & 16000 & +\texttt{1101} & \textbf{read} 16 unsigned bits \\ +\texttt{0110} & 22050 & +\texttt{1110} & (\textbf{read} 16 unsigned bits) $\times$ 10 \\ +\texttt{0111} & 24000 & +\texttt{1111} & invalid \\ +\end{tabular} +} + +\subsubsection{Decoding Bits per Sample} +{\relsize{-1} +\begin{tabular}{rl||rl} +encoded & bits-per-sample & +encoded & bits-per-sample \\ +\hline +\texttt{000} & from STREAMINFO & +\texttt{100} & 16 \\ +\texttt{001} & 8 & +\texttt{101} & 20 \\ +\texttt{010} & 12 & +\texttt{110} & 24 \\ +\texttt{011} & invalid & +\texttt{111} & invalid \\ +\end{tabular} +} + +\subsubsection{Decoding Channel Count and Assignment} +{\relsize{-1} +\begin{tabular}{rrl} +& channel & \\ +encoded & count & channel assignment \\ +\hline +\texttt{0000} & 1 & front Center \\ +\texttt{0001} & 2 & front Left, front Right \\ +\texttt{0010} & 3 & front Left, front Right, front Center \\ +\texttt{0011} & 4 & front Left, front Right, back Left, back Right \\ +\texttt{0100} & 5 & fL, fR, fC, back/surround left, back/surround right \\ +\texttt{0101} & 6 & fL, fR, fC, LFE, back/surround left, back/surround right \\ +\texttt{0110} & 7 & undefined \\ +\texttt{0111} & 8 & undefined \\ +\texttt{1000} & 2 & front Left, Difference \\ +\texttt{1001} & 2 & Difference, front Right \\ +\texttt{1010} & 2 & Average, Difference \\ +\texttt{1011} & & reserved \\ +\texttt{1100} & & reserved \\ +\texttt{1101} & & reserved \\ +\texttt{1110} & & reserved \\ +\texttt{1111} & & reserved \\ +\end{tabular} +} + +\subsubsection{Frame Header Decoding Example} +\begin{figure}[h] +\includegraphics{flac/figures/header-example.pdf} +\end{figure} +{\relsize{-1} +\begin{tabular}{rcl} +\textsf{sync code} & = & \texttt{0x3FFE} \\ +\textsf{encoded block size} & = & \texttt{1100b} \\ +\textsf{encoded sample rate} & = & \texttt{1001b} \\ +\textsf{encoded channels} & = & \texttt{0001b} \\ +\textsf{encoded bps} & = & \texttt{100b} \\ +\textsf{frame number} & = & 0 \\ +\textsf{block size} & = & 4096 samples \\ +\textsf{sample rate} & = & 44100Hz \\ +\textsf{bits per sample} & = & 16 \\ +\textsf{channel count} & = & 2 \\ +\textsf{channel assignment} & = & front left, front right +\end{tabular} +} +\subsubsection{Calculating Frame Header CRC-8} +\label{flac:verify_crc8} +Given a header byte and previous CRC-8 checksum, +or 0 as an initial value: +\begin{equation*} +\text{checksum}_i = \texttt{CRC8}(byte\xor\text{checksum}_{i - 1}) +\end{equation*} +\begin{table}[h] +{\relsize{-3}\ttfamily +\begin{tabular}{|r||r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} +\hline + & 0x?0 & 0x?1 & 0x?2 & 0x?3 & 0x?4 & 0x?5 & 0x?6 & 0x?7 & 0x?8 & 0x?9 & 0x?A & 0x?B & 0x?C & 0x?D & 0x?E & 0x?F \\ +\hline +0x0? & 0x00 & 0x07 & 0x0E & 0x09 & 0x1C & 0x1B & 0x12 & 0x15 & 0x38 & 0x3F & 0x36 & 0x31 & 0x24 & 0x23 & 0x2A & 0x2D \\ +0x1? & 0x70 & 0x77 & 0x7E & 0x79 & 0x6C & 0x6B & 0x62 & 0x65 & 0x48 & 0x4F & 0x46 & 0x41 & 0x54 & 0x53 & 0x5A & 0x5D \\ +0x2? & 0xE0 & 0xE7 & 0xEE & 0xE9 & 0xFC & 0xFB & 0xF2 & 0xF5 & 0xD8 & 0xDF & 0xD6 & 0xD1 & 0xC4 & 0xC3 & 0xCA & 0xCD \\ +0x3? & 0x90 & 0x97 & 0x9E & 0x99 & 0x8C & 0x8B & 0x82 & 0x85 & 0xA8 & 0xAF & 0xA6 & 0xA1 & 0xB4 & 0xB3 & 0xBA & 0xBD \\ +0x4? & 0xC7 & 0xC0 & 0xC9 & 0xCE & 0xDB & 0xDC & 0xD5 & 0xD2 & 0xFF & 0xF8 & 0xF1 & 0xF6 & 0xE3 & 0xE4 & 0xED & 0xEA \\ +0x5? & 0xB7 & 0xB0 & 0xB9 & 0xBE & 0xAB & 0xAC & 0xA5 & 0xA2 & 0x8F & 0x88 & 0x81 & 0x86 & 0x93 & 0x94 & 0x9D & 0x9A \\ +0x6? & 0x27 & 0x20 & 0x29 & 0x2E & 0x3B & 0x3C & 0x35 & 0x32 & 0x1F & 0x18 & 0x11 & 0x16 & 0x03 & 0x04 & 0x0D & 0x0A \\ +0x7? & 0x57 & 0x50 & 0x59 & 0x5E & 0x4B & 0x4C & 0x45 & 0x42 & 0x6F & 0x68 & 0x61 & 0x66 & 0x73 & 0x74 & 0x7D & 0x7A \\ +0x8? & 0x89 & 0x8E & 0x87 & 0x80 & 0x95 & 0x92 & 0x9B & 0x9C & 0xB1 & 0xB6 & 0xBF & 0xB8 & 0xAD & 0xAA & 0xA3 & 0xA4 \\ +0x9? & 0xF9 & 0xFE & 0xF7 & 0xF0 & 0xE5 & 0xE2 & 0xEB & 0xEC & 0xC1 & 0xC6 & 0xCF & 0xC8 & 0xDD & 0xDA & 0xD3 & 0xD4 \\ +0xA? & 0x69 & 0x6E & 0x67 & 0x60 & 0x75 & 0x72 & 0x7B & 0x7C & 0x51 & 0x56 & 0x5F & 0x58 & 0x4D & 0x4A & 0x43 & 0x44 \\ +0xB? & 0x19 & 0x1E & 0x17 & 0x10 & 0x05 & 0x02 & 0x0B & 0x0C & 0x21 & 0x26 & 0x2F & 0x28 & 0x3D & 0x3A & 0x33 & 0x34 \\ +0xC? & 0x4E & 0x49 & 0x40 & 0x47 & 0x52 & 0x55 & 0x5C & 0x5B & 0x76 & 0x71 & 0x78 & 0x7F & 0x6A & 0x6D & 0x64 & 0x63 \\ +0xD? & 0x3E & 0x39 & 0x30 & 0x37 & 0x22 & 0x25 & 0x2C & 0x2B & 0x06 & 0x01 & 0x08 & 0x0F & 0x1A & 0x1D & 0x14 & 0x13 \\ +0xE? & 0xAE & 0xA9 & 0xA0 & 0xA7 & 0xB2 & 0xB5 & 0xBC & 0xBB & 0x96 & 0x91 & 0x98 & 0x9F & 0x8A & 0x8D & 0x84 & 0x83 \\ +0xF? & 0xDE & 0xD9 & 0xD0 & 0xD7 & 0xC2 & 0xC5 & 0xCC & 0xCB & 0xE6 & 0xE1 & 0xE8 & 0xEF & 0xFA & 0xFD & 0xF4 & 0xF3 \\ +\hline +\end{tabular} +} +\end{table} +\begin{align*} +\text{checksum}_0 = \texttt{CRC8}(\texttt{FF}\xor\texttt{00}) = \texttt{F3} & & +\text{checksum}_3 = \texttt{CRC8}(\texttt{18}\xor\texttt{E6}) = \texttt{F4} \\ +\text{checksum}_1 = \texttt{CRC8}(\texttt{F8}\xor\texttt{F3}) = \texttt{31} & & +\text{checksum}_4 = \texttt{CRC8}(\texttt{00}\xor\texttt{F4}) = \texttt{C2} \\ +\text{checksum}_2 = \texttt{CRC8}(\texttt{C9}\xor\texttt{31}) = \texttt{E6} & & +\text{checksum}_5 = \texttt{CRC8}(\texttt{C2}\xor\texttt{C2}) = \texttt{00} \\ +\end{align*} +Note that the final checksum (including the CRC-8 byte itself) +should always be 0. + + +\clearpage + +\subsection{Decoding a FLAC Subframe} +\label{flac:decode_subframe} +{\relsize{-1} +\ALGORITHM{the frame's block size and bits per sample, the subframe's channel assignment and the FLAC file stream}{decoded signed PCM samples} +\SetKwData{TYPEORDER}{type/order} +\SetKwData{WASTEDBPS}{wasted BPS} +\SetKwData{EFFECTIVEBPS}{subframe's BPS} +\SetKwData{DIFFERENCE}{difference} +\SetKwData{ORDER}{order} +\SetKwData{SAMPLE}{sample} +\SetKwData{BLOCKSIZE}{block size} +\SKIP 1 bit\; +$\TYPEORDER \leftarrow$ \READ 6 unsigned bits\; +\eIf(\tcc*[f]{account for wasted bits}){$((\READ~\textnormal{1 unsigned bit}) = 1)$}{ + $\WASTEDBPS \leftarrow$ (\UNARY with stop bit 1) + 1\; +}{ + $\WASTEDBPS \leftarrow 0$\; +} +\eIf{subframe's channel assignment is \DIFFERENCE}{ + $\EFFECTIVEBPS \leftarrow \text{frame header's bits per sample} - \WASTEDBPS + 1$\; +}{ + $\EFFECTIVEBPS \leftarrow \text{frame header's bits per sample} - \WASTEDBPS$\; +} +\uIf{$\TYPEORDER = 0$}{ + \SAMPLE $\leftarrow$ \hyperref[flac:decode_constant]{decode CONSTANT subframe with \BLOCKSIZE, \EFFECTIVEBPS}\; +} +\uElseIf{$\TYPEORDER = 1$} { + \SAMPLE $\leftarrow$ \hyperref[flac:decode_verbatim]{decode VERBATIM subframe with \BLOCKSIZE, \EFFECTIVEBPS}\; +} +\uElseIf{$8 \leq \TYPEORDER \leq 12$} { + $\ORDER \leftarrow \TYPEORDER - 8$\; + \SAMPLE $\leftarrow$ \hyperref[flac:decode_fixed]{decode FIXED subframe with \BLOCKSIZE, \EFFECTIVEBPS, \ORDER}\; +} +\uElseIf{$32 \leq \TYPEORDER \leq 63$} { + $\ORDER \leftarrow \TYPEORDER - 31$\; + \SAMPLE $\leftarrow$ \hyperref[flac:decode_lpc]{decode LPC subframe with \BLOCKSIZE, \EFFECTIVEBPS, \ORDER}\; +} +\Else { + undefined subframe type error\; +} +\If(\tcc*[f]{prepend any wasted bits to each sample}){$\WASTEDBPS > 0$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\SAMPLE}_i \leftarrow \text{\SAMPLE}_i \times 2 ^ {\WASTEDBPS}$\; + } +} +\Return \SAMPLE\; +\EALGORITHM +} +\begin{figure}[h] +\includegraphics{flac/figures/subframes.pdf} +\end{figure} + +\clearpage + +\subsubsection{Decoding CONSTANT Subframe} +\label{flac:decode_constant} +\ALGORITHM{the frame's block size, the subframe's bits per sample}{decoded signed PCM samples} +\SetKwData{CONSTANT}{constant} +\SetKwData{BPS}{subframe's BPS} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{SAMPLE}{sample} +$\CONSTANT \leftarrow$ \READ (\BPS) signed bits\; +\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\SAMPLE}_i \leftarrow \CONSTANT$\; +} +\Return \SAMPLE\; +\EALGORITHM +\begin{figure}[h] + \includegraphics{flac/figures/constant.pdf} +\end{figure} + +\subsubsection{Decoding VERBATIM Subframe} +\label{flac:decode_verbatim} +\ALGORITHM{the frame's block size, the subframe's bits per sample}{decoded signed PCM samples} +\SetKwData{BPS}{bits per sample} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{SAMPLE}{sample} +\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\SAMPLE}_i \leftarrow $ \READ (\BPS) signed bits\; +} +\Return \SAMPLE\; +\EALGORITHM +\begin{figure}[h] +\includegraphics{flac/figures/verbatim.pdf} +\end{figure} + +\clearpage + +\subsubsection{Decoding FIXED Subframe} +\label{flac:decode_fixed} +{\relsize{-1} +\ALGORITHM{the frame's block size, the subframe's bits per sample and predictor order}{decoded signed PCM samples} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{BPS}{subframe's BPS} +\SetKwData{ORDER}{order} +\SetKwData{SAMPLE}{sample} +\SetKwData{RESIDUAL}{residual} +\For(\tcc*[f]{warm-up samples}){$i \leftarrow 0$ \emph{\KwTo}\ORDER}{ + $\text{\SAMPLE}_i \leftarrow $ \READ (\BPS) signed bits\; +} +\BlankLine +$\RESIDUAL \leftarrow$ \hyperref[flac:decode_residual]{read residual block with frame's \BLOCKSIZE and subframe's \ORDER}\; +\BlankLine +\Switch{\ORDER}{ + \uCase{0} { + \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\SAMPLE}_i \leftarrow \text{\RESIDUAL}_i$ + } + } + \uCase{1} { + \For{$i \leftarrow 1$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\SAMPLE}_i \leftarrow \text{\SAMPLE}_{i - 1} + \text{\RESIDUAL}_{i - 1}$ + } + } + \uCase{2} { + \For{$i \leftarrow 2$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\SAMPLE}_i \leftarrow (2 \times \text{\SAMPLE}_{i - 1}) - \text{\SAMPLE}_{i - 2} + \text{\RESIDUAL}_{i - 2}$ + } + } + \uCase{3} { + \For{$i \leftarrow 3$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\SAMPLE}_i \leftarrow (3 \times \text{\SAMPLE}_{i - 1}) - (3 \times \text{\SAMPLE}_{i - 2}) + \text{\SAMPLE}_{i - 3} + \text{\RESIDUAL}_{i - 3}$ + } + } + \Case{4} { + \For{$i \leftarrow 4$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\SAMPLE}_i \leftarrow (4 \times \text{\SAMPLE}_{i - 1}) - (6 \times \text{\SAMPLE}_{i - 2}) + (4 \times \text{\SAMPLE}_{i - 3}) - \text{\SAMPLE}_{i - 4} + \text{\RESIDUAL}_{i - 4}$ + } + } +} +\Return \SAMPLE\; +\EALGORITHM +} +\begin{figure}[h] +\includegraphics{flac/figures/fixed.pdf} +\end{figure} + +\clearpage + +\subsubsection{FIXED Subframe Decoding Example} + +Given the subframe bytes of a 16 bits per sample stream\footnote{Decoding the residual block is explained on page \pageref{flac:decode_residual}}: +\begin{figure}[h] +\includegraphics{flac/figures/fixed-parse.pdf} +\end{figure} +\begin{align*} +\text{subframe type} &\leftarrow \text{FIXED} \\ +\text{subframe order} &\leftarrow 1 \\ +\text{wasted BPS} &\leftarrow 0 \\ +\text{warm-up sample} &\leftarrow \texttt{[37]} \\ +\end{align*} +\begin{center} +\begin{tabular}{r||r|>{$}r<{$}} +$i$ & $\textsf{residual}_{i - 1}$ & \textsf{sample}_i \\ +\hline +0 & & 37 \\ +1 & -2 & 37 - 2 = 35 \\ +2 & 3 & 35 + 3 = 38 \\ +3 & -1 & 38 - 1 = 37 \\ +4 & -5 & 37 - 5 = 32 \\ +5 & 1 & 32 + 1 = 33 \\ +6 & -5 & 33 - 5 = 27 \\ +7 & 4 & 27 + 4 = 31 \\ +8 & -2 & 31 - 2 = 29 \\ +9 & -3 & 29 - 3 = 26 \\ +10 & 1 & 26 + 1 = 27 \\ +\end{tabular} +\end{center} + +\clearpage + +\subsubsection{Decoding LPC Subframe} +\label{flac:decode_lpc} +{\relsize{-1} +\ALGORITHM{the frame's block size, the subframe's bits per sample and predictor order}{decoded signed PCM samples} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{BPS}{subframe's BPS} +\SetKwData{ORDER}{order} +\SetKwData{QLPPREC}{QLP precision} +\SetKwData{QLPSHIFT}{QLP shift needed} +\SetKwData{COEFF}{QLP coefficient} +\SetKwData{RESIDUAL}{residual} +\SetKwData{SAMPLE}{sample} +\SetKwFunction{MAX}{max} +\For(\tcc*[f]{warm-up samples}){$i \leftarrow 0$ \emph{\KwTo}\ORDER}{ + $\text{\SAMPLE}_i \leftarrow $ \READ (\BPS) signed bits\; +} +$\QLPPREC \leftarrow$ (\READ 4 unsigned bits) + 1\; +$\QLPSHIFT \leftarrow \MAX(\text{\READ 5 signed bits}~,~0)$\footnote{negative shifts are no-ops in the decoder}\; +\For{$i \leftarrow 0$ \emph{\KwTo}\ORDER}{ + $\text{\COEFF}_i \leftarrow$ \READ (\QLPPREC) signed bits\; +} +\BlankLine +$\RESIDUAL \leftarrow$ \hyperref[flac:decode_residual]{read residual block with frame's \BLOCKSIZE and subframe's \ORDER}\; +\BlankLine +\For{$i \leftarrow \ORDER$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\SAMPLE}_i \leftarrow \left\lfloor \frac{\overset{\ORDER - 1}{\underset{j = 0}{\sum}} + \text{\COEFF}_j \times \text{\SAMPLE}_{i - j - 1} } {2 ^ \text{\QLPSHIFT}}\right\rfloor + \text{\RESIDUAL}_{i - \ORDER}$\; +} +\Return \SAMPLE\; +\EALGORITHM +} +\begin{figure}[h] +\includegraphics{flac/figures/lpc.pdf} +\end{figure} + +\clearpage + +\subsubsection{LPC Subframe Decoding Example} + +\begin{figure}[h] +\includegraphics{flac/figures/lpc-parse.pdf} +\end{figure} +{\relsize{-1} + \begin{align*} + \text{subframe type} &\leftarrow \text{LPC} \\ + \text{subframe order} &\leftarrow 3 \\ + \text{wasted BPS} &\leftarrow 0 \\ + \text{warm-up sample} &\leftarrow \texttt{[43, 48, 50]} \\ + \text{QLP precision} &\leftarrow 11 + 1 = 12 \\ + \text{QLP shift needed} &\leftarrow 10 \\ + \text{QLP coefficient} &\leftarrow \texttt{[1451, -323, -110]} + \end{align*} + \begin{center} + \begin{tabular}{r||r|>{$}r<{$}} + $i$ & $\textsf{residual}_{i - 3}$ & \textsf{sample}_i \\ + \hline + 0 & & 43 \\ + 1 & & 48 \\ + 2 & & 50 \\ + 3 & 4 & \left\lfloor\frac{(1451 \times 50) + (-323 \times 48) + (-110 \times 43)}{2 ^ {10}}\right\rfloor + 4 = 55 \\ [1ex] + 4 & 0 & \left\lfloor\frac{(1451 \times 55) + (-323 \times 50) + (-110 \times 48)}{2 ^ {10}}\right\rfloor + 0 = 57 \\ [1ex] + 5 & 1 & \left\lfloor\frac{(1451 \times 57) + (-323 \times 55) + (-110 \times 50)}{2 ^ {10}}\right\rfloor + 1 = 59 \\ [1ex] + 6 & -2 & \left\lfloor\frac{(1451 \times 59) + (-323 \times 57) + (-110 \times 55)}{2 ^ {10}}\right\rfloor - 2 = 57 \\ [1ex] + 7 & -3 & \left\lfloor\frac{(1451 \times 57) + (-323 \times 59) + (-110 \times 57)}{2 ^ {10}}\right\rfloor - 3 = 53 \\ + \end{tabular} + \end{center} +} + +\clearpage + +\subsubsection{Decoding Residual Block} +\label{flac:decode_residual} +{\relsize{-1} +\ALGORITHM{the frame's block size and predictor order}{decoded signed residual values} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{CODING}{coding method} +\SetKwData{PORDER}{partition order} +\SetKwData{PCOUNT}{partition residual count} +\SetKwData{ORDER}{predictor order} +\SetKwData{RESIDUAL}{residual} +\SetKwData{RICE}{Rice} +\SetKwData{ESCAPE}{escape code} +\SetKwData{MSB}{MSB} +\SetKwData{LSB}{LSB} +\SetKwData{UNSIGNED}{unsigned} +\SetKw{AND}{and} +\SetKw{OR}{or} +$\CODING \leftarrow$ \READ 2 unsigned bits\; +$\PORDER \leftarrow$ \READ 4 unsigned bits\; +$i \leftarrow 0$\; +\BlankLine +\For(\tcc*[f]{read residual partitions}){$p \leftarrow 0$ \emph{\KwTo}$2 ^ {\text{\PORDER}}$}{ + \uIf{$\text{\CODING} = 0$}{ + $\text{\RICE}_p \leftarrow$ \READ 4 unsigned bits\; + } + \uElseIf{$\text{\CODING} = 1$}{ + $\text{\RICE}_p \leftarrow$ \READ 5 unsigned bits\; + } + \Else{ + undefined residual coding method error\; + } + \eIf{p = 0}{ + $\text{\PCOUNT}_p \leftarrow \lfloor\text{\BLOCKSIZE} \div 2 ^ {\PORDER}\rfloor - \text{\ORDER}$ + }{ + $\text{\PCOUNT}_p \leftarrow \lfloor\text{\BLOCKSIZE} \div 2 ^ {\PORDER}\rfloor$ + } + \eIf{$((\text{\CODING} = 0)~\text{\AND}~(\text{\RICE}_p = 15))$ \OR + $((\text{\CODING} = 1)~\text{\AND}~(\text{\RICE}_p = 31))$}{ + $\text{\ESCAPE}_p \leftarrow$ \READ 5 unsigned bits\; + \For{$j \leftarrow 0$ \emph{\KwTo}$\text{\PCOUNT}_p$}{ + $\text{\RESIDUAL}_i \leftarrow$ \READ $\text{\ESCAPE}_p$ signed bits\; + $i \leftarrow i + 1$\; + } + }{ + \For{$j \leftarrow 0$ \emph{\KwTo}$\text{\PCOUNT}_p$} { + $\text{\MSB}_i \leftarrow$ \UNARY with stop bit 1\; + $\text{\LSB}_i \leftarrow$ \READ $\text{\RICE}_p$ unsigned bits\; + $\text{\UNSIGNED}_i \leftarrow \text{\MSB}_i \times 2 ^ {\text{\RICE}_p} + \text{\LSB}_i$\; + \eIf(\tcc*[f]{apply sign bit}){$(\text{\UNSIGNED}_i \bmod 2) = 0$}{ + $\text{\RESIDUAL}_i \leftarrow \text{\UNSIGNED}_i \div 2$\; + }{ + $\text{\RESIDUAL}_i \leftarrow -\lfloor \text{\UNSIGNED}_i \div 2 \rfloor - 1$\; + } + $i \leftarrow i + 1$\; + } + } +} +\Return \RESIDUAL\; +\EALGORITHM +} +\begin{figure}[h] + \includegraphics{flac/figures/residual.pdf} +\end{figure} + +\clearpage + +\subsubsection{Residual Block Decoding Example} +\begin{figure}[h] +\includegraphics{flac/figures/residual-parse.pdf} +\end{figure} +\begin{align*} + \textsf{coding method} &\leftarrow 0 \\ + \textsf{partition order} &\leftarrow 0 \\ + \textsf{Rice parameter} &\leftarrow 2 \\ +\end{align*} +\begin{center} + + {\renewcommand{\arraystretch}{1.25} + \begin{tabular}{r||rr|>{$}r<{$}|>{$}r<{$}} + $i$ & $\textsf{MSB}_i$ & $\textsf{LSB}_i$ & \textsf{unsigned}_i & \textsf{residual}_i \\ + \hline + 0 & 0 & 3 & 0 \times 2 ^ 2 + 3 = 3 & -\lfloor 3 \div 2\rfloor - 1 = -2 \\ + 1 & 1 & 2 & 1 \times 2 ^ 2 + 2 = 6 & 6 \div 2 = 3 \\ + 2 & 0 & 1 & 0 \times 2 ^ 2 + 1 = 1 & -\lfloor 1 \div 2\rfloor - 1 = -1 \\ + 3 & 2 & 1 & 2 \times 2 ^ 2 + 1 = 9 & -\lfloor 9 \div 2\rfloor - 1 = -5 \\ + 4 & 0 & 2 & 0 \times 2 ^ 2 + 2 = 2 & 2 \div 2 = 1 \\ + 5 & 2 & 1 & 2 \times 2 ^ 2 + 1 = 9 & -\lfloor 9 \div 2\rfloor - 1 = -5 \\ + 6 & 2 & 0 & 2 \times 2 ^ 2 + 0 = 8 & 8 \div 2 = 4 \\ + 7 & 0 & 3 & 0 \times 2 ^ 2 + 3 = 3 & -\lfloor 3 \div 2\rfloor - 1 = -2 \\ + 8 & 1 & 1 & 1 \times 2 ^ 2 + 1 = 5 & -\lfloor 5 \div 2\rfloor - 1 = -3 \\ + 9 & 0 & 2 & 0 \times 2 ^ 2 + 2 = 2 & 2 \div 2 = 1 \\ + \end{tabular}} +\end{center} +\par +\noindent +for a final set of residuals: \texttt{[-2, 3, -1, -5, 1, -5, 4, -2, -3, 1]} + +\clearpage + +\subsection{Calculating Frame CRC-16} +\label{flac:verify_crc16} +CRC-16 is used to checksum the entire FLAC frame, including the header +and any padding bits after the final subframe. +Given a byte of input and the previous CRC-16 checksum, +or 0 as an initial value, the current checksum can be calculated as follows: +\begin{equation} +\text{checksum}_i = \texttt{CRC16}(byte\xor(\text{checksum}_{i - 1} \gg 8 ))\xor(\text{checksum}_{i - 1} \ll 8) +\end{equation} +\par +\noindent +and the checksum is always truncated to 16-bits. +\begin{table}[h] +{\relsize{-3}\ttfamily +\begin{tabular}{|r||r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} +\hline + & 0x?0 & 0x?1 & 0x?2 & 0x?3 & 0x?4 & 0x?5 & 0x?6 & 0x?7 & 0x?8 & 0x?9 & 0x?A & 0x?B & 0x?C & 0x?D & 0x?E & 0x?F \\ +\hline +0x0? & 0000 & 8005 & 800f & 000a & 801b & 001e & 0014 & 8011 & 8033 & 0036 & 003c & 8039 & 0028 & 802d & 8027 & 0022 \\ +0x1? & 8063 & 0066 & 006c & 8069 & 0078 & 807d & 8077 & 0072 & 0050 & 8055 & 805f & 005a & 804b & 004e & 0044 & 8041 \\ +0x2? & 80c3 & 00c6 & 00cc & 80c9 & 00d8 & 80dd & 80d7 & 00d2 & 00f0 & 80f5 & 80ff & 00fa & 80eb & 00ee & 00e4 & 80e1 \\ +0x3? & 00a0 & 80a5 & 80af & 00aa & 80bb & 00be & 00b4 & 80b1 & 8093 & 0096 & 009c & 8099 & 0088 & 808d & 8087 & 0082 \\ +0x4? & 8183 & 0186 & 018c & 8189 & 0198 & 819d & 8197 & 0192 & 01b0 & 81b5 & 81bf & 01ba & 81ab & 01ae & 01a4 & 81a1 \\ +0x5? & 01e0 & 81e5 & 81ef & 01ea & 81fb & 01fe & 01f4 & 81f1 & 81d3 & 01d6 & 01dc & 81d9 & 01c8 & 81cd & 81c7 & 01c2 \\ +0x6? & 0140 & 8145 & 814f & 014a & 815b & 015e & 0154 & 8151 & 8173 & 0176 & 017c & 8179 & 0168 & 816d & 8167 & 0162 \\ +0x7? & 8123 & 0126 & 012c & 8129 & 0138 & 813d & 8137 & 0132 & 0110 & 8115 & 811f & 011a & 810b & 010e & 0104 & 8101 \\ +0x8? & 8303 & 0306 & 030c & 8309 & 0318 & 831d & 8317 & 0312 & 0330 & 8335 & 833f & 033a & 832b & 032e & 0324 & 8321 \\ +0x9? & 0360 & 8365 & 836f & 036a & 837b & 037e & 0374 & 8371 & 8353 & 0356 & 035c & 8359 & 0348 & 834d & 8347 & 0342 \\ +0xA? & 03c0 & 83c5 & 83cf & 03ca & 83db & 03de & 03d4 & 83d1 & 83f3 & 03f6 & 03fc & 83f9 & 03e8 & 83ed & 83e7 & 03e2 \\ +0xB? & 83a3 & 03a6 & 03ac & 83a9 & 03b8 & 83bd & 83b7 & 03b2 & 0390 & 8395 & 839f & 039a & 838b & 038e & 0384 & 8381 \\ +0xC? & 0280 & 8285 & 828f & 028a & 829b & 029e & 0294 & 8291 & 82b3 & 02b6 & 02bc & 82b9 & 02a8 & 82ad & 82a7 & 02a2 \\ +0xD? & 82e3 & 02e6 & 02ec & 82e9 & 02f8 & 82fd & 82f7 & 02f2 & 02d0 & 82d5 & 82df & 02da & 82cb & 02ce & 02c4 & 82c1 \\ +0xE? & 8243 & 0246 & 024c & 8249 & 0258 & 825d & 8257 & 0252 & 0270 & 8275 & 827f & 027a & 826b & 026e & 0264 & 8261 \\ +0xF? & 0220 & 8225 & 822f & 022a & 823b & 023e & 0234 & 8231 & 8213 & 0216 & 021c & 8219 & 0208 & 820d & 8207 & 0202 \\ +\hline +\end{tabular} +} +\end{table} +\par +\noindent +For example, given the frame bytes: +\texttt{FF F8 CC 1C 00 C0 EB 00 00 00 00 00 00 00 00}, +the frame's CRC-16 can be calculated: +{\relsize{-2} +\begin{align*} +\CRCSIXTEEN{0}{0xFF}{0x0000}{0xFF}{0x0000}{0x0202} \\ +\CRCSIXTEEN{1}{0xF8}{0x0202}{0xFA}{0x0200}{0x001C} \\ +\CRCSIXTEEN{2}{0xCC}{0x001C}{0xCC}{0x1C00}{0x1EA8} \\ +\CRCSIXTEEN{3}{0x1C}{0x1EA8}{0x02}{0xA800}{0x280F} \\ +\CRCSIXTEEN{4}{0x00}{0x280F}{0x28}{0x0F00}{0x0FF0} \\ +\CRCSIXTEEN{5}{0xC0}{0x0FF0}{0xCF}{0xF000}{0xF2A2} \\ +\CRCSIXTEEN{6}{0xEB}{0xF2A2}{0x19}{0xA200}{0x2255} \\ +\CRCSIXTEEN{7}{0x00}{0x2255}{0x22}{0x5500}{0x55CC} \\ +\CRCSIXTEEN{8}{0x00}{0x55CC}{0x55}{0xCC00}{0xCDFE} \\ +\CRCSIXTEEN{9}{0x00}{0xCDFE}{0xCD}{0xFE00}{0x7CAD} \\ +\CRCSIXTEEN{10}{0x00}{0x7CAD}{0x7C}{0xAD00}{0x2C0B} \\ +\CRCSIXTEEN{11}{0x00}{0x2C0B}{0x2C}{0x0B00}{0x8BEB} \\ +\CRCSIXTEEN{12}{0x00}{0x8BEB}{0x8B}{0xEB00}{0xE83A} \\ +\CRCSIXTEEN{13}{0x00}{0xE83A}{0xE8}{0x3A00}{0x3870} \\ +\CRCSIXTEEN{14}{0x00}{0x3870}{0x38}{0x7000}{0xF093} \\ +\intertext{Thus, the next two bytes after the final subframe should be +\texttt{0xF0} and \texttt{0x93}. +Again, when the checksum bytes are run through the checksumming procedure:} +\CRCSIXTEEN{15}{0xF0}{0xF093}{0x00}{0x9300}{0x9300} \\ +\CRCSIXTEEN{16}{0x93}{0x9300}{0x00}{0x0000}{0x0000} +\end{align*} +the result will also always be 0, just as in the CRC-8. +} + +\clearpage + +\subsection{Recombining Subframes} +\label{flac:recombine_subframes} +\ALGORITHM{the frame's block size and channel assignment, a set of decoded subframe samples\footnote{$\textsf{subframe}_{x~y}$ indicates the $y$th sample in subframe $x$}}{a list of signed PCM frames per channel} +\SetKwData{ENCODEDCHANNELS}{encoded channels} +\SetKwData{CHANNELCOUNT}{channel count} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{SUBFRAME}{subframe} +\SetKwData{CHANNEL}{channel} +\uIf(\tcc*[f]{independent}){$0 \leq \ENCODEDCHANNELS \leq 7$}{ + \CHANNELCOUNT $\leftarrow \ENCODEDCHANNELS + 1$\; + \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ + \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\CHANNEL}_{c~i} \leftarrow \text{\SUBFRAME}_{c~i}$ + } + } +} +\uElseIf(\tcc*[f]{left-difference}){$\ENCODEDCHANNELS = 8$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\CHANNEL}_{0~i} \leftarrow \text{\SUBFRAME}_{0~i}$\; + $\text{\CHANNEL}_{1~i} \leftarrow \text{\SUBFRAME}_{0~i} - \text{\SUBFRAME}_{1~i}$\; + } +} +\uElseIf(\tcc*[f]{difference-right}){$\ENCODEDCHANNELS = 9$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\CHANNEL}_{0~i} \leftarrow \text{\SUBFRAME}_{0~i} + \text{\SUBFRAME}_{1~i}$\; + $\text{\CHANNEL}_{1~i} \leftarrow \text{\SUBFRAME}_{1~i}$\; + } +} +\ElseIf(\tcc*[f]{average-difference}){$\ENCODEDCHANNELS = 10$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\CHANNEL}_{0~i} \leftarrow \lfloor(((\text{\SUBFRAME}_{0~i} \times 2) + (\text{\SUBFRAME}_{1~i} \bmod 2)) + \text{\SUBFRAME}_{1~i}) \div 2\rfloor$\; + $\text{\CHANNEL}_{1~i} \leftarrow \lfloor(((\text{\SUBFRAME}_{0~i} \times 2) + (\text{\SUBFRAME}_{1~i} \bmod 2)) - \text{\SUBFRAME}_{1~i}) \div 2\rfloor$\; + } +} +\Return \CHANNEL\; +\EALGORITHM + +\clearpage + +\subsection{Updating Stream MD5 Sum} +\label{flac:update_md5} +\ALGORITHM{the frame's signed PCM samples\footnote{$channel_{c~i}$ indicates the $i$th sample in channel $c$}}{the stream's updated MD5 sum} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{CHANNELCOUNT}{channel count} +\SetKwData{CHANNEL}{channel} +\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ + \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ + bytes $\leftarrow \text{\CHANNEL}_{c~i}$ as signed, little-endian bytes\; + update stream's MD5 sum with bytes\; + } +} +\Return stream's MD5 sum\; +\EALGORITHM +\vskip .25in +\par +\noindent +For example, given a 16 bits per sample stream with the signed sample values: +\begin{table}[h] +\begin{tabular}{r|rr} +$i$ & $\textsf{channel}_{0~i}$ & $\textsf{channel}_{1~i}$ \\ +\hline +0 & 1 & -1 \\ +1 & 2 & -2 \\ +2 & 3 & -3 \\ +\end{tabular} +\end{table} +\par +\noindent +are translated to the bytes: +\begin{table}[h] +\begin{tabular}{r|rr} +$i$ & $\textsf{channel}_{0~i}$ & $\textsf{channel}_{1~i}$ \\ +\hline +0 & \texttt{01 00} & \texttt{FF FF} \\ +1 & \texttt{02 00} & \texttt{FE FF} \\ +2 & \texttt{03 00} & \texttt{FD FF} \\ +\end{tabular} +\end{table} +\par +\noindent +and combined as: +\vskip .15in +\par +\noindent +\texttt{01 00 FF FF 02 00 FE FF 03 00 FD FF} +\vskip .15in +\par +\noindent +whose MD5 sum is: +\vskip .15in +\par +\noindent +\texttt{E7482f6462B27EE04EADC079291C79E9}
View file
audiotools-2.19.tar.gz/docs/reference/flac/encode
Added
+(directory)
View file
audiotools-2.19.tar.gz/docs/reference/flac/encode.tex
Added
@@ -0,0 +1,772 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\section{FLAC Encoding} + +The basic process for encoding a FLAC file is as follows: +\par +\noindent +\ALGORITHM{PCM frames, various encoding parameters: +\newline +{\relsize{-1} +\begin{tabular}{rll} +parameter & possible values & typical values \\ +\hline +block size & a positive number of PCM frames & 1152 or 4096 \\ +maximum LPC order & integer between 0 and 32, inclusive & 0, 6, 8 or 12 \\ +minimum partition order & integer between 0 and 16, inclusive & 0 \\ +maximum partition order & integer between 0 and 16, inclusive & 3, 4, 5 or 6 \\ +maximum Rice parameter & 14 if bits-per-sample $\leq 16$, otherwise 30 & \\ +try mid-side & true or false & \\ +try adaptive mid-side & true or false & \\ +QLP precision & $\begin{cases} +7 & \text{ if } 0 < \text{block size} \leq 192 \\ +8 & \text{ if } 192 < \text{block size} \leq 384 \\ +9 & \text{ if } 384 < \text{block size} \leq 576 \\ +10 & \text{ if } 576 < \text{block size} \leq 1152 \\ +11 & \text{ if } 1152 < \text{block size} \leq 2304 \\ +12 & \text{ if } 2304 < \text{block size} \leq 4608 \\ +13 & \text{ if } \text{block size} > 4608 \\ +\end{cases}$ & \\ +exhaustive model search & true or false & \\ +\end{tabular} +} +}{an encoded FLAC file} +\SetKwData{BLOCKSIZE}{block size} +$\texttt{"fLaC"} \rightarrow$ \WRITE 4 bytes\; +\hyperref[flac:write_placeholder_blocks]{write placeholder STREAMINFO metadata block}\; +write PADDING metadata block\; +initialize stream's MD5 sum\; +\While{PCM frames remain}{ + take \BLOCKSIZE PCM frames from the input\; + \hyperref[flac:update_md5_w]{update the stream's MD5 sum with that PCM data}\; + \hyperref[flac:encode_frame]{encode a FLAC frame from PCM frames using the given encoding parameters}\; + update STREAMINFO's values from the FLAC frame\; +} +return to the start of the file and rewrite the STREAMINFO metadata block\; +\EALGORITHM +\begin{figure}[h] +\includegraphics{flac/figures/stream3.pdf} +\end{figure} +\par +\noindent +All of the fields in the FLAC stream are big-endian. + +\clearpage + +\subsection{Writing Placeholder Metadata Blocks} +\label{flac:write_placeholder_blocks} +\ALGORITHM{input stream's attributes, a default block size}{1 or more metadata blocks to the FLAC file stream} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{SAMPLERATE}{sample rate} +\SetKwData{CHANNELS}{channel count} +\SetKwData{BPS}{bits per sample} +$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{is last block} +$0 \rightarrow$ \WRITE 7 unsigned bits\tcc*[r]{STREAMINFO type} +$34 \rightarrow$ \WRITE 24 unsigned bits\tcc*[r]{STREAMINFO size} +$\BLOCKSIZE \rightarrow$ \WRITE 16 unsigned bits\tcc*[r]{minimum block size} +$\BLOCKSIZE \rightarrow$ \WRITE 16 unsigned bits\tcc*[r]{maximum block size} +$0 \rightarrow$ \WRITE 24 unsigned bits\tcc*[r]{minimum frame size} +$0 \rightarrow$ \WRITE 24 unsigned bits\tcc*[r]{maximum frame size} +$\SAMPLERATE \rightarrow$ \WRITE 20 unsigned bits\; +$\CHANNELS - 1 \rightarrow$ \WRITE 3 unsigned bits\; +$\BPS - 1 \rightarrow$ \WRITE 5 unsigned bits\; +$0 \rightarrow$ \WRITE 36 unsigned bits\tcc*[r]{total PCM frames} +$0 \rightarrow$ \WRITE 16 bytes\tcc*[r]{stream's MD5 sum} +\BlankLine +\BlankLine +$1 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{is last block} +$1 \rightarrow$ \WRITE 7 unsigned bits\tcc*[r]{PADDING type} +$4096 \rightarrow$ \WRITE 24 unsigned bits\tcc*[r]{PADDING size} +$0 \rightarrow$ \WRITE 4096 bytes\tcc*[r]{PADDING's data} +\EALGORITHM +\par +\noindent +PADDING can be some size other than 4096 bytes. +One simply wants to leave enough room for a VORBIS\_COMMENT block, +SEEKTABLE and so forth. +Other fields such as the minimum/maximum frame size +and the stream's final MD5 sum can't be known in advance; +we'll need to return to this block once encoding is finished +in order to populate them. +\begin{figure}[h] +\includegraphics{flac/figures/metadata.pdf} +\end{figure} + + +\clearpage + +\subsection{Updating Stream MD5 Sum} +\label{flac:update_md5_w} +\ALGORITHM{the frame's signed PCM input samples\footnote{$channel_{c~i}$ indicates the $i$th sample in channel $c$}}{the stream's updated MD5 sum} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{CHANNELCOUNT}{channel count} +\SetKwData{CHANNEL}{channel} +\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ + \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ + bytes $\leftarrow \text{\CHANNEL}_{c~i}$ as signed, little-endian bytes\; + update stream's MD5 sum with bytes\; + } +} +\Return stream's MD5 sum\; +\EALGORITHM +\par +\noindent +For example, given a 16 bits per sample stream with the signed sample values: +\begin{table}[h] +\begin{tabular}{r|rr} +$i$ & $\textsf{channel}_0$ & $\textsf{channel}_1$ \\ +\hline +0 & 1 & -1 \\ +1 & 2 & -2 \\ +2 & 3 & -3 \\ +\end{tabular} +\end{table} +\par +\noindent +are translated to the bytes: +\begin{table}[h] +\begin{tabular}{r|rr} +$i$ & $\textsf{channel}_0$ & $\textsf{channel}_1$ \\ +\hline +0 & \texttt{01 00} & \texttt{FF FF} \\ +1 & \texttt{02 00} & \texttt{FE FF} \\ +2 & \texttt{03 00} & \texttt{FD FF} \\ +\end{tabular} +\end{table} +\par +\noindent +and combined as: +\vskip .15in +\par +\noindent +\texttt{01 00 FF FF 02 00 FE FF 03 00 FD FF} +\vskip .15in +\par +\noindent +whose MD5 sum is: +\vskip .15in +\par +\noindent +\texttt{E7482f6462B27EE04EADC079291C79E9} +\vskip .25in +\par +This process is identical to the MD5 sum calculation performed +during FLAC decoding, but performed in the opposite order. + +\clearpage + +\subsection{Encoding a FLAC Frame} +\label{flac:encode_frame} +{\relsize{-1} +\ALGORITHM{up to ``block size'' number of PCM frames, encoding parameters}{a single FLAC frame} +\SetKw{AND}{and} +\SetKw{OR}{or} +\SetKwData{CHANNELCOUNT}{channel count} +\SetKwData{BPS}{bits per sample} +\SetKwData{CHANNEL}{channel} +\SetKwData{AVERAGE}{average} +\SetKwData{DIFFERENCE}{difference} +\SetKwData{LEFTS}{left subframe} +\SetKwData{RIGHTS}{right subframe} +\SetKwData{AVGS}{average subframe} +\SetKwData{DIFFS}{difference subframe} +\SetKwData{IBITS}{independent} +\SetKwData{LDBITS}{left/difference} +\SetKwData{DRBITS}{difference/right} +\SetKwData{ADBITS}{average/difference} +\SetKwData{SUBFRAME}{subframe} +\SetKwFunction{LEN}{len} +\SetKwFunction{MIN}{min} +\SetKwFunction{BUILDSUBFRAME}{build subframe} +\eIf{$\CHANNELCOUNT = 2$ \AND (try mid-side \OR try adaptive mid-side)}{ + \begin{tabular}{rcl} + $(\AVERAGE~,~\DIFFERENCE)$ & $\leftarrow$ & \hyperref[flac:calc_midside]{calculate average-difference of $\text{\CHANNEL}_0$ and $\text{\CHANNEL}_1$} \\ + \LEFTS & $\leftarrow$ & \hyperref[flac:encode_subframe]{encode $\text{\CHANNEL}_0$ as subframe at $\BPS$} \\ + \RIGHTS & $\leftarrow$ & \hyperref[flac:encode_subframe]{encode $\text{\CHANNEL}_1$ as subframe at $\BPS$} \\ + \AVGS & $\leftarrow$ & \hyperref[flac:encode_subframe]{encode $\text{\AVERAGE}$ as subframe at $\BPS$} \\ + \DIFFS & $\leftarrow$ & \hyperref[flac:encode_subframe]{encode $\text{\DIFFERENCE}$ as subframe at $(\BPS + 1)$} \\ + \IBITS & $\leftarrow$ & $\LEN(\LEFTS) + \LEN(\RIGHTS)$ \\ + \LDBITS & $\leftarrow$ & $\LEN(\LEFTS) + \LEN(\DIFFS)$ \\ + \DRBITS & $\leftarrow$ & $\LEN(\DIFFS) + \LEN(\RIGHTS)$ \\ + \ADBITS & $\leftarrow$ & $\LEN(\AVGS) + \LEN(\DIFFS)$ \\ + \end{tabular}\; + \BlankLine + \uIf{try mid-side}{ + \uIf{$\IBITS < \MIN(\LDBITS~,~\DRBITS~,~\ADBITS)$}{ + \hyperref[flac:write_frame_header]{write frame header with channel assignment \texttt{0x1}}\; + write \LEFTS\; + write \RIGHTS\; + } + \uElseIf{$\LDBITS < \MIN(\DRBITS~,~\ADBITS)$}{ + \hyperref[flac:write_frame_header]{write frame header with channel assignment \texttt{0x8}}\; + write \LEFTS\; + write \DIFFS\; + } + \uElseIf{$\DRBITS < \ADBITS$}{ + \hyperref[flac:write_frame_header]{write frame header with channel assignment \texttt{0x9}}\; + write \DIFFS\; + write \RIGHTS\; + } + \Else{ + \hyperref[flac:write_frame_header]{write frame header with channel assignment \texttt{0xA}}\; + write \AVGS\; + write \DIFFS\; + } + }\uElseIf{$\IBITS < \ADBITS$}{ + \hyperref[flac:write_frame_header]{write frame header with channel assignment \texttt{0x1}}\; + write \LEFTS\; + write \RIGHTS\; + } + \Else{ + \hyperref[flac:write_frame_header]{write frame header with channel assignment \texttt{0xA}}\; + write \AVGS\; + write \DIFFS\; + } +}(\tcc*[f]{store subframes independently}){ + \hyperref[flac:write_frame_header]{write frame header with channel assignment $\CHANNELCOUNT - 1$}\; + \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELCOUNT}{ + $\text{\SUBFRAME}_c \leftarrow $ \hyperref[flac:encode_subframe]{encode $\text{\CHANNEL}_c$ as subframe at $\BPS$}\; + write $\text{\SUBFRAME}_c$\; + } +} +byte align the stream\; +\hyperref[flac:calculate_crc16]{write frame's CRC-16 checksum}\; +\EALGORITHM +} + +\clearpage + +\subsubsection{Calculating Average-Difference} +\label{flac:calc_midside} +\ALGORITHM{block size, 2 channels of PCM data}{2 channels stored as average / difference} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{CHANNEL}{channel} +\SetKwData{AVERAGE}{average} +\SetKwData{DIFFERENCE}{difference} +\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\AVERAGE}_i \leftarrow \lfloor (\text{\CHANNEL}_{0~i} + \text{\CHANNEL}_{1~i}) \div 2\rfloor$\; + $\text{\DIFFERENCE}_i \leftarrow \text{\CHANNEL}_{0~i} - \text{\CHANNEL}_{1~i}$\; +} +\Return $\left\lbrace\begin{tabular}{l} +\AVERAGE \\ +\DIFFERENCE \\ +\end{tabular}\right.$\; +\EALGORITHM +\begin{align*} +\intertext{For example, given the input samples:} +\textsf{channel}_{0~0} &\leftarrow 10 \\ +\textsf{channel}_{1~0} &\leftarrow 15 +\intertext{Our average and difference samples are:} +\textsf{average}_0 &\leftarrow \left\lfloor\frac{10 + 15}{2}\right\rfloor = 12 \\ +\textsf{difference}_0 &\leftarrow 10 - 15 = -5 +\intertext{Note that the \textsf{difference} channel is identical +for left-difference, difference-right and average-difference +channel assignments. +For example, when recombined from left-difference\footnotemark:} +\textsf{sample}_0 &\leftarrow 10 \\ +\textsf{sample}_1 &\leftarrow 10 - (-5) = 15 +\intertext{difference-right:} +\textsf{sample}_0 &\leftarrow -5 + 15 = 10 \\ +\textsf{sample}_1 &\leftarrow 15 +\intertext{and average-difference:} +\textsf{sample}_0 &\leftarrow \lfloor(((12 \times 2) + (-5 \bmod 2)) + -5) \div 2\rfloor = \lfloor((24 + 1 - 5) \div 2\rfloor = 10 \\ +\textsf{sample}_1 &\leftarrow \lfloor(((12 \times 2) + (-5 \bmod 2)) - -5) \div 2\rfloor = \lfloor((24 + 1 + 5) \div 2\rfloor = 15 +\end{align*} +\footnotetext{See the recombining subframes algorithms on page +\pageref{flac:recombine_subframes}.} + +\clearpage + +\subsubsection{Writing Frame Header} +\label{flac:write_frame_header} +{\relsize{-1} +\ALGORITHM{the frame's channel assignment, the input stream's parameters}{a FLAC frame header} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{SAMPLERATE}{sample rate} +\SetKwData{FRAMENUMBER}{frame number} +\SetKwData{EBLOCKSIZE}{encoded block size} +\SetKwData{ESAMPLERATE}{encoded sample rate} +\SetKwData{EBPS}{encoded bits per sample} +\SetKwData{ASSIGNMENT}{channel assignment} +\SetKwData{CRC}{CRC-8} +\SetKw{OR}{or} +$\texttt{0x3FFE} \rightarrow$ \WRITE 14 unsigned bits\tcc*[r]{sync code} +$0 \rightarrow$ \WRITE 1 unsigned bit\; +$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{blocking strategy} +$\EBLOCKSIZE \rightarrow$ \WRITE 4 unsigned bits\; +$\ESAMPLERATE \rightarrow$ \WRITE 4 unsigned bits\; +$\ASSIGNMENT \rightarrow$ \WRITE 4 unsigned bits\; +$\EBPS \rightarrow$ \WRITE 3 unsigned bits\; +$0 \rightarrow$ \WRITE 1 unsigned bit\; +$\FRAMENUMBER \rightarrow$ \WRITE \hyperref[flac:write_utf8]{as UTF-8 encoded value}\; +\uIf{$\EBLOCKSIZE = 6$}{ + $(\BLOCKSIZE - 1) \rightarrow$ \WRITE 8 unsigned bits\; +} +\ElseIf{$\EBLOCKSIZE = 7$}{ + $\BLOCKSIZE - 1 \rightarrow$ \WRITE 16 unsigned bits\; +} +\uIf{$\ESAMPLERATE = 12$}{ + $\SAMPLERATE \div 1000 \rightarrow$ \WRITE 8 unsigned bits\; +} +\uElseIf{$\ESAMPLERATE = 13$}{ + $\SAMPLERATE \rightarrow$ \WRITE 16 unsigned bits\; +} +\ElseIf{$\ESAMPLERATE = 14$}{ + $\SAMPLERATE \div 10 \rightarrow$ \WRITE 16 unsigned bits\; +} +$\CRC \leftarrow$ \hyperref[flac:calculate_crc8]{calculate frame header's CRC-8}\; +$\CRC \rightarrow$ \WRITE 8 unsigned bits\; +\EALGORITHM +} + +\subsubsection{Encoding Block Size} +{\relsize{-1} +\ALGORITHM{block size in samples}{encoded block size as 4 bit value} +\SetKwData{BLOCKSIZE}{block size} +\Switch{\BLOCKSIZE}{ +\lCase{192}{\Return 1}\; +\lCase{256}{\Return 8}\; +\lCase{512}{\Return 9}\; +\lCase{576}{\Return 2}\; +\lCase{1024}{\Return 10}\; +\lCase{1152}{\Return 3}\; +\lCase{2048}{\Return 11}\; +\lCase{2304}{\Return 4}\; +\lCase{4096}{\Return 12}\; +\lCase{4608}{\Return 5}\; +\lCase{8192}{\Return 13}\; +\lCase{16384}{\Return 14}\; +\lCase{32768}{\Return 15}\; +\Other{ + \lIf{$\BLOCKSIZE \leq 256$}{\Return 6}\; + \lElseIf{$\BLOCKSIZE \leq 65536$}{\Return 7}\; + \lElse{\Return 0} +} +} +\EALGORITHM +} + +\clearpage + +\subsubsection{Encoding Sample Rate} +{\relsize{-1} +\ALGORITHM{sample rate in Hz}{encoded sample rate as 4 bit value} +\SetKw{AND}{and} +\SetKwData{SAMPLERATE}{sample rate} +\Switch{\SAMPLERATE}{ +\lCase{8000}{\Return 4}\; +\lCase{16000}{\Return 5}\; +\lCase{22050}{\Return 6}\; +\lCase{24000}{\Return 7}\; +\lCase{32000}{\Return 8}\; +\lCase{44100}{\Return 9}\; +\lCase{48000}{\Return 10}\; +\lCase{88200}{\Return 1}\; +\lCase{96000}{\Return 11}\; +\lCase{176400}{\Return 2}\; +\lCase{192000}{\Return 3}\; +\Other{ + \lIf{$(\SAMPLERATE \bmod 1000 = 0)$ \AND $(\SAMPLERATE \leq 255000)$}{\Return 12}\; + \lElseIf{$(\SAMPLERATE \bmod 10 = 0)$ \AND $(\SAMPLERATE \leq 655350)$}{\Return 14}\; + \lElseIf{$\SAMPLERATE \leq 65535$}{\Return 13}\; + \lElse{\Return 0} +} +} +\EALGORITHM +} +\subsubsection{Encoding Bits Per Sample} +{\relsize{-1} +\ALGORITHM{bits per sample}{encoded bits per sample as 3 bit value} +\SetKwData{BPS}{bits per sample} +\Switch{\BPS}{ +\lCase{8}{\Return 1}\; +\lCase{12}{\Return 2}\; +\lCase{16}{\Return 4}\; +\lCase{20}{\Return 5}\; +\lCase{24}{\Return 6}\; +\lOther{\Return 0}\; +} +\EALGORITHM +} +\begin{figure}[h] +\includegraphics{flac/figures/frames.pdf} +\end{figure} + +\clearpage + +\subsubsection{Encoding UTF-8 Frame Number} +\label{flac:write_utf8} +{\relsize{-1} +\ALGORITHM{value as unsigned integer}{1 or more UTF-8 bytes} +\SetKwData{VALUE}{value} +\SetKwData{TOTALBYTES}{total bytes} +\SetKwData{SHIFT}{shift} +\eIf{$\VALUE \leq 127$}{ + $\VALUE \rightarrow$ \WRITE 8 unsigned bits\; +}{ + \uIf{$\VALUE \leq 2047$}{ + $\TOTALBYTES \leftarrow 2$\; + } + \uElseIf{$\VALUE \leq 65535$}{ + $\TOTALBYTES \leftarrow 3$\; + } + \uElseIf{$\VALUE \leq 2097151$}{ + $\TOTALBYTES \leftarrow 4$\; + } + \uElseIf{$\VALUE \leq 67108863$}{ + $\TOTALBYTES \leftarrow 5$\; + } + \ElseIf{$\VALUE \leq 2147483647$}{ + $\TOTALBYTES \leftarrow 6$\; + } + $\SHIFT \leftarrow (\TOTALBYTES - 1) \times 6$\; + $\TOTALBYTES \rightarrow$ \WUNARY with stop bit 0\; + $\lfloor \text{\VALUE} \div 2 ^ \text{\SHIFT} \rfloor \rightarrow$ \WRITE $(7 - \TOTALBYTES)$ unsigned bits\tcc*[r]{initial value} + $\SHIFT \leftarrow \SHIFT - 6$\; + \While{$\SHIFT \geq 0$}{ + $2 \rightarrow$ \WRITE 2 unsigned bits\tcc*[r]{continuation header} + $\lfloor \VALUE \div 2 ^ \text{\SHIFT} \rfloor \bmod 64 \rightarrow$ \WRITE 6 unsigned bits\tcc*[r]{continuation bits} + $\SHIFT \leftarrow \SHIFT - 6$\; + } +} +\EALGORITHM +} +\par +\noindent +For example, encoding the frame number 4228 in UTF-8: +\par +\noindent +\begin{wrapfigure}[10]{r}{2.375in} +\includegraphics{flac/figures/utf8.pdf} +\end{wrapfigure} +\begin{align*} +\textsf{total bytes} &\leftarrow 3 \\ +\textsf{shift} &\leftarrow 12 \\ +& 3 \rightarrow \textbf{write unary} \text{ with stop bit 1} \\ +& 1 \rightarrow \textbf{write} \text{ in 4 unsigned bits} \\ +\textsf{shift} &\leftarrow 12 - 6 = 6 \\ +& 2 \rightarrow \textbf{write} \text{ in 2 unsigned bits} \\ +& 2 \rightarrow \textbf{write} \text{ in 6 unsigned bits} \\ +\textsf{shift} &\leftarrow 6 - 6 = 0 \\ +& 2 \rightarrow \textbf{write} \text{ in 2 unsigned bits} \\ +& 4 \rightarrow \textbf{write} \text{ in 6 unsigned bits} +\end{align*} + +\clearpage + +\subsubsection{Calculating CRC-8} +\label{flac:calculate_crc8} +Given a header byte and previous CRC-8 checksum, +or 0 as an initial value: +\begin{equation*} +\text{checksum}_i = \text{CRC8}(byte\xor\text{checksum}_{i - 1}) +\end{equation*} +\begin{table}[h] +{\relsize{-3}\ttfamily +\begin{tabular}{|r||r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} +\hline + & 0x?0 & 0x?1 & 0x?2 & 0x?3 & 0x?4 & 0x?5 & 0x?6 & 0x?7 & 0x?8 & 0x?9 & 0x?A & 0x?B & 0x?C & 0x?D & 0x?E & 0x?F \\ +\hline +0x0? & 0x00 & 0x07 & 0x0E & 0x09 & 0x1C & 0x1B & 0x12 & 0x15 & 0x38 & 0x3F & 0x36 & 0x31 & 0x24 & 0x23 & 0x2A & 0x2D \\ +0x1? & 0x70 & 0x77 & 0x7E & 0x79 & 0x6C & 0x6B & 0x62 & 0x65 & 0x48 & 0x4F & 0x46 & 0x41 & 0x54 & 0x53 & 0x5A & 0x5D \\ +0x2? & 0xE0 & 0xE7 & 0xEE & 0xE9 & 0xFC & 0xFB & 0xF2 & 0xF5 & 0xD8 & 0xDF & 0xD6 & 0xD1 & 0xC4 & 0xC3 & 0xCA & 0xCD \\ +0x3? & 0x90 & 0x97 & 0x9E & 0x99 & 0x8C & 0x8B & 0x82 & 0x85 & 0xA8 & 0xAF & 0xA6 & 0xA1 & 0xB4 & 0xB3 & 0xBA & 0xBD \\ +0x4? & 0xC7 & 0xC0 & 0xC9 & 0xCE & 0xDB & 0xDC & 0xD5 & 0xD2 & 0xFF & 0xF8 & 0xF1 & 0xF6 & 0xE3 & 0xE4 & 0xED & 0xEA \\ +0x5? & 0xB7 & 0xB0 & 0xB9 & 0xBE & 0xAB & 0xAC & 0xA5 & 0xA2 & 0x8F & 0x88 & 0x81 & 0x86 & 0x93 & 0x94 & 0x9D & 0x9A \\ +0x6? & 0x27 & 0x20 & 0x29 & 0x2E & 0x3B & 0x3C & 0x35 & 0x32 & 0x1F & 0x18 & 0x11 & 0x16 & 0x03 & 0x04 & 0x0D & 0x0A \\ +0x7? & 0x57 & 0x50 & 0x59 & 0x5E & 0x4B & 0x4C & 0x45 & 0x42 & 0x6F & 0x68 & 0x61 & 0x66 & 0x73 & 0x74 & 0x7D & 0x7A \\ +0x8? & 0x89 & 0x8E & 0x87 & 0x80 & 0x95 & 0x92 & 0x9B & 0x9C & 0xB1 & 0xB6 & 0xBF & 0xB8 & 0xAD & 0xAA & 0xA3 & 0xA4 \\ +0x9? & 0xF9 & 0xFE & 0xF7 & 0xF0 & 0xE5 & 0xE2 & 0xEB & 0xEC & 0xC1 & 0xC6 & 0xCF & 0xC8 & 0xDD & 0xDA & 0xD3 & 0xD4 \\ +0xA? & 0x69 & 0x6E & 0x67 & 0x60 & 0x75 & 0x72 & 0x7B & 0x7C & 0x51 & 0x56 & 0x5F & 0x58 & 0x4D & 0x4A & 0x43 & 0x44 \\ +0xB? & 0x19 & 0x1E & 0x17 & 0x10 & 0x05 & 0x02 & 0x0B & 0x0C & 0x21 & 0x26 & 0x2F & 0x28 & 0x3D & 0x3A & 0x33 & 0x34 \\ +0xC? & 0x4E & 0x49 & 0x40 & 0x47 & 0x52 & 0x55 & 0x5C & 0x5B & 0x76 & 0x71 & 0x78 & 0x7F & 0x6A & 0x6D & 0x64 & 0x63 \\ +0xD? & 0x3E & 0x39 & 0x30 & 0x37 & 0x22 & 0x25 & 0x2C & 0x2B & 0x06 & 0x01 & 0x08 & 0x0F & 0x1A & 0x1D & 0x14 & 0x13 \\ +0xE? & 0xAE & 0xA9 & 0xA0 & 0xA7 & 0xB2 & 0xB5 & 0xBC & 0xBB & 0x96 & 0x91 & 0x98 & 0x9F & 0x8A & 0x8D & 0x84 & 0x83 \\ +0xF? & 0xDE & 0xD9 & 0xD0 & 0xD7 & 0xC2 & 0xC5 & 0xCC & 0xCB & 0xE6 & 0xE1 & 0xE8 & 0xEF & 0xFA & 0xFD & 0xF4 & 0xF3 \\ +\hline +\end{tabular} +} +\end{table} + +\subsubsection{Frame Header Encoding Example} +Given a frame header with the following attributes: +\begin{table}[h] +\begin{tabular}{rl} +block size : & 4096 PCM frames \\ +sample rate : & 44100 Hz \\ +channel assignment : & 1 (2 channels stored independently) \\ +bits per sample : & 16 \\ +frame number : & 0 +\end{tabular} +\end{table} +\par +\noindent +we generate the following frame header bytes: +\begin{figure}[h] +\includegraphics{flac/figures/header-example.pdf} +\end{figure} +\par +\noindent +Note how the CRC-8 is calculated from the preceding 5 header bytes: +\begin{align*} +\text{checksum}_0 = \text{CRC8}(\texttt{FF}\xor\texttt{00}) = \texttt{F3} & & +\text{checksum}_3 = \text{CRC8}(\texttt{18}\xor\texttt{E6}) = \texttt{F4} \\ +\text{checksum}_1 = \text{CRC8}(\texttt{F8}\xor\texttt{F3}) = \texttt{31} & & +\text{checksum}_4 = \text{CRC8}(\texttt{00}\xor\texttt{F4}) = \texttt{C2} \\ +\text{checksum}_2 = \text{CRC8}(\texttt{C9}\xor\texttt{31}) = \texttt{E6} \\ +\end{align*} + +\clearpage + +\subsection{Encoding a FLAC Subframe} +\label{flac:encode_subframe} +{\relsize{-1} +\ALGORITHM{block size, signed subframe samples, subframe's bits per sample}{a FLAC subframe} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{SAMPLE}{sample} +\SetKwData{WASTEDBPS}{wasted BPS} +\SetKwData{BPS}{subframe's BPS} +\SetKwData{CONSTANT}{CONSTANT subframe} +\SetKwData{FIXED}{FIXED subframe} +\SetKwData{VERBATIM}{VERBATIM subframe} +\SetKwData{ORDER}{LPC order} +\SetKwData{QLPPRECISION}{QLP precision} +\SetKwData{QLPSHIFT}{QLP shift needed} +\SetKwData{QLPCOEFFS}{QLP coefficients} +\SetKwData{LPC}{LPC subframe} +\SetKwFunction{LEN}{len} +\SetKwFunction{MIN}{min} +\eIf{all samples are the same}{ + \Return \hyperref[flac:encode_constant_subframe]{\CONSTANT from} + $\left\lbrace\begin{tabular}{l} + $\text{\SAMPLE}_0$ \\ + $\BPS$ \\ + \end{tabular}\right.$\; +}{ + $\WASTEDBPS \leftarrow$ \hyperref[flac:calculate_wasted_bps]{calculate wasted bits per sample for $\text{\SAMPLE}$}\; + \If{$\WASTEDBPS > 0$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\SAMPLE}_i \leftarrow \text{\SAMPLE}_i \div 2 ^ \text{\WASTEDBPS}$\; + } + $\BPS \leftarrow \BPS - \WASTEDBPS$\; + } + $\VERBATIM \leftarrow$ \hyperref[flac:encode_verbatim_subframe]{build VERBATIM subframe from} + $\left\lbrace\begin{tabular}{l} + \SAMPLE \\ + \BLOCKSIZE \\ + \BPS \\ + \WASTEDBPS \\ + \end{tabular}\right.$\; + \BlankLine + $\FIXED \leftarrow$ \hyperref[flac:encode_fixed_subframe]{build FIXED subframe from} + $\left\lbrace\begin{tabular}{l} + \SAMPLE \\ + \BLOCKSIZE \\ + \BPS \\ + \WASTEDBPS \\ + \end{tabular}\right.$\; + \BlankLine + \eIf(\tcc*[f]{from encoding parameters}){maximum LPC order $ > 0$}{ + $\left.\begin{tabular}{r} + $\ORDER$ \\ + $\QLPPRECISION$ \\ + $\QLPSHIFT$ \\ + $\QLPCOEFFS$ \\ + \end{tabular}\right\rbrace \leftarrow$ + \hyperref[flac:compute_lpc_params]{compute LPC parameters from \SAMPLE and \BLOCKSIZE}\; + $\text{\LPC} \leftarrow$ \hyperref[flac:encode_lpc_subframe]{build LPC subframe from} + $\left\lbrace\begin{tabular}{l} + \SAMPLE \\ + \BLOCKSIZE \\ + \BPS \\ + \WASTEDBPS \\ + \ORDER \\ + \QLPPRECISION \\ + \QLPSHIFT \\ + \QLPCOEFFS \\ + \end{tabular}\right.$\; + \BlankLine + \uIf{$\LEN(\VERBATIM) \leq \MIN(\LEN(\LPC)~,~\LEN(\FIXED))$}{ + \Return \VERBATIM\; + } + \uElseIf{$\LEN(\FIXED) \leq \LEN(\LPC)$}{ + \Return \FIXED\; + } + \Else{ + \Return \LPC\; + } + }{ + \eIf{$\LEN(\VERBATIM) \leq \LEN(\FIXED)$}{ + \Return \VERBATIM\; + }{ + \Return \FIXED\; + } + } +} +\EALGORITHM +} + +\clearpage + +\subsubsection{Calculating Wasted Bits Per Sample} +\label{flac:calculate_wasted_bps} +\ALGORITHM{a list of signed PCM samples}{an unsigned integer} +\SetKwData{WASTEDBPS}{wasted bps} +\SetKwData{SAMPLE}{sample} +\SetKwData{BLOCKSIZE}{block size} +\SetKwFunction{MIN}{min} +\SetKwFunction{WASTED}{wasted} +$\text{\WASTEDBPS} \leftarrow \infty$\tcc*[r]{maximum unsigned integer} +\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\WASTEDBPS} \leftarrow \MIN(\WASTED(\text{\SAMPLE}_i)~,~\text{\WASTEDBPS})$\; + \If{$\text{\WASTEDBPS} = 0$}{ + \Return 0\; + } +} +\eIf(\tcc*[f]{all samples are 0}){$\WASTEDBPS = \infty$}{ + \Return 0\; +}{ + \Return \WASTEDBPS\; +} +\EALGORITHM +where the \texttt{wasted} function is defined as: +\begin{equation*} + \texttt{wasted}(x) = + \begin{cases} + \infty & \text{if } x = 0 \\ + 0 & \text{if } x \bmod 2 = 1 \\ + 1 + \texttt{wasted}(x \div 2) & \text{if } x \bmod 2 = 0 \\ + \end{cases} +\end{equation*} + +\clearpage + +\subsection{Encoding a CONSTANT Subframe} +\label{flac:encode_constant_subframe} +\ALGORITHM{signed subframe sample, subframe's bits per sample}{a CONSTANT subframe} +\SetKwData{SAMPLE}{sample} +\SetKwData{BPS}{subframe's BPS} +$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{pad} +$0 \rightarrow$ \WRITE 6 unsigned bits\tcc*[r]{subframe type} +$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{no wasted BPS} +$\text{\SAMPLE} \rightarrow$ \WRITE $(\BPS)$ signed bits\; +\Return a CONSTANT subframe\; +\EALGORITHM +\begin{figure}[h] + \includegraphics{flac/figures/constant.pdf} +\end{figure} + +\clearpage + +\subsection{Encoding a VERBATIM Subframe} +\label{flac:encode_verbatim_subframe} +\ALGORITHM{signed subframe samples, block size, subframe's bits per sample, wasted BPS}{a VERBATIM subframe} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{SAMPLE}{sample} +\SetKwData{BPS}{subframe's BPS} +\SetKwData{WASTEDBPS}{wasted BPS} +$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{pad} +$1 \rightarrow$ \WRITE 6 unsigned bits\tcc*[r]{subframe type} +\eIf{$\WASTEDBPS > 0$}{ + $1 \rightarrow$ \WRITE 1 unsigned bit\; + $(\text{\WASTEDBPS} - 1) \rightarrow$ \WUNARY with stop bit 1\; +}{ + $0 \rightarrow$ \WRITE 1 unsigned bit\; +} +\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\SAMPLE}_i \rightarrow$ \WRITE $(\BPS)$ signed bits\; +} +\Return a VERBATIM subframe\; +\EALGORITHM +\begin{figure}[h] + \includegraphics{flac/figures/verbatim.pdf} +\end{figure} + +\clearpage + +\input{flac/encode/fixed} + +\clearpage + +\input{flac/encode/residual} + +\clearpage + +\input{flac/encode/lpc} + +\clearpage + +\subsection{Calculating Frame CRC-16} +\label{flac:calculate_crc16} +CRC-16 is used to checksum the entire FLAC frame, including the header +and any padding bits after the final subframe. +Given a byte of input and the previous CRC-16 checksum, +or 0 as an initial value, the current checksum can be calculated as follows: +\begin{equation} +\text{checksum}_i = \texttt{CRC16}(byte\xor(\text{checksum}_{i - 1} \gg 8 ))\xor(\text{checksum}_{i - 1} \ll 8) +\end{equation} +\par +\noindent +and the checksum is always truncated to 16-bits. +\begin{table}[h] +{\relsize{-3}\ttfamily +\begin{tabular}{|r||r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} +\hline + & 0x?0 & 0x?1 & 0x?2 & 0x?3 & 0x?4 & 0x?5 & 0x?6 & 0x?7 & 0x?8 & 0x?9 & 0x?A & 0x?B & 0x?C & 0x?D & 0x?E & 0x?F \\ +\hline +0x0? & 0000 & 8005 & 800f & 000a & 801b & 001e & 0014 & 8011 & 8033 & 0036 & 003c & 8039 & 0028 & 802d & 8027 & 0022 \\ +0x1? & 8063 & 0066 & 006c & 8069 & 0078 & 807d & 8077 & 0072 & 0050 & 8055 & 805f & 005a & 804b & 004e & 0044 & 8041 \\ +0x2? & 80c3 & 00c6 & 00cc & 80c9 & 00d8 & 80dd & 80d7 & 00d2 & 00f0 & 80f5 & 80ff & 00fa & 80eb & 00ee & 00e4 & 80e1 \\ +0x3? & 00a0 & 80a5 & 80af & 00aa & 80bb & 00be & 00b4 & 80b1 & 8093 & 0096 & 009c & 8099 & 0088 & 808d & 8087 & 0082 \\ +0x4? & 8183 & 0186 & 018c & 8189 & 0198 & 819d & 8197 & 0192 & 01b0 & 81b5 & 81bf & 01ba & 81ab & 01ae & 01a4 & 81a1 \\ +0x5? & 01e0 & 81e5 & 81ef & 01ea & 81fb & 01fe & 01f4 & 81f1 & 81d3 & 01d6 & 01dc & 81d9 & 01c8 & 81cd & 81c7 & 01c2 \\ +0x6? & 0140 & 8145 & 814f & 014a & 815b & 015e & 0154 & 8151 & 8173 & 0176 & 017c & 8179 & 0168 & 816d & 8167 & 0162 \\ +0x7? & 8123 & 0126 & 012c & 8129 & 0138 & 813d & 8137 & 0132 & 0110 & 8115 & 811f & 011a & 810b & 010e & 0104 & 8101 \\ +0x8? & 8303 & 0306 & 030c & 8309 & 0318 & 831d & 8317 & 0312 & 0330 & 8335 & 833f & 033a & 832b & 032e & 0324 & 8321 \\ +0x9? & 0360 & 8365 & 836f & 036a & 837b & 037e & 0374 & 8371 & 8353 & 0356 & 035c & 8359 & 0348 & 834d & 8347 & 0342 \\ +0xA? & 03c0 & 83c5 & 83cf & 03ca & 83db & 03de & 03d4 & 83d1 & 83f3 & 03f6 & 03fc & 83f9 & 03e8 & 83ed & 83e7 & 03e2 \\ +0xB? & 83a3 & 03a6 & 03ac & 83a9 & 03b8 & 83bd & 83b7 & 03b2 & 0390 & 8395 & 839f & 039a & 838b & 038e & 0384 & 8381 \\ +0xC? & 0280 & 8285 & 828f & 028a & 829b & 029e & 0294 & 8291 & 82b3 & 02b6 & 02bc & 82b9 & 02a8 & 82ad & 82a7 & 02a2 \\ +0xD? & 82e3 & 02e6 & 02ec & 82e9 & 02f8 & 82fd & 82f7 & 02f2 & 02d0 & 82d5 & 82df & 02da & 82cb & 02ce & 02c4 & 82c1 \\ +0xE? & 8243 & 0246 & 024c & 8249 & 0258 & 825d & 8257 & 0252 & 0270 & 8275 & 827f & 027a & 826b & 026e & 0264 & 8261 \\ +0xF? & 0220 & 8225 & 822f & 022a & 823b & 023e & 0234 & 8231 & 8213 & 0216 & 021c & 8219 & 0208 & 820d & 8207 & 0202 \\ +\hline +\end{tabular} +} +\end{table} +\par +\noindent +For example, given the frame bytes: +\texttt{FF F8 CC 1C 00 C0 EB 00 00 00 00 00 00 00 00}, +the frame's CRC-16 can be calculated: +{\relsize{-2} +\begin{align*} +\CRCSIXTEEN{0}{0xFF}{0x0000}{0xFF}{0x0000}{0x0202} \\ +\CRCSIXTEEN{1}{0xF8}{0x0202}{0xFA}{0x0200}{0x001C} \\ +\CRCSIXTEEN{2}{0xCC}{0x001C}{0xCC}{0x1C00}{0x1EA8} \\ +\CRCSIXTEEN{3}{0x1C}{0x1EA8}{0x02}{0xA800}{0x280F} \\ +\CRCSIXTEEN{4}{0x00}{0x280F}{0x28}{0x0F00}{0x0FF0} \\ +\CRCSIXTEEN{5}{0xC0}{0x0FF0}{0xCF}{0xF000}{0xF2A2} \\ +\CRCSIXTEEN{6}{0xEB}{0xF2A2}{0x19}{0xA200}{0x2255} \\ +\CRCSIXTEEN{7}{0x00}{0x2255}{0x22}{0x5500}{0x55CC} \\ +\CRCSIXTEEN{8}{0x00}{0x55CC}{0x55}{0xCC00}{0xCDFE} \\ +\CRCSIXTEEN{9}{0x00}{0xCDFE}{0xCD}{0xFE00}{0x7CAD} \\ +\CRCSIXTEEN{10}{0x00}{0x7CAD}{0x7C}{0xAD00}{0x2C0B} \\ +\CRCSIXTEEN{11}{0x00}{0x2C0B}{0x2C}{0x0B00}{0x8BEB} \\ +\CRCSIXTEEN{12}{0x00}{0x8BEB}{0x8B}{0xEB00}{0xE83A} \\ +\CRCSIXTEEN{13}{0x00}{0xE83A}{0xE8}{0x3A00}{0x3870} \\ +\CRCSIXTEEN{14}{0x00}{0x3870}{0x38}{0x7000}{0xF093} \\ +\intertext{Thus, the next two bytes after the final subframe should be +\texttt{0xF0} and \texttt{0x93}. +Again, when the checksum bytes are run through the checksumming procedure:} +\CRCSIXTEEN{15}{0xF0}{0xF093}{0x00}{0x9300}{0x9300} \\ +\CRCSIXTEEN{16}{0x93}{0x9300}{0x00}{0x0000}{0x0000} +\end{align*} +the result will also always be 0, just as in the CRC-8. +}
View file
audiotools-2.19.tar.gz/docs/reference/flac/encode/fixed.tex
Added
@@ -0,0 +1,99 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\subsection{Encoding a FIXED Subframe} +\label{flac:encode_fixed_subframe} +{\relsize{-1} +\ALGORITHM{signed subframe samples, block size, subframe's bits per sample, wasted BPS}{a FIXED subframe} +\SetKwData{BPS}{subframe's BPS} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{RESIDUAL}{residual} +\SetKwData{SAMPLE}{sample} +\SetKwData{ERROR}{total error} +\SetKwData{ORDER}{order} +\SetKwData{WASTEDBPS}{wasted BPS} +\tcc{first decide which FIXED subframe order to use} +\For(\tcc*[f]{order 0}){$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\RESIDUAL}_{0~i} \leftarrow \text{\SAMPLE}_i$\; +} +$\text{\ERROR}_0 \leftarrow \overset{\BLOCKSIZE - 1}{\underset{i = 4}{\sum}}|\text{\RESIDUAL}_{0~i}|$\; +\BlankLine +\eIf{$\BLOCKSIZE > 4$}{ +\For(\tcc*[f]{order 1-4}){$\ORDER \leftarrow 1$ \emph{\KwTo}5}{ + \For{$i \leftarrow 0$ \emph{\KwTo}$\BLOCKSIZE - \ORDER$}{ + $\text{\RESIDUAL}_{\ORDER~i} \leftarrow \text{\RESIDUAL}_{(\ORDER - 1)~(i + 1)} - \text{\RESIDUAL}_{(\ORDER - 1)~i}$\; + } + $\text{\ERROR}_{\ORDER} \leftarrow \overset{\BLOCKSIZE - \ORDER - 1}{\underset{i = 4 - \ORDER}{\sum}}|\text{\RESIDUAL}_{\ORDER~i}|$\; +} +\BlankLine +choose subframe \ORDER such that $\text{\ERROR}_{\ORDER}$ is smallest\; +}{ +use subframe \ORDER 0\; +} +\BlankLine +\tcc{then return a FIXED subframe with best order} +$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{pad} +$1 \rightarrow$ \WRITE 3 unsigned bits\tcc*[r]{subframe type} +$\text{\ORDER} \rightarrow$ \WRITE 3 unsigned bits\; +\eIf{$\WASTEDBPS > 0$}{ + $1 \rightarrow$ \WRITE 1 unsigned bit\; + $(\text{\WASTEDBPS} - 1) \rightarrow$ \WUNARY with stop bit 1\; +}{ + $0 \rightarrow$ \WRITE 1 unsigned bit\; +} +\For(\tcc*[f]{warm-up samples}){$i \leftarrow 0$ \emph{\KwTo}\ORDER}{ + $\text{\SAMPLE}_i \rightarrow$ \WRITE $(\BPS)$ signed bits\; +} +\hyperref[flac:write_residual_block]{write encoded residual block based on $\text{\RESIDUAL}_{\ORDER}$, \BLOCKSIZE and \ORDER}\; +\BlankLine +\Return a FIXED subframe\; +\EALGORITHM +} +\begin{figure}[h] + \includegraphics{flac/figures/fixed.pdf} +\end{figure} + +\clearpage + +\subsubsection{FIXED Subframe Calculation Example} + +Given the subframe samples: \texttt{18, 20, 26, 24, 24, 23, 21, 24, 23, 20}: +\begin{table}[h] +\begin{tabular}{r|r|r|r|r|r} +& \textsf{order} = 0 & \textsf{order} = 1 & \textsf{order} = 2 & \textsf{order} = 3 & \textsf{order} = 4 \\ +\hline +$\textsf{residual}_{\textsf{order}~0}$ & \texttt{\color{gray}18} & \texttt{\color{gray}2} & \texttt{\color{gray}4} & \texttt{\color{gray}-12} & \texttt{22} \\ +$\textsf{residual}_{\textsf{order}~1}$ & \texttt{\color{gray}20} & \texttt{\color{gray}6} & \texttt{\color{gray}-8} & \texttt{10} & \texttt{-13} \\ +$\textsf{residual}_{\textsf{order}~2}$ & \texttt{\color{gray}26} & \texttt{\color{gray}-2} & \texttt{2} & \texttt{-3} & \texttt{3} \\ +$\textsf{residual}_{\textsf{order}~3}$ & \texttt{\color{gray}24} & \texttt{0} & \texttt{-1} & \texttt{0} & \texttt{6} \\ +$\textsf{residual}_{\textsf{order}~4}$ & \texttt{24} & \texttt{-1} & \texttt{-1} & \texttt{6} & \texttt{-15} \\ +$\textsf{residual}_{\textsf{order}~5}$ & \texttt{23} & \texttt{-2} & \texttt{5} & \texttt{-9} & \texttt{11} \\ +$\textsf{residual}_{\textsf{order}~6}$ & \texttt{21} & \texttt{3} & \texttt{-4} & \texttt{2} \\ +$\textsf{residual}_{\textsf{order}~7}$ & \texttt{24} & \texttt{-1} & \texttt{-2} \\ +$\textsf{residual}_{\textsf{order}~8}$ & \texttt{23} & \texttt{-3} \\ +$\textsf{residual}_{\textsf{order}~9}$ & \texttt{20} \\ +\hline +$\textsf{total error}_{\textsf{order}}$ & \texttt{135} & \texttt{10} & \texttt{15} & \texttt{30} & \texttt{70} \\ +\end{tabular} +\end{table} +\par +\noindent +Note how the total number of residuals equals the +total number of samples minus the subframe's order, +to account for the warm-up samples. +Also note that if you remove the first $4 - \textsf{order}$ residuals +and sum the absolute value of the remaining residuals, +the result is the \VAR{total error} value +used when calculating the best FIXED subframe order. +\par +Since $\textsf{total error}_1$'s value of 10 is the smallest, +the best order for this FIXED subframe is 1. + +\begin{figure}[h] + \includegraphics{flac/figures/fixed-enc-example.pdf} +\end{figure}
View file
audiotools-2.19.tar.gz/docs/reference/flac/encode/lpc.tex
Added
@@ -0,0 +1,566 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\subsection{Computing Best LPC Parameters} +\label{flac:compute_lpc_params} +{\relsize{-1} +\ALGORITHM{signed subframe samples, encoding parameters}{LPC order, QLP coefficients, QLP precision, QLP shift needed} +\SetKwFunction{LEN}{len} +\SetKw{AND}{and} +\SetKw{NOT}{not} +\SetKwData{SAMPLE}{sample} +\SetKwData{WINDOWED}{windowed} +\SetKwData{AUTOCORRELATIONS}{autocorrelation} +\SetKwData{LPCOEFFS}{LP coefficients} +\SetKwData{ERRORS}{error} +\SetKwData{ORDER}{LPC order} +\SetKwData{QLPCOEFFS}{QLP coefficients} +\SetKwData{QLPPRECISION}{QLP precision} +\SetKwData{QLPSHIFT}{QLP shift needed} +\SetKwData{BESTLPCPARAMS}{best LPC parameters} +\SetKwData{LPCPARAMS}{LPC parameters} +\SetKwData{LPCDATA}{LPC subframe data} +\tcc{windowed sample count equals subframe sample count} +$\WINDOWED \leftarrow$ \hyperref[flac:window]{window \SAMPLE}\; +\BlankLine +\tcc{autocorrelation value count equals the maximum LPC order + 1} +$\AUTOCORRELATIONS \leftarrow$ \hyperref[flac:autocorrelate]{autocorrelate \WINDOWED}\; +\BlankLine +\eIf{$\LEN(\AUTOCORRELATIONS) > 1$ \AND \AUTOCORRELATIONS aren't all 0.0}{ + $\left.\begin{tabular}{r} + $\LPCOEFFS$ \\ + $\ERRORS$ \\ + \end{tabular}\right\rbrace \leftarrow$ \hyperref[flac:compute_lp_coeffs]{compute LP coefficients from \AUTOCORRELATIONS}\; + \BlankLine + \eIf(\tcc*[f]{from encoding parameters}){\NOT exhaustive model search}{ + \tcc{estimate which set of LP coefficients is the smallest + and return those} + $\ORDER \leftarrow$ \hyperref[flac:estimate_best_order]{estimate best order from \ERRORS, sample count and bits per sample}\; + $\left.\begin{tabular}{r} + \QLPCOEFFS \\ + \QLPPRECISION \\ + \QLPSHIFT \\ + \end{tabular}\right\rbrace \leftarrow$ + \hyperref[flac:quantize_lp_coeffs]{quantize \LPCOEFFS at \ORDER}\; + \Return $\left\lbrace\begin{tabular}{l} + \ORDER \\ + \QLPCOEFFS \\ + \QLPPRECISION \\ + \QLPSHIFT \\ + \end{tabular}\right.$\; + }{ + \tcc{build a complete LPC subframe from each set of LP coefficients + and return the parameters of the one which is smallest} + \For{$o \leftarrow 1$ \emph{\KwTo}maximum LPC order + 1}{ + $\left.\begin{tabular}{r} + ${\QLPCOEFFS}_o$ \\ + ${\QLPPRECISION}_o$ \\ + ${\QLPSHIFT}_o$ \\ + \end{tabular}\right\rbrace \leftarrow$ + \hyperref[flac:quantize_lp_coeffs]{quantize \LPCOEFFS at order o}\; + $\text{\LPCDATA}_o \leftarrow$ \hyperref[flac:encode_lpc_subframe]{build LPC subframe from} + $\left\lbrace\begin{tabular}{l} + \SAMPLE \\ + \textsf{block size} \\ + \textsf{subframe's BPS} \\ + \textsf{wasted BPS} \\ + \ORDER $o$ \\ + ${\QLPPRECISION}_o$ \\ + ${\QLPSHIFT}_o$ \\ + ${\QLPCOEFFS}_o$ \\ + \end{tabular}\right.$\; + } + choose \ORDER $o$ whose $\text{\LPCDATA}_o$ block is smallest\; + \BlankLine + \Return $\left\lbrace\begin{tabular}{l} + \ORDER $o$\\ + ${\QLPCOEFFS}_o$ \\ + ${\QLPPRECISION}_o$ \\ + ${\QLPSHIFT}_o$ \\ + \end{tabular}\right.$\; + } +}{ + \tcc{all samples are 0, so return very basic coefficients} + \Return $\left\lbrace\begin{tabular}{lcl} + \ORDER & $\leftarrow$ & 1 \\ + \QLPCOEFFS & $\leftarrow$ & \texttt{[0]} \\ + \QLPPRECISION & $\leftarrow$ & 2 \\ + \QLPSHIFT & $\leftarrow$ & 0 \\ + \end{tabular}\right.$\; +} +\EALGORITHM +} + +\clearpage + + +\subsubsection{Windowing the Input Samples} +\label{flac:window} +\ALGORITHM{a list of signed input sample integers}{a list of signed windowed samples as floats} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{SAMPLE}{sample} +\SetKwData{WINDOWED}{windowed} +\SetKwFunction{TUKEY}{tukey} +\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\WINDOWED}_i = \text{\SAMPLE}_i \times \TUKEY(i)$\; +} +\Return \WINDOWED\; +\EALGORITHM +\par +\noindent +The \VAR{Tukey} function is defined as: +\begin{equation*} +\texttt{tukey}(n) = +\begin{cases} +\frac{1}{2} \times \left[1 + cos\left(\pi \times \left(\frac{2 \times n}{\alpha \times (N - 1)} - 1 \right)\right)\right] & \text{ if } 0 \leq n \leq \frac{\alpha \times (N - 1)}{2} \\ +1 & \text{ if } \frac{\alpha \times (N - 1)}{2} \leq n \leq (N - 1) \times (1 - \frac{\alpha}{2}) \\ +\frac{1}{2} \times \left[1 + cos\left(\pi \times \left(\frac{2 \times n}{\alpha \times (N - 1)} - \frac{2}{\alpha} + 1 \right)\right)\right] & \text{ if } (N - 1) \times (1 - \frac{\alpha}{2}) \leq n \leq (N - 1) \\ +\end{cases} +\end{equation*} +\par +\noindent +Where $N$ is the total number of samples and $\alpha$ is $\nicefrac{1}{2}$. +\vskip .25in +\par +For example, given the input samples: \texttt{[18, 20, 26, 24, 24, 23, 21, 24, 23, 20]} +\begin{wrapfigure}[5]{r}{3in} +\includegraphics{flac/figures/tukey.pdf} +\end{wrapfigure} +\begin{table}[h] +\begin{tabular}{r|r|r|r} +$i$ & $\textsf{sample}_i$ & $\texttt{tukey}(i)$ & $\textsf{windowed}_i$ \\ +\hline +0 & 18 & 0 & 0.0 \\ +1 & 20 & .41 & 8.2 \\ +2 & 26 & .97 & 25.2 \\ +3 & 24 & 1 & 24.0 \\ +4 & 24 & 1 & 24.0 \\ +5 & 23 & 1 & 23.0 \\ +6 & 21 & 1 & 21.0 \\ +7 & 24 & .97 & 23.3 \\ +8 & 23 & .41 & 9.4 \\ +9 & 20 & 0 & 0.0 \\ +\end{tabular} +\end{table} + +\clearpage + +\subsubsection{Performing Autocorrelation} +\label{flac:autocorrelate} +{\relsize{-1} + \ALGORITHM{a list of signed windowed samples, the maximum LPC order}{a list of signed autocorrelation values} + \SetKwData{MAXLPCORDER}{max LPC order} + \SetKwData{BLOCKSIZE}{block size} + \SetKwData{WINDOWED}{windowed} + \SetKwData{AUTOCORRELATED}{autocorrelated} + \For{$l \leftarrow 0$ \emph{\KwTo}$(\text{\MAXLPCORDER} + 1)$}{ + $\text{\AUTOCORRELATED}_{l} = \overset{\text{\BLOCKSIZE} - l - 1}{\underset{i = 0}{\sum}}\text{\WINDOWED}_i \times \text{\WINDOWED}_{i + l}$\; + } +\Return \AUTOCORRELATED\; +\EALGORITHM +} +For example, given the windowed samples: +\texttt{[0.0, 8.2, 25.2, 24.0, 24.0, 23.0, 21.0, 23.3, 9.4, 0.0]} +and a maximum LPC order of 3: +\begin{figure}[h] +\subfloat{ + {\relsize{-2} + \begin{tabular}{rrrrr} + \texttt{0.0} & $\times$ & \texttt{0.0} & $=$ & \texttt{0.00} \\ + \texttt{8.2} & $\times$ & \texttt{8.2} & $=$ & \texttt{67.24} \\ + \texttt{25.2} & $\times$ & \texttt{25.2} & $=$ & \texttt{635.04} \\ + \texttt{24.0} & $\times$ & \texttt{24.0} & $=$ & \texttt{576.00} \\ + \texttt{24.0} & $\times$ & \texttt{24.0} & $=$ & \texttt{576.00} \\ + \texttt{23.0} & $\times$ & \texttt{23.0} & $=$ & \texttt{529.00} \\ + \texttt{21.0} & $\times$ & \texttt{21.0} & $=$ & \texttt{441.00} \\ + \texttt{23.3} & $\times$ & \texttt{23.3} & $=$ & \texttt{542.89} \\ + \texttt{9.4} & $\times$ & \texttt{9.4} & $=$ & \texttt{88.36} \\ + \texttt{0.0} & $\times$ & \texttt{0.0} & $=$ & \texttt{0.00} \\ + \hline + \multicolumn{3}{r}{$\textsf{autocorrelated}_0$} & $=$ & \texttt{3455.53} \\ + \end{tabular} + } +} +\includegraphics{flac/figures/lag0.pdf} + +\subfloat{ + {\relsize{-2} + \begin{tabular}{rrrrr} + \texttt{0.0} & $\times$ & \texttt{8.2} & $=$ & \texttt{0.00} \\ + \texttt{8.2} & $\times$ & \texttt{25.2} & $=$ & \texttt{206.64} \\ + \texttt{25.2} & $\times$ & \texttt{24.0} & $=$ & \texttt{604.80} \\ + \texttt{24.0} & $\times$ & \texttt{24.0} & $=$ & \texttt{576.00} \\ + \texttt{24.0} & $\times$ & \texttt{23.0} & $=$ & \texttt{552.00} \\ + \texttt{23.0} & $\times$ & \texttt{21.0} & $=$ & \texttt{483.00} \\ + \texttt{21.0} & $\times$ & \texttt{23.3} & $=$ & \texttt{489.30} \\ + \texttt{23.3} & $\times$ & \texttt{9.4} & $=$ & \texttt{219.02} \\ + \texttt{9.4} & $\times$ & \texttt{0.0} & $=$ & \texttt{0.00} \\ + \hline + \multicolumn{3}{r}{$\textsf{autocorrelated}_1$} & $=$ & \texttt{3130.76} \\ + \end{tabular} + } +} +\includegraphics{flac/figures/lag1.pdf} + +\subfloat{ + {\relsize{-2} + \begin{tabular}{rrrrr} + \texttt{0.0} & $\times$ & \texttt{25.2} & $=$ & \texttt{0.00} \\ + \texttt{8.2} & $\times$ & \texttt{24.0} & $=$ & \texttt{196.80} \\ + \texttt{25.2} & $\times$ & \texttt{24.0} & $=$ & \texttt{604.80} \\ + \texttt{24.0} & $\times$ & \texttt{23.0} & $=$ & \texttt{552.00} \\ + \texttt{24.0} & $\times$ & \texttt{21.0} & $=$ & \texttt{504.00} \\ + \texttt{23.0} & $\times$ & \texttt{23.3} & $=$ & \texttt{535.90} \\ + \texttt{21.0} & $\times$ & \texttt{9.4} & $=$ & \texttt{197.40} \\ + \texttt{23.3} & $\times$ & \texttt{0.0} & $=$ & \texttt{0.00} \\ + \hline + \multicolumn{3}{r}{$\textsf{autocorrelated}_2$} & $=$ & \texttt{2590.90} \\ + \end{tabular} + } +} +\includegraphics{flac/figures/lag2.pdf} + +\subfloat{ + {\relsize{-2} + \begin{tabular}{rrrrr} + \texttt{0.0} & $\times$ & \texttt{24.0} & $=$ & \texttt{0.00} \\ + \texttt{8.2} & $\times$ & \texttt{24.0} & $=$ & \texttt{196.80} \\ + \texttt{25.2} & $\times$ & \texttt{23.0} & $=$ & \texttt{579.60} \\ + \texttt{24.0} & $\times$ & \texttt{21.0} & $=$ & \texttt{504.00} \\ + \texttt{24.0} & $\times$ & \texttt{23.3} & $=$ & \texttt{559.20} \\ + \texttt{23.0} & $\times$ & \texttt{9.4} & $=$ & \texttt{216.20} \\ + \texttt{21.0} & $\times$ & \texttt{0.0} & $=$ & \texttt{0.00} \\ + \hline + \multicolumn{3}{r}{$\textsf{autocorrelated}_3$} & $=$ & \texttt{2055.80} \\ + \end{tabular} + } +} +\includegraphics{flac/figures/lag3.pdf} +\end{figure} +\par +\noindent +Note that the total number of autocorrelation values equals +the maximum LPC order + 1. + +\clearpage + +\subsubsection{LP Coefficient Calculation} +\label{flac:compute_lp_coeffs} +{\relsize{-1} +\ALGORITHM{a list of autocorrelation floats, the maximum LPC order}{a list of LP coefficient lists, a list of error values} +\SetKwData{MAXLPCORDER}{max LPC order} +\SetKwData{LPCOEFF}{LP coefficient} +\SetKwData{ERROR}{error} +\SetKwData{AUTOCORRELATION}{autocorrelated} +\begin{tabular}{rcl} +$\kappa_0$ &$\leftarrow$ & $ \AUTOCORRELATION_1 \div \AUTOCORRELATION_0$ \\ +$\LPCOEFF_{0~0}$ &$\leftarrow$ & $ \kappa_0$ \\ +$\ERROR_0$ &$\leftarrow$ & $ \AUTOCORRELATION_0 \times (1 - {\kappa_0} ^ 2)$ \\ +\end{tabular}\; +\For{$i \leftarrow 1$ \emph{\KwTo}\MAXLPCORDER}{ + \tcc{"zip" all of the previous row's LP coefficients + \newline + and the reversed autocorrelation values from 1 to i + 1 + \newline + into ($c$,$a$) pairs + \newline + $q_i$ is $\AUTOCORRELATION_{i + 1}$ minus the sum of those multiplied ($c$,$a$) pairs} + $q_i \leftarrow \AUTOCORRELATION_{i + 1}$\; + \For{$j \leftarrow 0$ \emph{\KwTo}i}{ + $q_i \leftarrow q_i - (\LPCOEFF_{(i - 1)~j} \times \AUTOCORRELATION_{i - j})$\; + } + \BlankLine + \tcc{"zip" all of the previous row's LP coefficients + \newline + and the previous row's LP coefficients reversed + into ($c$,$r$) pairs} + $\kappa_i = q_i \div \ERROR_{i - 1}$\; + \For{$j \leftarrow 0$ \emph{\KwTo}i}{ + \tcc{then build a new coefficient list of $c - (\kappa_i * r)$ for each ($c$,$r$) pair} + $\LPCOEFF_{i~j} \leftarrow \LPCOEFF_{(i - 1)~j} - (\kappa_i \times \LPCOEFF_{(i - 1)~(i - j - 1)})$\; + } + $\text{\LPCOEFF}_{i~i} \leftarrow \kappa_i$\tcc*[r]{and append $\kappa_i$ as the final coefficient in that list} + \BlankLine + $\ERROR_i \leftarrow \ERROR_{i - 1} \times (1 - {\kappa_i}^2)$\; +} +\Return $\left\lbrace\begin{tabular}{l} +$\LPCOEFF$ \\ +$\ERROR$ \\ +\end{tabular}\right.$ +\EALGORITHM +} + +\clearpage + +\subsubsection{LP Coefficient Calculation Example} +Given a maximum LPC order of 3 and 4 autocorrelation values: +{\relsize{-1} + \begin{align*} + \kappa_0 &\leftarrow \textsf{autocorrelation}_1 \div \textsf{autocorrelation}_0 \\ + \textsf{LP coefficient}_{0~0} &\leftarrow \kappa_0 \\ + \textsf{error}_0 &\leftarrow \textsf{autocorrelation}_0 \times (1 - {\kappa_0} ^ 2) \\ + i &= 1 \\ + q_1 &\leftarrow \textsf{autocorrelation}_2 - (\textsf{LP coefficient}_{0~0} \times \textsf{autocorrelation}_{1}) \\ + \kappa_1 &\leftarrow q_1 \div error_0 \\ + \textsf{LP coefficient}_{1~0} &\leftarrow \textsf{LP coefficient}_{0~0} - (\kappa_1 \times \textsf{LP coefficient}_{0~0}) \\ + \textsf{LP coefficient}_{1~1} &\leftarrow \kappa_1 \\ + \textsf{error}_1 &\leftarrow \textsf{error}_0 \times (1 - {\kappa_1} ^ 2) \\ + i &= 2 \\ + q_2 &\leftarrow \textsf{autocorrelation}_3 - (\textsf{LP coefficient}_{1~0} \times \textsf{autocorrelation}_{2} + \textsf{LP coefficient}_{1~1} \times \textsf{autocorrelation}_{1}) \\ + \kappa_2 &\leftarrow q_2 \div \textsf{error}_1 \\ + \textsf{LP coefficient}_{2~0} &\leftarrow \textsf{LP coefficient}_{1~0} - (\kappa_2 \times \textsf{LP coefficient}_{1~1}) \\ + \textsf{LP coefficient}_{2~1} &\leftarrow \textsf{LP coefficient}_{1~1} - (\kappa_2 \times \textsf{LP coefficient}_{1~0}) \\ + \textsf{LP coefficient}_{2~2} &\leftarrow \kappa_2 \\ + \textsf{error}_2 &\leftarrow \textsf{error}_1 \times (1 - {\kappa_2} ^ 2) \\ +\end{align*} +} +\par +\noindent +With \textsf{autocorrelation} values: \texttt{[3455.53, 3130.76, 2590.90, 2055.80]} +{\relsize{-1} + \begin{align*} + \kappa_0 &\leftarrow 3130.76 \div 3455.53 = 0.906 \\ + \textsf{LP coefficient}_{0~0} &\leftarrow \textbf{0.906} \\ + \textsf{error}_0 &\leftarrow 3455.53 \times (1 - {0.906} ^ 2) = \textbf{619.107} \\ + i &= 1 \\ + q_1 &\leftarrow 2590.90 - (0.906 \times 3130.76) = -245.569 \\ + \kappa_1 &\leftarrow -245.569 \div 619.107 = -0.397 \\ + \textsf{LP coefficient}_{1~0} &\leftarrow 0.906 - (-0.397 \times 0.906) = \textbf{1.266} \\ + \textsf{LP coefficient}_{1~1} &\leftarrow \textbf{-0.397} \\ + \textsf{error}_1 &\leftarrow 619.107 \times (1 - {-0.397} ^ 2) = \textbf{521.530} \\ + i &= 2 \\ + q_2 &\leftarrow 2055.80 - (1.266 \times 2590.90 + -0.397 \times 3130.76) = 18.632 \\ + \kappa_2 &\leftarrow 18.632 \div 521.53 = 0.036 \\ + \textsf{LP coefficient}_{2~0} &\leftarrow 1.266 - (0.036 \times -0.397) = \textbf{1.28} \\ + \textsf{LP coefficient}_{2~1} &\leftarrow -0.397 - (0.036 \times 1.266) = \textbf{-0.443} \\ + \textsf{LP coefficient}_{2~2} &\leftarrow \textbf{0.036} \\ + \textsf{error}_2 &\leftarrow 521.53 \times (1 - {0.036} ^ 2) = \textbf{520.854} \\ + \end{align*} +} + +\clearpage + +\subsubsection{Estimating Best Order} +\label{flac:estimate_best_order} +\ALGORITHM{floating point error values, block size, bits per sample,\newline QLP precision and maximum LPC order from encoding parameters}{the best estimated order value to use} +\SetKwData{ORDER}{predictor order} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{MAXLPCORDER}{max LPC order} +\SetKwData{HEADERBITS}{header bits} +\SetKwData{QLPPREC}{QLP precision} +\SetKwData{BPS}{bits per sample} +\SetKwData{BPR}{bits per residual} +\SetKwData{ERROR}{error} +\SetKwData{ERRORSCALE}{error scale} +\SetKwData{SUBFRAMEBITS}{subframe bits} +\SetKwFunction{MAX}{max} +\ERRORSCALE $\leftarrow \frac{({\log_e 2}) ^ 2}{\text{\BLOCKSIZE} \times 2}$\; +\For{$i \leftarrow 0$ \emph{\KwTo}\MAXLPCORDER}{ + $o \leftarrow i + 1$\tcc*[r]{current order} + \uIf{$\text{\ERROR}_i > 0.0$}{ + $\text{\HEADERBITS}_o \leftarrow o \times (\text{\BPS} + \text{\QLPPREC})$\; + $\text{\BPR}_o \leftarrow \MAX\left(\frac{\log_e(\text{\ERROR}_i \times \text{\ERRORSCALE})}{({\log_e 2}) \times 2}~,~0.0\right)$\; + $\text{\SUBFRAMEBITS}_o \leftarrow \text{\HEADERBITS}_o + \text{\BPR}_o \times (\text{\BLOCKSIZE} - o)$\; + } + \ElseIf{$\text{\ERROR}_i = 0.0$}{ + \Return $o$\; + } +} +\BlankLine +\Return LPC order $o$ such that $\text{\SUBFRAMEBITS}_o$ is smallest\; +\EALGORITHM + +\clearpage + +\subsubsection{Estimating Best Order Example} + +Given the error values \texttt{[619.107, 521.530, 520.854]}, +a block size of 10, 16 bits per sample, a QLP precision of 12 and maximum LPC order of 3: +\begin{align*} + \textsf{error scale} &\leftarrow \frac{({\log_e 2}) ^ 2}{10 \times 2} = 0.024 \\ + i &\leftarrow 0 \\ + o &\leftarrow 0 + 1 = 1 \\ + \textsf{header bits}_1 &\leftarrow 1 \times (16 + 12) = 28 \\ + \textsf{bits per residual}_1 &\leftarrow \frac{\log_e(619.107 \times 0.024)}{({\log_e 2}) \times 2} = 1.947 \\ + \textsf{subframe bits}_1 &\leftarrow 28 + 1.947 \times (10 - 1) = \textbf{45.523} \\ + i &\leftarrow 1 \\ + o &\leftarrow 1 + 1 = 2 \\ + \textsf{header bits}_2 &\leftarrow 2 \times (16 + 12) = 56 \\ + \textsf{bits per residual}_2 &\leftarrow \frac{\log_e(521.530 \times 0.024)}{({\log_e 2}) \times 2} = 1.823 \\ + \textsf{subframe bits}_2 &\leftarrow 56 + 1.823 \times (10 - 2) = \textbf{70.584} \\ + i &\leftarrow 2 \\ + o &\leftarrow 2 + 1 = 3 \\ + \textsf{header bits}_3 &\leftarrow 3 \times (16 + 12) = 84 \\ + \textsf{bits per residual}_3 &\leftarrow \frac{\log_e(520.854 \times 0.024)}{({\log_e 2}) \times 2} = 1.822 \\ + \textsf{subframe bits}_3 &\leftarrow 84 + 1.822 \times (10 - 3) = \textbf{96.754} \\ +\end{align*} +\par +\noindent +Since the $\textsf{subframe bits}_1$ value of 45.523 is the smallest, +the best LPC order to use is 1. + +\clearpage + +\subsubsection{Quantizing LP Coefficients} +\label{flac:quantize_lp_coeffs} +\ALGORITHM{LPC order value, LP coefficients, QLP precision from encoding parameters}{QLP coefficients, QLP precision, QLP shift needed} +\SetKwData{LPCOEFF}{LP coefficient} +\SetKwData{QLPSHIFT}{QLP shift needed} +\SetKwData{QLPPREC}{QLP precision} +\SetKwData{ORDER}{order} +\SetKwData{QLPMAX}{QLP max} +\SetKwData{QLPMIN}{QLP min} +\SetKwData{ERROR}{error} +\SetKwData{QLPCOEFF}{QLP coefficient} +\SetKwFunction{MIN}{min} +\SetKwFunction{MAX}{max} +\SetKwFunction{ROUND}{round} +$l \leftarrow $ maximum $|c|$ for $c$ in $\text{\LPCOEFF}_{(\ORDER - 1)~0}$ to $\text{\LPCOEFF}_{(\ORDER - 1)~\ORDER}$\; +$\QLPSHIFT \leftarrow (\text{\QLPPREC} - 1) - (\lfloor \log_2(l) \rfloor - 1) - 1$\; +\uIf(\tcc*[f]{must fit into signed 5 bit field}){$\QLPSHIFT > 2 ^ 4 - 1$}{ + $\QLPSHIFT \leftarrow 2 ^ 4 - 1$\; +} +\ElseIf{$\QLPSHIFT < -(2 ^ 4)$}{ + \Return error\tcc*[r]{too much shift required for coefficients} +} + +\BlankLine +\tcc{QLP min and max are the smallest and largest QLP coefficients that fit in a signed field that's "QLP precision" bits wide} +$\QLPMAX \leftarrow 2 ^ {\text{\QLPPREC} - 1} - 1$\; +$\QLPMIN \leftarrow -(2 ^ {\text{\QLPPREC} - 1})$\; +$\ERROR \leftarrow 0.0$\; +\eIf{$\text{\QLPSHIFT} \geq 0$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}\ORDER}{ + $\ERROR \leftarrow \ERROR + \text{\LPCOEFF}_{\ORDER - 1~i} \times 2 ^ \text{\QLPSHIFT}$\; + $\text{\QLPCOEFF}_i \leftarrow \MIN(\MAX(\ROUND(\ERROR)~,~\text{\QLPMIN})~,~\text{\QLPMAX})$\; + $\ERROR \leftarrow \ERROR - \text{\QLPCOEFF}_i$\; + } + \Return $\left\lbrace\begin{tabular}{l} + \QLPCOEFF \\ + \QLPPREC \\ + \QLPSHIFT \\ + \end{tabular}\right.$\; +}(\tcc*[f]{negative shifts are not allowed, so shrink coefficients}){ + \For{$i \leftarrow 0$ \emph{\KwTo}\ORDER}{ + $\ERROR \leftarrow \ERROR + \text{\LPCOEFF}_{\ORDER - 1~i} \div 2 ^ {-\text{\QLPSHIFT}}$\; + $\text{\QLPCOEFF}_i \leftarrow \MIN(\MAX(\ROUND(\ERROR)~,~\text{\QLPMIN})~,~\text{\QLPMAX})$\; + $\ERROR \leftarrow \ERROR - \text{\QLPCOEFF}_i$\; + } + \Return $\left\lbrace\begin{tabular}{l} + \QLPCOEFF \\ + \QLPPREC \\ + 0 \\ + \end{tabular}\right.$\; +} +\EALGORITHM + +\clearpage + +\subsubsection{Quantizing LP Coefficients Example} + +Given the $\textsf{LP coefficient}_3$ \texttt{[1.280, -0.443, 0.036]}, +an \textsf{order} of \texttt{3} and a \textsf{QLP precision} \texttt{12}: +\begin{align*} +l &\leftarrow 1.280 \\ +\textsf{QLP shift} &\leftarrow 12 - \lfloor \log_2(1.280) \rfloor - 2 = 10 \\ +\textsf{QLP max} &\leftarrow 2 ^ {12 - 1} - 1 = 2047 \\ +\textsf{QLP min} &\leftarrow -(2 ^ {12 - 1}) = -2048 \\ +\textsf{error} &\leftarrow 0.0 \\ +i &= 0 \\ +\textsf{error} &\leftarrow 0.0 + 1.280 \times 2 ^ {10} = 1310.72 \\ +\textsf{QLP coefficient}_0 &\leftarrow 1311 \\ +\textsf{error} &\leftarrow 1310.72 - 1311 = -0.28 \\ +i &= 1 \\ +\textsf{error} &\leftarrow -0.28 + -0.443 \times 2 ^ {10} = -453.912 \\ +\textsf{QLP coefficient}_1 &\leftarrow -454 \\ +\textsf{error} &\leftarrow -453.912 - -454 = 0.088 \\ +i &= 2 \\ +\textsf{error} &\leftarrow 0.088 + 0.036 \times 2 ^ {10} = 36.952 \\ +\textsf{QLP coefficient}_2 &\leftarrow 37 \\ +\textsf{error} &\leftarrow 36.952 - 37 = -0.048 \\ +\end{align*} +\par +\noindent +Resulting in the QLP coefficients \texttt{1311, -454, 37} +and a QLP shift of \texttt{10}. +These values, in addition to QLP precision, +are inserted directly into a desired QLP subframe header +and are also used to calculate its residuals. + +\clearpage + +\subsection{Encoding an LPC Subframe} +\label{flac:encode_lpc_subframe} +\ALGORITHM{signed samples, block size, subframe's bits per sample, wasted BPS, LPC order, QLP precision, QLP shift needed, QLP coefficients}{an LPC subframe} +\SetKwData{ORDER}{LPC order} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{WASTEDBPS}{wasted BPS} +\SetKwData{BPS}{bits per sample} +\SetKwData{SAMPLE}{sample} +\SetKwData{RESIDUAL}{residual} +\SetKwData{QLPPREC}{QLP precision} +\SetKwData{QLPSHIFT}{QLP shift needed} +\SetKwData{QLPCOEFF}{QLP coefficient} +$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{pad} +$1 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{subframe type} +$(\text{\ORDER} - 1) \rightarrow$ \WRITE 5 unsigned bits\; +\eIf{$\WASTEDBPS > 0$}{ + $1 \rightarrow$ \WRITE 1 unsigned bit\; + $(\text{\WASTEDBPS} - 1) \rightarrow$ \WUNARY with stop bit 1\; +}{ + $0 \rightarrow$ \WRITE 1 unsigned bit\; +} +\For(\tcc*[f]{warm-up samples}){$i \leftarrow 0$ \emph{\KwTo}\ORDER}{ + $\text{\SAMPLE}_i \rightarrow$ \WRITE $(\BPS)$ signed bits\; +} +$(\text{\QLPPREC} - 1) \rightarrow$ \WRITE 4 unsigned bits\; +$\text{\QLPSHIFT} \rightarrow$ \WRITE 5 signed bits\; +\For(\tcc*[f]{QLP coefficients}){$i \leftarrow 0$ \emph{\KwTo}\ORDER}{ + $\text{\QLPCOEFF}_i \rightarrow$ \WRITE $(\QLPPREC)$ signed bits\; +} +\BlankLine +\For(\tcc*[f]{calculate signed residuals}){$i \leftarrow 0$ \emph{\KwTo}$\BLOCKSIZE - \ORDER$}{ + $\text{\RESIDUAL}_i \leftarrow \text{\SAMPLE}_{i + \ORDER} - \left \lfloor \frac{\overset{\ORDER - 1}{\underset{j = 0}{\sum}} \text{\QLPCOEFF}_j \times \text{\SAMPLE}_{i + \ORDER - j - 1} }{2 ^ \text{\QLPSHIFT}} \right \rfloor$ +} +\hyperref[flac:write_residual_block]{write encoded residual block based on \RESIDUAL, \BLOCKSIZE and \ORDER}\; +\Return LPC subframe\; +\EALGORITHM +\begin{figure}[h] +\includegraphics{flac/figures/lpc.pdf} +\end{figure} + +\clearpage + +\subsubsection{LPC Subframe Residuals Calculation Example} +{\relsize{-1} + \begin{tabular}{rl} + \textsf{samples} : & \texttt{[18, 20, 26, 24, 24, 23, 21, 24, 23, 20]} \\ + \textsf{block size} : & 10 \\ + \textsf{subframe's bits per sample} : & 16 \\ + \textsf{wasted BPS} : & 0 \\ + \textsf{LPC order} : & \texttt{3} \\ + \textsf{QLP precision} : &\texttt{12} \\ + \textsf{QLP shift needed} : & \texttt{10} \\ + \textsf{QLP coefficients} : & \texttt{[1311, -454, 37]} \\ + \end{tabular} + \newline + \begin{align*} + \textsf{residual}_0 &\leftarrow 24 - \left\lfloor\frac{(1311 \times 26) + (-454 \times 20) + (37 \times 18)}{2 ^ {10}}\right\rfloor = -1 \\ + \textsf{residual}_1 &\leftarrow 24 - \left\lfloor\frac{(1311 \times 24) + (-454 \times 26) + (37 \times 20)}{2 ^ {10}}\right\rfloor = 5 \\ + \textsf{residual}_2 &\leftarrow 23 - \left\lfloor\frac{(1311 \times 24) + (-454 \times 24) + (37 \times 26)}{2 ^ {10}}\right\rfloor = 2 \\ + \textsf{residual}_3 &\leftarrow 21 - \left\lfloor\frac{(1311 \times 23) + (-454 \times 24) + (37 \times 24)}{2 ^ {10}}\right\rfloor = 2 \\ + \textsf{residual}_4 &\leftarrow 24 - \left\lfloor\frac{(1311 \times 21) + (-454 \times 23) + (37 \times 24)}{2 ^ {10}}\right\rfloor = 7 \\ + \textsf{residual}_5 &\leftarrow 23 - \left\lfloor\frac{(1311 \times 24) + (-454 \times 21) + (37 \times 23)}{2 ^ {10}}\right\rfloor = 1 \\ + \textsf{residual}_6 &\leftarrow 20 - \left\lfloor\frac{(1311 \times 23) + (-454 \times 24) + (37 \times 21)}{2 ^ {10}}\right\rfloor = 1 + \end{align*} + Leading to a final set of 7 residual values: \texttt{[-1, 5, 2, 2, 7, 1, 1]}. + Encoding them to a residual block, our final LPC subframe is: +} +\begin{figure}[h] +\includegraphics{flac/figures/lpc-parse2.pdf} +\end{figure}
View file
audiotools-2.19.tar.gz/docs/reference/flac/encode/residual.tex
Added
@@ -0,0 +1,181 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\subsection{Residual Encoding} +\label{flac:write_residual_block} +{\relsize{-1} +\ALGORITHM{a set of signed residual values, the subframe's block size and predictor order, minimum and maximum partition order from encoding parameters}{an encoded block of residuals} +\SetKwData{MINPORDER}{minimum partition order} +\SetKwData{MAXPORDER}{maximum partition order} +\SetKwData{ORDER}{predictor order} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{PAORDER}{partition order} +\SetKwData{PSIZE}{partitions size} +\SetKwData{PSUM}{partition sum} +\SetKwData{RICE}{Rice} +\SetKwData{CODING}{coding method} +\SetKwData{UNSIGNED}{unsigned} +\SetKwData{PARTITION}{partition} +\SetKwData{PLEN}{partition length} +\SetKwData{MSB}{MSB} +\SetKwData{LSB}{LSB} +\SetKwFunction{SUM}{sum} +\SetKwFunction{MAX}{max} +\SetKw{BREAK}{break} +\tcc{generate set of partitions for each partition order} +\For{$o \leftarrow \text{\MINPORDER}$ \emph{\KwTo}(\MAXPORDER + 1)}{ + \eIf{$(\BLOCKSIZE \bmod 2^{o}) = 0$}{ + $\left.\begin{tabular}{r} + $\text{\RICE}_o$ \\ + $\text{\PARTITION}_o$ \\ + $\text{\PSIZE}_o$ \\ + \end{tabular}\right\rbrace \leftarrow$ \hyperref[flac:write_residual_partition]{encode residual partitions from} + $\left\lbrace\begin{tabular}{l} + partition order $o$ \\ + \textsf{predictor order} \\ + \textsf{residual values} \\ + \BLOCKSIZE \\ + \end{tabular}\right.$ + }{ + \BREAK\; + } +} +\BlankLine +choose partition order $o$ such that $\PSIZE_{o}$ is smallest\; +\BlankLine +\eIf{$\MAX(\text{\RICE}_{o}) > 14$}{ + $\CODING \leftarrow 1$\; +}{ + $\CODING \leftarrow 0$\; +} +\BlankLine +\tcc{write 1 or more residual partitions to residual block} +$\CODING \rightarrow$ \WRITE 2 unsigned bits\; +$o \rightarrow$ \WRITE 4 unsigned bits\; +\For{$p \leftarrow 0$ \emph{\KwTo}$2 ^ {o}$} { + \eIf{$\CODING = 0$}{ + $\text{\RICE}_{o~p} \rightarrow$ \WRITE 4 unsigned bits\; + }{ + $\text{\RICE}_{o~p} \rightarrow$ \WRITE 5 unsigned bits\; + } + \BlankLine + \eIf{$p = 0$}{ + $\text{\PLEN}_{o~0} \leftarrow \BLOCKSIZE \div 2 ^ {o} - \ORDER$\; + }{ + $\text{\PLEN}_{o~p} \leftarrow \BLOCKSIZE \div 2 ^ {o}$\; + } + \BlankLine + \For(\tcc*[f]{write residual partition}){$i \leftarrow 0$ \emph{\KwTo}$\text{\PLEN}_{o~p}$}{ + \eIf{$\text{\PARTITION}_{o~p~i} \geq 0$}{ + $\text{\UNSIGNED}_i \leftarrow \text{\PARTITION}_{o~p~i} \times 2$\; + }{ + $\text{\UNSIGNED}_i \leftarrow (-\text{\PARTITION}_{o~p~i} - 1) \times 2 + 1$\; + } + $\text{\MSB}_i \leftarrow \lfloor \text{\UNSIGNED}_i \div 2 ^ \text{\RICE} \rfloor$\; + $\text{\LSB}_i \leftarrow \text{\UNSIGNED}_i - (\text{\MSB}_i \times 2 ^ \text{\RICE})$\; + $\text{\MSB}_i \rightarrow$ \WUNARY with stop bit 1\; + $\text{\LSB}_i \rightarrow$ \WRITE $\text{\RICE}$ unsigned bits\; + } +} +\Return encoded residual block\; +\EALGORITHM +} + +\clearpage + +\subsubsection{Encoding Partitions} +\label{flac:write_residual_partition} +{\relsize{-1} +\ALGORITHM{partition order $o$, predictor order, residual values, block size, maximum Rice parameter from encoding parameters}{Rice parameter, 1 or more residual partitions, total estimated size} +\SetKwData{ORDER}{predictor order} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{PSIZE}{partitions size} +\SetKwData{PLEN}{plength} +\SetKwData{PARTITION}{partition} +\SetKwData{RESIDUAL}{residual} +\SetKwData{PSUM}{partition sum} +\SetKwData{RICE}{Rice} +\SetKwData{MAXPARAMETER}{maximum Rice parameter} +\SetKw{BREAK}{break} +$\text{\PSIZE} \leftarrow 0$\; +\BlankLine +\For(\tcc*[f]{split residuals into partitions}){$p \leftarrow 0$ \emph{\KwTo}$2 ^ {o}$}{ + \eIf{$p = 0$}{ + $\text{\PLEN}_{0} \leftarrow \BLOCKSIZE \div 2 ^ {o} - \ORDER$\; + }{ + $\text{\PLEN}_{p} \leftarrow \BLOCKSIZE \div 2 ^ {o}$\; + } + $\text{\PARTITION}_{p} \leftarrow$ get next $\text{\PLEN}_{p}$ values from \RESIDUAL\; + \BlankLine + $\text{\PSUM}_{p} \leftarrow \overset{\text{\PLEN}_{p} - 1}{\underset{i = 0}{\sum}} |\text{\PARTITION}_{p~i}|$\; + \BlankLine + $\text{\RICE}_{p} \leftarrow 0$\tcc*[r]{compute best Rice parameter for partition} + \While{$\text{\PLEN}_{p} \times 2 ^ {\text{\RICE}_{p}} < \text{\PSUM}_{p}$}{ + \eIf{$\text{\RICE}_{p} < \MAXPARAMETER$}{ + $\text{\RICE}_{p} \leftarrow \text{\RICE}_{p} + 1$\; + }{ + \BREAK\; + } + } + \BlankLine + \eIf(\tcc*[f]{add estimated size of partition to total size}){$\text{\RICE}_{p} > 0$}{ + $\text{\PSIZE} \leftarrow \text{\PSIZE} + 4 + ((1 + \text{\RICE}_{p}) \times \text{\PLEN}_{p}) + \left\lfloor\frac{\text{\PSUM}_{p}}{2 ^ {\text{\RICE}_{p} - 1}}\right\rfloor - \left\lfloor\frac{\text{\PLEN}_{p}}{2}\right\rfloor$\; + }{ + $\text{\PSIZE} \leftarrow \text{\PSIZE} + 4 + \text{\PLEN}_{p} + (\text{\PSUM}_{p} \times 2) - \left\lfloor\frac{\text{\PLEN}_{p}}{2}\right\rfloor$\; + } +} +\BlankLine +\Return $\left\lbrace\begin{tabular}{l} +$\text{\RICE}$ \\ +$\text{\PARTITION}$ \\ +$\text{\PSIZE}$ \\ +\end{tabular}\right.$\; +\EALGORITHM +} + +\begin{figure}[h] +\includegraphics{flac/figures/residual.pdf} +\end{figure} + +\clearpage + +\subsubsection{Residual Encoding Example} +Given a set of residuals \texttt{[2, 6, -2, 0, -1, -2, 3, -1, -3]}, +block size of 10 and predictor order of 1: +{\relsize{-1} + \begin{align*} + \intertext{$\text{partition order}~o = 0$:} + \textsf{plength}_{0~0} &\leftarrow 10 \div 2 ^ 0 - 1 = 9 \\ + \textsf{partition}_{0~0} &\leftarrow \texttt{[2, 6, -2, 0, -1, -2, 3, -1, -3]} \\ + \textsf{partition sum}_{0~0} &\leftarrow 2 + 6 + 2 + 0 + 1 + 2 + 3 + 1 + 3 = 20 \\ + \textsf{Rice}_{0~0} &\leftarrow \textbf{1}~~(9 \times 2 ^ \textbf{1} < 20 \text{ and } 9 \times 2 ^ \textbf{2} > 20) \\ + \textsf{partitions size}_0 &\leftarrow 0 + 4 + ((1 + 1) \times 9) + \left\lfloor\frac{20}{2 ^ 1 - 1}\right\rfloor - \left\lfloor\frac{9}{2}\right\rfloor = \textbf{38} \\ + \intertext{$\text{partition order}~o = 1$:} + \textsf{plength}_{1~0} &\leftarrow 10 \div 2 ^ 1 - 1 = 4 \\ + \textsf{partition}_{1~0} &\leftarrow \texttt{[2, 6, -2, 0]} \\ + \textsf{partition sum}_{1~0} &\leftarrow 2 + 6 + 2 + 0 = 10 \\ + \textsf{Rice}_{1~0} &\leftarrow \textbf{1}~~(4 \times 2 ^ \textbf{1} < 10 \text{ and } 4 \times 2 ^ \textbf{2} > 10) \\ + \textsf{partitions size}_1 &\leftarrow 0 + 4 + ((1 + 1) \times 4) + \left\lfloor\frac{10}{2 ^ 1 - 1}\right\rfloor - \left\lfloor\frac{4}{2}\right\rfloor = \textbf{20} \\ + \textsf{plength}_{1~1} &\leftarrow 10 \div 2 ^ 1 = 5 \\ + \textsf{partition}_{1~1} &\leftarrow \texttt{[-1, -2, 3, -1, -3]} \\ + \textsf{partition sum}_{1~1} &\leftarrow 1 + 2 + 3 + 1 + 3 = 10 \\ + \textsf{Rice}_{1~1} &\leftarrow \textbf{0}~~(5 \times 2 ^ \textbf{0} < 10 \text{ and } 5 \times 2 ^ \textbf{1} = 10) \\ + \textsf{partitions size}_1 &\leftarrow \textbf{20} + 4 + 5 + (10 \times + 2) - \left\lfloor\frac{5}{2}\right\rfloor = \textbf{47} +\end{align*}} +\par +\noindent +Since block size of $10 \bmod 2 ^ 2 \neq 0$, we stop at partition order 1 +because the list of residuals can't be divided equally into more partitions. +And because $\textsf{partitions size}_0$ of 38 is smaller than +$\textsf{partitions size}_1$ of 47, we use partition order 0 +to encode our residuals into a single partition with 9 residuals. + +\begin{figure}[h] + \includegraphics{flac/figures/residuals-enc-example.pdf} +\end{figure}
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures
Added
+(directory)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/application.bdx
Added
@@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<diagram> + <row> + <col width=".125">Block₀</col> + <col width=".125">Block₁</col> + <col width=".125" id="block">Block₂</col> + <col width=".125">Block₃</col> + <col width=".125">Block₄</col> + <col width=".125">Block₅</col> + <col width=".125">Block₆</col> + <col width=".125" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="0" width=".20" id="block_h_s">last block</col> + <col start="1" end="7" width=".30">block type (2)</col> + <col start="8" end="31" width=".50" id="block_h_e">block size</col> + </row> + <row> + <col start="0" end="31" width=".25">application ID</col> + <col start="32" width=".75">application data</col> + </row> + <line style="dotted"> + <start id="block" corner="sw"/> + <end id="block_h_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="block" corner="se"/> + <end id="block_h_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/block_header.bpx
Changed
(renamed from docs/reference/figures/flac/block_header.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/constant.bdx
Added
@@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<diagram> + <row> + <col width=".18">Frame Header</col> + <col width=".15" id="subframe">Subframe₀</col> + <col width=".15">Subframe₁</col> + <col width=".15">Subframe₂</col> + <col width=".15">Subframe₃</col> + <col width=".05" style="dashed">...</col> + <col width=".07" style="dashed">align</col> + <col width=".1" start="0" end="15" id="frame_e">CRC-16</col> + </row> + <spacer/> + <row> + <col width=".1" start="0" end="0" id="subframe_s">pad</col> + <col width=".45" start="1" end="6">subframe type (0) </col> + <col width=".2" start="7" end="7">has wasted BPS</col> + <col style="dashed" width=".25" id="subframe_e">wasted BPS (+1)</col> + </row> + <row> + <col start="0" end="subframe bps - 1">constant</col> + </row> + <line style="dotted"> + <start id="subframe" corner="sw"/> + <end id="subframe_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="subframe" corner="se"/> + <end id="subframe_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/cuesheet-example.bypx
Added
@@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="block header">0500021C</field> + <field label="catalog number">3439383830303234333137383600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000</field> + <field label="lead-in samples">0000000000015888</field> + <field label="CDDA">80</field> + <field label="NULL">000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000</field> + <field label="tracks">03</field> + <field label="track offset₀">0000000000000000</field> + <field label="num₀">01</field> + <field label="ISRC₀">4a5056493030323133333430</field> + <field label="flags₀">00</field> + <field label="reserved₀">00000000000000000000000000</field> +</bytestruct>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/cuesheet.bdx
Added
@@ -0,0 +1,79 @@ +<?xml version="1.0" ?> +<diagram> + <row> + <col width=".125">Block₀</col> + <col width=".125">Block₁</col> + <col width=".125">Block₂</col> + <col width=".125">Block₃</col> + <col width=".125">Block₄</col> + <col width=".125" id="block">Block₅</col> + <col width=".125">Block₆</col> + <col width=".125" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="0" width=".20" id="block_h_s">last block</col> + <col start="1" end="7" width=".30">block type (5)</col> + <col start="8" end="31" width=".50" id="block_h_e">block size</col> + </row> + <row> + <col end="1023" start="0" width=".25">catalog number</col> + <col end="1087" start="1024" width=".25">lead-in samples</col> + <col end="1088" start="1088" width=".15">is cdda</col> + <col end="3159" start="1089" width=".10">NULL</col> + <col end="3167" start="3160" width=".25">track count</col> + </row> + <row> + <col id="track" start="3168" width=".20">Track₀</col> + <col width=".20">Track₁</col> + <col width=".20">Track₂</col> + <col width=".20">Track₃</col> + <col width=".20" style="dashed">...</col> + </row> + <row><blank/></row> + <row> + <col end="63" id="track_offset" start="0" width=".20">offset</col> + <col end="71" start="64" width=".15">number</col> + <col end="167" start="72" width=".15">ISRC</col> + <col end="168" start="168" width=".20">track type</col> + <col end="169" start="169" width=".20">pre-emphasis</col> + <col end="279" id="track_null" start="170" width=".10">NULL</col> + </row> + <row> + <col end="287" start="280" width=".30">index points</col> + <col id="index" start="288" end="383" width=".25">Index₀</col> + <col width=".25" start="384" end="479">Index₁</col> + <col style="dashed" width=".20">...</col> + </row> + <row><blank/></row> + <row> + <blank width=".15"/> + <col end="63" id="index_offset" start="0" width=".25">index offset</col> + <col end="71" start="64" width=".25">index number</col> + <col end="95" id="index_end" start="72" width=".20">NULL</col> + </row> + <line style="dotted"> + <start id="block" corner="sw"/> + <end id="block_h_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="block" corner="se"/> + <end id="block_h_e" corner="ne"/> + </line> + <line style="dotted"> + <start id="track" corner="sw"/> + <end id="track_offset" corner="nw"/> + </line> + <line style="dotted"> + <start id="track" corner="se"/> + <end id="track_null" corner="ne"/> + </line> + <line style="dotted"> + <start id="index" corner="sw"/> + <end id="index_offset" corner="nw"/> + </line> + <line style="dotted"> + <start id="index" corner="se"/> + <end id="index_end" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/fixed-enc-example.bpx
Added
@@ -0,0 +1,9 @@ +<?xml version="1.0" ?> +<struct endianness="big"> + <field size="1" value="0">pad</field> + <field size="3" value="1">type</field> + <field size="3" value="1">order</field> + <field size="1" value="0">wasted</field> + <field size="16" value="18">warm-up sample₀</field> + <field size="43" value="0000000001001000000010011101101100010110011b">residual block</field> +</struct>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/fixed-parse.bpx
Changed
(renamed from docs/reference/figures/flac/fixed-parse.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/fixed.bdx
Added
@@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<diagram> + <row> + <col width=".18">Frame Header</col> + <col width=".15">Subframe₀</col> + <col width=".15">Subframe₁</col> + <col width=".15" id="subframe">Subframe₂</col> + <col width=".15">Subframe₃</col> + <col width=".05" style="dashed">...</col> + <col width=".07" style="dashed">align</col> + <col width=".1" start="0" end="15" id="frame_e">CRC-16</col> + </row> + <spacer/> + <row> + <col width=".1" start="0" end="0" id="subframe_s">pad</col> + <col width=".225" start="1" end="3">type (1)</col> + <col width=".225" start="4" end="6">order</col> + <col width=".2" start="7" end="7">has wasted BPS</col> + <col style="dashed" width=".25" id="subframe_e">wasted BPS (+1)</col> </row> + <row> + <col width=".283" start="0" + end="subframe bps - 1">warm-up sample₀</col> + <col width=".283" start="0" + end="subframe bps - 1">warm-up sample₁</col> + <col width=".283" start="0" + end="subframe bps - 1">warm-up sample₂</col> + <col width=".15" style="dashed">...</col> + </row> + <row> + <col>residual block</col> + </row> + <line style="dotted"> + <start id="subframe" corner="sw"/> + <end id="subframe_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="subframe" corner="se"/> + <end id="subframe_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/fixed2.bdx
Changed
(renamed from docs/reference/figures/flac/fixed2.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/frame.bdx
Changed
(renamed from docs/reference/figures/flac/frame.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/frames.bdx
Added
@@ -0,0 +1,54 @@ +<?xml version="1.0" ?> +<diagram> + <row> + <col width=".333333" id="frame">Frame₀</col> + <col width=".333333">Frame₁</col> + <col style="dashed" width=".333333">...</col> + </row> + <spacer/> + <row> + <col width=".18" id="frame_s">Frame Header</col> + <col width=".15">Subframe₀</col> + <col width=".15">Subframe₁</col> + <col width=".15">Subframe₂</col> + <col width=".15">Subframe₃</col> + <col width=".05" style="dashed">...</col> + <col width=".07" style="dashed">align</col> + <col width=".1" start="0" end="15" id="frame_e">CRC-16</col> + </row> + <spacer/> + <row> + <col end="13" start="0" width=".55" id="frame_h_s">sync code (0x3FFE)</col> + <col end="14" start="14" width=".20">reserved (0)</col> + <col end="15" start="15" width=".25" id="frame_h_e">blocking strategy</col> + </row> + <row> + <col end="19" start="16" width=".20">block size</col> + <col end="23" start="20" width=".20">sample rate</col> + <col end="27" start="24" width=".25">channel assignment</col> + <col end="30" start="28" width=".20">bits per sample</col> + <col end="31" start="31" width=".15">padding</col> + </row> + <row> + <col end="39-87" start="32" width=".30">sample/frame number</col> + <col end="0/7/15" start="0" style="dashed" width=".25">block size</col> + <col end="0/7/15" start="0" style="dashed" width=".25">sample rate</col> + <col end="7" start="0" width=".20">CRC-8</col> + </row> + <line style="dotted"> + <start id="frame" corner="sw"/> + <end id="frame_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame" corner="se"/> + <end id="frame_e" corner="ne"/> + </line> + <line style="dotted"> + <start id="frame_s" corner="sw"/> + <end id="frame_h_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="frame_s" corner="se"/> + <end id="frame_h_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/header-example.bpx
Changed
(renamed from docs/reference/figures/flac/header-example.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/lag0.fig
Changed
(renamed from docs/reference/figures/flac/lag0.fig)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/lag1.fig
Changed
(renamed from docs/reference/figures/flac/lag1.fig)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/lag2.fig
Changed
(renamed from docs/reference/figures/flac/lag2.fig)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/lag3.fig
Changed
(renamed from docs/reference/figures/flac/lag3.fig)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/lpc-parse.bpx
Added
@@ -0,0 +1,16 @@ +<?xml version="1.0" ?> +<struct endianness="big"> + <field size="1" value="0">pad</field> + <field size="1" value="1">type</field> + <field size="5" value="00010b">order (+1)</field> + <field size="1" value="0">wasted</field> + <field size="16" value="43">warm-up sample₀</field> + <field size="16" value="48">warm-up sample₁</field> + <field size="16" value="50">warm-up sample₂</field> + <field size="4" value="11">QLP precision (+1)</field> + <field size="5" value="10">QLP shift needed</field> + <field size="12" value="1451">QLP coefficient₀</field> + <field size="12" value="0xebd">QLP coefficient₁</field> + <field size="12" value="0xf92">QLP coefficient₂</field> + <field size="28" value="0000010010001001001101110101b">residual block</field> +</struct>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/lpc-parse2.bpx
Changed
(renamed from docs/reference/figures/flac/lpc-parse2.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/lpc.bdx
Added
@@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="utf-8"?> +<diagram> + <row> + <col width=".18">Frame Header</col> + <col width=".15">Subframe₀</col> + <col width=".15">Subframe₁</col> + <col width=".15">Subframe₂</col> + <col width=".15" id="subframe">Subframe₃</col> + <col width=".05" style="dashed">...</col> + <col width=".07" style="dashed">align</col> + <col width=".1" start="0" end="15" id="frame_e">CRC-16</col> + </row> + <spacer/> + <row> + <col width=".1" start="0" end="0" id="subframe_s">pad</col> + <col width=".225" start="1" end="1">type (1)</col> + <col width=".225" start="2" end="6">order (+1)</col> + <col width=".2" start="7" end="7">has wasted BPS</col> + <col style="dashed" width=".25" id="subframe_e">wasted BPS (+1)</col> + </row> + <row> + <col start="(subframe bps) bits" + end="(subframe bps) bits" + width=".25">warm-up sample₀</col> + <col start="(subframe bps) bits" + end="(subframe bps) bits" + width=".25">warm-up sample₁</col> + <col start="(subframe bps) bits" + end="(subframe bps) bits" + width=".25">warm-up sample₂</col> + <col width=".25" style="dashed">...</col> + </row> + <row> + <col start="0" end="3" width=".40">QLP precision (+1)</col> + <col start="4" end="8" width=".60">QLP shift needed</col> + </row> + <row> + <col start="(QLP precision) bits" + end ="(QLP precision) bits" + width=".25">QLP coefficient₀</col> + <col start="(QLP precision) bits" + end ="(QLP precision) bits" + width=".25">QLP coefficient₁</col> + <col start="(QLP precision) bits" + end ="(QLP precision) bits" + width=".25">QLP coefficient₂</col> + <col style="dashed" width=".25">...</col> + </row> + <row> + <col>residual block</col> + </row> + <line style="dotted"> + <start id="subframe" corner="sw"/> + <end id="subframe_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="subframe" corner="se"/> + <end id="subframe_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/lpc2.bdx
Changed
(renamed from docs/reference/figures/flac/lpc2.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/metadata-blocks.bdx
Added
@@ -0,0 +1,44 @@ +<?xml version="1.0" ?> +<diagram> + <row> + <col end="31" start="0" width=".20">header (‘fLaC’)</col> + <col id="blocks" width=".30">Metadata Blocks</col> + <col width=".50">Frames</col> + </row> + <spacer/> + <row> + <col width=".125" id="blocks_s">Block₀</col> + <col width=".125">Block₁</col> + <col width=".125">Block₂</col> + <col width=".125">Block₃</col> + <col width=".125">Block₄</col> + <col width=".125">Block₅</col> + <col width=".125">Block₆</col> + <col width=".125" style="dashed" id="blocks_e">...</col> + </row> + <spacer/> + <row> + <col start="0" end="0" width=".20" id="block_h_s">last block</col> + <col start="1" end="7" width=".30">block type</col> + <col start="8" end="31" width=".50" id="block_h_e">block size</col> + </row> + <row> + <col>Metadata Block Data</col> + </row> + <line style="dotted"> + <start id="blocks" corner="sw"/> + <end id="blocks_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="blocks" corner="se"/> + <end id="blocks_e" corner="ne"/> + </line> + <line style="dotted"> + <start id="blocks_s" corner="sw"/> + <end id="block_h_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="blocks_s" corner="se"/> + <end id="block_h_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/metadata.bdx
Changed
(renamed from docs/reference/figures/flac/metadata.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/padding.bdx
Added
@@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<diagram> + <row> + <col width=".125">Block₀</col> + <col width=".125" id="block">Block₁</col> + <col width=".125">Block₂</col> + <col width=".125">Block₃</col> + <col width=".125">Block₄</col> + <col width=".125">Block₅</col> + <col width=".125">Block₆</col> + <col width=".125" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="0" width=".20" id="block_h_s">last block</col> + <col start="1" end="7" width=".30">block type (1)</col> + <col start="8" end="31" width=".50" id="block_h_e">block size</col> + </row> + <row> + <col>NULL bytes</col> + </row> + <line style="dotted"> + <start id="block" corner="sw"/> + <end id="block_h_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="block" corner="se"/> + <end id="block_h_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/picture-example.bypx
Added
@@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="block header">060000A6</field> + <field label="picture type">00000003</field> + <field label="MIME type length">00000009</field> + <field label="MIME type">696d6167652f706e67</field> + <field label="description length">00000000</field> + <field label="width">0000000b</field> + <field label="height">0000000c</field> + <field label="color depth">00000008</field> + <field label="color count">00000000</field> + <field label="data length">0000007d</field> + <field label="image data">89504e470d0a1a0a0000000d494844520000000b0000000c080000000091c21842000000017352474200aece1ce9000000097048597300000b1300000b1301009a9c180000000774494d4507dc0606120915a0bef4340000000f4944415408d763f88f000c83800d00fd8c837d8842fa250000000049454e44ae426082</field> +</bytestruct>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/picture.bdx
Added
@@ -0,0 +1,53 @@ +<?xml version="1.0" ?> +<diagram> + <row> + <col width=".125">Block₀</col> + <col width=".125">Block₁</col> + <col width=".125">Block₂</col> + <col width=".125">Block₃</col> + <col width=".125">Block₄</col> + <col width=".125">Block₅</col> + <col width=".125" id="block">Block₆</col> + <col width=".125" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="0" width=".20" id="block_h_s">last block</col> + <col start="1" end="7" width=".30">block type (6)</col> + <col start="8" end="31" width=".50" id="block_h_e">block size</col> + </row> + <row> + <col end="31" start="0">picture type</col> + </row> + <row> + <col start="32" end="63" width=".25">MIME type length</col> + <col width=".75" + start="(MIME type length) bytes" + end="(MIME type length) bytes">MIME type</col> + </row> + <row> + <col width=".25" start="0" end="31">description length</col> + <col width=".75" + start="(description length) bytes" + end="(description length) bytes">description</col> + </row> + <row> + <col width=".25" start="0" end="31">width</col> + <col width=".25" start="32" end="63">height</col> + <col width=".25" start="64" end="95">color depth</col> + <col width=".25" start="96" end="127">color count</col> + </row> + <row> + <col width=".25" start="128" end="159">data length</col> + <col width=".75" + start="(data length) bytes" end="(data length) bytes">image data</col> + </row> + <line style="dotted"> + <start id="block" corner="sw"/> + <end id="block_h_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="block" corner="se"/> + <end id="block_h_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/read_utf8.dot
Changed
(renamed from docs/reference/figures/flac/read_utf8.dot)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/residual-example1.bpx
Changed
(renamed from docs/reference/figures/flac/residual-example1.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/residual-example2.bpx
Changed
(renamed from docs/reference/figures/flac/residual-example2.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/residual-example3.bpx
Changed
(renamed from docs/reference/figures/flac/residual-example3.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/residual-example4.bpx
Changed
(renamed from docs/reference/figures/flac/residual-example4.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/residual-example5.bpx
Changed
(renamed from docs/reference/figures/flac/residual-example5.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/residual-parse.bpx
Added
@@ -0,0 +1,26 @@ +<?xml version="1.0" ?> +<struct endianness="big"> + <field size="2" value="00b">coding</field> + <field size="4" value="0000b">partition order</field> + <field size="4" value="0010b">Rice parameter₁</field> + <field size="1" value="1b">MSB₀</field> + <field size="2" value="11b">LSB₀</field> + <field size="2" value="01b">MSB₁</field> + <field size="2" value="10b">LSB₁</field> + <field size="1" value="1b">MSB₂</field> + <field size="2" value="01b">LSB₂</field> + <field size="3" value="001b">MSB₃</field> + <field size="2" value="01b">LSB₃</field> + <field size="1" value="1b">MSB₄</field> + <field size="2" value="10b">LSB₄</field> + <field size="3" value="001b">MSB₅</field> + <field size="2" value="01b">LSB₅</field> + <field size="3" value="001b">MSB₆</field> + <field size="2" value="00b">LSB₆</field> + <field size="1" value="1b">MSB₇</field> + <field size="2" value="11b">LSB₇</field> + <field size="2" value="01b">MSB₈</field> + <field size="2" value="01b">LSB₈</field> + <field size="1" value="1b">MSB₉</field> + <field size="2" value="10b">LSB₉</field> +</struct>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/residual.bdx
Changed
(renamed from docs/reference/figures/flac/residual.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/residuals-enc-example.bpx
Added
@@ -0,0 +1,24 @@ +<?xml version="1.0" ?> +<struct endianness="big"> + <field size="2" value="0">coding</field> + <field size="4" value="0">partition order</field> + <field size="4" value="1">Rice parameter₀</field> + <field size="3" value="001b">MSB₀</field> + <field size="1" value="0">LSB₀</field> + <field size="7" value="0000001b">MSB₁</field> + <field size="1" value="0">LSB₁</field> + <field size="2" value="01b">MSB₂</field> + <field size="1" value="1">LSB₂</field> + <field size="1" value="1b">MSB₃</field> + <field size="1" value="0">LSB₃</field> + <field size="1" value="1b">MSB₄</field> + <field size="1" value="1">LSB₄</field> + <field size="2" value="01b">MSB₅</field> + <field size="1" value="1">LSB₅</field> + <field size="4" value="0001b">MSB₆</field> + <field size="1" value="0">LSB₆</field> + <field size="1" value="1b">MSB₇</field> + <field size="1" value="1">LSB₇</field> + <field size="3" value="001b">MSB₈</field> + <field size="1" value="1">LSB₈</field> +</struct>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/seektable-example.bypx
Added
@@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="block header">0300006C</field> + <field label="sample number₀">0000000000000000</field> + <field label="byte offset₀">0000000000000000</field> + <field label="samples₀">1000</field> + <field label="sample number₁">000000000006b000</field> + <field label="byte offset₁">000000000006a7ef</field> + <field label="samples₁">1000</field> + <field label="sample number₂">00000000000d7000</field> + <field label="byte offset₂">00000000000d6bf4</field> + <field label="samples₂">1000</field> + <field label="sample number₃">0000000000142000</field> + <field label="byte offset₃">0000000000141f7a</field> + <field label="samples₃">1000</field> + <field label="sample number₄">00000000001ae000</field> + <field label="byte offset₄">00000000001ae85f</field> + <field>1000</field> + <field label="sample number₅">000000000021a000</field> + <field label="byte offset₅">000000000021a6d3</field> + <field label="samples₅">1000</field> +</bytestruct>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/seektable.bdx
Added
@@ -0,0 +1,46 @@ +<?xml version="1.0" ?> +<diagram> + <row> + <col width=".125">Block₀</col> + <col width=".125">Block₁</col> + <col width=".125">Block₂</col> + <col width=".125" id="block">Block₃</col> + <col width=".125">Block₄</col> + <col width=".125">Block₅</col> + <col width=".125">Block₆</col> + <col width=".125" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="0" width=".20" id="block_h_s">last block</col> + <col start="1" end="7" width=".30">block type (3)</col> + <col start="8" end="31" width=".50" id="block_h_e">block size</col> + </row> + <row> + <col end="143" id="seekpoint" start="0" width=".333333">Seekpoint₀</col> + <col end="287" start="144" width=".333333">Seekpoint₁</col> + <col style="dashed" width=".333333">...</col> + </row> + <spacer/> + <row> + <col start="0" end="63" id="sample_number" width=".40">sample number</col> + <col end="127" start="64" width=".30">byte offset</col> + <col end="143" id="samples_in_frame" start="128" width=".30">samples</col> + </row> + <line style="dotted"> + <start id="block" corner="sw"/> + <end id="block_h_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="block" corner="se"/> + <end id="block_h_e" corner="ne"/> + </line> + <line style="dotted"> + <start corner="sw" id="seekpoint"/> + <end corner="nw" id="sample_number"/> + </line> + <line style="dotted"> + <start corner="se" id="seekpoint"/> + <end corner="ne" id="samples_in_frame"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/stream.bdx
Added
@@ -0,0 +1,7 @@ +<?xml version="1.0" ?><diagram> + <row> + <col end="31" start="0" width=".20">header (‘fLaC’)</col> + <col width=".30">Metadata Blocks</col> + <col width=".50">Frames</col> + </row> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/stream2.bdx
Changed
(renamed from docs/reference/figures/flac/stream2.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/stream3.bdx
Changed
(renamed from docs/reference/figures/flac/stream3.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/streaminfo-example.bpx
Added
@@ -0,0 +1,15 @@ +<?xml version="1.0" ?> +<struct endianness="big"> + <field size="1" value="0x0">last</field> + <field size="7" value="0x0">block type</field> + <field size="24" value="0x22">block size</field> + <field size="16" value="4096">minimum block size</field> + <field size="16" value="4096">maximum block size</field> + <field size="24" value="1542">minimum frame size</field> + <field size="24" value="8546">maximum frame size</field> + <field size="20" value="44100">sample rate</field> + <field size="3" value="1">channels (+1)</field> + <field size="5" value="15">bits per sample (+1)</field> + <field size="36" value="304844">total PCM frames</field> + <bytes value="faf2692ffdec2d5b300176b462887d92">MD5 sum</bytes> +</struct>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/streaminfo.bdx
Added
@@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<diagram> + <row> + <col width=".125" id="blocks_s">Block₀</col> + <col width=".125">Block₁</col> + <col width=".125">Block₂</col> + <col width=".125">Block₃</col> + <col width=".125">Block₄</col> + <col width=".125">Block₅</col> + <col width=".125">Block₆</col> + <col width=".125" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="0" width=".20" id="block_h_s">last block</col> + <col start="1" end="7" width=".30">block type (0)</col> + <col start="8" end="31" width=".50" id="block_h_e">block size (34)</col> + </row> + <row> + <col start="0" end="15" width=".5">minimum block size (in PCM frames)</col> + <col start="16" end="31" width=".5">maximum block size (in PCM frames)</col> + </row> + <row> + <col start="32" end="55" width=".5">minimum frame size (in bytes)</col> + <col start="56" end="79" width=".5">maximum frame size (in bytes)</col> + </row> + <row> + <col start="80" end="99" width=".40">sample rate</col> + <col start="100" end="102" width=".30">channels (+ 1)</col> + <col start="103" end="107" width=".30">bits per sample (+ 1)</col> + </row> + <row> + <col start="108" end="143">total PCM frames</col> + </row> + <row> + <col start="144" end="271">MD5 sum of PCM data</col> + </row> + <line style="dotted"> + <start id="blocks_s" corner="sw"/> + <end id="block_h_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="blocks_s" corner="se"/> + <end id="block_h_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/subframes.bdx
Added
@@ -0,0 +1,31 @@ +<?xml version="1.0" ?> +<diagram> + <row> + <col width=".18">Frame Header</col> + <col width=".15" id="subframe">Subframe₀</col> + <col width=".15">Subframe₁</col> + <col width=".15">Subframe₂</col> + <col width=".15">Subframe₃</col> + <col width=".05" style="dashed">...</col> + <col width=".07" style="dashed">align</col> + <col width=".1" start="0" end="15">CRC-16</col> + </row> + <spacer/> + <row> + <col width=".1" start="0" end="0" id="subframe_s">pad</col> + <col width=".45" start="1" end="6">subframe type and order</col> + <col width=".2" start="7" end="7">has wasted BPS</col> + <col style="dashed" width=".25" id="subframe_e">wasted BPS (+1)</col> + </row> + <row> + <col id="subframe_data">subframe data</col> + </row> + <line style="dotted"> + <start id="subframe" corner="sw"/> + <end id="subframe_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="subframe" corner="se"/> + <end id="subframe_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/tukey.plot
Changed
(renamed from docs/reference/figures/flac/tukey.plot)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/utf8.bpx
Changed
(renamed from docs/reference/figures/flac/utf8.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/verbatim.bdx
Added
@@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<diagram> + <row> + <col width=".18">Frame Header</col> + <col width=".15">Subframe₀</col> + <col width=".15" id="subframe">Subframe₁</col> + <col width=".15">Subframe₂</col> + <col width=".15">Subframe₃</col> + <col width=".05" style="dashed">...</col> + <col width=".07" style="dashed">align</col> + <col width=".1" start="0" end="15" id="frame_e">CRC-16</col> + </row> + <spacer/> + <row> + <col width=".1" start="0" end="0" id="subframe_s">pad</col> + <col width=".45" start="1" end="6">subframe type (1) </col> + <col width=".2" start="7" end="7">has wasted BPS</col> + <col style="dashed" width=".25" id="subframe_e">wasted BPS (+1)</col> + </row> + <row> + <col width=".283" start="0" + end="subframe bps - 1">uncompressed sample₀</col> + <col width=".283" start="0" + end="subframe bps - 1">uncompressed sample₁</col> + <col width=".283" start="0" + end="subframe bps - 1">uncompressed sample₂</col> + <col width=".15" style="dashed">...</col> + </row> + <line style="dotted"> + <start id="subframe" corner="sw"/> + <end id="subframe_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="subframe" corner="se"/> + <end id="subframe_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/vorbiscomment-example.bypx
Added
@@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<bytestruct> + <field label="block header">04000072</field> + <field label="vendor length">20000000</field> + <field label="vendor string">7265666572656e6365206c6962464c414320312e322e31203230303730393137</field> + <field label="count">04000000</field> + <field label="length₀">0b000000</field> + <field label="comment₀">414c42554d3d616c62756d</field> + <field label="length₁">0d000000</field> + <field label="comment₁">545241434b4e554d4245523d31</field> + <field label="length₂">10000000</field> + <field label="comment₂">5449544c453d747261636b206e616d65</field> + <field label="length₃">12000000</field> + <field label="comment₃">4152544953543d617274697374206e616d65</field> +</bytestruct>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/vorbiscomment.bdx
Added
@@ -0,0 +1,58 @@ +<?xml version="1.0" ?> +<diagram> + <row> + <col width=".125">Block₀</col> + <col width=".125">Block₁</col> + <col width=".125">Block₂</col> + <col width=".125">Block₃</col> + <col width=".125" id="block">Block₄</col> + <col width=".125">Block₅</col> + <col width=".125">Block₆</col> + <col width=".125" style="dashed">...</col> + </row> + <spacer/> + <row> + <col start="0" end="0" width=".20" id="block_h_s">last block</col> + <col start="1" end="7" width=".30">block type (4)</col> + <col start="8" end="31" width=".50" id="block_h_e">block size</col> + </row> + <row> + <col start="0" end="31" width=".35" + background-color="#0000FF30">vendor string length</col> + <col width=".65" + start="(vendor string length) bytes" + end="(vendor string length) bytes" + background-color="#0000FF30">vendor string</col> + </row> + <row> + <col start="0" end="31" + background-color="#0000FF30">comment string count</col> + </row> + <row> + <col start="0" end="31" width=".35" + background-color="#0000FF30">comment string length₀</col> + <col width=".65" + start="(comment string length₀) bytes" + end="(comment string length₀) bytes" + background-color="#0000FF30">comment string₀</col> + </row> + <row> + <col start="0" end="31" width=".35" + background-color="#0000FF30">comment string length₁</col> + <col width=".65" + start="(comment string length₁) bytes" + end="(comment string length₁) bytes" + background-color="#0000FF30">comment string₁</col> + </row> + <row> + <col background-color="#0000FF30">...</col> + </row> + <line style="dotted"> + <start id="block" corner="sw"/> + <end id="block_h_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="block" corner="se"/> + <end id="block_h_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/flac/figures/write_utf8.dot
Changed
(renamed from docs/reference/figures/flac/write_utf8.dot)
View file
audiotools-2.19.tar.gz/docs/reference/flac/metadata.tex
Added
@@ -0,0 +1,272 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\section{Metadata Blocks} +\begin{figure}[h] + \includegraphics{flac/figures/metadata-blocks.pdf} +\end{figure} +\par +\noindent +\VAR{last block} of \texttt{1} indicates the block is the last one +in the set of metadata blocks. +\VAR{block size} indicates the size of the metadata block data, +not including its 32 bit header. +\VAR{block type} is one of the following: +\begin{table}[h] +\begin{tabular}{r | l} + block type & block \\ + \hline + \texttt{0} & STREAMINFO \\ + \texttt{1} & PADDING \\ + \texttt{2} & APPLICATION \\ + \texttt{3} & SEEKTABLE \\ + \texttt{4} & VORBIS\_COMMENT \\ + \texttt{5} & CUESHEET \\ + \texttt{6} & PICTURE \\ + \texttt{7-126} & reserved \\ + \texttt{127} & invalid \\ +\end{tabular} +\end{table} + +\clearpage + +\subsection{STREAMINFO} +\begin{figure}[h] +\includegraphics{flac/figures/streaminfo.pdf} +\end{figure} +\par +\noindent +STREAMINFO must always be the first metadata block and always +has a block size of 34 bytes. +\VAR{minimum block size} and \VAR{maximum block size} +indicate the length of the largest and smallest +frame in the stream, in PCM frames. +For streams with a fixed block size (which are the most common), +these numbers will be the same. +\VAR{minimum frame size} and \VAR{maximum frame size} +indicate the size of the largest and smallest frame in the stream, +in bytes. +\VAR{sample rate}, \VAR{channels} and \VAR{bits per sample} +indicate the stream's characteristics. +Finally, \VAR{MD5 sum} is a hash of the stream's raw decoded data +when its PCM sample integers are decoded to signed, little-endian bytes. + +\clearpage + +\subsubsection{STREAMINFO example} +\begin{figure}[h] + \includegraphics{flac/figures/streaminfo-example.pdf} +\end{figure} +\begin{tabular}{rrl} +last block & \texttt{0} & not last block in set of blocks \\ +block type & \texttt{0} & STREAMINFO \\ +block size & \texttt{34} & bytes \\ +minimum block size & \texttt{4096} & PCM frames \\ +maximum block size & \texttt{4096} & PCM frames \\ +minimum frame size & \texttt{1542} & bytes \\ +maximum frame size & \texttt{8546} & bytes \\ +sample rate & \texttt{44100} & Hz \\ +channels & \texttt{1} & $+ 1 = 2$ \\ +bits per sample & \texttt{15} & $+ 1 = 16$ \\ +total PCM frames & \texttt{304844} & \\ +MD5 sum & \multicolumn{2}{l}{\texttt{FAF2692FFDEC2D5B300176B462887D92}} \\ +\end{tabular} + +\clearpage + +\subsection{PADDING} +\begin{figure}[h] + \includegraphics{flac/figures/padding.pdf} +\end{figure} +\par +\noindent +PADDING is simply a block full of NULL (\texttt{0x00}) bytes. +Its purpose is to provide extra metadata space within the FLAC file. +By having a padding block, other metadata blocks can be grown or +shrunk without having to rewrite the entire FLAC file by removing or +adding space to the padding. + +\subsection{APPLICATION} +\begin{figure}[h] +\includegraphics{flac/figures/application.pdf} +\end{figure} +\noindent +APPLICATION is a general-purpose metadata block used by a variety of +different programs. +Its contents are defined by the 4 byte ASCII Application ID value. +\begin{table}[h] +{\relsize{-1} +\begin{tabular}{rl} +\texttt{Fica} & CUE Splitter \\ +\texttt{Ftol} & flac-tools \\ +\texttt{aiff} & FLAC AIFF chunk storage \\ +\texttt{imag} & flac-image application for storing arbitrary files in APPLICATION metadata blocks \\ +\texttt{riff} & FLAC RIFF chunk storage \\ +\texttt{xmcd} & xmcd \\ +\end{tabular} +} +\caption{some defined application IDs} +\end{table} + +\clearpage + +\subsection{SEEKTABLE} +\begin{figure}[h] +\includegraphics{flac/figures/seektable.pdf} +\end{figure} +\par +\noindent +There are $\textsf{block size} \div 18$ seek points in the SEEKTABLE block. +Each seek point indicates a position in the FLAC stream. +\VAR{sample number} indicates the seek point's sample number, in PCM frames. +\VAR{byte offset} indicates the seek point's +byte position in the stream starting from the beginning +of the first FLAC frame (\textit{not} the beginning of the FLAC file itself). +\VAR{samples} indicates the number of PCM frames +in the seek point's FLAC frame. +\subsubsection{SEEKTABLE example} +\begin{figure}[h] + \includegraphics{flac/figures/seektable-example.pdf} +\end{figure} +\begin{table}[h] +{\relsize{-1} +\begin{tabular}{r|rrr} +$i$ & $\text{sample number}_i$ & $\text{byte offset}_i$ & $\text{samples}_i$ \\ +\hline +0 & 0 & 0 & 4096 \\ +1 & 438272 & 436207 & 4096 \\ +2 & 880640 & 879604 & 4096 \\ +3 & 1318912 & 1318778 & 4096 \\ +4 & 1761280 & 1763423 & 4096 \\ +5 & 2203648 & 2205395 & 4096 \\ +\end{tabular} +} +\end{table} + +\clearpage + +\subsection{VORBIS\_COMMENT} +\begin{figure}[h] +\includegraphics{flac/figures/vorbiscomment.pdf} +\end{figure} +\par +\noindent +The blue fields are all stored little-endian. +Comment strings are \texttt{"KEY=value"} pairs +where \texttt{KEY} is an ASCII value in the range \texttt{0x20} +through \texttt{0x7D}, excluding \texttt{0x3D}, +is case-insensitive and may occur in multiple comment strings. +\texttt{value} is a UTF-8 value. +\begin{table}[h] + {\relsize{-1} + \begin{tabular}{rl} + \texttt{ALBUM} & album name \\ + \texttt{ARTIST} & artist name \\ + \texttt{CATALOG} & CD spine number \\ + \texttt{COMPOSER} & the work's author \\ + \texttt{COMMENT} & a short comment \\ + \texttt{CONDUCTOR} & performing ensemble's leader \\ + \texttt{COPYRIGHT} & copyright attribution \\ + \texttt{DATE} & recording date \\ + \texttt{DISCNUMBER} & disc number for multi-volume work \\ + \texttt{DISCTOTAL} & disc total for multi-volume work \\ + \texttt{GENRE} & a short music genre label \\ + \texttt{ISRC} & ISRC number for the track \\ + \texttt{PERFORMER} & performer name, orchestra, actor, etc. \\ + \texttt{PUBLISHER} & album's publisher \\ + \texttt{SOURCE MEDIUM} & CD, radio, cassette, vinyl LP, etc. \\ + \texttt{TITLE} & track name \\ + \texttt{TRACKNUMBER} & track number \\ + \texttt{TRACKTOTAL} & total number of tracks \\ + \end{tabular} + } +\end{table} + +\clearpage + +\subsubsection{VORBIS\_COMMENT Example} +\begin{figure}[h] + \includegraphics{flac/figures/vorbiscomment-example.pdf} +\end{figure} +\begin{table}[h] +\begin{tabular}{rl} + vendor string : & \texttt{reference libFLAC 1.2.1 20070917} \\ + $\text{comment}_0$ : & \texttt{ALBUM=album} \\ + $\text{comment}_1$ : & \texttt{TRACKNUMBER=1} \\ + $\text{comment}_2$ : & \texttt{TITLE=track name} \\ + $\text{comment}_3$ : & \texttt{ARTIST=artist name} \\ +\end{tabular} +\end{table} + +\clearpage + +%% \subsubsection{CUESHEET Example} + +%% \begin{figure}[h] +%% \includegraphics{flac/figures/cuesheet-example.pdf} +%% \end{figure} + +%% \clearpage + +\subsection{PICTURE} +\begin{figure}[h] +\includegraphics{flac/figures/picture.pdf} +\end{figure} +{\relsize{-1} +\begin{tabular}{r|l} +picture type & type \\ +\hline +0 & Other \\ +1 & 32x32 pixels `file icon' (PNG only) \\ +2 & Other file icon \\ +3 & Cover (front) \\ +4 & Cover (back) \\ +5 & Leaflet page \\ +6 & Media (e.g. label side of CD) \\ +7 & Lead artist / Lead performer / Soloist \\ +8 & Artist / Performer \\ +9 & Conductor \\ +10 & Band / Orchestra \\ +11 & Composer \\ +12 & Lyricist / Text writer \\ +13 & Recording location \\ +14 & During recording \\ +15 & During performance \\ +16 & Movie / Video screen capture \\ +17 & A bright colored fish \\ +18 & Illustration \\ +19 & Band / Artist logotype \\ +20 & Publisher / Studio logotype \\ +\end{tabular} +} +\clearpage + +\subsubsection{PICTURE Example} +\begin{figure}[h] + \includegraphics{flac/figures/picture-example.pdf} +\end{figure} +\begin{table}[h] +{\relsize{-1} + \begin{tabular}{rl} + MIME type : & \texttt{"image/png"} \\ + description : & \texttt{""} (empty string) \\ + width : & \texttt{11} pixels \\ + height : & \texttt{12} pixels \\ + color depth : & \texttt{8} bits per channel \\ + color count : & \texttt{0} (not an indexed color image) \\ + data length : & \texttt{125} bytes \\ + \end{tabular} +} +\end{table} + +\clearpage + +\subsection{CUESHEET} +\begin{figure}[h] + \includegraphics{flac/figures/cuesheet.pdf} +\end{figure}
View file
audiotools-2.19.tar.gz/docs/reference/footer.tex
Changed
(renamed from docs/reference/footer.m4)
View file
audiotools-2.19.tar.gz/docs/reference/header.tex
Added
@@ -0,0 +1,77 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\documentclass[<<const:PAPERSIZE>>]{scrbook} +\setlength{\pdfpagewidth}{\paperwidth} +\setlength{\pdfpageheight}{\paperheight} +\setlength{\textwidth}{6in} +\usepackage{amsmath} +\usepackage{graphicx} +\usepackage{picins} +\usepackage{fancyvrb} +\usepackage{relsize} +\usepackage{array} +\usepackage{wrapfig} +\usepackage{subfig} +\usepackage{multicol} +\usepackage{paralist} +\usepackage{textcomp} +\usepackage{fancyvrb} +\usepackage{multirow} +\usepackage{rotating} +\usepackage[toc,page]{appendix} +\usepackage{hyperref} +\usepackage{units} +\usepackage{color} +\definecolor{gray}{rgb}{0.5,0.5,0.5} +\definecolor{red}{rgb}{1.0,0.0,0.0} +\definecolor{orange}{rgb}{1.0,0.4,0.0} +\definecolor{fuchsia}{rgb}{1.0,0.0,1.0} +\definecolor{blue}{rgb}{0.0,0.0,1.0} +\definecolor{green}{rgb}{0.0,1.0,0.0} +\definecolor{darkgreen}{rgb}{0.0,0.75,0.0} +\usepackage[vlined,lined,commentsnumbered]{algorithm2e} +\usepackage{lscape} +\newcommand{\xor}{\textbf{ xor }} +%#1 = i +%#2 = byte +%#3 = previous checksum +%#4 = shift results +%#5 = new xor +%#6 = new CRC-16 +\newcommand{\CRCSIXTEEN}[6]{\text{checksum}_{#1} &= \text{CRC16}(\texttt{#2}\xor(\texttt{#3} \gg \texttt{8}))\xor(\texttt{#3} \ll \texttt{8}) = \text{CRC16}(\texttt{#4})\xor \texttt{#5} = \texttt{#6}} +\newcommand{\LINK}[1]{\href{#1}{\texttt{#1}}} +\newcommand{\REFERENCE}[2]{\item #1 \\ \LINK{#2}} +\newcommand{\VAR}[1]{``{#1}''} +\newcommand{\ATOM}[1]{\texttt{#1}} +\newcommand{\IDOTS}{\mathrel{\ldotp\ldotp}} +\newcommand{\ALGORITHM}[2]{\begin{algorithm}[H] + \DontPrintSemicolon + \SetKw{READ}{read} + \SetKw{WRITE}{write} + \SetKw{UNARY}{read unary} + \SetKw{WUNARY}{write unary} + \SetKw{SKIP}{skip} + \SetKw{ASSERT}{assert} + \SetKw{IN}{in} + \KwIn{#1} + \KwOut{#2} + \BlankLine +} +\newcommand{\EALGORITHM}{\end{algorithm}} +\long\def\symbolfootnote[#1]#2{\begingroup% + \def\thefootnote{\fnsymbol{footnote}}\footnote[#1]{#2}\endgroup} +\long\def\symbolfootnotemark[#1]{\begingroup% + \def\thefootnote{\fnsymbol{footnote}}\footnotemark[#1]\endgroup} +\long\def\symbolfootnotetext[#1]#2{\begingroup% + \def\thefootnote{\fnsymbol{footnote}}\footnotetext[#1]{#2}\endgroup} +\title{Audio Formats Reference} +\author{Brian Langenberger} +\begin{document} +\maketitle +\tableofcontents
View file
audiotools-2.18.tar.gz/docs/reference/introduction.tex -> audiotools-2.19.tar.gz/docs/reference/introduction.tex
Changed
@@ -1,44 +1,44 @@ \chapter{Introduction} -This book is intended as a reference for anyone who's ever looked -at their collection of audio files and wondered how they worked. -Though still a work-in-progress, my goal is to create documentation -on the full decoding/encoding process of as many audio formats as -possible. +Python Audio Tools, as a collection of software, +is built in a stack-like fashion. +At the top of the stack sits the user-facing utilities +such as \texttt{tracktag} or \texttt{track2track}. +These take command-line arguments, display progress to the user, +have manual pages and so forth. +But these utilities don't do very much work by themselves; +much of \texttt{track2track}'s code is there to facilitate +calling a simple \texttt{convert()} Python method. -Though to be honest, the audience for this is myself. -I enjoy figuring out the little details of how these formats operate. -And as I figure them out and implement them in Python Audio Tools, -I then add some documentation here on what I've just discovered. -That way, when I have to come back to something six months from now, -I can return to some written documentation instead of having to go -directly to my source code. +The \texttt{audiotools} Python module is the next layer of the stack. +It's documented separately and offers a superset of the functionality +offered by the user-facing utilities. +Python programmers can assemble its wide array of classes +and functions into specialized utilities for particular tasks. +One can even use Python's interactive mode to work with +the tools directly (something I find quite helpful when debugging). -Therefore, I try to make my documentation as clear and concise -as possible. -Otherwise, what's the advantage over simply diving back into the source? -Yet this process often turns into a challenge of its own; -I'll discover that a topic I thought I'd understood wasn't so -easy to grasp once I had to simplify and explain it to some -hypothetical future self. -Thus, I'll have to learn it better in order to explain it better. +But Python, as a language, simply isn't designed to do +high-speed numerical processing on its own. +So the third layer of the stack is a set of Python extension +modules written in C to perform audio file decoding, encoding +and stream processing in chunks at an acceptable rate. +Even Python programmers are unlikely to fiddle with these +directly; it's easier to let the higher-level Python routines +smooth out the rough edges for you. -That said, there's still much work left to do. -Because it's a repository of my knowledge, it also illustrates -the limits of my knowledge. -Many formats are little more than ``stubs'', containing -just enough information to extract such metadata as -sample rate or bits-per-sample. -These are formats in which my Python Audio Tools passes the -encoding/decoding task to a binary ``black-box'' executable -since I haven't yet taken the time to learn how to perform that -work myself. -But my hope is that as I learn more, this work will become -more fleshed-out and widely useful. +Yet there's still one more layer to the stack, +which is this documentation on exactly how the +audio formats and their metadata operate. +Everything above sits atop these notes, pseudocode and examples. +Even if Python and C should suddenly vanish in a puff of logic, +I could rebuild my utilities through this documentation +in any language available. -In the meantime, by including it with Python Audio Tools, -my hope is that someone else with some passing interest might also -get some use out of what I've learned. -And though I strive for accuracy (for my own sake, at least) -I cannot guarantee it. -When in doubt, consult the references on page \pageref{references} -for links to external sources which may have additional information. +At the same time, it's also useful for anyone curious +on the inner workings of the audio formats and metadata +supported by Python Audio Tools. +Wherever possible, pseudocode and examples have been +placed on opposite pages so one can work through the code +by hand. +Don't be afraid to open up your files with a hex editor +to see how your music is stored and tagged.
View file
audiotools-2.18.tar.gz/docs/reference/m4a.tex -> audiotools-2.19.tar.gz/docs/reference/m4a.tex
Changed
@@ -145,14 +145,14 @@ \includegraphics{figures/m4a/stsc.pdf} \end{figure} +\clearpage + \subsection{the stsz Atom} \begin{figure}[h] \includegraphics{figures/m4a/stsz.pdf} \end{figure} -\clearpage - \subsection{the stco Atom} \begin{figure}[h] @@ -167,53 +167,43 @@ In that instance, they \textbf{must} be re-adjusted in the \ATOM{stco} atom or the file may become unplayable. +\clearpage + \subsection{the meta Atom} \label{m4a_meta} \begin{figure}[h] \parpic[r]{ \includegraphics{figures/m4a/meta_atoms.pdf} } -\includegraphics{figures/m4a/meta.pdf} -The atoms within the \ATOM{ilst} container are all containers themselves, -each with a \ATOM{data} atom of its own. -Notice that many of \ATOM{ilst}'s sub-atoms begin with the -non-ASCII 0xA9 byte. - -\includegraphics{figures/m4a/data.pdf} +\includegraphics[height=4in,keepaspectratio]{figures/m4a/meta.pdf} +Text data atoms have a \VAR{type} of 1, +which indicates that \VAR{data} is UTF-8 encoded text. +Binary data atoms typically have a \VAR{type} of 0. +\end{figure} +\vskip .10in \par \noindent -Text data atoms have a \VAR{Type} of 1. -Binary data atoms typically have a \VAR{Type} of 0. -\end{figure} -\begin{table}[h] -{\relsize{-1} -\begin{tabular}{|r|l||r|l||r|l|} -\hline -Atom & Description & Atom & Description & Atom & Description \\ -\hline -\ATOM{alb} & Album Name & -\ATOM{ART} & Track Artist & -\ATOM{cmt} & Comments \\ -\ATOM{covr} & Cover Image & -\ATOM{cpil} & Compilation & -\ATOM{cprt} & Copyright \\ -\ATOM{day} & Year & -\ATOM{disk} & Disc Number & -\ATOM{gnre} & Genre \\ -\ATOM{grp} & Grouping & -\ATOM{----} & iTunes-specific & -\ATOM{nam} & Track Name \\ -\ATOM{rtng} & Rating & -\ATOM{tmpo} & BMP & -\ATOM{too} & Encoder \\ -\ATOM{trkn} & Track Number & -\ATOM{wrt} & Composer & -& \\ -\hline -\end{tabular} -\caption{Known \ATOM{ilst} sub-atoms} +{\relsize{0} + \begin{tabular}{rl} + \texttt{alb} & album name \\ + \texttt{ART} & track artist \\ + \texttt{cmt} & comments \\ + \texttt{covr} & cover image \\ + \texttt{cpil} & compilation \\ + \texttt{cprt} & copyright \\ + \texttt{day} & year \\ + \texttt{disk} & disc number \\ + \texttt{gnre} & genre \\ + \texttt{grp} & grouping \\ + \texttt{----} & iTunes-specific \\ + \texttt{nam} & track name \\ + \texttt{rtng} & rating \\ + \texttt{tmpo} & BMP \\ + \texttt{too} & encoder \\ + \texttt{trkn} & track number \\ + \texttt{wrt} & composer \\ + \end{tabular} } -\end{table} \clearpage @@ -232,3 +222,50 @@ \begin{figure}[h] \includegraphics{figures/m4a/disk.pdf} \end{figure} + +\clearpage + +\subsubsection{meta Atom Example} +\begin{figure}[h] +\includegraphics[height=6in]{figures/m4a/meta-example.pdf} +\end{figure} + +\clearpage + +\subsubsection{nam Atom Example} +\includegraphics{figures/m4a/nam-example.pdf} +\par +\noindent +\begin{tabular}{rl} + type : & \texttt{1} (text) \\ + data : & \texttt{"Track Name"} (UTF-8 encoded text) \\ +\end{tabular} + +\subsubsection{alb Atom Example} +\includegraphics{figures/m4a/alb-example.pdf} +\par +\noindent +\begin{tabular}{rl} + type : & \texttt{1} (text) \\ + data : & \texttt{"Album Name"} (UTF-8 encoded text) \\ +\end{tabular} + +\subsubsection{trkn Atom Example} +\includegraphics{figures/m4a/trkn-example.pdf} +\par +\noindent +\begin{tabular}{rl} + type : & \texttt{0} (binary) \\ + track number : & \texttt{1} \\ + total tracks : & \texttt{2} \\ +\end{tabular} + +\subsubsection{disk Atom Example} +\includegraphics{figures/m4a/disk-example.pdf} +\par +\noindent +\begin{tabular}{rl} + type : & \texttt{0} (binary) \\ + disk number : & \texttt{3} \\ + total disks : & \texttt{4} \\ +\end{tabular}
View file
audiotools-2.19.tar.gz/docs/reference/mp3-codec.template
Added
@@ -0,0 +1,3 @@ +<<file:header.tex>> +\include{mp3} +<<file:footer.tex>>
View file
audiotools-2.18.tar.gz/docs/reference/mp3.tex -> audiotools-2.19.tar.gz/docs/reference/mp3.tex
Changed
@@ -123,8 +123,8 @@ \section{ID3v1 Tags} ID3v1 tags are very simple metadata tags appended to an MP3 file. -All of the fields are fixed length and the text encoding is -undefined. +All of the fields are fixed length, padded with NULLs if necessary, +and the text encoding is undefined. There are two versions of ID3v1 tags. ID3v1.1 has a track number field as a 1 byte value at the end of the comment field. @@ -146,6 +146,24 @@ \clearpage +\subsection{ID3v1.1 Tag Example} +\begin{figure}[h] + \includegraphics{figures/mp3/id3v11-example.pdf} +\end{figure} +\begin{table}[h] + \begin{tabular}{rl} + track title : & \texttt{some track name} \\ + artist name : & \texttt{artist's name} \\ + album name : & \texttt{album title} \\ + year : & \texttt{2012} \\ + comment : & \texttt{a lengthy comment field} \\ + track number : & \texttt{1} \\ + genre : & \texttt{0} \\ + \end{tabular} +\end{table} + +\clearpage + \section{ID3v2 Tags} The ID3v2 tag was invented to address the deficiencies in the original @@ -157,58 +175,109 @@ \begin{figure}[h] \includegraphics{figures/mp3/id3v2_stream.pdf} \end{figure} +\par +\noindent +\VAR{major version} is 2 for ID3v2.2, 3 for ID3v2.3 and 4 for ID3v2.4. +\VAR{minor version} is always 0. +The four \VAR{size} fields are recombined as follows: +\begin{equation*} + \text{ID3 Frames size} = (\text{size}_3 \times 2 ^ {21}) + (\text{size}_2 \times 2 ^ {14}) + (\text{size}_1 \times 2 ^ 7) + \text{size}_0 +\end{equation*} +Splitting the field with 0 bits ensures that no size value +will appear to be an MP3 frame sync. -\subsection{ID3v2.2} +\clearpage -\subsubsection{ID3v2.2 Header} +\subsection{ID3v2 Header Example} +\begin{figure}[h] + \includegraphics{figures/mp3/id3v2_stream-example.pdf} +\end{figure} +\begin{table}[h] + \begin{tabular}{rl} + ID : & \texttt{"ID3"} \\ + major version : & \texttt{3} \\ + minor version : & \texttt{0} \\ + flags : & \texttt{0} \\ + $\text{size}_3$ : & \texttt{0} \\ + $\text{size}_2$ : & \texttt{0} \\ + $\text{size}_1$ : & \texttt{1} \\ + $\text{size}_0$ : & \texttt{21} \\ + ID3 Frames size : & $(0 \times 2 ^ {21}) + (0 \times 2 ^ {14}) + (1 \times 2 ^ 7) + 21 = 149$ bytes \\ + \end{tabular} +\end{table} +\par +\noindent +Which indicates this is an ID3v2.3 tag with 149 bytes of ID3v2.3 frames. +Since there is no `total number of frames' field, +we must use the size field to determine when to stop reading +additional ID3 frames. + +\clearpage + +\subsection{ID3v2.2 Header} \begin{figure}[h] \includegraphics{figures/id3v22/header.pdf} \end{figure} \par \noindent -The single Size field is split by NULL (0x00) bytes in order to make -it `sync-safe'. -That is, no possible size value will result in a false -MP3 frame sync (11 bits set in a row). -This size field is the length of the entire tag, not including the header. +The \VAR{unsync} and \VAR{compression} flags are normally unused. + +\subsection{ID3v2.2 Frames} +\begin{figure}[h] +\includegraphics{figures/id3v22/frames.pdf} +\end{figure} +\par +\noindent +\VAR{frame ID} is a 3 byte ASCII string. +\VAR{frame size} is the length of the frame data, +not including its 6 byte header. + +\clearpage \subsubsection{COM Frame} \begin{figure}[h] \includegraphics{figures/id3v22/com.pdf} \end{figure} -This frame is a lengthy string of comment text. -\VAR{Encoding} is 0 for Latin-1, 1 for UCS-2. -\VAR{Language} is a 3 byte ASCII string. -\VAR{Short Content Description} is a NULL-terminated string +\par +\noindent +This frame supports a lengthy string of comment text. +\VAR{encoding} is 0 for Latin-1, 1 for UCS-2, +indicating the text encoding of the \VAR{short description} +and \VAR{comment text} fields. +\VAR{language} is a 3 byte ASCII string. +\VAR{short description} is a NULL-terminated string containing a short description of the comment. Note that for UCS-2 comments, the NULL terminator is 2 bytes. The remainder of the frame is the comment text itself. -\clearpage - -\subsubsection{GEO Frame} +\subsubsection{COM Frame Example} \begin{figure}[h] -\includegraphics{figures/id3v22/geo.pdf} + \includegraphics{figures/id3v22/com-example.pdf} \end{figure} -This frame contains an embedded file. -\VAR{MIME Type} and \VAR{Filename} are encoded as NULL-terminated -Latin-1 strings. -\VAR{Description} is encoded as a Latin-1 or UCS-2 NULL-terminated -string, depending on if \VAR{Encoding} is 0 or 1, respectively. -The remainder of the frame is the file's binary data. +\begin{table}[h] + \begin{tabular}{rl} + encoding : & \texttt{0} (Latin-1) \\ + language : & \texttt{"eng"} \\ + short description : & \texttt{""} (empty string) \\ + comment text : & \texttt{"comment text"} \\ + \end{tabular} +\end{table} + +\clearpage \subsubsection{PIC Frame} \begin{figure}[h] \includegraphics{figures/id3v22/pic.pdf} \end{figure} +\par +\noindent This frame contains an embedded image file. -\VAR{Image Format} is a 3 byte string indicating the format of the image, +\VAR{image format} is a 3 byte string indicating the format of the image, typically `JPG' for JPEG images or 'PNG' for PNG images. -\VAR{Description} is a Latin-1 or UCS-2 encoded NULL-terminated string, -depending on if \VAR{Encoding} is 0 or 1, respectively. -\VAR{Picture Type is one of the following:} - +\VAR{description} is a Latin-1 or UCS-2 encoded NULL-terminated string, +depending on if \VAR{encoding} is 0 or 1, respectively. +\VAR{picture type} is one of the following: \begin{table}[h] {\relsize{-1} \begin{tabular}{|r|l||r|l|} @@ -234,17 +303,34 @@ \clearpage +\subsubsection{PIC Frame Example} +\begin{figure}[h] + \includegraphics{figures/id3v22/pic-example.pdf} +\end{figure} +\begin{table}[h] + \begin{tabular}{rl} + encoding : & \texttt{0} (Latin-1) \\ + image format : & \texttt{"PNG"} \\ + image type : & \texttt{3} (front cover) \\ + description : & \texttt{"Description"} \\ +\end{tabular} +\end{table} + +\clearpage + \subsubsection{Text Frames} \begin{figure}[h] \includegraphics{figures/id3v22/t__.pdf} \end{figure} -These frames are for textual information. -\VAR{Encoding} is 0 for Latin-1 encoding, 1 for UCS-2 encoding. -\begin{table}[h] -{\relsize{-2} -\begin{tabular}{|r|l|} -\hline -Frame ID & Meaning \\ +\par +\noindent +Frames whose ID starts with `T' are for textual information. +\VAR{encoding} is 0 for Latin-1 encoding, 1 for UCS-2 encoding. +\par +\noindent +{\relsize{-1} +\begin{tabular}{r|l} +frame ID & meaning \\ \hline \texttt{TAL} & album / movie / show title \\ \texttt{TBP} & BPM (beats per minute) \\ @@ -281,92 +367,143 @@ \texttt{TT3} & subtitle / description refinement \\ \texttt{TXT} & lyricist / text writer \\ \texttt{TYE} & year \\ -\hline \end{tabular} } -\end{table} -\par + +\clearpage + The \texttt{TRK} and \texttt{TPA} numeric fields may be extended with the ``/'' character, indicating the field's total number. For example, a \texttt{TRK} of ``3/5'' means the track is number 3 out of a total of 5. -\clearpage +\subsubsection{Text Frame Example} +\begin{figure}[h] + \includegraphics{figures/id3v22/t__-example.pdf} +\end{figure} +\begin{table}[h] +\begin{tabular}{rl} +frame ID : & \texttt{"TT2"} (title / song name / content description) \\ +encoding : & \texttt{0} (Latin-1) \\ +text : & \texttt{"some track name"} +\end{tabular} +\end{table} +\subsubsection{User-Defined Text Frame} \begin{figure}[h] \includegraphics{figures/id3v22/txx.pdf} \end{figure} +\par +\noindent This frame is for user-defined text information. -\VAR{Description} is a NULL-terminated string indicating +\VAR{description} is a NULL-terminated string indicating +what the frame is for. +\VAR{encoding} is 0 for Latin-1 encoding, 1 for UCS-2 encoding. + +\clearpage + +\subsubsection{URL Frames} +\begin{figure}[h] + \includegraphics{figures/id3v22/w__.pdf} +\end{figure} +\par +\noindent +Frames whose ID begins with `W' contain URL links to external resources. +\par +\begin{table}[h] + \begin{tabular}{|r|l|} + \hline + Frame ID & Meaning \\ + \hline + \texttt{WAF} & official audio file webpage \\ + \texttt{WAR} & official artist / performer webpage \\ + \texttt{WAS} & official audio source webpage \\ + \texttt{WCM} & commercial information \\ + \texttt{WCP} & copyright / legal information \\ + \texttt{WPB} & publishers official webpage \\ + \hline + \end{tabular} +\end{table} +\begin{figure}[h] + \includegraphics{figures/id3v22/wxx.pdf} +\end{figure} +\par +\noindent +This is a frame for user-defined links to external resources. +\VAR{description} is a NULL-terminated string indicating what the frame is for. -\VAR{Encoding} is 0 for Latin-1 encoding, 1 for UCS-2 encoding. +\VAR{encoding} is 0 for Latin-1 encoding, 1 for UCS-2 encoding. + +\clearpage \subsubsection{ULT Frame} \begin{figure}[h] \includegraphics{figures/id3v22/ult.pdf} \end{figure} +\par +\noindent This frame is for unsynchronized (non-karaoke) lyrics text, similar to a comment frame. -\VAR{Encoding} is 0 for Latin-1, 1 for UCS-2. -\VAR{Language} is a 3 byte ASCII string. -\VAR{Short Description} is a NULL-terminated string +\VAR{encoding} is 0 for Latin-1, 1 for UCS-2. +\VAR{language} is a 3 byte ASCII string. +\VAR{short description} is a NULL-terminated string containing a short description of the lyrics. Note that for UCS-2 lyrics, the NULL terminator is 2 bytes. The remainder of the frame is the lyrics text itself. \clearpage -\subsubsection{URL Frames} +\subsubsection{GEO Frame} \begin{figure}[h] -\includegraphics{figures/id3v22/w__.pdf} + \includegraphics{figures/id3v22/geo.pdf} \end{figure} -These frames are for URL links to external resources. \par -\begin{table}[h] -\begin{tabular}{|r|l|} -\hline -Frame ID & Meaning \\ -\hline -\texttt{WAF} & official audio file webpage \\ -\texttt{WAR} & official artist / performer webpage \\ -\texttt{WAS} & official audio source webpage \\ -\texttt{WCM} & commercial information \\ -\texttt{WCP} & copyright / legal information \\ -\texttt{WPB} & publishers official webpage \\ -\hline -\end{tabular} -\end{table} -\begin{figure}[h] -\includegraphics{figures/id3v22/wxx.pdf} -\end{figure} -This is a frame for user-defined links to external resources. -\VAR{Description} is a NULL-terminated string indicating -what the frame is for. -\VAR{Encoding} is 0 for Latin-1 encoding, 1 for UCS-2 encoding. +\noindent +This frame contains an embedded file (i.e. a ``general encapsulated object''). +\VAR{MIME Type} and \VAR{filename} are encoded as NULL-terminated +Latin-1 strings. +\VAR{description} is encoded as a Latin-1 or UCS-2 NULL-terminated +string, depending on if \VAR{encoding} is 0 or 1, respectively. +The remainder of the frame is the file's binary data. \clearpage -\subsection{ID3v2.3} -\subsubsection{ID3v2.3 Header} +\subsection{ID3v2.3 Header} \begin{figure}[h] \includegraphics{figures/id3v23/header.pdf} \end{figure} \par \noindent -The single Size field is split by NULL (0x00) bytes in order to make -it `sync-safe'. -This size field is the length of the entire tag, not including the header. +The \VAR{unsync}, \VAR{extended}, \VAR{experimental} and \VAR{footer} +flags are normally unused. + +\subsection{ID3v2.3 Frames} +\begin{figure}[h] + \includegraphics{figures/id3v23/frames.pdf} +\end{figure} +\par +\noindent +\VAR{frame ID} is a 4 byte ASCII string. +\VAR{frame size} is the length of the frame data, +not including its 10 byte header. +The \VAR{tag alter}, \VAR{file alter}, \VAR{read only}, +\VAR{compression}, \VAR{encryption} and \VAR{grouping} flags +are normally unused. + +\clearpage \subsubsection{APIC Frame} \begin{figure}[h] \includegraphics{figures/id3v23/apic.pdf} \end{figure} +\par +\noindent This frame contains an embedded image file. -\VAR{MIME Type} is a NULL-terminated MIME type such as ``image/jpeg'' +\VAR{MIME type} is a NULL-terminated MIME type such as ``image/jpeg'' or ``image/png''. -\VAR{Description} is a Latin-1 or UCS-2 encoded NULL-terminated string, -depending on if \VAR{Encoding} is 0 or 1, respectively. -\VAR{Picture Type} is one of the following: +\VAR{description} is a Latin-1 or UCS-2 encoded NULL-terminated string, +depending on if \VAR{encoding} is 0 or 1, respectively. +\VAR{picture Type} is one of the following: \begin{table}[h] {\relsize{-1} \begin{tabular}{|r|l||r|l|} @@ -392,110 +529,153 @@ \clearpage -\subsubsection{COMM Frame} -\begin{figure}[h] -\includegraphics{figures/id3v23/comm.pdf} -\end{figure} -This frame is a lengthy string of comment text. -\VAR{Encoding} is 0 for Latin-1, 1 for UCS-2. -\VAR{Language} is a 3 byte ASCII string. -\VAR{Description} is a NULL-terminated string -containing a short description of the comment. -Note that for UCS-2 comments, the NULL terminator is 2 bytes. -The remainder of the frame is the comment text itself. - -\subsubsection{GEOB Frame} +\subsubsection{APIC Frame Example} \begin{figure}[h] -\includegraphics{figures/id3v23/geob.pdf} + \includegraphics{figures/id3v23/apic-example.pdf} \end{figure} -This frame contains an embedded file. -\VAR{MIME Type} is a NULL-terminated Latin-1 string. -\VAR{Filename} and \VAR{Content Description} are NULL-terminated -Latin-1 or UCS-2 encoded strings, if \VAR{Encoding} is 0 or 1, -respectively. -\VAR{Encapsulated Object} is binary file data. +\begin{table}[h] +\begin{tabular}{rl} +text encoding : & \texttt{0} (Latin-1) \\ +MIME type : & \texttt{"image/png"} \\ +picture type : & \texttt{3} (front cover) \\ +description : & \texttt{"Description"} \\ +\end{tabular} +\end{table} \clearpage \subsubsection{Text Frames} \begin{figure}[h] -\includegraphics{figures/id3v23/t___.pdf} + \includegraphics{figures/id3v23/t___.pdf} \end{figure} -\VAR{Encoding} is 0 for Latin-1 encoding, 1 for UCS-2 encoding. +\par +\noindent +Frames beginning with `T' are text frames. +\VAR{encoding} is 0 for Latin-1 encoding, 1 for UCS-2 encoding. \begin{table}[h] -{\relsize{-2} -\begin{tabular}{|c|l||c|l|} -\hline -ID & Meaning & ID & Meaning \\ -\hline -\texttt{TALB} & album name & -\texttt{TBPM} & beats-per-minute \\ -\texttt{TCOM} & composer & -\texttt{TCON} & content type \\ -\texttt{TCOP} & copyright message & -\texttt{TDAT} & date \\ -\texttt{TDLY} & playlist delay & -\texttt{TENC} & encoded by \\ -\texttt{TEXT} & lyricist / text writer & -\texttt{TFLT} & file type \\ -\texttt{TIME} & time & -\texttt{TIT1} & content group description \\ -\texttt{TIT2} & title / songname / content description & -\texttt{TIT3} & subtitle / description refinement \\ -\texttt{TKEY} & initial key & -\texttt{TLAN} & language(s) \\ -\texttt{TLEN} & length & -\texttt{TMED} & media type \\ -\texttt{TOAL} & original album / movie / show title & -\texttt{TOFN} & original filename \\ -\texttt{TOLY} & original lyricist(s) / text writer(s) & -\texttt{TOPE} & original artist(s) / performer(s) \\ -\texttt{TORY} & original release year & -\texttt{TOWN} & file owner / licensee \\ -\texttt{TPE1} & lead performer(s) / soloist(s) & -\texttt{TPE2} & band / orchestra / accompaniment \\ -\texttt{TPE3} & conductor / performer refinement & -\texttt{TPE4} & interpreted, remixed, or otherwise modified by \\ -\texttt{TPOS} & part of a set & -\texttt{TPUB} & publisher \\ -\texttt{TRCK} & track number / position in set & -\texttt{TRDA} & recording dates \\ -\texttt{TRSN} & internet radio station name & -\texttt{TRSO} & internet radio station owner \\ -\texttt{TSIZ} & size & -\texttt{TSRC} & ISRC (International Standard Recording Code) \\ -\texttt{TSSE} & software / hardware and encoding settings & -\texttt{TYER} & year \\ -\hline -\end{tabular} -} + {\relsize{-2} + \begin{tabular}{|c|l||c|l|} + \hline + ID & Meaning & ID & Meaning \\ + \hline + \texttt{TALB} & album name & + \texttt{TBPM} & beats-per-minute \\ + \texttt{TCOM} & composer & + \texttt{TCON} & content type \\ + \texttt{TCOP} & copyright message & + \texttt{TDAT} & date \\ + \texttt{TDLY} & playlist delay & + \texttt{TENC} & encoded by \\ + \texttt{TEXT} & lyricist / text writer & + \texttt{TFLT} & file type \\ + \texttt{TIME} & time & + \texttt{TIT1} & content group description \\ + \texttt{TIT2} & title / songname / content description & + \texttt{TIT3} & subtitle / description refinement \\ + \texttt{TKEY} & initial key & + \texttt{TLAN} & language(s) \\ + \texttt{TLEN} & length & + \texttt{TMED} & media type \\ + \texttt{TOAL} & original album / movie / show title & + \texttt{TOFN} & original filename \\ + \texttt{TOLY} & original lyricist(s) / text writer(s) & + \texttt{TOPE} & original artist(s) / performer(s) \\ + \texttt{TORY} & original release year & + \texttt{TOWN} & file owner / licensee \\ + \texttt{TPE1} & lead performer(s) / soloist(s) & + \texttt{TPE2} & band / orchestra / accompaniment \\ + \texttt{TPE3} & conductor / performer refinement & + \texttt{TPE4} & interpreted, remixed, or otherwise modified by \\ + \texttt{TPOS} & part of a set & + \texttt{TPUB} & publisher \\ + \texttt{TRCK} & track number / position in set & + \texttt{TRDA} & recording dates \\ + \texttt{TRSN} & internet radio station name & + \texttt{TRSO} & internet radio station owner \\ + \texttt{TSIZ} & size & + \texttt{TSRC} & ISRC (International Standard Recording Code) \\ + \texttt{TSSE} & software / hardware and encoding settings & + \texttt{TYER} & year \\ + \hline + \end{tabular} + } \end{table} +\subsubsection{Text Frame Example} \begin{figure}[h] -\includegraphics{figures/id3v23/txxx.pdf} + \includegraphics{figures/id3v23/t___-example.pdf} \end{figure} +\par +\noindent +\begin{tabular}{rl} +frame type : & \texttt{"TIT2"} (title / song name / content description) \\ +encoding : & \texttt{0} (Latin-1) \\ +text : & \texttt{"Track Name"} \\ +\end{tabular} + +\clearpage + +\subsubsection{User-Defined Text Frame} + +\begin{figure}[h] + \includegraphics{figures/id3v23/txxx.pdf} +\end{figure} +\par +\noindent This frame is for user-defined text information. -\VAR{Description} is a NULL-terminated string indicating +\VAR{description} is a NULL-terminated string indicating what the frame is for. -\VAR{Encoding} is 0 for Latin-1 encoding, 1 for UCS-2 encoding. +\VAR{encoding} is 0 for Latin-1 encoding, 1 for UCS-2 encoding. + +\subsubsection{GEOB Frame} +\begin{figure}[h] +\includegraphics{figures/id3v23/geob.pdf} +\end{figure} +This frame contains an embedded file (i.e. a ``general encapsulated object''). +\VAR{MIME type} is a NULL-terminated Latin-1 string. +\VAR{filename} and \VAR{content description} are NULL-terminated +Latin-1 or UCS-2 encoded strings, if \VAR{encoding} is 0 or 1, +respectively. +\VAR{encapsulated object} is binary file data. \clearpage -\subsubsection{USLT Frame} -This frame is for unsynchronized (non-karaoke) lyrics text, -similar to a comment frame. -\VAR{Encoding} is 0 for Latin-1, 1 for UCS-2. -\VAR{Language} is a 3 byte ASCII string. -\VAR{Description} is a NULL-terminated string -containing a short description of the lyrics. +\subsubsection{COMM Frame} +\begin{figure}[h] +\includegraphics{figures/id3v23/comm.pdf} +\end{figure} +\par +\noindent +This frame supports a lengthy string of comment text. +\VAR{encoding} is 0 for Latin-1, 1 for UCS-2. +\VAR{language} is a 3 byte ASCII string. +\VAR{description} is a NULL-terminated string +containing a short description of the comment. Note that for UCS-2 comments, the NULL terminator is 2 bytes. -The remainder of the frame is the lyrics text itself. +The remainder of the frame is the comment text itself. + +\subsubsection{COMM Frame Example} +\begin{figure}[h] + \includegraphics{figures/id3v23/comm-example.pdf} +\end{figure} +\begin{table}[h] +\begin{tabular}{rl} +encoding : & \texttt{0} (Latin-1) \\ +language : & \texttt{"eng"} \\ +description : & \texttt{""} (empty string) \\ +comment text : & \texttt{"Comment Text"} \\ +\end{tabular} +\end{table} + +\clearpage \subsubsection{URL Frames} \begin{figure}[h] \includegraphics{figures/id3v23/w___.pdf} \end{figure} -These frames are for URL links to external resources. +\par +\noindent +Frames beginning with `W' are URL links to external resources. \par \begin{table}[h] \begin{tabular}{|c|l|} @@ -513,39 +693,72 @@ \hline \end{tabular} \end{table} + +\subsubsection{User-Defined URL Frame} + \begin{figure}[h] \includegraphics{figures/id3v23/wxxx.pdf} \end{figure} +\par +\noindent This is a frame for user-defined links to external resources. -\VAR{Description} is a NULL-terminated string indicating +\VAR{description} is a NULL-terminated string indicating what the frame is for. -\VAR{Encoding} is 0 for Latin-1 encoding, 1 for UCS-2 encoding. +\VAR{encoding} is 0 for Latin-1 encoding, 1 for UCS-2 encoding. + +\clearpage + +\subsubsection{USLT Frame} +\begin{figure}[h] + \includegraphics{figures/id3v23/uslt.pdf} +\end{figure} +\par +\noindent +This frame is for unsynchronized (non-karaoke) lyrics text, +similar to a comment frame. +\VAR{encoding} is 0 for Latin-1, 1 for UCS-2. +\VAR{language} is a 3 byte ASCII string. +\VAR{description} is a NULL-terminated string +containing a short description of the lyrics. +Note that for UCS-2 comments, the NULL terminator is 2 bytes. +The remainder of the frame is the lyrics text itself. \clearpage -\subsection{ID3v2.4} -\subsubsection{ID3v2.4 Header} +\subsection{ID3v2.4 Header} \begin{figure}[h] \includegraphics{figures/id3v24/header.pdf} \end{figure} +\subsection{ID3v2.4 Frames} +\begin{figure}[h] + \includegraphics{figures/id3v24/frames.pdf} +\end{figure} +\par +\noindent +The ID3v2.4 frame size field also also ``sync safe'', like the ID3v2 headers. +It also contains \VAR{tag alter}, \VAR{file alter}, \VAR{read only}, +\VAR{grouping}, \VAR{compression}, \VAR{encryption}, \VAR{unsync} +and \VAR{data length} flags which are typically usused. + +\clearpage + \subsubsection{APIC Frame} \begin{figure}[h] \includegraphics{figures/id3v24/apic.pdf} \end{figure} +\par +\noindent This frame contains an embedded image file. -\VAR{MIME Type} is a NULL-terminated MIME type such as ``image/jpeg'' +\VAR{MIME type} is a NULL-terminated MIME type such as ``image/jpeg'' or ``image/png''. -\VAR{Description} is a NULL-terminated string -in one of the following encodings: -\begin{inparaenum} -\item[\itshape 0\upshape)] Latin-1 -\item[\itshape 1\upshape)] UTF-16 -\item[\itshape 2\upshape)] UTF-16BE -\item[\itshape 3\upshape)] UTF-8 -\end{inparaenum} -, depending on \VAR{Encoding}. -\VAR{Picture Type} is one of the following: +\VAR{encoding} determines the encoding of the NULL-terminated +\VAR{description} field. +Valid encodings are \texttt{0} for Latin-1, +\texttt{1} for UTF-16, +\texttt{2} for UTF-16BE +and \texttt{3} for UTF-8. +\VAR{picture type} is one of the following: \begin{table}[h] {\relsize{-1} \begin{tabular}{|r|l||r|l|} @@ -571,135 +784,155 @@ \clearpage -\subsubsection{COMM Frame} +\subsubsection{APIC Frame Example} \begin{figure}[h] -\includegraphics{figures/id3v24/comm.pdf} +\includegraphics{figures/id3v24/apic-example.pdf} \end{figure} -This frame is a lengthy string of comment text. -\VAR{Language} is a 3 byte ASCII string. -\VAR{Description} is a NULL-terminated string -in one of the following encodings: -\begin{inparaenum} -\item[\itshape 0\upshape)] Latin-1 -\item[\itshape 1\upshape)] UTF-16 -\item[\itshape 2\upshape)] UTF-16BE -\item[\itshape 3\upshape)] UTF-8 -\end{inparaenum} -, depending on \VAR{Encoding}. -The remainder of the frame is the comment text itself, -in the same encoding as \VAR{Description}. - -\subsubsection{GEOB Frame} +\begin{table}[h] +\begin{tabular}{rl} +encoding : & \texttt{0} (Latin-1) \\ +MIME type : & \texttt{"image/png"} \\ +picture type : & \texttt{3} (front cover) \\ +description : & \texttt{"Description"} \\ +\end{tabular} +\end{table} +\par +\noindent +As with all ID3v2.4 frames, the \VAR{frame size} field contains +embedded 0 bits as follows: \begin{figure}[h] -\includegraphics{figures/id3v24/geob.pdf} +\includegraphics{figures/id3v24/size.pdf} \end{figure} -This frame contains an embedded file. -\VAR{MIME Type} is a NULL-terminated Latin-1 string. -\VAR{Filename} and \VAR{Content Description} are NULL-terminated -strings in one of the following encodings: -\begin{inparaenum} -\item[\itshape 0\upshape)] Latin-1 -\item[\itshape 1\upshape)] UTF-16 -\item[\itshape 2\upshape)] UTF-16BE -\item[\itshape 3\upshape)] UTF-8 -\end{inparaenum} -, depending on \VAR{Encoding}. -\VAR{Encapsulated Object} is binary file data. +\par +\noindent +which indicates the frame's data size is: +\begin{equation*} +(0 \times 2 ^ {21}) + (0 \times 2 ^ {14}) + (1 \times 2 ^ {7}) + 55 = 183 \text{ bytes} +\end{equation*} \clearpage \subsubsection{Text Frames} \begin{figure}[h] -\includegraphics{figures/id3v24/t___.pdf} + \includegraphics{figures/id3v24/t___.pdf} \end{figure} -\VAR{Encoding} is 0 for Latin-1 encoding, 1 for UTF-16 encoding, +\par +\noindent +Frames beginning with `T' are text frames. +\VAR{encoding} is 0 for Latin-1 encoding, 1 for UTF-16 encoding, 2 for UTF-16BE encoding and 3 for UTF-8 encoding. \begin{table}[h] -{\relsize{-2} -\begin{tabular}{|c|l||c|l|} -\hline -ID & Meaning & ID & Meaning \\ -\hline -\texttt{TALB} & album name & -\texttt{TBPM} & beats-per-minute \\ -\texttt{TCOM} & composer & -\texttt{TCON} & content type \\ -\texttt{TCOP} & copyright message & -\texttt{TDAT} & date \\ -\texttt{TDEN} & encoding time & -\texttt{TDLY} & playlist delay \\ -\texttt{TDOR} & original release time & -\texttt{TDRC} & recording time \\ -\texttt{TDRL} & release time & -\texttt{TDTG} & tagging time \\ -\texttt{TENC} & encoded by & -\texttt{TEXT} & lyricist / text writer \\ -\texttt{TFLT} & file type & -\texttt{TIPL} & involved people list \\ -\texttt{TIT1} & content group description & -\texttt{TIT2} & title / songname / content description \\ -\texttt{TIT3} & subtitle / description refinement & -\texttt{TKEY} & initial key \\ -\texttt{TLAN} & language(s) & -\texttt{TLEN} & length \\ -\texttt{TMCL} & musician credits list & -\texttt{TMED} & media type \\ -\texttt{TMOO} & mood & -\texttt{TOAL} & original album / movie / show title \\ -\texttt{TOFN} & original filename & -\texttt{TOLY} & original lyricist(s) / text writer(s) \\ -\texttt{TOPE} & original artist(s) / performer(s) & -\texttt{TOWN} & file owner / licensee \\ -\texttt{TPE1} & lead performer(s) / soloist(s) & -\texttt{TPE2} & band / orchestra / accompaniment \\ -\texttt{TPE3} & conductor / performer refinement & -\texttt{TPE4} & interpreted, remixed, or otherwise modified by \\ -\texttt{TPOS} & part of a set & -\texttt{TPRO} & produced notice \\ -\texttt{TPUB} & publisher & -\texttt{TRCK} & track number / position in set \\ -\texttt{TRSN} & internet radio station name & -\texttt{TRSO} & internet radio station owner \\ -\texttt{TSOA} & album sort order & -\texttt{TSOP} & performer sort order \\ -\texttt{TSOT} & title sort order & -\texttt{TSRC} & ISRC (International Standard Recording Code) \\ -\texttt{TSSE} & software / hardware and encoding settings & -\texttt{TSST} & set subtitle \\ -\hline -\end{tabular} -} + {\relsize{-2} + \begin{tabular}{|c|l||c|l|} + \hline + ID & Meaning & ID & Meaning \\ + \hline + \texttt{TALB} & album name & + \texttt{TBPM} & beats-per-minute \\ + \texttt{TCOM} & composer & + \texttt{TCON} & content type \\ + \texttt{TCOP} & copyright message & + \texttt{TDAT} & date \\ + \texttt{TDEN} & encoding time & + \texttt{TDLY} & playlist delay \\ + \texttt{TDOR} & original release time & + \texttt{TDRC} & recording time \\ + \texttt{TDRL} & release time & + \texttt{TDTG} & tagging time \\ + \texttt{TENC} & encoded by & + \texttt{TEXT} & lyricist / text writer \\ + \texttt{TFLT} & file type & + \texttt{TIPL} & involved people list \\ + \texttt{TIT1} & content group description & + \texttt{TIT2} & title / songname / content description \\ + \texttt{TIT3} & subtitle / description refinement & + \texttt{TKEY} & initial key \\ + \texttt{TLAN} & language(s) & + \texttt{TLEN} & length \\ + \texttt{TMCL} & musician credits list & + \texttt{TMED} & media type \\ + \texttt{TMOO} & mood & + \texttt{TOAL} & original album / movie / show title \\ + \texttt{TOFN} & original filename & + \texttt{TOLY} & original lyricist(s) / text writer(s) \\ + \texttt{TOPE} & original artist(s) / performer(s) & + \texttt{TOWN} & file owner / licensee \\ + \texttt{TPE1} & lead performer(s) / soloist(s) & + \texttt{TPE2} & band / orchestra / accompaniment \\ + \texttt{TPE3} & conductor / performer refinement & + \texttt{TPE4} & interpreted, remixed, or otherwise modified by \\ + \texttt{TPOS} & part of a set & + \texttt{TPRO} & produced notice \\ + \texttt{TPUB} & publisher & + \texttt{TRCK} & track number / position in set \\ + \texttt{TRSN} & internet radio station name & + \texttt{TRSO} & internet radio station owner \\ + \texttt{TSOA} & album sort order & + \texttt{TSOP} & performer sort order \\ + \texttt{TSOT} & title sort order & + \texttt{TSRC} & ISRC (International Standard Recording Code) \\ + \texttt{TSSE} & software / hardware and encoding settings & + \texttt{TSST} & set subtitle \\ + \hline + \end{tabular} + } \end{table} \clearpage +\subsubsection{Text Frame Example} \begin{figure}[h] -\includegraphics{figures/id3v24/txxx.pdf} + \includegraphics{figures/id3v24/t___-example.pdf} \end{figure} +\begin{table}[h] +\begin{tabular}{rl} +frame ID : & \texttt{"TIT2"} (title / song name / content description) \\ +encoding : & \texttt{0} (Latin-1) \\ +text: & \texttt{"Track Name"} \\ +\end{tabular} +\end{table} + +\subsubsection{User-Defined Text Frame} +\begin{figure}[h] + \includegraphics{figures/id3v24/txxx.pdf} +\end{figure} +\par +\noindent This frame is for user-defined text information. -\VAR{Description} is a NULL-terminated string indicating -what the frame is for. -\VAR{Encoding} is 0 for Latin-1 encoding, 1 for UTF-16 encoding, +\VAR{encoding} is 0 for Latin-1 encoding, 1 for UTF-16 encoding, 2 for UTF-16BE encoding and 3 for UTF-8 encoding. +\VAR{description} is a NULL-terminated string indicating +what the frame is for. -\subsubsection{USLT Frame} +\clearpage + +\subsubsection{COMM Frame} \begin{figure}[h] -\includegraphics{figures/id3v24/uslt.pdf} +\includegraphics{figures/id3v24/comm.pdf} \end{figure} -This frame is for unsynchronized (non-karaoke) lyrics text, -similar to a comment frame. -\VAR{Language} is a 3 byte ASCII string. -\VAR{Description} is a NULL-terminated string -in one of the following encodings: -\begin{inparaenum} -\item[\itshape 0\upshape)] Latin-1 -\item[\itshape 1\upshape)] UTF-16 -\item[\itshape 2\upshape)] UTF-16BE -\item[\itshape 3\upshape)] UTF-8 -\end{inparaenum} -, depending on \VAR{Encoding}. -The remainder of the frame is the lyrics text itself, -in the same encoding as \VAR{Description}. +\par +\noindent +This frame supports a lengthy string of comment text. +\VAR{encoding} determines the encoding of the NULL-terminated +\VAR{description} field and \VAR{comment text} field. +Valid encodings are \texttt{0} for Latin-1, +\texttt{1} for UTF-16, +\texttt{2} for UTF-16BE +and \texttt{3} for UTF-8. +\VAR{language} is a 3 byte ASCII string. + +\subsubsection{COMM Frame Example} +\begin{figure}[h] +\includegraphics{figures/id3v24/comm-example.pdf} +\end{figure} +\begin{table}[h] +\begin{tabular}{rl} +encoding : & \texttt{0} (Latin-1) \\ +language : & \texttt{"eng"} \\ +description : & \texttt{""} (empty string) \\ +comment text : & \texttt{"Comment Text"} \\ +\end{tabular} +\end{table} \clearpage @@ -707,7 +940,9 @@ \begin{figure}[h] \includegraphics{figures/id3v24/w___.pdf} \end{figure} -These frames are for URL links to external resources. +\par +\noindent +Frames with IDs beginning with `W' are URL links to external resources. \par \begin{table}[h] \begin{tabular}{|c|l|} @@ -726,11 +961,47 @@ \end{tabular} \end{table} +\clearpage + +\subsubsection{User-Defined URL Frame} + \begin{figure}[h] \includegraphics{figures/id3v24/wxxx.pdf} \end{figure} +\par +\noindent This is a frame for user-defined links to external resources. -\VAR{Description} is a NULL-terminated string indicating -what the frame is for. -\VAR{Encoding} is 0 for Latin-1 encoding, 1 for UTF-16 encoding, +\VAR{encoding} is 0 for Latin-1 encoding, 1 for UTF-16 encoding, 2 for UTF-16BE encoding and 3 for UTF-8 encoding. +\VAR{description} is a NULL-terminated string indicating +what the frame is for. + +\clearpage + +\subsubsection{GEOB Frame} +\begin{figure}[h] + \includegraphics{figures/id3v24/geob.pdf} +\end{figure} +\par +\noindent +This frame contains an embedded file (i.e. a ``general encapsulated object''). +\VAR{MIME type} is a NULL-terminated Latin-1 string. +\VAR{filename} and \VAR{content description} are NULL-terminated +strings encoded as Latin-1, UTF-16, UTF-16BE or UTF-8, +depending on the \VAR{encoding field}. +\VAR{encapsulated object} is binary file data. + +\clearpage + +\subsubsection{USLT Frame} +\begin{figure}[h] + \includegraphics{figures/id3v24/uslt.pdf} +\end{figure} +\par +\noindent +This frame is for unsynchronized (non-karaoke) lyrics text, +similar to a comment frame. +\VAR{language} is a 3 byte ASCII string. +\VAR{description} and \VAR{lyrics text} are a NULL-terminated strings +encoded as Latin-1, UTF-16, UTF-16BE or UTF-8, +depending on the \VAR{encoding field}.
View file
audiotools-2.18.tar.gz/docs/reference/musicbrainz.tex -> audiotools-2.19.tar.gz/docs/reference/musicbrainz.tex
Changed
@@ -27,7 +27,7 @@ and receives information such as album name, artist name, track names and so forth as an XML file. -\pagebreak +\clearpage \subsection{the Disc ID} Calculating a MusicBrainz disc ID requires knowing a CD's first track number, @@ -86,7 +86,7 @@ So to complete our example, the hash value becomes a disc ID of \texttt{2j2TBGJ3PdV7vkO1Na1qRXE48Hk-} -\pagebreak +\clearpage \subsection{Server Query} MusicBrainz runs as a service on HTTP port 80.
View file
audiotools-2.19.tar.gz/docs/reference/ogg-codec.template
Added
@@ -0,0 +1,3 @@ +<<file:header.tex>> +\include{ogg} +<<file:footer.tex>>
View file
audiotools-2.19.tar.gz/docs/reference/ogg.tex
Added
@@ -0,0 +1,195 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\chapter{Ogg} +\label{ogg} +Ogg is a general-purpose container for storing audio and/or video streams. +It is used by Ogg Vorbis, Ogg FLAC, Ogg Speex and others +to store a stream of packets. +It consists of a stream of Ogg pages where each page stores +1 or more segments. +Those segments are combined into packets based on their length. +All of its fields are little-endian. +\begin{figure}[h] + \includegraphics{figures/ogg/packets.pdf} + \caption{Ogg stream overview} +\end{figure} + +\clearpage + +\section{Ogg Decoding} +{\relsize{-1} +\ALGORITHM{a file stream}{0 or more Ogg packets} +\SetKwData{CRC}{CRC} +\SetKwData{MAGIC}{magic number} +\SetKwData{VERSION}{version} +\SetKwData{CONT}{continuation} +\SetKwData{BOS}{beginning of stream} +\SetKwData{EOS}{end of stream} +\SetKwData{GRANULEPOS}{granule position} +\SetKwData{SERIALNUM}{bitstream serial number} +\SetKwData{SEQUENCENUM}{page sequence number} +\SetKwData{SEGCOUNT}{page segment count} +\SetKwData{SEGLENGTH}{page segment length} +\SetKwData{SEGMENT}{page segment} +\SetKwData{PACKET}{Ogg packet} +$p \leftarrow 0$\; +$\text{\PACKET}_p \leftarrow$ empty string of data\; +\Repeat{$\EOS = 1$}{ + \hyperref[ogg:checksum]{begin CRC calculation with initial checksum of 0}\; + $\MAGIC \leftarrow$ \READ 4 bytes\; + \ASSERT $\MAGIC = \texttt{"OggS"}$\; + $\VERSION \leftarrow$ \READ 8 unsigned bits\; + \ASSERT $\VERSION = 0$\; + $\CONT \leftarrow$ \READ 1 unsigned bit\; + $\BOS \leftarrow$ \READ 1 unsigned bit\; + $\EOS \leftarrow$ \READ 1 unsigned bit\; + \SKIP 5 bits\; + $\GRANULEPOS \leftarrow$ \READ 64 signed bits\; + $\SERIALNUM \leftarrow$ \READ 32 unsigned bits\; + $\SEQUENCENUM \leftarrow$ \READ 32 unsigned bits\; + suspend CRC calculation\; + $\CRC \leftarrow$ \READ 32 unsigned bits\; + resume CRC calculation, feeding checksum 4 bytes with 0 values\; + $\SEGCOUNT \leftarrow$ \READ 8 unsigned bits\; + \BlankLine + \For{$i \leftarrow 0$ \emph{\KwTo}\SEGCOUNT}{ + $\text{\SEGLENGTH}_i \leftarrow$ \READ 8 unsigned bits\; + } + \For{$i \leftarrow 0$ \emph{\KwTo}\SEGCOUNT}{ + $\text{\SEGMENT}_i \leftarrow $ \READ $(\text{\SEGLENGTH}_i)$ bytes\; + append $\text{\SEGMENT}_i$ to $\text{\PACKET}_p$\; + \If{$\text{\SEGLENGTH}_i < 255$}{ + \Return $\text{\PACKET}_p$\; + $p \leftarrow p + 1$\; + $\text{\PACKET}_p \leftarrow$ empty string of data\; + } + } + \ASSERT $\CRC = \text{page's calculated checksum}$\; +} +\EALGORITHM +} +\begin{table}[h] +{\relsize{-1} +\begin{tabular}{rl} +\textsf{magic number} & always \texttt{OggS} \\ +\textsf{version} & always 0 \\ +\textsf{continuation} & 1 if page's Ogg packet continues from previous page, 0 otherwise \\ +\textsf{beginning of stream} & 1 if page is first in stream, 0 otherwise \\ +\textsf{end of stream} & 1 if page is last in stream, 0 otherwise \\ +\textsf{granule position} & codec-specific position value in stream \\ +\textsf{bitstream serial number} & stream's unique identifier, in case an Ogg file contains more than one \\ +\textsf{page sequence number} & page number in stream, starting from 0 \\ +\textsf{CRC} & checksum of all page data \\ +\textsf{page segment count} & number of segments \\ +\textsf{page segment length} & length of each segment \\ +\textsf{page segment} & raw segment data \\ +\end{tabular} +} +\end{table} +\clearpage + +\begin{figure}[h] + \includegraphics{figures/ogg/stream.pdf} +\end{figure} + +\clearpage + +\section{Checksum Calculation} +\label{ogg:checksum} +Given an Ogg page byte and a previous CRC checksum, +or 0 as an initial value: +\begin{equation*} + \text{CRC}_i = (\text{CRC}_{i - 1} \times 2 ^ 8) \xor \texttt{CRC32}((\lfloor\text{CRC}_{i - 1} \div 2 ^ {24}\rfloor \bmod 256) \xor byte) +\end{equation*} +where $\text{CRC}_i$ is truncated to 32 bits. +\begin{table}[h] +{\relsize{-3} + \ttfamily + \begin{tabular}{|r||r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} + \hline + & 0x?0 & 0x?1 & 0x?2 & 0x?3 & 0x?4 & 0x?5 & 0x?6 & 0x?7 \\ + \hline + 0x0? & 0x00000000 & 0x04C11DB7 & 0x09823B6E & 0x0D4326D9 & + 0x130476DC & 0x17C56B6B & 0x1A864DB2 & 0x1E475005 \\ + 0x1? & 0x4C11DB70 & 0x48D0C6C7 & 0x4593E01E & 0x4152FDA9 & + 0x5F15ADAC & 0x5BD4B01B & 0x569796C2 & 0x52568B75 \\ + 0x2? & 0x9823B6E0 & 0x9CE2AB57 & 0x91A18D8E & 0x95609039 & + 0x8B27C03C & 0x8FE6DD8B & 0x82A5FB52 & 0x8664E6E5 \\ + 0x3? & 0xD4326D90 & 0xD0F37027 & 0xDDB056FE & 0xD9714B49 & + 0xC7361B4C & 0xC3F706FB & 0xCEB42022 & 0xCA753D95 \\ + 0x4? & 0x34867077 & 0x30476DC0 & 0x3D044B19 & 0x39C556AE & + 0x278206AB & 0x23431B1C & 0x2E003DC5 & 0x2AC12072 \\ + 0x5? & 0x7897AB07 & 0x7C56B6B0 & 0x71159069 & 0x75D48DDE & + 0x6B93DDDB & 0x6F52C06C & 0x6211E6B5 & 0x66D0FB02 \\ + 0x6? & 0xACA5C697 & 0xA864DB20 & 0xA527FDF9 & 0xA1E6E04E & + 0xBFA1B04B & 0xBB60ADFC & 0xB6238B25 & 0xB2E29692 \\ + 0x7? & 0xE0B41DE7 & 0xE4750050 & 0xE9362689 & 0xEDF73B3E & + 0xF3B06B3B & 0xF771768C & 0xFA325055 & 0xFEF34DE2 \\ + 0x8? & 0x690CE0EE & 0x6DCDFD59 & 0x608EDB80 & 0x644FC637 & + 0x7A089632 & 0x7EC98B85 & 0x738AAD5C & 0x774BB0EB \\ + 0x9? & 0x251D3B9E & 0x21DC2629 & 0x2C9F00F0 & 0x285E1D47 & + 0x36194D42 & 0x32D850F5 & 0x3F9B762C & 0x3B5A6B9B \\ + 0xA? & 0xF12F560E & 0xF5EE4BB9 & 0xF8AD6D60 & 0xFC6C70D7 & + 0xE22B20D2 & 0xE6EA3D65 & 0xEBA91BBC & 0xEF68060B \\ + 0xB? & 0xBD3E8D7E & 0xB9FF90C9 & 0xB4BCB610 & 0xB07DABA7 & + 0xAE3AFBA2 & 0xAAFBE615 & 0xA7B8C0CC & 0xA379DD7B \\ + 0xC? & 0x5D8A9099 & 0x594B8D2E & 0x5408ABF7 & 0x50C9B640 & + 0x4E8EE645 & 0x4A4FFBF2 & 0x470CDD2B & 0x43CDC09C \\ + 0xD? & 0x119B4BE9 & 0x155A565E & 0x18197087 & 0x1CD86D30 & + 0x029F3D35 & 0x065E2082 & 0x0B1D065B & 0x0FDC1BEC \\ + 0xE? & 0xC5A92679 & 0xC1683BCE & 0xCC2B1D17 & 0xC8EA00A0 & + 0xD6AD50A5 & 0xD26C4D12 & 0xDF2F6BCB & 0xDBEE767C \\ + 0xF? & 0x89B8FD09 & 0x8D79E0BE & 0x803AC667 & 0x84FBDBD0 & + 0x9ABC8BD5 & 0x9E7D9662 & 0x933EB0BB & 0x97FFAD0C \\ + \hline + \hline + & 0x?8 & 0x?9 & 0x?A & 0x?B & 0x?C & 0x?D & 0x?E & 0x?F \\ + \hline + 0x0? & 0x2608EDB8 & 0x22C9F00F & 0x2F8AD6D6 & 0x2B4BCB61 & + 0x350C9B64 & 0x31CD86D3 & 0x3C8EA00A & 0x384FBDBD \\ + 0x1? & 0x6A1936C8 & 0x6ED82B7F & 0x639B0DA6 & 0x675A1011 & + 0x791D4014 & 0x7DDC5DA3 & 0x709F7B7A & 0x745E66CD \\ + 0x2? & 0xBE2B5B58 & 0xBAEA46EF & 0xB7A96036 & 0xB3687D81 & + 0xAD2F2D84 & 0xA9EE3033 & 0xA4AD16EA & 0xA06C0B5D \\ + 0x3? & 0xF23A8028 & 0xF6FB9D9F & 0xFBB8BB46 & 0xFF79A6F1 & + 0xE13EF6F4 & 0xE5FFEB43 & 0xE8BCCD9A & 0xEC7DD02D \\ + 0x4? & 0x128E9DCF & 0x164F8078 & 0x1B0CA6A1 & 0x1FCDBB16 & + 0x018AEB13 & 0x054BF6A4 & 0x0808D07D & 0x0CC9CDCA \\ + 0x5? & 0x5E9F46BF & 0x5A5E5B08 & 0x571D7DD1 & 0x53DC6066 & + 0x4D9B3063 & 0x495A2DD4 & 0x44190B0D & 0x40D816BA \\ + 0x6? & 0x8AAD2B2F & 0x8E6C3698 & 0x832F1041 & 0x87EE0DF6 & + 0x99A95DF3 & 0x9D684044 & 0x902B669D & 0x94EA7B2A \\ + 0x7? & 0xC6BCF05F & 0xC27DEDE8 & 0xCF3ECB31 & 0xCBFFD686 & + 0xD5B88683 & 0xD1799B34 & 0xDC3ABDED & 0xD8FBA05A \\ + 0x8? & 0x4F040D56 & 0x4BC510E1 & 0x46863638 & 0x42472B8F & + 0x5C007B8A & 0x58C1663D & 0x558240E4 & 0x51435D53 \\ + 0x9? & 0x0315D626 & 0x07D4CB91 & 0x0A97ED48 & 0x0E56F0FF & + 0x1011A0FA & 0x14D0BD4D & 0x19939B94 & 0x1D528623 \\ + 0xA? & 0xD727BBB6 & 0xD3E6A601 & 0xDEA580D8 & 0xDA649D6F & + 0xC423CD6A & 0xC0E2D0DD & 0xCDA1F604 & 0xC960EBB3 \\ + 0xB? & 0x9B3660C6 & 0x9FF77D71 & 0x92B45BA8 & 0x9675461F & + 0x8832161A & 0x8CF30BAD & 0x81B02D74 & 0x857130C3 \\ + 0xC? & 0x7B827D21 & 0x7F436096 & 0x7200464F & 0x76C15BF8 & + 0x68860BFD & 0x6C47164A & 0x61043093 & 0x65C52D24 \\ + 0xD? & 0x3793A651 & 0x3352BBE6 & 0x3E119D3F & 0x3AD08088 & + 0x2497D08D & 0x2056CD3A & 0x2D15EBE3 & 0x29D4F654 \\ + 0xE? & 0xE3A1CBC1 & 0xE760D676 & 0xEA23F0AF & 0xEEE2ED18 & + 0xF0A5BD1D & 0xF464A0AA & 0xF9278673 & 0xFDE69BC4 \\ + 0xF? & 0xAFB010B1 & 0xAB710D06 & 0xA6322BDF & 0xA2F33668 & + 0xBCB4666D & 0xB8757BDA & 0xB5365D03 & 0xB1F740B4 \\ + \hline + \end{tabular} +} +\caption{\texttt{CRC32} lookup} +\end{table} +\par +\noindent +The CRC is calculated over all the bytes in an Ogg page, +not counting the 32-bit CRC field itself. +That field is treated as having a value of 0 during calculation.
View file
audiotools-2.18.tar.gz/docs/reference/replaygain.tex -> audiotools-2.19.tar.gz/docs/reference/replaygain.tex
Changed
@@ -67,6 +67,8 @@ should generate a multiplier higher than 1.024 ($0.9765625 \times 1.024 = 1.0$). +\clearpage + \section{Calculating ReplayGain} As explained earlier, ReplayGain requires a peak and gain value @@ -117,7 +119,7 @@ When filtering at the start of the stream, treat any samples before the beginning as 0. -\pagebreak +\clearpage \subsubsection{a filtering example} Let's assume we have a 44100Hz stream and our previous input and output @@ -230,7 +232,7 @@ Therefore, $\text{Butter}_{100} = -0.24 - -0.71 = 0.47$ , which is the next sample from the equal loudness filter. -\pagebreak +\clearpage \subsection{RMS Energy Blocks} The next step is to take our stream of filtered samples and convert
View file
audiotools-2.19.tar.gz/docs/reference/shorten
Added
+(directory)
View file
audiotools-2.19.tar.gz/docs/reference/shorten-codec.template
Added
@@ -0,0 +1,3 @@ +<<file:header.tex>> +\include{shorten} +<<file:footer.tex>>
View file
audiotools-2.18.tar.gz/docs/reference/shorten.tex -> audiotools-2.19.tar.gz/docs/reference/shorten.tex
Changed
@@ -1,6 +1,14 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + \chapter{Shorten} \begin{figure}[h] -\includegraphics{figures/shorten/stream.pdf} +\includegraphics{shorten/figures/stream.pdf} \end{figure} A Shorten file is almost entirely comprised of three variable-length data types which we'll call \texttt{unsigned}, \texttt{signed} and @@ -21,7 +29,7 @@ \SetKwData{UNSIGNED}{unsigned} $\text{\UNSIGNED} \leftarrow$ read \texttt{unsigned}($c + 1$)\; \eIf(\tcc*[f]{positive}){$\UNSIGNED \bmod 2 = 0$}{ - \Return $\lfloor\UNSIGNED \div 2\rfloor$\; + \Return $\UNSIGNED \div 2$\; }(\tcc*[f]{negative}){ \Return $-\lfloor\UNSIGNED \div 2\rfloor - 1$\; } @@ -36,1013 +44,8 @@ \clearpage -\section{Shorten Decoding} -{\relsize{-1} -\ALGORITHM{a Shorten encoded file}{PCM samples} -\SetKwData{MAGIC}{magic number} -\SetKwData{VERSION}{version} -\SetKwData{FILETYPE}{file type} -\SetKwData{CHANNELS}{channels} -\SetKwData{BLOCKLENGTH}{block length} -\SetKwData{MAXLPC}{max LPC} -\SetKwData{MEANCOUNT}{mean count} -\SetKwData{SKIPBYTES}{bytes to skip} -\SetKwData{COMMAND}{command} -\SetKwData{SAMPLES}{samples} -$\MAGIC \leftarrow$ \READ 4 bytes\; -\ASSERT $\MAGIC = \texttt{"ajkg"}$\; -$\VERSION \leftarrow$ \READ 8 unsigned bits\; -\ASSERT $\VERSION = 2$\; -\BlankLine -\tcc{read Shorten header} -\begin{tabular}{r>{$}c<{$}l} - \FILETYPE & \leftarrow & read \texttt{long}()\; \\ - \CHANNELS & \leftarrow & read \texttt{long}()\; \\ - \BLOCKLENGTH & \leftarrow & read \texttt{long}()\; \\ - \MAXLPC & \leftarrow & read \texttt{long}()\; \\ - \MEANCOUNT & \leftarrow & read \texttt{long}()\; \\ - \SKIPBYTES & \leftarrow & read \texttt{long}()\; \\ -\end{tabular}\; -\SKIP $(\SKIPBYTES)$ bytes\; -\BlankLine -\hyperref[shorten:process_commands]{process Shorten commands to PCM samples}\; -\EALGORITHM -} - -\begin{table}[h] -{\relsize{-1} -\begin{tabular}{|r|l||r|l|} -\hline -file type & format & file type & format \\ -\hline -0 & lossless \textmu-Law & -7 & lossy \textmu-Law \\ -1 & signed 8 bit & -8 & new \textmu-Law with zero mapping \\ -2 & unsigned 8 bit & -9 & lossless a-Law \\ -3 & signed 16 bit, big-endian & -10 & lossy a-Law \\ -4 & unsigned 16 bit, big-endian & -11 & Microsoft .wav \\ -5 & signed 16 bit, little-endian & -12 & Apple .aiff \\ -6 & unsigned 16 bit, little-endian & -& \\ -\hline -\end{tabular} -} -\end{table} -\par -\noindent -\VAR{channels} is the total number of channels in the stream -and \VAR{block length} is the number of PCM frames output -from the next command, which is typically constant -until the final set of commands in the stream. - -\clearpage - -\subsubsection{Header Decoding Example} - -\begin{figure}[h] - \includegraphics{figures/shorten/header_parse.pdf} -\end{figure} -In this example, \VAR{magic number} is \texttt{"ajkg"} and -\VAR{version} is 2. -Since \VAR{file type} is a \texttt{long}\footnote{which is defined as - \texttt{unsigned}(\texttt{unsigned}(2))} -it breaks down as follows: -\par -\noindent -\begin{wrapfigure}[3]{r}{140pt} - \includegraphics{figures/shorten/filetype.pdf} -\end{wrapfigure} -{\relsize{-1} - \begin{align*} - \textsf{MSB}_0 &\leftarrow 0 - \text{ (number of \texttt{0} bits until next \texttt{1})} \\ - \textsf{LSB}_0 &\leftarrow 3 - \text{ (read 2 unsigned bits)} \\ - \texttt{unsigned}(2) &\leftarrow 0 \times 2 ^ 2 + 3 = 3 \\ - \textsf{MSB}_1 &\leftarrow 0 - \text{ (number of \texttt{0} bits until next \texttt{1})} \\ - \textsf{LSB}_1 &\leftarrow 5 - \text{ (read 3 unsigned bits)} \\ - \textsf{file type} &\leftarrow 0 \times 2 ^ 3 + 5 = 5 \\ - \end{align*} -} -\par -\noindent -meaning our file consists of signed, 16-bit, little-endian data. -The remaining fields are determined as follows: -\begin{table}[h] - \begin{tabular}{r>{$}c<{$}rr>{$}r<{$}rr>{$}r<{$}} - & & $\textsf{MSB}_0$ & $\textsf{LSB}_0$ & $\texttt{unsigned}(2)$ & - $\textsf{MSB}_1$ & $\textsf{LSB}_1$ & \texttt{long} \text{ value} \\ - \hline - \textsf{channels} & \leftarrow & {\color{red} 0} & {\color{orange} 2} & - {\color{red} 0} \times 2 ^ 2 + {\color{orange} 2} = {\color{fuchsia} 2} & - {\color{blue} 0} & {\color{green} 2} & - {\color{blue} 0} \times 2 ^ {\color{fuchsia} 2} + {\color{green} 2} = - \textbf{2} \\ - \textsf{block length} & \leftarrow & {\color{red} 2} & {\color{orange} 1} & - {\color{red} 2} \times 2 ^ 2 + {\color{orange} 1} = {\color{fuchsia} 9} & - {\color{blue} 0} & {\color{green} 256} & - {\color{blue} 0} \times 2 ^ {\color{fuchsia} 9} + {\color{green} 256} = - \textbf{256} \\ - \textsf{max LPC} & \leftarrow & {\color{red} 0} & {\color{orange} 0} & - {\color{red} 0} \times 2 ^ 2 + {\color{orange} 0} = {\color{fuchsia} 0} & - {\color{blue} 0} & {\color{green} 0} & - {\color{blue} 0} \times 2 ^ {\color{fuchsia} 0} + {\color{green} 0} = - \textbf{0} \\ - \textsf{bytes to skip} & \leftarrow & {\color{red} 0} & {\color{orange} 0} & - {\color{red} 0} \times 2 ^ 2 + {\color{orange} 0} = {\color{fuchsia} 0} & - {\color{blue} 0} & {\color{green} 0} & - {\color{blue} 0} \times 2 ^ {\color{fuchsia} 0} + {\color{green} 0} = - \textbf{0} \\ - \end{tabular} -\end{table} - +\input{shorten/decode} \clearpage -\subsection{Processing Shorten Commands} -\label{shorten:process_commands} -{\relsize{-1} - \begin{algorithm}[H] - \DontPrintSemicolon - \SetKw{OR}{or} - \SetKwData{CHANNELS}{channels} - \SetKwData{BLOCKLENGTH}{block length} - \SetKwData{COMMAND}{command} - \SetKwData{LEFTSHIFT}{left shift} - \SetKwData{SAMPLES}{samples} - \SetKwData{MEANCOUNT}{mean count} - \SetKwData{MEANS}{means} - \SetKwData{SHIFTED}{unshifted} - \tcc{setup initial variables} - $i \leftarrow 0$\; - $\LEFTSHIFT \leftarrow 0$\; - \BlankLine - \tcc{process commands} - \Repeat{$\COMMAND = 4$}{ - $\COMMAND \leftarrow$ read \texttt{unsigned}(2)\; - \eIf(\tcc*[f]{audio data commands}){$(0 \leq \COMMAND \leq 3)$ \OR $(7 \leq \COMMAND \leq 8)$}{ - $c \leftarrow i \bmod \CHANNELS$\tcc*{current channel} - $m \leftarrow \lfloor i \div \CHANNELS\rfloor$\; - \Switch{\COMMAND}{ - \uCase(\tcc*[f]{DIFF0}){0}{ - $\text{\SAMPLES}_c \leftarrow$ \hyperref[shorten:read_diff0]{read \texttt{DIFF0} with \BLOCKLENGTH, - and $\text{\MEANS}_{c~[m - \text{\MEANCOUNT} \IDOTS m - 1]}$}\; - } - \uCase(\tcc*[f]{DIFF1}){1}{ - $\text{\SAMPLES}_c \leftarrow$ \hyperref[shorten:read_diff1]{read \texttt{DIFF1} with \BLOCKLENGTH and - previous $\text{\SAMPLES}_c$}\; - } - \uCase(\tcc*[f]{DIFF2}){2}{ - $\text{\SAMPLES}_c \leftarrow$ \hyperref[shorten:read_diff2]{read \texttt{DIFF2} with \BLOCKLENGTH and - previous $\text{\SAMPLES}_c$}\; - } - \uCase(\tcc*[f]{DIFF3}){3}{ - $\text{\SAMPLES}_c \leftarrow$ \hyperref[shorten:read_diff3]{read \texttt{DIFF3} with \BLOCKLENGTH and - previous $\text{\SAMPLES}_c$}\; - } - \uCase(\tcc*[f]{QLPC}){7}{ - $\text{\SAMPLES}_c \leftarrow$ \hyperref[shorten:read_qlpc]{read \texttt{QLPC} with \BLOCKLENGTH, - $\text{\MEANS}_{c~[m - \text{\MEANCOUNT} \IDOTS m - 1]}$\newline and previous $\text{\SAMPLES}_c$}\; - } - \Case(\tcc*[f]{ZERO}){8}{ - \For{$j \leftarrow 0$ \emph{\KwTo}\BLOCKLENGTH}{ - $\text{\SAMPLES}_{c~j} \leftarrow 0$\; - } - } - } - $\text{\MEANS}_{c~m} \leftarrow \hyperref[shorten:shnmean]{\texttt{shnmean}(\text{\SAMPLES}_c~,~\BLOCKLENGTH)}$\; - \hyperref[shorten:wrap_samples]{wrap $\text{\SAMPLES}_c$ for next set of channels}\; - $\text{\SHIFTED}_c \leftarrow$ \hyperref[shorten:apply_leftshift]{apply \LEFTSHIFT to $\text{\SAMPLES}_c$}\; - $i \leftarrow i + 1$\; - \If{$i \bmod \CHANNELS = 0$}{ - \Return \SHIFTED as a complete set of PCM frames\; - } - }(\tcc*[f]{non audio commands}){ - \Switch{\COMMAND}{ - \uCase(\tcc*[f]{QUIT}){4}{ - \tcc{do nothing} - } - \uCase(\tcc*[f]{BLOCKSIZE}){5}{ - $\BLOCKLENGTH \leftarrow$ read \texttt{long}()\; - } - \uCase(\tcc*[f]{BITSHIFT}){6}{ - $\LEFTSHIFT \leftarrow$ read \texttt{unsigned}(2)\; - } - \uCase(\tcc*[f]{VERBATIM}){9}{ - \hyperref[shorten:read_verbatim]{handle verbatim block of non-audio data}\; - } - \Other{unknown command} - } - } - }(\tcc*[f]{QUIT command}) - \end{algorithm} -} - -\clearpage - -\subsubsection{The \texttt{shnmean} Function} -\label{shorten:shnmean} -The \texttt{shnmean} function is defined as: -\begin{equation*} - \texttt{shnmean}(values~,~count) = - \left\lfloor - \frac{\left\lfloor\frac{count}{2}\right\rfloor + - \overset{count - 1}{\underset{i = 0}{\sum}}{values}_i }{count} - \right\rfloor -\end{equation*} -where $values$ is a list and $count$ is the length of that list. - - -\subsection{Handling Verbatim Data} -\label{shorten:read_verbatim} -These are non-audio blocks designed to hold Wave/AIFF headers or footers. -They are expected to be in the Shorten file in the same order -they would be output to disk. -\par -\noindent -\ALGORITHM{Shorten stream}{1 or more bytes of non-audio file data} -\SetKwData{BYTES}{bytes} -$size \leftarrow$ read \texttt{unsigned}(5)\; -\For{$i \leftarrow 0$ \emph{\KwTo}size}{ - $\text{\BYTES}_i \leftarrow$ read \texttt{unsigned}(8)\; -} -\Return \BYTES\; -\EALGORITHM -\begin{figure}[h] -\includegraphics{figures/shorten/verbatim.pdf} -\end{figure} - -\clearpage - -\subsection{Reading \texttt{DIFF0}} -\label{shorten:read_diff0} -{\relsize{-1} -\ALGORITHM{block length, previous \VAR{mean count} means for channel $c$}{decoded samples} -\SetKwData{BLOCKLENGTH}{block length} -\SetKwData{OFFSET}{offset} -\SetKwData{MEANS}{previous means} -\SetKwData{ENERGY}{energy} -\SetKwData{RESIDUAL}{residual} -\SetKwData{SAMPLES}{samples} -$\text{\OFFSET} \leftarrow \texttt{shnmean}(\text{\MEANS}_c~,~\text{mean count})$\; -$\ENERGY \leftarrow$ read \texttt{unsigned}(3)\; -\BlankLine -\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKLENGTH}{ - $\text{\RESIDUAL}_i \leftarrow$ read \texttt{signed}(\ENERGY)\; - $\text{\SAMPLES}_{c~i} \leftarrow \text{\RESIDUAL}_i + \OFFSET$\; -} -\Return $\text{\SAMPLES}_c$\; -\EALGORITHM -} - -\subsection{Reading \texttt{DIFF1}} -\label{shorten:read_diff1} -{\relsize{-1} -\ALGORITHM{block length, previously decoded samples}{decoded samples} -\SetKwData{BLOCKLENGTH}{block length} -\SetKwData{ENERGY}{energy} -\SetKwData{RESIDUAL}{residual} -\SetKwData{SAMPLES}{samples} -$\ENERGY \leftarrow$ read \texttt{unsigned}(3)\; -\BlankLine -\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKLENGTH}{ - $\text{\RESIDUAL}_i \leftarrow$ read \texttt{signed}(\ENERGY)\; - $\text{\SAMPLES}_{c~i} \leftarrow \text{\SAMPLES}_{c~(i - 1)} + \text{\RESIDUAL}_i$\; -} -\Return $\text{\SAMPLES}_c$\; -\EALGORITHM -\par -\noindent -$\text{samples}_{c~(-1)}$ is from the previously -decoded samples on channel $c$, -or 0 if there are none -} - -\subsection{Reading \texttt{DIFF2}} -\label{shorten:read_diff2} -{\relsize{-1} - \ALGORITHM{block length, previously decoded samples}{decoded samples} - \SetKwData{BLOCKLENGTH}{block length} - \SetKwData{ENERGY}{energy} - \SetKwData{RESIDUAL}{residual} - \SetKwData{SAMPLES}{samples} - $\ENERGY \leftarrow$ read \texttt{unsigned}(3)\; - \BlankLine - \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKLENGTH}{ - $\text{\RESIDUAL}_i \leftarrow$ read \texttt{signed}(\ENERGY)\; - $\text{\SAMPLES}_{c~i} \leftarrow (2 \times \text{\SAMPLES}_{c~(i - 1)}) - \text{\SAMPLES}_{c~(i - 2)} + \text{\RESIDUAL}_i$\; - } - \Return $\text{\SAMPLES}_c$\; - \EALGORITHM - \par - \noindent - $\text{samples}_{c~(-1)}$ and $\text{samples}_{c~(-2)}$ are from - the previously decoded samples on channel $c$, - or 0 if there are none -} - -\subsection{Reading \texttt{DIFF3}} -\label{shorten:read_diff3} -{\relsize{-1} - \ALGORITHM{block length, previously decoded samples}{decoded samples} - \SetKwData{BLOCKLENGTH}{block length} - \SetKwData{ENERGY}{energy} - \SetKwData{RESIDUAL}{residual} - \SetKwData{SAMPLES}{samples} - $\ENERGY \leftarrow$ read \texttt{unsigned}(3)\; - \BlankLine - \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKLENGTH}{ - $\text{\RESIDUAL}_i \leftarrow$ read \texttt{signed}(\ENERGY)\; - $\text{\SAMPLES}_{c~i} \leftarrow (3 \times (\text{\SAMPLES}_{c~(i - 1)} - \text{\SAMPLES}_{c~(i - 2)})) + \text{\SAMPLES}_{c~(i - 3)} + \text{\RESIDUAL}_i$\; - } - \Return $\text{\SAMPLES}_c$\; - \EALGORITHM - \par - \noindent - $\text{samples}_{c~(-1)}$, $\text{samples}_{c~(-2)}$ and - $\text{samples}_{c~(-3)}$ are from - the previously decoded samples on channel $c$, - or 0 if there are none -} - -\clearpage - -\subsection{\texttt{DIFF3} Parsing Example} -Given a \texttt{DIFF3} command issued with a current \VAR{block length} of 15 -and the bytes: -\begin{figure}[h] -\includegraphics{figures/shorten/block1.pdf} -\end{figure} -\begin{equation*} -\text{energy} \leftarrow 0 \times 2 ^ 3 + 1 = 1 -\end{equation*} -\begin{table}[h] - {\relsize{-1} - \renewcommand{\arraystretch}{1.25} - \begin{tabular}{rrr>{$}r<{$}>{$}r<{$}>{$}r<{$}} - $i$ & $\textsf{MSB}_i$ & $\textsf{LSB}_i$ & - \textsf{unsigned}_i & \textsf{residual}_i & \textsf{sample}_i \\ - \hline - -3 & & & & & 0 \\ - -2 & & & & & 0 \\ - -1 & & & & & 0 \\ - \hline - 0 & - 0 & 0 & 0 \times 2 ^ {2} + 0 = 0 & - \lfloor 0 \div 2 \rfloor = 0 & - (3 \times (0 - 0)) + 0 + 0 = 0 \\ - 1 & - 8 & 0 & 8 \times 2 ^ {2} + 0 = 32 & - \lfloor 32 \div 2 \rfloor = 16 & - (3 \times (0 - 0)) + 0 + 16 = 16 \\ - 2 & - 8 & 1 & 8 \times 2 ^ {2} + 1 = 33 & - -\lfloor 33 \div 2 \rfloor - 1 = -17 & - (3 \times (16 - 0)) + 0 - 17 = 31 \\ - 3 & - 0 & 1 & 0 \times 2 ^ {2} + 1 = 1 & - -\lfloor 1 \div 2 \rfloor - 1 = -1 & - (3 \times (31 - 16)) + 0 - 1 = 44 \\ - 4 & - 0 & 1 & 0 \times 2 ^ {2} + 1 = 1 & - -\lfloor 1 \div 2 \rfloor - 1 = -1 & - (3 \times (44 - 31)) + 16 - 1 = 54 \\ - 5 & - 0 & 0 & 0 \times 2 ^ {2} + 0 = 0 & - \lfloor 0 \div 2 \rfloor = 0 & - (3 \times (54 - 44)) + 31 + 0 = 61 \\ - 6 & - 0 & 1 & 0 \times 2 ^ {2} + 1 = 1 & - -\lfloor 1 \div 2 \rfloor - 1 = -1 & - (3 \times (61 - 54)) + 44 - 1 = 64 \\ - 7 & - 0 & 0 & 0 \times 2 ^ {2} + 0 = 0 & - \lfloor 0 \div 2 \rfloor = 0 & - (3 \times (64 - 61)) + 54 + 0 = 63 \\ - 8 & - 0 & 0 & 0 \times 2 ^ {2} + 0 = 0 & - \lfloor 0 \div 2 \rfloor = 0 & - (3 \times (63 - 64)) + 61 + 0 = 58 \\ - 9 & - 0 & 0 & 0 \times 2 ^ {2} + 0 = 0 & - \lfloor 0 \div 2 \rfloor = 0 & - (3 \times (58 - 63)) + 64 + 0 = 49 \\ - 10 & - 1 & 0 & 1 \times 2 ^ {2} + 0 = 4 & - \lfloor 4 \div 2 \rfloor = 2 & - (3 \times (49 - 58)) + 63 + 2 = 38 \\ - 11 & - 0 & 1 & 0 \times 2 ^ {2} + 1 = 1 & - -\lfloor 1 \div 2 \rfloor - 1 = -1 & - (3 \times (38 - 49)) + 58 - 1 = 24 \\ - 12 & - 0 & 2 & 0 \times 2 ^ {2} + 2 = 2 & - \lfloor 2 \div 2 \rfloor = 1 & - (3 \times (24 - 38)) + 49 + 1 = 8 \\ - 13 & - 1 & 0 & 1 \times 2 ^ {2} + 0 = 4 & - \lfloor 4 \div 2 \rfloor = 2 & - (3 \times (8 - 24)) + 38 + 2 = -8 \\ - 14 & - 0 & 0 & 0 \times 2 ^ {2} + 0 = 0 & - \lfloor 0 \div 2 \rfloor = 0 & - (3 \times (-8 - 8)) + 24 + 0 = -24 \\ - \hline - \end{tabular} - \renewcommand{\arraystretch}{1.0} - } -\end{table} -\par -\noindent -Note that the negative $i$ samples are only used -for calculation and not re-output by the \texttt{DIFF} command. - -\clearpage - -\subsection{\texttt{DIFF3} Parsing Example 2} -Given a \texttt{DIFF3} command issued from the same channel as -the previous example with a current \VAR{block length} of 10 -and the bytes: - -\begin{figure}[h] -\includegraphics{figures/shorten/block2.pdf} -\end{figure} -\begin{equation*} - \text{energy} \leftarrow 0 \times 2 ^ 3 + 0 = 0 -\end{equation*} -\begin{table}[h] - {\relsize{-1} - \renewcommand{\arraystretch}{1.25} - \begin{tabular}{rrr>{$}r<{$}>{$}r<{$}>{$}r<{$}} - $i$ & $\textsf{MSB}_i$ & $\textsf{LSB}_i$ & - \textsf{unsigned}_i & \textsf{residual}_i & \textsf{sample}_i \\ - \hline - -3 & & & & & 8 \\ - -2 & & & & & -8 \\ - -1 & & & & & -24 \\ - \hline - 0 & - 2 & 0 & 2 \times 2 ^ {1} + 0 = 4 & - \lfloor 4 \div 2 \rfloor = 2 & - (3 \times (-24 + 8)) + 8 + 2 = -38 \\ - 1 & - 1 & 0 & 1 \times 2 ^ {1} + 0 = 2 & - \lfloor 2 \div 2 \rfloor = 1 & - (3 \times (-38 + 24)) - 8 + 1 = -49 \\ - 2 & - 0 & 1 & 0 \times 2 ^ {1} + 1 = 1 & - -\lfloor 1 \div 2 \rfloor - 1 = -1 & - (3 \times (-49 + 38)) - 24 - 1 = -58 \\ - 3 & - 2 & 0 & 2 \times 2 ^ {1} + 0 = 4 & - \lfloor 4 \div 2 \rfloor = 2 & - (3 \times (-58 + 49)) - 38 + 2 = -63 \\ - 4 & - 0 & 0 & 0 \times 2 ^ {1} + 0 = 0 & - \lfloor 0 \div 2 \rfloor = 0 & - (3 \times (-63 + 58)) - 49 + 0 = -64 \\ - 5 & - 0 & 0 & 0 \times 2 ^ {1} + 0 = 0 & - \lfloor 0 \div 2 \rfloor = 0 & - (3 \times (-64 + 63)) - 58 + 0 = -61 \\ - 6 & - 0 & 0 & 0 \times 2 ^ {1} + 0 = 0 & - \lfloor 0 \div 2 \rfloor = 0 & - (3 \times (-61 + 64)) - 63 + 0 = -54 \\ - 7 & - 0 & 1 & 0 \times 2 ^ {1} + 1 = 1 & - -\lfloor 1 \div 2 \rfloor - 1 = -1 & - (3 \times (-54 + 61)) - 64 - 1 = -44 \\ - 8 & - 0 & 0 & 0 \times 2 ^ {1} + 0 = 0 & - \lfloor 0 \div 2 \rfloor = 0 & - (3 \times (-44 + 54)) - 61 + 0 = -31 \\ - 9 & - 0 & 1 & 0 \times 2 ^ {1} + 1 = 1 & - -\lfloor 1 \div 2 \rfloor - 1 = -1 & - (3 \times (-31 + 44)) - 54 - 1 = -16 \\ - \end{tabular} - \renewcommand{\arraystretch}{1.0} - } -\end{table} -\par -\noindent -Note that because this \texttt{DIFF} is issued on the same channel -as the previous \texttt{DIFF}: -\begin{table}[h] -\begin{tabular}{rcl} - $\text{current sample}_{(-3)}$ & = & $\text{previous sample}_{12}$ \\ - $\text{current sample}_{(-2)}$ & = & $\text{previous sample}_{13}$ \\ - $\text{current sample}_{(-1)}$ & = & $\text{previous sample}_{14}$ \\ -\end{tabular} -\end{table} -\par -\noindent -the samples have been ``wrapped around'' from one channel to the next. -But again, those negative samples are not re-output by -this \texttt{DIFF} command. - -\clearpage - -\subsection{Reading \texttt{QLPC}} -\label{shorten:read_qlpc} -{\relsize{-1} -\ALGORITHM{block length, previous \VAR{mean count} means for channel $c$, previously decoded samples}{decoded samples} -\SetKwData{BLOCKLENGTH}{block length} -\SetKwData{OFFSET}{offset} -\SetKwData{MEANS}{previous means} -\SetKwData{ENERGY}{energy} -\SetKwData{LPCCOUNT}{LPC count} -\SetKwData{COEFF}{LPC coefficient} -\SetKwData{RESIDUAL}{residual} -\SetKwData{UNOFFSET}{unoffset} -\SetKwData{SAMPLES}{samples} -$\text{\OFFSET} \leftarrow \texttt{shnmean}(\text{\MEANS}_c~,~\text{mean count})$\; -$\ENERGY \leftarrow$ read \texttt{unsigned}(3)\; -$\LPCCOUNT \leftarrow$ read \texttt{unsigned}(2)\; -\For{$i \leftarrow 0$ \emph{\KwTo}\LPCCOUNT}{ - $\text{\COEFF}_i \leftarrow$ read \texttt{signed}(5)\; -} -\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKLENGTH}{ - $\text{\RESIDUAL}_i \leftarrow$ read \texttt{signed}(\ENERGY)\; - $\text{sum} \leftarrow 2 ^ 5$\; - \For{$j \leftarrow 0$ \emph{\KwTo}\LPCCOUNT}{ - \eIf(\tcc*[f]{remove offset from warm-up samples}){$i - j - 1 < 0$}{ - $\text{sum} \leftarrow \text{sum} + \text{\COEFF}_j \times (\text{\SAMPLES}_{c~(i - j - 1)} - \OFFSET)$\; - }{ - $\text{sum} \leftarrow \text{sum} + \text{\COEFF}_j \times \text{\UNOFFSET}_{(i - j - 1)}$\; - } - } - $\text{\UNOFFSET}_{i} \leftarrow \left\lfloor\frac{\text{sum}}{2 ^ 5}\right\rfloor + \text{\RESIDUAL}_i$\; -} -\For(\tcc*[f]{add offset to output samples}){$i \leftarrow 0$ \emph{\KwTo}\BLOCKLENGTH}{ - $\text{\SAMPLES}_{c~i} \leftarrow \text{\UNOFFSET}_i + \OFFSET$ -} -\Return $\text{\SAMPLES}_c$\; -\EALGORITHM -} -\begin{figure}[h] -\includegraphics{figures/shorten/qlpc.pdf} -\end{figure} -\par -\noindent -As with the \texttt{DIFF} commands, negative samples are from the -previously decoded samples on channel $c$, or 0 if there are none. - -In practice, encoded Shorten files typically contain no -\texttt{QLPC} commands at all. -Because the reference implementation uses a 32-bit -accumulator for the LPC sum, -calculation will overflow when using a nontrivial number of -coefficients. -Instead, files usually contain only \texttt{DIFF1}, \texttt{DIFF2}, -\texttt{DIFF3}, and \texttt{ZERO} audio commands. - -\clearpage - -\subsection{Reading \texttt{QLPC} Example} -\begin{figure}[h] -\includegraphics{figures/shorten/qlpc1.pdf} -\end{figure} - -In this example: -\begin{table}[h] - \begin{tabular}{r>{$}c<{$}>{$}l<{$}} - \textsf{offset} & \leftarrow & 0 \\ - $\textsf{LPC count}$ & \leftarrow & 1 \\ - $\textsf{LPC coefficient}_0$ & \leftarrow & 29 \\ - \end{tabular} -\end{table} -\begin{table}[h] -{\relsize{-1} - \renewcommand{\arraystretch}{1.25} - \begin{tabular}{rr>{$}r<{$}>{$}r<{$}||>{$}r<{$}} - $i$ & $\textsf{residual}_i$ & \textsf{sum}_i & \textsf{unoffset}_i & - \textsf{sample}_{c~i} \\ - \hline - 0 & -1 & - 2 ^ 5 + 29 \times (0 - 0) = 32 & - \left\lfloor\frac{32}{2 ^ 5}\right\rfloor - 1 = 0 & - 0 + 0 = 0 \\ - 1 & 15 & - 2 ^ 5 + 29 \times 0 = 32 & - \left\lfloor\frac{32}{2 ^ 5}\right\rfloor + 15 = 16 & - 16 + 0 = 16 \\ - 2 & 16 & - 2 ^ 5 + 29 \times 16 = 496 & - \left\lfloor\frac{496}{2 ^ 5}\right\rfloor + 16 = 31 & - 31 + 0 = 31 \\ - 3 & 15 & - 2 ^ 5 + 29 \times 31 = 931 & - \left\lfloor\frac{931}{2 ^ 5}\right\rfloor + 15 = 44 & - 44 + 0 = 44 \\ - 4 & 14 & - 2 ^ 5 + 29 \times 44 = 1308 & - \left\lfloor\frac{1308}{2 ^ 5}\right\rfloor + 14 = 54 & - 54 + 0 = 54 \\ - 5 & 12 & - 2 ^ 5 + 29 \times 54 = 1598 & - \left\lfloor\frac{1598}{2 ^ 5}\right\rfloor + 12 = 61 & - 61 + 0 = 61 \\ - 6 & 8 & - 2 ^ 5 + 29 \times 61 = 1801 & - \left\lfloor\frac{1801}{2 ^ 5}\right\rfloor + 8 = 64 & - 64 + 0 = 64 \\ - 7 & 4 & - 2 ^ 5 + 29 \times 64 = 1888 & - \left\lfloor\frac{1888}{2 ^ 5}\right\rfloor + 4 = 63 & - 63 + 0 = 63 \\ - 8 & 0 & - 2 ^ 5 + 29 \times 63 = 1859 & - \left\lfloor\frac{1859}{2 ^ 5}\right\rfloor + 0 = 58 & - 58 + 0 = 58 \\ - 9 & -4 & - 2 ^ 5 + 29 \times 58 = 1714 & - \left\lfloor\frac{1714}{2 ^ 5}\right\rfloor - 4 = 49 & - 49 + 0 = 49 \\ - %% 10 & -7 & - %% 2 ^ 5 + 29 \times 49 = 1453 & - %% \left\lfloor\frac{1453}{2 ^ 5}\right\rfloor - 7 = 38 & - %% 38 + 0 = 38 \\ - %% 11 & -11 & - %% 2 ^ 5 + 29 \times 38 = 1134 & - %% \left\lfloor\frac{1134}{2 ^ 5}\right\rfloor - 11 = 24 & - %% 24 + 0 = 24 \\ - %% 12 & -14 & - %% 2 ^ 5 + 29 \times 24 = 728 & - %% \left\lfloor\frac{728}{2 ^ 5}\right\rfloor - 14 = 8 & - %% 8 + 0 = 8 \\ - %% 13 & -16 & - %% 2 ^ 5 + 29 \times 8 = 264 & - %% \left\lfloor\frac{264}{2 ^ 5}\right\rfloor - 16 = -8 & - %% -8 + 0 = -8 \\ - %% 14 & -17 & - %% 2 ^ 5 + 29 \times -8 = -200 & - %% \left\lfloor\frac{-200}{2 ^ 5}\right\rfloor - 17 = -24 & - %% -24 + 0 = -24 \\ - %% 15 & -17 & - %% 2 ^ 5 + 29 \times -24 = -664 & - %% \left\lfloor\frac{-664}{2 ^ 5}\right\rfloor - 17 = -38 & - %% -38 + 0 = -38 \\ - %% 16 & -15 & - %% 2 ^ 5 + 29 \times -38 = -1070 & - %% \left\lfloor\frac{-1070}{2 ^ 5}\right\rfloor - 15 = -49 & - %% -49 + 0 = -49 \\ - %% 17 & -14 & - %% 2 ^ 5 + 29 \times -49 = -1389 & - %% \left\lfloor\frac{-1389}{2 ^ 5}\right\rfloor - 14 = -58 & - %% -58 + 0 = -58 \\ - %% 18 & -11 & - %% 2 ^ 5 + 29 \times -58 = -1650 & - %% \left\lfloor\frac{-1650}{2 ^ 5}\right\rfloor - 11 = -63 & - %% -63 + 0 = -63 \\ - %% 19 & -7 & - %% 2 ^ 5 + 29 \times -63 = -1795 & - %% \left\lfloor\frac{-1795}{2 ^ 5}\right\rfloor - 7 = -64 & - %% -64 + 0 = -64 \\ - \end{tabular} - \renewcommand{\arraystretch}{1.0} -} -\end{table} - -\clearpage - -\subsection{Wrapping Samples} -\label{shorten:wrap_samples} - -For any given channel, in order to process \texttt{DIFF} or -\texttt{QLPC} $\text{command}_i$, -one requires a certain number of negative-indexed samples. -As an example, to process a \texttt{DIFF3}, -$\textsf{sample}_{(-3)}$, $\textsf{sample}_{(-2)}$ and $\textsf{sample}_{(-1)}$ -are required. -These samples are ``wrapped around'' from the end of $\text{command}_{(i - 1)}$ -on the same channel. -This is what the \VAR{samples to wrap} field in the Shorten header is for. -It indicates how many samples must be wrapped from one command -to the next. - -\subsection{Applying Left Shift to Samples} -\label{shorten:apply_leftshift} -\ALGORITHM{a list of samples for a given channel $c$, a left shift value}{a list of unshifted samples} -\SetKwData{LEFTSHIFT}{left shift} -\SetKwData{SAMPLES}{samples} -\SetKwData{BLOCKLENGTH}{block length} -\SetKwData{UNSHIFTED}{unshifted} -\eIf{$\LEFTSHIFT = 0$}{ - \Return $\text{\SAMPLES}_c$\tcc*{no change} -}{ - \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKLENGTH}{ - $\text{\UNSHIFTED}_{c~i} \leftarrow \text{\SAMPLES}_{c~i} \times 2 ^ {\LEFTSHIFT}$\; - } - \Return $\text{\UNSHIFTED}_{c}$\; -} -\EALGORITHM - -\clearpage - -\section{Shorten Encoding} -As with decoding, one needs \texttt{unsigned}, \texttt{signed} and \texttt{long} -functions: - -\subsubsection{Writing \texttt{unsigned}} -\ALGORITHM{a bit count $c$, an unsigned value}{a written \texttt{unsigned} value} -\SetKwData{VALUE}{value} -\SetKwData{MSB}{MSB} -\SetKwData{LSB}{LSB} -$\text{\MSB} \leftarrow \lfloor\text{\VALUE} \div 2 ^ c\rfloor$\; -$\text{\LSB} \leftarrow \text{\VALUE} - \text{\MSB} \times 2 ^ c$\; -$\MSB \rightarrow$ \WUNARY with stop bit 1\; -$\LSB \rightarrow$ \WRITE $c$ unsigned bits\; -\EALGORITHM - -\subsubsection{Writing \texttt{signed}} -\ALGORITHM{a bit count $c$, a signed value}{a written \texttt{signed} value} -\SetKwData{VALUE}{value} -\eIf{$\text{\VALUE} \geq 0$}{ - write $\texttt{unsigned}(c + 1~,~\text{\VALUE} \times 2)$\; -}{ - write $\texttt{unsigned}(c + 1~,~(-\text{\VALUE} - 1) \times 2 + 1)$\; -} -\EALGORITHM - -\subsubsection{Writing \texttt{long}} -\ALGORITHM{an unsigned value}{a written \texttt{long} value} -\SetKwData{VALUE}{value} -\eIf{$\text{\VALUE} = 0$}{ - write $\texttt{unsigned}(2~,~0)$\; - write $\texttt{unsigned}(0~,~0)$\; -}{ - $\text{LSBs} \leftarrow \lfloor\log_2(\text{\VALUE})\rfloor + 1$\; - write $\texttt{unsigned}(2~,~\text{LSBs})$\; - write $\texttt{unsigned}(\text{LSBs}~,~\text{\VALUE})$\; -} -\EALGORITHM - -\clearpage - -{\relsize{-1} - \ALGORITHM{PCM frames, a block size parameter, a wave or aiff header and footer}{an encoded Shorten file} - \SetKwData{BITSPERSAMPLE}{bits per sample} - \SetKwData{CHANNELS}{channel count} - \SetKwData{BLOCKSIZE}{block size} - \SetKw{IN}{in} - \SetKwData{LEFTSHIFT}{left shift} - \SetKwData{WASTEDBITS}{wasted BPS} - \SetKwData{SHIFTED}{shifted} - \SetKwData{DIFF}{diff} - \SetKwData{RESIDUALS}{residual} - \SetKwData{ENERGY}{energy} - \SetKwData{SAMPLES}{samples} - \SetKwData{CHANNEL}{channel} - \hyperref[shorten:write_header]{write Shorten header with \BITSPERSAMPLE, \CHANNELS and \BLOCKSIZE}\; - write \texttt{unsigned}(2~,~9)\tcc*[r]{VERBATIM command} - write \texttt{unsigned}(5~,~header byte count)\; - \ForEach{byte \IN header}{ - write \texttt{unsigned}(8~,~\textit{byte})\; - } - $\text{\LEFTSHIFT} \leftarrow 0$\; - \BlankLine - \While{PCM frames remain}{ - $\text{\SAMPLES} \leftarrow$ take \BLOCKSIZE PCM frames from input stream\; - \If{$\text{\SAMPLES PCM frame count} \neq \BLOCKSIZE$}{ - $\text{\BLOCKSIZE} \leftarrow \textit{\SAMPLES PCM frame count}$\; - write \texttt{unsigned}(2~,~5)\tcc*[r]{BLOCKSIZE command} - write \texttt{long}(\BLOCKSIZE)\; - } - \ForEach{\CHANNEL \IN \SAMPLES}{ - \eIf{$\text{all samples in \CHANNEL} = 0$}{ - write \texttt{unsigned}(2~,~8)\tcc*[r]{ZERO command} - \hyperref[shorten:wrap_samples]{wrap \CHANNEL for next set of channel data}\; - }{ - $\text{\WASTEDBITS} \leftarrow$ \hyperref[shorten:calculate_wasted_bps]{calculate wasted BPS for \CHANNEL}\; - \If{$\text{\LEFTSHIFT} \neq \text{\WASTEDBITS}$}{ - $\text{\LEFTSHIFT} \leftarrow \text{\WASTEDBITS}$\; - write \texttt{unsigned}(2~,~6)\tcc*[r]{BITSHIFT command} - write \texttt{unsigned}(2~,~\LEFTSHIFT)\; - } - \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\SHIFTED}_i \leftarrow \text{\CHANNEL}_i \div 2 ^ {\text{\LEFTSHIFT}}$\; - } - $\left.\begin{tabular}{r} - \DIFF \\ - \ENERGY \\ - \RESIDUALS \\ - \end{tabular}\right\rbrace \leftarrow$ \hyperref[shorten:compute_best_diff]{compute best \texttt{DIFF}, energy and residual values for \SHIFTED}\; - write \texttt{unsigned}(2~,~\DIFF)\tcc*[r]{DIFF command} - write \texttt{unsigned}(3~,~\ENERGY)\; - \ForEach{r \IN \RESIDUALS}{ - write \texttt{signed}(\ENERGY~,~r)\; - } - \hyperref[shorten:wrap_samples]{wrap \SHIFTED for next set of channel data}\; - } - } - } - \BlankLine - \If{$\text{footer byte count} > 0$}{ - write \texttt{unsigned}(2~,~9)\tcc*[r]{VERBATIM command} - write \texttt{unsigned}(5~,~footer byte count)\; - \ForEach{byte \IN footer}{ - write \texttt{unsigned}(8~,~\textit{byte})\; - } - } - \BlankLine - write \texttt{unsigned}(2~,~5)\tcc*[r]{QUIT command} - \BlankLine - \tcc{Shorten output (not including 5 bytes of magic + version) - must be a multiple of 4 bytes, or the reference decoder's - bit stream reader will fail} - byte align the stream\; - \While{$(\text{total file size} - 5) \bmod 4 = 0$}{ - \WRITE 0 in 8 unsigned bits\; - } -\EALGORITHM -} - -\clearpage - -\subsection{Writing Shorten Header} -\label{shorten:write_header} -{\relsize{-1} - \ALGORITHM{the input stream's bits-per-sample, sample signedness and endianness;\newline channel count and initial block size}{a Shorten header} - \SetKwData{BITSPERSAMPLE}{bits per sample} - \SetKwData{ENDIANNESS}{endianness} - \SetKwData{SIGNEDNESS}{signedness} - \SetKwData{CHANNELS}{channel count} - \SetKwData{BLOCKSIZE}{block size} - $\texttt{"ajkg"} \rightarrow$ \WRITE 4 bytes\; - $2 \rightarrow$ \WRITE 8 unsigned bits\; - \uIf{$\BITSPERSAMPLE = 8$}{ - \eIf{$\SIGNEDNESS = signed$}{ - write \texttt{long}(1)\tcc*[r]{signed, 8 bit} - }{ - write \texttt{long}(2)\tcc*[r]{unsigned, 8 bit} - } - } - \uElseIf{$\BITSPERSAMPLE = 16$}{ - \eIf{$\SIGNEDNESS = signed$}{ - \eIf{$\ENDIANNESS = big$}{ - write \texttt{long}(3)\tcc*[r]{signed, 16 bit, big-endian} - }{ - write \texttt{long}(5)\tcc*[r]{signed, 16 bit, little-endian} - } - }{ - \eIf{$\ENDIANNESS = big$}{ - write \texttt{long}(4)\tcc*[r]{unsigned, 16 bit, big-endian} - }{ - write \texttt{long}(6)\tcc*[r]{unsigned, 16 bit, little-endian} - } - } - - } - \Else{ - unsupported number of bits per sample\; - } - write \texttt{long}(\CHANNELS)\; - write \texttt{long}(\BLOCKSIZE)\; - write \texttt{long}(0)\tcc*[r]{maximum LPC} - write \texttt{long}(0)\tcc*[r]{mean count} - write \texttt{long}(0)\tcc*[r]{bytes to skip} -\EALGORITHM -} - -\subsection{Calculating Wasted Bits per Sample} -\label{shorten:calculate_wasted_bps} -{\relsize{-1} - \ALGORITHM{a list of signed PCM samples}{an unsigned integer} - \SetKwData{WASTEDBPS}{wasted bps} - \SetKwData{SAMPLE}{sample} - \SetKwFunction{MIN}{min} - \SetKwFunction{WASTED}{wasted} - $\text{\WASTEDBPS} \leftarrow \infty$\tcc*[r]{maximum unsigned integer} - \For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ - $\text{\WASTEDBPS} \leftarrow \MIN(\WASTED(\text{\SAMPLE}_i)~,~\text{\WASTEDBPS})$\; - } - \eIf(\tcc*[f]{all samples are 0}){$\WASTEDBPS = \infty$}{ - \Return 0\; - }{ - \Return \WASTEDBPS\; - } - \EALGORITHM - where the \texttt{wasted} function is defined as: - \begin{equation*} - \texttt{wasted}(x) = - \begin{cases} - \infty & \text{if } x = 0 \\ - 0 & \text{if } x \bmod 2 = 1 \\ - 1 + \texttt{wasted}(x \div 2) & \text{if } x \bmod 2 = 0 \\ - \end{cases} - \end{equation*} -} - -\clearpage - -\subsection{Computing Best \texttt{DIFF} Command, Energy and Residuals} -\label{shorten:compute_best_diff} -\ALGORITHM{a list of samples for a given channel and the channel's current block size}{a \texttt{DIFF} command, unsigned energy value and list of residuals} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{SAMPLE}{sample} -\SetKwData{DELTA}{delta} -\SetKwData{ENERGY}{energy} -\SetKwData{SUM}{sum} -\SetKwFunction{MIN}{min} -\For{$i \leftarrow -2$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\DELTA}_{1~i} \leftarrow \text{\SAMPLE}_i - \text{\SAMPLE}_{(i - 1)}$\; -} -\For{$i \leftarrow -1$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\DELTA}_{2~i} \leftarrow \text{\DELTA}_{1~i} - \text{\DELTA}_{1~(i - 1)}$\; -} -\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ - $\text{\DELTA}_{3~i} \leftarrow \text{\DELTA}_{2~i} - \text{\DELTA}_{2~(i - 1)}$\; -} -\BlankLine -\For{$d \leftarrow 1$ \emph{\KwTo}4}{ - $\text{\SUM}_d \leftarrow \overset{\BLOCKSIZE - 1}{\underset{i = 0}{\sum}}|\text{\DELTA}_{d~i}|$\; -} -\BlankLine -$\ENERGY \leftarrow 0$\; -\uIf{$\text{\SUM}_1 < \MIN(\text{\SUM}_2~,~\text{\SUM}_3)$}{ - \While{$\BLOCKSIZE \times 2 ^ \text{\ENERGY} < \text{\SUM}_1$}{ - $\ENERGY \leftarrow \ENERGY + 1$\; - } - \Return $(1~,~\ENERGY~,~\text{\DELTA}_{1~[0 \IDOTS \BLOCKSIZE]})$\; -} -\uElseIf{$\text{\SUM}_2 < \text{\SUM}_3$}{ - \While{$\BLOCKSIZE \times 2 ^ \text{\ENERGY} < \text{\SUM}_2$}{ - $\ENERGY \leftarrow \ENERGY + 1$\; - } - \Return $(2~,~\ENERGY~,~\text{\DELTA}_{2~[0 \IDOTS \BLOCKSIZE]})$\; -} -\Else{ - \While{$\BLOCKSIZE \times 2 ^ \text{\ENERGY} < \text{\SUM}_3$}{ - $\ENERGY \leftarrow \ENERGY + 1$\; - } - \Return $(3~,~\ENERGY~,~\text{\DELTA}_{3~[0 \IDOTS \BLOCKSIZE]})$\; -} -\EALGORITHM -\par -\noindent -Negative sample values are taken from the channel's previous samples, -or 0 if there are none. -Although negative delta values are needed for determining the next delta, -only the non-negative deltas are used for calculating the sums -and as returned residuals. - -\clearpage - -\subsubsection{Computing Best \texttt{DIFF} Command Example} -{\relsize{-1} - \begin{tabular}{r|r|rrr} - $i$ & $\textsf{sample}_i$ & $\textsf{delta}_{1~i}$ & $\textsf{delta}_{2~i}$ & $\textsf{delta}_{3~i}$ \\ - \hline - \hline - -3 & 0 & & & \\ - -2 & 0 & 0 & & \\ - -1 & 0 & 0 & 0 & \\ - \hline - 0 & 0 & 0 & 0 & 0 \\ - 1 & 16 & 16 & 16 & 16 \\ - 2 & 31 & 15 & -1 & -17 \\ - 3 & 44 & 13 & -2 & -1 \\ - 4 & 54 & 10 & -3 & -1 \\ - 5 & 61 & 7 & -3 & 0 \\ - 6 & 64 & 3 & -4 & -1 \\ - 7 & 63 & -1 & -4 & 0 \\ - 8 & 58 & -5 & -4 & 0 \\ - 9 & 49 & -9 & -4 & 0 \\ - 10 & 38 & -11 & -2 & 2 \\ - 11 & 24 & -14 & -3 & -1 \\ - 12 & 8 & -16 & -2 & 1 \\ - 13 & -8 & -16 & 0 & 2 \\ - 14 & -24 & -16 & 0 & 0 \\ - \hline - \hline - \multicolumn{2}{r}{$\textsf{sum}_d$} & 152 & 48 & 42 \\ - \end{tabular} -\vskip 1em -\par -\noindent -Since the $\textsf{sum}_3$ value of 42 is the smallest, -we'll use a \texttt{DIFF3} command. -The loop for calculation the energy value is: -\begin{align*} -\text{(block size) } 15 \times 2 ^ 0 &< 42 \text{ ($\textsf{sum}_3$)} \\ -15 \times 2 ^ 1 &< 42 \\ -15 \times 2 ^ 2 &> 42 \\ -\end{align*} -Which means the best energy value to use is 1, the residuals are: -\newline -\texttt{[0, 16, -17, -1, -1, 0, -1, 0, 0, 0, 2, -1, 1, 2, 0]} -\newline -and the entire \texttt{DIFF3} command is encoded as: -} -\begin{figure}[h] -\includegraphics{figures/shorten/block1.pdf} -\end{figure} +\input{shorten/encode}
View file
audiotools-2.19.tar.gz/docs/reference/shorten/decode.tex
Added
@@ -0,0 +1,724 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\section{Shorten Decoding} +{\relsize{-1} +\ALGORITHM{a Shorten encoded file}{PCM samples} +\SetKwData{MAGIC}{magic number} +\SetKwData{VERSION}{version} +\SetKwData{FILETYPE}{file type} +\SetKwData{CHANNELS}{channels} +\SetKwData{BLOCKLENGTH}{block length} +\SetKwData{MAXLPC}{max LPC} +\SetKwData{MEANCOUNT}{mean count} +\SetKwData{SKIPBYTES}{bytes to skip} +\SetKwData{COMMAND}{command} +\SetKwData{SAMPLES}{samples} +$\MAGIC \leftarrow$ \READ 4 bytes\; +\ASSERT $\MAGIC = \texttt{"ajkg"}$\; +$\VERSION \leftarrow$ \READ 8 unsigned bits\; +\ASSERT $\VERSION = 2$\; +\BlankLine +\tcc{read Shorten header} +\begin{tabular}{r>{$}c<{$}l} + \FILETYPE & \leftarrow & read \texttt{long}()\; \\ + \CHANNELS & \leftarrow & read \texttt{long}()\; \\ + \BLOCKLENGTH & \leftarrow & read \texttt{long}()\; \\ + \MAXLPC & \leftarrow & read \texttt{long}()\; \\ + \MEANCOUNT & \leftarrow & read \texttt{long}()\; \\ + \SKIPBYTES & \leftarrow & read \texttt{long}()\; \\ +\end{tabular}\; +\SKIP $(\SKIPBYTES)$ bytes\; +\BlankLine +\hyperref[shorten:process_commands]{process Shorten commands to PCM samples}\; +\EALGORITHM +} + +\begin{table}[h] +{\relsize{-1} +\begin{tabular}{|r|l||r|l|} +\hline +file type & format & file type & format \\ +\hline +0 & lossless \textmu-Law & +7 & lossy \textmu-Law \\ +1 & signed 8 bit & +8 & new \textmu-Law with zero mapping \\ +2 & unsigned 8 bit & +9 & lossless a-Law \\ +3 & signed 16 bit, big-endian & +10 & lossy a-Law \\ +4 & unsigned 16 bit, big-endian & +11 & Microsoft .wav \\ +5 & signed 16 bit, little-endian & +12 & Apple .aiff \\ +6 & unsigned 16 bit, little-endian & +& \\ +\hline +\end{tabular} +} +\end{table} +\par +\noindent +\VAR{channels} is the total number of channels in the stream +and \VAR{block length} is the number of PCM frames output +from the next command, which is typically constant +until the final set of commands in the stream. + +\clearpage + +\subsubsection{Header Decoding Example} + +\begin{figure}[h] + \includegraphics{shorten/figures/header_parse.pdf} +\end{figure} +In this example, \VAR{magic number} is \texttt{"ajkg"} and +\VAR{version} is 2. +Since \VAR{file type} is a \texttt{long}\footnote{which is defined as + \texttt{unsigned}(\texttt{unsigned}(2))} +it breaks down as follows: +\par +\noindent +\begin{wrapfigure}[3]{r}{140pt} + \includegraphics{shorten/figures/filetype.pdf} +\end{wrapfigure} +{\relsize{-1} + \begin{align*} + \textsf{MSB}_0 &\leftarrow 0 + \text{ (number of \texttt{0} bits until next \texttt{1})} \\ + \textsf{LSB}_0 &\leftarrow 3 + \text{ (read 2 unsigned bits)} \\ + \texttt{unsigned}(2) &\leftarrow 0 \times 2 ^ 2 + 3 = 3 \\ + \textsf{MSB}_1 &\leftarrow 0 + \text{ (number of \texttt{0} bits until next \texttt{1})} \\ + \textsf{LSB}_1 &\leftarrow 5 + \text{ (read 3 unsigned bits)} \\ + \textsf{file type} &\leftarrow 0 \times 2 ^ 3 + 5 = 5 \\ + \end{align*} +} +\par +\noindent +meaning our file consists of signed, 16-bit, little-endian data. +The remaining fields are determined as follows: +\begin{table}[h] + \begin{tabular}{r>{$}c<{$}rr>{$}r<{$}rr>{$}r<{$}} + & & $\textsf{MSB}_0$ & $\textsf{LSB}_0$ & $\texttt{unsigned}(2)$ & + $\textsf{MSB}_1$ & $\textsf{LSB}_1$ & \texttt{long} \text{ value} \\ + \hline + \textsf{channels} & \leftarrow & {\color{red} 0} & {\color{orange} 2} & + {\color{red} 0} \times 2 ^ 2 + {\color{orange} 2} = {\color{fuchsia} 2} & + {\color{blue} 0} & {\color{green} 2} & + {\color{blue} 0} \times 2 ^ {\color{fuchsia} 2} + {\color{green} 2} = + \textbf{2} \\ + \textsf{block length} & \leftarrow & {\color{red} 2} & {\color{orange} 1} & + {\color{red} 2} \times 2 ^ 2 + {\color{orange} 1} = {\color{fuchsia} 9} & + {\color{blue} 0} & {\color{green} 256} & + {\color{blue} 0} \times 2 ^ {\color{fuchsia} 9} + {\color{green} 256} = + \textbf{256} \\ + \textsf{max LPC} & \leftarrow & {\color{red} 0} & {\color{orange} 0} & + {\color{red} 0} \times 2 ^ 2 + {\color{orange} 0} = {\color{fuchsia} 0} & + {\color{blue} 0} & {\color{green} 0} & + {\color{blue} 0} \times 2 ^ {\color{fuchsia} 0} + {\color{green} 0} = + \textbf{0} \\ + \textsf{number of means} & \leftarrow & {\color{red} 0} & + {\color{orange} 3} & + {\color{red} 0} \times 2 ^ 2 + {\color{orange} 3} = + {\color{fuchsia} 3} & + {\color{blue} 0} & {\color{green} 4} & + {\color{blue} 0} \times 2 ^ {\color{fuchsia} 3} + {\color{green} 4} = + \textbf{4} \\ + \textsf{bytes to skip} & \leftarrow & {\color{red} 0} & {\color{orange} 0} & + {\color{red} 0} \times 2 ^ 2 + {\color{orange} 0} = {\color{fuchsia} 0} & + {\color{blue} 0} & {\color{green} 0} & + {\color{blue} 0} \times 2 ^ {\color{fuchsia} 0} + {\color{green} 0} = + \textbf{0} \\ + \end{tabular} +\end{table} + + +\clearpage + +\subsection{Processing Shorten Commands} +\label{shorten:process_commands} +{\relsize{-1} + \begin{algorithm}[H] + \DontPrintSemicolon + \SetKw{OR}{or} + \SetKwData{CHANNELS}{channels} + \SetKwData{BLOCKLENGTH}{block length} + \SetKwData{COMMAND}{command} + \SetKwData{LEFTSHIFT}{left shift} + \SetKwData{SAMPLES}{samples} + \SetKwData{MEANCOUNT}{mean count} + \SetKwData{MEANS}{means} + \SetKwData{SHIFTED}{unshifted} + \tcc{setup initial variables} + $i \leftarrow 0$\; + $\LEFTSHIFT \leftarrow 0$\; + \BlankLine + \tcc{process commands} + \Repeat{$\COMMAND = 4$}{ + $\COMMAND \leftarrow$ read \texttt{unsigned}(2)\; + \eIf(\tcc*[f]{audio data commands}){$(0 \leq \COMMAND \leq 3)$ \OR $(7 \leq \COMMAND \leq 8)$}{ + $c \leftarrow i \bmod \CHANNELS$\tcc*{current channel} + $m \leftarrow \lfloor i \div \CHANNELS\rfloor$\; + \Switch{\COMMAND}{ + \uCase(\tcc*[f]{DIFF0}){0}{ + $\text{\SAMPLES}_c \leftarrow$ \hyperref[shorten:read_diff0]{read \texttt{DIFF0} with \BLOCKLENGTH, + and $\text{\MEANS}_{c~[m - \text{\MEANCOUNT} \IDOTS m - 1]}$}\; + } + \uCase(\tcc*[f]{DIFF1}){1}{ + $\text{\SAMPLES}_c \leftarrow$ \hyperref[shorten:read_diff1]{read \texttt{DIFF1} with \BLOCKLENGTH and + previous $\text{\SAMPLES}_c$}\; + } + \uCase(\tcc*[f]{DIFF2}){2}{ + $\text{\SAMPLES}_c \leftarrow$ \hyperref[shorten:read_diff2]{read \texttt{DIFF2} with \BLOCKLENGTH and + previous $\text{\SAMPLES}_c$}\; + } + \uCase(\tcc*[f]{DIFF3}){3}{ + $\text{\SAMPLES}_c \leftarrow$ \hyperref[shorten:read_diff3]{read \texttt{DIFF3} with \BLOCKLENGTH and + previous $\text{\SAMPLES}_c$}\; + } + \uCase(\tcc*[f]{QLPC}){7}{ + $\text{\SAMPLES}_c \leftarrow$ \hyperref[shorten:read_qlpc]{read \texttt{QLPC} with \BLOCKLENGTH, + $\text{\MEANS}_{c~[m - \text{\MEANCOUNT} \IDOTS m - 1]}$\newline and previous $\text{\SAMPLES}_c$}\; + } + \Case(\tcc*[f]{ZERO}){8}{ + \For{$j \leftarrow 0$ \emph{\KwTo}\BLOCKLENGTH}{ + $\text{\SAMPLES}_{c~j} \leftarrow 0$\; + } + } + } + $\text{\MEANS}_{c~m} \leftarrow \hyperref[shorten:shnmean]{\texttt{shnmean}(\text{\SAMPLES}_c~,~\BLOCKLENGTH)}$\; + \hyperref[shorten:wrap_samples]{wrap $\text{\SAMPLES}_c$ for next set of channels}\; + $\text{\SHIFTED}_c \leftarrow$ \hyperref[shorten:apply_leftshift]{apply \LEFTSHIFT to $\text{\SAMPLES}_c$}\; + $i \leftarrow i + 1$\; + \If{$i \bmod \CHANNELS = 0$}{ + \Return \SHIFTED as a complete set of PCM frames\; + } + }(\tcc*[f]{non audio commands}){ + \Switch{\COMMAND}{ + \uCase(\tcc*[f]{QUIT}){4}{ + \tcc{do nothing} + } + \uCase(\tcc*[f]{BLOCKSIZE}){5}{ + $\BLOCKLENGTH \leftarrow$ read \texttt{long}()\; + } + \uCase(\tcc*[f]{BITSHIFT}){6}{ + $\LEFTSHIFT \leftarrow$ read \texttt{unsigned}(2)\; + } + \uCase(\tcc*[f]{VERBATIM}){9}{ + \hyperref[shorten:read_verbatim]{handle verbatim block of non-audio data}\; + } + \Other{unknown command} + } + } + }(\tcc*[f]{QUIT command}) + \end{algorithm} +} + +\clearpage + +\subsubsection{The \texttt{shnmean} Function} +\label{shorten:shnmean} +The \texttt{shnmean} function is defined as: +\begin{equation*} + \texttt{shnmean}(values~,~count) = + \left\lfloor + \frac{\left\lfloor\frac{count}{2}\right\rfloor + + \overset{count - 1}{\underset{i = 0}{\sum}}{values}_i }{count} + \right\rfloor +\end{equation*} +where $values$ is a list and $count$ is the length of that list. + + +\subsection{Handling Verbatim Data} +\label{shorten:read_verbatim} +These are non-audio blocks designed to hold Wave/AIFF headers or footers. +They are expected to be in the Shorten file in the same order +they would be output to disk. +\par +\noindent +\ALGORITHM{Shorten stream}{1 or more bytes of non-audio file data} +\SetKwData{BYTES}{bytes} +$size \leftarrow$ read \texttt{unsigned}(5)\; +\For{$i \leftarrow 0$ \emph{\KwTo}size}{ + $\text{\BYTES}_i \leftarrow$ read \texttt{unsigned}(8)\; +} +\Return \BYTES\; +\EALGORITHM +\begin{figure}[h] +\includegraphics{shorten/figures/verbatim.pdf} +\end{figure} + +\clearpage + +\subsection{Reading \texttt{DIFF0}} +\label{shorten:read_diff0} +{\relsize{-1} +\ALGORITHM{block length, previous \VAR{mean count} means for channel $c$}{decoded samples} +\SetKwData{BLOCKLENGTH}{block length} +\SetKwData{OFFSET}{offset} +\SetKwData{MEANS}{previous means} +\SetKwData{ENERGY}{energy} +\SetKwData{RESIDUAL}{residual} +\SetKwData{SAMPLES}{samples} +$\text{\OFFSET} \leftarrow \texttt{shnmean}(\text{\MEANS}_c~,~\text{mean count})$\; +$\ENERGY \leftarrow$ read \texttt{unsigned}(3)\; +\BlankLine +\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKLENGTH}{ + $\text{\RESIDUAL}_i \leftarrow$ read \texttt{signed}(\ENERGY)\; + $\text{\SAMPLES}_{c~i} \leftarrow \text{\RESIDUAL}_i + \OFFSET$\; +} +\Return $\text{\SAMPLES}_c$\; +\EALGORITHM +} + +\subsection{Reading \texttt{DIFF1}} +\label{shorten:read_diff1} +{\relsize{-1} +\ALGORITHM{block length, previously decoded samples}{decoded samples} +\SetKwData{BLOCKLENGTH}{block length} +\SetKwData{ENERGY}{energy} +\SetKwData{RESIDUAL}{residual} +\SetKwData{SAMPLES}{samples} +$\ENERGY \leftarrow$ read \texttt{unsigned}(3)\; +\BlankLine +\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKLENGTH}{ + $\text{\RESIDUAL}_i \leftarrow$ read \texttt{signed}(\ENERGY)\; + $\text{\SAMPLES}_{c~i} \leftarrow \text{\SAMPLES}_{c~(i - 1)} + \text{\RESIDUAL}_i$\; +} +\Return $\text{\SAMPLES}_c$\; +\EALGORITHM +\par +\noindent +$\text{samples}_{c~(-1)}$ is from the previously +decoded samples on channel $c$, +or 0 if there are none +} + +\subsection{Reading \texttt{DIFF2}} +\label{shorten:read_diff2} +{\relsize{-1} + \ALGORITHM{block length, previously decoded samples}{decoded samples} + \SetKwData{BLOCKLENGTH}{block length} + \SetKwData{ENERGY}{energy} + \SetKwData{RESIDUAL}{residual} + \SetKwData{SAMPLES}{samples} + $\ENERGY \leftarrow$ read \texttt{unsigned}(3)\; + \BlankLine + \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKLENGTH}{ + $\text{\RESIDUAL}_i \leftarrow$ read \texttt{signed}(\ENERGY)\; + $\text{\SAMPLES}_{c~i} \leftarrow (2 \times \text{\SAMPLES}_{c~(i - 1)}) - \text{\SAMPLES}_{c~(i - 2)} + \text{\RESIDUAL}_i$\; + } + \Return $\text{\SAMPLES}_c$\; + \EALGORITHM + \par + \noindent + $\text{samples}_{c~(-1)}$ and $\text{samples}_{c~(-2)}$ are from + the previously decoded samples on channel $c$, + or 0 if there are none +} + +\subsection{Reading \texttt{DIFF3}} +\label{shorten:read_diff3} +{\relsize{-1} + \ALGORITHM{block length, previously decoded samples}{decoded samples} + \SetKwData{BLOCKLENGTH}{block length} + \SetKwData{ENERGY}{energy} + \SetKwData{RESIDUAL}{residual} + \SetKwData{SAMPLES}{samples} + $\ENERGY \leftarrow$ read \texttt{unsigned}(3)\; + \BlankLine + \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKLENGTH}{ + $\text{\RESIDUAL}_i \leftarrow$ read \texttt{signed}(\ENERGY)\; + $\text{\SAMPLES}_{c~i} \leftarrow (3 \times (\text{\SAMPLES}_{c~(i - 1)} - \text{\SAMPLES}_{c~(i - 2)})) + \text{\SAMPLES}_{c~(i - 3)} + \text{\RESIDUAL}_i$\; + } + \Return $\text{\SAMPLES}_c$\; + \EALGORITHM + \par + \noindent + $\text{samples}_{c~(-1)}$, $\text{samples}_{c~(-2)}$ and + $\text{samples}_{c~(-3)}$ are from + the previously decoded samples on channel $c$, + or 0 if there are none +} + +\clearpage + +\subsection{\texttt{DIFF3} Parsing Example} +Given a \texttt{DIFF3} command issued with a current \VAR{block length} of 15 +and the bytes: +\begin{figure}[h] +\includegraphics{shorten/figures/block1.pdf} +\end{figure} +\begin{equation*} +\text{energy} \leftarrow 0 \times 2 ^ 3 + 1 = 1 +\end{equation*} +\begin{table}[h] + {\relsize{-1} + \renewcommand{\arraystretch}{1.25} + \begin{tabular}{rrr>{$}r<{$}>{$}r<{$}>{$}r<{$}} + $i$ & $\textsf{MSB}_i$ & $\textsf{LSB}_i$ & + \textsf{unsigned}_i & \textsf{residual}_i & \textsf{sample}_i \\ + \hline + -3 & & & & & 0 \\ + -2 & & & & & 0 \\ + -1 & & & & & 0 \\ + \hline + 0 & + 0 & 0 & 0 \times 2 ^ {2} + 0 = 0 & + \lfloor 0 \div 2 \rfloor = 0 & + (3 \times (0 - 0)) + 0 + 0 = 0 \\ + 1 & + 8 & 0 & 8 \times 2 ^ {2} + 0 = 32 & + \lfloor 32 \div 2 \rfloor = 16 & + (3 \times (0 - 0)) + 0 + 16 = 16 \\ + 2 & + 8 & 1 & 8 \times 2 ^ {2} + 1 = 33 & + -\lfloor 33 \div 2 \rfloor - 1 = -17 & + (3 \times (16 - 0)) + 0 - 17 = 31 \\ + 3 & + 0 & 1 & 0 \times 2 ^ {2} + 1 = 1 & + -\lfloor 1 \div 2 \rfloor - 1 = -1 & + (3 \times (31 - 16)) + 0 - 1 = 44 \\ + 4 & + 0 & 1 & 0 \times 2 ^ {2} + 1 = 1 & + -\lfloor 1 \div 2 \rfloor - 1 = -1 & + (3 \times (44 - 31)) + 16 - 1 = 54 \\ + 5 & + 0 & 0 & 0 \times 2 ^ {2} + 0 = 0 & + \lfloor 0 \div 2 \rfloor = 0 & + (3 \times (54 - 44)) + 31 + 0 = 61 \\ + 6 & + 0 & 1 & 0 \times 2 ^ {2} + 1 = 1 & + -\lfloor 1 \div 2 \rfloor - 1 = -1 & + (3 \times (61 - 54)) + 44 - 1 = 64 \\ + 7 & + 0 & 0 & 0 \times 2 ^ {2} + 0 = 0 & + \lfloor 0 \div 2 \rfloor = 0 & + (3 \times (64 - 61)) + 54 + 0 = 63 \\ + 8 & + 0 & 0 & 0 \times 2 ^ {2} + 0 = 0 & + \lfloor 0 \div 2 \rfloor = 0 & + (3 \times (63 - 64)) + 61 + 0 = 58 \\ + 9 & + 0 & 0 & 0 \times 2 ^ {2} + 0 = 0 & + \lfloor 0 \div 2 \rfloor = 0 & + (3 \times (58 - 63)) + 64 + 0 = 49 \\ + 10 & + 1 & 0 & 1 \times 2 ^ {2} + 0 = 4 & + \lfloor 4 \div 2 \rfloor = 2 & + (3 \times (49 - 58)) + 63 + 2 = 38 \\ + 11 & + 0 & 1 & 0 \times 2 ^ {2} + 1 = 1 & + -\lfloor 1 \div 2 \rfloor - 1 = -1 & + (3 \times (38 - 49)) + 58 - 1 = 24 \\ + 12 & + 0 & 2 & 0 \times 2 ^ {2} + 2 = 2 & + \lfloor 2 \div 2 \rfloor = 1 & + (3 \times (24 - 38)) + 49 + 1 = 8 \\ + 13 & + 1 & 0 & 1 \times 2 ^ {2} + 0 = 4 & + \lfloor 4 \div 2 \rfloor = 2 & + (3 \times (8 - 24)) + 38 + 2 = -8 \\ + 14 & + 0 & 0 & 0 \times 2 ^ {2} + 0 = 0 & + \lfloor 0 \div 2 \rfloor = 0 & + (3 \times (-8 - 8)) + 24 + 0 = -24 \\ + \hline + \end{tabular} + \renewcommand{\arraystretch}{1.0} + } +\end{table} +\par +\noindent +Note that the negative $i$ samples are only used +for calculation and not re-output by the \texttt{DIFF} command. + +\clearpage + +\subsection{\texttt{DIFF3} Parsing Example 2} +Given a \texttt{DIFF3} command issued from the same channel as +the previous example with a current \VAR{block length} of 10 +and the bytes: + +\begin{figure}[h] +\includegraphics{shorten/figures/block2.pdf} +\end{figure} +\begin{equation*} + \text{energy} \leftarrow 0 \times 2 ^ 3 + 0 = 0 +\end{equation*} +\begin{table}[h] + {\relsize{-1} + \renewcommand{\arraystretch}{1.25} + \begin{tabular}{rrr>{$}r<{$}>{$}r<{$}>{$}r<{$}} + $i$ & $\textsf{MSB}_i$ & $\textsf{LSB}_i$ & + \textsf{unsigned}_i & \textsf{residual}_i & \textsf{sample}_i \\ + \hline + -3 & & & & & 8 \\ + -2 & & & & & -8 \\ + -1 & & & & & -24 \\ + \hline + 0 & + 2 & 0 & 2 \times 2 ^ {1} + 0 = 4 & + \lfloor 4 \div 2 \rfloor = 2 & + (3 \times (-24 + 8)) + 8 + 2 = -38 \\ + 1 & + 1 & 0 & 1 \times 2 ^ {1} + 0 = 2 & + \lfloor 2 \div 2 \rfloor = 1 & + (3 \times (-38 + 24)) - 8 + 1 = -49 \\ + 2 & + 0 & 1 & 0 \times 2 ^ {1} + 1 = 1 & + -\lfloor 1 \div 2 \rfloor - 1 = -1 & + (3 \times (-49 + 38)) - 24 - 1 = -58 \\ + 3 & + 2 & 0 & 2 \times 2 ^ {1} + 0 = 4 & + \lfloor 4 \div 2 \rfloor = 2 & + (3 \times (-58 + 49)) - 38 + 2 = -63 \\ + 4 & + 0 & 0 & 0 \times 2 ^ {1} + 0 = 0 & + \lfloor 0 \div 2 \rfloor = 0 & + (3 \times (-63 + 58)) - 49 + 0 = -64 \\ + 5 & + 0 & 0 & 0 \times 2 ^ {1} + 0 = 0 & + \lfloor 0 \div 2 \rfloor = 0 & + (3 \times (-64 + 63)) - 58 + 0 = -61 \\ + 6 & + 0 & 0 & 0 \times 2 ^ {1} + 0 = 0 & + \lfloor 0 \div 2 \rfloor = 0 & + (3 \times (-61 + 64)) - 63 + 0 = -54 \\ + 7 & + 0 & 1 & 0 \times 2 ^ {1} + 1 = 1 & + -\lfloor 1 \div 2 \rfloor - 1 = -1 & + (3 \times (-54 + 61)) - 64 - 1 = -44 \\ + 8 & + 0 & 0 & 0 \times 2 ^ {1} + 0 = 0 & + \lfloor 0 \div 2 \rfloor = 0 & + (3 \times (-44 + 54)) - 61 + 0 = -31 \\ + 9 & + 0 & 1 & 0 \times 2 ^ {1} + 1 = 1 & + -\lfloor 1 \div 2 \rfloor - 1 = -1 & + (3 \times (-31 + 44)) - 54 - 1 = -16 \\ + \end{tabular} + \renewcommand{\arraystretch}{1.0} + } +\end{table} +\par +\noindent +Note that because this \texttt{DIFF} is issued on the same channel +as the previous \texttt{DIFF}: +\begin{table}[h] +\begin{tabular}{rcl} + $\text{current sample}_{(-3)}$ & = & $\text{previous sample}_{12}$ \\ + $\text{current sample}_{(-2)}$ & = & $\text{previous sample}_{13}$ \\ + $\text{current sample}_{(-1)}$ & = & $\text{previous sample}_{14}$ \\ +\end{tabular} +\end{table} +\par +\noindent +the samples have been ``wrapped around'' from one channel to the next. +But again, those negative samples are not re-output by +this \texttt{DIFF} command. + +\clearpage + +\subsection{Wrapping Samples} +\label{shorten:wrap_samples} + +For any given channel, in order to process \texttt{DIFF} or +\texttt{QLPC} $\text{command}_i$, +one requires a certain number of negative-indexed samples. +As an example, to process a \texttt{DIFF3}, +$\textsf{sample}_{(-3)}$, $\textsf{sample}_{(-2)}$ and $\textsf{sample}_{(-1)}$ +are required. +These samples are ``wrapped around'' from the end of $\text{command}_{(i - 1)}$ +on the same channel. +This is what the \VAR{samples to wrap} field in the Shorten header is for. +It indicates how many samples must be wrapped from one command +to the next. + +\subsection{Applying Left Shift to Samples} +\label{shorten:apply_leftshift} +\ALGORITHM{a list of samples for a given channel $c$, a left shift value}{a list of unshifted samples} +\SetKwData{LEFTSHIFT}{left shift} +\SetKwData{SAMPLES}{samples} +\SetKwData{BLOCKLENGTH}{block length} +\SetKwData{UNSHIFTED}{unshifted} +\eIf{$\LEFTSHIFT = 0$}{ + \Return $\text{\SAMPLES}_c$\tcc*{no change} +}{ + \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKLENGTH}{ + $\text{\UNSHIFTED}_{c~i} \leftarrow \text{\SAMPLES}_{c~i} \times 2 ^ {\LEFTSHIFT}$\; + } + \Return $\text{\UNSHIFTED}_{c}$\; +} +\EALGORITHM + +\clearpage + +\subsection{Reading \texttt{QLPC}} +\label{shorten:read_qlpc} +{\relsize{-1} +\ALGORITHM{block length, previous \VAR{mean count} means for channel $c$, previously decoded samples}{decoded samples} +\SetKwData{BLOCKLENGTH}{block length} +\SetKwData{OFFSET}{offset} +\SetKwData{MEANS}{previous means} +\SetKwData{ENERGY}{energy} +\SetKwData{LPCCOUNT}{LPC count} +\SetKwData{COEFF}{LPC coefficient} +\SetKwData{RESIDUAL}{residual} +\SetKwData{UNOFFSET}{unoffset} +\SetKwData{SAMPLES}{samples} +$\text{\OFFSET} \leftarrow \texttt{shnmean}(\text{\MEANS}_c~,~\text{mean count})$\; +$\ENERGY \leftarrow$ read \texttt{unsigned}(3)\; +$\LPCCOUNT \leftarrow$ read \texttt{unsigned}(2)\; +\For{$i \leftarrow 0$ \emph{\KwTo}\LPCCOUNT}{ + $\text{\COEFF}_i \leftarrow$ read \texttt{signed}(5)\; +} +\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKLENGTH}{ + $\text{\RESIDUAL}_i \leftarrow$ read \texttt{signed}(\ENERGY)\; + $\text{sum} \leftarrow 2 ^ 5$\; + \For{$j \leftarrow 0$ \emph{\KwTo}\LPCCOUNT}{ + \eIf(\tcc*[f]{remove offset from warm-up samples}){$i - j - 1 < 0$}{ + $\text{sum} \leftarrow \text{sum} + \text{\COEFF}_j \times (\text{\SAMPLES}_{c~(i - j - 1)} - \OFFSET)$\; + }{ + $\text{sum} \leftarrow \text{sum} + \text{\COEFF}_j \times \text{\UNOFFSET}_{(i - j - 1)}$\; + } + } + $\text{\UNOFFSET}_{i} \leftarrow \left\lfloor\frac{\text{sum}}{2 ^ 5}\right\rfloor + \text{\RESIDUAL}_i$\; +} +\For(\tcc*[f]{add offset to output samples}){$i \leftarrow 0$ \emph{\KwTo}\BLOCKLENGTH}{ + $\text{\SAMPLES}_{c~i} \leftarrow \text{\UNOFFSET}_i + \OFFSET$ +} +\Return $\text{\SAMPLES}_c$\; +\EALGORITHM +} +\begin{figure}[h] +\includegraphics{shorten/figures/qlpc.pdf} +\end{figure} +\par +\noindent +As with the \texttt{DIFF} commands, negative samples are from the +previously decoded samples on channel $c$, or 0 if there are none. + +In practice, encoded Shorten files typically contain no +\texttt{QLPC} commands at all. +Because the reference implementation uses a 32-bit +accumulator for the LPC sum, +calculation will overflow when using a nontrivial number of +coefficients. +Instead, files usually contain only \texttt{DIFF1}, \texttt{DIFF2}, +\texttt{DIFF3}, and \texttt{ZERO} audio commands. + +\clearpage + +\subsection{Reading \texttt{QLPC} Example} +\begin{figure}[h] +\includegraphics{shorten/figures/qlpc1.pdf} +\end{figure} + +In this example: +\begin{table}[h] + \begin{tabular}{r>{$}c<{$}>{$}l<{$}} + \textsf{offset} & \leftarrow & 0 \\ + $\textsf{LPC count}$ & \leftarrow & 1 \\ + $\textsf{LPC coefficient}_0$ & \leftarrow & 29 \\ + \end{tabular} +\end{table} +\begin{table}[h] +{\relsize{-1} + \renewcommand{\arraystretch}{1.25} + \begin{tabular}{rr>{$}r<{$}>{$}r<{$}||>{$}r<{$}} + $i$ & $\textsf{residual}_i$ & \textsf{sum}_i & \textsf{unoffset}_i & + \textsf{sample}_{c~i} \\ + \hline + 0 & -1 & + 2 ^ 5 + 29 \times (0 - 0) = 32 & + \left\lfloor\frac{32}{2 ^ 5}\right\rfloor - 1 = 0 & + 0 + 0 = 0 \\ + 1 & 15 & + 2 ^ 5 + 29 \times 0 = 32 & + \left\lfloor\frac{32}{2 ^ 5}\right\rfloor + 15 = 16 & + 16 + 0 = 16 \\ + 2 & 16 & + 2 ^ 5 + 29 \times 16 = 496 & + \left\lfloor\frac{496}{2 ^ 5}\right\rfloor + 16 = 31 & + 31 + 0 = 31 \\ + 3 & 15 & + 2 ^ 5 + 29 \times 31 = 931 & + \left\lfloor\frac{931}{2 ^ 5}\right\rfloor + 15 = 44 & + 44 + 0 = 44 \\ + 4 & 14 & + 2 ^ 5 + 29 \times 44 = 1308 & + \left\lfloor\frac{1308}{2 ^ 5}\right\rfloor + 14 = 54 & + 54 + 0 = 54 \\ + 5 & 12 & + 2 ^ 5 + 29 \times 54 = 1598 & + \left\lfloor\frac{1598}{2 ^ 5}\right\rfloor + 12 = 61 & + 61 + 0 = 61 \\ + 6 & 8 & + 2 ^ 5 + 29 \times 61 = 1801 & + \left\lfloor\frac{1801}{2 ^ 5}\right\rfloor + 8 = 64 & + 64 + 0 = 64 \\ + 7 & 4 & + 2 ^ 5 + 29 \times 64 = 1888 & + \left\lfloor\frac{1888}{2 ^ 5}\right\rfloor + 4 = 63 & + 63 + 0 = 63 \\ + 8 & 0 & + 2 ^ 5 + 29 \times 63 = 1859 & + \left\lfloor\frac{1859}{2 ^ 5}\right\rfloor + 0 = 58 & + 58 + 0 = 58 \\ + 9 & -4 & + 2 ^ 5 + 29 \times 58 = 1714 & + \left\lfloor\frac{1714}{2 ^ 5}\right\rfloor - 4 = 49 & + 49 + 0 = 49 \\ + %% 10 & -7 & + %% 2 ^ 5 + 29 \times 49 = 1453 & + %% \left\lfloor\frac{1453}{2 ^ 5}\right\rfloor - 7 = 38 & + %% 38 + 0 = 38 \\ + %% 11 & -11 & + %% 2 ^ 5 + 29 \times 38 = 1134 & + %% \left\lfloor\frac{1134}{2 ^ 5}\right\rfloor - 11 = 24 & + %% 24 + 0 = 24 \\ + %% 12 & -14 & + %% 2 ^ 5 + 29 \times 24 = 728 & + %% \left\lfloor\frac{728}{2 ^ 5}\right\rfloor - 14 = 8 & + %% 8 + 0 = 8 \\ + %% 13 & -16 & + %% 2 ^ 5 + 29 \times 8 = 264 & + %% \left\lfloor\frac{264}{2 ^ 5}\right\rfloor - 16 = -8 & + %% -8 + 0 = -8 \\ + %% 14 & -17 & + %% 2 ^ 5 + 29 \times -8 = -200 & + %% \left\lfloor\frac{-200}{2 ^ 5}\right\rfloor - 17 = -24 & + %% -24 + 0 = -24 \\ + %% 15 & -17 & + %% 2 ^ 5 + 29 \times -24 = -664 & + %% \left\lfloor\frac{-664}{2 ^ 5}\right\rfloor - 17 = -38 & + %% -38 + 0 = -38 \\ + %% 16 & -15 & + %% 2 ^ 5 + 29 \times -38 = -1070 & + %% \left\lfloor\frac{-1070}{2 ^ 5}\right\rfloor - 15 = -49 & + %% -49 + 0 = -49 \\ + %% 17 & -14 & + %% 2 ^ 5 + 29 \times -49 = -1389 & + %% \left\lfloor\frac{-1389}{2 ^ 5}\right\rfloor - 14 = -58 & + %% -58 + 0 = -58 \\ + %% 18 & -11 & + %% 2 ^ 5 + 29 \times -58 = -1650 & + %% \left\lfloor\frac{-1650}{2 ^ 5}\right\rfloor - 11 = -63 & + %% -63 + 0 = -63 \\ + %% 19 & -7 & + %% 2 ^ 5 + 29 \times -63 = -1795 & + %% \left\lfloor\frac{-1795}{2 ^ 5}\right\rfloor - 7 = -64 & + %% -64 + 0 = -64 \\ + \end{tabular} + \renewcommand{\arraystretch}{1.0} +} +\end{table}
View file
audiotools-2.19.tar.gz/docs/reference/shorten/encode.tex
Added
@@ -0,0 +1,316 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\section{Shorten Encoding} +As with decoding, one needs \texttt{unsigned}, \texttt{signed} and \texttt{long} +functions: + +\subsubsection{Writing \texttt{unsigned}} +\ALGORITHM{a bit count $c$, an unsigned value}{a written \texttt{unsigned} value} +\SetKwData{VALUE}{value} +\SetKwData{MSB}{MSB} +\SetKwData{LSB}{LSB} +$\text{\MSB} \leftarrow \lfloor\text{\VALUE} \div 2 ^ c\rfloor$\; +$\text{\LSB} \leftarrow \text{\VALUE} - \text{\MSB} \times 2 ^ c$\; +$\MSB \rightarrow$ \WUNARY with stop bit 1\; +$\LSB \rightarrow$ \WRITE $c$ unsigned bits\; +\EALGORITHM + +\subsubsection{Writing \texttt{signed}} +\ALGORITHM{a bit count $c$, a signed value}{a written \texttt{signed} value} +\SetKwData{VALUE}{value} +\eIf{$\text{\VALUE} \geq 0$}{ + write $\texttt{unsigned}(c + 1~,~\text{\VALUE} \times 2)$\; +}{ + write $\texttt{unsigned}(c + 1~,~(-\text{\VALUE} - 1) \times 2 + 1)$\; +} +\EALGORITHM + +\subsubsection{Writing \texttt{long}} +\ALGORITHM{an unsigned value}{a written \texttt{long} value} +\SetKwData{VALUE}{value} +\eIf{$\text{\VALUE} = 0$}{ + write $\texttt{unsigned}(2~,~0)$\; + write $\texttt{unsigned}(0~,~0)$\; +}{ + $\text{LSBs} \leftarrow \lfloor\log_2(\text{\VALUE})\rfloor + 1$\; + write $\texttt{unsigned}(2~,~\text{LSBs})$\; + write $\texttt{unsigned}(\text{LSBs}~,~\text{\VALUE})$\; +} +\EALGORITHM + +\clearpage + +{\relsize{-1} + \ALGORITHM{PCM frames, a block size parameter, a wave or aiff header and footer}{an encoded Shorten file} + \SetKwData{BITSPERSAMPLE}{bits per sample} + \SetKwData{CHANNELS}{channel count} + \SetKwData{BLOCKSIZE}{block size} + \SetKw{IN}{in} + \SetKwData{LEFTSHIFT}{left shift} + \SetKwData{WASTEDBITS}{wasted BPS} + \SetKwData{SHIFTED}{shifted} + \SetKwData{DIFF}{diff} + \SetKwData{RESIDUALS}{residual} + \SetKwData{ENERGY}{energy} + \SetKwData{SAMPLES}{samples} + \SetKwData{CHANNEL}{channel} + \hyperref[shorten:write_header]{write Shorten header with \BITSPERSAMPLE, \CHANNELS and \BLOCKSIZE}\; + write \texttt{unsigned}(2~,~9)\tcc*[r]{VERBATIM command} + write \texttt{unsigned}(5~,~header byte count)\; + \ForEach{byte \IN header}{ + write \texttt{unsigned}(8~,~\textit{byte})\; + } + $\text{\LEFTSHIFT} \leftarrow 0$\; + \BlankLine + \While{PCM frames remain}{ + $\text{\SAMPLES} \leftarrow$ take \BLOCKSIZE PCM frames from input stream\; + \If{$\text{\SAMPLES PCM frame count} \neq \BLOCKSIZE$}{ + $\text{\BLOCKSIZE} \leftarrow \textit{\SAMPLES PCM frame count}$\; + write \texttt{unsigned}(2~,~5)\tcc*[r]{BLOCKSIZE command} + write \texttt{long}(\BLOCKSIZE)\; + } + \ForEach{\CHANNEL \IN \SAMPLES}{ + \eIf{$\text{all samples in \CHANNEL} = 0$}{ + write \texttt{unsigned}(2~,~8)\tcc*[r]{ZERO command} + \hyperref[shorten:wrap_samples]{wrap \CHANNEL for next set of channel data}\; + }{ + $\text{\WASTEDBITS} \leftarrow$ \hyperref[shorten:calculate_wasted_bps]{calculate wasted BPS for \CHANNEL}\; + \If{$\text{\LEFTSHIFT} \neq \text{\WASTEDBITS}$}{ + $\text{\LEFTSHIFT} \leftarrow \text{\WASTEDBITS}$\; + write \texttt{unsigned}(2~,~6)\tcc*[r]{BITSHIFT command} + write \texttt{unsigned}(2~,~\LEFTSHIFT)\; + } + \For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\SHIFTED}_i \leftarrow \text{\CHANNEL}_i \div 2 ^ {\text{\LEFTSHIFT}}$\; + } + $\left.\begin{tabular}{r} + \DIFF \\ + \ENERGY \\ + \RESIDUALS \\ + \end{tabular}\right\rbrace \leftarrow$ \hyperref[shorten:compute_best_diff]{compute best \texttt{DIFF}, energy and residual values for \SHIFTED}\; + write \texttt{unsigned}(2~,~\DIFF)\tcc*[r]{DIFF command} + write \texttt{unsigned}(3~,~\ENERGY)\; + \ForEach{r \IN \RESIDUALS}{ + write \texttt{signed}(\ENERGY~,~r)\; + } + \hyperref[shorten:wrap_samples]{wrap \SHIFTED for next set of channel data}\; + } + } + } + \BlankLine + \If{$\text{footer byte count} > 0$}{ + write \texttt{unsigned}(2~,~9)\tcc*[r]{VERBATIM command} + write \texttt{unsigned}(5~,~footer byte count)\; + \ForEach{byte \IN footer}{ + write \texttt{unsigned}(8~,~\textit{byte})\; + } + } + \BlankLine + write \texttt{unsigned}(2~,~5)\tcc*[r]{QUIT command} + \BlankLine + \tcc{Shorten output (not including 5 bytes of magic + version) + must be a multiple of 4 bytes, or the reference decoder's + bit stream reader will fail} + byte align the stream\; + \While{$(\text{total file size} - 5) \bmod 4 = 0$}{ + \WRITE 0 in 8 unsigned bits\; + } +\EALGORITHM +} + +\clearpage + +\subsection{Writing Shorten Header} +\label{shorten:write_header} +\ALGORITHM{the input stream's bits-per-sample, sample signedness and endianness;\newline channel count and initial block size}{a Shorten header} +\SetKwData{BITSPERSAMPLE}{bits per sample} +\SetKwData{ENDIANNESS}{endianness} +\SetKwData{SIGNEDNESS}{signedness} +\SetKwData{CHANNELS}{channel count} +\SetKwData{BLOCKSIZE}{block size} +$\texttt{"ajkg"} \rightarrow$ \WRITE 4 bytes\; +$2 \rightarrow$ \WRITE 8 unsigned bits\; +\uIf{$\BITSPERSAMPLE = 8$}{ + \eIf{$\SIGNEDNESS = signed$}{ + write \texttt{long}(1)\tcc*[r]{signed, 8 bit} + }{ + write \texttt{long}(2)\tcc*[r]{unsigned, 8 bit} + } +} +\uElseIf{$\BITSPERSAMPLE = 16$}{ + \eIf{$\SIGNEDNESS = signed$}{ + \eIf{$\ENDIANNESS = big$}{ + write \texttt{long}(3)\tcc*[r]{signed, 16 bit, big-endian} + }{ + write \texttt{long}(5)\tcc*[r]{signed, 16 bit, little-endian} + } + }{ + \eIf{$\ENDIANNESS = big$}{ + write \texttt{long}(4)\tcc*[r]{unsigned, 16 bit, big-endian} + }{ + write \texttt{long}(6)\tcc*[r]{unsigned, 16 bit, little-endian} + } + } + +} +\Else{ + unsupported number of bits per sample\; +} +write \texttt{long}(\CHANNELS)\; +write \texttt{long}(\BLOCKSIZE)\; +write \texttt{long}(0)\tcc*[r]{maximum LPC} +write \texttt{long}(0)\tcc*[r]{mean count} +write \texttt{long}(0)\tcc*[r]{bytes to skip} +\EALGORITHM + +\clearpage + +\subsection{Calculating Wasted Bits per Sample} +\label{shorten:calculate_wasted_bps} +\ALGORITHM{a list of signed PCM samples}{an unsigned integer} +\SetKwData{WASTEDBPS}{wasted bps} +\SetKwData{SAMPLE}{sample} +\SetKwFunction{MIN}{min} +\SetKwFunction{WASTED}{wasted} +$\text{\WASTEDBPS} \leftarrow \infty$\tcc*[r]{maximum unsigned integer} +\For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ + $\text{\WASTEDBPS} \leftarrow \MIN(\WASTED(\text{\SAMPLE}_i)~,~\text{\WASTEDBPS})$\; +} +\eIf(\tcc*[f]{all samples are 0}){$\WASTEDBPS = \infty$}{ + \Return 0\; +}{ + \Return \WASTEDBPS\; +} +\EALGORITHM +where the \texttt{wasted} function is defined as: +\begin{equation*} + \texttt{wasted}(x) = + \begin{cases} + \infty & \text{if } x = 0 \\ + 0 & \text{if } x \bmod 2 = 1 \\ + 1 + \texttt{wasted}(x \div 2) & \text{if } x \bmod 2 = 0 \\ + \end{cases} +\end{equation*} + +\clearpage + +\subsection{Computing Best \texttt{DIFF} Command, Energy and Residuals} +\label{shorten:compute_best_diff} +\ALGORITHM{a list of samples for a given channel and the channel's current block size}{a \texttt{DIFF} command, unsigned energy value and list of residuals} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{SAMPLE}{sample} +\SetKwData{DELTA}{delta} +\SetKwData{ENERGY}{energy} +\SetKwData{SUM}{sum} +\SetKwFunction{MIN}{min} +\For{$i \leftarrow -2$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\DELTA}_{1~i} \leftarrow \text{\SAMPLE}_i - \text{\SAMPLE}_{(i - 1)}$\; +} +\For{$i \leftarrow -1$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\DELTA}_{2~i} \leftarrow \text{\DELTA}_{1~i} - \text{\DELTA}_{1~(i - 1)}$\; +} +\For{$i \leftarrow 0$ \emph{\KwTo}\BLOCKSIZE}{ + $\text{\DELTA}_{3~i} \leftarrow \text{\DELTA}_{2~i} - \text{\DELTA}_{2~(i - 1)}$\; +} +\BlankLine +\For{$d \leftarrow 1$ \emph{\KwTo}4}{ + $\text{\SUM}_d \leftarrow \overset{\BLOCKSIZE - 1}{\underset{i = 0}{\sum}}|\text{\DELTA}_{d~i}|$\; +} +\BlankLine +$\ENERGY \leftarrow 0$\; +\uIf{$\text{\SUM}_1 < \MIN(\text{\SUM}_2~,~\text{\SUM}_3)$}{ + \While{$\BLOCKSIZE \times 2 ^ \text{\ENERGY} < \text{\SUM}_1$}{ + $\ENERGY \leftarrow \ENERGY + 1$\; + } + \Return $\left\lbrace\begin{tabular}{l} + 1 \\ + \ENERGY \\ + $\text{\DELTA}_{1~[0 \IDOTS \BLOCKSIZE]}$ \\ + \end{tabular}\right.$\; +} +\uElseIf{$\text{\SUM}_2 < \text{\SUM}_3$}{ + \While{$\BLOCKSIZE \times 2 ^ \text{\ENERGY} < \text{\SUM}_2$}{ + $\ENERGY \leftarrow \ENERGY + 1$\; + } + \Return $\left\lbrace\begin{tabular}{l} + 2 \\ + \ENERGY \\ + $\text{\DELTA}_{2~[0 \IDOTS \BLOCKSIZE]}$ \\ + \end{tabular}\right.$\; +} +\Else{ + \While{$\BLOCKSIZE \times 2 ^ \text{\ENERGY} < \text{\SUM}_3$}{ + $\ENERGY \leftarrow \ENERGY + 1$\; + } + \Return $\left\lbrace\begin{tabular}{l} + 3 \\ + \ENERGY \\ + $\text{\DELTA}_{3~[0 \IDOTS \BLOCKSIZE]}$ \\ + \end{tabular}\right.$\; +} +\EALGORITHM +\par +\noindent +Negative sample values are taken from the channel's previous samples, +or 0 if there are none. +Although negative delta values are needed for determining the next delta, +only the non-negative deltas are used for calculating the sums +and as returned residuals. + +\clearpage + +\subsubsection{Computing Best \texttt{DIFF} Command Example} +{\relsize{-1} + \begin{tabular}{r|r|rrr} + $i$ & $\textsf{sample}_i$ & $\textsf{delta}_{1~i}$ & $\textsf{delta}_{2~i}$ & $\textsf{delta}_{3~i}$ \\ + \hline + \hline + -3 & 0 & & & \\ + -2 & 0 & 0 & & \\ + -1 & 0 & 0 & 0 & \\ + \hline + 0 & 0 & 0 & 0 & 0 \\ + 1 & 16 & 16 & 16 & 16 \\ + 2 & 31 & 15 & -1 & -17 \\ + 3 & 44 & 13 & -2 & -1 \\ + 4 & 54 & 10 & -3 & -1 \\ + 5 & 61 & 7 & -3 & 0 \\ + 6 & 64 & 3 & -4 & -1 \\ + 7 & 63 & -1 & -4 & 0 \\ + 8 & 58 & -5 & -4 & 0 \\ + 9 & 49 & -9 & -4 & 0 \\ + 10 & 38 & -11 & -2 & 2 \\ + 11 & 24 & -14 & -3 & -1 \\ + 12 & 8 & -16 & -2 & 1 \\ + 13 & -8 & -16 & 0 & 2 \\ + 14 & -24 & -16 & 0 & 0 \\ + \hline + \hline + \multicolumn{2}{r}{$\textsf{sum}_d$} & 152 & 48 & 42 \\ + \end{tabular} +\vskip 1em +\par +\noindent +Since the $\textsf{sum}_3$ value of 42 is the smallest, +we'll use a \texttt{DIFF3} command. +The loop for calculating the energy value is: +\begin{align*} +\text{(block size) } 15 \times 2 ^ 0 &< 42 \text{ ($\textsf{sum}_3$)} \\ +15 \times 2 ^ 1 &< 42 \\ +15 \times 2 ^ 2 &> 42 \\ +\end{align*} +Which means the best energy value to use is 1, the residuals are: +\newline +\texttt{[0, 16, -17, -1, -1, 0, -1, 0, 0, 0, 2, -1, 1, 2, 0]} +\newline +and the entire \texttt{DIFF3} command is encoded as: +} +\begin{figure}[h] +\includegraphics{shorten/figures/block1.pdf} +\end{figure}
View file
audiotools-2.19.tar.gz/docs/reference/shorten/figures
Added
+(directory)
View file
audiotools-2.19.tar.gz/docs/reference/shorten/figures/block1.bpx
Changed
(renamed from docs/reference/figures/shorten/block1.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/shorten/figures/block2.bpx
Changed
(renamed from docs/reference/figures/shorten/block2.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/shorten/figures/filetype.bpx
Changed
(renamed from docs/reference/figures/shorten/filetype.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/shorten/figures/header_parse.bpx
Changed
(renamed from docs/reference/figures/shorten/header_parse.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/shorten/figures/qlpc.bdx
Changed
(renamed from docs/reference/figures/shorten/qlpc.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/shorten/figures/qlpc1.bpx
Changed
(renamed from docs/reference/figures/shorten/qlpc1.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/shorten/figures/stream.bdx
Changed
(renamed from docs/reference/figures/shorten/stream.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/shorten/figures/verbatim.bdx
Changed
(renamed from docs/reference/figures/shorten/verbatim.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/simple-template.py
Added
@@ -0,0 +1,41 @@ +#!/usr/bin/python + +class Template: + def __init__(self, replacements): + import re + + self.__replacements__ = replacements + self.__template__ = re.compile(r'<<([a-z]+?):(.+?)>>') + + def process_string(self, s): + return self.__template__.sub(self.process_command, s) + + def process_command(self, match): + command = match.group(1) + argument = match.group(2) + if (command == "const"): + return (self.__replacements__[argument] + if argument in self.__replacements__ else "") + elif (command == "file"): + return self.process_string(open(argument, "rb").read().strip()) + else: + print >>sys.stderr,"*** Unknown command \"%s\"" % (command) + sys.exit(1) + + +if (__name__ == "__main__"): + import sys + import optparse + + parser = optparse.OptionParser() + + parser.add_option("-D", + action="append", + dest="const", + help="constant definition") + + (options, args) = parser.parse_args() + + template = Template(dict([arg.split("=", 1) for arg in options.const]) + if options.const is not None else {}) + sys.stdout.write(template.process_string(open(args[0], "rb").read()))
View file
audiotools-2.19.tar.gz/docs/reference/vorbis-codec.template
Added
@@ -0,0 +1,3 @@ +<<file:header.tex>> +\include{vorbis} +<<file:footer.tex>>
View file
audiotools-2.18.tar.gz/docs/reference/vorbis.tex -> audiotools-2.19.tar.gz/docs/reference/vorbis.tex
Changed
@@ -8,332 +8,702 @@ \chapter{Ogg Vorbis} \label{vorbis} -Ogg Vorbis is Vorbis audio in an Ogg container. -Ogg containers are a series of Ogg pages, each containing -one or more segments of data. +Ogg Vorbis is a set of Vorbis audio packets +in an \hyperref[ogg]{Ogg container}. All of the fields within Ogg Vorbis are little-endian. -\section{Ogg File Stream} -\begin{figure}[h] -\includegraphics{figures/ogg_stream.pdf} -\end{figure} -\parpic[r]{ -\begin{tabular}{|c|l|} -\hline -bits & Header Type \\ -\hline -\texttt{001} & Continuation \\ -\texttt{010} & Beginning of Stream \\ -\texttt{100} & End of Stream \\ -\hline -\end{tabular} -} -\VAR{Granule position} is a time marker. -In the case of Ogg Vorbis, it is the sample count. - -\VAR{Bitstream Serial Number} is an identifier for the given -bitstream which is unique within the Ogg file. -For instance, an Ogg file might contain both video and -audio pages, interleaved. -The Ogg pages for the audio will have a different -serial number from those of the video so that the decoder -knows where to send the data of each. - -\VAR{Page Sequence Number} is an integer counter which starts from 0 -and increments 1 for each Ogg page. -Multiple bitstreams will have separate sequence numbers. - -\VAR{Checksum} is a 32-bit checksum of the entire Ogg page. - -The \VAR{Page Segments} value indicates how many segments are in -this Ogg page. -Each segment will have an 8-bit length. -If that length is 255, it indicates the next segment is -part of the current one and should be concatenated with it when -creating packets from the segments. -In this way, packets larger than 255 bytes can be stored in -an Ogg page. -If the final segment in the Ogg page has a length of 255 bytes, -the packet it is a part of continues into the next Ogg page. - -\clearpage -\subsection{Ogg Packets} \begin{figure}[h] -\includegraphics{figures/ogg_packets.pdf} +\includegraphics{figures/vorbis/stream.pdf} \end{figure} \par \noindent -This is an example Ogg stream to illustrate a few key points about -the format. -Note that Ogg pages may have one or more segments, -and packets are composed of one of more segments, -yet the boundaries between packets are segments -that are less than 255 bytes long. -Which segment belongs to which Ogg page is not important -for building packets. - -\section{Vorbis Headers} - -The first three packets in a valid Ogg Vorbis file must be -\VAR{Identification}, \VAR{Comment} and \VAR{Setup}, in that order. -Each header packet is prefixed by a 7 byte common header. +The first Ogg page contains the identification packet. +The second Ogg packet contains the comment. +The third Ogg packet contains the setup. +Subsequent Ogg packets contain audio. \clearpage \subsection{the Identification Packet} -The first packet within a Vorbis stream is the Identification packet. +The first packet within a Vorbis stream is the \VAR{Identification} packet. This contains the sample rate and number of channels. Vorbis does not have a bits-per-sample field, as samples are stored internally as floating point values and are converted into a certain number of bits in the decoding process. To find the total samples, use the \VAR{Granule Position} value in the stream's final Ogg page. -\begin{figure}[h] -\includegraphics{figures/vorbis/identification.pdf} -\end{figure} \par \noindent -\VAR{Channels} and \VAR{Sample Rate} must be greater than 0. -The two, 4-bit \VAR{Blocksize} fields are stored as a power of 2. -For example: -\begin{align*} -\text{Blocksize}_0 &= 2 ^ {\text{Field}_0} \\ -\text{Blocksize}_1 &= 2 ^ {\text{Field}_1} -\end{align*} -where $\text{Blocksize}_i$ must be 64, 128, 256, 512, 1024, 2048, 4096 or 8192 -and -\linebreak -$\text{Blocksize}_0 \leq \text{Blocksize}_1$ - -\clearpage - -\subsection{the Comment Packet} -\label{vorbiscomment} -The second packet within a Vorbis stream is the Comment packet. - -\begin{figure}[h] -\includegraphics{figures/vorbis/comment.pdf} -\end{figure} - -The \VAR{Vendor String} and \VAR{Comment Strings} are all UTF-8 encoded. -Keys are not case-sensitive and may occur multiple times, -indicating multiple values for the same field. -For instance, a track with multiple artists may have -more than one \texttt{ARTIST}. - -\begin{multicols}{2} -{\relsize{-2} -\begin{description} -\item[ALBUM] album name -\item[ARTIST] artist name, band name, composer, author, etc. -\item[CATALOGNUMBER*] CD spine number -\item[COMPOSER*] the work's author -\item[CONDUCTOR*] performing ensemble's leader -\item[COPYRIGHT] copyright attribution -\item[DATE] recording date -\item[DESCRIPTION] a short description -\item[DISCNUMBER*] disc number for multi-volume work -\item[ENGINEER*] the recording masterer -\item[ENSEMBLE*] performing group -\item[GENRE] a short music genre label -\item[GUEST ARTIST*] collaborating artist -\item[ISRC] ISRC number for the track -\item[LICENSE] license information -\item[LOCATION] recording location -\item[OPUS*] number of the work -\item[ORGANIZATION] record label -\item[PART*] track's movement title -\item[PERFORMER] performer name, orchestra, actor, etc. -\item[PRODUCER*] person responsible for the project -\item[PRODUCTNUMBER*] UPC, EAN, or JAN code -\item[PUBLISHER*] album's publisher -\item[RELEASE DATE*] date the album was published -\item[REMIXER*] person who created the remix -\item[SOURCE ARTIST*] artist of the work being performed -\item[SOURCE MEDIUM*] CD, radio, cassette, vinyl LP, etc. -\item[SOURCE WORK*] a soundtrack's original work -\item[SPARS*] DDD, ADD, AAD, etc. -\item[SUBTITLE*] for multiple track names in a single file -\item[TITLE] track name -\item[TRACKNUMBER] track number -\item[VERSION] track version -\end{description} +{\relsize{-1} +\ALGORITHM{the first Ogg packet}{Vorbis identification information} +\SetKwData{CHANNELS}{channels} +\SetKwData{SAMPLERATE}{sample rate} +\SetKwData{BITRATEMAX}{bitrate maximum} +\SetKwData{BITRATENOM}{bitrate nominal} +\SetKwData{BITRATEMIN}{bitrate minimum} +\SetKwData{BLOCKSIZE}{block size} +\ASSERT $(\text{\READ 8 unsigned bits}) = 1$\tcc*[r]{packet type} +\ASSERT $(\text{\READ 6 bytes}) = \texttt{"vorbis"}$\; +\BlankLine +\ASSERT $(\text{\READ 32 unsigned bits}) = 0$\tcc*[r]{Vorbis version} +$\CHANNELS \leftarrow$ \READ 8 unsigned bits\; +\ASSERT $\CHANNELS > 0$\; +$\SAMPLERATE \leftarrow$ \READ 32 unsigned bits\; +\ASSERT $\SAMPLERATE > 0$\; +$\BITRATEMAX \leftarrow$ \READ 32 signed bits\; +$\BITRATENOM \leftarrow$ \READ 32 signed bits\; +$\BITRATEMIN \leftarrow$ \READ 32 signed bits\; +$\text{\BLOCKSIZE}_0 \leftarrow 2 ^ {(\text{\READ 4 unsigned bits)}}$\; +\ASSERT $\text{\BLOCKSIZE}_0$ \IN \texttt{[64, 128, 256, 512, 1024, 2048, 4096, 8192]}\; +$\text{\BLOCKSIZE}_1 \leftarrow 2 ^ {(\text{\READ 4 unsigned bits})}$\; +\ASSERT $\text{\BLOCKSIZE}_1$ \IN \texttt{[64, 128, 256, 512, 1024, 2048, 4096, 8192]}\; +\ASSERT $\text{\BLOCKSIZE}_0 \leq \text{\BLOCKSIZE}_1$\; +\ASSERT $\text{(\READ 1 unsigned bit)} = 1$\tcc*[r]{framing bit} +\BlankLine +\Return $\left\lbrace\begin{tabular}{l} +\CHANNELS \\ +\SAMPLERATE \\ +\BITRATEMAX \\ +\BITRATENOM \\ +\BITRATEMIN \\ +\BLOCKSIZE \\ +\end{tabular}\right.$\; +\EALGORITHM } -\end{multicols} -\par -\noindent -Fields marked with * are proposed extension fields and not part of the official Vorbis comment specification. - -\clearpage - -\subsection{the Setup Packet} - -The third packet in the Vorbis stream is the Setup packet. - \begin{figure}[h] -\includegraphics{figures/vorbis/setup_packet.pdf} +\includegraphics{figures/vorbis/identification.pdf} \end{figure} -It contains six sections of data required for decoding. - \clearpage -\subsubsection{Codebooks} - -The \VAR{Codebooks} section of the setup packet stores -the Huffman lookup trees. +\subsubsection{Identification Packet Example} \begin{figure}[h] -\includegraphics{figures/vorbis/codebooks.pdf} + \includegraphics{figures/vorbis/identification_example.pdf} \end{figure} -\par -\noindent -This section contains two optional sets of data, -a list of Huffman table entry lengths -and the lookup table values each entry length may resolve to. -\VAR{Total Entries} indicates the total number of entry lengths present. -These lengths may be stored in one of three ways: -unordered without sparse entries, unordered with sparse entries -and ordered. - -Unordered without sparse entries is the simplest method; -each entry length is stored as a 5 bit value, plus one. -Unordered with sparse entries is almost as simple; -each 5 bit length is prefixed by a single bit indicating -whether it is present or not. - -Ordered entries are more complicated. -The initial length is stored as a 5 bit value, plus one. -The number of entries with that length are stored as a series of -\VAR{Length Count} values in the bitstream, whose sizes -are determined by the number of remaining entries. -\begin{align*} -\text{Length Count}_i \text{ Size} &= \lfloor\log_2 (\text{Remaining Entries}_i)\rfloor + 1 -\intertext{For example, given a \VAR{Total Entries} value of 8 and an -\VAR{Initial Length} value of 2:} -\text{Length Count}_0 \text{ Size} &= \lfloor\log_2 8\rfloor + 1 = 4 \text{ bits} -\end{align*} -which means we read a 4 bit value to determine how many -\VAR{Entry Length} values are 2 bits long. -Therefore, if $\text{Length Count}_0$ is read from the bitstream as 2, -our \VAR{Entry Length} values are: -\begin{align*} -\text{Entry Length}_0 &= 2 \\ -\text{Entry Length}_1 &= 2 -\end{align*} -and the next set of lengths are 3 bits long. -Since we still have remaining entries, we read another \VAR{Length Count} -value of the length: -\begin{equation*} -\text{Length Count}_1 \text{ Size} = \lfloor\log_2 6\rfloor + 1 = 3 \text{ bits} -\end{equation*} -Thus, if $\text{Length Count}_1$ is also a value of 2 from the bitstream, -our \VAR{Entry Length} values are: -\begin{align*} -\text{Entry Length}_2 &= 3 \\ -\text{Entry Length}_3 &= 3 -\end{align*} -and the next set of lengths are 4 bits long. -We then read one more \VAR{Length Count} value: -\begin{equation*} -\text{Length Count}_2 \text{ Size} = \lfloor\log_2 4\rfloor + 1 = 3 \text{ bits} -\end{equation*} -Finally, if $\text{Length Count}_2$ is 4 from the bitstream, -our \VAR{Entry Length} values are: -\begin{align*} -\text{Entry Length}_4 &= 4 \\ -\text{Entry Length}_5 &= 4 \\ -\text{Entry Length}_6 &= 4 \\ -\text{Entry Length}_7 &= 4 -\end{align*} -At this point, we've assigned lengths to all the values -indicated by \VAR{Total Entries}, so the process is complete. +{\relsize{-1} +\begin{tabular}{rl} +packet type : & \texttt{1} \\ +vorbis : & \texttt{"vorbis"} \\ +version : & \texttt{0} \\ +channels : & \texttt{2} \\ +sample rate : & \texttt{44100} Hz \\ +maximum bitrate : & \texttt{0} \\ +nominal bitrate : & \texttt{112000} \\ +minimum bitrate : & \texttt{0} \\ +$\text{block size}_0$ : & $2 ^ \texttt{8} = 256$ \\ +$\text{block size}_1$ : & $2 ^ \texttt{11} = 2048$ \\ +framing : & 1 \\ +\end{tabular} +} \clearpage -\subsubsection{Transforming Entry Lengths to Huffman Tree} - -Once a set of entry length values is parsed from the stream, -transforming them into a Huffman decision tree -is performed by taking our entry lengths from -$\text{Entry Length}_0$ to $\text{Entry Length}_{total - 1}$ -and placing them in the tree recursively such that the 0 bit -branches are populated first. -For example, given the parsed entry length values: -\begin{align*} -\text{Entry Length}_0 &= 2 & \text{Entry Length}_1 &= 4 & \text{Entry Length}_2 &= 4 & \text{Entry Length}_3 &= 4 \\ -\text{Entry Length}_4 &= 4 & \text{Entry Length}_5 &= 2 & \text{Entry Length}_6 &= 3 & \text{Entry Length}_7 &= 3 -\end{align*} +\subsection{the Comment Packet} +\label{vorbiscomment} +The second packet within a Vorbis stream is the Comment packet. \par \noindent -We first place $\text{Entry Length}_0$ into the Huffman tree as a 2 bit value. -Since the zero bits are filled first when adding a node 2 bits deep, -it initially looks like: +{\relsize{-1} +\ALGORITHM{the second Ogg packet}{Vorbis comment information} +\SetKwData{VENLENGTH}{vendor string length} +\SetKwData{VENSTRING}{vendor string} +\SetKwData{COMCOUNT}{commment string count} +\SetKwData{LEN}{comment string length} +\SetKwData{COMMENT}{comment string} +\ASSERT $(\text{\READ 8 unsigned bits}) = 3$\tcc*[r]{packet type} +\ASSERT $(\text{\READ 6 bytes}) = \texttt{"vorbis"}$\; +\BlankLine +$\VENLENGTH \leftarrow$ \READ 32 unsigned bits\; +$\VENSTRING \leftarrow$ \READ (\VENLENGTH) bytes as UTF-8 string\; +$\COMCOUNT \leftarrow$ \READ 32 unsigned bits\; +\For{$i \leftarrow 0$ \emph{\KwTo}\COMCOUNT}{ + $\text{\LEN}_i \leftarrow$ \READ 32 unsigned bits\; + $\text{\COMMENT}_i \leftarrow$ \READ ($\text{\LEN}_i$) bytes as UTF-8 string\; +} +\ASSERT $\text{(\READ 1 unsigned bit)} = 1$\tcc*[r]{framing bit} +\BlankLine +\Return $\left\lbrace\begin{tabular}{l} +\VENSTRING \\ +\COMMENT \\ +\end{tabular}\right.$\; +\EALGORITHM +} \begin{figure}[h] -\includegraphics{figures/vorbis/huffman_example1.pdf} -\caption{$\text{Entry Length}_0$ placed with 2 bits} -\end{figure} -\par -\noindent -We then place $\text{Entry Length}_1$ into the Huffman tree as a 4 bit value. -Since the \texttt{0 0} branch is already populated by $\text{Entry Length}_0$, -we switch to the empty \texttt{0 1} branch as follows: -\begin{figure}[h] -\includegraphics{figures/vorbis/huffman_example2.pdf} -\caption{$\text{Entry Length}_1$ placed with 4 bits} -\end{figure} -\par -\noindent -The 4 bit $\text{Entry Length}_2$, $\text{Entry Length}_3$ and $\text{Entry Length}_4$ -values are placed similarly along the \texttt{0 1} branch: -\begin{figure}[h] -\includegraphics{figures/vorbis/huffman_example3.pdf} -\caption{$\text{Entry Length}_2$, $\text{Entry Length}_3$ and $\text{Entry Length}_4$ placed with 4 bits} +\includegraphics{figures/vorbis/comment.pdf} \end{figure} \par \noindent -Finally, the remaining three entries are populated along the \texttt{1} branch: -\begin{figure}[h] -\includegraphics{figures/vorbis/huffman_example4.pdf} -\caption{$\text{Entry Length}_5$, $\text{Entry Length}_6$ and $\text{Entry Length}_7$ are placed} -\end{figure} +Comment strings are \texttt{"KEY=value"} pairs +where \texttt{KEY} is an ASCII value in the range \texttt{0x20} +through \texttt{0x7D}, excluding \texttt{0x3D}, +is case-insensitive and may occur in multiple comment strings. +\texttt{value} is a UTF-8 value. +\begin{table}[h] +{\relsize{-1} + \begin{tabular}{rlrl} + \texttt{ALBUM} & album name & + \texttt{ARTIST} & artist name \\ + \texttt{CATALOG} & CD spine number & + \texttt{COMPOSER} & the work's author \\ + \texttt{COMMENT} & a short comment & + \texttt{CONDUCTOR} & performing ensemble's leader \\ + \texttt{COPYRIGHT} & copyright attribution & + \texttt{DATE} & recording date \\ + \texttt{DISCNUMBER} & disc number for multi-volume work & + \texttt{DISCTOTAL} & disc total for multi-volume work \\ + \texttt{GENRE} & a short music genre label & + \texttt{ISRC} & ISRC number for the track \\ + \texttt{PERFORMER} & performer name, orchestra, actor, etc. & + \texttt{PUBLISHER} & album's publisher \\ + \texttt{SOURCE MEDIUM} & CD, radio, cassette, vinyl LP, etc. & + \texttt{TITLE} & track name \\ + \texttt{TRACKNUMBER} & track number & + \texttt{TRACKTOTAL} & total number of tracks \\ + \end{tabular} +} +\end{table} -\subsubsection{The Lookup Table} +\clearpage -The lookup table is only present if \VAR{Lookup Type} is 1 or 2. -A \VAR{Lookup Type} of 0 indicates no lookup table, while anything -greater than 2 is an error. +\subsubsection{Comment Packet Example} -\VAR{Minimum Value} and \VAR{Delta Value} are 32-bit floating point values -which can be parsed in the following way: \begin{figure}[h] -\includegraphics{figures/vorbis/float32.pdf} + \includegraphics{figures/vorbis/comment-example.pdf} \end{figure} -\begin{equation*} -\text{Float} = -\begin{cases} -\text{Mantissa} \times 2 ^ {\text{Exponent} - 788} & \text{ if Sign = 0} \\ --\text{Mantissa} \times 2 ^ {\text{Exponent} - 788} & \text{ if Sign = 1} -\end{cases} -\end{equation*} -\par -\VAR{Value Bits} indicates the size of each value in bits, plus one. -\VAR{Sequence P} is a 1 bit flag. -The total number of multiplicand values depends on whether \VAR{Lookup Type} -is 1 or 2. -\begin{align*} -\intertext{if \VAR{Lookup Type} = 1:} -\text{Multiplicand Count} &= \text{max}(\text{Int}) ^ \text{Dimensions} \text{ where } \text{Int} ^ \text{Dimensions} \leq \text{Total Entries} -\intertext{if \VAR{Lookup Type} = 2:} -\text{Multiplicand Count} &= \text{Dimensions} \times \text{Total Entries} -\end{align*} -\par -The \VAR{Multiplicand} values themselves are a list of unsigned integers, -each \VAR{Value Bits} (+ 1) bits large. +\begin{table}[h] +\begin{tabular}{rl} + vendor string : & \texttt{"Xiph.Org libVorbis I 20090709"} \\ + comment string count : & \texttt{2} \\ + $\text{comment string}_0$ : & \texttt{"title=Title"} \\ + $\text{comment string}_1$ : & \texttt{"artist=Artist"} \\ +\end{tabular} +\end{table} +%% \subsection{the Setup Packet} + +%% The third packet in the Vorbis stream is the Setup packet. +%% \par +%% \noindent +%% {\relsize{-1} +%% \ALGORITHM{the third Ogg packet}{Vorbis decoding information} +%% \SetKwData{CBCOUNT}{codebook count} +%% \SetKwData{CB}{codebook} +%% \SetKwData{VTCOUNT}{time count} +%% \SetKwData{FLCOUNT}{floor count} +%% \SetKwData{FLTYPE}{floor type} +%% \SetKwData{FLOOR}{floor} +%% \SetKwData{RESCOUNT}{residue count} +%% \SetKwData{RESTYPE}{residue type} +%% \SetKwData{RESIDUE}{residue} +%% \SetKwData{CHANNELS}{audio channels} +%% \SetKwData{MAPCOUNT}{mapping count} +%% \SetKwData{MAPCONF}{mapping configuration} +%% \SetKwData{MODECOUNT}{mode count} +%% \SetKwData{MODECONF}{mode configuration} +%% \SetKwFunction{ILOG}{ilog} +%% \ASSERT $(\text{\READ 8 unsigned bits}) = 5$\tcc*[r]{packet type} +%% \ASSERT $(\text{\READ 6 bytes}) = \texttt{"vorbis"}$\; +%% \BlankLine +%% $\CBCOUNT \leftarrow \text{(\READ 8 unsigned bits)} + 1$\tcc*[r]{codebooks} +%% \For{$i \leftarrow 0$ \emph{\KwTo}\CBCOUNT}{ +%% $\text{\CB}_i \leftarrow$ \hyperref[vorbis:codebooks]{read codebook}\; +%% } +%% \BlankLine +%% $\VTCOUNT \leftarrow \text{(\READ 6 unsigned bits)} + 1$\tcc*[r]{time domain transforms} +%% \For{$i \leftarrow 0$ \emph{\KwTo}\VTCOUNT}{ +%% \ASSERT $(\text{\READ 16 unsigned bits}) = 0$\; +%% } +%% \BlankLine +%% $\FLCOUNT \leftarrow \text{(\READ 6 unsigned bits) + 1}$\tcc*[r]{floors} +%% \For{$i \leftarrow 0$ \emph{\KwTo}\FLCOUNT}{ +%% $\text{\FLTYPE}_i \leftarrow$ \READ 16 unsigned bits\; +%% \uIf{$0 \leq \text{\FLTYPE}_i \leq 1$}{ +%% $\text{\FLOOR}_i \leftarrow$ \hyperref[vorbis:floors]{read floor of type $\text{\FLTYPE}_i$}\; +%% } +%% \lElse{ +%% unsupported floor type error\; +%% } +%% } +%% \BlankLine +%% $\RESCOUNT \leftarrow \text{(\READ 6 unsigned bits) + 1}$\tcc*[r]{residues} +%% \For{$i \leftarrow 0$ \emph{\KwTo}\RESCOUNT}{ +%% $\text{\RESTYPE}_i \leftarrow$ \READ 16 unsigned bits\; +%% \uIf{$0 \leq \text{\RESTYPE}_i \leq 2$}{ +%% $\text{\RESIDUE}_i \leftarrow$ read residue of type $\text{\RESTYPE}_i$\; +%% } +%% \lElse{ +%% unsupported residue type error\; +%% } +%% } +%% $\MAPCOUNT \leftarrow \text{(\READ 6 unsigned bits) + 1}$\tcc*[r]{mappings} +%% \For{$i \leftarrow 0$ \emph{\KwTo}\MAPCOUNT}{ +%% $\text{\MAPCONF}_i \leftarrow$ \hyperref[vorbis:mappings]{read mapping}\; +%% } +%% \BlankLine +%% $\MODECOUNT \leftarrow (\text{\READ 6 unsigned bits}) + 1$\tcc*[r]{modes} +%% \For{$i \leftarrow 0$ \emph{\KwTo}\MODECOUNT}{ +%% $\MODECONF \leftarrow$ \hyperref[vorbis:mode]{read mode}\; +%% } +%% \BlankLine +%% \ASSERT $(\text{\READ 1 unsigned bit}) = 1$\tcc*[r]{framing bit} +%% \BlankLine +%% \Return $\left\lbrace\begin{tabular}{l} +%% \CBCOUNT \\ +%% \CB \\ +%% \VTCOUNT \\ +%% \FLCOUNT \\ +%% \FLTYPE \\ +%% \FLOOR \\ +%% \RESCOUNT \\ +%% \RESTYPE \\ +%% \RESIDUE \\ +%% \MAPCOUNT \\ +%% \MAPCONF \\ +%% \MODECOUNT \\ +%% \MODECONF \\ +%% \end{tabular}\right.$\; +%% \EALGORITHM +%% } + +%% \clearpage + +%% \begin{figure}[h] +%% \includegraphics{figures/vorbis/setup_packet.pdf} +%% \end{figure} + +%% \clearpage + +%% \subsubsection{Codebooks} +%% \label{vorbis:codebooks} +%% {\relsize{-1} +%% \ALGORITHM{Vorbis setup packet data}{Vorbis codebook information} +%% \SetKwData{CBDIMENSIONS}{codebook dimensions} +%% \SetKwData{CBENTRIES}{codebook entries} +%% \SetKwData{ORDERED}{ordered} +%% \SetKwData{SPARSE}{sparse} +%% \SetKwData{LENGTH}{codeword length} +%% \SetKwData{CENTRY}{current entry} +%% \SetKwData{CLENGTH}{current length} +%% \SetKwData{NUMBER}{number} +%% \SetKwData{CBLOOKUPTYPE}{codebook lookup type} +%% \SetKwData{CBMINVALUE}{codecook minimum value} +%% \SetKwData{CBDELTAVALUE}{codebook delta value} +%% \SetKwData{CBVALUEBITS}{codebook value bits} +%% \SetKwData{CBSEQUENCEP}{codebook sequence p} +%% \SetKwData{CBLOOKUPVAL}{codebook lookup values} +%% \SetKwData{CBMULT}{codebook multiplicands} +%% \SetKwFunction{ILOG}{ilog} +%% \SetKwFunction{FLOAT}{float32\_unpack} +%% \SetKwFunction{LOOKUPVALS}{lookup1\_values} +%% \ASSERT $\text{(\READ 24 unsigned bits) = \texttt{0x564342}}$\tcc*[r]{sync pattern} +%% $\CBDIMENSIONS \leftarrow$ \READ 16 unsigned bits\; +%% $\CBENTRIES \leftarrow$ \READ 24 unsigned bits\; +%% $\ORDERED \leftarrow$ \READ 1 unsigned bit\; +%% \eIf{$\ORDERED = 0$}{ +%% $\SPARSE \leftarrow$ \READ 1 unsigned bit\; +%% \For{$i \leftarrow 0$ \emph{\KwTo}\CBENTRIES}{ +%% \eIf{$\SPARSE = 1$}{ +%% \eIf{$(\textnormal{\READ 1 unsigned bit}) = 1$}{ +%% $\text{\LENGTH}_i \leftarrow (\text{\READ 5 unsigned bits}) + 1$\; +%% }{ +%% $\text{\LENGTH}_i \leftarrow$ marked as unused\; +%% } +%% }{ +%% $\text{\LENGTH}_i \leftarrow (\text{\READ 5 unsigned bits}) + 1$\; +%% } +%% } +%% }{ +%% $\CENTRY \leftarrow 0$\; +%% $\CLENGTH \leftarrow (\text{\READ 5 unsigned bits}) + 1$\; +%% \Repeat{$\CENTRY = \CBENTRIES$}{ +%% $\NUMBER \leftarrow$ \READ $\ILOG(\CBENTRIES - \CENTRY)$ unsigned bits\; +%% \ASSERT $(\CENTRY + \NUMBER) \leq \CBENTRIES$\; +%% \For{$i \leftarrow 0$ \emph{\KwTo}\NUMBER}{ +%% $\text{\LENGTH}_{\CENTRY} \leftarrow \CLENGTH$\; +%% $\CENTRY \leftarrow \CENTRY + 1$\; +%% } +%% $\CLENGTH \leftarrow \CLENGTH + 1$\; +%% } +%% } +%% \BlankLine +%% $\CBLOOKUPTYPE \leftarrow$ \READ 4 unsigned bits\; +%% \uIf{$1 \leq \CBLOOKUPTYPE \leq 2$}{ +%% $\CBMINVALUE \leftarrow \FLOAT(\text{\READ 32 unsigned bits})$\; +%% $\CBDELTAVALUE \leftarrow \FLOAT(\text{\READ 32 unsigned bits})$\; +%% $\CBVALUEBITS \leftarrow (\text{\READ 4 unsigned bits}) + 1$\; +%% $\CBSEQUENCEP \leftarrow$ \READ 1 unsigned bit\; +%% \eIf{$\CBLOOKUPTYPE = 1$}{ +%% $\CBLOOKUPVAL \leftarrow \LOOKUPVALS(\CBENTRIES~,~\CBDIMENSIONS)$\; +%% }{ +%% $\CBLOOKUPVAL \leftarrow \CBENTRIES \times \CBDIMENSIONS$\; +%% } +%% \For{$i \leftarrow 0$ \emph{\KwTo}\CBLOOKUPVAL}{ +%% $\text{\CBMULT}_i \leftarrow$ \READ $(\CBVALUEBITS)$ unsigned bits\; +%% } +%% } +%% \ElseIf{$\CBLOOKUPTYPE > 2$}{ +%% invalid lookup type error\; +%% } +%% \EALGORITHM +%% } + +%% \clearpage + +%% {\relsize{-1} +%% \begin{algorithm}[H] +%% \SetKwData{CBLOOKUPTYPE}{codebook lookup type} +%% \SetKwData{LAST}{last} +%% \SetKwData{IDIVISOR}{index divisor} +%% \SetKwData{MULTOFFSET}{multiplicant offset} +%% \SetKwData{CBDIMENSIONS}{codebook dimensions} +%% \SetKwData{LOOKUPOFFSET}{lookup offset} %??? +%% \SetKwData{CBLOOKUPVAL}{codebook lookup values} +%% \SetKwData{VECTOR}{vector element} +%% \SetKwData{CBMULT}{codebook multiplicands} +%% \SetKwData{CBDELTAVALUE}{codebook delta value} +%% \SetKwData{CBMINVALUE}{codecook minimum value} +%% \SetKwData{CBSEQUENCEP}{codebook sequence p} +%% \DontPrintSemicolon +%% \uIf{$\CBLOOKUPTYPE = 1$}{ +%% $\LAST = 0$\; +%% $\IDIVISOR \leftarrow 1$\; +%% \For{$i \leftarrow 0$ \emph{\KwTo}\CBDIMENSIONS}{ +%% $\MULTOFFSET \leftarrow \lfloor\LOOKUPOFFSET \div \IDIVISOR \rfloor \bmod \CBLOOKUPVAL$\; +%% $\text{\VECTOR}_{i} \leftarrow \text{\CBMULT}_{\MULTOFFSET} \times \CBDELTAVALUE + \CBMINVALUE + \LAST$\; +%% \If{$\CBSEQUENCEP = 1$}{ +%% $\LAST \leftarrow \text{\VECTOR}_i$\; +%% } +%% $\IDIVISOR \leftarrow \IDIVISOR \times \CBLOOKUPVAL$\; +%% } +%% } +%% \ElseIf{$\CBLOOKUPTYPE = 2$}{ +%% $\LAST = 0$\; +%% $\MULTOFFSET \leftarrow \LOOKUPOFFSET \times \CBDIMENSIONS$\; +%% \For{$i \leftarrow 0$ \emph{\KwTo}\CBDIMENSIONS}{ +%% $\text{\VECTOR}_i \leftarrow \text{\CBMULT}_{\MULTOFFSET} \times \CBDELTAVALUE + \CBMINVALUE + \LAST$\; +%% \If{$\CBSEQUENCEP = 1$}{ +%% $\LAST \leftarrow \text{\VECTOR}_i$\; +%% } +%% $\MULTOFFSET \leftarrow \MULTOFFSET + 1$\; +%% } +%% } +%% \Return \VECTOR\; +%% \end{algorithm} +%% } + +%% \clearpage + +%% \subsubsection{Floors} +%% \label{vorbis:floors} + +%% {\relsize{-1} +%% \ALGORITHM{Vorbis setup packet data, floor type, maximum codebook number}{Vorbis floor information} +%% \SetKwData{FLTYPE}{floor type} +%% \SetKwData{ZORDER}{floor 0 order} +%% \SetKwData{ZRATE}{floor 0 rate} +%% \SetKwData{ZBARKMAPSIZE}{floor 0 bark map size} +%% \SetKwData{ZAMPBITS}{floor 0 amplitude bits} +%% \SetKwData{ZAMPOFFSET}{floor 0 amplitude offset} +%% \SetKwData{ZNUMBOOKS}{floor 0 number of books} +%% \SetKwData{ZBOOKLIST}{floor 0 book list} +%% \SetKwData{MAXCODEBOOK}{maximum codebook number} +%% \SetKwData{OPARTITIONS}{floor 1 partitions} +%% \SetKwData{OPCLASSLIST}{floor 1 partition class list} +%% \SetKwData{OCLASSLIST}{floor 1 class subclasses} +%% \SetKwData{ODIMENSIONS}{floor 1 class dimensions} +%% \SetKwData{OMASTERBOOKS}{floor 1 class master books} +%% \SetKwData{OSUBCLASSBOOKS}{floor 1 subclass books} +%% \SetKwData{OMULT}{floor 1 multiplier} +%% \SetKwData{OXLIST}{floor 1 X list} +%% \SetKwData{OVALUES}{floor 1 values} +%% \SetKwData{MAXCLASS}{maximum class} +%% \SetKwData{RANGEBITS}{range bits} +%% \SetKwData{CURCLASSNUM}{current class number} +%% \eIf{$\FLTYPE = 0$}{ +%% \begin{tabular}{rcl} +%% \ZORDER & $\leftarrow$ & \READ 8 unsigned bits \\ +%% \ZRATE & $\leftarrow$ & \READ 8 unsigned bits \\ +%% \ZBARKMAPSIZE & $\leftarrow$ & \READ 16 unsigned bits \\ +%% \ZAMPBITS & $\leftarrow$ & \READ 6 unsigned bits \\ +%% \ZAMPOFFSET & $\leftarrow$ & \READ 8 unsigned bits \\ +%% \ZNUMBOOKS & $\leftarrow$ & $\text{(\READ 4 unsigned bits)} + 1$ \\ +%% \end{tabular}\; +%% \For{$i \leftarrow 0$ \emph{\KwTo}\ZNUMBOOKS}{ +%% $\text{\ZBOOKLIST}_i \leftarrow$ \READ 8 unsigned bits\; +%% \ASSERT $\text{\ZBOOKLIST}_i \leq \MAXCODEBOOK$\; +%% } +%% }{ +%% $\OPARTITIONS \leftarrow$ \READ 5 unsigned bits\; +%% \For{$i \leftarrow 0$ \emph{\KwTo}\OPARTITIONS}{ +%% $\text{\OPCLASSLIST}_i \leftarrow$ \READ 4 unsigned bits\; +%% } +%% $\MAXCLASS \leftarrow$ maximum value in $\text{\OPCLASSLIST}$, or 0 if $\OPARTITIONS = 0$\; +%% \For{$i \leftarrow 0$ \emph{\KwTo}\MAXCLASS}{ +%% $\text{\ODIMENSIONS}_i \leftarrow (\text{\READ 3 unsigned bits}) + 1$\; +%% $\text{\OCLASSLIST}_i \leftarrow$ \READ 2 unsigned bits\; +%% \If{$\text{\OCLASSLIST}_i \neq 0$}{ +%% $\text{\OMASTERBOOKS}_i \leftarrow$ \READ 8 unsigned bits\; +%% } +%% \For{$j \leftarrow 0$ \emph{\KwTo}$2 ^ {\text{\OCLASSLIST}_i} - 1$}{ +%% $\text{\OSUBCLASSBOOKS}_{i~j} \leftarrow (\text{\READ 8 unsigned bits} - 1)$\; +%% } +%% } +%% $\OMULT \leftarrow (\text{\READ 2 unsigned bits}) + 1$\; +%% $\RANGEBITS \leftarrow$ \READ 4 unsigned bits\; +%% $\text{\OXLIST}_0 \leftarrow 0$\; +%% $\text{\OXLIST}_1 \leftarrow 2 ^ {\RANGEBITS}$\; +%% $\OVALUES \leftarrow 2$\; +%% \For{$i \leftarrow 0$ \emph{\KwTo}\OPARTITIONS}{ +%% $\CURCLASSNUM \leftarrow \text{\OPCLASSLIST}_i$\; +%% \For{$j \leftarrow 0$ \emph{\KwTo}$\text{\ODIMENSIONS}_{\CURCLASSNUM}$}{ +%% $\text{\OXLIST}_{\OVALUES} \leftarrow$ \READ $(\RANGEBITS)$ unsigned bits\; +%% $\OVALUES \leftarrow \OVALUES + 1$\; +%% } +%% } +%% } +%% \EALGORITHM +%% } + +%% \clearpage + +%% \subsubsection{Mappings} +%% \label{vorbis:mappings} +%% {\relsize{-1} +%% \ALGORITHM{Vorbis setup packet data}{Vorbis mapping information} +%% \SetKwData{CHANNELS}{audio channels} +%% \SetKwData{SUBMAPS}{mapping submaps} +%% \SetKwData{MAPCOUPSTEPS}{mapping coupling steps} +%% \SetKwData{MAPMAG}{mapping magnitude} +%% \SetKwData{MAPANGLE}{mapping angle} +%% \SetKwData{MAPMUX}{mapping mux} +%% \SetKwData{MAPSUBFLOOR}{mapping submap floor} +%% \SetKwData{MAPSUBRES}{mapping submap residue} +%% \ASSERT $(\text{\READ 16 unsigned bits}) = 0$\tcc*[r]{mapping type} +%% \eIf{$(\textnormal{\READ 1 unsigned bit}) = 1$}{ +%% $\text{\SUBMAPS} \leftarrow \text{(\READ 4 unsigned bits) + 1}$\; +%% }{ +%% $\text{\SUBMAPS} \leftarrow 1$\; +%% } +%% \eIf{$(\textnormal{\READ 1 unsigned bit}) = 1$}{ +%% $\text{\MAPCOUPSTEPS} \leftarrow (\text{\READ 8 unsigned bits}) + 1$\; +%% \For{$j \leftarrow 0$ \emph{\KwTo}$\text{\MAPCOUPSTEPS}_i$}{ +%% $\text{\MAPMAG}_{j} \leftarrow$ \READ $\ILOG(\CHANNELS - 1)$ unsigned bits\; +%% $\text{\MAPANGLE}_{j} \leftarrow$ \READ $\ILOG(\CHANNELS - 1)$ unsigned bits\; +%% %%FIXME - add assertion here +%% } +%% }{ +%% $\text{\MAPCOUPSTEPS} \leftarrow 0$\; +%% } +%% \ASSERT $(\text{\READ 2 unsigned bits}) = 0$\tcc*[r]{reserved} +%% \If{$\text{\SUBMAPS} > 1$}{ +%% \For{$j \leftarrow 0$ \emph{\KwTo}\CHANNELS}{ +%% $\text{\MAPMUX}_{j} \leftarrow$ \READ 4 unsigned bits\; +%% \ASSERT $\text{\MAPMUX}_{j} < \text{\SUBMAPS}_i$\; +%% } +%% } +%% \For{$j \leftarrow 0$ \emph{\KwTo}$\text{\SUBMAPS}$}{ +%% \SKIP 8 bits\tcc*[r]{unused time configuration placeholder} +%% $\text{\MAPSUBFLOOR}_{j} \leftarrow$ \READ 8 unsigned bits\; +%% \ASSERT $\text{\MAPSUBFLOOR}_{j} \leq \text{highest number floor configurated for bitstream}$\; +%% $\text{\MAPSUBRES}_{j} \leftarrow$ \READ 8 unsigned bits\; +%% \ASSERT $\text{\MAPSUBRES}_{j} \leq \text{highest number residue configured for bitstream}$\; +%% } +%% \Return $\left\lbrace\begin{tabular}{l} +%% \SUBMAPS \\ +%% \MAPCOUPSTEPS \\ +%% \MAPMAG \\ +%% \MAPANGLE \\ +%% \MAPMUX \\ +%% \MAPSUBFLOOR \\ +%% \MAPSUBRES \\ +%% \end{tabular}\right.$\; +%% \EALGORITHM +%% } + +%% \clearpage + +%% \subsubsection{Modes} +%% \label{vorbis:mode} +%% {\relsize{-1} +%% \ALGORITHM{Vorbis setup packet data}{Vorbis modes information} +%% \SetKwData{MODEBLOCKFLAG}{mode block flag} +%% \SetKwData{MODEWINDOWTYPE}{mode window type} +%% \SetKwData{MODETRANSFORMTYPE}{mode transform type} +%% \SetKwData{MODEMAPPING}{mode mapping} +%% $\text{\MODEBLOCKFLAG} \leftarrow$ \READ 1 unsigned bit\; +%% $\text{\MODEWINDOWTYPE} \leftarrow$ \READ 16 unsigned bits\; +%% \ASSERT $\text{\MODEWINDOWTYPE} = 0$\; +%% $\text{\MODETRANSFORMTYPE} \leftarrow$ \READ 16 unsigned bits\; +%% \ASSERT $\text{\MODETRANSFORMTYPE} = 0$\; +%% $\text{\MODEMAPPING} \leftarrow$ \READ 8 unsigned bits\; +%% \ASSERT $\text{\MODEMAPPING} \leq \text{highest number mapping in use}$\; +%% \Return $\left\lbrace\begin{tabular}{l} +%% \MODEBLOCKFLAG \\ +%% \MODEWINDOWTYPE \\ +%% \MODETRANSFORMTYPE \\ +%% \MODEMAPPING \\ +%% \end{tabular}\right.$\; +%% \EALGORITHM +%% } + +%% \clearpage + +%% \subsubsection{Codebooks} + +%% The \VAR{Codebooks} section of the setup packet stores +%% the Huffman lookup trees. + +%% \begin{figure}[h] +%% \includegraphics{figures/vorbis/codebooks.pdf} +%% \end{figure} +%% \par +%% \noindent +%% This section contains two optional sets of data, +%% a list of Huffman table entry lengths +%% and the lookup table values each entry length may resolve to. +%% \VAR{Total Entries} indicates the total number of entry lengths present. +%% These lengths may be stored in one of three ways: +%% unordered without sparse entries, unordered with sparse entries +%% and ordered. + +%% Unordered without sparse entries is the simplest method; +%% each entry length is stored as a 5 bit value, plus one. +%% Unordered with sparse entries is almost as simple; +%% each 5 bit length is prefixed by a single bit indicating +%% whether it is present or not. + +%% Ordered entries are more complicated. +%% The initial length is stored as a 5 bit value, plus one. +%% The number of entries with that length are stored as a series of +%% \VAR{Length Count} values in the bitstream, whose sizes +%% are determined by the number of remaining entries. +%% \begin{align*} +%% \text{Length Count}_i \text{ Size} &= \lfloor\log_2 (\text{Remaining Entries}_i)\rfloor + 1 +%% \intertext{For example, given a \VAR{Total Entries} value of 8 and an +%% \VAR{Initial Length} value of 2:} +%% \text{Length Count}_0 \text{ Size} &= \lfloor\log_2 8\rfloor + 1 = 4 \text{ bits} +%% \end{align*} +%% which means we read a 4 bit value to determine how many +%% \VAR{Entry Length} values are 2 bits long. +%% Therefore, if $\text{Length Count}_0$ is read from the bitstream as 2, +%% our \VAR{Entry Length} values are: +%% \begin{align*} +%% \text{Entry Length}_0 &= 2 \\ +%% \text{Entry Length}_1 &= 2 +%% \end{align*} +%% and the next set of lengths are 3 bits long. +%% Since we still have remaining entries, we read another \VAR{Length Count} +%% value of the length: +%% \begin{equation*} +%% \text{Length Count}_1 \text{ Size} = \lfloor\log_2 6\rfloor + 1 = 3 \text{ bits} +%% \end{equation*} +%% Thus, if $\text{Length Count}_1$ is also a value of 2 from the bitstream, +%% our \VAR{Entry Length} values are: +%% \begin{align*} +%% \text{Entry Length}_2 &= 3 \\ +%% \text{Entry Length}_3 &= 3 +%% \end{align*} +%% and the next set of lengths are 4 bits long. +%% We then read one more \VAR{Length Count} value: +%% \begin{equation*} +%% \text{Length Count}_2 \text{ Size} = \lfloor\log_2 4\rfloor + 1 = 3 \text{ bits} +%% \end{equation*} +%% Finally, if $\text{Length Count}_2$ is 4 from the bitstream, +%% our \VAR{Entry Length} values are: +%% \begin{align*} +%% \text{Entry Length}_4 &= 4 \\ +%% \text{Entry Length}_5 &= 4 \\ +%% \text{Entry Length}_6 &= 4 \\ +%% \text{Entry Length}_7 &= 4 +%% \end{align*} +%% At this point, we've assigned lengths to all the values +%% indicated by \VAR{Total Entries}, so the process is complete. + +%% \clearpage + +%% \subsubsection{Transforming Entry Lengths to Huffman Tree} + +%% Once a set of entry length values is parsed from the stream, +%% transforming them into a Huffman decision tree +%% is performed by taking our entry lengths from +%% $\text{Entry Length}_0$ to $\text{Entry Length}_{total - 1}$ +%% and placing them in the tree recursively such that the 0 bit +%% branches are populated first. +%% For example, given the parsed entry length values: +%% \begin{align*} +%% \text{Entry Length}_0 &= 2 & \text{Entry Length}_1 &= 4 & \text{Entry Length}_2 &= 4 & \text{Entry Length}_3 &= 4 \\ +%% \text{Entry Length}_4 &= 4 & \text{Entry Length}_5 &= 2 & \text{Entry Length}_6 &= 3 & \text{Entry Length}_7 &= 3 +%% \end{align*} +%% \par +%% \noindent +%% We first place $\text{Entry Length}_0$ into the Huffman tree as a 2 bit value. +%% Since the zero bits are filled first when adding a node 2 bits deep, +%% it initially looks like: + +%% \begin{figure}[h] +%% \includegraphics{figures/vorbis/huffman_example1.pdf} +%% \caption{$\text{Entry Length}_0$ placed with 2 bits} +%% \end{figure} +%% \par +%% \noindent +%% We then place $\text{Entry Length}_1$ into the Huffman tree as a 4 bit value. +%% Since the \texttt{0 0} branch is already populated by $\text{Entry Length}_0$, +%% we switch to the empty \texttt{0 1} branch as follows: +%% \begin{figure}[h] +%% \includegraphics{figures/vorbis/huffman_example2.pdf} +%% \caption{$\text{Entry Length}_1$ placed with 4 bits} +%% \end{figure} +%% \par +%% \noindent +%% The 4 bit $\text{Entry Length}_2$, $\text{Entry Length}_3$ and $\text{Entry Length}_4$ +%% values are placed similarly along the \texttt{0 1} branch: +%% \begin{figure}[h] +%% \includegraphics{figures/vorbis/huffman_example3.pdf} +%% \caption{$\text{Entry Length}_2$, $\text{Entry Length}_3$ and $\text{Entry Length}_4$ placed with 4 bits} +%% \end{figure} +%% \par +%% \noindent +%% Finally, the remaining three entries are populated along the \texttt{1} branch: +%% \begin{figure}[h] +%% \includegraphics{figures/vorbis/huffman_example4.pdf} +%% \caption{$\text{Entry Length}_5$, $\text{Entry Length}_6$ and $\text{Entry Length}_7$ are placed} +%% \end{figure} + +%% \subsubsection{The Lookup Table} + +%% The lookup table is only present if \VAR{Lookup Type} is 1 or 2. +%% A \VAR{Lookup Type} of 0 indicates no lookup table, while anything +%% greater than 2 is an error. + +%% \VAR{Minimum Value} and \VAR{Delta Value} are 32-bit floating point values +%% which can be parsed in the following way: +%% \begin{figure}[h] +%% \includegraphics{figures/vorbis/float32.pdf} +%% \end{figure} +%% \begin{equation*} +%% \text{Float} = +%% \begin{cases} +%% \text{Mantissa} \times 2 ^ {\text{Exponent} - 788} & \text{ if Sign = 0} \\ +%% -\text{Mantissa} \times 2 ^ {\text{Exponent} - 788} & \text{ if Sign = 1} +%% \end{cases} +%% \end{equation*} +%% \par +%% \VAR{Value Bits} indicates the size of each value in bits, plus one. +%% \VAR{Sequence P} is a 1 bit flag. +%% The total number of multiplicand values depends on whether \VAR{Lookup Type} +%% is 1 or 2. +%% \begin{align*} +%% \intertext{if \VAR{Lookup Type} = 1:} +%% \text{Multiplicand Count} &= \text{max}(\text{Int}) ^ \text{Dimensions} \text{ where } \text{Int} ^ \text{Dimensions} \leq \text{Total Entries} +%% \intertext{if \VAR{Lookup Type} = 2:} +%% \text{Multiplicand Count} &= \text{Dimensions} \times \text{Total Entries} +%% \end{align*} +%% \par +%% The \VAR{Multiplicand} values themselves are a list of unsigned integers, +%% each \VAR{Value Bits} (+ 1) bits large. -\clearpage \section{Channel Assignment} \begin{table}[h]
View file
audiotools-2.19.tar.gz/docs/reference/wavpack
Added
+(directory)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack-codec.template
Added
@@ -0,0 +1,3 @@ +<<file:header.tex>> +\include{wavpack} +<<file:footer.tex>>
View file
audiotools-2.18.tar.gz/docs/reference/wavpack.tex -> audiotools-2.19.tar.gz/docs/reference/wavpack.tex
Changed
@@ -17,14 +17,14 @@ \section{the WavPack File Stream} \begin{figure}[h] -\includegraphics{figures/wavpack/stream.pdf} +\includegraphics{wavpack/figures/stream.pdf} \end{figure} \clearpage \subsection{Block Header} \begin{figure}[h] -\includegraphics{figures/wavpack/block_header.pdf} +\includegraphics{wavpack/figures/block_header.pdf} \end{figure} \begin{table}[h] @@ -120,7 +120,7 @@ \subsection{Sub Block} \begin{figure}[h] -\includegraphics{figures/wavpack/subblock.pdf} +\includegraphics{wavpack/figures/subblock.pdf} \end{figure} \par \noindent @@ -160,4614 +160,8 @@ \clearpage -\section{WavPack Decoding} - -\ALGORITHM{a WavPack encoded file}{PCM samples} -\SetKwData{INDEX}{index} -\SetKwData{SAMPLES}{samples} -\SetKwData{TOTALSAMPLES}{total samples} -\SetKwData{FINALBLOCK}{final block} -\SetKwData{OUTPUTCHANNEL}{output channel} -\SetKwData{BLOCKCHANNEL}{block channel} -\SetKwData{BLOCKDATASIZE}{block data size} -\Repeat{block header's $\text{\INDEX} + \text{\SAMPLES} >= \text{\TOTALSAMPLES}$}{ - $c \leftarrow 0$\; - \Repeat{block header's $\text{\FINALBLOCK} = 1$}{ - \hyperref[wavpack:read_block_header]{read block header}\; - read \BLOCKDATASIZE bytes of block data\; - \hyperref[wavpack:decode_block]{decode block header and block data to 1 or 2 channels of PCM data}\; - \eIf{2 channels}{ - $\text{\OUTPUTCHANNEL}_c \leftarrow \text{\BLOCKCHANNEL}_0$\; - $\text{\OUTPUTCHANNEL}_{c + 1} \leftarrow \text{\BLOCKCHANNEL}_1$\; - $c \leftarrow c + 2$\; - }{ - $\text{\OUTPUTCHANNEL}_c \leftarrow \text{\BLOCKCHANNEL}_0$\; - $c \leftarrow c + 1$\; - } - } - update stream MD5 sum with \OUTPUTCHANNEL data\; - \Return \OUTPUTCHANNEL data\; -} -\BlankLine -\hyperref[wavpack:md5_sum]{attempt to read one additional block for MD5 sub block}\; -\If{MD5 sub block found}{ - \ASSERT sub block MD5 sum = stream MD5 sum\; -} -\EALGORITHM -\par -\noindent -For example, four blocks with the following attributes: -\begin{table}[h] -\begin{tabular}{rrr} -channel count & initial block & final block \\ -\hline -2 & 1 & 0 \\ -1 & 0 & 0 \\ -1 & 0 & 0 \\ -2 & 0 & 1 \\ -\end{tabular} -\end{table} -\par -\noindent -combine into 6 channels of PCM output as follows: -\begin{figure}[h] -\includegraphics{figures/wavpack/block_channels.pdf} -\end{figure} - -\clearpage - -\subsection{Reading Block Header} -\label{wavpack:read_block_header} -{\relsize{-1} -\ALGORITHM{a WavPack file stream}{block header fields} -\SetKwData{BLOCKID}{block ID} -\SetKwData{BLOCKSIZE}{block data size} -\SetKwData{VERSION}{version} -\SetKwData{TRACKNUMBER}{track number} -\SetKwData{INDEXNUMBER}{index number} -\SetKwData{TOTALSAMPLES}{total samples} -\SetKwData{BLOCKINDEX}{block index} -\SetKwData{BLOCKSAMPLES}{block samples} -\SetKwData{BITSPERSAMPLE}{bits per sample} -\SetKwData{MONOOUTPUT}{mono output} -\SetKwData{HYBRIDMODE}{hybrid mode} -\SetKwData{JOINTSTEREO}{joint stereo} -\SetKwData{CHANNELDECORR}{channel decorrelation} -\SetKwData{HYBRIDNOISESHAPING}{hybrid noise shaping} -\SetKwData{FLOATINGPOINTDATA}{floating point data} -\SetKwData{EXTENDEDSIZEINTEGERS}{extended size integers} -\SetKwData{HYBRIDCONTROLSBITRATE}{hybrid controls bitrate} -\SetKwData{HYBRIDNOISEBALANCED}{hybrid noise balanced} -\SetKwData{INITIALBLOCK}{initial block} -\SetKwData{FINALBLOCK}{final block} -\SetKwData{LEFTSHIFTDATA}{left shift data} -\SetKwData{MAXIMUMMAGNITUDE}{maximum magnitude} -\SetKwData{SAMPLERATE}{sample rate} -\SetKwData{USEIIR}{use IIR} -\SetKwData{FALSESTEREO}{false stereo} -\SetKwData{RESERVED}{reserved} -\SetKwData{CRC}{CRC} -\begin{tabular}{r>{$}c<{$}l} -\BLOCKID & \leftarrow & \READ 4 bytes\; \\ -& & \ASSERT $\text{\BLOCKID} = \texttt{"wvpk"}$\; \\ -\BLOCKSIZE & \leftarrow & (\READ 32 unsigned bits) - 24\; \\ -\VERSION & \leftarrow & \READ 16 unsigned bits\; \\ -\TRACKNUMBER & \leftarrow & \READ 8 unsigned bits\; \\ -\INDEXNUMBER & \leftarrow & \READ 8 unsigned bits\; \\ -\TOTALSAMPLES & \leftarrow & \READ 32 unsigned bits\; \\ -\BLOCKINDEX & \leftarrow & \READ 32 unsigned bits\; \\ -\BLOCKSAMPLES & \leftarrow & \READ 32 unsigned bits\; \\ -\BITSPERSAMPLE & \leftarrow & $(\text{\READ 2 unsigned bits} + 1) \times 8$\; \\ -\MONOOUTPUT & \leftarrow & \READ 1 unsigned bit\; \\ -\HYBRIDMODE & \leftarrow & \READ 1 unsigned bit\; \\ -\JOINTSTEREO & \leftarrow & \READ 1 unsigned bit\; \\ -\CHANNELDECORR & \leftarrow & \READ 1 unsigned bit\; \\ -\HYBRIDNOISESHAPING & \leftarrow & \READ 1 unsigned bit\; \\ -\FLOATINGPOINTDATA & \leftarrow & \READ 1 unsigned bit\; \\ -\EXTENDEDSIZEINTEGERS & \leftarrow & \READ 1 unsigned bit\; \\ -\HYBRIDCONTROLSBITRATE & \leftarrow & \READ 1 unsigned bit\; \\ -\HYBRIDNOISEBALANCED & \leftarrow & \READ 1 unsigned bit\; \\ -\INITIALBLOCK & \leftarrow & \READ 1 unsigned bit\; \\ -\FINALBLOCK & \leftarrow & \READ 1 unsigned bit\; \\ -\LEFTSHIFTDATA & \leftarrow & \READ 5 unsigned bits\; \\ -\MAXIMUMMAGNITUDE & \leftarrow & \READ 5 unsigned bits\; \\ -\textit{encoded sample rate} & \leftarrow & \READ 4 unsigned bits\; \\ -& & \SKIP 2 bits\; \\ -\USEIIR & \leftarrow & \READ 1 unsigned bit\; \\ -\FALSESTEREO & \leftarrow & \READ 1 unsigned bit\; \\ -\RESERVED & \leftarrow & \READ 1 unsigned bit\; \\ -& & \ASSERT $\text{\RESERVED} = 0$\; \\ -\CRC & \leftarrow & \READ 32 unsigned bits\; \\ -\end{tabular} -\EALGORITHM -} -{\relsize{-1} -\begin{tabular}{rr} - \textit{encoded sample rate} & sample rate \\ - \hline - \texttt{0} & 6000 Hz \\ - \texttt{1} & 8000 Hz \\ - \texttt{2} & 9600 Hz \\ - \texttt{3} & 11025 Hz \\ - \texttt{4} & 12000 Hz \\ - \texttt{5} & 16000 Hz \\ - \texttt{6} & 22050 Hz \\ - \texttt{7} & 24000 Hz \\ - \texttt{8} & 32000 Hz \\ - \texttt{9} & 44100 Hz \\ - \texttt{10} & 48000 Hz \\ - \texttt{11} & 64000 Hz \\ - \texttt{12} & 88200 Hz \\ - \texttt{13} & 96000 Hz \\ - \texttt{14} & 192000 Hz \\ - \texttt{15} & reserved \\ -\end{tabular} -} - -\clearpage - -\subsubsection{Reading Block Header Example} -\includegraphics{figures/wavpack/block_header_parse.pdf} - -%% \clearpage - -%% \begin{table}[h] -%% \begin{tabular}{rrl} -%% field & value & meaning \\ -%% \hline -%% \textbf{block ID} & \texttt{0x6B707677} & \texttt{"wvpk"} \\ -%% \textbf{block data size} & \texttt{0x000000B2} & $178 - 24 = 154$ bytes \\ -%% \textbf{version} & \texttt{0x0407} \\ -%% \textbf{track number} & \texttt{0} \\ -%% \textbf{index number} & \texttt{0} \\ -%% \textbf{total samples} & \texttt{0x00000019} & 25 PCM frames \\ -%% \textbf{block index} & \texttt{0x00000000} & 0 PCM frames \\ -%% \textbf{block samples} & \texttt{0x00000019} & 25 PCM frames \\ -%% \textbf{bits per sample} & \texttt{1} & 16 bits per sample \\ -%% \textbf{mono output} & \texttt{0} & 2 channel block \\ -%% \textbf{hybrid mode} & \texttt{0} & lossless data \\ -%% \textbf{joint stereo} & \texttt{1} & channels stored in joint stereo \\ -%% \textbf{channel decorrelation} & \texttt{1} & cross-channel decorrelation used \\ -%% \textbf{hybrid noise shaping} & \texttt{0} \\ -%% \textbf{floating point data} & \texttt{0} & samples stored as integers \\ -%% \textbf{extended size integers} & \texttt{0} \\ -%% \textbf{hybrid controls bitrate} & \texttt{0} \\ -%% \textbf{hybrid noise balanced} & \texttt{0} \\ -%% \textbf{initial block} & \texttt{1} & is initial block in sequence \\ -%% \textbf{final block} & \texttt{1} & is final block in sequence \\ -%% \textbf{left shift data} & \texttt{0} \\ -%% \textbf{maximum magnitude} & \texttt{15} & 16 bits per sample output \\ -%% \textbf{sample rate} & \texttt{9} & 44100 Hz \\ -%% \textbf{reserved} & \texttt{0} \\ -%% \textbf{use IIR} & \texttt{0} \\ -%% \textbf{false stereo} & \texttt{0} & both channels are not identical \\ -%% \textbf{reserved} & \texttt{0} \\ -%% \textbf{CRC} & \texttt{0x22D25AD7} \\ -%% \end{tabular} -%% \end{table} - -\clearpage - -\subsection{Block Decoding} -\label{wavpack:decode_block} -{\relsize{-1} -\ALGORITHM{block header, block data}{1 or 2 channels of PCM sample data} -\SetKwData{METAFUNC}{metadata function} -\SetKwData{NONDECODER}{nondecoder data} -\SetKwData{ONELESS}{actual size 1 less} -\SetKwData{BLOCKDATA}{block data size} -\SetKwData{SUBBLOCKSIZE}{sub block size} -\SetKwData{TERMS}{decorrelation terms} -\SetKwData{DELTAS}{decorrelation deltas} -\SetKwData{WEIGHTS}{decorrelation weights} -\SetKwData{SAMPLES}{decorrelation samples} -\SetKwData{ENTROPIES}{entropies} -\SetKwData{RESIDUALS}{residuals} -\SetKwData{ZEROES}{zero bits} -\SetKwData{ONES}{one bits} -\SetKwData{DUPES}{duplicate bits} -\SetKw{AND}{and} -\tcc{read decoding parameters from sub blocks} -\While{$\text{\BLOCKDATA} > 0$}{ - \tcc{read sub block header and data} - \METAFUNC $\leftarrow$ \READ 5 unsigned bits\; - \NONDECODER $\leftarrow$ \READ 1 unsigned bit\; - \ONELESS $\leftarrow$ \READ 1 unsigned bit\; - \textit{large sub block} $\leftarrow$ \READ 1 unsigned bit\; - \eIf{$\text{large sub block} = 0$}{ - \SUBBLOCKSIZE $\leftarrow$ \READ 8 unsigned bits\; - }{ - \SUBBLOCKSIZE $\leftarrow$ \READ 24 unsigned bits\; - } - \eIf{$\ONELESS = 0$}{ - read $(\SUBBLOCKSIZE \times 2)$ bytes of sub block data\; - }{ - read $(\SUBBLOCKSIZE \times 2) - 1$ bytes of sub block data\; - \SKIP 1 byte\; - } - \BlankLine - \tcc{decode audio-related sub blocks} - \If{$\text{\NONDECODER} = 0$}{ - \Switch{\METAFUNC}{ - \uCase{2}{ - $\TERMS, \DELTAS \leftarrow$ \hyperref[wavpack:decode_decorrelation_terms]{decode decorrelation terms sub block}\; - } - \uCase{3}{ - \ASSERT \TERMS have been read\; - $\WEIGHTS \leftarrow$ \hyperref[wavpack:decode_decorrelation_weights]{decode decorrelation weights sub block}\; - } - \uCase{4}{ - \ASSERT \TERMS have been read\; - $\SAMPLES \leftarrow$ \hyperref[wavpack:decode_decorrelation_samples]{decode decorrelation samples sub block}\; - } - \uCase{5}{ - $\ENTROPIES \leftarrow$ \hyperref[wavpack:decode_entropy_variables]{decode entropy variables sub block}\; - } - \uCase{9}{ - $\ZEROES, \ONES, \DUPES \leftarrow$ \hyperref[wavpack:decode_extended_integers]{decode extended integers sub block}\; - } - \Case{10}{ - \ASSERT \ENTROPIES have been read\; - $\RESIDUALS \leftarrow$ \hyperref[wavpack:decode_bitstream]{decode WavPack bitstream sub block}\; - } - } - } - \eIf{$\text{large sub block} = 0$}{ - $\BLOCKDATA \leftarrow \BLOCKDATA - (2 + 2 \times \text{\SUBBLOCKSIZE})$ - }{ - $\BLOCKDATA \leftarrow \BLOCKDATA - (4 + 2 \times \text{\SUBBLOCKSIZE})$ - } -} -\If{\TERMS have been read}{ - \ASSERT \WEIGHTS, \SAMPLES have been read\; -} -\ASSERT \RESIDUALS have been read\; -\EALGORITHM -} - -\clearpage - -{\relsize{-1} -\begin{algorithm}[H] -\SetKwData{TERMS}{decorrelation terms} -\SetKwData{MONOOUTPUT}{mono output} -\SetKwData{FALSESTEREO}{false stereo} -\SetKwData{JOINTSTEREO}{joint stereo} -\SetKwData{EXTENDEDINTS}{extended integers} -\SetKwData{RESIDUALS}{residuals} -\SetKwData{DECORRELATED}{decorrelated} -\SetKwData{LEFTRIGHT}{left/right} -\SetKwData{UNSHIFTED}{unshifted} -\SetKwFunction{LEN}{len} -\SetKwFunction{UNDOEXTENDEDINTS}{undo\_extended\_integers} -\SetKw{AND}{and} -\DontPrintSemicolon -\tcc{build PCM data from decoding parameters} -\eIf(\tcc*[f]{2 channels of output}){$\text{\MONOOUTPUT} = 0$ \AND $\text{\FALSESTEREO} = 0$}{ - \eIf{\TERMS have been read \AND $\LEN(\TERMS) > 0$}{ - $\text{\DECORRELATED}_0/\text{\DECORRELATED}_1 \leftarrow$ \hyperref[wavpack:decorrelate_channels]{decorrelate $\text{\RESIDUALS}_0/\text{\RESIDUALS}_1$}\; - }{ - $\text{\DECORRELATED}_0/\text{\DECORRELATED}_1 \leftarrow \text{\RESIDUALS}_0/\text{\RESIDUALS}_1$\; - } - \eIf{$\text{\JOINTSTEREO} = 1$}{ - $\LEFTRIGHT \leftarrow$ \hyperref[wavpack:undo_joint_stereo]{undo $\text{\DECORRELATED}_0/\text{\DECORRELATED}_1$ channels joint stereo}\; - }{ - $\LEFTRIGHT \leftarrow \text{\DECORRELATED}_0/\text{\DECORRELATED}_1$\; - } - \hyperref[wavpack:verify_crc]{verify CRC of $\LEFTRIGHT$ against CRC in block header}\; - \eIf{extended integers have been read}{ - $\text{\UNSHIFTED}_0/\text{\UNSHIFTED}_1 \leftarrow$ \hyperref[wavpack:undo_extended_integers]{undo \LEFTRIGHT channels extended integers}\; - }{ - $\text{\UNSHIFTED}_0/\text{\UNSHIFTED}_1 \leftarrow \LEFTRIGHT$ - } - \Return $\text{\UNSHIFTED}_0$ and $\text{\UNSHIFTED}_1$\; -}(\tcc*[f]{1 or 2 channels of output}){ - \eIf{\TERMS have been read \AND $\LEN(\TERMS) > 0$}{ - $\text{\DECORRELATED}_0 \leftarrow$ \hyperref[wavpack:decorrelate_channels]{decorrelate $\text{\RESIDUALS}_0$}\; - }{ - $\text{\DECORRELATED}_0 \leftarrow \text{\RESIDUALS}_0$\; - } -\hyperref[wavpack:verify_crc]{ verify CRC of $\text{\DECORRELATED}_0$ against CRC in block header}\; - \eIf{extended integers have been read}{ - $\text{\UNSHIFTED}_0 \leftarrow$ \hyperref[wavpack:undo_extended_integers]{undo $\text{\DECORRELATED}_0$ channel extended integers}\; - }{ - $\text{\UNSHIFTED}_0 \leftarrow \text{\DECORRELATED}_0$\; - } - \eIf{$\text{\FALSESTEREO} = 0$}{ - \Return $\text{\UNSHIFTED}_0$\; - }{ - \Return $\text{\UNSHIFTED}_0$ and $\text{\UNSHIFTED}_0$\tcc*[r]{duplicate channel} - } -} -\end{algorithm} -} -\begin{figure}[h] - \includegraphics{figures/wavpack/typical_block.pdf} -\end{figure} - -%% \clearpage - -%% \subsection{Reading Sub Block Header} -%% \ALGORITHM{block data}{metadata function integer, nondecoder data flag, \VAR{actual size 1 less} flag, sub block size} -%% \SetKwData{METAFUNC}{metadata function} -%% \SetKwData{NONDECODER}{nondecoder data} -%% \SetKwData{SUBBLOCKSIZE}{sub block size} -%% \SetKwData{ACTUALSIZEONELESS}{actual size 1 less} -%% \METAFUNC $\leftarrow$ \READ 5 unsigned bits\; -%% \NONDECODER $\leftarrow$ \READ 1 unsigned bit\; -%% \ACTUALSIZEONELESS $\leftarrow$ \READ 1 unsigned bit\; -%% \textit{large sub block} $\leftarrow$ \READ 1 unsigned bit\; -%% \eIf{$\text{large sub block} = 0$}{ -%% \SUBBLOCKSIZE $\leftarrow$ (\READ 8 unsigned bits) $\times 2$\; -%% }{ -%% \SUBBLOCKSIZE $\leftarrow$ (\READ 24 unsigned bits) $\times 2$\; -%% } -%% \Return (\METAFUNC , \NONDECODER , \ACTUALSIZEONELESS , \SUBBLOCKSIZE)\; -%% \EALGORITHM - -%% \subsubsection{Reading Sub Block Header Example} -%% \begin{figure}[h] -%% \includegraphics{figures/wavpack/subblock_parse.pdf} -%% \end{figure} -%% \begin{table}[h] -%% \begin{tabular}{r>{$}c<{$}l} -%% metadata function & \leftarrow & 2 \\ -%% nondecoder data & \leftarrow & 0 \\ -%% actual size 1 less & \leftarrow & 1 \\ -%% large sub block & \leftarrow & 0 \\ -%% sub block size & \leftarrow & $3 \times 2 = 6$ bytes\; -%% \end{tabular} -%% \end{table} -%% \par -%% \noindent -%% Note that although the total length of the sub block is -%% 8 bytes (2 bytes for the header plus the 6 bytes indicated by -%% \VAR{sub block size}), -%% \VAR{actual size 1 less} indicates that only the first 5 bytes -%% contain actual data and the final byte is padding. - -%% \clearpage - -%% \subsection{Reading Decoding Parameters} - -%% {\relsize{-1} -%% \ALGORITHM{\VAR{metadata function}, \VAR{nondecoder data}, \VAR{actual size 1 less}, \VAR{sub block size} from sub block header; block header}{parameters required for block decoding} -%% \SetKwData{METAFUNC}{metadata function} -%% \SetKwData{NONDECODER}{nondecoder data} -%% \eIf{$\text{\NONDECODER} = 0$}{ -%% \Switch{\METAFUNC}{ -%% \uCase{2}{ -%% read decorrelation terms and deltas from decorrelation terms sub block\; -%% \Return list of signed decorrelation terms and list of unsigned deltas\; -%% } -%% \uCase{3}{ -%% read decorrelation weights from decorrelation weights sub block\; -%% \Return signed decorrelation weight per channel per decorrelation term\; -%% } -%% \uCase{4}{ -%% read decorrelation samples from decorrelation samples sub block\; -%% \Return list of signed decorrelation samples per channel per decorrelation term\; -%% } -%% \uCase{5}{ -%% read 2 lists of 3 signed entropy variables\; -%% \Return 2 lists of signed entropy variables\; -%% } -%% \uCase{9}{ -%% read zero bits, one bits and duplicate bits from extended integers sub block\; -%% \Return zero bits, one bits, duplicate bits values\; -%% } -%% \uCase{10}{ -%% read WavPack bitstream\; -%% \Return list of signed residual values per channel\; -%% } -%% \Other{ -%% skip sub block\; -%% } -%% } -%% }{ -%% skip sub block\; -%% } -%% \EALGORITHM -%% } - -%% \subsubsection{The Decoding Parameters} -%% These parameters will be populated by sub blocks during decoding. -%% Once all the sub blocks have been read, -%% they will be used to transform residual data -%% into 1 or 2 channels of PCM data. -%% \begin{description} -%% \item[decorrelation terms] one signed integer from -3 to 18 per decorrelation pass -%% \item[decorrelation deltas] one unsigned integer per decorrelation pass -%% \item[decorrelation weights] one signed integer per decorrelation pass per channel -%% \item[decorrelation samples] one list of signed integers per decorrelation pass per channel -%% \item[entropy variables] two lists of three signed integers -%% \item[zero bits/one bits/duplicate bits] three unsigned integers indicating extended integers -%% \item[residuals] one list of signed integers per channel -%% \end{description} - -%% \clearpage - -%% \subsubsection{Decorrelation Pass Parameters} -%% The number of terms in the \VAR{decorrelation terms} sub block -%% determines how many decorrelation passes will run over the -%% block's residual data. -%% Decorrelation weight and decorrelation samples parameters -%% for those passes will be delivered in subsequent sub blocks. -%% \par -%% For example, given a 1 channel block -%% containing a sub block with 5 decorrelation terms, -%% decorrelation pass parameters may be laid out as follows: -%% \begin{figure}[h] -%% \includegraphics{figures/wavpack/decoding_params.pdf} -%% \end{figure} -%% \par -%% \noindent -%% Note that although the parameters are delivered in decrementing order -%% ($\text{term}_4$ to $\text{term}_0$), -%% the decorrelation passes are applied in incrementing order -%% ($\text{term}_0$ to $\text{term}_4$). - -\clearpage - -\subsection{Decode Decorrelation Terms} -\label{wavpack:decode_decorrelation_terms} -\ALGORITHM{\VAR{actual size 1 less} and \VAR{sub block size} values from sub block header, sub block data}{a list of signed decorrelation term integers, a list of unsigned decorrelation delta integers\footnote{$\text{term}_p$ and $\text{delta}_p$ indicate the term and delta values for decorrelation pass $p$}} -\SetKwData{PASSES}{passes} -\SetKwData{SUBBLOCKSIZE}{sub block size} -\SetKwData{ACTUALSIZEONELESS}{actual size 1 less} -\SetKwData{TERM}{term} -\SetKwData{DELTA}{delta} -\SetKw{OR}{or} -\SetKw{KwDownTo}{downto} -\eIf{$\text{\ACTUALSIZEONELESS} = 0$}{ - \PASSES $\leftarrow \text{\SUBBLOCKSIZE} \times 2$\; -}{ - \PASSES $\leftarrow \text{\SUBBLOCKSIZE} \times 2 - 1$\; -} -\ASSERT $\text{\PASSES} \leq 16$\; -\BlankLine -\For(\tcc*[f]{populate in reverse order}){$p \leftarrow \PASSES$ \emph{\KwDownTo}0}{ - $\text{\TERM}_p \leftarrow$ (\READ 5 unsigned bits) - 5\; - \ASSERT $\text{\TERM}_p$ \IN \texttt{[-3, -2, -1, 1, 2, 3, 4, 5, 6, 7, 8, 17, 18]} - \BlankLine - $\text{\DELTA}_p \leftarrow$ \READ 3 unsigned bits\; -} -\Return decorrelation \TERM and decorrelation \DELTA lists\; -\EALGORITHM - -\begin{figure}[h] - \includegraphics{figures/wavpack/decorr_terms.pdf} -\end{figure} - -\clearpage - -\subsubsection{Reading Decorrelation Terms Example} - -\begin{figure}[h] -\includegraphics{figures/wavpack/terms_parse.pdf} -\end{figure} -\begin{center} -{\renewcommand{\arraystretch}{1.25} -\begin{tabular}{>{$}r<{$}>{$}c<{$}>{$}r<{$}|>{$}r<{$}>{$}r<{$}>{$}r<{$}} -\text{decorrelation term}_4 & \leftarrow & 23 - 5 = 18 & -\text{decorrelation delta}_4 & \leftarrow & 2 \\ -\text{decorrelation term}_3 & \leftarrow & 23 - 5 = 18 & -\text{decorrelation delta}_3 & \leftarrow & 2 \\ -\text{decorrelation term}_2 & \leftarrow & 7 - 5 = 2 & -\text{decorrelation delta}_2 & \leftarrow & 2 \\ -\text{decorrelation term}_1 & \leftarrow & 22 - 5 = 17 & -\text{decorrelation delta}_1 & \leftarrow & 2 \\ -\text{decorrelation term}_0 & \leftarrow & 8 - 5 = 3 & -\text{decorrelation delta}_0 & \leftarrow & 2 \\ -\end{tabular} -\renewcommand{\arraystretch}{1.0} -} -\end{center} - -\clearpage - -\subsection{Decode Decorrelation Weights} -\label{wavpack:decode_decorrelation_weights} -{\relsize{-1} -\ALGORITHM{\VAR{mono output} and \VAR{false stereo} from block header, decorrelation terms count\footnote{from the decorrelation terms sub block, which must be read prior to this sub block}, \VAR{actual size 1 less} and \VAR{sub block size} values from sub block header, sub block data}{a list of signed weight integers per channel\footnote{$\text{weight}_{p~c}$ indicates weight value decorrelation pass $p$, channel $c$}} -\SetKwData{MONO}{mono output} -\SetKwData{FALSESTEREO}{false stereo} -\SetKwData{SUBBLOCKSIZE}{sub block size} -\SetKwData{ACTUALSIZEONELESS}{actual size 1 less} -\SetKwData{WEIGHTCOUNT}{weight count} -\SetKwData{TERMCOUNT}{term count} -\SetKwData{WEIGHTVAL}{weight value} -\SetKwData{WEIGHT}{weight} -\SetKw{AND}{and} -\tcc{read as many 8 bit weight values as possible} -\eIf{$\text{\ACTUALSIZEONELESS} = 0$}{ - \WEIGHTCOUNT $\leftarrow \text{\SUBBLOCKSIZE} \times 2$\; -}{ - \WEIGHTCOUNT $\leftarrow \text{\SUBBLOCKSIZE} \times 2 - 1$\; -} -\For{$i \leftarrow 0$ \emph{\KwTo}\WEIGHTCOUNT}{ - $\text{value}_i \leftarrow$ \READ 8 signed bits\; - $\text{\WEIGHTVAL}_i \leftarrow\begin{cases} -\text{value}_i \times 2 ^ 3 + \left\lfloor\frac{\text{value}_i \times 2 ^ 3 + 2 ^ 6}{2 ^ 7}\right\rfloor & \text{if }\text{value}_i > 0 \\ -0 & \text{if }\text{value}_i = 0 \\ -\text{value}_i \times 2 ^ 3 & \text{if }\text{value}_i < 0 -\end{cases}$\; -} -\BlankLine -\tcc{populate weight values by channel, in reverse order} -\eIf(\tcc*[f]{two channels}){$\text{\MONO} = 0$ \AND $\text{\FALSESTEREO} = 0$}{ - \ASSERT $\lfloor\WEIGHTCOUNT \div 2\rfloor \leq \TERMCOUNT$\; - \For{$i \leftarrow 0$ \emph{\KwTo}$\lfloor\WEIGHTCOUNT \div 2\rfloor$}{ - $\text{\WEIGHT}_{(\TERMCOUNT - i - 1)~0} \leftarrow \text{\WEIGHTVAL}_{i \times 2}$\; - $\text{\WEIGHT}_{(\TERMCOUNT - i - 1)~1} \leftarrow \text{\WEIGHTVAL}_{i \times 2 + 1}$\; - } - \For{$i \leftarrow \lfloor\WEIGHTCOUNT \div 2\rfloor$ \emph{\KwTo}\TERMCOUNT}{ - $\text{\WEIGHT}_{(\TERMCOUNT - i - 1)~0} \leftarrow 0$\; - $\text{\WEIGHT}_{(\TERMCOUNT - i - 1)~1} \leftarrow 0$\; - } - \Return a \WEIGHT value per pass, per channel\; -}(\tcc*[f]{one channel}){ - \ASSERT $\WEIGHTCOUNT \leq \TERMCOUNT$\; - \For{$i \leftarrow 0$ \emph{\KwTo}\WEIGHTCOUNT}{ - $\text{\WEIGHT}_{(\TERMCOUNT - i - 1)~0} \leftarrow \text{\WEIGHTVAL}_{i}$\; - } - \For{$i \leftarrow \WEIGHTCOUNT$ \emph{\KwTo}\TERMCOUNT}{ - $\text{\WEIGHT}_{(\TERMCOUNT - i - 1)~0} \leftarrow 0$\; - } - \Return a \WEIGHT value per pass\; -} -\EALGORITHM -} -\begin{figure}[h] - \includegraphics{figures/wavpack/decorr_weights.pdf} -\end{figure} - -\clearpage - -\subsubsection{Reading Decorrelation Weights Example} -Given a 2 channel block containing 5 decorrelation terms: -\begin{figure}[h] -\includegraphics{figures/wavpack/decorr_weights_parse.pdf} -\end{figure} -\begin{center} -{\renewcommand{\arraystretch}{1.25} -\begin{tabular}{r|r|>{$}r<{$}} -$i$ & $\text{value}_i$ & \text{weight value}_i \\ -\hline -0 & 6 & 6 \times 2 ^ 3 + \lfloor(6 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 48 \\ -1 & 6 & 6 \times 2 ^ 3 + \lfloor(6 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 48 \\ -2 & 6 & 6 \times 2 ^ 3 + \lfloor(6 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 48 \\ -3 & 6 & 6 \times 2 ^ 3 + \lfloor(6 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 48 \\ -4 & 4 & 4 \times 2 ^ 3 + \lfloor(4 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 32 \\ -5 & 4 & 4 \times 2 ^ 3 + \lfloor(4 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 32 \\ -6 & 6 & 6 \times 2 ^ 3 + \lfloor(6 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 48 \\ -7 & 6 & 6 \times 2 ^ 3 + \lfloor(6 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 48 \\ -8 & 2 & 2 \times 2 ^ 3 + \lfloor(2 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 16 \\ -9 & 3 & 3 \times 2 ^ 3 + \lfloor(3 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 24 \\ -\end{tabular} -\renewcommand{\arraystretch}{1.0} -} -\end{center} -\begin{center} -\begin{tabular}{>{$}r<{$}||>{$}r<{$}} -\text{weight}_{4~0} = \text{weight value}_0 = 48 & -\text{weight}_{4~1} = \text{weight value}_1 = 48 \\ -\text{weight}_{3~0} = \text{weight value}_2 = 48 & -\text{weight}_{3~1} = \text{weight value}_3 = 48 \\ -\text{weight}_{2~0} = \text{weight value}_4 = 32 & -\text{weight}_{2~1} = \text{weight value}_5 = 32 \\ -\text{weight}_{1~0} = \text{weight value}_6 = 48 & -\text{weight}_{1~1} = \text{weight value}_7 = 48 \\ -\text{weight}_{0~0} = \text{weight value}_8 = 16 & -\text{weight}_{0~1} = \text{weight value}_9 = 24 \\ -\end{tabular} -\end{center} - -\clearpage - -\subsection{Decoding Decorrelation Samples} -\label{wavpack:decode_decorrelation_samples} -{\relsize{-2} -\ALGORITHM{\VAR{mono output} and \VAR{false stereo} from block header, decorrelation terms, sub block size and data}{a list of signed decorrelation sample lists per channel per decorrelation term\footnote{\relsize{-1}$\text{sample}_{p~c~s}$ indicates the $s$th sample of decorrelation pass $p$ for channel $c$}} -\SetKwData{MONO}{mono output} -\SetKwData{FALSESTEREO}{false stereo} -\SetKwData{CHANNELS}{channel count} -\SetKwData{SAMPLE}{sample} -\SetKwData{TOTALSAMPLES}{sample count} -\SetKwData{TERMCOUNT}{term count} -\SetKwData{TERM}{term} -\SetKwFunction{EXP}{wv\_exp2} -\SetKw{KwDownTo}{downto} -\SetKw{AND}{and} -\eIf{$(\MONO = 0)$ \AND $(\FALSESTEREO = 0)$}{ - $\CHANNELS \leftarrow 2$\; -}{ - $\CHANNELS \leftarrow 1$\; -} -\For{$p \leftarrow \TERMCOUNT$ \emph{\KwDownTo}0}{ - \uIf(\tcc*[f]{2 samples per channel}){$17 \leq \text{\TERM}_p \leq 18$}{ - \eIf{$\text{sub block bytes remaining} \geq (\CHANNELS \times 4)$}{ - \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELS}{ - $\text{\SAMPLE}_{p~c~0} \leftarrow \text{read \EXP value}$\; - $\text{\SAMPLE}_{p~c~1} \leftarrow \text{read \EXP value}$\; - } - }{ - \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELS}{ - $\text{\SAMPLE}_{p~c} \leftarrow \texttt{[0, 0]}$\; - } - } - } - \uElseIf(\tcc*[f]{"term" samples per channel}){$1 \leq \text{\TERM}_p \leq 8$}{ - \eIf{$\text{sub block bytes remaining} \geq (\CHANNELS \times \text{\TERM}_p \times 2)$}{ - \For{$s \leftarrow 0$ \emph{\KwTo}$\text{\TERM}_p$}{ - \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELS}{ - $\text{\SAMPLE}_{p~c~s} \leftarrow \text{read \EXP value}$\; - } - } - }{ - \For{$s \leftarrow 0$ \emph{\KwTo}$\text{\TERM}_p$}{ - \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELS}{ - $\text{\SAMPLE}_{p~c~s} \leftarrow 0$\; - } - } - } - } - \ElseIf(\tcc*[f]{1 sample per channel}){$-3 \leq \text{\TERM}_p \leq -1$}{ - \eIf{$\text{sub block bytes remaining} \geq 4$}{ - $\text{\SAMPLE}_{p~0~0} \leftarrow \text{read \EXP value}$\; - $\text{\SAMPLE}_{p~1~0} \leftarrow \text{read \EXP value}$\; - }{ - $\text{\SAMPLE}_{p~0~0} \leftarrow 0$\; - $\text{\SAMPLE}_{p~1~0} \leftarrow 0$\; - } - } -} -\Return $\text{\SAMPLE}$ lists per pass, per channel\; -\EALGORITHM -} -\begin{figure}[h] - \includegraphics{figures/wavpack/decorr_samples.pdf} -\end{figure} - -\clearpage - -\subsubsection{Reading wv\_exp2 Values} -\label{wavpack_wvexp2} -{\relsize{-1} -\ALGORITHM{2 bytes of sub block data}{a signed value} -\SetKwFunction{EXP}{wexp} -$value \leftarrow$ \READ 16 signed bits\; -\BlankLine -\uIf{$-32768 \leq value < -2304$}{ - \Return $-(\EXP(-value \bmod{256}) \times 2 ^ {\lfloor -value \div 2 ^ 8 \rfloor - 9})$\; -} -\uElseIf{$-2304 \leq value < 0$}{ - \Return $-\lfloor \EXP(-value \bmod{256}) \div 2 ^ {9 - \lfloor -value \div 2 ^ 8 \rfloor} \rfloor$\; -} -\uElseIf{$0 \leq value \leq 2304$}{ - \Return $\lfloor \EXP(value \bmod{256}) \div 2 ^ {9 - \lfloor value \div 2 ^ 8 \rfloor} \rfloor$\; -} -\ElseIf{$2304 < value \leq 32767$}{ - \Return $\EXP(value \bmod{256}) \times 2 ^ {\lfloor value \div 2 ^ 8 \rfloor - 9}$\; -} -\EALGORITHM -} -\par -\noindent -where \texttt{wexp}(\textit{x}) is defined from the following table: -\vskip .10in -\par -\noindent -{\relsize{-3}\ttfamily -\begin{tabular}{| c | c | c | c | c | c | c | c | c | c | c | c | c | c | c | c | c |} -\hline -& 0x?0 & 0x?1 & 0x?2 & 0x?3 & 0x?4 & 0x?5 & 0x?6 & 0x?7 & 0x?8 & 0x?9 & 0x?A & 0x?B & 0x?C & 0x?D & 0x?E & 0x?F \\ -\hline -0x0? & 256 & 257 & 257 & 258 & 259 & 259 & 260 & 261 & 262 & 262 & 263 & 264 & 264 & 265 & 266 & 267 \\ -0x1? & 267 & 268 & 269 & 270 & 270 & 271 & 272 & 272 & 273 & 274 & 275 & 275 & 276 & 277 & 278 & 278 \\ -0x2? & 279 & 280 & 281 & 281 & 282 & 283 & 284 & 285 & 285 & 286 & 287 & 288 & 288 & 289 & 290 & 291 \\ -0x3? & 292 & 292 & 293 & 294 & 295 & 296 & 296 & 297 & 298 & 299 & 300 & 300 & 301 & 302 & 303 & 304 \\ -0x4? & 304 & 305 & 306 & 307 & 308 & 309 & 309 & 310 & 311 & 312 & 313 & 314 & 314 & 315 & 316 & 317 \\ -0x5? & 318 & 319 & 320 & 321 & 321 & 322 & 323 & 324 & 325 & 326 & 327 & 328 & 328 & 329 & 330 & 331 \\ -0x6? & 332 & 333 & 334 & 335 & 336 & 337 & 337 & 338 & 339 & 340 & 341 & 342 & 343 & 344 & 345 & 346 \\ -0x7? & 347 & 348 & 349 & 350 & 350 & 351 & 352 & 353 & 354 & 355 & 356 & 357 & 358 & 359 & 360 & 361 \\ -0x8? & 362 & 363 & 364 & 365 & 366 & 367 & 368 & 369 & 370 & 371 & 372 & 373 & 374 & 375 & 376 & 377 \\ -0x9? & 378 & 379 & 380 & 381 & 382 & 383 & 384 & 385 & 386 & 387 & 388 & 389 & 391 & 392 & 393 & 394 \\ -0xA? & 395 & 396 & 397 & 398 & 399 & 400 & 401 & 402 & 403 & 405 & 406 & 407 & 408 & 409 & 410 & 411 \\ -0xB? & 412 & 413 & 415 & 416 & 417 & 418 & 419 & 420 & 421 & 422 & 424 & 425 & 426 & 427 & 428 & 429 \\ -0xC? & 431 & 432 & 433 & 434 & 435 & 436 & 438 & 439 & 440 & 441 & 442 & 444 & 445 & 446 & 447 & 448 \\ -0xD? & 450 & 451 & 452 & 453 & 454 & 456 & 457 & 458 & 459 & 461 & 462 & 463 & 464 & 466 & 467 & 468 \\ -0xE? & 470 & 471 & 472 & 473 & 475 & 476 & 477 & 478 & 480 & 481 & 482 & 484 & 485 & 486 & 488 & 489 \\ -0xF? & 490 & 492 & 493 & 494 & 496 & 497 & 498 & 500 & 501 & 502 & 504 & 505 & 506 & 508 & 509 & 511 \\ -\hline -\end{tabular} -} - -\subsubsection{Reading Decorrelation Samples Example} -Given a stereo block containing the sub-block: -\begin{figure}[h] -\includegraphics{figures/wavpack/decorr_samples_parse.pdf} -\end{figure} -\begin{center} -{\relsize{-2} -\begin{tabular}{r|r|r|>{$}r<{$}|>{$}r<{$}} -$p$ & $\text{term}_p$ & $s$ & -\text{sample}_{p~0~s} & -\text{sample}_{p~1~s} \\ -\hline -4 & 18 & 0 & --\lfloor \texttt{wexp}(1841 \bmod{256}) \div 2 ^ {9 - \lfloor 1841 \div 2 ^ 8 \rfloor} \rfloor = -73 & -\lfloor \EXP(1487 \bmod{256}) \div 2 ^ {9 - \lfloor 1487 \div 2 ^ 8 \rfloor} \rfloor = 28 \\ -& & 1 & --\lfloor \EXP(1865 \bmod{256}) \div 2 ^ {9 - \lfloor 1865 \div 2 ^ 8 \rfloor} \rfloor = -78 & -\lfloor \EXP(1459 \bmod{256}) \div 2 ^ {9 - \lfloor 1459 \div 2 ^ 8 \rfloor} \rfloor = 26 \\ -\hline -3 & 18 & 0 & 0 & 0 \\ -& & 1 & 0 & 0 \\ -\hline -2 & 2 & 0 & 0 & 0 \\ -& & 1 & 0 & 0 \\ -\hline -1 & 17 & 0 & 0 & 0 \\ -& & 1 & 0 & 0 \\ -\hline -0 & 3 & 0 & 0 & 0 \\ -& & 1 & 0 & 0 \\ -& & 2 & 0 & 0 \\ -\hline -\end{tabular} -\renewcommand{\arraystretch}{1.0} -} -\end{center} - -\clearpage - -\subsection{Decoding Entropy Variables} -\label{wavpack:decode_entropy_variables} -{\relsize{-1} -\ALGORITHM{\VAR{mono output} and \VAR{false stereo} from block header, \VAR{sub block size}, \VAR{actual size 1 less}, sub block data}{2 lists of 3 signed entropy value integers\footnote{$\text{entropy}_{c~i}$ indicates the $i$th entropy of channel $c$}} -\SetKwData{ENTROPY}{entropy} -\SetKwFunction{EXP}{wv\_exp2} -\SetKwData{MONO}{mono output} -\SetKwData{FALSESTEREO}{false stereo} -\SetKwData{ONELESS}{actual size 1 less} -\SetKwData{SUBBLOCKSIZE}{sub block size} -\SetKw{AND}{and} -\ASSERT $\text{\ONELESS} = 0$\; -\eIf(\tcc*[f]{2 channels}){$\text{\MONO} = 0$ \AND $\text{\FALSESTEREO} = 0$}{ - \ASSERT $\text{\SUBBLOCKSIZE} = 6$\; - $\text{\ENTROPY}_{0~0} \leftarrow \text{read \EXP value}$\; - $\text{\ENTROPY}_{0~1} \leftarrow \text{read \EXP value}$\; - $\text{\ENTROPY}_{0~2} \leftarrow \text{read \EXP value}$\; - $\text{\ENTROPY}_{1~0} \leftarrow \text{read \EXP value}$\; - $\text{\ENTROPY}_{1~1} \leftarrow \text{read \EXP value}$\; - $\text{\ENTROPY}_{1~2} \leftarrow \text{read \EXP value}$\; -}(\tcc*[f]{1 channel}){ - \ASSERT $\text{\SUBBLOCKSIZE} = 3$\; - $\text{\ENTROPY}_{0~0} \leftarrow \text{read \EXP value}$\; - $\text{\ENTROPY}_{0~1} \leftarrow \text{read \EXP value}$\; - $\text{\ENTROPY}_{0~2} \leftarrow \text{read \EXP value}$\; - $\text{\ENTROPY}_{1} \leftarrow \texttt{[0, 0, 0]}$\; -} -\Return $\text{\ENTROPY}_0$ list and $\text{\ENTROPY}_1$ list\; -\EALGORITHM - -\begin{figure}[h] - \includegraphics{figures/wavpack/entropy_vars.pdf} -\end{figure} - -\clearpage - -\subsubsection{Reading Entropy Variables Example} -Given a 2 channel block containing the subframe: -\begin{figure}[h] -\includegraphics{figures/wavpack/entropy_vars_parse.pdf} -\end{figure} -\begin{center} -{\relsize{-1} -\renewcommand{\arraystretch}{1.25} -\begin{tabular}{r|>{$}r<{$}|>{$}r<{$}} -$i$ & \text{entropy}_{0~i} & \text{entropy}_{1~i} \\ -\hline -0 & -\lfloor \EXP(2018 \bmod{256}) \div 2 ^ {9 - \lfloor 2018 \div 2 ^ 8 \rfloor} \rfloor = 118 & -\lfloor \EXP(2018 \bmod{256}) \div 2 ^ {9 - \lfloor 2018 \div 2 ^ 8 \rfloor} \rfloor = 118 \\ -1 & -\lfloor \EXP(2203 \bmod{256}) \div 2 ^ {9 - \lfloor 2203 \div 2 ^ 8 \rfloor} \rfloor = 194 & -\lfloor \EXP(2166 \bmod{256}) \div 2 ^ {9 - \lfloor 2166 \div 2 ^ 8 \rfloor} \rfloor = 176 \\ -2 & -\EXP(2389 \bmod{256}) \times 2 ^ {\lfloor 2389 \div 2 ^ 8 \rfloor - 9} = 322 & -\lfloor \EXP(2234 \bmod{256}) \div 2 ^ {9 - \lfloor 2234 \div 2 ^ 8 \rfloor} \rfloor = 212 \\ -\end{tabular} -\renewcommand{\arraystretch}{1.0} -} -\end{center} - -\clearpage - -\subsection{Reading Bitstream} -\label{wavpack:decode_bitstream} -\ALGORITHM{\VAR{mono output}, \VAR{false stereo} and \VAR{block samples} from block header, -entropy values\footnote{from the entropy variables sub block, which must be read prior to this sub block}, sub block data}{a list of signed residual values per channel\footnote{$\text{residual}_{c~i}$ indicates the $i$th residual of channel $c$}} -\SetKwData{ENTROPY}{entropy} -\SetKwData{MONO}{mono output} -\SetKwData{FALSESTEREO}{false stereo} -\SetKwData{BLOCKSAMPLES}{block samples} -\SetKwData{CHANNELS}{channel count} -\SetKwData{UNDEFINED}{undefined} -\SetKwData{RESIDUAL}{residual} -\SetKwData{ZEROES}{zeroes} -\SetKw{AND}{and} -\eIf{$\text{\MONO} = 0$ \AND $\text{\FALSESTEREO} = 0$}{ - \CHANNELS $\leftarrow 2$\; -}{ - \CHANNELS $\leftarrow 1$\; -} -$u_{-1} \leftarrow \UNDEFINED$\; -$i \leftarrow 0$\; -\While{$i < (\BLOCKSAMPLES \times \CHANNELS)$}{ - \eIf{$(u_{i - 1} = \UNDEFINED)$ \AND - $(\text{\ENTROPY}_{0~0} < 2)$ \AND - $(\text{\ENTROPY}_{1~0} < 2)$}{ - \tcc{handle long run of 0 residuals} - $\ZEROES \leftarrow$ read modified Elias gamma code\; - \If{$\text{\ZEROES} > 0$}{ - \For{$j \leftarrow 0$ \emph{\KwTo}\ZEROES}{ - $\text{\RESIDUAL}_{(i \bmod \CHANNELS)~\lfloor i \div \CHANNELS \rfloor} \leftarrow $ 0\; - $i \leftarrow i + 1$\; - } - \BlankLine - $\text{\ENTROPY}_{0~0} \leftarrow \text{\ENTROPY}_{0~1} \leftarrow \text{\ENTROPY}_{0~2} \leftarrow 0$\; - $\text{\ENTROPY}_{1~0} \leftarrow \text{\ENTROPY}_{1~1} \leftarrow \text{\ENTROPY}_{1~2} \leftarrow 0$\; - } - \If{$i < (\BLOCKSAMPLES \times \CHANNELS)$}{ - $\text{\RESIDUAL}_{(i \bmod \CHANNELS)~\lfloor i \div \CHANNELS \rfloor} \leftarrow $ read residual value\; - $i \leftarrow i + 1$\; - } - }{ - $\text{\RESIDUAL}_{(i \bmod \CHANNELS)~\lfloor i \div \CHANNELS \rfloor} \leftarrow $ read residual value\; - $i \leftarrow i + 1$\; - } -} -\Return a list of signed \RESIDUAL values per channel\; -\EALGORITHM - -\subsubsection{Reading Modified Elias Gamma Code} -{\relsize{-1} - \ALGORITHM{the sub block bitstream}{an unsigned integer} - $t \leftarrow$ \UNARY with stop bit 0\; - \eIf{$t > 1$}{ - $p \leftarrow$ \READ $(t - 1)$ unsigned bits\; - \Return $2 ^ {(t - 1)} + p$\; - }{ - \Return $t$\; - } - \EALGORITHM -} - -\subsubsection{Reading Residual Value} -{\relsize{-1} -\ALGORITHM{sub block bitstream, previous residual's unary value $u_{i - 1}$, \VAR{entropy} values for channel $c$}{a signed residual value; new unary value $u_i$, updated \VAR{entropy} values for channel $c$} -\EALGORITHM -} - -\clearpage - -{\relsize{-1} -\begin{algorithm}[H] -\DontPrintSemicolon -\SetKw{READ}{read} -\SetKw{UNARY}{read unary} -\SetKwData{UNDEFINED}{undefined} -\SetKwData{ENTROPY}{entropy} -\SetKwData{RESIDUAL}{residual} -\uIf(\tcc*[f]{determine new "u" and "m" values}){$u_{i - 1} = \UNDEFINED$}{ - $u_i \leftarrow$ \UNARY with stop bit 0\; - \If{$u_i = 16$}{ - $c_i \leftarrow$ read modified Elias gamma code\; - $u_i \leftarrow u_i + c_i$\; - } - $m_i \leftarrow \lfloor u_i \div 2\rfloor$\; -} -\uElseIf{$u_{i - 1} \bmod 2 = 1$}{ - $u_i \leftarrow$ \UNARY with stop bit 0\; - \If{$u_i = 16$}{ - $c_i \leftarrow$ read modified Elias gamma code\; - $u_i \leftarrow u_i + c_i$\; - } - $m_i \leftarrow \lfloor u_i \div 2\rfloor + 1$\; -} -\ElseIf{$u_{i - 1} \bmod 2 = 0$}{ - $u_i \leftarrow \UNDEFINED$\; - $m_i \leftarrow 0$\; -} -\BlankLine -\Switch(\tcc*[f]{determine "base", "add" and new entropy values}){$m_i$}{ - \uCase{0}{ - $base \leftarrow 0$\; - $add \leftarrow \lfloor\text{\ENTROPY}_{c~0} \div 2 ^ 4\rfloor$\; - $\text{\ENTROPY}_{c~0} \leftarrow \text{\ENTROPY}_{c~0} - \lfloor(\text{\ENTROPY}_{c~0} + 126) \div 2 ^ 7\rfloor \times 2$\; - } - \uCase{1}{ - $base \leftarrow \lfloor\text{\ENTROPY}_{c~0} \div 2 ^ 4\rfloor + 1$\; - $add \leftarrow \lfloor\text{\ENTROPY}_{c~1} \div 2 ^ 4\rfloor$\; - $\text{\ENTROPY}_{c~0} \leftarrow \text{\ENTROPY}_{c~0} + \lfloor(\text{\ENTROPY}_{c~0} + 128) \div 2 ^ 7\rfloor \times 5$\; - $\text{\ENTROPY}_{c~1} \leftarrow \text{\ENTROPY}_{c~1} - \lfloor(\text{\ENTROPY}_{c~1} + 62) \div 2 ^ 6\rfloor \times 2$\; - } - \uCase{2}{ - $base \leftarrow (\lfloor\text{\ENTROPY}_{c~0} \div 2 ^ 4\rfloor + 1) + (\lfloor\text{\ENTROPY}_{c~1} \div 2 ^ 4\rfloor + 1)$\; - $add \leftarrow \lfloor\text{\ENTROPY}_{c~2} \div 2 ^ 4\rfloor$\; - $\text{\ENTROPY}_{c~0} \leftarrow \text{\ENTROPY}_{c~0} + \lfloor(\text{\ENTROPY}_{c~0} + 128) \div 2 ^ 7\rfloor \times 5$\; - $\text{\ENTROPY}_{c~1} \leftarrow \text{\ENTROPY}_{c~1} + \lfloor(\text{\ENTROPY}_{c~1} + 64) \div 2 ^ 6\rfloor \times 5$\; - $\text{\ENTROPY}_{c~2} \leftarrow \text{\ENTROPY}_{c~2} - \lfloor(\text{\ENTROPY}_{c~2} + 30) \div 2 ^ 5\rfloor \times 2$\; - } - \Other{ - $base \leftarrow (\lfloor\text{\ENTROPY}_{c~0} \div 2 ^ 4\rfloor + 1) + (\lfloor\text{\ENTROPY}_{c~1} \div 2 ^ 4\rfloor + 1) + ((\lfloor\text{\ENTROPY}_{c~2} \div 2 ^ 4\rfloor + 1) \times (m_i - 2))$\; - $add \leftarrow \lfloor\text{\ENTROPY}_{c~2} \div 2 ^ 4\rfloor$\; - $\text{\ENTROPY}_{c~0} \leftarrow \text{\ENTROPY}_{c~0} + \lfloor(\text{\ENTROPY}_{c~0} + 128) \div 2 ^ 7\rfloor \times 5$\; - $\text{\ENTROPY}_{c~1} \leftarrow \text{\ENTROPY}_{c~1} + \lfloor(\text{\ENTROPY}_{c~1} + 64) \div 2 ^ 6\rfloor \times 5$\; - $\text{\ENTROPY}_{c~2} \leftarrow \text{\ENTROPY}_{c~2} + \lfloor(\text{\ENTROPY}_{c~2} + 32) \div 2 ^ 5\rfloor \times 5$\; - } -} -\BlankLine -\eIf(\tcc*[f]{determine final residual value}){$add = 0$}{ - $unsigned \leftarrow base$\; -}{ - $p \leftarrow \lfloor\log_2(add)\rfloor$\; - $e \leftarrow 2 ^ {p + 1} - add - 1$\; - $r_i \leftarrow$ \READ $p$ unsigned bits\; - \eIf{$r \geq e$}{ - $b_i \leftarrow$ \READ 1 unsigned bit\; - $unsigned \leftarrow base + (r_i \times 2) - e + b_i$\; - }{ - $unsigned \leftarrow base + r_i$\; - } -} -\BlankLine -$sign \leftarrow$ \READ 1 unsigned bit\; -\eIf{$sign = 1$}{ - \Return $(-unsigned - 1)$ along with unary value $u_i$ and updated entropy values\; -}{ - \Return $unsigned$ along with unary value $u_i$ and updated entropy values\; -} -\EALGORITHM -} - -\clearpage - -\begin{figure}[h] - \includegraphics{figures/wavpack/residuals.pdf} -\end{figure} - -\clearpage - -\subsubsection{Residual Decoding Example} -\begin{figure}[h] -\includegraphics{figures/wavpack/residuals_parse.pdf} -\end{figure} - -\clearpage - -\begin{landscape} - -Given a 2 channel block with $\text{entropies}_0 = \texttt{[118, 194, 322]}$ and -$\text{entropies}_1 = \texttt{[118, 176, 212]}$: -\vskip .10in - -{\relsize{-2} -\renewcommand{\arraystretch}{1.5} -\begin{tabular}{|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|} -i & u_i & m_i & -\text{base} & \text{add} & \text{entropy}_{c~0} & \text{entropy}_{c~1} & \text{entropy}_{c~2} \\ -\hline -0 & -7 & -\lfloor 7 \div 2 \rfloor = 3 & -2 + \left\lfloor\frac{118}{2 ^ 4}\right\rfloor + \left\lfloor\frac{194}{2 ^ 4}\right\rfloor + \left(\left\lfloor\frac{322}{2 ^ 4}\right\rfloor \times 1\right) = 42 & \left\lfloor\frac{322}{2 ^ 4}\right\rfloor = 20 & 118 + 5 = 123 & 194 + 20 = 214 & 322 + 55 = 377 -\\ -1 & -3 & -\lfloor 3 \div 2 \rfloor + 1 = 2 & -2 + \left\lfloor\frac{118}{2 ^ 4}\right\rfloor + \left\lfloor\frac{176}{2 ^ 4}\right\rfloor = 20 & \left\lfloor\frac{212}{2 ^ 4}\right\rfloor = 13 & 118 + 5 = 123 & 176 + 15 = 191 & 212 - 35 = 198 -\\ -\hline -2 & -3 & -\lfloor 3 \div 2 \rfloor + 1 = 2 & -2 + \left\lfloor\frac{123}{2 ^ 4}\right\rfloor + \left\lfloor\frac{214}{2 ^ 4}\right\rfloor = 22 & \left\lfloor\frac{377}{2 ^ 4}\right\rfloor = 23 & 123 + 5 = 128 & 214 + 20 = 234 & 377 - 60 = 353 -\\ -3 & -3 & -\lfloor 3 \div 2 \rfloor + 1 = 2 & -2 + \left\lfloor\frac{123}{2 ^ 4}\right\rfloor + \left\lfloor\frac{191}{2 ^ 4}\right\rfloor = 20 & \left\lfloor\frac{198}{2 ^ 4}\right\rfloor = 12 & 123 + 5 = 128 & 191 + 15 = 206 & 198 - 35 = 184 -\\ -\hline -4 & -1 & -\lfloor 1 \div 2 \rfloor + 1 = 1 & -1 + \left\lfloor\frac{128}{2 ^ 4}\right\rfloor = 9 & \left\lfloor\frac{234}{2 ^ 4}\right\rfloor = 14 & 128 + 10 = 138 & 234 - 8 = 226 & 353 -\\ -5 & -4 & -\lfloor 4 \div 2 \rfloor + 1 = 3 & -2 + \left\lfloor\frac{128}{2 ^ 4}\right\rfloor + \left\lfloor\frac{206}{2 ^ 4}\right\rfloor + \left(\left\lfloor\frac{184}{2 ^ 4}\right\rfloor \times 1\right) = 34 & \left\lfloor\frac{184}{2 ^ 4}\right\rfloor = 11 & 128 + 10 = 138 & 206 + 20 = 226 & 184 + 30 = 214 -\\ -\hline -6 & -\textit{undefined} & -0 & -0 & \left\lfloor\frac{138}{2 ^ 4}\right\rfloor = 8 & 138 - 4 = 134 & 226 & 353 -\\ -7 & -5 & -\lfloor 5 \div 2 \rfloor = 2 & -2 + \left\lfloor\frac{138}{2 ^ 4}\right\rfloor + \left\lfloor\frac{226}{2 ^ 4}\right\rfloor = 24 & \left\lfloor\frac{214}{2 ^ 4}\right\rfloor = 13 & 138 + 10 = 148 & 226 + 20 = 246 & 214 - 35 = 200 -\\ -\hline -8 & -1 & -\lfloor 1 \div 2 \rfloor + 1 = 1 & -1 + \left\lfloor\frac{134}{2 ^ 4}\right\rfloor = 9 & \left\lfloor\frac{226}{2 ^ 4}\right\rfloor = 14 & 134 + 10 = 144 & 226 - 8 = 218 & 353 -\\ -9 & -3 & -\lfloor 3 \div 2 \rfloor + 1 = 2 & -2 + \left\lfloor\frac{148}{2 ^ 4}\right\rfloor + \left\lfloor\frac{246}{2 ^ 4}\right\rfloor = 26 & \left\lfloor\frac{200}{2 ^ 4}\right\rfloor = 12 & 148 + 10 = 158 & 246 + 20 = 266 & 200 - 35 = 186 -\\ -\hline -10 & -3 & -\lfloor 3 \div 2 \rfloor + 1 = 2 & -2 + \left\lfloor\frac{144}{2 ^ 4}\right\rfloor + \left\lfloor\frac{218}{2 ^ 4}\right\rfloor = 24 & \left\lfloor\frac{353}{2 ^ 4}\right\rfloor = 22 & 144 + 10 = 154 & 218 + 20 = 238 & 353 - 55 = 331 -\\ -11 & -3 & -\lfloor 3 \div 2 \rfloor + 1 = 2 & -2 + \left\lfloor\frac{158}{2 ^ 4}\right\rfloor + \left\lfloor\frac{266}{2 ^ 4}\right\rfloor = 27 & \left\lfloor\frac{186}{2 ^ 4}\right\rfloor = 11 & 158 + 10 = 168 & 266 + 25 = 291 & 186 - 30 = 174 -\\ -\hline -12 & -5 & -\lfloor 5 \div 2 \rfloor + 1 = 3 & -2 + \left\lfloor\frac{154}{2 ^ 4}\right\rfloor + \left\lfloor\frac{238}{2 ^ 4}\right\rfloor + \left(\left\lfloor\frac{331}{2 ^ 4}\right\rfloor \times 1\right) = 46 & \left\lfloor\frac{331}{2 ^ 4}\right\rfloor = 20 & 154 + 10 = 164 & 238 + 20 = 258 & 331 + 55 = 386 -\\ -13 & -1 & -\lfloor 1 \div 2 \rfloor + 1 = 1 & -1 + \left\lfloor\frac{168}{2 ^ 4}\right\rfloor = 11 & \left\lfloor\frac{291}{2 ^ 4}\right\rfloor = 18 & 168 + 10 = 178 & 291 - 10 = 281 & 174 -\\ -\hline -14 & -5 & -\lfloor 5 \div 2 \rfloor + 1 = 3 & -2 + \left\lfloor\frac{164}{2 ^ 4}\right\rfloor + \left\lfloor\frac{258}{2 ^ 4}\right\rfloor + \left(\left\lfloor\frac{386}{2 ^ 4}\right\rfloor \times 1\right) = 53 & \left\lfloor\frac{386}{2 ^ 4}\right\rfloor = 24 & 164 + 10 = 174 & 258 + 25 = 283 & 386 + 65 = 451 -\\ -15 & -1 & -\lfloor 1 \div 2 \rfloor + 1 = 1 & -1 + \left\lfloor\frac{178}{2 ^ 4}\right\rfloor = 12 & \left\lfloor\frac{281}{2 ^ 4}\right\rfloor = 17 & 178 + 10 = 188 & 281 - 10 = 271 & 174 -\\ -\hline -16 & -4 & -\lfloor 4 \div 2 \rfloor + 1 = 3 & -2 + \left\lfloor\frac{174}{2 ^ 4}\right\rfloor + \left\lfloor\frac{283}{2 ^ 4}\right\rfloor + \left(\left\lfloor\frac{451}{2 ^ 4}\right\rfloor \times 1\right) = 58 & \left\lfloor\frac{451}{2 ^ 4}\right\rfloor = 28 & 174 + 10 = 184 & 283 + 25 = 308 & 451 + 75 = 526 -\\ -17 & -\textit{undefined} & -0 & -0 & \left\lfloor\frac{188}{2 ^ 4}\right\rfloor = 11 & 188 - 4 = 184 & 271 & 174 -\\ -\hline -18 & -6 & -\lfloor 6 \div 2 \rfloor = 3 & -2 + \left\lfloor\frac{184}{2 ^ 4}\right\rfloor + \left\lfloor\frac{308}{2 ^ 4}\right\rfloor + \left(\left\lfloor\frac{526}{2 ^ 4}\right\rfloor \times 1\right) = 65 & \left\lfloor\frac{526}{2 ^ 4}\right\rfloor = 32 & 184 + 10 = 194 & 308 + 25 = 333 & 526 + 85 = 611 -\\ -19 & -\textit{undefined} & -0 & -0 & \left\lfloor\frac{184}{2 ^ 4}\right\rfloor = 11 & 184 - 4 = 180 & 271 & 174 -\\ -\hline -\end{tabular} -\renewcommand{\arraystretch}{1.0} -} - -\clearpage - -\begin{table}[h] -{\relsize{-2} -\renewcommand{\arraystretch}{1.5} -\begin{tabular}{|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|} -i & \text{base} & \text{add} & p & e & r_i & b_i & unsigned & sign_i & residual_i \\ -\hline -0 & -42 & 20 & -\lfloor\log_2(20)\rfloor = 4 & -2 ^ {4 + 1} - 20 - 1 = 11 & -14 & -1 & 42 + (14 \times 2) - 11 + 1 = 60 & -1 & -60 - 1 = -61 -\\ -1 & -20 & 13 & -\lfloor\log_2(13)\rfloor = 3 & -2 ^ {3 + 1} - 13 - 1 = 2 & -6 & -1 & 20 + (6 \times 2) - 2 + 1 = 31 & -0 & 31 -\\ -\hline -2 & -22 & 23 & -\lfloor\log_2(23)\rfloor = 4 & -2 ^ {4 + 1} - 23 - 1 = 8 & -9 & -0 & 22 + (9 \times 2) - 8 + 0 = 32 & -1 & -32 - 1 = -33 -\\ -3 & -20 & 12 & -\lfloor\log_2(12)\rfloor = 3 & -2 ^ {3 + 1} - 12 - 1 = 3 & -7 & -1 & 20 + (7 \times 2) - 3 + 1 = 32 & -0 & 32 -\\ -\hline -4 & -9 & 14 & -\lfloor\log_2(14)\rfloor = 3 & -2 ^ {3 + 1} - 14 - 1 = 1 & -4 & -1 & 9 + (4 \times 2) - 1 + 1 = 17 & -1 & -17 - 1 = -18 -\\ -5 & -34 & 11 & -\lfloor\log_2(11)\rfloor = 3 & -2 ^ {3 + 1} - 11 - 1 = 4 & -2 & - & 34 + 2 = 36 & -0 & 36 -\\ -\hline -6 & -0 & 8 & -\lfloor\log_2(8)\rfloor = 3 & -2 ^ {3 + 1} - 8 - 1 = 7 & -1 & - & 0 + 1 = 1 & -0 & 1 -\\ -7 & -24 & 13 & -\lfloor\log_2(13)\rfloor = 3 & -2 ^ {3 + 1} - 13 - 1 = 2 & -7 & -1 & 24 + (7 \times 2) - 2 + 1 = 37 & -0 & 37 -\\ -\hline -8 & -9 & 14 & -\lfloor\log_2(14)\rfloor = 3 & -2 ^ {3 + 1} - 14 - 1 = 1 & -6 & -0 & 9 + (6 \times 2) - 1 + 0 = 20 & -0 & 20 -\\ -9 & -26 & 12 & -\lfloor\log_2(12)\rfloor = 3 & -2 ^ {3 + 1} - 12 - 1 = 3 & -6 & -0 & 26 + (6 \times 2) - 3 + 0 = 35 & -0 & 35 -\\ -\hline -10 & -24 & 22 & -\lfloor\log_2(22)\rfloor = 4 & -2 ^ {4 + 1} - 22 - 1 = 9 & -10 & -0 & 24 + (10 \times 2) - 9 + 0 = 35 & -0 & 35 -\\ -11 & -27 & 11 & -\lfloor\log_2(11)\rfloor = 3 & -2 ^ {3 + 1} - 11 - 1 = 4 & -4 & -0 & 27 + (4 \times 2) - 4 + 0 = 31 & -0 & 31 -\\ -\hline -12 & -46 & 20 & -\lfloor\log_2(20)\rfloor = 4 & -2 ^ {4 + 1} - 20 - 1 = 11 & -4 & - & 46 + 4 = 50 & -0 & 50 -\\ -13 & -11 & 18 & -\lfloor\log_2(18)\rfloor = 4 & -2 ^ {4 + 1} - 18 - 1 = 13 & -13 & -1 & 11 + (13 \times 2) - 13 + 1 = 25 & -0 & 25 -\\ -\hline -14 & -53 & 24 & -\lfloor\log_2(24)\rfloor = 4 & -2 ^ {4 + 1} - 24 - 1 = 7 & -8 & -0 & 53 + (8 \times 2) - 7 + 0 = 62 & -0 & 62 -\\ -15 & -12 & 17 & -\lfloor\log_2(17)\rfloor = 4 & -2 ^ {4 + 1} - 17 - 1 = 14 & -6 & - & 12 + 6 = 18 & -0 & 18 -\\ -\hline -16 & -58 & 28 & -\lfloor\log_2(28)\rfloor = 4 & -2 ^ {4 + 1} - 28 - 1 = 3 & -6 & -1 & 58 + (6 \times 2) - 3 + 1 = 68 & -0 & 68 -\\ -17 & -0 & 11 & -\lfloor\log_2(11)\rfloor = 3 & -2 ^ {3 + 1} - 11 - 1 = 4 & -7 & -0 & 0 + (7 \times 2) - 4 + 0 = 10 & -0 & 10 -\\ -\hline -18 & -65 & 32 & -\lfloor\log_2(32)\rfloor = 5 & -2 ^ {5 + 1} - 32 - 1 = 31 & -6 & - & 65 + 6 = 71 & -0 & 71 -\\ -19 & -0 & 11 & -\lfloor\log_2(11)\rfloor = 3 & -2 ^ {3 + 1} - 11 - 1 = 4 & -0 & - & 0 + 0 = 0 & -0 & 0 -\\ -\hline -\end{tabular} -} -\end{table} -\par -\noindent -Resulting in: -\newline -\begin{tabular}{rr} -channel 0 residuals : & \texttt{[-61,~-33,~-18,~~1,~20,~35,~50,~62,~68,~71]}\\ -channel 1 residuals : & \texttt{[~31,~~32,~~36,~37,~35,~31,~25,~18,~10,~~0]}\\ -\end{tabular} - -\clearpage - -\subsubsection{2nd Residual Decoding Example} -Given a 1 channel block with $\text{entropies}_0 = \texttt{[0, 0, 0]}$: -\vskip .10in -{\relsize{-2} -\renewcommand{\arraystretch}{1.5} -\begin{tabular}{|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|} -i & u_i & m_i & -\text{base} & \text{add} & \text{entropy}_{0~0} & \text{entropy}_{0~1} & \text{entropy}_{0~2} \\ -\hline -& \multicolumn{7}{c|}{read modified Elias gamma code: $t \leftarrow 0~,~\text{zeroes} \leftarrow 0$} \\ -0 & -3 & \lfloor 3 \div 2\rfloor = 1 & -1 + \left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 1 & -\left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 0 & -0 + 5 = 5 & 0 - 0 = 0 & 0 -\\ -1 & -3 & \lfloor 3 \div 2\rfloor + 1 = 2 & -2 + \left\lfloor\frac{5}{2 ^ 4}\right\rfloor + \left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 2 & -\left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 0 & -5 + 5 = 10 & 0 + 5 = 5 & 0 - 0 = 0 -\\ -2 & -5 & \lfloor 5 \div 2\rfloor + 1 = 3 & -2 + \left\lfloor\frac{10}{2 ^ 4}\right\rfloor + \left\lfloor\frac{5}{2 ^ 4}\right\rfloor + \left(\left\lfloor\frac{0}{2 ^ 4}\right\rfloor \times 1\right) = 3 & -\left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 0 & -10 + 5 = 15 & 5 + 5 = 10 & 0 + 5 = 5 -\\ -3 & -2 & \lfloor 2 \div 2\rfloor + 1 = 2 & -2 + \left\lfloor\frac{15}{2 ^ 4}\right\rfloor + \left\lfloor\frac{10}{2 ^ 4}\right\rfloor = 2 & -\left\lfloor\frac{5}{2 ^ 4}\right\rfloor = 0 & -15 + 5 = 20 & 10 + 5 = 15 & 5 - 2 = 3 -\\ -4 & -\textit{undefined} & 0 & -0 & \left\lfloor\frac{20}{2 ^ 4}\right\rfloor = 1 & -20 - 2 = 18 & 15 & 3 -\\ -5 & -0 & \lfloor 0 \div 2\rfloor = 0 & -0 & \left\lfloor\frac{18}{2 ^ 4}\right\rfloor = 1 & -18 - 2 = 16 & 15 & 3 -\\ -6 & -\textit{undefined} & 0 & -0 & \left\lfloor\frac{16}{2 ^ 4}\right\rfloor = 1 & -16 - 2 = 14 & 15 & 3 -\\ -7 & -0 & \lfloor 0 \div 2\rfloor = 0 & -0 & \left\lfloor\frac{14}{2 ^ 4}\right\rfloor = 0 & -14 - 2 = 12 & 15 & 3 -\\ -8 & -\textit{undefined} & 0 & -0 & \left\lfloor\frac{12}{2 ^ 4}\right\rfloor = 0 & -12 - 2 = 10 & 15 & 3 -\\ -9 & -0 & \lfloor 0 \div 2\rfloor = 0 & -0 & \left\lfloor\frac{10}{2 ^ 4}\right\rfloor = 0 & -10 - 2 = 8 & 15 & 3 -\\ -10 & -\textit{undefined} & 0 & -0 & \left\lfloor\frac{8}{2 ^ 4}\right\rfloor = 0 & -8 - 2 = 6 & 15 & 3 -\\ -11 & -0 & \lfloor 0 \div 2\rfloor = 0 & -0 & \left\lfloor\frac{6}{2 ^ 4}\right\rfloor = 0 & -6 - 2 = 4 & 15 & 3 -\\ -12 & -\textit{undefined} & 0 & -0 & \left\lfloor\frac{4}{2 ^ 4}\right\rfloor = 0 & -4 - 2 = 2 & 15 & 3 -\\ -13 & -0 & \lfloor 0 \div 2\rfloor = 0 & -0 & \left\lfloor\frac{2}{2 ^ 4}\right\rfloor = 0 & -2 - 2 = 0 & 15 & 3 -\\ -14 & -\textit{\color{red}undefined} & 0 & -0 & \left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 0 & -0 - 0 = {\color{red}0} & 15 & 3 -\\ -15-24 & \multicolumn{7}{c|}{read modified Elias gamma code: $t \leftarrow 4~,~p \leftarrow 2~,~\text{zeroes} \leftarrow 2 ^ {(4 - 1)} + 2 = 10$} \\ -25 & -1 & \lfloor 1 \div 2\rfloor = 0 & -0 & \left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 0 & -0 - 0 = 0 & 0 & 0 -\\ -26 & -1 & \lfloor 1 \div 2\rfloor + 1 = 1 & -1 + \left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 1 & -\left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 0 & -0 + 5 = 5 & 0 - 0 = 0 & 0 -\\ -27 & -3 & \lfloor 3 \div 2\rfloor + 1 = 2 & -2 + \left\lfloor\frac{5}{2 ^ 4}\right\rfloor + \left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 2 & -\left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 0 & -5 + 5 = 10 & 0 + 5 = 5 & 0 - 0 = 0 -\\ -28 & -0 & \lfloor 0 \div 2\rfloor + 1 = 1 & -1 + \left\lfloor\frac{10}{2 ^ 4}\right\rfloor = 1 & -\left\lfloor\frac{5}{2 ^ 4}\right\rfloor = 0 & -10 + 5 = 15 & 5 - 2 = 3 & 0 -\\ -29 & -\textit{undefined} & 0 & -0 & \left\lfloor\frac{15}{2 ^ 4}\right\rfloor = 0 & -15 - 2 = 13 & 3 & 0 -\\ -\hline -\end{tabular} -} - -\clearpage - -\begin{table}[h] -{\relsize{-2} -\renewcommand{\arraystretch}{1.5} -\begin{tabular}{|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|} -i & \text{base} & \text{add} & p & e & r_i & b_i & unsigned & sign_i & residual_i \\ -\hline -0 & -1 & 0 & -& & & & 1 & -0 & 1 -\\ -1 & -2 & 0 & -& & & & 2 & -0 & 2 -\\ -2 & -3 & 0 & -& & & & 3 & -0 & 3 -\\ -3 & -2 & 0 & -& & & & 2 & -0 & 2 -\\ -4 & -0 & 1 & -\lfloor\log_2(1)\rfloor = 0 & -2 ^ {0 + 1} - 1 - 1 = 0 & -0 & -1 & 0 + (0 \times 2) - 0 + 1 = 1 & -0 & 1 -\\ -5 & -0 & 1 & -\lfloor\log_2(1)\rfloor = 0 & -2 ^ {0 + 1} - 1 - 1 = 0 & -0 & -0 & 0 + (0 \times 2) - 0 + 0 = 0 & -0 & 0 -\\ -6 & -0 & 1 & -\lfloor\log_2(1)\rfloor = 0 & -2 ^ {0 + 1} - 1 - 1 = 0 & -0 & -0 & 0 + (0 \times 2) - 0 + 0 = 0 & -0 & 0 -\\ -7 & -0 & 0 & -& & & & 0 & -0 & 0 -\\ -8 & -0 & 0 & -& & & & 0 & -0 & 0 -\\ -9 & -0 & 0 & -& & & & 0 & -0 & 0 -\\ -10 & -0 & 0 & -& & & & 0 & -0 & 0 -\\ -11 & -0 & 0 & -& & & & 0 & -0 & 0 -\\ -12 & -0 & 0 & -& & & & 0 & -0 & 0 -\\ -13 & -0 & 0 & -& & & & 0 & -0 & 0 -\\ -14 & -0 & 0 & -& & & & 0 & -0 & 0 -\\ -15-24 & \multicolumn{8}{c|}{long run of 10, 0 residual values} & 0 \\ -25 & -0 & 0 & -& & & & 0 & -1 & -0 - 1 = -1 -\\ -26 & -1 & 0 & -& & & & 1 & -1 & -1 - 1 = -2 -\\ -27 & -2 & 0 & -& & & & 2 & -1 & -2 - 1 = -3 -\\ -28 & -1 & 0 & -& & & & 1 & -1 & -1 - 1 = -2 -\\ -29 & -0 & 0 & -& & & & 0 & -1 & -0 - 1 = -1 -\\ -\hline -\end{tabular} -} -\end{table} -\par -\noindent -{\relsize{-1} -Resulting in channel 0 residuals: -\newline -\texttt{[~~1,~~2,~~3,~~2,~~1,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~-1,~-2,~-3,~-2,~-1]} -} - -\end{landscape} - -\begin{figure}[h] -\includegraphics{figures/wavpack/residuals_parse2.pdf} -\caption{2nd residual parsing example} -\end{figure} - -\subsection{Channel Decorrelation} -\label{wavpack:decorrelate_channels} -\ALGORITHM{a list of signed residuals per channel, a list of decorrelation terms and deltas, a decorrelation weight per term and channel, a list of decorrelation samples per term and channel}{a list of signed samples per channel} -\SetKwData{TERMCOUNT}{term count} -\SetKwData{PASS}{pass} -\SetKwData{TERM}{term} -\SetKwData{DELTA}{delta} -\SetKwData{WEIGHT}{weight} -\SetKwData{SAMPLES}{sample} -\SetKwData{RESIDUALS}{residual} -\eIf{$\text{channel count} = 1$}{ - $\text{\PASS}_{(-1)~0} \leftarrow \text{\RESIDUALS}_{0}$\; - \For{$p \leftarrow 0$ \emph{\KwTo}\TERMCOUNT}{ - $\text{\PASS}_{p} \leftarrow$ \hyperref[wavpack:decorr_pass_1ch]{decorrelate 1 channel $\text{\PASS}_{(p - 1)}$} using $\left\lbrace\begin{tabular}{l} - $\text{\TERM}_{p}$ \\ - $\text{\DELTA}_{p}$ \\ - $\text{\WEIGHT}_{p~0}$ \\ - $\text{\SAMPLES}_{p~0}$ \\ - \end{tabular}\right.$\; - } - \Return $\text{\PASS}_{(\text{\TERMCOUNT} - 1)~0}$\; -}{ - $\text{\PASS}_{-1~0} \leftarrow \text{\RESIDUALS}_{0}$\; - $\text{\PASS}_{-1~1} \leftarrow \text{\RESIDUALS}_{1}$\; - \For{$p \leftarrow 0$ \emph{\KwTo}\TERMCOUNT}{ - $\text{\PASS}_{p} \leftarrow$ \hyperref[wavpack:decorr_pass_2ch]{decorrelate 2 channel $\text{\PASS}_{(p - 1)}$} using $\left\lbrace\begin{tabular}{l} - $\text{\TERM}_{p}$ \\ - $\text{\DELTA}_{p}$ \\ - $\text{\WEIGHT}_{p~0}$ \\ - $\text{\WEIGHT}_{p~1}$ \\ - $\text{\SAMPLES}_{p~0}$ \\ - $\text{\SAMPLES}_{p~1}$ \\ - \end{tabular}\right.$\; - } - \Return $\text{\PASS}_{(\text{\TERMCOUNT} - 1)~0}$ and $\text{\PASS}_{(\text{\TERMCOUNT} - 1)~1}$\; -} -\EALGORITHM - -\clearpage - -\subsubsection{Visualizing Decorrelation Passes} - -This is an example of a relatively small set of residuals -being transformed back into a set of samples -over the course of 5 decorrelation passes. -What's important to note is that each pass -adjusts the residual values' overall range. -The number of these passes and their decorrelation terms -are what separate high levels of WavPack compression from low levels. - -\begin{figure}[h] - \subfloat{ - \includegraphics{figures/wavpack/decorrelation0.pdf} - } - \subfloat{ - \includegraphics{figures/wavpack/decorrelation1.pdf} - } - \newline - \subfloat{ - \includegraphics{figures/wavpack/decorrelation2.pdf} - } - \subfloat{ - \includegraphics{figures/wavpack/decorrelation3.pdf} - } - \newline - \subfloat{ - \includegraphics{figures/wavpack/decorrelation4.pdf} - } - \subfloat{ - \includegraphics{figures/wavpack/decorrelation5.pdf} - } -\end{figure} - - -\clearpage - -\subsubsection{1 Channel Decorrelation Pass} -\label{wavpack:decorr_pass_1ch} -{\relsize{-1} -\ALGORITHM{a list of signed correlated samples, decorrelation term and delta, decorrelation weight, list of decorrelation samples}{a list of signed decorrelated samples} -\SetKwData{CORRELATED}{correlated} -\SetKwData{DECORRELATED}{decorrelated} -\SetKwData{DECORRSAMPLE}{decorrelation sample} -\SetKwData{WEIGHT}{weight} -\SetKwData{TEMP}{temp} -\SetKw{OR}{or} -\SetKw{XOR}{xor} -\SetKwFunction{APPLYWEIGHT}{apply\_weight} -\SetKwFunction{UPDATEWEIGHT}{update\_weight} -$\text{\WEIGHT}_0 \leftarrow$ decorrelation weight\; -\BlankLine -\uIf{$term = 18$}{ - $\text{\DECORRELATED}_0 \leftarrow \text{\DECORRSAMPLE}_1$\; - $\text{\DECORRELATED}_1 \leftarrow \text{\DECORRSAMPLE}_0$\; - \For{$i \leftarrow 0$ \emph{\KwTo}correlated samples length}{ - $\text{\TEMP}_{i} \leftarrow \lfloor(3 \times \text{\DECORRELATED}_{i + 1} - \text{\DECORRELATED}_{i}) \div 2 \rfloor$\; - $\text{\DECORRELATED}_{i + 2} \leftarrow \APPLYWEIGHT(\text{\WEIGHT}_i~,~\text{\TEMP}_{i}) + \text{\CORRELATED}_i$\; - $\text{\WEIGHT}_{i + 1} \leftarrow \text{\WEIGHT}_i + \UPDATEWEIGHT(\text{\TEMP}_{i}~,~\text{\CORRELATED}_i~,~delta)$\; - } - \Return \DECORRELATED samples starting from 2\; -} -\uElseIf{$term = 17$}{ - $\text{\DECORRELATED}_0 \leftarrow \text{\DECORRSAMPLE}_1$\; - $\text{\DECORRELATED}_1 \leftarrow \text{\DECORRSAMPLE}_0$\; - \For{$i \leftarrow 0$ \emph{\KwTo}correlated samples length}{ - $\text{\TEMP}_{i} \leftarrow 2 \times \text{\DECORRELATED}_{i + 1} - \text{\DECORRELATED}_{i}$\; - $\text{\DECORRELATED}_{i + 2} \leftarrow \APPLYWEIGHT(\text{\WEIGHT}_i~,~\text{\TEMP}_{i}) + \text{\CORRELATED}_i$\; - $\text{\WEIGHT}_{i + 1} \leftarrow \text{\WEIGHT}_i + \UPDATEWEIGHT(\text{\TEMP}_{i}~,~\text{\CORRELATED}_i~,~delta)$\; - } - \Return \DECORRELATED samples starting from 2\; -} -\uElseIf{$1 \leq term \leq 8$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}term}{ - $\text{\DECORRELATED}_i \leftarrow \text{\DECORRSAMPLE}_i$\; - } - \For{$i \leftarrow 0$ \emph{\KwTo}correlated samples length}{ - $\text{\DECORRELATED}_{i + term} \leftarrow \APPLYWEIGHT(\text{\WEIGHT}_i~,~\text{\DECORRELATED}_{i}) + \text{\CORRELATED}_i$\; - $\text{\WEIGHT}_{i + 1} \leftarrow \text{\WEIGHT}_i + \UPDATEWEIGHT(\text{\DECORRELATED}_{i}~,~\text{\CORRELATED}_i~,~delta)$\; - } - \BlankLine - \Return \DECORRELATED samples starting from $term$\; -} -\Else{ - invalid decorrelation term\; -} -\EALGORITHM -\par -\noindent -\begin{align*} -\intertext{where \texttt{apply\_weight} is defined as:} -\texttt{apply\_weight}(weight~,~sample) &= \left\lfloor\frac{weight \times sample + 2 ^ 9}{2 ^ {10}}\right\rfloor \\ -\intertext{and \texttt{update\_weight} is defined as:} -\texttt{update\_weight}(source~,~result~,~delta) &= -\begin{cases} -0 & \text{ if } source = 0 \text{ or } result = 0 \\ -delta & \text{ if } (source \textbf{ xor } result ) \geq 0 \\ --delta & \text{ if } (source \textbf{ xor } result) < 0 -\end{cases} -\end{align*} - -} - -\clearpage - -\subsubsection{2 Channel Decorrelation Pass} -\label{wavpack:decorr_pass_2ch} -{\relsize{-1} -\ALGORITHM{2 lists of signed correlated samples, decorrelation term and delta, 2 decorrelation weights, 2 lists of decorrelation samples}{2 lists of signed decorrelated samples} -\SetKwData{CORRELATED}{correlated} -\SetKwData{DECORRELATED}{decorrelated} -\SetKwData{DECORRSAMPLE}{decorrelation sample} -\SetKwData{WEIGHT}{weight} -\SetKw{OR}{or} -\SetKw{XOR}{xor} -\SetKwFunction{MIN}{min} -\SetKwFunction{MAX}{max} -\SetKwFunction{APPLYWEIGHT}{apply\_weight} -\SetKwFunction{UPDATEWEIGHT}{update\_weight} -\uIf{$17 \leq term \leq 18$ \OR $1 \leq term \leq 8$}{ - $\text{\DECORRELATED}_0 \leftarrow$ 1 channel decorrelation pass of $\text{\CORRELATED}_0$\; - $\text{\DECORRELATED}_1 \leftarrow$ 1 channel decorrelation pass of $\text{\CORRELATED}_1$\; - \Return $\text{\DECORRELATED}_0$ and $\text{\DECORRELATED}_1$\; -} -\uElseIf{$-3 \leq term \leq -1$}{ - $\text{\WEIGHT}_{0~0} \leftarrow$ decorrelation weight 0\; - $\text{\WEIGHT}_{1~0} \leftarrow$ decorrelation weight 1\; - $\text{\DECORRELATED}_{0~0} \leftarrow \text{\DECORRSAMPLE}_{1~0}$\; - $\text{\DECORRELATED}_{1~0} \leftarrow \text{\DECORRSAMPLE}_{0~0}$\; - \uIf{$term = -1$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}correlated samples length}{ - $\text{\DECORRELATED}_{0~(i + 1)} \leftarrow \APPLYWEIGHT(\text{\WEIGHT}_{0~i}~,~\text{\DECORRELATED}_{1~i}) + \text{\CORRELATED}_{0~i}$\; - $\text{\DECORRELATED}_{1~(i + 1)} \leftarrow \APPLYWEIGHT(\text{\WEIGHT}_{1~i}~,~\text{\DECORRELATED}_{0~(i + 1)}) + \text{\CORRELATED}_{1~i}$\; - $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \text{\WEIGHT}_{0~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{1~i}~,~\text{\CORRELATED}_{0~i}~,~delta)$\; - $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \text{\WEIGHT}_{1~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{0~(i + 1)}~,~\text{\CORRELATED}_{1~i}~,~delta)$\; - $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{0~(i + 1)}~,~1024)~,~-1024)$\; - $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{1~(i + 1)}~,~1024)~,~-1024)$\; - } - } - \uElseIf{$term = -2$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}correlated samples length}{ - $\text{\DECORRELATED}_{1~(i + 1)} \leftarrow \APPLYWEIGHT(\text{\WEIGHT}_{1~i}~,~\text{\DECORRELATED}_{0~i}) + \text{\CORRELATED}_{1~i}$\; - $\text{\DECORRELATED}_{0~(i + 1)} \leftarrow \APPLYWEIGHT(\text{\WEIGHT}_{0~i}~,~\text{\DECORRELATED}_{1~(i + 1)}) + \text{\CORRELATED}_{0~i}$\; - $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \text{\WEIGHT}_{1~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{0~i}~,~\text{\CORRELATED}_{1~i}~,~delta)$\; - $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \text{\WEIGHT}_{0~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{1~(i + 1)},~\text{\CORRELATED}_{0~i}~,~delta)$\; - $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{1~(i + 1)}~,~1024)~,~-1024)$\; - $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{0~(i + 1)}~,~1024)~,~-1024)$\; - } - } - \ElseIf{$term = -3$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}correlated samples length}{ - $\text{\DECORRELATED}_{0~(i + 1)} \leftarrow \APPLYWEIGHT(\text{\WEIGHT}_{0~i}~,~\text{\DECORRELATED}_{1~i}) + \text{\CORRELATED}_{0~i}$\; - $\text{\DECORRELATED}_{1~(i + 1)} \leftarrow \APPLYWEIGHT(\text{\WEIGHT}_{1~i}~,~\text{\DECORRELATED}_{0~i}) + \text{\CORRELATED}_{1~i}$\; - $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \text{\WEIGHT}_{0~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{1~i}~,~\text{\CORRELATED}_{0~i}~,~delta)$\; - $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \text{\WEIGHT}_{1~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{0~i}~,~\text{\CORRELATED}_{1~i}~,~delta)$\; - $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{0~(i + 1)}~,~1024)~,~-1024)$\; - $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{1~(i + 1)}~,~1024)~,~-1024)$\; - } - } - \Return $\text{\DECORRELATED}_0$ starting from 1 and $\text{\DECORRELATED}_1$ starting from 1\; -} -\Else{ - invalid decorrelation term\; -} -\EALGORITHM -} - -\clearpage - -\subsubsection{Channel Decorrelation Example} -Given the values from the \VAR{Decorrelation Terms}, -\VAR{Decorrelation Weights} and \VAR{Decorrelation Samples} -sub blocks: -\begin{figure}[h] -{\relsize{-1} - \subfloat{ - \begin{tabular}{|r|r|r|} - \multicolumn{3}{c}{Decorrelation Terms} \\ - \hline - $p$ & $\textsf{term}_p$ & $\textsf{delta}_p$ \\ - \hline - 4 & 18 & 2 \\ - 3 & 18 & 2 \\ - 2 & 2 & 2 \\ - 1 & 17 & 2 \\ - 0 & 3 & 2 \\ - \hline - \end{tabular} - } - \subfloat{ - \begin{tabular}{|r|r|r|} - \multicolumn{3}{c}{Decorrelation Weights} \\ - \hline - $p$ & $\textsf{weight}_{p~0}$ & $\textsf{weight}_{p~1}$ \\ - \hline - 4 & 48 & 48 \\ - 3 & 48 & 48 \\ - 2 & 32 & 32 \\ - 1 & 48 & 48 \\ - 0 & 16 & 24 \\ - \hline - \end{tabular} - } - \subfloat{ - \begin{tabular}{|r|r|r|} - \multicolumn{3}{c}{Decorrelation Samples} \\ - \hline - $p$ & $\textsf{sample}_{p~0~s}$ & $\textsf{sample}_{p~1~s}$ \\ - \hline - 4 & \texttt{[-73, -78]} & \texttt{[28, 26]} \\ - 3 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ - 2 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ - 1 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ - 0 & \texttt{[0, 0, 0]} & \texttt{[0, 0, 0]} \\ - \hline - \end{tabular} - } -} -\end{figure} -\par -\noindent -we combine them into a single set of arguments for each decorrelation pass: -\begin{table}[h] -{\relsize{-1} - \begin{tabular}{|r|r|r|r|r|r|} - \hline - & $\textbf{pass}_0$ & $\textbf{pass}_1$ & $\textbf{pass}_2$ & - $\textbf{pass}_3$ & $\textbf{pass}_4$ \\ - \hline - $\textsf{term}_p$ & 3 & 17 & 2 & 18 & 18 \\ - $\textsf{delta}_p$ & 2 & 2 & 2 & 2 & 2 \\ - $\textsf{weight}_{p~0}$ & 16 & 48 & 32 & 48 & 48 \\ - $\textsf{sample}_{p~0~s}$ & \texttt{[0, 0, 0]} & \texttt{[0, 0]} & - \texttt{[0, 0]} & \texttt{[0, 0]} & \texttt{[-73, -78]} \\ - $\textsf{weight}_{p~1}$ & 24 & 48 & 32 & 48 & 48 \\ - $\textsf{sample}_{p~1~s}$ & \texttt{[0, 0, 0]} & \texttt{[0, 0]} & - \texttt{[0, 0]} & \texttt{[0, 0]} & \texttt{[28, 26]} \\ - \hline - \end{tabular} -} -\end{table} -\par -\noindent -which we apply to the residuals from the bitstream sub-block: -\par -\noindent -{\relsize{-1} - \begin{tabular}{|r|r|r|r|r|r|} - \hline - $\textsf{residual}_{0~i}$ & - after $\textbf{pass}_0$ & - after $\textbf{pass}_1$ & - after $\textbf{pass}_2$ & - after $\textbf{pass}_3$ & - after $\textbf{pass}_4$ \\ - \hline - -61 & -61 & -61 & -61 & -61 & -64 \\ - -33 & -33 & -39 & -39 & -43 & -46 \\ - -18 & -18 & -19 & -21 & -23 & -25 \\ - 1 & 0 & 0 & -1 & -2 & -3 \\ - 20 & 20 & 21 & 20 & 20 & 20 \\ - 35 & 35 & 37 & 37 & 39 & 41 \\ - 50 & 50 & 53 & 54 & 57 & 60 \\ - 62 & 62 & 66 & 67 & 71 & 75 \\ - 68 & 68 & 73 & 75 & 80 & 85 \\ - 71 & 72 & 77 & 79 & 84 & 90 \\ - \hline - \hline - $\textsf{residual}_{1~i}$ & - after $\textbf{pass}_0$ & - after $\textbf{pass}_1$ & - after $\textbf{pass}_2$ & - after $\textbf{pass}_3$ & - after $\textbf{pass}_4$ \\ - \hline - 31 & 31 & 31 & 31 & 31 & 32 \\ - 32 & 32 & 35 & 35 & 37 & 39 \\ - 36 & 36 & 38 & 39 & 41 & 43 \\ - 37 & 38 & 40 & 41 & 43 & 45 \\ - 35 & 36 & 38 & 39 & 41 & 44 \\ - 31 & 32 & 34 & 36 & 38 & 40 \\ - 25 & 26 & 28 & 30 & 32 & 34 \\ - 18 & 19 & 20 & 21 & 23 & 25 \\ - 10 & 11 & 12 & 13 & 14 & 15 \\ - 0 & 1 & 1 & 2 & 3 & 4 \\ - \hline - \end{tabular} -} -\par -\noindent -Resulting in final decorrelated samples: -\newline -\begin{tabular}{rr} -$\textsf{channel}_0$ : & \texttt{[-64,~-46,~-25,~-3,~20,~41,~60,~75,~85,~90]} \\ -$\textsf{channel}_1$ : & \texttt{[~32,~~39,~~43,~45,~44,~40,~34,~25,~15,~~4]} \\ -\end{tabular} - -\clearpage - -{\relsize{-2} -\begin{tabular}{r||r|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}} -%% \multicolumn{5}{c}{\textbf{pass 0} - term 3 - delta 2 - weight 16} \\ -& $i$ & \textsf{correlated}_i & \textsf{temp}_i & \textsf{decorrelated}_{i + 3} & \textsf{weight}_{i + 1} \\ -\hline -\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_0$ - term 3\end{sideways}} -& 0 & -61 & & -\lfloor(16 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor - 61 = -61 & -16 + 0 = 16 -\\ -& 1 & -33 & & -\lfloor(16 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor - 33 = -33 & -16 + 0 = 16 -\\ -& 2 & -18 & & -\lfloor(16 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor - 18 = -18 & -16 + 0 = 16 -\\ -& 3 & 1 & & -\lfloor(16 \times -61 + 2 ^ 9) \div 2 ^ {10}\rfloor + 1 = 0 & -16 - 2 = 14 -\\ -& 4 & 20 & & -\lfloor(14 \times -33 + 2 ^ 9) \div 2 ^ {10}\rfloor + 20 = 20 & -14 - 2 = 12 -\\ -& 5 & 35 & & -\lfloor(12 \times -18 + 2 ^ 9) \div 2 ^ {10}\rfloor + 35 = 35 & -12 - 2 = 10 -\\ -& 6 & 50 & & -\lfloor(10 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor + 50 = 50 & -10 + 0 = 10 -\\ -& 7 & 62 & & -\lfloor(10 \times 20 + 2 ^ 9) \div 2 ^ {10}\rfloor + 62 = 62 & -10 + 2 = 12 -\\ -& 8 & 68 & & -\lfloor(12 \times 35 + 2 ^ 9) \div 2 ^ {10}\rfloor + 68 = 68 & -12 + 2 = 14 -\\ -& 9 & 71 & & -\lfloor(14 \times 50 + 2 ^ 9) \div 2 ^ {10}\rfloor + 71 = 72 & -14 + 2 = 16 -\\ -\hline -\hline -%% \multicolumn{5}{c}{\textbf{pass 1 } - term 17 - delta 2 - weight 48} \\ -& $i$ & \textsf{correlated}_i & \textsf{temp}_i & \textsf{decorrelated}_{i + 2} & \textsf{weight}_{i + 1} \\ -\hline -\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_1$ - term 17\end{sideways}} -& 0 & -61 & -2 \times 0 - 0 = 0 & -\lfloor(48 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor - 61 = -61 & -48 + 0 = 48 -\\ -& 1 & -33 & -2 \times -61 - 0 = -122 & -\lfloor(48 \times -122 + 2 ^ 9) \div 2 ^ {10}\rfloor - 33 = -39 & -48 + 2 = 50 -\\ -& 2 & -18 & -2 \times -39 + 61 = -17 & -\lfloor(50 \times -17 + 2 ^ 9) \div 2 ^ {10}\rfloor - 18 = -19 & -50 + 2 = 52 -\\ -& 3 & 0 & -2 \times -19 + 39 = 1 & -\lfloor(52 \times 1 + 2 ^ 9) \div 2 ^ {10}\rfloor + 0 = 0 & -52 + 0 = 52 -\\ -& 4 & 20 & -2 \times 0 + 19 = 19 & -\lfloor(52 \times 19 + 2 ^ 9) \div 2 ^ {10}\rfloor + 20 = 21 & -52 + 2 = 54 -\\ -& 5 & 35 & -2 \times 21 - 0 = 42 & -\lfloor(54 \times 42 + 2 ^ 9) \div 2 ^ {10}\rfloor + 35 = 37 & -54 + 2 = 56 -\\ -& 6 & 50 & -2 \times 37 - 21 = 53 & -\lfloor(56 \times 53 + 2 ^ 9) \div 2 ^ {10}\rfloor + 50 = 53 & -56 + 2 = 58 -\\ -& 7 & 62 & -2 \times 53 - 37 = 69 & -\lfloor(58 \times 69 + 2 ^ 9) \div 2 ^ {10}\rfloor + 62 = 66 & -58 + 2 = 60 -\\ -& 8 & 68 & -2 \times 66 - 53 = 79 & -\lfloor(60 \times 79 + 2 ^ 9) \div 2 ^ {10}\rfloor + 68 = 73 & -60 + 2 = 62 -\\ -& 9 & 72 & -2 \times 73 - 66 = 80 & -\lfloor(62 \times 80 + 2 ^ 9) \div 2 ^ {10}\rfloor + 72 = 77 & -62 + 2 = 64 -\\ -\hline -\hline -%% \multicolumn{5}{c}{\textbf{pass 2 } - term 2 - delta 2 - weight 32} \\ -& $i$ & \textsf{correlated}_i & \textsf{temp}_i & \textsf{decorrelated}_{i + 2} & \textsf{weight}_{i + 1} \\ -\hline -\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_2$ - term 2\end{sideways}} -& 0 & -61 & & -\lfloor(32 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor - 61 = -61 & -32 + 0 = 32 -\\ -& 1 & -39 & & -\lfloor(32 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor - 39 = -39 & -32 + 0 = 32 -\\ -& 2 & -19 & & -\lfloor(32 \times -61 + 2 ^ 9) \div 2 ^ {10}\rfloor - 19 = -21 & -32 + 2 = 34 -\\ -& 3 & 0 & & -\lfloor(34 \times -39 + 2 ^ 9) \div 2 ^ {10}\rfloor + 0 = -1 & -34 + 0 = 34 -\\ -& 4 & 21 & & -\lfloor(34 \times -21 + 2 ^ 9) \div 2 ^ {10}\rfloor + 21 = 20 & -34 - 2 = 32 -\\ -& 5 & 37 & & -\lfloor(32 \times -1 + 2 ^ 9) \div 2 ^ {10}\rfloor + 37 = 37 & -32 - 2 = 30 -\\ -& 6 & 53 & & -\lfloor(30 \times 20 + 2 ^ 9) \div 2 ^ {10}\rfloor + 53 = 54 & -30 + 2 = 32 -\\ -& 7 & 66 & & -\lfloor(32 \times 37 + 2 ^ 9) \div 2 ^ {10}\rfloor + 66 = 67 & -32 + 2 = 34 -\\ -& 8 & 73 & & -\lfloor(34 \times 54 + 2 ^ 9) \div 2 ^ {10}\rfloor + 73 = 75 & -34 + 2 = 36 -\\ -& 9 & 77 & & -\lfloor(36 \times 67 + 2 ^ 9) \div 2 ^ {10}\rfloor + 77 = 79 & -36 + 2 = 38 -\\ -\hline -\hline -%% \multicolumn{5}{c}{\textbf{pass 3 } - term 18 - delta 2 - weight 48} \\ -& $i$ & \textsf{correlated}_i & \textsf{temp}_i & \textsf{decorrelated}_{i + 2} & \textsf{weight}_{i + 1} \\ -\hline -\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_3$ - term 18\end{sideways}} -& 0 & -61 & -\lfloor(3 \times 0 - 0) \div 2\rfloor = 0 & -\lfloor(48 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor - 61 = -61 & -48 + 0 = 48 -\\ -& 1 & -39 & -\lfloor(3 \times -61 - 0) \div 2\rfloor = -92 & -\lfloor(48 \times -92 + 2 ^ 9) \div 2 ^ {10}\rfloor - 39 = -43 & -48 + 2 = 50 -\\ -& 2 & -21 & -\lfloor(3 \times -43 + 61) \div 2\rfloor = -34 & -\lfloor(50 \times -34 + 2 ^ 9) \div 2 ^ {10}\rfloor - 21 = -23 & -50 + 2 = 52 -\\ -& 3 & -1 & -\lfloor(3 \times -23 + 43) \div 2\rfloor = -13 & -\lfloor(52 \times -13 + 2 ^ 9) \div 2 ^ {10}\rfloor - 1 = -2 & -52 + 2 = 54 -\\ -& 4 & 20 & -\lfloor(3 \times -2 + 23) \div 2\rfloor = 8 & -\lfloor(54 \times 8 + 2 ^ 9) \div 2 ^ {10}\rfloor + 20 = 20 & -54 + 2 = 56 -\\ -& 5 & 37 & -\lfloor(3 \times 20 + 2) \div 2\rfloor = 31 & -\lfloor(56 \times 31 + 2 ^ 9) \div 2 ^ {10}\rfloor + 37 = 39 & -56 + 2 = 58 -\\ -& 6 & 54 & -\lfloor(3 \times 39 - 20) \div 2\rfloor = 48 & -\lfloor(58 \times 48 + 2 ^ 9) \div 2 ^ {10}\rfloor + 54 = 57 & -58 + 2 = 60 -\\ -& 7 & 67 & -\lfloor(3 \times 57 - 39) \div 2\rfloor = 66 & -\lfloor(60 \times 66 + 2 ^ 9) \div 2 ^ {10}\rfloor + 67 = 71 & -60 + 2 = 62 -\\ -& 8 & 75 & -\lfloor(3 \times 71 - 57) \div 2\rfloor = 78 & -\lfloor(62 \times 78 + 2 ^ 9) \div 2 ^ {10}\rfloor + 75 = 80 & -62 + 2 = 64 -\\ -& 9 & 79 & -\lfloor(3 \times 80 - 71) \div 2\rfloor = 84 & -\lfloor(64 \times 84 + 2 ^ 9) \div 2 ^ {10}\rfloor + 79 = 84 & -64 + 2 = 66 -\\ -\hline -\hline -%% \multicolumn{5}{c}{\textbf{pass 4 } - term 18 - delta 2 - weight 48} \\ -& $i$ & \textsf{correlated}_i & \textsf{temp}_i & \textsf{decorrelated}_{i + 2} & \textsf{weight}_{i + 1} \\ -\hline -\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_4$ - term 18\end{sideways}} -& 0 & -61 & -\lfloor(3 \times -73 + 78) \div 2\rfloor = -71 & -\lfloor(48 \times -71 + 2 ^ 9) \div 2 ^ {10}\rfloor - 61 = -64 & -48 + 2 = 50 -\\ -& 1 & -43 & -\lfloor(3 \times -64 + 73) \div 2\rfloor = -60 & -\lfloor(50 \times -60 + 2 ^ 9) \div 2 ^ {10}\rfloor - 43 = -46 & -50 + 2 = 52 -\\ -& 2 & -23 & -\lfloor(3 \times -46 + 64) \div 2\rfloor = -37 & -\lfloor(52 \times -37 + 2 ^ 9) \div 2 ^ {10}\rfloor - 23 = -25 & -52 + 2 = 54 -\\ -& 3 & -2 & -\lfloor(3 \times -25 + 46) \div 2\rfloor = -15 & -\lfloor(54 \times -15 + 2 ^ 9) \div 2 ^ {10}\rfloor - 2 = -3 & -54 + 2 = 56 -\\ -& 4 & 20 & -\lfloor(3 \times -3 + 25) \div 2\rfloor = 8 & -\lfloor(56 \times 8 + 2 ^ 9) \div 2 ^ {10}\rfloor + 20 = 20 & -56 + 2 = 58 -\\ -& 5 & 39 & -\lfloor(3 \times 20 + 3) \div 2\rfloor = 31 & -\lfloor(58 \times 31 + 2 ^ 9) \div 2 ^ {10}\rfloor + 39 = 41 & -58 + 2 = 60 -\\ -& 6 & 57 & -\lfloor(3 \times 41 - 20) \div 2\rfloor = 51 & -\lfloor(60 \times 51 + 2 ^ 9) \div 2 ^ {10}\rfloor + 57 = 60 & -60 + 2 = 62 -\\ -& 7 & 71 & -\lfloor(3 \times 60 - 41) \div 2\rfloor = 69 & -\lfloor(62 \times 69 + 2 ^ 9) \div 2 ^ {10}\rfloor + 71 = 75 & -62 + 2 = 64 -\\ -& 8 & 80 & -\lfloor(3 \times 75 - 60) \div 2\rfloor = 82 & -\lfloor(64 \times 82 + 2 ^ 9) \div 2 ^ {10}\rfloor + 80 = 85 & -64 + 2 = 66 -\\ -& 9 & 84 & -\lfloor(3 \times 85 - 75) \div 2\rfloor = 90 & -\lfloor(66 \times 90 + 2 ^ 9) \div 2 ^ {10}\rfloor + 84 = 90 & -66 + 2 = 68 -\\ -\end{tabular} -} -\begin{center} -$\text{channel}_0$ decorrelation passes -\end{center} - -\clearpage - -\subsection{Undo Joint Stereo} -\label{wavpack:undo_joint_stereo} -\ALGORITHM{mid and side channels of signed sample data, in that order}{left and right channels of signed sample data, in that order} -\SetKwData{MID}{mid} -\SetKwData{SIDE}{side} -\SetKwData{LEFT}{left} -\SetKwData{RIGHT}{right} -\For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ - $\text{\RIGHT}_i \leftarrow \text{\SIDE}_i - \lfloor\text{\MID}_i \div 2\rfloor$\; - $\text{\LEFT}_i \leftarrow \text{\MID}_i + \text{\RIGHT}_i$\; -} -\Return left and right channels\; -\EALGORITHM - -\subsubsection{Joint Stereo Example} -\begin{table}[h] -\begin{tabular}{|r|r|r||>{$}r<{$}|>{$}r<{$}|} -$i$ & $\textsf{mid}_i$ & $\textsf{side}_i$ & \textsf{right}_i & \textsf{left}_i \\ -\hline -0 & -64 & 32 & -32 - \lfloor-64 \div 2\rfloor = 64 & --64 + 64 = 0 \\ -1 & -46 & 39 & -39 - \lfloor-46 \div 2\rfloor = 62 & --46 + 62 = 16 \\ -2 & -25 & 43 & -43 - \lfloor-25 \div 2\rfloor = 56 & --25 + 56 = 31 \\ -3 & -3 & 45 & -45 - \lfloor-3 \div 2\rfloor = 47 & --3 + 47 = 44 \\ -4 & 20 & 44 & -44 - \lfloor20 \div 2\rfloor = 34 & -20 + 34 = 54 \\ -5 & 41 & 40 & -40 - \lfloor41 \div 2\rfloor = 20 & -41 + 20 = 61 \\ -6 & 60 & 34 & -34 - \lfloor60 \div 2\rfloor = 4 & -60 + 4 = 64 \\ -7 & 75 & 25 & -25 - \lfloor75 \div 2\rfloor = -12 & -75 + -12 = 63 \\ -8 & 85 & 15 & -15 - \lfloor85 \div 2\rfloor = -27 & -85 + -27 = 58 \\ -9 & 90 & 4 & -4 - \lfloor90 \div 2\rfloor = -41 & -90 + -41 = 49 \\ -\hline -\end{tabular} -\end{table} - -\begin{landscape} - \label{wavpack:verify_crc} -\subsection{Checksum Calculation} -\ALGORITHM{one or two channels of signed audio samples}{an unsigned 32-bit CRC integer} -\SetKwData{MONO}{mono output} -\SetKwData{CRC}{CRC} -\SetKwData{LCRC}{LCRC} -\SetKwData{SCRC}{SCRC} -\SetKwData{CHANNEL}{channel} -$\text{\CRC}_{-1} \leftarrow \texttt{0xFFFFFFFF}$\; -\For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ - \eIf{$\text{\MONO} = 0$}{ - $\text{\LCRC}_i \leftarrow (3 \times \text{\CRC}_{i - 1}) + \text{\CHANNEL}_{0~i}$\tcc*[r]{calculate signed CRC of left channel} - $\text{\SCRC}_i \leftarrow (3 \times \text{\LCRC}_{i - 1}) + \text{\CHANNEL}_{1~i}$\tcc*[r]{calculate signed CRC of right channel} - }{ - $\text{\SCRC}_i \leftarrow (3 \times \text{\CRC}_{i - 1}) + \text{\CHANNEL}_{0~i}$\tcc*[r]{calculate signed CRC of channel} - } - \BlankLine - \eIf(\tcc*[f]{convert signed CRC to unsigned, 32-bit integer}){$\text{\SCRC}_i \geq 0$}{ - $\text{\CRC}_i \leftarrow \text{\SCRC}_i \bmod \texttt{0x100000000}$\; - }{ - $\text{\CRC}_i \leftarrow (2 ^ {32} - (-\text{\SCRC}_i)) \bmod \texttt{0x100000000}$\; - } -} -\Return $\text{\CRC}_{\text{sample count} - 1}$\; -\EALGORITHM - -\subsubsection{Checksum Calculation Example} -{\relsize{-1} -\begin{tabular}{|r|r|r||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|} -$i$ & $\textsf{channel}_{0~i}$ & $\textsf{channel}_{1~i}$ & \textsf{LCRC}_i & \text{SCRC}_i & \textsf{CRC}_i \\ -\hline -0 & 0 & 64 & -(3 \times \texttt{0xFFFFFFFF}) + 0 = \texttt{0x2FFFFFFFD} & -(3 \times \texttt{0x2FFFFFFFD}) + 64 = \texttt{0x900000037} & -\texttt{0x00000037} \\ -1 & 16 & 62 & -(3 \times \texttt{0x00000037}) + 16 = \texttt{0x000000B5} & -(3 \times \texttt{0x000000B5}) + 62 = \texttt{0x0000025D} & -\texttt{0x0000025D} \\ -2 & 31 & 56 & -(3 \times \texttt{0x0000025D}) + 31 = \texttt{0x00000736} & -(3 \times \texttt{0x00000736}) + 56 = \texttt{0x000015DA} & -\texttt{0x000015DA} \\ -3 & 44 & 47 & -(3 \times \texttt{0x000015DA}) + 44 = \texttt{0x000041BA} & -(3 \times \texttt{0x000041BA}) + 47 = \texttt{0x0000C55D} & -\texttt{0x0000C55D} \\ -4 & 54 & 34 & -(3 \times \texttt{0x0000C55D}) + 54 = \texttt{0x0002504D} & -(3 \times \texttt{0x0002504D}) + 34 = \texttt{0x0006F109} & -\texttt{0x0006F109} \\ -5 & 61 & 20 & -(3 \times \texttt{0x0006F109}) + 61 = \texttt{0x0014D358} & -(3 \times \texttt{0x0014D358}) + 20 = \texttt{0x003E7A1C} & -\texttt{0x003E7A1C} \\ -6 & 64 & 4 & -(3 \times \texttt{0x003E7A1C}) + 64 = \texttt{0x00BB6E94} & -(3 \times \texttt{0x00BB6E94}) + 4 = \texttt{0x02324BC0} & -\texttt{0x02324BC0} \\ -7 & 63 & -12 & -(3 \times \texttt{0x02324BC0}) + 63 = \texttt{0x0696E37F} & -(3 \times \texttt{0x0696E37F}) - 12 = \texttt{0x13C4AA71} & -\texttt{0x13C4AA71} \\ -8 & 58 & -27 & -(3 \times \texttt{0x13C4AA71}) + 58 = \texttt{0x3B4DFF8D} & -(3 \times \texttt{0x3B4DFF8D}) - 27 = \texttt{0xB1E9FE8C} & -\texttt{0xB1E9FE8C} \\ -9 & 49 & -41 & -(3 \times \texttt{0xB1E9FE8C}) + 49 = \texttt{0x215BDFBD5} & -(3 \times \texttt{0x215BDFBD5}) - 41 = \texttt{0x64139F356} & -\texttt{0x4139F356} \\ -\end{tabular} -} -\vskip 1em -\par -\noindent -Resulting in a final CRC of \texttt{0x4139F356} -\end{landscape} - -\clearpage - -\subsection{Decode Extended Integers} -\label{wavpack:decode_extended_integers} -\ALGORITHM{sub block data}{\VAR{zero bits}, \VAR{one bits}, \VAR{duplicate bits} values as unsigned integers} -\SetKwData{SENTS}{sent bits} -\SetKwData{ZEROES}{zero bits} -\SetKwData{ONES}{one bits} -\SetKwData{DUPLICATES}{duplicate bits} -\SENTS $\leftarrow$ \READ 8 unsigned bits\tcc*[r]{unused} -\ZEROES $\leftarrow$ \READ 8 unsigned bits\; -\ONES $\leftarrow$ \READ 8 unsigned bits\; -\DUPLICATES $\leftarrow$ \READ 8 unsigned bits\; -\Return \ZEROES, \ONES and \DUPLICATES\; -\EALGORITHM - -\begin{figure}[h] - \includegraphics{figures/wavpack/extended_integers.pdf} -\end{figure} - -\subsection{Undoing Extended Integers} -\label{wavpack:undo_extended_integers} -\ALGORITHM{\VAR{zero bits}, \VAR{one bits}, \VAR{duplicate bits} values; 1 or 2 channels of shifted PCM data}{1 or 2 channels of un-shifted PCM data} -\SetKwData{ZEROES}{zero bits} -\SetKwData{ONES}{one bits} -\SetKwData{DUPLICATES}{duplicate bits} -\SetKwData{SHIFTED}{shifted channel} -\SetKwData{UNSHIFTED}{un-shifted channel} -\For{$c \leftarrow 0$ \emph{\KwTo}channel count}{ - \uIf{$\text{\ZEROES} > 0$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ - $\text{\UNSHIFTED}_{c~i} \leftarrow \text{\SHIFTED}_{c~i} \times 2 ^ {\text{\ZEROES}}$\; - } - } - \uElseIf{$\text{\ONES} > 0$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ - $\text{\UNSHIFTED}_{c~i} \leftarrow \text{\SHIFTED}_{c~i} \times 2 ^ {\text{\ONES}} + (2 ^ {\text{\ONES}} - 1)$\; - } - } - \uElseIf{$\text{\DUPLICATES} > 0$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ - \eIf{$\text{\SHIFTED}_{c~i} \bmod 2 = 0$}{ - $\text{\UNSHIFTED}_{c~i} \leftarrow \text{\SHIFTED}_{c~i} \times 2 ^ {\text{\DUPLICATES}}$\; - }{ - $\text{\UNSHIFTED}_{c~i} \leftarrow \text{\SHIFTED}_{c~i} \times 2 ^ {\text{\DUPLICATES}} + (2 ^ {\text{\DUPLICATES}} - 1)$\; - } - } - } - \Else{ - $\text{\UNSHIFTED}_c \leftarrow \text{\SHIFTED}_c$\; - } -} -\Return \UNSHIFTED data\; -\EALGORITHM - -\clearpage - -\subsection{MD5 Sum} -\label{wavpack:md5_sum} -The MD5 is the hash of all the PCM samples, on a PCM frame basis, -in little-endian format and signed if the bits per sample is greater than 0. - -\begin{figure}[h] -\includegraphics{figures/wavpack/md5sum.pdf} -\end{figure} - -\subsection{RIFF WAVE Header and Footer} - -These sub-blocks are typically found in the first and last -WavPack block, respectively. -The header must always be present in the file while -the footer is optional. - -\begin{figure}[h] -\includegraphics{figures/wavpack/pcm_sandwich.pdf} -\end{figure} - -\clearpage - -\section{WavPack Encoding} - -\ALGORITHM{PCM frames, Wave header\footnote{Everything between the file's start and the start of the \texttt{data} chunk's contents. If one is encoding a WavPack from raw PCM input, this header will need to be generated.}, optional Wave footer\footnote{Everything between the end of the \texttt{data} chunk's contents and the file's end, if anything.}, encoding parameters: -\newline -{\relsize{-1} -\begin{tabular}{rll} -parameter & possible values & typical values \\ -\hline -block size & a positive number of PCM frames & 22050 \\ -correlation passes & 0, 1, 2, 5, 10 or 16 & 5 \\ -\end{tabular} -} -}{an encoded WavPack file} -\SetKwData{BLOCKSIZE}{block size} -\SetKwData{BLOCKINDEX}{block index} -\SetKwData{BLOCKSETCOUNT}{block count} -\SetKwData{BLOCKSETCHANNELS}{block channels} -\SetKwData{PASSES}{correlation passes} -\SetKwData{PARAMS}{block params} -\SetKwData{FIRST}{first} -\SetKwData{LAST}{last} -\SetKwData{BLOCK}{block} -\SetKwData{CHANNELS}{channels} -\SetKwData{CHANNEL}{channel} -$(\text{\BLOCKSETCOUNT}~,~\text{\BLOCKSETCHANNELS}) \leftarrow$ \hyperref[wavpack:block_split]{determine block split}\; -\For{$b \leftarrow 0$ \emph{\KwTo}\BLOCKSETCOUNT}{ - $\text{\PARAMS}_{0~b} \leftarrow$ \hyperref[wavpack:initial_correlation_parameters]{determine initial correlation parameters and entropy variables from correlation passes and $\text{\BLOCKSETCHANNELS}_b$}\; -} -\BlankLine -$\text{\BLOCKINDEX} \leftarrow 0$\; -$s \leftarrow 0$\tcc*[r]{the number of block sets written} -\While{PCM frames remain}{ - $\text{\CHANNELS} \leftarrow$ take up to \BLOCKSIZE PCM frames from the input\; - update the stream's MD5 sum with that PCM data\; - $c \leftarrow 0$\; - \For(\tcc*[f]{blocks in each set}){$b \leftarrow 0$ \emph{\KwTo}\BLOCKSETCOUNT}{ - \lIf{$b = 0$}{$\text{\FIRST} = 1$} - \lElse{$\text{\FIRST} = 0$}\; - \lIf{$b = \text{\BLOCKSETCOUNT} - 1$}{$\text{\LAST} = 1$} - \lElse{$\text{\LAST} = 0$}\; - \uIf{$\text{\BLOCKSETCHANNELS}_b = 1$}{ - $(\text{\BLOCK}_{(s \times \text{\BLOCKSETCHANNELS}) + b}~,~\text{\PARAMS}_{(s + 1)~b}) \leftarrow$ \hyperref[wavpack:write_block]{write block}\newline - using $\text{\CHANNEL}_c$, \BLOCKINDEX, \FIRST, \LAST and $\text{\PARAMS}_{s~b}$\; - $c \leftarrow c + 1$\; - } - \ElseIf{$\text{\BLOCKSETCHANNELS}_b = 2$}{ - $(\text{\BLOCK}_{(s \times \text{\BLOCKSETCHANNELS}) + b}~,~\text{\PARAMS}_{(s + 1)~b}) \leftarrow$ \hyperref[wavpack:write_block]{write block}\newline - using $\text{\CHANNEL}_c/\text{\CHANNEL}_{c + 1}$, \BLOCKINDEX, \FIRST, \LAST and $\text{\PARAMS}_{s~b}$\; - $c \leftarrow c + 2$\; - } - } - $s \leftarrow s + 1$\; - $\text{\BLOCKINDEX} \leftarrow \text{\BLOCKINDEX} + \text{PCM data's frame count}$\; -} -\BlankLine -write final block containing optional \hyperref[wavpack:write_wave_header]{Wave footer} and \hyperref[wavpack:write_md5]{MD5 sum} sub blocks\; -update Wave header's \texttt{data} chunk size, if generated from scratch\; -update \VAR{total samples} field in all block headers with \BLOCKINDEX\; -\EALGORITHM - -\clearpage - -\subsection{Determine Block Split} -\label{wavpack:block_split} -\ALGORITHM{input stream's channel assignment}{number of blocks per set, list of channel counts per block} -\SetKwData{BLOCKCOUNT}{block count} -\SetKwData{BLOCKCHANNELS}{block channels} -\Switch(\tcc*[f]{split channels by left/right pairs}){channel assignment}{ - \uCase{mono}{ - $\text{\BLOCKCOUNT} \leftarrow 1$\; - $\text{\BLOCKCHANNELS} \leftarrow \texttt{[1]}$\; - } - \uCase{front left, front right}{ - $\text{\BLOCKCOUNT} \leftarrow 1$\; - $\text{\BLOCKCHANNELS} \leftarrow \texttt{[2]}$\; - } - \uCase{front left, front right, front center}{ - $\text{\BLOCKCOUNT} \leftarrow 2$\; - $\text{\BLOCKCHANNELS} \leftarrow \texttt{[2, 1]}$\; - } - \uCase{front left, front right, back left, back right}{ - $\text{\BLOCKCOUNT} \leftarrow 2$\; - $\text{\BLOCKCHANNELS} \leftarrow \texttt{[2, 2]}$\; - } - \uCase{front left, front right, front center, back center}{ - $\text{\BLOCKCOUNT} \leftarrow 3$\; - $\text{\BLOCKCHANNELS} \leftarrow \texttt{[2, 1, 1]}$\; - } - \uCase{front left, front right, front center, back left, back right}{ - $\text{\BLOCKCOUNT} \leftarrow 3$\; - $\text{\BLOCKCHANNELS} \leftarrow \texttt{[2, 1, 2]}$\; - } - \uCase{front left, front right, front center, LFE, back left, back right}{ - $\text{\BLOCKCOUNT} \leftarrow 4$\; - $\text{\BLOCKCHANNELS} \leftarrow \texttt{[2, 1, 1, 2]}$\; - } - \Other(\tcc*[f]{save them independently}){ - $\text{\BLOCKCOUNT} \leftarrow$ channel count\; - $\text{\BLOCKCHANNELS} \leftarrow$ 1 per channel\; - } -} -\Return \BLOCKCOUNT and \BLOCKCHANNELS -\EALGORITHM -\vskip 1ex -\par -\noindent -One could invent alternate channel splits for other obscure assignments. -WavPack's only requirement is that all channels must be in -Wave order\footnote{see page \pageref{wave_channel_assignment}} -and each block must contain 1 or 2 channels. - -\begin{figure}[h] -\includegraphics{figures/wavpack/block_channels.pdf} -\end{figure} - -\begin{landscape} - -\subsection{Determine Correlation Parameters and Entropy Variables} -\label{wavpack:initial_correlation_parameters} -{\relsize{-1} -\begin{description} -\item[$\text{term}_{b~p}$] correlation term for block $b$, correlation pass $p$ -\item[$\text{delta}_{b~p}$] correlation delta for block $b$, correlation pass $p$ -\item[$\text{weight}_{b~p~c}$] correlation weight for block $b$, correlation pass $p$, channel $c$ -\item[$\text{sample}_{b~p~c~s}$] correlation sample $s$ for block $b$, correlation pass $p$, channel $c$ -\item[$\text{entropy}_{b~c~m}$] median $m$ for block $b$, channel $c$ -\end{description} -\par -\noindent -We'll omit the block $b$ parameter since it will be the same -throughout the block encode, but one must keep it in mind -when transferring parameters from the block of one set of channels -to the next block of those same channels. -} -\vskip .10in -\par -\noindent -\ALGORITHM{correlation pass count, block's channel count of 1 or 2}{correlation term, delta, weights and samples for each pass; 3 entropy variables for each channel} -{\relsize{-2} -$\text{entropy}_0 \leftarrow \texttt{[0, 0, 0]}$\; -$\text{entropy}_1 \leftarrow \texttt{[0, 0, 0]}$\; -\BlankLine -\If{$\text{channel count} = 1$}{ -\Switch{correlation pass count}{ -\uCase{1}{ -\begin{tabular}{r|rrrl} -$\text{pass}~p$ & $\text{term}_p$ & $\text{delta}_p$ & $\text{weight}_{p~0}$ & $\text{samples}_{p~0}$ \\ -\hline -0 & 18 & 2 & 0 & \texttt{[0, 0]} \\ -\end{tabular} -} -\uCase{2}{ -\begin{tabular}{r|rrrl} -$\text{pass}~p$ & $\text{term}_p$ & $\text{delta}_p$ & $\text{weight}_{p~0}$ & $\text{samples}_{p~0}$ \\ -\hline -0 & 17 & 2 & 0 & \texttt{[0, 0]} \\ -1 & 18 & 2 & 0 & \texttt{[0, 0]} \\ -\end{tabular} -} -\uCase(\tcc*[f]{one channel blocks don't use negative terms}){5, 10, or 16}{ -\begin{tabular}{r|rrrl} -$\text{pass}~p$ & $\text{term}_p$ & $\text{delta}_p$ & $\text{weight}_{p~0}$ & $\text{samples}_{p~0}$ \\ -\hline -0 & 3 & 2 & 0 & \texttt{[0, 0, 0]} \\ -1 & 17 & 2 & 0 & \texttt{[0, 0]} \\ -2 & 2 & 2 & 0 & \texttt{[0, 0]} \\ -3 & 18 & 2 & 0 & \texttt{[0, 0]} \\ -4 & 18 & 2 & 0 & \texttt{[0, 0]} \\ -\end{tabular} -}}}} -\EALGORITHM - -\clearpage - -\begin{algorithm} -{\relsize{-2} -\ElseIf{$\text{channel count} = 2$}{ -\Switch{correlation pass count}{ -\uCase{1}{ -\begin{tabular}{r|rrrrll} -$\text{pass}~p$ & $\text{term}_p$ & $\text{delta}_p$ & $\text{weight}_{p~0}$ & $\text{weight}_{p~1}$ & $\text{samples}_{p~0}$ & $\text{samples}_{p~1}$ \\ -\hline -0 & 18 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -\end{tabular} -} -\uCase{2}{ -\begin{tabular}{r|rrrrll} -$\text{pass}~p$ & $\text{term}_p$ & $\text{delta}_p$ & $\text{weight}_{p~0}$ & $\text{weight}_{p~1}$ & $\text{samples}_{p~0}$ & $\text{samples}_{p~1}$ \\ -\hline -0 & 17 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -1 & 18 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -\end{tabular} -} -\uCase{5}{ -\begin{tabular}{r|rrrrll} -$\text{pass}~p$ & $\text{term}_p$ & $\text{delta}_p$ & $\text{weight}_{p~0}$ & $\text{weight}_{p~1}$ & $\text{samples}_{p~0}$ & $\text{samples}_{p~1}$ \\ -\hline -0 & 3 & 2 & 48 & 48 & \texttt{[0, 0, 0]} & \texttt{[0, 0, 0]} \\ -1 & 17 & 2 & 48 & 48 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -2 & 2 & 2 & 32 & 32 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -3 & 18 & 2 & 48 & 48 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -4 & 18 & 2 & 16 & 24 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -\end{tabular} -} -\uCase{10}{ -\begin{tabular}{r|rrrrll} -$\text{pass}~p$ & $\text{term}_p$ & $\text{delta}_p$ & $\text{weight}_{p~0}$ & $\text{weight}_{p~1}$ & $\text{samples}_{p~0}$ & $\text{samples}_{p~1}$ \\ -\hline -0 & 4 & 2 & 0 & 0 & \texttt{[0, 0, 0, 0]} & \texttt{[0, 0, 0, 0]} \\ -1 & 17 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -2 & -1 & 2 & 0 & 0 & \texttt{[0]} & \texttt{[0]} \\ -3 & 5 & 2 & 0 & 0 & \texttt{[0, 0, 0, 0, 0]} & \texttt{[0, 0, 0, 0, 0]} \\ -4 & 3 & 2 & 0 & 0 & \texttt{[0, 0, 0]} & \texttt{[0, 0, 0]} \\ -5 & 2 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -6 & -2 & 2 & 0 & 0 & \texttt{[0]} & \texttt{[0]} \\ -7 & 18 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -8 & 18 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -9 & 18 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -\end{tabular} -} -\Case{16}{ -\begin{tabular}{r|rrrrll} -$\text{pass}~p$ & $\text{term}_p$ & $\text{delta}_p$ & $\text{weight}_{p~0}$ & $\text{weight}_{p~1}$ & $\text{samples}_{p~0}$ & $\text{samples}_{p~1}$ \\ -\hline -0 & 2 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -1 & 18 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -2 & -1 & 2 & 0 & 0 & \texttt{[0]} & \texttt{[0]} \\ -3 & 8 & 2 & 0 & 0 & \texttt{[0, 0, 0, 0, 0, 0, 0, 0]} & \texttt{[0, 0, 0, 0, 0, 0, 0, 0]} \\ -4 & 6 & 2 & 0 & 0 & \texttt{[0, 0, 0, 0, 0, 0]} & \texttt{[0, 0, 0, 0, 0, 0]} \\ -5 & 3 & 2 & 0 & 0 & \texttt{[0, 0, 0]} & \texttt{[0, 0, 0]} \\ -6 & 5 & 2 & 0 & 0 & \texttt{[0, 0, 0, 0, 0]} & \texttt{[0, 0, 0, 0, 0]} \\ -7 & 7 & 2 & 0 & 0 & \texttt{[0, 0, 0, 0, 0, 0, 0]} & \texttt{[0, 0, 0, 0, 0, 0, 0]} \\ -8 & 4 & 2 & 0 & 0 & \texttt{[0, 0, 0, 0]} & \texttt{[0, 0, 0, 0]} \\ -9 & 2 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -10 & 18 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -11 & -2 & 2 & 0 & 0 & \texttt{[0]} & \texttt{[0]} \\ -12 & 3 & 2 & 0 & 0 & \texttt{[0, 0, 0]} & \texttt{[0, 0, 0]} \\ -13 & 2 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -14 & 18 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -15 & 18 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ -\end{tabular} -}}}} -\end{algorithm} - -\end{landscape} - -%% \subsection{Writing Block Set} -%% {\relsize{-1} -%% \ALGORITHM{PCM frames and their channel assignment, block index, encoding parameters}{one or more WavPack blocks} -%% \SetKwData{CHANNEL}{channel} -%% \SetKwData{FIRST}{first} -%% \SetKwData{LAST}{last} -%% \Switch(\tcc*[f]{split channels by left/right pairs}){channel assignment}{ -%% \uCase{mono}{ -%% \begin{tabular}{lrr} -%% write $\text{\CHANNEL}_0$ & $\text{\FIRST} = 1$ & $\text{\LAST} = 1$ \\ -%% \end{tabular}\; -%% } -%% \uCase{front left, front right}{ -%% \begin{tabular}{lrr} -%% write $\text{\CHANNEL}_0/\text{\CHANNEL}_1$ & $\text{\FIRST} = 1$ & $\text{\LAST} = 1$ \\ -%% \end{tabular}\; -%% } -%% \uCase{front left, front right, front center}{ -%% \begin{tabular}{lrr} -%% write $\text{\CHANNEL}_0/\text{\CHANNEL}_1$ & $\text{\FIRST} = 1$ & $\text{\LAST} = 0$ \\ -%% write $\text{\CHANNEL}_2$ & $\text{\FIRST} = 0$ & $\text{\LAST} = 1$ \\ -%% \end{tabular}\; -%% } -%% \uCase{front left, front right, back left, back right}{ -%% \begin{tabular}{lrr} -%% write $\text{\CHANNEL}_0/\text{\CHANNEL}_1$ & $\text{\FIRST} = 1$ & $\text{\LAST} = 0$ \\ -%% write $\text{\CHANNEL}_2/\text{\CHANNEL}_3$ & $\text{\FIRST} = 0$ & $\text{\LAST} = 1$ \\ -%% \end{tabular}\; -%% } -%% \uCase{front left, front right, front center, back center}{ -%% \begin{tabular}{lrr} -%% write $\text{\CHANNEL}_0/\text{\CHANNEL}_1$ & $\text{\FIRST} = 1$ & $\text{\LAST} = 0$ \\ -%% write $\text{\CHANNEL}_2$ & $\text{\FIRST} = 0$ & $\text{\LAST} = 0$ \\ -%% write $\text{\CHANNEL}_3$ & $\text{\FIRST} = 0$ & $\text{\LAST} = 1$ \\ -%% \end{tabular}\; -%% } -%% \uCase{front left, front right, front center, back left, back right}{ -%% \begin{tabular}{lrr} -%% write $\text{\CHANNEL}_0/\text{\CHANNEL}_1$ & $\text{\FIRST} = 1$ & $\text{\LAST} = 0$ \\ -%% write $\text{\CHANNEL}_2$ & $\text{\FIRST} = 0$ & $\text{\LAST} = 0$ \\ -%% write $\text{\CHANNEL}_3/\text{\CHANNEL}_4$ & $\text{\FIRST} = 0$ & $\text{\LAST} = 1$ \\ -%% \end{tabular}\; -%% } -%% \uCase{front left, front right, front center, LFE, back left, back right}{ -%% \begin{tabular}{lrr} -%% write $\text{\CHANNEL}_0/\text{\CHANNEL}_1$ & $\text{\FIRST} = 1$ & $\text{\LAST} = 0$ \\ -%% write $\text{\CHANNEL}_2$ & $\text{\FIRST} = 0$ & $\text{\LAST} = 0$ \\ -%% write $\text{\CHANNEL}_3$ & $\text{\FIRST} = 0$ & $\text{\LAST} = 0$ \\ -%% write $\text{\CHANNEL}_4/\text{\CHANNEL}_5$ & $\text{\FIRST} = 0$ & $\text{\LAST} = 1$ \\ -%% \end{tabular}\; -%% } -%% \Other(\tcc*[f]{save them independently}){ -%% \For{$i \leftarrow 0$ \emph{\KwTo}channel count}{ -%% \lIf{$i = 0$}{$\text{\FIRST} = 1$} -%% \lElse{$\text{\FIRST} = 0$}\; -%% \lIf{$i = \text{channel count} - 1$}{$\text{\LAST} = 1$} -%% \lElse{$\text{\LAST} = 0$}\; -%% write $\text{\CHANNEL}_i$ to block\; -%% } -%% } -%% } -%% \Return set of encoded WavPack blocks\; -%% \EALGORITHM - - -%% \clearpage - -\subsection{Writing Block} -\label{wavpack:write_block} -{\relsize{-1} -\ALGORITHM{1 or 2 channels of PCM frames, block index, first block, last block, encoding parameters from previous block}{a WavPack block, encoding parameters for next block} -\SetKwData{CHANNEL}{channel} -\SetKwData{MONO}{mono output} -\SetKwData{FALSESTEREO}{false stereo} -\SetKwData{MAGNITUDE}{magnitude} -\SetKwData{WASTEDBPS}{wasted bps} -\SetKwData{SHIFTED}{shifted} -\SetKwData{CRC}{CRC} -\SetKwData{CORRELATED}{correlated} -\SetKwData{MID}{mid} -\SetKwData{SIDE}{side} -\SetKwData{TERMS}{terms} -\SetKwData{DELTAS}{deltas} -\SetKwData{WEIGHTS}{weights} -\SetKwData{SAMPLES}{samples} -\SetKwData{BITSTREAM}{bitstream} -\SetKwData{ENTROPY}{entropy} -\SetKw{OR}{or} -\SetKwFunction{MAX}{max} -\SetKwFunction{MIN}{min} -\eIf(\tcc*[f]{1 channel block}){$\text{channel count} = 1$ \OR $\text{\CHANNEL}_0 = \text{\CHANNEL}_1$}{ - \eIf{$\text{channel count} = 1$}{ - $\text{\MONO} \leftarrow 1$\; - $\text{\FALSESTEREO} \leftarrow 0$\; - }{ - $\text{\MONO} \leftarrow 0$\; - $\text{\FALSESTEREO} \leftarrow 1$\; - } - $\text{\MAGNITUDE} \leftarrow$ \hyperref[wavpack:maximum_magnitude]{maximum magnitude of $\text{\CHANNEL}_0$}\; - $\text{\WASTEDBPS} \leftarrow$ \hyperref[wavpack:wasted_bps]{wasted bps of $\text{\CHANNEL}_0$}\; - \eIf{$\text{\WASTEDBPS} > 0$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}block size}{ - $\text{\SHIFTED}_{0~i} \leftarrow \lfloor\text{\CHANNEL}_{0~i} \div 2 ^ \text{\WASTEDBPS}\rfloor$\; - } - }{ - $\text{\SHIFTED}_0 \leftarrow \text{\CHANNEL}_0$\; - } - $\text{\CRC} \leftarrow$ \hyperref[wavpack:calc_crc]{calculate CRC of $\text{\SHIFTED}_0$}\; -}(\tcc*[f]{2 channel block}){ - $\text{\MONO} \leftarrow 0$\; - $\text{\FALSESTEREO} \leftarrow 0$\; - $\text{\MAGNITUDE} \leftarrow \hyperref[wavpack:maximum_magnitude]{\MAX(\text{maximum magnitude of \CHANNEL}_0~,~\text{maximum magnitude of \CHANNEL}_1)}$\; - $\text{\WASTEDBPS} \leftarrow \hyperref[wavpack:wasted_bps]{\MIN(\text{wasted bps of \CHANNEL}_0~,~\text{wasted bps of \CHANNEL}_1)}$\; - \eIf{$\text{\WASTEDBPS} > 0$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}block size}{ - $\text{\SHIFTED}_{0~i} \leftarrow \lfloor\text{\CHANNEL}_{0~i} \div 2 ^ \text{\WASTEDBPS}\rfloor$\; - $\text{\SHIFTED}_{1~i} \leftarrow \lfloor\text{\CHANNEL}_{1~i} \div 2 ^ \text{\WASTEDBPS}\rfloor$\; - } - }{ - $\text{\SHIFTED}_0/\text{\SHIFTED}_1 \leftarrow \text{\CHANNEL}_0/\text{\CHANNEL}_1$\; - } - $\text{\CRC} \leftarrow$ \hyperref[wavpack:calc_crc]{calculate CRC of $\text{\SHIFTED}_0/\text{\SHIFTED}_1$}\; - $\text{\MID}/\text{\SIDE} \leftarrow$ \hyperref[wavpack:calc_joint_stereo]{convert $\text{\SHIFTED}_0/\text{\SHIFTED}_1$ to joint stereo}\; -} -\EALGORITHM -} - -\begin{figure}[h] - \includegraphics{figures/wavpack/typical_block.pdf} -\end{figure} - -\clearpage -{\relsize{-1} -\begin{algorithm}[H] -\DontPrintSemicolon -\SetKwData{CHANNEL}{channel} -\SetKwData{CORRELATED}{correlated} -\SetKwData{SHIFTED}{shifted} -\SetKwData{MID}{mid} -\SetKwData{SIDE}{side} -\SetKwData{SUBBLOCK}{sub block} -\SetKwData{WASTEDBPS}{wasted bps} -\SetKwData{BITSTREAM}{bitstream} -\SetKwData{TERMS}{terms} -\SetKwData{DELTAS}{deltas} -\SetKwData{WEIGHTS}{weights} -\SetKwData{SAMPLES}{samples} -\SetKwData{ENTROPY}{entropy} -\SetKw{NOT}{not} -\SetKw{IN}{in} -\SetKw{OR}{or} -$i \leftarrow 0$\; -\If{first block in file}{ - $\text{\SUBBLOCK}_i \leftarrow$ \hyperref[wavpack:write_wave_header]{wave header}\; - $i \leftarrow i + 1$\; -} -\If{$\text{decorrelation passes} > 0$}{ - $\text{\SUBBLOCK}_i \leftarrow$ \hyperref[wavpack:write_decorr_terms]{decorrelation terms sub block from \TERMS and \DELTAS}\; - $\text{\SUBBLOCK}_{i + 1} \leftarrow$ \hyperref[wavpack:write_decorr_weights]{decorrelation weights sub block from \WEIGHTS}\; - $\text{\SUBBLOCK}_{i + 2} \leftarrow$ \hyperref[wavpack:write_decorr_samples]{decorrelation samples sub block from \SAMPLES}\; - $i \leftarrow i + 3$\; -} -\If{$\text{\WASTEDBPS} > 0$}{ - $\text{\SUBBLOCK}_i \leftarrow$ \hyperref[wavpack:write_extended_integers]{extended integers}\; - $i \leftarrow i + 1$\; -} -\If{$\text{total channel count} > 2$}{ - $\text{\SUBBLOCK}_i \leftarrow$ \hyperref[wavpack:write_channel_info]{channel info}\; - $i \leftarrow i + 1$\; -} -\If{sample rate not defined in block header}{ - $\text{\SUBBLOCK}_i \leftarrow$ \hyperref[wavpack:write_sample_rate]{sample rate}\; - $i \leftarrow i + 1$\; -} -\eIf(\tcc*[f]{1 channel block}){$\text{channel count} = 1$ \OR $\text{\CHANNEL}_0 = \text{\CHANNEL}_1$}{ - $(\text{\CORRELATED}_0~,~\text{\WEIGHTS}'~,~\text{\SAMPLES}') \leftarrow$ \hyperref[wavpack:correlate_channels]{correlate $\text{\SHIFTED}_0$\newline - with \TERMS, \DELTAS, \WEIGHTS and \SAMPLES from encoding parameters}\; - $\text{\SUBBLOCK}_i \leftarrow$ \hyperref[wavpack:write_entropy]{entropy variables sub block from \ENTROPY}\; - $(\text{\BITSTREAM}~,~\text{\ENTROPY}') \leftarrow$ \hyperref[wavpack:write_bitstream]{calculate bitstream from $\text{\CORRELATED}_0$\newline - and $\text{\ENTROPY}$ from encoding parameters}\; - \BlankLine - $\text{\SUBBLOCK}_{i + 1} \leftarrow$ bitstream sub block from \BITSTREAM\; -}(\tcc*[f]{2 channel block}){ - $(\text{\CORRELATED}_0/\text{\CORRELATED}_1~,~\text{\WEIGHTS}'~,~\text{\SAMPLES}') \leftarrow$ \hyperref[wavpack:correlate_channels]{correlate $\text{\MID}/\text{\SIDE}$\newline - with \TERMS, \DELTAS, \WEIGHTS and \SAMPLES from encoding parameters}\; - \BlankLine - $\text{\SUBBLOCK}_i \leftarrow$ \hyperref[wavpack:write_entropy]{entropy variables sub block from \ENTROPY}\; - $(\text{\BITSTREAM}~,~\text{\ENTROPY}') \leftarrow$ \hyperref[wavpack:write_bitstream]{calculate bitstream from $\text{\CORRELATED}_0/\text{\CORRELATED}_0$\newline - and $\text{\ENTROPY}$ from encoding parameters}\; - $\text{\SUBBLOCK}_{i + 1} \leftarrow$ bitstream sub block from \BITSTREAM\; -} -$i \leftarrow i + 2$\; -\BlankLine -\hyperref[wavpack:write_block_header]{write block header}\; -\For{$j \leftarrow 0$ \emph{\KwTo}i}{ - write $\text{\SUBBLOCK}_j$\; -} -\Return block data, $\text{\WEIGHTS}'$, $\text{\SAMPLES}'$ and $\text{\ENTROPY}'$ -\end{algorithm} -} - -\clearpage - -\subsection{Calculating Maximum Magnitude} -\label{wavpack:maximum_magnitude} -{\relsize{-1} -\ALGORITHM{a list of signed PCM samples for a single channel}{an unsigned integer} -\SetKwData{MAXMAGNITUDE}{maximum magnitude} -\SetKwData{SAMPLE}{sample} -\SetKwFunction{MAX}{max} -\SetKwFunction{BITS}{bits} -$\text{\MAXMAGNITUDE} \leftarrow 0$\; -\For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ - $\text{\MAXMAGNITUDE} \leftarrow \MAX(\BITS(|\text{\SAMPLE}_i|)~,~\text{\MAXMAGNITUDE})$\; -} -\Return \MAXMAGNITUDE\; -\EALGORITHM -where the \texttt{bits} function is defined as: -\begin{equation*} -\texttt{bits}(x) = -\begin{cases} -0 & \text{if } x = 0 \\ -1 + \texttt{bits}(\lfloor x \div 2 \rfloor) & \text{if } x > 0 -\end{cases} -\end{equation*} -} -\subsubsection{Maximum Magnitude Example} -\begin{table}[h] -{\relsize{-1} -\begin{tabular}{r|rrrrrrrrrr} -$i$ & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\ -\hline -$\text{sample}_i$ & 0 & 16 & 31 & 44 & 54 & 61 & 64 & 63 & 58 & 49 \\ -$\texttt{bits}(|\text{sample}_i|)$ & 0 & 5 & 5 & 6 & 6 & 6 & 7 & 6 & 6 & 6 -\end{tabular} -} -\end{table} -\par -\noindent -for a maximum magnitude of 7. - -\subsection{Calculating Wasted Bits Per Sample} -\label{wavpack:wasted_bps} -{\relsize{-1} -\ALGORITHM{a list of signed PCM samples for a single channel}{an unsigned integer} -\SetKwData{WASTEDBPS}{wasted bps} -\SetKwData{SAMPLE}{sample} -\SetKwFunction{MIN}{min} -\SetKwFunction{WASTED}{wasted} -$\text{\WASTEDBPS} \leftarrow \infty$\tcc*[r]{maximum unsigned integer} -\For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ - $\text{\WASTEDBPS} \leftarrow \MIN(\WASTED(\text{\SAMPLE}_i)~,~\text{\WASTEDBPS})$\; -} -\eIf(\tcc*[f]{all samples are 0}){$\WASTEDBPS = \infty$}{ - \Return 0\; -}{ - \Return \WASTEDBPS\; -} -\EALGORITHM -where the \texttt{wasted} function is defined as: -\begin{equation*} -\texttt{wasted}(x) = -\begin{cases} -\infty & \text{if } x = 0 \\ -0 & \text{if } x \bmod 2 = 1 \\ -1 + \texttt{wasted}(x \div 2) & \text{if } x \bmod 2 = 0 \\ -\end{cases} -\end{equation*} -} -\subsubsection{Wasted Bits Example} -\begin{table}[h] -{\relsize{-1} -\begin{tabular}{r|rrrrrrrrrr} -$i$ & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\ -\hline -$\text{sample}_i$ & 0 & 16 & 31 & 44 & 54 & 61 & 64 & 63 & 58 & 49 \\ -$\texttt{wasted}(\text{sample}_i)$ & $\infty$ & 4 & 0 & 2 & 1 & 0 & 6 & 0 & 1 & 0 \\ -\end{tabular} -} -\end{table} -\par -\noindent -for a wasted bps of 0 (which is typical). - -\begin{landscape} - -\subsection{Calculating CRC} -\label{wavpack:calc_crc} -\ALGORITHM{one or two channels of signed audio samples}{an unsigned 32-bit CRC integer} -\SetKwData{MONO}{mono output} -\SetKwData{CRC}{CRC} -\SetKwData{LCRC}{LCRC} -\SetKwData{SCRC}{SCRC} -\SetKwData{CHANNEL}{channel} -$\text{\CRC}_{-1} \leftarrow \texttt{0xFFFFFFFF}$\; -\For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ - \eIf{$\text{\MONO} = 0$}{ - $\text{\LCRC}_i \leftarrow (3 \times \text{\CRC}_{i - 1}) + \text{\CHANNEL}_{0~i}$\tcc*[r]{calculate signed CRC of left channel} - $\text{\SCRC}_i \leftarrow (3 \times \text{\LCRC}_{i - 1}) + \text{\CHANNEL}_{1~i}$\tcc*[r]{calculate signed CRC of right channel} - }{ - $\text{\SCRC}_i \leftarrow (3 \times \text{\CRC}_{i - 1}) + \text{\CHANNEL}_{0~i}$\tcc*[r]{calculate signed CRC of channel} - } - \BlankLine - \eIf(\tcc*[f]{convert signed CRC to unsigned, 32-bit integer}){$\text{\SCRC}_i \geq 0$}{ - $\text{\CRC}_i \leftarrow \text{\SCRC}_i \bmod \texttt{0x100000000}$\; - }{ - $\text{\CRC}_i \leftarrow (2 ^ {32} - (-\text{\SCRC}_i)) \bmod \texttt{0x100000000}$\; - } -} -\Return $\text{\CRC}_{\text{sample count} - 1}$\; -\EALGORITHM - -\subsubsection{Checksum Calculation Example} -{\relsize{-1} -\begin{tabular}{|r|r|r||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|} -$i$ & $\textsf{channel}_{0~i}$ & $\textsf{channel}_{1~i}$ & \textsf{LCRC}_i & \textsf{SCRC}_i & \textsf{CRC}_i \\ -\hline -0 & 0 & 64 & -(3 \times \texttt{0xFFFFFFFF}) + 0 = \texttt{0x2FFFFFFFD} & -(3 \times \texttt{0x2FFFFFFFD}) + 64 = \texttt{0x900000037} & -\texttt{0x00000037} \\ -1 & 16 & 62 & -(3 \times \texttt{0x00000037}) + 16 = \texttt{0x000000B5} & -(3 \times \texttt{0x000000B5}) + 62 = \texttt{0x0000025D} & -\texttt{0x0000025D} \\ -2 & 31 & 56 & -(3 \times \texttt{0x0000025D}) + 31 = \texttt{0x00000736} & -(3 \times \texttt{0x00000736}) + 56 = \texttt{0x000015DA} & -\texttt{0x000015DA} \\ -3 & 44 & 47 & -(3 \times \texttt{0x000015DA}) + 44 = \texttt{0x000041BA} & -(3 \times \texttt{0x000041BA}) + 47 = \texttt{0x0000C55D} & -\texttt{0x0000C55D} \\ -4 & 54 & 34 & -(3 \times \texttt{0x0000C55D}) + 54 = \texttt{0x0002504D} & -(3 \times \texttt{0x0002504D}) + 34 = \texttt{0x0006F109} & -\texttt{0x0006F109} \\ -5 & 61 & 20 & -(3 \times \texttt{0x0006F109}) + 61 = \texttt{0x0014D358} & -(3 \times \texttt{0x0014D358}) + 20 = \texttt{0x003E7A1C} & -\texttt{0x003E7A1C} \\ -6 & 64 & 4 & -(3 \times \texttt{0x003E7A1C}) + 64 = \texttt{0x00BB6E94} & -(3 \times \texttt{0x00BB6E94}) + 4 = \texttt{0x02324BC0} & -\texttt{0x02324BC0} \\ -7 & 63 & -12 & -(3 \times \texttt{0x02324BC0}) + 63 = \texttt{0x0696E37F} & -(3 \times \texttt{0x0696E37F}) - 12 = \texttt{0x13C4AA71} & -\texttt{0x13C4AA71} \\ -8 & 58 & -27 & -(3 \times \texttt{0x13C4AA71}) + 58 = \texttt{0x3B4DFF8D} & -(3 \times \texttt{0x3B4DFF8D}) - 27 = \texttt{0xB1E9FE8C} & -\texttt{0xB1E9FE8C} \\ -9 & 49 & -41 & -(3 \times \texttt{0xB1E9FE8C}) + 49 = \texttt{0x215BDFBD5} & -(3 \times \texttt{0x215BDFBD5}) - 41 = \texttt{0x64139F356} & -\texttt{0x4139F356} \\ -\end{tabular} -} -\vskip 1em -\par -\noindent -Resulting in a final CRC of \texttt{0x4139F356} - -\end{landscape} - -\subsection{Joint Stereo Conversion} -\label{wavpack:calc_joint_stereo} -\ALGORITHM{left and right channels of signed integers}{mid and side channels of signed integers} -\SetKwData{LEFT}{left} -\SetKwData{RIGHT}{right} -\SetKwData{MID}{mid} -\SetKwData{SIDE}{side} -\For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ - $\text{\MID}_i \leftarrow \text{\LEFT}_i - \text{\RIGHT}_i$\; - $\text{\SIDE}_i \leftarrow \lfloor(\text{\LEFT}_i + \text{\RIGHT}_i) \div 2\rfloor$\; -} -\Return \MID and \SIDE channels\; -\EALGORITHM - -\subsubsection{Joint Stereo Example} -\begin{table}[h] -{\relsize{-1} -\begin{tabular}{|r|r|r||>{$}r<{$}|>{$}r<{$}|} -$i$ & $\textsf{left}_i$ & $\textsf{right}_i$ & \textsf{mid}_i & \textsf{side}_i \\ -\hline -0 & 0 & 64 & 0 - 64 = -64 & \lfloor(0 + 64) \div 2\rfloor = 32 \\ -1 & 16 & 62 & 16 - 62 = -46 & \lfloor(16 + 62) \div 2\rfloor = 39 \\ -2 & 31 & 56 & 31 - 56 = -25 & \lfloor(31 + 56) \div 2\rfloor = 43 \\ -3 & 44 & 47 & 44 - 47 = -3 & \lfloor(44 + 47) \div 2\rfloor = 45 \\ -4 & 54 & 34 & 54 - 34 = 20 & \lfloor(54 + 34) \div 2\rfloor = 44 \\ -5 & 61 & 20 & 61 - 20 = 41 & \lfloor(61 + 20) \div 2\rfloor = 40 \\ -6 & 64 & 4 & 64 - 4 = 60 & \lfloor(64 + 4) \div 2\rfloor = 34 \\ -7 & 63 & -12 & 63 - -12 = 75 & \lfloor(63 + -12) \div 2\rfloor = 25 \\ -8 & 58 & -27 & 58 - -27 = 85 & \lfloor(58 + -27) \div 2\rfloor = 15 \\ -9 & 49 & -41 & 49 - -41 = 90 & \lfloor(49 + -41) \div 2\rfloor = 4 \\ -\end{tabular} -} -\end{table} - -\clearpage - -\subsection{Correlation Passes} -\label{wavpack:correlate_channels} -\ALGORITHM{a list of signed samples per channel; correlation terms, deltas, weights and samples}{a list of signed residuals per channel; correlation weights and samples for the next block} -\SetKwData{PASS}{pass} -\SetKwData{CHANNEL}{channel} -\SetKwData{TERMCOUNT}{term count} -\SetKwData{TERM}{term} -\SetKwData{DELTA}{delta} -\SetKwData{WEIGHT}{weight} -\SetKwData{SAMPLES}{sample} -\SetKw{KwDownTo}{downto} -\eIf{$\text{channel count} = 1$}{ - $\text{\PASS}_{-1~0} \leftarrow \text{\CHANNEL}_0$\; - \For(\tcc*[f]{perform passes in reverse order}){$i \leftarrow 0$ \emph{\KwTo}\TERMCOUNT}{ - $p \leftarrow \TERMCOUNT - i - 1$\; - $\left.\begin{tabular}{r} - $\text{\PASS}_{i~0}$ \\ - $\text{\WEIGHT}_{p~0}$ \\ - $\text{\SAMPLES}_{p~0}$ \\ - \end{tabular}\right\rbrace \leftarrow$ \hyperref[wavpack:correlate_1ch]{correlate 1 channel $\text{\PASS}_{i - 1}$} using $\left\lbrace\begin{tabular}{l} - $\text{\TERM}_{i}$ \\ - $\text{\DELTA}_{i}$ \\ - $\text{\WEIGHT}_{p~0}$ \\ - $\text{\SAMPLES}_{p~0}$ \\ - \end{tabular}\right.$\; - } - \Return $\text{\PASS}_{(\TERMCOUNT - 1)}$, updated $\text{\WEIGHT}$, updated $\text{\SAMPLES}$\; -}{ - $\text{\PASS}_{-1~0} \leftarrow \text{\CHANNEL}_0$\; - $\text{\PASS}_{-1~1} \leftarrow \text{\CHANNEL}_1$\; - \For(\tcc*[f]{perform passes in reverse order}){$i \leftarrow 0$ \emph{\KwTo}\TERMCOUNT}{ - $p \leftarrow \TERMCOUNT - i - 1$\; - $\left.\begin{tabular}{r} - $\text{\PASS}_{i~0}$ \\ - $\text{\PASS}_{i~1}$ \\ - $\text{\WEIGHT}_{p~0}$ \\ - $\text{\WEIGHT}_{p~1}$ \\ - $\text{\SAMPLES}_{p~0}$ \\ - $\text{\SAMPLES}_{p~1}$ \\ - \end{tabular}\right\rbrace \leftarrow$ \hyperref[wavpack:correlate_2ch]{correlate 2 channel $\text{\PASS}_{i - 1}$} using $\left\lbrace\begin{tabular}{l} - $\text{\TERM}_{i}$ \\ - $\text{\DELTA}_{i}$ \\ - $\text{\WEIGHT}_{p~0}$ \\ - $\text{\WEIGHT}_{p~1}$ \\ - $\text{\SAMPLES}_{p~0}$ \\ - $\text{\SAMPLES}_{p~1}$ \\ - \end{tabular}\right.$\; - } - \Return $\text{\PASS}_{(\TERMCOUNT - 1)}$, updated $\text{\WEIGHT}$, updated $\text{\SAMPLES}$\; -} -\EALGORITHM - -\clearpage - -\subsection{1 Channel Correlation Pass} -\label{wavpack:correlate_1ch} -{\relsize{-1} -\ALGORITHM{a list of signed uncorrelated samples; correlation term, delta, weight and samples}{a list of signed correlated samples; updated weight and sample values} -\SetKwData{CORRELATED}{correlated} -\SetKwData{DECORRELATED}{uncorrelated} -\SetKwData{DECORRSAMPLE}{correlation sample} -\SetKwData{WEIGHT}{weight} -\SetKwData{DELTA}{delta} -\SetKwData{TEMP}{temp} -\SetKw{OR}{or} -\SetKw{XOR}{xor} -\SetKwFunction{APPLYWEIGHT}{apply\_weight} -\SetKwFunction{UPDATEWEIGHT}{update\_weight} -$\text{\WEIGHT}_0 \leftarrow$ decorrelation weight\; -\BlankLine -\uIf{$term = 18$}{ - $\text{\DECORRELATED}_{-2} \leftarrow \text{\DECORRSAMPLE}_1$\; - $\text{\DECORRELATED}_{-1} \leftarrow \text{\DECORRSAMPLE}_0$\; - \For{$i \leftarrow 0$ \emph{\KwTo}uncorrelated samples length}{ - $\text{\TEMP}_{i} \leftarrow \lfloor(3 \times \text{\DECORRELATED}_{i - 1} - \text{\DECORRELATED}_{i - 2}) \div 2 \rfloor$\; - $\text{\CORRELATED}_i \leftarrow \text{\DECORRELATED}_i - \APPLYWEIGHT(\text{\WEIGHT}_i~,~\text{\TEMP}_{i})$\; - $\text{\WEIGHT}_{i + 1} \leftarrow \text{\WEIGHT}_i + \UPDATEWEIGHT(\text{\TEMP}_{i}~,~\text{\CORRELATED}_i~,~\DELTA)$\; - } - \Return $\left\lbrace\begin{tabular}{l} - \CORRELATED \\ - $\text{\WEIGHT}_i$ \\ - $\DECORRSAMPLE \leftarrow$ \texttt{[}$\text{\DECORRELATED}_{(i - 2)}$, $\text{\DECORRELATED}_{(i - 1)}$ \texttt{]} \\ - \end{tabular}\right.$\; -} -\uElseIf{$term = 17$}{ - $\text{\DECORRELATED}_{-2} \leftarrow \text{\DECORRSAMPLE}_1$\; - $\text{\DECORRELATED}_{-1} \leftarrow \text{\DECORRSAMPLE}_0$\; - \For{$i \leftarrow 0$ \emph{\KwTo}uncorrelated samples length}{ - $\text{\TEMP}_{i} \leftarrow 2 \times \text{\DECORRELATED}_{i - 1} - \text{\DECORRELATED}_{i - 2}$\; - $\text{\CORRELATED}_i \leftarrow \text{\DECORRELATED}_i - \APPLYWEIGHT(\text{\WEIGHT}_i~,~\text{\TEMP}_{i})$\; - $\text{\WEIGHT}_{i + 1} \leftarrow \text{\WEIGHT}_i + \UPDATEWEIGHT(\text{\TEMP}_{i}~,~\text{\CORRELATED}_i~,~\DELTA)$\; - } - \Return $\left\lbrace\begin{tabular}{l} - \CORRELATED \\ - $\text{\WEIGHT}_i$ \\ - $\DECORRSAMPLE \leftarrow$ \texttt{[}$\text{\DECORRELATED}_{(i - 2)}$, $\text{\DECORRELATED}_{(i - 1)}$ \texttt{]} \\ - \end{tabular}\right.$\; -} -\uElseIf{$1 \leq term \leq 8$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}term}{ - $\text{\DECORRELATED}_{i - \text{term}} \leftarrow \text{\DECORRSAMPLE}_i$\; - } - \For{$i \leftarrow 0$ \emph{\KwTo}correlated samples length}{ - $\text{\CORRELATED}_i \leftarrow \text{\DECORRELATED}_i - \APPLYWEIGHT(\text{\WEIGHT}_i~,~\text{\DECORRELATED}_{i - \text{term}})$\; - $\text{\WEIGHT}_{i + 1} \leftarrow \text{\WEIGHT}_i + \UPDATEWEIGHT(\text{\DECORRELATED}_{i - \text{term}}~,~\text{\CORRELATED}_i~,~\DELTA)$\; - } - \Return $\left\lbrace\begin{tabular}{l} - \CORRELATED \\ - $\text{\WEIGHT}_i$ \\ - $\DECORRSAMPLE \leftarrow$ last $term$ \DECORRELATED samples \\ - \end{tabular}\right.$\; -} -\Else{ - invalid decorrelation term\; -} -\EALGORITHM -\par -\noindent -\begin{align*} -\intertext{where \texttt{apply\_weight} is defined as:} -\texttt{apply\_weight}(weight~,~sample) &= \left\lfloor\frac{weight \times sample + 2 ^ 9}{2 ^ {10}}\right\rfloor \\ -\intertext{and \texttt{update\_weight} is defined as:} -\texttt{update\_weight}(source~,~result~,~delta) &= -\begin{cases} -0 & \text{ if } source = 0 \text{ or } result = 0 \\ -delta & \text{ if } (source \textbf{ xor } result ) \geq 0 \\ --delta & \text{ if } (source \textbf{ xor } result) < 0 -\end{cases} -\end{align*} -} - -\clearpage - -\subsection{2 Channel Correlation Pass} -\label{wavpack:correlate_2ch} -{\relsize{-1} -\ALGORITHM{2 lists of signed uncorrelated samples; correlation term and delta, 2 correlation weights, 2 lists of correlation samples}{2 lists of signed correlated samples; updated weight and sample values per channel} -\SetKwData{TERM}{term} -\SetKwData{DELTA}{delta} -\SetKwData{CORRELATED}{correlated} -\SetKwData{DECORRELATED}{uncorrelated} -\SetKwData{DECORRSAMPLE}{correlation sample} -\SetKwData{WEIGHT}{weight} -\SetKw{OR}{or} -\SetKw{XOR}{xor} -\SetKwFunction{MIN}{min} -\SetKwFunction{MAX}{max} -\SetKwFunction{APPLYWEIGHT}{apply\_weight} -\SetKwFunction{UPDATEWEIGHT}{update\_weight} -\uIf{$(17 \leq \TERM \leq 18)$ \OR $(1 \leq \TERM \leq 8)$}{ - $\left.\begin{tabular}{r} - $\text{\CORRELATED}_{0}$ \\ - $\text{\WEIGHT}_{0}$ \\ - $\text{\DECORRSAMPLE}_{0}$ \\ - \end{tabular}\right\rbrace \leftarrow$ \hyperref[wavpack:correlate_1ch]{correlate 1 channel $\text{\DECORRELATED}_{0}$} using $\left\lbrace\begin{tabular}{l} - $\text{\TERM}$ \\ - $\text{\DELTA}$ \\ - $\text{\WEIGHT}_{0}$ \\ - $\text{\DECORRSAMPLE}_{0}$ \\ - \end{tabular}\right.$\; - $\left.\begin{tabular}{r} - $\text{\CORRELATED}_{1}$ \\ - $\text{\WEIGHT}_{1}$ \\ - $\text{\DECORRSAMPLE}_{1}$ \\ - \end{tabular}\right\rbrace \leftarrow$ \hyperref[wavpack:correlate_1ch]{correlate 1 channel $\text{\DECORRELATED}_{1}$} using $\left\lbrace\begin{tabular}{l} - $\text{\TERM}$ \\ - $\text{\DELTA}$ \\ - $\text{\WEIGHT}_{1}$ \\ - $\text{\DECORRSAMPLE}_{1}$ \\ - \end{tabular}\right.$\; - \Return $\left\lbrace\begin{tabular}{l} - $\text{\CORRELATED}$ \\ - $\text{\WEIGHT}$ \\ - $\text{\DECORRSAMPLE}$ \\ - \end{tabular}\right.$\; -} -\uElseIf{$-3 \leq \TERM \leq -1$}{ - $\text{\WEIGHT}_{0~0} \leftarrow$ correlation weight 0\; - $\text{\WEIGHT}_{1~0} \leftarrow$ correlation weight 1\; - $\text{\DECORRELATED}_{0~-1} \leftarrow \text{\DECORRSAMPLE}_{1~0}$\; - $\text{\DECORRELATED}_{1~-1} \leftarrow \text{\DECORRSAMPLE}_{0~0}$\; - \uIf{$\TERM = -1$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}uncorrelated samples length}{ - $\text{\CORRELATED}_{0~i} \leftarrow \text{\DECORRELATED}_{0~i} - \APPLYWEIGHT(\text{\WEIGHT}_{0~i}~,~\text{\DECORRELATED}_{1~(i - 1)})$\; - $\text{\CORRELATED}_{1~i} \leftarrow \text{\DECORRELATED}_{1~i} - \APPLYWEIGHT(\text{\WEIGHT}_{1~i}~,~\text{\DECORRELATED}_{0~i})$\; - $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \text{\WEIGHT}_{0~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{1~(i - 1)}~,~\text{\CORRELATED}_{0~i}~,~\DELTA)$\; - $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \text{\WEIGHT}_{1~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{0~i}~,~\text{\CORRELATED}_{1~i}~,~\DELTA)$\; - $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{0~(i + 1)}~,~1024)~,~-1024)$\; - $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{1~(i + 1)}~,~1024)~,~-1024)$\; - } - } - \uElseIf{$\TERM = -2$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}uncorrelated samples length}{ - $\text{\CORRELATED}_{0~i} \leftarrow \text{\DECORRELATED}_{0~i} - \APPLYWEIGHT(\text{\WEIGHT}_{0~i}~,~\text{\DECORRELATED}_{1~i})$\; - $\text{\CORRELATED}_{1~i} \leftarrow \text{\DECORRELATED}_{1~i} - \APPLYWEIGHT(\text{\WEIGHT}_{1~i}~,~\text{\DECORRELATED}_{0~({i - 1})})$\; - $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \text{\WEIGHT}_{0~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{1~i}~,~\text{\CORRELATED}_{0~i}~,~\DELTA)$\; - $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \text{\WEIGHT}_{1~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{0~(i - 1)}~,~\text{\CORRELATED}_{1~i}~,~\DELTA)$\; - $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{0~(i + 1)}~,~1024)~,~-1024)$\; - $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{1~(i + 1)}~,~1024)~,~-1024)$\; - } - } - \ElseIf{$\TERM = -3$}{ - \For{$i \leftarrow 0$ \emph{\KwTo}uncorrelated samples length}{ - $\text{\CORRELATED}_{0~i} \leftarrow \text{\DECORRELATED}_{0~i} - \APPLYWEIGHT(\text{\WEIGHT}_{0~i}~,~\text{\DECORRELATED}_{1~(i - 1)})$\; - $\text{\CORRELATED}_{1~i} \leftarrow \text{\DECORRELATED}_{1~i} - \APPLYWEIGHT(\text{\WEIGHT}_{1~i}~,~\text{\DECORRELATED}_{0~(i - 1)})$\; - $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \text{\WEIGHT}_{0~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{1~(i - 1)}~,~\text{\CORRELATED}_{0~i}~,~\DELTA)$\; - $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \text{\WEIGHT}_{1~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{0~(i - 1)}~,~\text{\CORRELATED}_{1~i}~,~\DELTA)$\; - $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{0~(i + 1)}~,~1024)~,~-1024)$\; - $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{1~(i + 1)}~,~1024)~,~-1024)$\; - } - } - \Return $\left\lbrace\begin{tabular}{l} - $\text{\CORRELATED}$ \\ - $\text{\WEIGHT}_{0~i}$ \\ - $\text{\WEIGHT}_{1~i}$ \\ - $\text{\DECORRSAMPLE}_{0~0} \leftarrow \text{\DECORRELATED}_{1~i}$ \\ - $\text{\DECORRSAMPLE}_{1~0} \leftarrow \text{\DECORRELATED}_{0~i}$ \\ - \end{tabular}\right.$\; -} -\Else{ - invalid decorrelation term\; -} -\EALGORITHM -} - -\clearpage - -\subsection{Channel Correlation Example} -\begin{figure}[h] -{\relsize{-1} - \subfloat{ - \begin{tabular}{|r|r|r|} - \multicolumn{3}{c}{Correlation Terms} \\ - \hline - $p$ & $\textsf{term}_p$ & $\textsf{delta}_p$ \\ - \hline - 0 & 3 & 2 \\ - 1 & 17 & 2 \\ - 2 & 2 & 2 \\ - 3 & 18 & 2 \\ - 4 & 18 & 2 \\ - \hline - \end{tabular} - } - \subfloat{ - \begin{tabular}{|r|r|r|} - \multicolumn{3}{c}{Correlation Weights} \\ - \hline - $p$ & $\textsf{weight}_{p~0}$ & $\textsf{weight}_{p~1}$ \\ - \hline - 0 & 16 & 24 \\ - 1 & 48 & 48 \\ - 2 & 32 & 32 \\ - 3 & 48 & 48 \\ - 4 & 48 & 48 \\ - \hline - \end{tabular} - } - \subfloat{ - \begin{tabular}{|r|r|r|} - \multicolumn{3}{c}{Correlation Samples} \\ - \hline - $p$ & $\textsf{sample}_{p~0~s}$ & $\textsf{sample}_{p~1~s}$ \\ - \hline - 0 & \texttt{[0, 0, 0]} & \texttt{[0, 0, 0]} \\ - 1 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ - 2 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ - 3 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ - 4 & \texttt{[-73, -78]} & \texttt{[28, 26]} \\ - \hline - \end{tabular} - } -} -\end{figure} -\par -\noindent -we combine them into a single set of arguments for each correlation pass: -\begin{table}[h] -{\relsize{-1} - \begin{tabular}{|r|r|r|r|r|r|} - \hline - & $\textbf{pass}_0$ & $\textbf{pass}_1$ & $\textbf{pass}_2$ & - $\textbf{pass}_3$ & $\textbf{pass}_3$ \\ - \hline - $\textsf{term}_p$ & 18 & 18 & 2 & 17 & 3 \\ - $\textsf{delta}_p$ & 2 & 2 & 2 & 2 & 2 \\ - $\textsf{weight}_{p~0}$ & 48 & 48 & 32 & 48 & 16 \\ - $\textsf{sample}_{p~0~s}$ & \texttt{[-73, -78]} & \texttt{[0, 0]} & - \texttt{[0, 0]} & \texttt{[0, 0]} & \texttt{[0, 0, 0]} \\ - $\textsf{weight}_{p~1}$ & 48 & 48 & 32 & 48 & 24 \\ - $\textsf{sample}_{p~1~s}$ & \texttt{[28, 26]} & \texttt{[0, 0]} & - \texttt{[0, 0]} & \texttt{[0, 0]} & \texttt{[0, 0, 0]} \\ - \hline - \end{tabular} -} -\end{table} -\par -\noindent -which we apply to the residuals from the bitstream sub-block: -\par -\noindent -{\relsize{-1} - \begin{tabular}{|r|r|r|r|r|r|} - \hline - $\textsf{channel}_{0~i}$ & - after $\textbf{pass}_0$ & - after $\textbf{pass}_1$ & - after $\textbf{pass}_2$ & - after $\textbf{pass}_3$ & - after $\textbf{pass}_4$ \\ - \hline - -64 & -61 & -61 & -61 & -61 & -61 \\ - -46 & -43 & -39 & -39 & -33 & -33 \\ - -25 & -23 & -21 & -19 & -18 & -18 \\ - -3 & -2 & -1 & 0 & 0 & 1 \\ - 20 & 20 & 20 & 21 & 20 & 20 \\ - 41 & 39 & 37 & 37 & 35 & 35 \\ - 60 & 57 & 54 & 53 & 50 & 50 \\ - 75 & 71 & 67 & 66 & 62 & 62 \\ - 85 & 80 & 75 & 73 & 68 & 68 \\ - 90 & 84 & 79 & 77 & 72 & 71 \\ - \hline - \hline - $\textsf{channel}_{1~i}$ & - after $\textbf{pass}_0$ & - after $\textbf{pass}_1$ & - after $\textbf{pass}_2$ & - after $\textbf{pass}_3$ & - after $\textbf{pass}_4$ \\ - \hline - 32 & 31 & 31 & 31 & 31 & 31 \\ - 39 & 37 & 35 & 35 & 32 & 32 \\ - 43 & 41 & 39 & 38 & 36 & 36 \\ - 45 & 43 & 41 & 40 & 38 & 37 \\ - 44 & 41 & 39 & 38 & 36 & 35 \\ - 40 & 38 & 36 & 34 & 32 & 31 \\ - 34 & 32 & 30 & 28 & 26 & 25 \\ - 25 & 23 & 21 & 20 & 19 & 18 \\ - 15 & 14 & 13 & 12 & 11 & 10 \\ - 4 & 3 & 2 & 1 & 1 & 0 \\ - \hline - \end{tabular} -} -\par -\noindent -Resulting in final correlated samples: -\newline -\begin{tabular}{rr} -$\textsf{residual}_0$ : & \texttt{[-61,~-33,~-18,~~1,~20,~35,~50,~62,~68,~71]} \\ -$\textsf{residual}_1$ : & \texttt{[~31,~~32,~~36,~37,~35,~31,~25,~18,~10,~~0]} \\ -\end{tabular} - -\clearpage - -{\relsize{-2} -\begin{tabular}{r||r|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}} -& $i$ & \textsf{uncorrelated}_i & \textsf{temp}_i & \textsf{correlated}_i & \textsf{weight}_{i + 1} \\ -\hline -%%START -\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_0$ - term 18\end{sideways}} -& 0 & -64 & -\lfloor(3 \times -73 + 78) \div 2\rfloor = -71 & --64 - \lfloor(48 \times -71 + 2 ^ 9) \div 2 ^ {10}\rfloor = -61 & -48 + 2 = 50 -\\ -& 1 & -46 & -\lfloor(3 \times -64 + 73) \div 2\rfloor = -60 & --46 - \lfloor(50 \times -60 + 2 ^ 9) \div 2 ^ {10}\rfloor = -43 & -50 + 2 = 52 -\\ -& 2 & -25 & -\lfloor(3 \times -46 + 64) \div 2\rfloor = -37 & --25 - \lfloor(52 \times -37 + 2 ^ 9) \div 2 ^ {10}\rfloor = -23 & -52 + 2 = 54 -\\ -& 3 & -3 & -\lfloor(3 \times -25 + 46) \div 2\rfloor = -15 & --3 - \lfloor(54 \times -15 + 2 ^ 9) \div 2 ^ {10}\rfloor = -2 & -54 + 2 = 56 -\\ -& 4 & 20 & -\lfloor(3 \times -3 + 25) \div 2\rfloor = 8 & -20 - \lfloor(56 \times 8 + 2 ^ 9) \div 2 ^ {10}\rfloor = 20 & -56 + 2 = 58 -\\ -& 5 & 41 & -\lfloor(3 \times 20 + 3) \div 2\rfloor = 31 & -41 - \lfloor(58 \times 31 + 2 ^ 9) \div 2 ^ {10}\rfloor = 39 & -58 + 2 = 60 -\\ -& 6 & 60 & -\lfloor(3 \times 41 - 20) \div 2\rfloor = 51 & -60 - \lfloor(60 \times 51 + 2 ^ 9) \div 2 ^ {10}\rfloor = 57 & -60 + 2 = 62 -\\ -& 7 & 75 & -\lfloor(3 \times 60 - 41) \div 2\rfloor = 69 & -75 - \lfloor(62 \times 69 + 2 ^ 9) \div 2 ^ {10}\rfloor = 71 & -62 + 2 = 64 -\\ -& 8 & 85 & -\lfloor(3 \times 75 - 60) \div 2\rfloor = 82 & -85 - \lfloor(64 \times 82 + 2 ^ 9) \div 2 ^ {10}\rfloor = 80 & -64 + 2 = 66 -\\ -& 9 & 90 & -\lfloor(3 \times 85 - 75) \div 2\rfloor = 90 & -90 - \lfloor(66 \times 90 + 2 ^ 9) \div 2 ^ {10}\rfloor = 84 & -66 + 2 = 68 -\\ -\hline -\hline -\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_1$ - term 18\end{sideways}} -& 0 & -61 & -\lfloor(3 \times 0 - 0) \div 2\rfloor = 0 & --61 - \lfloor(48 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor = -61 & -48 + 0 = 48 -\\ -& 1 & -43 & -\lfloor(3 \times -61 - 0) \div 2\rfloor = -92 & --43 - \lfloor(48 \times -92 + 2 ^ 9) \div 2 ^ {10}\rfloor = -39 & -48 + 2 = 50 -\\ -& 2 & -23 & -\lfloor(3 \times -43 + 61) \div 2\rfloor = -34 & --23 - \lfloor(50 \times -34 + 2 ^ 9) \div 2 ^ {10}\rfloor = -21 & -50 + 2 = 52 -\\ -& 3 & -2 & -\lfloor(3 \times -23 + 43) \div 2\rfloor = -13 & --2 - \lfloor(52 \times -13 + 2 ^ 9) \div 2 ^ {10}\rfloor = -1 & -52 + 2 = 54 -\\ -& 4 & 20 & -\lfloor(3 \times -2 + 23) \div 2\rfloor = 8 & -20 - \lfloor(54 \times 8 + 2 ^ 9) \div 2 ^ {10}\rfloor = 20 & -54 + 2 = 56 -\\ -& 5 & 39 & -\lfloor(3 \times 20 + 2) \div 2\rfloor = 31 & -39 - \lfloor(56 \times 31 + 2 ^ 9) \div 2 ^ {10}\rfloor = 37 & -56 + 2 = 58 -\\ -& 6 & 57 & -\lfloor(3 \times 39 - 20) \div 2\rfloor = 48 & -57 - \lfloor(58 \times 48 + 2 ^ 9) \div 2 ^ {10}\rfloor = 54 & -58 + 2 = 60 -\\ -& 7 & 71 & -\lfloor(3 \times 57 - 39) \div 2\rfloor = 66 & -71 - \lfloor(60 \times 66 + 2 ^ 9) \div 2 ^ {10}\rfloor = 67 & -60 + 2 = 62 -\\ -& 8 & 80 & -\lfloor(3 \times 71 - 57) \div 2\rfloor = 78 & -80 - \lfloor(62 \times 78 + 2 ^ 9) \div 2 ^ {10}\rfloor = 75 & -62 + 2 = 64 -\\ -& 9 & 84 & -\lfloor(3 \times 80 - 71) \div 2\rfloor = 84 & -84 - \lfloor(64 \times 84 + 2 ^ 9) \div 2 ^ {10}\rfloor = 79 & -64 + 2 = 66 -\\ -\hline -\hline -\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_2$ - term 2\end{sideways}} -& 0 & -61 & & --61 - \lfloor(32 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor = -61 & -32 + 0 = 32 -\\ -& 1 & -39 & & --39 - \lfloor(32 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor = -39 & -32 + 0 = 32 -\\ -& 2 & -21 & & --21 - \lfloor(32 \times -61 + 2 ^ 9) \div 2 ^ {10}\rfloor = -19 & -32 + 2 = 34 -\\ -& 3 & -1 & & --1 - \lfloor(34 \times -39 + 2 ^ 9) \div 2 ^ {10}\rfloor = 0 & -34 + 0 = 34 -\\ -& 4 & 20 & & -20 - \lfloor(34 \times -21 + 2 ^ 9) \div 2 ^ {10}\rfloor = 21 & -34 - 2 = 32 -\\ -& 5 & 37 & & -37 - \lfloor(32 \times -1 + 2 ^ 9) \div 2 ^ {10}\rfloor = 37 & -32 - 2 = 30 -\\ -& 6 & 54 & & -54 - \lfloor(30 \times 20 + 2 ^ 9) \div 2 ^ {10}\rfloor = 53 & -30 + 2 = 32 -\\ -& 7 & 67 & & -67 - \lfloor(32 \times 37 + 2 ^ 9) \div 2 ^ {10}\rfloor = 66 & -32 + 2 = 34 -\\ -& 8 & 75 & & -75 - \lfloor(34 \times 54 + 2 ^ 9) \div 2 ^ {10}\rfloor = 73 & -34 + 2 = 36 -\\ -& 9 & 79 & & -79 - \lfloor(36 \times 67 + 2 ^ 9) \div 2 ^ {10}\rfloor = 77 & -36 + 2 = 38 -\\ -\hline -\hline -\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_3$ - term 17\end{sideways}} -& 0 & -61 & -2 \times 0 - 0 = 0 & --61 - \lfloor(48 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor = -61 & -48 + 0 = 48 -\\ -& 1 & -39 & -2 \times -61 - 0 = -122 & --39 - \lfloor(48 \times -122 + 2 ^ 9) \div 2 ^ {10}\rfloor = -33 & -48 + 2 = 50 -\\ -& 2 & -19 & -2 \times -39 + 61 = -17 & --19 - \lfloor(50 \times -17 + 2 ^ 9) \div 2 ^ {10}\rfloor = -18 & -50 + 2 = 52 -\\ -& 3 & 0 & -2 \times -19 + 39 = 1 & -0 - \lfloor(52 \times 1 + 2 ^ 9) \div 2 ^ {10}\rfloor = 0 & -52 + 0 = 52 -\\ -& 4 & 21 & -2 \times 0 + 19 = 19 & -21 - \lfloor(52 \times 19 + 2 ^ 9) \div 2 ^ {10}\rfloor = 20 & -52 + 2 = 54 -\\ -& 5 & 37 & -2 \times 21 - 0 = 42 & -37 - \lfloor(54 \times 42 + 2 ^ 9) \div 2 ^ {10}\rfloor = 35 & -54 + 2 = 56 -\\ -& 6 & 53 & -2 \times 37 - 21 = 53 & -53 - \lfloor(56 \times 53 + 2 ^ 9) \div 2 ^ {10}\rfloor = 50 & -56 + 2 = 58 -\\ -& 7 & 66 & -2 \times 53 - 37 = 69 & -66 - \lfloor(58 \times 69 + 2 ^ 9) \div 2 ^ {10}\rfloor = 62 & -58 + 2 = 60 -\\ -& 8 & 73 & -2 \times 66 - 53 = 79 & -73 - \lfloor(60 \times 79 + 2 ^ 9) \div 2 ^ {10}\rfloor = 68 & -60 + 2 = 62 -\\ -& 9 & 77 & -2 \times 73 - 66 = 80 & -77 - \lfloor(62 \times 80 + 2 ^ 9) \div 2 ^ {10}\rfloor = 72 & -62 + 2 = 64 -\\ -\hline -\hline -\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_4$ - term 3\end{sideways}} -& 0 & -61 & & --61 - \lfloor(16 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor = -61 & -16 + 0 = 16 -\\ -& 1 & -33 & & --33 - \lfloor(16 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor = -33 & -16 + 0 = 16 -\\ -& 2 & -18 & & --18 - \lfloor(16 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor = -18 & -16 + 0 = 16 -\\ -& 3 & 0 & & -0 - \lfloor(16 \times -61 + 2 ^ 9) \div 2 ^ {10}\rfloor = 1 & -16 - 2 = 14 -\\ -& 4 & 20 & & -20 - \lfloor(14 \times -33 + 2 ^ 9) \div 2 ^ {10}\rfloor = 20 & -14 - 2 = 12 -\\ -& 5 & 35 & & -35 - \lfloor(12 \times -18 + 2 ^ 9) \div 2 ^ {10}\rfloor = 35 & -12 - 2 = 10 -\\ -& 6 & 50 & & -50 - \lfloor(10 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor = 50 & -10 + 0 = 10 -\\ -& 7 & 62 & & -62 - \lfloor(10 \times 20 + 2 ^ 9) \div 2 ^ {10}\rfloor = 62 & -10 + 2 = 12 -\\ -& 8 & 68 & & -68 - \lfloor(12 \times 35 + 2 ^ 9) \div 2 ^ {10}\rfloor = 68 & -12 + 2 = 14 -\\ -& 9 & 72 & & -72 - \lfloor(14 \times 50 + 2 ^ 9) \div 2 ^ {10}\rfloor = 71 & -14 + 2 = 16 -\\ -%%END -\end{tabular} -} -\begin{center} -$\text{channel}_0$ correlation passes -\end{center} - -\clearpage - -\subsection{Writing Decorrelation Terms} -\label{wavpack:write_decorr_terms} -\ALGORITHM{a list of decorrelation terms, a list of decorrelation deltas}{decorrelation terms sub block data} -\SetKwData{TERM}{term} -\SetKwData{DELTA}{delta} -\SetKwData{KwDownTo}{downto} -\For(\tcc*[f]{populate in reverse order}){$p \leftarrow \text{decorrelation pass count}$ \emph{\KwDownTo}0}{ - $\text{\TERM}_p + 5 \rightarrow$ \WRITE 5 unsigned bits\; - $\text{\DELTA}_p \rightarrow$ \WRITE 3 unsigned bits\; -} -\Return decorrelation terms sub block data\; -\EALGORITHM -\begin{figure}[h] - \includegraphics{figures/wavpack/decorr_terms.pdf} -\end{figure} - -\clearpage -For example, given decorrelation terms and deltas: -\begin{table}[h] -\begin{tabular}{rrr} -$p$ & $\textsf{term}_p$ & $\textsf{delta}_p$ \\ -\hline -0 & 3 & 2 \\ -1 & 17 & 2 \\ -2 & 2 & 2 \\ -3 & 18 & 2 \\ -4 & 18 & 2 \\ -\end{tabular} -\end{table} -\par -\noindent -the decorrelation terms sub block is written as: -\begin{figure}[h] -\includegraphics{figures/wavpack/terms_parse.pdf} -\end{figure} - -\clearpage - -\subsection{Writing Decorrelation Weights} -\label{wavpack:write_decorr_weights} -\ALGORITHM{a list of decorrelation weights per channel}{decorrelation weights sub block data} -\SetKwData{WEIGHT}{weight} -\SetKwFunction{STOREWEIGHT}{store\_weight} -\SetKwData{KwDownTo}{downto} -\For(\tcc*[f]{populate in reverse order}){$p \leftarrow \text{decorrelation pass count}$ \emph{\KwDownTo}0}{ - $\texttt{\STOREWEIGHT}(\text{\WEIGHT}_{p~0}) \rightarrow$ \WRITE 8 signed bits\; - \If{$\text{channel count} = 2$}{ - $\texttt{\STOREWEIGHT}(\text{\WEIGHT}_{p~1}) \rightarrow$ \WRITE 8 signed bits\; - } -} -\Return decorrelation weights sub block data\; -\EALGORITHM -\par -\noindent -where \texttt{store\_weight} is defined as: -\begin{equation*} -\texttt{store\_weight}(w) = -\begin{cases} -\left\lfloor\frac{\texttt{min}(w, 1024) - \lfloor(\texttt{min}(w,1024) + 2 ^ 6) \div 2 ^ 7\rfloor + 4}{2 ^ 3}\right\rfloor & \text{ if } w > 0 \\ -0 & \text{ if } w = 0 \\ -\left\lfloor \frac{\texttt{max}(w, -1024) + 4}{2 ^ 3} \right\rfloor & \text{ if } w < 0 \\ -\end{cases} -\end{equation*} -\begin{figure}[h] - \includegraphics{figures/wavpack/decorr_weights.pdf} -\end{figure} -\clearpage -For example, given the decorrelation weight values: -\begin{table}[h] -\begin{tabular}{rrrrr} -$p$ & $\textsf{weight}_{p~0}$ & $\textsf{weight}_{p~1}$ & -$\texttt{store\_weight}(\textsf{weight}_{p~0})$ & -$\texttt{store\_weight}(\textsf{weight}_{p~1})$ \\ -\hline -0 & 16 & 24 & 2 & 3 \\ -1 & 48 & 48 & 6 & 6 \\ -2 & 32 & 32 & 4 & 4 \\ -3 & 48 & 48 & 6 & 6 \\ -4 & 48 & 48 & 6 & 6 \\ -\end{tabular} -\end{table} -\par -\noindent -the decorrelation weights subframe is written as: -\begin{figure}[h] -\includegraphics{figures/wavpack/decorr_weights_parse.pdf} -\end{figure} - -\clearpage - -\subsection{Writing Decorrelation Samples} -\label{wavpack:write_decorr_samples} -\ALGORITHM{a list of decorrelation sample values per pass, per channel}{decorrelation samples sub block data} -\SetKwData{TERM}{term} -\SetKwData{SAMPLE}{sample} -\SetKwFunction{WVLOG}{wv\_log2} -\SetKw{KwDownTo}{downto} -\For{$p \leftarrow \text{decorrelation pass count}$ \emph{\KwDownTo}0}{ - \uIf{$17 \leq \text{\TERM}_p \leq 18$}{ - \For{$c \leftarrow 0$ \emph{\KwTo}channel count}{ - $\WVLOG(\text{\SAMPLE}_{p~c~0}) \rightarrow$ \WRITE 16 signed bits\; - $\WVLOG(\text{\SAMPLE}_{p~c~1}) \rightarrow$ \WRITE 16 signed bits\; - } - } - \uElseIf{$1 \leq \text{\TERM}_p \leq 8$}{ - \For{$s \leftarrow 0$ \emph{\KwTo}$\text{\TERM}_p$}{ - \For{$c \leftarrow 0$ \emph{\KwTo}channel count}{ - $\WVLOG(\text{\SAMPLE}_{p~c~s}) \rightarrow$ \WRITE 16 signed bits\; - } - } - } - \ElseIf{$-3 \leq \text{\TERM}_p \leq -1$}{ - $\WVLOG(\text{\SAMPLE}_{p~0~0}) \rightarrow$ \WRITE 16 signed bits\; - $\WVLOG(\text{\SAMPLE}_{p~1~0}) \rightarrow$ \WRITE 16 signed bits\; - } -} -\Return decorrelation samples data\; -\EALGORITHM - -\begin{figure}[h] - \includegraphics{figures/wavpack/decorr_samples.pdf} -\end{figure} - -\subsubsection{The wv\_log2 Function} -\ALGORITHM{a signed value}{a signed 16 bit value} -\SetKwFunction{LOG}{wlog} -$a \leftarrow |value| + \lfloor |value| \div 2 ^ 9\rfloor$\; -$c \leftarrow $\lIf{$a \neq 0$}{$\lfloor\log_2(a)\rfloor + 1$} -\lElse{$0$}\; -\eIf{$value \geq 0$}{ - \eIf{$0 \leq a < 256$}{ - \Return $c \times 2 ^ 8 + \LOG((a \times 2 ^ {9 - c}) \bmod 256)$\; - }{ - \Return $c \times 2 ^ 8 + \LOG(\lfloor a \div 2 ^ {c - 9}\rfloor \bmod 256)$\; - } -}{ - \eIf{$0 \leq a < 256$}{ - \Return $-(c \times 2 ^ 8 + \LOG((a \times 2 ^ {9 - c}) \bmod 256))$\; - }{ - \Return $-(c \times 2 ^ 8 + \LOG(\lfloor a \div 2 ^ {c - 9}\rfloor \bmod 256))$\; - } -} -\EALGORITHM - -\clearpage - -where \texttt{wlog} is defined from the following table: -\par -\noindent -{\relsize{-3}\ttfamily -\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} -\hline -& \texttt{0x?0} & \texttt{0x?1} & \texttt{0x?2} & \texttt{0x?3} & \texttt{0x?4} & \texttt{0x?5} & \texttt{0x?6} & \texttt{0x?7} & \texttt{0x?8} & \texttt{0x?9} & \texttt{0x?A} & \texttt{0x?B} & \texttt{0x?C} & \texttt{0x?D} & \texttt{0x?E} & \texttt{0x?F} \\ -\hline -\texttt{0x0?} & 0 & 1 & 3 & 4 & 6 & 7 & 9 & 10 & 11 & 13 & 14 & 16 & 17 & 18 & 20 & 21 \\ -\texttt{0x1?} & 22 & 24 & 25 & 26 & 28 & 29 & 30 & 32 & 33 & 34 & 36 & 37 & 38 & 40 & 41 & 42 \\ -\texttt{0x2?} & 44 & 45 & 46 & 47 & 49 & 50 & 51 & 52 & 54 & 55 & 56 & 57 & 59 & 60 & 61 & 62 \\ -\texttt{0x3?} & 63 & 65 & 66 & 67 & 68 & 69 & 71 & 72 & 73 & 74 & 75 & 77 & 78 & 79 & 80 & 81 \\ -\texttt{0x4?} & 82 & 84 & 85 & 86 & 87 & 88 & 89 & 90 & 92 & 93 & 94 & 95 & 96 & 97 & 98 & 99 \\ -\texttt{0x5?} & 100 & 102 & 103 & 104 & 105 & 106 & 107 & 108 & 109 & 110 & 111 & 112 & 113 & 114 & 116 & 117 \\ -\texttt{0x6?} & 118 & 119 & 120 & 121 & 122 & 123 & 124 & 125 & 126 & 127 & 128 & 129 & 130 & 131 & 132 & 133 \\ -\texttt{0x7?} & 134 & 135 & 136 & 137 & 138 & 139 & 140 & 141 & 142 & 143 & 144 & 145 & 146 & 147 & 148 & 149 \\ -\texttt{0x8?} & 150 & 151 & 152 & 153 & 154 & 155 & 155 & 156 & 157 & 158 & 159 & 160 & 161 & 162 & 163 & 164 \\ -\texttt{0x9?} & 165 & 166 & 167 & 168 & 169 & 169 & 170 & 171 & 172 & 173 & 174 & 175 & 176 & 177 & 178 & 178 \\ -\texttt{0xA?} & 179 & 180 & 181 & 182 & 183 & 184 & 185 & 185 & 186 & 187 & 188 & 189 & 190 & 191 & 192 & 192 \\ -\texttt{0xB?} & 193 & 194 & 195 & 196 & 197 & 198 & 198 & 199 & 200 & 201 & 202 & 203 & 203 & 204 & 205 & 206 \\ -\texttt{0xC?} & 207 & 208 & 208 & 209 & 210 & 211 & 212 & 212 & 213 & 214 & 215 & 216 & 216 & 217 & 218 & 219 \\ -\texttt{0xD?} & 220 & 220 & 221 & 222 & 223 & 224 & 224 & 225 & 226 & 227 & 228 & 228 & 229 & 230 & 231 & 231 \\ -\texttt{0xE?} & 232 & 233 & 234 & 234 & 235 & 236 & 237 & 238 & 238 & 239 & 240 & 241 & 241 & 242 & 243 & 244 \\ -\texttt{0xF?} & 244 & 245 & 246 & 247 & 247 & 248 & 249 & 249 & 250 & 251 & 252 & 252 & 253 & 254 & 255 & 255 \\ -\hline -\end{tabular} -} - -\subsubsection{Writing Decorrelation Samples Example} -Given a 2 channel subframe with 5 correlation passes containing -the following correlation samples: -\begin{table}[h] -\begin{tabular}{rrrr} -pass $p$ & $\text{term}_p$ & $\text{sample}_{p~0~s}$ & $\text{sample}_{p~1~s}$ \\ -\hline -0 & 3 & \texttt{[62, 68, 71]} & \texttt{[0, 10, 18]} \\ -1 & 17 & \texttt{[72, 68]} & \texttt{[11, 1]} \\ -2 & 2 & \texttt{[73, 77]} & \texttt{[1, 12]} \\ -3 & 18 & \texttt{[79, 75]} & \texttt{[13, 2]} \\ -4 & 18 & \texttt{[84, 80]} & \texttt{[14, 3]} \\ -\end{tabular} -\end{table} -\par -\noindent -$\text{sample}_{4~0~0}$ (pass 4, channel 0, sample 0) is encoded as: -\begin{align*} -a &\leftarrow |84| + \lfloor|84| \div 2 ^ 9\rfloor = 84 \\ -c &\leftarrow \lfloor\log_2(84)\rfloor + 1 = 7 \\ -value &\leftarrow 7 \times 2 ^ 8 + \texttt{wlog}((84 \times 2 ^ 2) \bmod 256) \\ -&\leftarrow 1792 + \texttt{wlog}(\texttt{0x50}) = 1892 = \texttt{0x764} -\end{align*} -\par -\noindent -and the entire sub block is written as: -\begin{figure}[h] -\includegraphics{figures/wavpack/decorr_samples_encode.pdf} -\end{figure} - -\clearpage - -\subsection{Writing Entropy Variables} -\label{wavpack:write_entropy} -\ALGORITHM{3 entropy variables per channel, channel count}{entropy variables sub block data} -\SetKwData{ENTROPY}{entropy} -\SetKwFunction{WVLOG}{wv\_log2} -$\WVLOG(\text{\ENTROPY}_{0~0}) \rightarrow$ \WRITE 16 signed bits\; -$\WVLOG(\text{\ENTROPY}_{0~1}) \rightarrow$ \WRITE 16 signed bits\; -$\WVLOG(\text{\ENTROPY}_{0~2}) \rightarrow$ \WRITE 16 signed bits\; -\If{$\text{channel count} = 2$}{ - $\WVLOG(\text{\ENTROPY}_{1~0}) \rightarrow$ \WRITE 16 signed bits\; - $\WVLOG(\text{\ENTROPY}_{1~1}) \rightarrow$ \WRITE 16 signed bits\; - $\WVLOG(\text{\ENTROPY}_{1~2}) \rightarrow$ \WRITE 16 signed bits\; -} -\Return entropy variables sub block data\; -\EALGORITHM - -\begin{figure}[h] - \includegraphics{figures/wavpack/entropy_vars.pdf} -\end{figure} - -\clearpage - -\subsection{Encoding Bitstream} -\label{wavpack:write_bitstream} -{\relsize{-1} -\ALGORITHM{channel count, entropy values, a list of signed residual values per channel}{bitstream sub block data} -\SetKwData{UNDEFINED}{undefined} -\SetKwData{RESIDUAL}{residual} -\SetKwData{ENTROPY}{entropy} -\SetKwData{BLOCKSAMPLES}{block samples} -\SetKwData{CHANNELCOUNT}{channel count} -\SetKwData{ZEROES}{zeroes} -\SetKwData{OFFSET}{offset} -\SetKwData{ADD}{add} -\SetKwData{SIGN}{sign} -\SetKw{AND}{and} -\SetKw{OR}{or} -\SetKw{IS}{is} -\SetKwFunction{FLUSH}{flush} -\SetKwFunction{UNARYUNDEFINED}{unary\_undefined} -\SetKwFunction{ENCODERESIDUAL}{encode\_residual} -\SetKwFunction{UNARY}{unary} -\SetKwFunction{WRITERESIDUAL}{write\_residual} -$u_{(-2)} \leftarrow \text{\UNDEFINED}$\; -$\text{\ZEROES}_{(-1)} \leftarrow m_{(-1)} \leftarrow \text{\OFFSET}_{(-1)} \leftarrow \text{\ADD}_{(-1)} \leftarrow \text{\SIGN}_{(-1)} \leftarrow \text{\UNDEFINED}$\tcc*[r]{buffered $\text{residual}_{i - 1}$} -$i \leftarrow 0$\; -\BlankLine -\While{$i < (\text{\BLOCKSAMPLES} \times \text{\CHANNELCOUNT})$}{ - $r_i \leftarrow \text{\RESIDUAL}_{(i \bmod \text{\CHANNELCOUNT})~\lfloor i \div \text{\CHANNELCOUNT}\rfloor}$\; - \eIf{$(\text{\ENTROPY}_{0~0} < 2)$ \AND $(\text{\ENTROPY}_{1~0} < 2)$ \AND $\UNARYUNDEFINED(u_{i - 2}~,~m_{i - 1})$}{ - \eIf(\tcc*[f]{in a block of zeroes}){$(\text{\ZEROES}_{i - 1} \neq \text{\UNDEFINED})$ \AND $(m_{i - 1} = \text{\UNDEFINED})$}{ - \eIf(\tcc*[f]{continue block of zeroes}){$r_i = 0$}{ - $\text{\ZEROES}_i \leftarrow \text{\ZEROES}_{i - 1} + 1$\; - $(u_{i - 1}~,~m_i~,~\text{\OFFSET}_i~,~\text{\ADD}_i~,~\text{\SIGN}_i) \leftarrow (u_{i - 2}~,~m_{i - 1}~,~\text{\OFFSET}_{i - 1}~,~\text{\ADD}_{i - 1}~,~\text{\SIGN}_{i - 1})$\; - }(\tcc*[f]{end block of zeroes}){ - $\text{\ZEROES}_i \leftarrow \text{\ZEROES}_{i - 1}$\; - $(m_i~,~\text{\OFFSET}_i~,~\text{\ADD}_i~,~\text{\SIGN}_i) \leftarrow \hyperref[wavpack:encode_residual]{\ENCODERESIDUAL(r_i~,~\text{\ENTROPY}_{(i \bmod \text{\CHANNELCOUNT})})}$\; - } - }(\tcc*[f]{start a new block of zeroes}){ - \eIf{$r_i = 0$}{ - $\text{\ZEROES}_i \leftarrow 1$\; - $m_i \leftarrow \text{\OFFSET}_i \leftarrow \text{\ADD}_i \leftarrow \text{\SIGN}_i \leftarrow \text{\UNDEFINED}$\; - $u_{i - 1} \leftarrow \hyperref[wavpack:flush_residual]{\FLUSH(u_{i - 2}~,~\text{\ZEROES}_{i - 1}~,~m_{i - 1}~,~\text{\OFFSET}_{i - 1}~,~\text{\ADD}_{i - 1}~,~\text{\SIGN}_{i - 1}~,~0)}$\; - $\text{\ENTROPY}_{0~0} \leftarrow \text{\ENTROPY}_{0~1} \leftarrow \text{\ENTROPY}_{0~2} \leftarrow \text{\ENTROPY}_{1~0} \leftarrow \text{\ENTROPY}_{1~1} \leftarrow \text{\ENTROPY}_{1~2} \leftarrow 0$\; - }(\tcc*[f]{false-alarm block of zeroes}){ - $\text{\ZEROES}_i \leftarrow 0$\; - $(m_i~,~\text{\OFFSET}_i~,~\text{\ADD}_i~,~\text{\SIGN}_i) \leftarrow \hyperref[wavpack:encode_residual]{\ENCODERESIDUAL(r_i~,~\text{\ENTROPY}_{(i \bmod \text{\CHANNELCOUNT})})}$\; - $u_{i - 1} \leftarrow \hyperref[wavpack:flush_residual]{\FLUSH(u_{i - 2}~,~\text{\ZEROES}_{i - 1}~,~m_{i - 1}~,~\text{\OFFSET}_{i - 1}~,~\text{\ADD}_{i - 1}~,~\text{\SIGN}_{i - 1}~,~m_i)}$\; - } - } - }(\tcc*[f]{encode regular residual}){ - $\text{\ZEROES}_i \leftarrow \text{\UNDEFINED}$\; - $(m_i~,~\text{\OFFSET}_i~,~\text{\ADD}_i~,~\text{\SIGN}_i) \leftarrow \hyperref[wavpack:encode_residual]{\ENCODERESIDUAL(r_i~,~\text{\ENTROPY}_{(i \bmod \text{\CHANNELCOUNT})})}$\; - $u_{i - 1} \leftarrow \hyperref[wavpack:flush_residual]{\FLUSH(u_{i - 2}~,~\text{\ZEROES}_{i - 1}~,~m_{i - 1}~,~\text{\OFFSET}_{i - 1}~,~\text{\ADD}_{i - 1}~,~\text{\SIGN}_{i - 1}~,~m_i)}$\; - } - $i \leftarrow i + 1$\; -} -\BlankLine -\tcc{flush final residual} -$u_{i - 1} \leftarrow \hyperref[wavpack:flush_residual]{\FLUSH(u_{i - 2}~,~\text{\ZEROES}_{i - 1}~,~m_{i - 1}~,~\text{\OFFSET}_{i - 1}~,~\text{\ADD}_{i - 1}~,~\text{\SIGN}_{i - 1}~,~0)}$\; -\BlankLine -\Return encoded bitstream sub block\; -\EALGORITHM - -\subsubsection{\texttt{unary\_undefined} Function} -\ALGORITHM{$u_{i - 2}$, $m_{i - 1}$}{whether $u_{i - 1}$ is defined} -\SetKw{AND}{and} -\SetKw{IS}{is} -\SetKw{NOT}{not} -\SetKwData{TRUE}{true} -\SetKwData{FALSE}{false} -\SetKwData{UNDEFINED}{undefined} -\uIf{$m_{i - 1} = \text{\UNDEFINED}$}{ - \Return \TRUE\tcc*[r]{$u_{i - 1}$ \IS \UNDEFINED} -} -\uElseIf{$(m_{i - 1} = 0)$ \AND $(u_{i - 2} \neq \UNDEFINED)$ \AND $(u_{i - 2} \bmod 2 = 0)$}{ - \Return \TRUE\tcc*[r]{$u_{i - 1}$ \IS \UNDEFINED} -} -\Else{ - \Return \FALSE\tcc*[r]{$u_{i - 1}$ \IS \NOT \UNDEFINED} -} -\EALGORITHM -} +\input{wavpack/decode} \clearpage -\subsubsection{\texttt{encode\_residual} Function} -\label{wavpack:encode_residual} -{\relsize{-1} - \ALGORITHM{signed residual value, entropy variables for channel}{$\text{m}_i$, $\text{offset}_i$, $\text{add}_i$, $\text{sign}_i$; updated entropy values} - \SetKwData{RESIDUAL}{residual} - \SetKwData{UNSIGNED}{unsigned} - \SetKwData{SIGN}{sign} - \SetKwData{ENTROPY}{entropy} - \SetKwData{MEDIAN}{median} - \SetKwData{MED}{m} - \SetKwData{ADD}{add} - \SetKwData{OFFSET}{offset} - \eIf{$\RESIDUAL \geq 0$}{ - $\UNSIGNED \leftarrow \RESIDUAL$\; - $\text{\SIGN}_i \leftarrow 0$\; - }{ - $\UNSIGNED \leftarrow -\RESIDUAL - 1$\; - $\text{\SIGN}_i \leftarrow 1$\; - } - $\text{\MEDIAN}_{c~0} \leftarrow \lfloor\text{\ENTROPY}_{c~0} \div 2 ^ 4\rfloor + 1$\; - $\text{\MEDIAN}_{c~1} \leftarrow \lfloor\text{\ENTROPY}_{c~1} \div 2 ^ 4\rfloor + 1$\; - $\text{\MEDIAN}_{c~2} \leftarrow \lfloor\text{\ENTROPY}_{c~2} \div 2 ^ 4\rfloor + 1$\; - \uIf{$\UNSIGNED < \text{\MEDIAN}_{c~0}$}{ - $\text{\MED}_i \leftarrow 0$\; - $\text{\OFFSET}_i \leftarrow \text{\UNSIGNED}$\tcc*[r]{offset is unsigned - base} - $\text{\ADD}_i \leftarrow \text{\MEDIAN}_{c~0} - 1$\; - $\text{\ENTROPY}_{c~0} \leftarrow \text{\ENTROPY}_{c~0} - \lfloor(\text{\ENTROPY}_{c~0} + 126) \div 128\rfloor \times 2$\; - } - \uElseIf{$(\UNSIGNED - \text{\MEDIAN}_{c~0}) < \text{\MEDIAN}_{c~1}$}{ - $\text{\MED}_i \leftarrow 1$\; - $\text{\OFFSET}_i \leftarrow \text{\UNSIGNED} - \text{\MEDIAN}_{c~0}$\; - $\text{\ADD}_i \leftarrow \text{\MEDIAN}_{c~1} - 1$\; - $\text{\ENTROPY}_{c~0} \leftarrow \text{\ENTROPY}_{c~0} + \lfloor(\text{\ENTROPY}_{c~0} + 128) \div 128\rfloor \times 5$\; - $\text{\ENTROPY}_{c~1} \leftarrow \text{\ENTROPY}_{c~1} - \lfloor(\text{\ENTROPY}_{c~1} + 62) \div 64\rfloor \times 2$\; - } - \uElseIf{$(\UNSIGNED - (\text{\MEDIAN}_{c~0} + \text{\MEDIAN}_{c~1})) < \text{\MEDIAN}_{c~2}$}{ - $\text{\MED}_i \leftarrow 2$\; - $\text{\OFFSET}_i \leftarrow \text{\UNSIGNED} - (\text{\MEDIAN}_{c~0} + \text{\ENTROPY}_{c~1})$\; - $\text{\ADD}_i \leftarrow \text{\MEDIAN}_{c~2} - 1$\; - $\text{\ENTROPY}_{c~0} \leftarrow \text{\ENTROPY}_{c~0} + \lfloor(\text{\ENTROPY}_{c~0} + 128) \div 128\rfloor \times 5$\; - $\text{\ENTROPY}_{c~1} \leftarrow \text{\ENTROPY}_{c~1} + \lfloor(\text{\ENTROPY}_{c~1} + 64) \div 64\rfloor \times 5$\; - $\text{\ENTROPY}_{c~2} \leftarrow \text{\ENTROPY}_{c~2} - \lfloor(\text{\ENTROPY}_{c~2} + 30) \div 32\rfloor \times 2$\; - } - \Else{ - $\text{\MED}_i \leftarrow \lfloor(\UNSIGNED - (\text{\MEDIAN}_{c~0} + \text{\MEDIAN}_{c~1})) \div \text{\MEDIAN}_{c~2}\rfloor + 2$\; - $\text{\OFFSET}_i \leftarrow \text{\UNSIGNED} - (\text{\MEDIAN}_{c~0} + \text{\MEDIAN}_{c~1} + ((\text{\MED}_i - 2) \times \text{\MEDIAN}_{c~2}))$\; - $\text{\ADD}_i \leftarrow \text{\MEDIAN}_{c~2} - 1$\; - $\text{\ENTROPY}_{c~0} \leftarrow \text{\ENTROPY}_{c~0} + \lfloor(\text{\ENTROPY}_{c~0} + 128) \div 128\rfloor \times 5$\; - $\text{\ENTROPY}_{c~1} \leftarrow \text{\ENTROPY}_{c~1} + \lfloor(\text{\ENTROPY}_{c~1} + 64) \div 64\rfloor \times 5$\; - $\text{\ENTROPY}_{c~2} \leftarrow \text{\ENTROPY}_{c~2} + \lfloor(\text{\ENTROPY}_{c~2} + 32) \div 32\rfloor \times 5$\; - } - \Return $\left\lbrace\begin{tabular}{l} - $\text{\MED}_i$ \\ - $\text{\OFFSET}_i$ \\ - $\text{\ADD}_i$ \\ - $\text{\SIGN}_i$ \\ - $\text{\ENTROPY}_c$ \\ - \end{tabular}\right.$\; - \EALGORITHM -} - -\subsubsection{Writing Modified Elias Gamma Code} -\label{wavpack:write_egc} -\ALGORITHM{a non-negative integer value}{an encoded value} -\eIf{$v \leq 1$}{ - $v \rightarrow$ \WUNARY with stop bit 0\; -}{ - $t \leftarrow \lfloor\log_2(v)\rfloor + 1$\; - $t \rightarrow$ \WUNARY with stop bit 0\; - $v \bmod 2 ^ {t - 1} \rightarrow$ \WRITE $(t - 1)$ unsigned bits\; -} -\Return encoded value\; -\EALGORITHM - -\clearpage - -\subsubsection{\texttt{flush} Function} -\label{wavpack:flush_residual} -{\relsize{-1} - \ALGORITHM{$u_{i - 2}$, $\text{zeroes}_{i - 1}$, $m_{i - 1}$, $\text{offset}_{i - 1}$, $\text{add}_{i - 1}$, $\text{sign}_{i - 1}$, $m_i$}{$u_{i - 1}$} - \SetKwData{UNDEFINED}{undefined} - \SetKwData{ZEROES}{zeroes} - \SetKwData{OFFSET}{offset} - \SetKwData{ADD}{add} - \SetKwData{SIGN}{sign} - \SetKwFunction{UNARY}{unary} - \SetKwFunction{WRITERESIDUAL}{write\_residual} - \SetKw{AND}{and} - \SetKw{OR}{or} - \If{$\text{\ZEROES}_{i - 1} \neq \text{\UNDEFINED}$}{ - write $\text{\ZEROES}_{i - 1}$ \hyperref[wavpack:write_egc]{in modified Elias gamma code}\; - } - \eIf{$m_{i - 1} \neq \text{\UNDEFINED}$}{ - \tcc{calculate $\text{unary}_{i - 1}$ value for $m_{i - 1}$ based on $m_i$} - \uIf{$(m_{i - 1} > 0)$ \AND $(m_i > 0)$}{ - \eIf{$(u_{i - 2} = \UNDEFINED)$ \OR $(u_{i - 2} \bmod 2 = 0)$}{ - $u_{i - 1} \leftarrow (m_{i - 1} \times 2) + 1$\; - }{ - $u_{i - 1} \leftarrow (m_{i - 1} \times 2) - 1$\; - } - } - \uElseIf{$(m_{i - 1} = 0)$ \AND $(m_i > 0)$}{ - \eIf{$(u_{i - 2} = \UNDEFINED)$ \OR $(u_{i - 2} \bmod 2 = 1)$}{ - $u_{i - 1} \leftarrow 1$\; - }{ - $u_{i - 1} \leftarrow \UNDEFINED$\; - } - } - \uElseIf{$(m_{i - 1} > 0)$ \AND $(m_i = 0)$}{ - \eIf{$(u_{i - 2} = \UNDEFINED)$ \OR $(u_{i - 2} \bmod 2 = 0)$}{ - $u_{i - 1} \leftarrow m_{i - 1} \times 2$\; - }{ - $u_{i - 1} \leftarrow (m_{i - 1} - 1) \times 2$\; - } - } - \ElseIf{$(m_{i - 1} = 0)$ \AND $(m_i = 0)$}{ - \eIf{$(u_{i - 2} = \UNDEFINED)$ \OR $(u_{i - 2} \bmod 2 = 1)$}{ - $u_{i - 1} \leftarrow 0$\; - }{ - $u_{i - 1} \leftarrow \UNDEFINED$\; - } - } - \BlankLine - \tcc{write $\text{residual}_{i - 1}$ to disk using $\text{unary}_{i - 1}$} - \If{$u_{i - 1} \neq \UNDEFINED$}{ - \eIf{$u_{i - 1} < 16$}{ - $u_{i - 1} \rightarrow$ \WUNARY with stop bit 0\; - }{ - $16 \rightarrow$ \WUNARY with stop bit 0\; - $u_{i - 1} - 16 \rightarrow$ \hyperref[wavpack:write_egc]{write in modified Elias gamma code}\; - } - } - \If{$\text{\ADD}_{i - 1} > 0$}{ - $p \leftarrow \lfloor\log_2(\text{\ADD}_{i - 1})\rfloor$\; - $e \leftarrow 2 ^ {p + 1} - \text{\ADD}_{i - 1} - 1$\; - \eIf{$\text{\OFFSET}_{i - 1} < e$}{ - $r \leftarrow \text{\OFFSET}_{i - 1}$\; - $r \rightarrow$ \WRITE $p$ unsigned bits\; - }{ - $r \leftarrow \lfloor(\text{\OFFSET}_{i - 1} + e) \div 2\rfloor$\; - $b \leftarrow (\text{\OFFSET}_{i - 1} + e) \bmod 2$\; - $r \rightarrow$ \WRITE $p$ unsigned bits\; - $b \rightarrow$ \WRITE 1 unsigned bit\; - } - } - $\text{\SIGN}_{i - 1} \rightarrow$ \WRITE 1 unsigned bit\; - }{ - $u_{i - 1} \leftarrow \text{\UNDEFINED}$\; - } - \Return $u_{i - 1}$\; - \EALGORITHM -} - -\clearpage - - -\begin{landscape} - -\subsubsection{Residual Encoding Example} -{\relsize{-2} -\renewcommand{\arraystretch}{1.75} -\begin{tabular}{|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|} -i & r_i & \text{unsigned}_i &\text{sign}_i & \text{median}_{c~0} & \text{median}_{c~1} & \text{median}_{c~2} & m_i & \text{offset}_i & \text{add}_i \\ -\hline -0 & -61 & -60 & 1 & -\left\lfloor\frac{118}{2 ^ 4}\right\rfloor + 1 = 8 & \left\lfloor\frac{194}{2 ^ 4}\right\rfloor + 1 = 13 & \left\lfloor\frac{322}{2 ^ 4}\right\rfloor + 1 = 21 & -3 & 60 - (8 + 13 + ((3 - 2) \times 21)) = 18 & 21 - 1 = 20 -\\ -1 & 31 & -31 & 0 & -\left\lfloor\frac{118}{2 ^ 4}\right\rfloor + 1 = 8 & \left\lfloor\frac{176}{2 ^ 4}\right\rfloor + 1 = 12 & \left\lfloor\frac{212}{2 ^ 4}\right\rfloor + 1 = 14 & -2 & 31 - (8 + 12) = 11 & 14 - 1 = 13 -\\ -\hline -2 & -33 & -32 & 1 & -\left\lfloor\frac{123}{2 ^ 4}\right\rfloor + 1 = 8 & \left\lfloor\frac{214}{2 ^ 4}\right\rfloor + 1 = 14 & \left\lfloor\frac{377}{2 ^ 4}\right\rfloor + 1 = 24 & -2 & 32 - (8 + 14) = 10 & 24 - 1 = 23 -\\ -3 & 32 & -32 & 0 & -\left\lfloor\frac{123}{2 ^ 4}\right\rfloor + 1 = 8 & \left\lfloor\frac{191}{2 ^ 4}\right\rfloor + 1 = 12 & \left\lfloor\frac{198}{2 ^ 4}\right\rfloor + 1 = 13 & -2 & 32 - (8 + 12) = 12 & 13 - 1 = 12 -\\ -\hline -4 & -18 & -17 & 1 & -\left\lfloor\frac{128}{2 ^ 4}\right\rfloor + 1 = 9 & \left\lfloor\frac{234}{2 ^ 4}\right\rfloor + 1 = 15 & \left\lfloor\frac{353}{2 ^ 4}\right\rfloor + 1 = 23 & -1 & 17 - 9 = 8 & 15 - 1 = 14 -\\ -5 & 36 & -36 & 0 & -\left\lfloor\frac{128}{2 ^ 4}\right\rfloor + 1 = 9 & \left\lfloor\frac{206}{2 ^ 4}\right\rfloor + 1 = 13 & \left\lfloor\frac{184}{2 ^ 4}\right\rfloor + 1 = 12 & -3 & 36 - (9 + 13 + ((3 - 2) \times 12)) = 2 & 12 - 1 = 11 -\\ -\hline -6 & 1 & -1 & 0 & -\left\lfloor\frac{138}{2 ^ 4}\right\rfloor + 1 = 9 & \left\lfloor\frac{226}{2 ^ 4}\right\rfloor + 1 = 15 & \left\lfloor\frac{353}{2 ^ 4}\right\rfloor + 1 = 23 & -0 & 1 & 9 - 1 = 8 -\\ -7 & 37 & -37 & 0 & -\left\lfloor\frac{138}{2 ^ 4}\right\rfloor + 1 = 9 & \left\lfloor\frac{226}{2 ^ 4}\right\rfloor + 1 = 15 & \left\lfloor\frac{214}{2 ^ 4}\right\rfloor + 1 = 14 & -2 & 37 - (9 + 15) = 13 & 14 - 1 = 13 -\\ -\hline -8 & 20 & -20 & 0 & -\left\lfloor\frac{134}{2 ^ 4}\right\rfloor + 1 = 9 & \left\lfloor\frac{226}{2 ^ 4}\right\rfloor + 1 = 15 & \left\lfloor\frac{353}{2 ^ 4}\right\rfloor + 1 = 23 & -1 & 20 - 9 = 11 & 15 - 1 = 14 -\\ -9 & 35 & -35 & 0 & -\left\lfloor\frac{148}{2 ^ 4}\right\rfloor + 1 = 10 & \left\lfloor\frac{246}{2 ^ 4}\right\rfloor + 1 = 16 & \left\lfloor\frac{200}{2 ^ 4}\right\rfloor + 1 = 13 & -2 & 35 - (10 + 16) = 9 & 13 - 1 = 12 -\\ -\hline -10 & 35 & -35 & 0 & -\left\lfloor\frac{144}{2 ^ 4}\right\rfloor + 1 = 10 & \left\lfloor\frac{218}{2 ^ 4}\right\rfloor + 1 = 14 & \left\lfloor\frac{353}{2 ^ 4}\right\rfloor + 1 = 23 & -2 & 35 - (10 + 14) = 11 & 23 - 1 = 22 -\\ -11 & 31 & -31 & 0 & -\left\lfloor\frac{158}{2 ^ 4}\right\rfloor + 1 = 10 & \left\lfloor\frac{266}{2 ^ 4}\right\rfloor + 1 = 17 & \left\lfloor\frac{186}{2 ^ 4}\right\rfloor + 1 = 12 & -2 & 31 - (10 + 17) = 4 & 12 - 1 = 11 -\\ -\hline -12 & 50 & -50 & 0 & -\left\lfloor\frac{154}{2 ^ 4}\right\rfloor + 1 = 10 & \left\lfloor\frac{238}{2 ^ 4}\right\rfloor + 1 = 15 & \left\lfloor\frac{331}{2 ^ 4}\right\rfloor + 1 = 21 & -3 & 50 - (10 + 15 + ((3 - 2) \times 21)) = 4 & 21 - 1 = 20 -\\ -13 & 25 & -25 & 0 & -\left\lfloor\frac{168}{2 ^ 4}\right\rfloor + 1 = 11 & \left\lfloor\frac{291}{2 ^ 4}\right\rfloor + 1 = 19 & \left\lfloor\frac{174}{2 ^ 4}\right\rfloor + 1 = 11 & -1 & 25 - 11 = 14 & 19 - 1 = 18 -\\ -\hline -14 & 62 & -62 & 0 & -\left\lfloor\frac{164}{2 ^ 4}\right\rfloor + 1 = 11 & \left\lfloor\frac{258}{2 ^ 4}\right\rfloor + 1 = 17 & \left\lfloor\frac{386}{2 ^ 4}\right\rfloor + 1 = 25 & -3 & 62 - (11 + 17 + ((3 - 2) \times 25)) = 9 & 25 - 1 = 24 -\\ -15 & 18 & -18 & 0 & -\left\lfloor\frac{178}{2 ^ 4}\right\rfloor + 1 = 12 & \left\lfloor\frac{281}{2 ^ 4}\right\rfloor + 1 = 18 & \left\lfloor\frac{174}{2 ^ 4}\right\rfloor + 1 = 11 & -1 & 18 - 12 = 6 & 18 - 1 = 17 -\\ -\hline -16 & 68 & -68 & 0 & -\left\lfloor\frac{174}{2 ^ 4}\right\rfloor + 1 = 11 & \left\lfloor\frac{283}{2 ^ 4}\right\rfloor + 1 = 18 & \left\lfloor\frac{451}{2 ^ 4}\right\rfloor + 1 = 29 & -3 & 68 - (11 + 18 + ((3 - 2) \times 29)) = 10 & 29 - 1 = 28 -\\ -17 & 10 & -10 & 0 & -\left\lfloor\frac{188}{2 ^ 4}\right\rfloor + 1 = 12 & \left\lfloor\frac{271}{2 ^ 4}\right\rfloor + 1 = 17 & \left\lfloor\frac{174}{2 ^ 4}\right\rfloor + 1 = 11 & -0 & 10 & 12 - 1 = 11 -\\ -\hline -18 & 71 & -71 & 0 & -\left\lfloor\frac{184}{2 ^ 4}\right\rfloor + 1 = 12 & \left\lfloor\frac{308}{2 ^ 4}\right\rfloor + 1 = 20 & \left\lfloor\frac{526}{2 ^ 4}\right\rfloor + 1 = 33 & -3 & 71 - (12 + 20 + ((3 - 2) \times 33)) = 6 & 33 - 1 = 32 -\\ -19 & 0 & -0 & 0 & -\left\lfloor\frac{184}{2 ^ 4}\right\rfloor + 1 = 12 & \left\lfloor\frac{271}{2 ^ 4}\right\rfloor + 1 = 17 & \left\lfloor\frac{174}{2 ^ 4}\right\rfloor + 1 = 11 & -0 & 0 & 12 - 1 = 11 -\\ -\hline -\end{tabular} -\renewcommand{\arraystretch}{1.0} -} - -\clearpage - -{\relsize{-2} -\renewcommand{\arraystretch}{1.75} -\begin{tabular}{|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|} -i & m_i & \text{offset}_i & \text{add}_i & u_i & p_i & e_i & r_i & b_i \\ -\hline -0 & 3 & -18 & 20 & -(3 \times 2) + 1 = 7 & -\lfloor\log_2(20)\rfloor = 4 & -2 ^ {(4 + 1)} - 20 - 1 = 11 & -\lfloor(18 + 11) \div 2 \rfloor = 14 & -(18 + 11) \bmod 2 = 1 \\ -1 & 2 & -11 & 13 & -(2 \times 2) - 1 = 3 & -\lfloor\log_2(13)\rfloor = 3 & -2 ^ {(3 + 1)} - 13 - 1 = 2 & -\lfloor(11 + 2) \div 2 \rfloor = 6 & -(11 + 2) \bmod 2 = 1 \\ -\hline -2 & 2 & -10 & 23 & -(2 \times 2) - 1 = 3 & -\lfloor\log_2(23)\rfloor = 4 & -2 ^ {(4 + 1)} - 23 - 1 = 8 & -\lfloor(10 + 8) \div 2 \rfloor = 9 & -(10 + 8) \bmod 2 = 0 \\ -3 & 2 & -12 & 12 & -(2 \times 2) - 1 = 3 & -\lfloor\log_2(12)\rfloor = 3 & -2 ^ {(3 + 1)} - 12 - 1 = 3 & -\lfloor(12 + 3) \div 2 \rfloor = 7 & -(12 + 3) \bmod 2 = 1 \\ -\hline -4 & 1 & -8 & 14 & -(1 \times 2) - 1 = 1 & -\lfloor\log_2(14)\rfloor = 3 & -2 ^ {(3 + 1)} - 14 - 1 = 1 & -\lfloor(8 + 1) \div 2 \rfloor = 4 & -(8 + 1) \bmod 2 = 1 \\ -5 & 3 & -2 & 11 & -(3 - 1) \times 2 = 4 & -\lfloor\log_2(11)\rfloor = 3 & -2 ^ {(3 + 1)} - 11 - 1 = 4 & -2 & \\ -\hline -6 & 0 & -1 & 8 & -\textit{undefined} & -\lfloor\log_2(8)\rfloor = 3 & -2 ^ {(3 + 1)} - 8 - 1 = 7 & -1 & \\ -7 & 2 & -13 & 13 & -(2 \times 2) + 1 = 5 & -\lfloor\log_2(13)\rfloor = 3 & -2 ^ {(3 + 1)} - 13 - 1 = 2 & -\lfloor(13 + 2) \div 2 \rfloor = 7 & -(13 + 2) \bmod 2 = 1 \\ -\hline -8 & 1 & -11 & 14 & -(1 \times 2) - 1 = 1 & -\lfloor\log_2(14)\rfloor = 3 & -2 ^ {(3 + 1)} - 14 - 1 = 1 & -\lfloor(11 + 1) \div 2 \rfloor = 6 & -(11 + 1) \bmod 2 = 0 \\ -9 & 2 & -9 & 12 & -(2 \times 2) - 1 = 3 & -\lfloor\log_2(12)\rfloor = 3 & -2 ^ {(3 + 1)} - 12 - 1 = 3 & -\lfloor(9 + 3) \div 2 \rfloor = 6 & -(9 + 3) \bmod 2 = 0 \\ -\hline -10 & 2 & -11 & 22 & -(2 \times 2) - 1 = 3 & -\lfloor\log_2(22)\rfloor = 4 & -2 ^ {(4 + 1)} - 22 - 1 = 9 & -\lfloor(11 + 9) \div 2 \rfloor = 10 & -(11 + 9) \bmod 2 = 0 \\ -11 & 2 & -4 & 11 & -(2 \times 2) - 1 = 3 & -\lfloor\log_2(11)\rfloor = 3 & -2 ^ {(3 + 1)} - 11 - 1 = 4 & -\lfloor(4 + 4) \div 2 \rfloor = 4 & -(4 + 4) \bmod 2 = 0 \\ -\hline -12 & 3 & -4 & 20 & -(3 \times 2) - 1 = 5 & -\lfloor\log_2(20)\rfloor = 4 & -2 ^ {(4 + 1)} - 20 - 1 = 11 & -4 & \\ -13 & 1 & -14 & 18 & -(1 \times 2) - 1 = 1 & -\lfloor\log_2(18)\rfloor = 4 & -2 ^ {(4 + 1)} - 18 - 1 = 13 & -\lfloor(14 + 13) \div 2 \rfloor = 13 & -(14 + 13) \bmod 2 = 1 \\ -\hline -14 & 3 & -9 & 24 & -(3 \times 2) - 1 = 5 & -\lfloor\log_2(24)\rfloor = 4 & -2 ^ {(4 + 1)} - 24 - 1 = 7 & -\lfloor(9 + 7) \div 2 \rfloor = 8 & -(9 + 7) \bmod 2 = 0 \\ -15 & 1 & -6 & 17 & -(1 \times 2) - 1 = 1 & -\lfloor\log_2(17)\rfloor = 4 & -2 ^ {(4 + 1)} - 17 - 1 = 14 & -6 & \\ -\hline -16 & 3 & -10 & 28 & -(3 - 1) \times 2 = 4 & -\lfloor\log_2(28)\rfloor = 4 & -2 ^ {(4 + 1)} - 28 - 1 = 3 & -\lfloor(9 + 3) \div 2 \rfloor = 6 & -(9 + 3) \bmod 2 = 0 \\ -17 & 0 & -10 & 11 & -\textit{undefined} & -\lfloor\log_2(11)\rfloor = 3 & -2 ^ {(3 + 1)} - 11 - 1 = 4 & -\lfloor(10 + 4) \div 2 \rfloor = 7 & -(10 + 4) \bmod 2 = 0 \\ -\hline -18 & 3 & -6 & 32 & -3 \times 2 = 6 & -\lfloor\log_2(32)\rfloor = 5 & -2 ^ {(5 + 1)} - 32 - 1 = 31 & -6 & \\ -19 & 0 & -0 & 11 & -\textit{undefined} & -\lfloor\log_2(11)\rfloor = 3 & -2 ^ {(3 + 1)} - 11 - 1 = 4 & -0 & \\ -\hline -\end{tabular} -\renewcommand{\arraystretch}{1.0} -} - -\end{landscape} - -\subsubsection{2nd Residual Encoding Example} -{\relsize{-2} -\renewcommand{\arraystretch}{1.75} -\begin{tabular}{|r|r|>{$}r<{$}>{$}r<{$}>{$}r<{$}||>{$}r<{$}|>{$}r<{$}>{$}r<{$}>{$}r<{$}>{$}r<{$}>{$}r<{$}|l} - $i$ & $r_i$ & \text{entropy}_{0~0} & \text{entropy}_{0~1} & \text{entropy}_{0~2} & u_i & \text{zeroes}_i & m_i & \text{offset}_i & \text{add}_i & \text{sign}_i \\ -\cline{0-10} --2 & & & & & \textit{und.} & & & & & \\ --1 & & {\color{red}0} & 0 & 0 & {\color{red}\textit{und.}} & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ -0 & 1 & 0 & 0 & 0 & 3 & {\color{blue}0} & 1 & 0 & 0 & 0 \\ -1 & 2 & 5 & 0 & 0 & 3 & \textit{und.} & 2 & 0 & 0 & 0 \\ -2 & 3 & 10 & 5 & 0 & 5 & \textit{und.} & 3 & 0 & 0 & 0 \\ -3 & 2 & 15 & 10 & 5 & 2 & \textit{und.} & 2 & 0 & 0 & 0 \\ -4 & 1 & 20 & 15 & 3 & \textit{und.} & \textit{und.} & 0 & 1 & 1 & 0 \\ -5 & 0 & 18 & 15 & 3 & 0 & \textit{und.} & 0 & 0 & 1 & 0 \\ -6 & 0 & 16 & 15 & 3 & \textit{und.} & \textit{und.} & 0 & 0 & 1 & 0 \\ -7 & 0 & 14 & 15 & 3 & 0 & \textit{und.} & 0 & 0 & 0 & 0 \\ -8 & 0 & 12 & 15 & 3 & \textit{und.} & \textit{und.} & 0 & 0 & 0 & 0 \\ -9 & 0 & 10 & 15 & 3 & 0 & \textit{und.} & 0 & 0 & 0 & 0 \\ -10 & 0 & 8 & 15 & 3 & \textit{und.} & \textit{und.} & 0 & 0 & 0 & 0 \\ -11 & 0 & 6 & 15 & 3 & 0 & \textit{und.} & 0 & 0 & 0 & 0 \\ -12 & 0 & 4 & 15 & 3 & \textit{und.} & \textit{und.} & 0 & 0 & 0 & 0 \\ -13 & 0 & 2 & 15 & 3 & 0 & \textit{und.} & 0 & 0 & 0 & 0 \\ -14 & 0 & {\color{red}0} & 15 & 3 & {\color{red}\textit{und.}} & \textit{und.} & 0 & 0 & 0 & 0 \\ -\cline{0-10} -15 & 0 & 0 & 15 & 3 & \textit{und.} & 1 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} & \multirow{10}{1em}{\begin{sideways}block of 10 zero residuals\end{sideways}} \\ -16 & 0 & 0 & 0 & 0 & \textit{und.} & 2 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ -17 & 0 & 0 & 0 & 0 & \textit{und.} & 3 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ -18 & 0 & 0 & 0 & 0 & \textit{und.} & 4 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ -19 & 0 & 0 & 0 & 0 & \textit{und.} & 5 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ -20 & 0 & 0 & 0 & 0 & \textit{und.} & 6 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ -21 & 0 & 0 & 0 & 0 & \textit{und.} & 7 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ -22 & 0 & 0 & 0 & 0 & \textit{und.} & 8 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ -23 & 0 & 0 & 0 & 0 & \textit{und.} & 9 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ -24 & 0 & 0 & 0 & 0 & \textit{und.} & 10 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ -\cline{0-10} -25 & -1 & 0 & 0 & 0 & 1 & {\color{blue}10} & 0 & 0 & 0 & 1 \\ -26 & -2 & 0 & 0 & 0 & 1 & \textit{und.} & 1 & 0 & 0 & 1 \\ -27 & -3 & 5 & 0 & 0 & 3 & \textit{und.} & 2 & 0 & 0 & 1 \\ -28 & -2 & 10 & 5 & 0 & 0 & \textit{und.} & 1 & 0 & 0 & 1 \\ -29 & -1 & 15 & 3 & 0 & \textit{und.} & \textit{und.} & 0 & 0 & 0 & 1 \\ -\cline{0-10} -\end{tabular} -} - -\clearpage - -This example is more simplified to demonstrate how the \VAR{zeroes} -value propagates in two instances. - -For $r_0$, because $\text{entropy}_{0~0} = 0$ and -$u_{(-1)} = \textit{undefined}$\footnote{as determined by the \texttt{unary\_undefined} function}, -we must handle a block of zeroes in some way. -But because $r_0 \neq 0$, we prepend a ``false alarm'' block of zeroes -and encode the residual normally. - -For $r_{15}$, because $\text{entropy}_{0~0} = 0$, -$u_{14} = \textit{undefined}$ and $r_{15} = 0$, -we flush $\text{residual}_{14}$'s values and begin a block of zeroes -which continues until $r_{25} \neq 0$. -This block of zeroes is prepended to $\text{residual}_{25}$'s -values, which are flushed once $\text{residual}_{26}$ is encoded -and $u_{25}$ can be calculated from $u_{24}$ and $m_{26}$. - -\begin{figure}[h] - \includegraphics{figures/wavpack/residuals_parse2.pdf} - \caption{2nd residual encoding example} -\end{figure} - -\begin{figure}[h] - \includegraphics{figures/wavpack/residuals.pdf} -\end{figure} - -\clearpage - -\subsection{Writing RIFF WAVE Header and Footer} -\label{wavpack:write_wave_header} -\begin{figure}[h] - \includegraphics{figures/wavpack/pcm_sandwich.pdf} -\end{figure} - - -\subsection{Writing MD5 Sum} -\label{wavpack:write_md5} -MD5 sum is calculated as if the PCM data had been read from -a wave file's \texttt{data} chunk. -That is, the samples are converted to little-endian format -and are signed if the stream's bits-per-sample is greater than 8. -\begin{figure}[h] - \includegraphics{figures/wavpack/md5sum.pdf} -\end{figure} - -\subsection{Writing Extended Integers} -\label{wavpack:write_extended_integers} -\begin{figure}[h] - \includegraphics{figures/wavpack/extended_integers.pdf} -\end{figure} - -\clearpage - -\subsection{Writing Channel Info} -\label{wavpack:write_channel_info} -\begin{figure}[h] - \includegraphics{figures/wavpack/channel_info.pdf} -\end{figure} - - -\subsection{Writing Sample Rate} -\label{wavpack:write_sample_rate} -\begin{figure}[h] - \includegraphics{figures/wavpack/sample_rate.pdf} -\end{figure} - -\clearpage - -\subsection{Writing Sub Block} -{\relsize{-1} -\ALGORITHM{metadata function, nondecoder data flag, sub block data}{sub block header data} -\SetKwData{DATA}{sub block data} -\SetKwFunction{LEN}{len} -$\text{metadata function} \rightarrow$ \WRITE 5 unsigned bits\; -$\text{nondecoder data} \rightarrow$ \WRITE 1 unsigned bit\; -$\LEN(\text{\DATA}) \bmod 2 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{actual size 1 less} -\eIf(\tcc*[f]{large block}){$\LEN(\text{\DATA}) > 255 \times 2$}{ - $1 \rightarrow$ \WRITE 1 unsigned bit\; - $\lceil\LEN(\text{\DATA}) \div 2\rceil \rightarrow$ \WRITE 24 unsigned bits\; -}{ - $0 \rightarrow$ \WRITE 1 unsigned bit\; - $\lceil\LEN(\text{\DATA}) \div 2\rceil \rightarrow$ \WRITE 8 unsigned bits\; -} -write \DATA\; -\If{$\LEN(\text{\DATA}) \bmod 2 = 1$}{ - $0 \rightarrow$ \WRITE 8 unsigned bits\; -} -\EALGORITHM -} - -\begin{figure}[h] -\includegraphics{figures/wavpack/block_header2.pdf} -\end{figure} - -\clearpage - -\subsection{Writing Block Header} -\label{wavpack:write_block_header} -{\relsize{-1} -\ALGORITHM{total sub blocks size, block index, block samples, bits per sample, channel count, joint stereo, correlation pass count, wasted BPS, initial block, final block, maximum magnitude, sample rate, false stereo, CRC}{block header data} -\SetKwData{SUBBLOCKSSIZE}{sub blocks size} -\SetKwData{BLOCKINDEX}{block index} -\SetKwData{BLOCKSAMPLES}{block samples} -\SetKwData{BITSPERSAMPLE}{bits per sample} -\SetKwData{CHANNELCOUNT}{channel count} -\SetKwData{PASSES}{correlation passes} -\SetKwData{WASTEDBPS}{wasted BPS} -\SetKwData{JOINTSTEREO}{joint stereo} -\SetKwData{INITIALBLOCK}{initial block} -\SetKwData{FINALBLOCK}{final block} -\SetKwData{MAXMAGNITUDE}{maximum magnitude} -\SetKwData{FALSESTEREO}{false stereo} -$\texttt{"wvpk"} \rightarrow$ \WRITE 4 bytes\; -$\SUBBLOCKSSIZE + 24 \rightarrow$ \WRITE 32 unsigned bits\; -$\texttt{0x0410} \rightarrow$ \WRITE 16 unsigned bits\tcc*[r]{version} -$0 \rightarrow$ \WRITE 8 unsigned bits\tcc*[r]{track number} -$0 \rightarrow$ \WRITE 8 unsigned bits\tcc*[r]{index number} -$\texttt{0xFFFFFFFF} \rightarrow$ \WRITE 32 unsigned bits\tcc*[r]{total samples placeholder} -$\BLOCKINDEX \rightarrow$ \WRITE 32 unsigned bits\; -$\BLOCKSAMPLES \rightarrow$ \WRITE 32 unsigned bits\; -$\text{\BITSPERSAMPLE} \div 8 - 1 \rightarrow$ \WRITE 2 unsigned bits\; -$2 - \text{\CHANNELCOUNT} \rightarrow$ \WRITE 1 unsigned bit\; -$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{hybrid mode} -\eIf(\tcc*[f]{joint stereo}){$\text{\CHANNELCOUNT} = 2$}{ - $1 \rightarrow$ \WRITE 1 unsigned bit -}{ - $0 \rightarrow$ \WRITE 1 unsigned bit -} -\eIf(\tcc*[f]{cross channel decorrelation}){$\text{\PASSES} > 5$}{ - $1 \rightarrow$ \WRITE 1 unsigned bit\; -}{ - $0 \rightarrow$ \WRITE 1 unsigned bit\; -} -$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{hybrid noise shaping} -$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{floating point data} -\eIf(\tcc*[f]{extended size integers}){$\WASTEDBPS > 0$}{ - $1 \rightarrow$ \WRITE 1 unsigned bit\; -}{ - $0 \rightarrow$ \WRITE 1 unsigned bit\; -} -$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{hybrid controls bitrate} -$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{hybrid noise balanced} -$\INITIALBLOCK \rightarrow$ \WRITE 1 unsigned bit\; -$\FINALBLOCK \rightarrow$ \WRITE 1 unsigned bit\; -$0 \rightarrow$ \WRITE 5 unsigned bits\tcc*[r]{left shift data} -$\MAXMAGNITUDE \rightarrow$ \WRITE 5 unsigned bits\; -$\textit{encoded sample rate} \rightarrow$ \WRITE 4 unsigned bits\; -\begin{tabular}{rr|rr|rr} - sample rate & \textit{encoded} & - sample rate & \textit{encoded} & - sample rate & \textit{encoded} \\ - \hline - 6000 Hz & \texttt{0} & - 22050 Hz & \texttt{6} & - 64000 Hz & \texttt{11} \\ - 8000 Hz & \texttt{1} & - 24000 Hz & \texttt{7} & - 88200 Hz & \texttt{12} \\ - 9600 Hz & \texttt{2} & - 32000 Hz & \texttt{8} & - 96000 Hz & \texttt{13} \\ - 11025 Hz & \texttt{3} & - 44100 Hz & \texttt{9} & - 192000 Hz & \texttt{14} \\ - 12000 Hz & \texttt{4} & - 48000 Hz & \texttt{10} & - otherwise & \texttt{15} \\ - 16000 Hz & \texttt{5} \\ -\end{tabular} \\ -$0 \rightarrow$ \WRITE 2 unsigned bits\tcc*[r]{reserved} -$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{use IIR} -$\FALSESTEREO \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{if $\text{channel}_0 = \text{channel}_1$} -$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{reserved} -$CRC \rightarrow$ \WRITE 32 unsigned bits\; -\EALGORITHM -} +\input{wavpack/encode}
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/decode
Added
+(directory)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/decode.tex
Added
@@ -0,0 +1,745 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\section{WavPack Decoding} + +\ALGORITHM{a WavPack encoded file}{PCM samples} +\SetKwData{INDEX}{index} +\SetKwData{SAMPLES}{samples} +\SetKwData{TOTALSAMPLES}{total samples} +\SetKwData{FINALBLOCK}{final block} +\SetKwData{OUTPUTCHANNEL}{output channel} +\SetKwData{BLOCKCHANNEL}{block channel} +\SetKwData{BLOCKDATASIZE}{block data size} +\Repeat{block header's $\text{\INDEX} + \text{\SAMPLES} >= \text{\TOTALSAMPLES}$}{ + $c \leftarrow 0$\; + \Repeat{block header's $\text{\FINALBLOCK} = 1$}{ + \hyperref[wavpack:read_block_header]{read block header}\; + read \BLOCKDATASIZE bytes of block data\; + \hyperref[wavpack:decode_block]{decode block header and block data to 1 or 2 channels of PCM data}\; + \eIf{2 channels}{ + $\text{\OUTPUTCHANNEL}_c \leftarrow \text{\BLOCKCHANNEL}_0$\; + $\text{\OUTPUTCHANNEL}_{c + 1} \leftarrow \text{\BLOCKCHANNEL}_1$\; + $c \leftarrow c + 2$\; + }{ + $\text{\OUTPUTCHANNEL}_c \leftarrow \text{\BLOCKCHANNEL}_0$\; + $c \leftarrow c + 1$\; + } + } + update stream MD5 sum with \OUTPUTCHANNEL data\; + \Return \OUTPUTCHANNEL data\; +} +\BlankLine +\hyperref[wavpack:md5_sum]{attempt to read one additional block for MD5 sub block}\; +\If{MD5 sub block found}{ + \ASSERT sub block MD5 sum = stream MD5 sum\; +} +\EALGORITHM +\par +\noindent +For example, four blocks with the following attributes: +\begin{table}[h] +\begin{tabular}{rrr} +channel count & initial block & final block \\ +\hline +2 & 1 & 0 \\ +1 & 0 & 0 \\ +1 & 0 & 0 \\ +2 & 0 & 1 \\ +\end{tabular} +\end{table} +\par +\noindent +combine into 6 channels of PCM output as follows: +\begin{figure}[h] +\includegraphics{wavpack/figures/block_channels.pdf} +\end{figure} + +\clearpage + +\subsection{Reading Block Header} +\label{wavpack:read_block_header} +{\relsize{-1} +\ALGORITHM{a WavPack file stream}{block header fields} +\SetKwData{BLOCKID}{block ID} +\SetKwData{BLOCKSIZE}{block data size} +\SetKwData{VERSION}{version} +\SetKwData{TRACKNUMBER}{track number} +\SetKwData{INDEXNUMBER}{index number} +\SetKwData{TOTALSAMPLES}{total samples} +\SetKwData{BLOCKINDEX}{block index} +\SetKwData{BLOCKSAMPLES}{block samples} +\SetKwData{BITSPERSAMPLE}{bits per sample} +\SetKwData{MONOOUTPUT}{mono output} +\SetKwData{HYBRIDMODE}{hybrid mode} +\SetKwData{JOINTSTEREO}{joint stereo} +\SetKwData{CHANNELDECORR}{channel decorrelation} +\SetKwData{HYBRIDNOISESHAPING}{hybrid noise shaping} +\SetKwData{FLOATINGPOINTDATA}{floating point data} +\SetKwData{EXTENDEDSIZEINTEGERS}{extended size integers} +\SetKwData{HYBRIDCONTROLSBITRATE}{hybrid controls bitrate} +\SetKwData{HYBRIDNOISEBALANCED}{hybrid noise balanced} +\SetKwData{INITIALBLOCK}{initial block} +\SetKwData{FINALBLOCK}{final block} +\SetKwData{LEFTSHIFTDATA}{left shift data} +\SetKwData{MAXIMUMMAGNITUDE}{maximum magnitude} +\SetKwData{SAMPLERATE}{sample rate} +\SetKwData{USEIIR}{use IIR} +\SetKwData{FALSESTEREO}{false stereo} +\SetKwData{RESERVED}{reserved} +\SetKwData{CRC}{CRC} +\begin{tabular}{r>{$}c<{$}l} +\BLOCKID & \leftarrow & \READ 4 bytes\; \\ +& & \ASSERT $\text{\BLOCKID} = \texttt{"wvpk"}$\; \\ +\BLOCKSIZE & \leftarrow & (\READ 32 unsigned bits) - 24\; \\ +\VERSION & \leftarrow & \READ 16 unsigned bits\; \\ +\TRACKNUMBER & \leftarrow & \READ 8 unsigned bits\; \\ +\INDEXNUMBER & \leftarrow & \READ 8 unsigned bits\; \\ +\TOTALSAMPLES & \leftarrow & \READ 32 unsigned bits\; \\ +\BLOCKINDEX & \leftarrow & \READ 32 unsigned bits\; \\ +\BLOCKSAMPLES & \leftarrow & \READ 32 unsigned bits\; \\ +\BITSPERSAMPLE & \leftarrow & $(\text{\READ 2 unsigned bits} + 1) \times 8$\; \\ +\MONOOUTPUT & \leftarrow & \READ 1 unsigned bit\; \\ +\HYBRIDMODE & \leftarrow & \READ 1 unsigned bit\; \\ +\JOINTSTEREO & \leftarrow & \READ 1 unsigned bit\; \\ +\CHANNELDECORR & \leftarrow & \READ 1 unsigned bit\; \\ +\HYBRIDNOISESHAPING & \leftarrow & \READ 1 unsigned bit\; \\ +\FLOATINGPOINTDATA & \leftarrow & \READ 1 unsigned bit\; \\ +\EXTENDEDSIZEINTEGERS & \leftarrow & \READ 1 unsigned bit\; \\ +\HYBRIDCONTROLSBITRATE & \leftarrow & \READ 1 unsigned bit\; \\ +\HYBRIDNOISEBALANCED & \leftarrow & \READ 1 unsigned bit\; \\ +\INITIALBLOCK & \leftarrow & \READ 1 unsigned bit\; \\ +\FINALBLOCK & \leftarrow & \READ 1 unsigned bit\; \\ +\LEFTSHIFTDATA & \leftarrow & \READ 5 unsigned bits\; \\ +\MAXIMUMMAGNITUDE & \leftarrow & \READ 5 unsigned bits\; \\ +\textit{encoded sample rate} & \leftarrow & \READ 4 unsigned bits\; \\ +& & \SKIP 2 bits\; \\ +\USEIIR & \leftarrow & \READ 1 unsigned bit\; \\ +\FALSESTEREO & \leftarrow & \READ 1 unsigned bit\; \\ +\RESERVED & \leftarrow & \READ 1 unsigned bit\; \\ +& & \ASSERT $\text{\RESERVED} = 0$\; \\ +\CRC & \leftarrow & \READ 32 unsigned bits\; \\ +\end{tabular} +\EALGORITHM +} +{\relsize{-1} +\begin{tabular}{rr} + \textit{encoded sample rate} & sample rate \\ + \hline + \texttt{0} & 6000 Hz \\ + \texttt{1} & 8000 Hz \\ + \texttt{2} & 9600 Hz \\ + \texttt{3} & 11025 Hz \\ + \texttt{4} & 12000 Hz \\ + \texttt{5} & 16000 Hz \\ + \texttt{6} & 22050 Hz \\ + \texttt{7} & 24000 Hz \\ + \texttt{8} & 32000 Hz \\ + \texttt{9} & 44100 Hz \\ + \texttt{10} & 48000 Hz \\ + \texttt{11} & 64000 Hz \\ + \texttt{12} & 88200 Hz \\ + \texttt{13} & 96000 Hz \\ + \texttt{14} & 192000 Hz \\ + \texttt{15} & reserved \\ +\end{tabular} +} + +\clearpage + +\subsubsection{Reading Block Header Example} +\begin{figure}[h] + \includegraphics[height=3.5in,keepaspectratio]{wavpack/figures/block_header_parse.pdf} +\end{figure} + +{\relsize{-2} + \begin{tabular}{rrl} + field & value & meaning \\ + \hline + \textbf{block ID} & \texttt{0x6B707677} & \texttt{"wvpk"} \\ + \textbf{block data size} & \texttt{0x000000B2} & $178 - 24 = 154$ bytes \\ + \textbf{version} & \texttt{0x0407} \\ + \textbf{track number} & \texttt{0} \\ + \textbf{index number} & \texttt{0} \\ + \textbf{total samples} & \texttt{0x00000019} & 25 PCM frames \\ + \textbf{block index} & \texttt{0x00000000} & 0 PCM frames \\ + \textbf{block samples} & \texttt{0x00000019} & 25 PCM frames \\ + \textbf{bits per sample} & \texttt{1} & 16 bits per sample \\ + \textbf{mono output} & \texttt{0} & 2 channel block \\ + \textbf{hybrid mode} & \texttt{0} & lossless data \\ + \textbf{joint stereo} & \texttt{1} & channels stored in joint stereo \\ + \textbf{channel decorrelation} & \texttt{1} & cross-channel decorrelation used \\ + \textbf{hybrid noise shaping} & \texttt{0} \\ + \textbf{floating point data} & \texttt{0} & samples stored as integers \\ + \textbf{extended size integers} & \texttt{0} \\ + \textbf{hybrid controls bitrate} & \texttt{0} \\ + \textbf{hybrid noise balanced} & \texttt{0} \\ + \textbf{initial block} & \texttt{1} & is initial block in sequence \\ + \textbf{final block} & \texttt{1} & is final block in sequence \\ + \textbf{left shift data} & \texttt{0} \\ + \textbf{maximum magnitude} & \texttt{15} & 16 bits per sample output \\ + \textbf{sample rate} & \texttt{9} & 44100 Hz \\ + \textbf{reserved} & \texttt{0} \\ + \textbf{use IIR} & \texttt{0} \\ + \textbf{false stereo} & \texttt{0} & both channels are not identical \\ + \textbf{reserved} & \texttt{0} \\ + \textbf{CRC} & \texttt{0x22D25AD7} \\ + \end{tabular} +} + +\clearpage + +\subsection{Block Decoding} +\label{wavpack:decode_block} +{\relsize{-1} +\ALGORITHM{block header, block data}{1 or 2 channels of PCM sample data} +\SetKwData{METAFUNC}{metadata function} +\SetKwData{NONDECODER}{nondecoder data} +\SetKwData{ONELESS}{actual size 1 less} +\SetKwData{BLOCKDATA}{block data size} +\SetKwData{SUBBLOCKSIZE}{sub block size} +\SetKwData{TERMS}{decorrelation terms} +\SetKwData{DELTAS}{decorrelation deltas} +\SetKwData{WEIGHTS}{decorrelation weights} +\SetKwData{SAMPLES}{decorrelation samples} +\SetKwData{ENTROPIES}{entropies} +\SetKwData{RESIDUALS}{residuals} +\SetKwData{ZEROES}{zero bits} +\SetKwData{ONES}{one bits} +\SetKwData{DUPES}{duplicate bits} +\SetKw{AND}{and} +\tcc{read decoding parameters from sub blocks} +\While{$\text{\BLOCKDATA} > 0$}{ + \tcc{read sub block header and data} + \METAFUNC $\leftarrow$ \READ 5 unsigned bits\; + \NONDECODER $\leftarrow$ \READ 1 unsigned bit\; + \ONELESS $\leftarrow$ \READ 1 unsigned bit\; + \textit{large sub block} $\leftarrow$ \READ 1 unsigned bit\; + \eIf{$\text{large sub block} = 0$}{ + \SUBBLOCKSIZE $\leftarrow$ \READ 8 unsigned bits\; + }{ + \SUBBLOCKSIZE $\leftarrow$ \READ 24 unsigned bits\; + } + \eIf{$\ONELESS = 0$}{ + read $(\SUBBLOCKSIZE \times 2)$ bytes of sub block data\; + }{ + read $(\SUBBLOCKSIZE \times 2) - 1$ bytes of sub block data\; + \SKIP 1 byte\; + } + \BlankLine + \If(\tcc*[f]{decode audio-related sub blocks}){$\text{\NONDECODER} = 0$}{ + \Switch{\METAFUNC}{ + \uCase{2}{ + $\left.\begin{tabular}{r} + \TERMS \\ + \DELTAS \\ + \end{tabular}\right\rbrace \leftarrow$ \hyperref[wavpack:decode_decorrelation_terms]{decode decorrelation terms sub block}\; + } + \uCase{3}{ + \ASSERT \TERMS have been read\; + $\WEIGHTS \leftarrow$ \hyperref[wavpack:decode_decorrelation_weights]{decode decorrelation weights sub block}\; + } + \uCase{4}{ + \ASSERT \TERMS have been read\; + $\SAMPLES \leftarrow$ \hyperref[wavpack:decode_decorrelation_samples]{decode decorrelation samples sub block}\; + } + \uCase{5}{ + $\ENTROPIES \leftarrow$ \hyperref[wavpack:decode_entropy_variables]{decode entropy variables sub block}\; + } + \uCase{9}{ + $\left.\begin{tabular}{r} + \ZEROES \\ + \ONES \\ + \DUPES \\ + \end{tabular}\right\rbrace \leftarrow$ \hyperref[wavpack:decode_extended_integers]{decode extended integers sub block}\; + } + \Case{10}{ + \ASSERT \ENTROPIES have been read\; + $\RESIDUALS \leftarrow$ \hyperref[wavpack:decode_bitstream]{decode WavPack bitstream sub block using \ENTROPIES}\; + } + } + } + \eIf{$\text{large sub block} = 0$}{ + $\BLOCKDATA \leftarrow \BLOCKDATA - (2 + 2 \times \text{\SUBBLOCKSIZE})$ + }{ + $\BLOCKDATA \leftarrow \BLOCKDATA - (4 + 2 \times \text{\SUBBLOCKSIZE})$ + } +} +\If{\TERMS have been read}{ + \ASSERT \WEIGHTS and \SAMPLES have been read\; +} +\ASSERT \RESIDUALS have been read\; +\EALGORITHM +} + +\clearpage + +{\relsize{-1} +\begin{algorithm}[H] +\SetKwData{TERMS}{decorrelation terms} +\SetKwData{MONOOUTPUT}{mono output} +\SetKwData{FALSESTEREO}{false stereo} +\SetKwData{JOINTSTEREO}{joint stereo} +\SetKwData{EXTENDEDINTS}{extended integers} +\SetKwData{RESIDUALS}{residuals} +\SetKwData{TERMS}{decorrelation terms} +\SetKwData{DELTAS}{decorrelation deltas} +\SetKwData{WEIGHTS}{decorrelation weights} +\SetKwData{SAMPLES}{decorrelation samples} +\SetKwData{DECORRELATED}{decorrelated} +\SetKwData{LEFT}{left} +\SetKwData{RIGHT}{right} +\SetKwData{UNSHIFTED}{unshifted} +\SetKwFunction{LEN}{len} +\SetKwFunction{UNDOEXTENDEDINTS}{undo\_extended\_integers} +\SetKw{AND}{and} +\DontPrintSemicolon +\tcc{build PCM data from decoding parameters} +\eIf(\tcc*[f]{2 channels of output}){$\text{\MONOOUTPUT} = 0$ \AND $\text{\FALSESTEREO} = 0$}{ + \eIf{\TERMS have been read \AND $\LEN(\TERMS) > 0$}{ + $\left.\begin{tabular}{r} + $\text{\DECORRELATED}_0$ \\ + $\text{\DECORRELATED}_1$ \\ + \end{tabular}\right\rbrace \leftarrow$ + \hyperref[wavpack:decorrelate_channels]{decorrelate} + $\left\lbrace\begin{tabular}{l} + $\text{\RESIDUALS}_0$ \\ + $\text{\RESIDUALS}_1$ \\ + \TERMS \\ + \DELTAS \\ + \WEIGHTS \\ + \SAMPLES \\ + \end{tabular}\right.$\; + }{ + $\text{\DECORRELATED}_0 \leftarrow \text{\RESIDUALS}_0$\; + $\text{\DECORRELATED}_1 \leftarrow \text{\RESIDUALS}_1$\; + } + \eIf{$\text{\JOINTSTEREO} = 1$}{ + $\left.\begin{tabular}{r} + \LEFT \\ + \RIGHT \\ + \end{tabular}\right\rbrace \leftarrow$ + \hyperref[wavpack:undo_joint_stereo]{undo joint stereo} + $\left\lbrace\begin{tabular}{l} + $\text{\DECORRELATED}_0$ \\ + $\text{\DECORRELATED}_1$ \\ + \end{tabular}\right.$\; + }{ + $\LEFT \leftarrow \text{\DECORRELATED}_0$\; + $\RIGHT \leftarrow \text{\DECORRELATED}_1$\; + } + \hyperref[wavpack:verify_crc]{verify CRC of \LEFT and \RIGHT against CRC in block header}\; + \eIf{extended integers have been read}{ + $\left.\begin{tabular}{r} + $\text{\UNSHIFTED}_0$ \\ + $\text{\UNSHIFTED}_1$ \\ + \end{tabular}\right\rbrace \leftarrow$ + \hyperref[wavpack:undo_extended_integers]{undo extended integers} + $\left\lbrace\begin{tabular}{l} + \LEFT \\ + \RIGHT \\ + \end{tabular}\right.$\; + }{ + $\text{\UNSHIFTED}_0 \leftarrow \LEFT$\; + $\text{\UNSHIFTED}_1 \leftarrow \RIGHT$\; + } + \Return $\text{\UNSHIFTED}_0$ and $\text{\UNSHIFTED}_1$\; +}(\tcc*[f]{1 or 2 channels of output}){ + \eIf{\TERMS have been read \AND $\LEN(\TERMS) > 0$}{ + $\text{\DECORRELATED}_0 \leftarrow$ + \hyperref[wavpack:decorrelate_channels]{decorrelate} + $\left\lbrace\begin{tabular}{l} + $\text{\RESIDUALS}_0$ \\ + \TERMS \\ + \DELTAS \\ + \WEIGHTS \\ + \SAMPLES \\ + \end{tabular}\right.$\; + }{ + $\text{\DECORRELATED}_0 \leftarrow \text{\RESIDUALS}_0$\; + } + \hyperref[wavpack:verify_crc]{ verify CRC of $\text{\DECORRELATED}_0$ against CRC in block header}\; + \eIf{extended integers have been read}{ + $\text{\UNSHIFTED}_0 \leftarrow$ \hyperref[wavpack:undo_extended_integers]{undo extended integers for} $\text{\DECORRELATED}_0$\; + }{ + $\text{\UNSHIFTED}_0 \leftarrow \text{\DECORRELATED}_0$\; + } + \eIf{$\text{\FALSESTEREO} = 0$}{ + \Return $\text{\UNSHIFTED}_0$\; + }{ + \Return $\text{\UNSHIFTED}_0$ and $\text{\UNSHIFTED}_0$\tcc*[r]{duplicate channel} + } +} +\end{algorithm} +} +%% \begin{figure}[h] +%% \includegraphics{wavpack/figures/typical_block.pdf} +%% \end{figure} + +%% \clearpage + +%% \subsection{Reading Sub Block Header} +%% \ALGORITHM{block data}{metadata function integer, nondecoder data flag, \VAR{actual size 1 less} flag, sub block size} +%% \SetKwData{METAFUNC}{metadata function} +%% \SetKwData{NONDECODER}{nondecoder data} +%% \SetKwData{SUBBLOCKSIZE}{sub block size} +%% \SetKwData{ACTUALSIZEONELESS}{actual size 1 less} +%% \METAFUNC $\leftarrow$ \READ 5 unsigned bits\; +%% \NONDECODER $\leftarrow$ \READ 1 unsigned bit\; +%% \ACTUALSIZEONELESS $\leftarrow$ \READ 1 unsigned bit\; +%% \textit{large sub block} $\leftarrow$ \READ 1 unsigned bit\; +%% \eIf{$\text{large sub block} = 0$}{ +%% \SUBBLOCKSIZE $\leftarrow$ (\READ 8 unsigned bits) $\times 2$\; +%% }{ +%% \SUBBLOCKSIZE $\leftarrow$ (\READ 24 unsigned bits) $\times 2$\; +%% } +%% \Return (\METAFUNC , \NONDECODER , \ACTUALSIZEONELESS , \SUBBLOCKSIZE)\; +%% \EALGORITHM + +%% \subsubsection{Reading Sub Block Header Example} +%% \begin{figure}[h] +%% \includegraphics{wavpack/figures/subblock_parse.pdf} +%% \end{figure} +%% \begin{table}[h] +%% \begin{tabular}{r>{$}c<{$}l} +%% metadata function & \leftarrow & 2 \\ +%% nondecoder data & \leftarrow & 0 \\ +%% actual size 1 less & \leftarrow & 1 \\ +%% large sub block & \leftarrow & 0 \\ +%% sub block size & \leftarrow & $3 \times 2 = 6$ bytes\; +%% \end{tabular} +%% \end{table} +%% \par +%% \noindent +%% Note that although the total length of the sub block is +%% 8 bytes (2 bytes for the header plus the 6 bytes indicated by +%% \VAR{sub block size}), +%% \VAR{actual size 1 less} indicates that only the first 5 bytes +%% contain actual data and the final byte is padding. + +%% \clearpage + +%% \subsection{Reading Decoding Parameters} + +%% {\relsize{-1} +%% \ALGORITHM{\VAR{metadata function}, \VAR{nondecoder data}, \VAR{actual size 1 less}, \VAR{sub block size} from sub block header; block header}{parameters required for block decoding} +%% \SetKwData{METAFUNC}{metadata function} +%% \SetKwData{NONDECODER}{nondecoder data} +%% \eIf{$\text{\NONDECODER} = 0$}{ +%% \Switch{\METAFUNC}{ +%% \uCase{2}{ +%% read decorrelation terms and deltas from decorrelation terms sub block\; +%% \Return list of signed decorrelation terms and list of unsigned deltas\; +%% } +%% \uCase{3}{ +%% read decorrelation weights from decorrelation weights sub block\; +%% \Return signed decorrelation weight per channel per decorrelation term\; +%% } +%% \uCase{4}{ +%% read decorrelation samples from decorrelation samples sub block\; +%% \Return list of signed decorrelation samples per channel per decorrelation term\; +%% } +%% \uCase{5}{ +%% read 2 lists of 3 signed entropy variables\; +%% \Return 2 lists of signed entropy variables\; +%% } +%% \uCase{9}{ +%% read zero bits, one bits and duplicate bits from extended integers sub block\; +%% \Return zero bits, one bits, duplicate bits values\; +%% } +%% \uCase{10}{ +%% read WavPack bitstream\; +%% \Return list of signed residual values per channel\; +%% } +%% \Other{ +%% skip sub block\; +%% } +%% } +%% }{ +%% skip sub block\; +%% } +%% \EALGORITHM +%% } + +%% \subsubsection{The Decoding Parameters} +%% These parameters will be populated by sub blocks during decoding. +%% Once all the sub blocks have been read, +%% they will be used to transform residual data +%% into 1 or 2 channels of PCM data. +%% \begin{description} +%% \item[decorrelation terms] one signed integer from -3 to 18 per decorrelation pass +%% \item[decorrelation deltas] one unsigned integer per decorrelation pass +%% \item[decorrelation weights] one signed integer per decorrelation pass per channel +%% \item[decorrelation samples] one list of signed integers per decorrelation pass per channel +%% \item[entropy variables] two lists of three signed integers +%% \item[zero bits/one bits/duplicate bits] three unsigned integers indicating extended integers +%% \item[residuals] one list of signed integers per channel +%% \end{description} + +%% \clearpage + +%% \subsubsection{Decorrelation Pass Parameters} +%% The number of terms in the \VAR{decorrelation terms} sub block +%% determines how many decorrelation passes will run over the +%% block's residual data. +%% Decorrelation weight and decorrelation samples parameters +%% for those passes will be delivered in subsequent sub blocks. +%% \par +%% For example, given a 1 channel block +%% containing a sub block with 5 decorrelation terms, +%% decorrelation pass parameters may be laid out as follows: +%% \begin{figure}[h] +%% \includegraphics{wavpack/figures/decoding_params.pdf} +%% \end{figure} +%% \par +%% \noindent +%% Note that although the parameters are delivered in decrementing order +%% ($\text{term}_4$ to $\text{term}_0$), +%% the decorrelation passes are applied in incrementing order +%% ($\text{term}_0$ to $\text{term}_4$). + +\clearpage + +\input{wavpack/decode/terms} + +\clearpage + +\input{wavpack/decode/weights} + +\clearpage + +\input{wavpack/decode/samples} + +\clearpage + +\input{wavpack/decode/entropy} + +\clearpage + +\input{wavpack/decode/bitstream} + +\clearpage + +\input{wavpack/decode/decorrelation} + +\clearpage + +\subsection{Undo Joint Stereo} +\label{wavpack:undo_joint_stereo} +\ALGORITHM{mid and side channels of signed sample data, in that order}{left and right channels of signed sample data, in that order} +\SetKwData{MID}{mid} +\SetKwData{SIDE}{side} +\SetKwData{LEFT}{left} +\SetKwData{RIGHT}{right} +\For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ + $\text{\RIGHT}_i \leftarrow \text{\SIDE}_i - \lfloor\text{\MID}_i \div 2\rfloor$\; + $\text{\LEFT}_i \leftarrow \text{\MID}_i + \text{\RIGHT}_i$\; +} +\Return left and right channels\; +\EALGORITHM + +\subsubsection{Joint Stereo Example} +\begin{table}[h] +\begin{tabular}{|r|r|r||>{$}r<{$}|>{$}r<{$}|} +$i$ & $\textsf{mid}_i$ & $\textsf{side}_i$ & \textsf{right}_i & \textsf{left}_i \\ +\hline +0 & -64 & 32 & +32 - \lfloor-64 \div 2\rfloor = 64 & +-64 + 64 = 0 \\ +1 & -46 & 39 & +39 - \lfloor-46 \div 2\rfloor = 62 & +-46 + 62 = 16 \\ +2 & -25 & 43 & +43 - \lfloor-25 \div 2\rfloor = 56 & +-25 + 56 = 31 \\ +3 & -3 & 45 & +45 - \lfloor-3 \div 2\rfloor = 47 & +-3 + 47 = 44 \\ +4 & 20 & 44 & +44 - \lfloor20 \div 2\rfloor = 34 & +20 + 34 = 54 \\ +5 & 41 & 40 & +40 - \lfloor41 \div 2\rfloor = 20 & +41 + 20 = 61 \\ +6 & 60 & 34 & +34 - \lfloor60 \div 2\rfloor = 4 & +60 + 4 = 64 \\ +7 & 75 & 25 & +25 - \lfloor75 \div 2\rfloor = -12 & +75 + -12 = 63 \\ +8 & 85 & 15 & +15 - \lfloor85 \div 2\rfloor = -27 & +85 + -27 = 58 \\ +9 & 90 & 4 & +4 - \lfloor90 \div 2\rfloor = -41 & +90 + -41 = 49 \\ +\hline +\end{tabular} +\end{table} + +\clearpage + +\label{wavpack:verify_crc} +\subsection{Checksum Calculation} +\ALGORITHM{one or two channels of signed audio samples}{an unsigned 32-bit CRC integer} +\SetKwData{MONO}{mono output} +\SetKwData{CRC}{CRC} +\SetKwData{LCRC}{LCRC} +\SetKwData{SCRC}{SCRC} +\SetKwData{CHANNEL}{channel} +$\text{\CRC}_{-1} \leftarrow \texttt{0xFFFFFFFF}$\; +\For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ + \eIf{$\text{\MONO} = 0$}{ + $\text{\LCRC}_i \leftarrow (3 \times \text{\CRC}_{i - 1}) + \text{\CHANNEL}_{0~i}$\tcc*[r]{calculate signed CRC of left channel} + $\text{\SCRC}_i \leftarrow (3 \times \text{\LCRC}_{i - 1}) + \text{\CHANNEL}_{1~i}$\tcc*[r]{calculate signed CRC of right channel} + }{ + $\text{\SCRC}_i \leftarrow (3 \times \text{\CRC}_{i - 1}) + \text{\CHANNEL}_{0~i}$\tcc*[r]{calculate signed CRC of channel} + } + \BlankLine + \eIf(\tcc*[f]{convert signed CRC to unsigned, 32-bit integer}){$\text{\SCRC}_i \geq 0$}{ + $\text{\CRC}_i \leftarrow \text{\SCRC}_i \bmod \texttt{0x100000000}$\; + }{ + $\text{\CRC}_i \leftarrow (2 ^ {32} - (-\text{\SCRC}_i)) \bmod \texttt{0x100000000}$\; + } +} +\Return $\text{\CRC}_{\text{sample count} - 1}$\; +\EALGORITHM + +\subsubsection{Checksum Calculation Example} +{\relsize{-1} +\begin{tabular}{|r|r|r||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|} +$i$ & $\textsf{channel}_{0~i}$ & $\textsf{channel}_{1~i}$ & \textsf{LCRC}_i & \text{SCRC}_i & \textsf{CRC}_i \\ +\hline +0 & 0 & 64 & +(3 \times \texttt{FFFFFFFF}) + 0 = \texttt{2FFFFFFFD} & +(3 \times \texttt{2FFFFFFFD}) + 64 = \texttt{900000037} & +\texttt{00000037} \\ +1 & 16 & 62 & +(3 \times \texttt{00000037}) + 16 = \texttt{000000B5} & +(3 \times \texttt{000000B5}) + 62 = \texttt{0000025D} & +\texttt{0000025D} \\ +2 & 31 & 56 & +(3 \times \texttt{0000025D}) + 31 = \texttt{00000736} & +(3 \times \texttt{00000736}) + 56 = \texttt{000015DA} & +\texttt{000015DA} \\ +3 & 44 & 47 & +(3 \times \texttt{000015DA}) + 44 = \texttt{000041BA} & +(3 \times \texttt{000041BA}) + 47 = \texttt{0000C55D} & +\texttt{0000C55D} \\ +4 & 54 & 34 & +(3 \times \texttt{0000C55D}) + 54 = \texttt{0002504D} & +(3 \times \texttt{0002504D}) + 34 = \texttt{0006F109} & +\texttt{0006F109} \\ +5 & 61 & 20 & +(3 \times \texttt{0006F109}) + 61 = \texttt{0014D358} & +(3 \times \texttt{0014D358}) + 20 = \texttt{003E7A1C} & +\texttt{003E7A1C} \\ +6 & 64 & 4 & +(3 \times \texttt{003E7A1C}) + 64 = \texttt{00BB6E94} & +(3 \times \texttt{00BB6E94}) + 4 = \texttt{02324BC0} & +\texttt{02324BC0} \\ +7 & 63 & -12 & +(3 \times \texttt{02324BC0}) + 63 = \texttt{0696E37F} & +(3 \times \texttt{0696E37F}) - 12 = \texttt{13C4AA71} & +\texttt{13C4AA71} \\ +8 & 58 & -27 & +(3 \times \texttt{13C4AA71}) + 58 = \texttt{3B4DFF8D} & +(3 \times \texttt{3B4DFF8D}) - 27 = \texttt{B1E9FE8C} & +\texttt{B1E9FE8C} \\ +9 & 49 & -41 & +(3 \times \texttt{B1E9FE8C}) + 49 = \texttt{215BDFBD5} & +(3 \times \texttt{215BDFBD5}) - 41 = \texttt{64139F356} & +\texttt{4139F356} \\ +\hline +\end{tabular} +} +\vskip 1em +\par +\noindent +Resulting in a final CRC of \texttt{0x4139F356} + +\clearpage + +\subsection{Decode Extended Integers} +\label{wavpack:decode_extended_integers} +\ALGORITHM{sub block data}{\VAR{zero bits}, \VAR{one bits}, \VAR{duplicate bits} values as unsigned integers} +\SetKwData{SENTS}{sent bits} +\SetKwData{ZEROES}{zero bits} +\SetKwData{ONES}{one bits} +\SetKwData{DUPLICATES}{duplicate bits} +\SENTS $\leftarrow$ \READ 8 unsigned bits\tcc*[r]{unused} +\ZEROES $\leftarrow$ \READ 8 unsigned bits\; +\ONES $\leftarrow$ \READ 8 unsigned bits\; +\DUPLICATES $\leftarrow$ \READ 8 unsigned bits\; +\Return $\left\lbrace\begin{tabular}{l} +\ZEROES \\ +\ONES \\ +\DUPLICATES \\ +\end{tabular}\right.$\; +\EALGORITHM + +\begin{figure}[h] + \includegraphics{wavpack/figures/extended_integers.pdf} +\end{figure} + +\subsection{Undoing Extended Integers} +\label{wavpack:undo_extended_integers} +\ALGORITHM{\VAR{zero bits}, \VAR{one bits}, \VAR{duplicate bits} values; 1 or 2 channels of shifted PCM data}{1 or 2 channels of un-shifted PCM data} +\SetKwData{ZEROES}{zero bits} +\SetKwData{ONES}{one bits} +\SetKwData{DUPLICATES}{duplicate bits} +\SetKwData{SHIFTED}{shifted channel} +\SetKwData{UNSHIFTED}{un-shifted channel} +\For{$c \leftarrow 0$ \emph{\KwTo}channel count}{ + \uIf{$\text{\ZEROES} > 0$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ + $\text{\UNSHIFTED}_{c~i} \leftarrow \text{\SHIFTED}_{c~i} \times 2 ^ {\text{\ZEROES}}$\; + } + } + \uElseIf{$\text{\ONES} > 0$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ + $\text{\UNSHIFTED}_{c~i} \leftarrow \text{\SHIFTED}_{c~i} \times 2 ^ {\text{\ONES}} + (2 ^ {\text{\ONES}} - 1)$\; + } + } + \uElseIf{$\text{\DUPLICATES} > 0$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ + \eIf{$\text{\SHIFTED}_{c~i} \bmod 2 = 0$}{ + $\text{\UNSHIFTED}_{c~i} \leftarrow \text{\SHIFTED}_{c~i} \times 2 ^ {\text{\DUPLICATES}}$\; + }{ + $\text{\UNSHIFTED}_{c~i} \leftarrow \text{\SHIFTED}_{c~i} \times 2 ^ {\text{\DUPLICATES}} + (2 ^ {\text{\DUPLICATES}} - 1)$\; + } + } + } + \Else{ + $\text{\UNSHIFTED}_c \leftarrow \text{\SHIFTED}_c$\; + } +} +\Return \UNSHIFTED data\; +\EALGORITHM + +\clearpage + +\subsection{MD5 Sum} +\label{wavpack:md5_sum} +The MD5 is the hash of all the PCM samples, on a PCM frame basis, +in little-endian format and signed if the bits per sample is greater than 0. + +\begin{figure}[h] +\includegraphics{wavpack/figures/md5sum.pdf} +\end{figure} + +\subsection{RIFF WAVE Header and Footer} + +These sub-blocks are typically found in the first and last +WavPack block, respectively. +The header must always be present in the file while +the footer is optional. + +\begin{figure}[h] +\includegraphics{wavpack/figures/pcm_sandwich.pdf} +\end{figure}
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/decode/bitstream.tex
Added
@@ -0,0 +1,745 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\subsection{Reading Bitstream} +\label{wavpack:decode_bitstream} +\ALGORITHM{\VAR{mono output}, \VAR{false stereo} and \VAR{block samples} from block header, +entropy values\footnote{from the entropy variables sub block, which must be read prior to this sub block}, sub block data}{a list of signed residual values per channel\footnote{$\text{residual}_{c~i}$ indicates the $i$th residual of channel $c$}} +\SetKwData{ENTROPY}{entropy} +\SetKwData{MONO}{mono output} +\SetKwData{FALSESTEREO}{false stereo} +\SetKwData{BLOCKSAMPLES}{block samples} +\SetKwData{CHANNELS}{channel count} +\SetKwData{UNDEFINED}{undefined} +\SetKwData{RESIDUAL}{residual} +\SetKwData{ZEROES}{zeroes} +\SetKw{AND}{and} +\eIf{$\text{\MONO} = 0$ \AND $\text{\FALSESTEREO} = 0$}{ + \CHANNELS $\leftarrow 2$\; +}{ + \CHANNELS $\leftarrow 1$\; +} +$u_{-1} \leftarrow \UNDEFINED$\; +$i \leftarrow 0$\; +\While{$i < (\BLOCKSAMPLES \times \CHANNELS)$}{ + \eIf{$(u_{i - 1} = \UNDEFINED)$ \AND + $(\text{\ENTROPY}_{0~0} < 2)$ \AND + $(\text{\ENTROPY}_{1~0} < 2)$}{ + \tcc{handle long run of 0 residuals} + $\ZEROES \leftarrow$ read modified Elias gamma code\; + \If{$\text{\ZEROES} > 0$}{ + \For{$j \leftarrow 0$ \emph{\KwTo}\ZEROES}{ + $\text{\RESIDUAL}_{(i \bmod \CHANNELS)~\lfloor i \div \CHANNELS \rfloor} \leftarrow $ 0\; + $i \leftarrow i + 1$\; + } + \BlankLine + $\text{\ENTROPY}_{0~0} \leftarrow \text{\ENTROPY}_{0~1} \leftarrow \text{\ENTROPY}_{0~2} \leftarrow 0$\; + $\text{\ENTROPY}_{1~0} \leftarrow \text{\ENTROPY}_{1~1} \leftarrow \text{\ENTROPY}_{1~2} \leftarrow 0$\; + } + \If{$i < (\BLOCKSAMPLES \times \CHANNELS)$}{ + $\text{\RESIDUAL}_{(i \bmod \CHANNELS)~\lfloor i \div \CHANNELS \rfloor} \leftarrow $ read residual value\; + $i \leftarrow i + 1$\; + } + }{ + $\text{\RESIDUAL}_{(i \bmod \CHANNELS)~\lfloor i \div \CHANNELS \rfloor} \leftarrow $ read residual value\; + $i \leftarrow i + 1$\; + } +} +\Return a list of signed \RESIDUAL values per channel\; +\EALGORITHM + +\subsubsection{Reading Modified Elias Gamma Code} +{\relsize{-1} + \ALGORITHM{the sub block bitstream}{an unsigned integer} + $t \leftarrow$ \UNARY with stop bit 0\; + \eIf{$t > 1$}{ + $p \leftarrow$ \READ $(t - 1)$ unsigned bits\; + \Return $2 ^ {(t - 1)} + p$\; + }{ + \Return $t$\; + } + \EALGORITHM +} + +\subsubsection{Reading Residual Value} +{\relsize{-1} +\ALGORITHM{sub block bitstream, previous residual's unary value $u_{i - 1}$, \VAR{entropy} values for channel $c$}{a signed residual value; new unary value $u_i$, updated \VAR{entropy} values for channel $c$} +\EALGORITHM +} + +\clearpage + +{\relsize{-1} +\begin{algorithm}[H] +\DontPrintSemicolon +\SetKw{READ}{read} +\SetKw{UNARY}{read unary} +\SetKwData{UNDEFINED}{undefined} +\SetKwData{ENTROPY}{entropy} +\SetKwData{RESIDUAL}{residual} +\uIf(\tcc*[f]{determine new "u" and "m" values}){$u_{i - 1} = \UNDEFINED$}{ + $u_i \leftarrow$ \UNARY with stop bit 0\; + \If{$u_i = 16$}{ + $c_i \leftarrow$ read modified Elias gamma code\; + $u_i \leftarrow u_i + c_i$\; + } + $m_i \leftarrow \lfloor u_i \div 2\rfloor$\; +} +\uElseIf{$u_{i - 1} \bmod 2 = 0$}{ + $u_i \leftarrow \UNDEFINED$\; + $m_i \leftarrow 0$\; +} +\ElseIf{$u_{i - 1} \bmod 2 = 1$}{ + $u_i \leftarrow$ \UNARY with stop bit 0\; + \If{$u_i = 16$}{ + $c_i \leftarrow$ read modified Elias gamma code\; + $u_i \leftarrow u_i + c_i$\; + } + $m_i \leftarrow \lfloor u_i \div 2\rfloor + 1$\; +} +\BlankLine +\Switch(\tcc*[f]{determine "base", "add" and new entropy values}){$m_i$}{ + \uCase{0}{ + $base \leftarrow 0$\; + $add \leftarrow \lfloor\text{\ENTROPY}_{c~0} \div 2 ^ 4\rfloor$\; + $\text{\ENTROPY}_{c~0} \leftarrow \text{\ENTROPY}_{c~0} - \lfloor(\text{\ENTROPY}_{c~0} + 126) \div 2 ^ 7\rfloor \times 2$\; + } + \uCase{1}{ + $base \leftarrow \lfloor\text{\ENTROPY}_{c~0} \div 2 ^ 4\rfloor + 1$\; + $add \leftarrow \lfloor\text{\ENTROPY}_{c~1} \div 2 ^ 4\rfloor$\; + $\text{\ENTROPY}_{c~0} \leftarrow \text{\ENTROPY}_{c~0} + \lfloor(\text{\ENTROPY}_{c~0} + 128) \div 2 ^ 7\rfloor \times 5$\; + $\text{\ENTROPY}_{c~1} \leftarrow \text{\ENTROPY}_{c~1} - \lfloor(\text{\ENTROPY}_{c~1} + 62) \div 2 ^ 6\rfloor \times 2$\; + } + \uCase{2}{ + $base \leftarrow (\lfloor\text{\ENTROPY}_{c~0} \div 2 ^ 4\rfloor + 1) + (\lfloor\text{\ENTROPY}_{c~1} \div 2 ^ 4\rfloor + 1)$\; + $add \leftarrow \lfloor\text{\ENTROPY}_{c~2} \div 2 ^ 4\rfloor$\; + $\text{\ENTROPY}_{c~0} \leftarrow \text{\ENTROPY}_{c~0} + \lfloor(\text{\ENTROPY}_{c~0} + 128) \div 2 ^ 7\rfloor \times 5$\; + $\text{\ENTROPY}_{c~1} \leftarrow \text{\ENTROPY}_{c~1} + \lfloor(\text{\ENTROPY}_{c~1} + 64) \div 2 ^ 6\rfloor \times 5$\; + $\text{\ENTROPY}_{c~2} \leftarrow \text{\ENTROPY}_{c~2} - \lfloor(\text{\ENTROPY}_{c~2} + 30) \div 2 ^ 5\rfloor \times 2$\; + } + \Other{ + $base \leftarrow (\lfloor\text{\ENTROPY}_{c~0} \div 2 ^ 4\rfloor + 1) + (\lfloor\text{\ENTROPY}_{c~1} \div 2 ^ 4\rfloor + 1) + ((\lfloor\text{\ENTROPY}_{c~2} \div 2 ^ 4\rfloor + 1) \times (m_i - 2))$\; + $add \leftarrow \lfloor\text{\ENTROPY}_{c~2} \div 2 ^ 4\rfloor$\; + $\text{\ENTROPY}_{c~0} \leftarrow \text{\ENTROPY}_{c~0} + \lfloor(\text{\ENTROPY}_{c~0} + 128) \div 2 ^ 7\rfloor \times 5$\; + $\text{\ENTROPY}_{c~1} \leftarrow \text{\ENTROPY}_{c~1} + \lfloor(\text{\ENTROPY}_{c~1} + 64) \div 2 ^ 6\rfloor \times 5$\; + $\text{\ENTROPY}_{c~2} \leftarrow \text{\ENTROPY}_{c~2} + \lfloor(\text{\ENTROPY}_{c~2} + 32) \div 2 ^ 5\rfloor \times 5$\; + } +} +\BlankLine +\eIf(\tcc*[f]{determine final residual value}){$add = 0$}{ + $unsigned \leftarrow base$\; +}{ + $p \leftarrow \lfloor\log_2(add)\rfloor$\; + $e \leftarrow 2 ^ {p + 1} - add - 1$\; + $r_i \leftarrow$ \READ $p$ unsigned bits\; + \eIf{$r \geq e$}{ + $b_i \leftarrow$ \READ 1 unsigned bit\; + $unsigned \leftarrow base + (r_i \times 2) - e + b_i$\; + }{ + $unsigned \leftarrow base + r_i$\; + } +} +\BlankLine +$sign \leftarrow$ \READ 1 unsigned bit\; +\eIf{$sign = 1$}{ + \Return $(-unsigned - 1)$ along with unary value $u_i$ and updated entropy values\; +}{ + \Return $unsigned$ along with unary value $u_i$ and updated entropy values\; +} +\EALGORITHM +} + +\clearpage + +\begin{figure}[h] + \includegraphics{wavpack/figures/residuals.pdf} +\end{figure} + +\subsubsection{Residual Decoding Example} +Given a 2 channel block with $\text{entropies}_0 = \texttt{[118, 194, 322]}$ +and $\text{entropies}_1 = \texttt{[118, 176, 212]}$: + +\begin{figure}[h] +\includegraphics[width=6in,keepaspectratio]{wavpack/figures/residuals_parse.pdf} +\end{figure} +\par +\noindent +Calculations of $\text{entropy}_0$ and $\text{entropy}_1$ +are left as an exercise for the reader. + +\clearpage + +\begin{table}[h] +{\relsize{-3} +\begin{tabular}{|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|} +i & u_i & m_i & +\text{base} & \text{add} \\ +%% \text{entropy}_{c~0} & \text{entropy}_{c~1} & \text{entropy}_{c~2} \\ +\hline +0 & +7 & +\lfloor 7 \div 2 \rfloor = 3 & +2 + \left\lfloor\frac{118}{2 ^ 4}\right\rfloor + \left\lfloor\frac{194}{2 ^ 4}\right\rfloor + \left(\left\lfloor\frac{322}{2 ^ 4}\right\rfloor \times 1\right) = 42 & \lfloor 322 \div 2 ^ 4 \rfloor = 20 \\ +%% 118 + 5 = 123 & 194 + 20 = 214 & 322 + 55 = 377 +1 & +3 & +\lfloor 3 \div 2 \rfloor + 1 = 2 & +2 + \left\lfloor\frac{118}{2 ^ 4}\right\rfloor + \left\lfloor\frac{176}{2 ^ 4}\right\rfloor = 20 & \lfloor 212 \div 2 ^ 4 \rfloor = 13 \\ +%% 118 + 5 = 123 & 176 + 15 = 191 & 212 - 35 = 198 +\hline +2 & +3 & +\lfloor 3 \div 2 \rfloor + 1 = 2 & +2 + \left\lfloor\frac{123}{2 ^ 4}\right\rfloor + \left\lfloor\frac{214}{2 ^ 4}\right\rfloor = 22 & \lfloor 377 \div 2 ^ 4 \rfloor = 23 \\ +%% 123 + 5 = 128 & 214 + 20 = 234 & 377 - 60 = 353 +3 & +3 & +\lfloor 3 \div 2 \rfloor + 1 = 2 & +2 + \left\lfloor\frac{123}{2 ^ 4}\right\rfloor + \left\lfloor\frac{191}{2 ^ 4}\right\rfloor = 20 & \lfloor 198 \div 2 ^ 4 \rfloor = 12 \\ +%% 123 + 5 = 128 & 191 + 15 = 206 & 198 - 35 = 184 +\hline +4 & +1 & +\lfloor 1 \div 2 \rfloor + 1 = 1 & +1 + \left\lfloor\frac{128}{2 ^ 4}\right\rfloor = 9 & \lfloor 234 \div 2 ^ 4 \rfloor = 14 \\ +%% 128 + 10 = 138 & 234 - 8 = 226 & 353 +5 & +4 & +\lfloor 4 \div 2 \rfloor + 1 = 3 & +2 + \left\lfloor\frac{128}{2 ^ 4}\right\rfloor + \left\lfloor\frac{206}{2 ^ 4}\right\rfloor + \left(\left\lfloor\frac{184}{2 ^ 4}\right\rfloor \times 1\right) = 34 & \lfloor 184 \div 2 ^ 4 \rfloor = 11 \\ +%% 128 + 10 = 138 & 206 + 20 = 226 & 184 + 30 = 214 +\hline +6 & +\textit{undefined} & +0 & +0 & \lfloor 138 \div 2 ^ 4 \rfloor = 8 \\ +%% 138 - 4 = 134 & 226 & 353 +7 & +5 & +\lfloor 5 \div 2 \rfloor = 2 & +2 + \left\lfloor\frac{138}{2 ^ 4}\right\rfloor + \left\lfloor\frac{226}{2 ^ 4}\right\rfloor = 24 & \lfloor 214 \div 2 ^ 4 \rfloor = 13 \\ +%% 138 + 10 = 148 & 226 + 20 = 246 & 214 - 35 = 200 +\hline +8 & +1 & +\lfloor 1 \div 2 \rfloor + 1 = 1 & +1 + \left\lfloor\frac{134}{2 ^ 4}\right\rfloor = 9 & \lfloor 226 \div 2 ^ 4 \rfloor = 14 \\ +%% 134 + 10 = 144 & 226 - 8 = 218 & 353 +9 & +3 & +\lfloor 3 \div 2 \rfloor + 1 = 2 & +2 + \left\lfloor\frac{148}{2 ^ 4}\right\rfloor + \left\lfloor\frac{246}{2 ^ 4}\right\rfloor = 26 & \lfloor 200 \div 2 ^ 4 \rfloor = 12 \\ +%% 148 + 10 = 158 & 246 + 20 = 266 & 200 - 35 = 186 +\hline +10 & +3 & +\lfloor 3 \div 2 \rfloor + 1 = 2 & +2 + \left\lfloor\frac{144}{2 ^ 4}\right\rfloor + \left\lfloor\frac{218}{2 ^ 4}\right\rfloor = 24 & \lfloor 353 \div 2 ^ 4 \rfloor = 22 \\ +%% 144 + 10 = 154 & 218 + 20 = 238 & 353 - 55 = 331 +11 & +3 & +\lfloor 3 \div 2 \rfloor + 1 = 2 & +2 + \left\lfloor\frac{158}{2 ^ 4}\right\rfloor + \left\lfloor\frac{266}{2 ^ 4}\right\rfloor = 27 & \lfloor 186 \div 2 ^ 4 \rfloor = 11 \\ +%% 158 + 10 = 168 & 266 + 25 = 291 & 186 - 30 = 174 +\hline +12 & +5 & +\lfloor 5 \div 2 \rfloor + 1 = 3 & +2 + \left\lfloor\frac{154}{2 ^ 4}\right\rfloor + \left\lfloor\frac{238}{2 ^ 4}\right\rfloor + \left(\left\lfloor\frac{331}{2 ^ 4}\right\rfloor \times 1\right) = 46 & \lfloor 331 \div 2 ^ 4 \rfloor = 20 \\ +%% 154 + 10 = 164 & 238 + 20 = 258 & 331 + 55 = 386 +13 & +1 & +\lfloor 1 \div 2 \rfloor + 1 = 1 & +1 + \left\lfloor\frac{168}{2 ^ 4}\right\rfloor = 11 & \lfloor 291 \div 2 ^ 4 \rfloor = 18 \\ +%% 168 + 10 = 178 & 291 - 10 = 281 & 174 +\hline +14 & +5 & +\lfloor 5 \div 2 \rfloor + 1 = 3 & +2 + \left\lfloor\frac{164}{2 ^ 4}\right\rfloor + \left\lfloor\frac{258}{2 ^ 4}\right\rfloor + \left(\left\lfloor\frac{386}{2 ^ 4}\right\rfloor \times 1\right) = 53 & \lfloor 386 \div 2 ^ 4 \rfloor = 24 \\ +%% 164 + 10 = 174 & 258 + 25 = 283 & 386 + 65 = 451 +15 & +1 & +\lfloor 1 \div 2 \rfloor + 1 = 1 & +1 + \left\lfloor\frac{178}{2 ^ 4}\right\rfloor = 12 & \lfloor 281 \div 2 ^ 4 \rfloor = 17 \\ +%% 178 + 10 = 188 & 281 - 10 = 271 & 174 +\hline +16 & +4 & +\lfloor 4 \div 2 \rfloor + 1 = 3 & +2 + \left\lfloor\frac{174}{2 ^ 4}\right\rfloor + \left\lfloor\frac{283}{2 ^ 4}\right\rfloor + \left(\left\lfloor\frac{451}{2 ^ 4}\right\rfloor \times 1\right) = 58 & \lfloor 451 \div 2 ^ 4 \rfloor = 28 \\ +%% 174 + 10 = 184 & 283 + 25 = 308 & 451 + 75 = 526 +17 & +\textit{undefined} & +0 & +0 & \lfloor 188 \div 2 ^ 4 \rfloor = 11 \\ +%% 188 - 4 = 184 & 271 & 174 +\hline +18 & +6 & +\lfloor 6 \div 2 \rfloor = 3 & +2 + \left\lfloor\frac{184}{2 ^ 4}\right\rfloor + \left\lfloor\frac{308}{2 ^ 4}\right\rfloor + \left(\left\lfloor\frac{526}{2 ^ 4}\right\rfloor \times 1\right) = 65 & \lfloor 526 \div 2 ^ 4 \rfloor = 32 \\ +%% 184 + 10 = 194 & 308 + 25 = 333 & 526 + 85 = 611 +19 & +\textit{undefined} & +0 & +0 & \lfloor 184 \div 2 ^ 4 \rfloor = 11 \\ +%% 184 - 4 = 180 & 271 & 174 +\hline +\end{tabular} +} +\vskip .25in +{\relsize{-3} +\renewcommand{\arraystretch}{1.5} +\begin{tabular}{|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|} +i & \text{base} & \text{add} & p & e & r_i & b_i & unsigned & sign_i & residual_i \\ +\hline +0 & +42 & 20 & +\lfloor\log_2(20)\rfloor = 4 & +2 ^ {4 + 1} - 20 - 1 = 11 & +14 & +1 & 42 + (14 \times 2) - 11 + 1 = 60 & +1 & -60 - 1 = -61 +\\ +1 & +20 & 13 & +\lfloor\log_2(13)\rfloor = 3 & +2 ^ {3 + 1} - 13 - 1 = 2 & +6 & +1 & 20 + (6 \times 2) - 2 + 1 = 31 & +0 & 31 +\\ +\hline +2 & +22 & 23 & +\lfloor\log_2(23)\rfloor = 4 & +2 ^ {4 + 1} - 23 - 1 = 8 & +9 & +0 & 22 + (9 \times 2) - 8 + 0 = 32 & +1 & -32 - 1 = -33 +\\ +3 & +20 & 12 & +\lfloor\log_2(12)\rfloor = 3 & +2 ^ {3 + 1} - 12 - 1 = 3 & +7 & +1 & 20 + (7 \times 2) - 3 + 1 = 32 & +0 & 32 +\\ +\hline +4 & +9 & 14 & +\lfloor\log_2(14)\rfloor = 3 & +2 ^ {3 + 1} - 14 - 1 = 1 & +4 & +1 & 9 + (4 \times 2) - 1 + 1 = 17 & +1 & -17 - 1 = -18 +\\ +5 & +34 & 11 & +\lfloor\log_2(11)\rfloor = 3 & +2 ^ {3 + 1} - 11 - 1 = 4 & +2 & + & 34 + 2 = 36 & +0 & 36 +\\ +\hline +6 & +0 & 8 & +\lfloor\log_2(8)\rfloor = 3 & +2 ^ {3 + 1} - 8 - 1 = 7 & +1 & + & 0 + 1 = 1 & +0 & 1 +\\ +7 & +24 & 13 & +\lfloor\log_2(13)\rfloor = 3 & +2 ^ {3 + 1} - 13 - 1 = 2 & +7 & +1 & 24 + (7 \times 2) - 2 + 1 = 37 & +0 & 37 +\\ +\hline +8 & +9 & 14 & +\lfloor\log_2(14)\rfloor = 3 & +2 ^ {3 + 1} - 14 - 1 = 1 & +6 & +0 & 9 + (6 \times 2) - 1 + 0 = 20 & +0 & 20 +\\ +9 & +26 & 12 & +\lfloor\log_2(12)\rfloor = 3 & +2 ^ {3 + 1} - 12 - 1 = 3 & +6 & +0 & 26 + (6 \times 2) - 3 + 0 = 35 & +0 & 35 +\\ +\hline +10 & +24 & 22 & +\lfloor\log_2(22)\rfloor = 4 & +2 ^ {4 + 1} - 22 - 1 = 9 & +10 & +0 & 24 + (10 \times 2) - 9 + 0 = 35 & +0 & 35 +\\ +11 & +27 & 11 & +\lfloor\log_2(11)\rfloor = 3 & +2 ^ {3 + 1} - 11 - 1 = 4 & +4 & +0 & 27 + (4 \times 2) - 4 + 0 = 31 & +0 & 31 +\\ +\hline +12 & +46 & 20 & +\lfloor\log_2(20)\rfloor = 4 & +2 ^ {4 + 1} - 20 - 1 = 11 & +4 & + & 46 + 4 = 50 & +0 & 50 +\\ +13 & +11 & 18 & +\lfloor\log_2(18)\rfloor = 4 & +2 ^ {4 + 1} - 18 - 1 = 13 & +13 & +1 & 11 + (13 \times 2) - 13 + 1 = 25 & +0 & 25 +\\ +\hline +14 & +53 & 24 & +\lfloor\log_2(24)\rfloor = 4 & +2 ^ {4 + 1} - 24 - 1 = 7 & +8 & +0 & 53 + (8 \times 2) - 7 + 0 = 62 & +0 & 62 +\\ +15 & +12 & 17 & +\lfloor\log_2(17)\rfloor = 4 & +2 ^ {4 + 1} - 17 - 1 = 14 & +6 & + & 12 + 6 = 18 & +0 & 18 +\\ +\hline +16 & +58 & 28 & +\lfloor\log_2(28)\rfloor = 4 & +2 ^ {4 + 1} - 28 - 1 = 3 & +6 & +1 & 58 + (6 \times 2) - 3 + 1 = 68 & +0 & 68 +\\ +17 & +0 & 11 & +\lfloor\log_2(11)\rfloor = 3 & +2 ^ {3 + 1} - 11 - 1 = 4 & +7 & +0 & 0 + (7 \times 2) - 4 + 0 = 10 & +0 & 10 +\\ +\hline +18 & +65 & 32 & +\lfloor\log_2(32)\rfloor = 5 & +2 ^ {5 + 1} - 32 - 1 = 31 & +6 & + & 65 + 6 = 71 & +0 & 71 +\\ +19 & +0 & 11 & +\lfloor\log_2(11)\rfloor = 3 & +2 ^ {3 + 1} - 11 - 1 = 4 & +0 & + & 0 + 0 = 0 & +0 & 0 +\\ +\hline +\end{tabular} +} +{\relsize{-1} + \vskip .2in + Resulting in: + \newline + \begin{tabular}{rr} + channel 0 residuals : & \texttt{[-61,~-33,~-18,~~1,~20,~35,~50,~62,~68,~71]}\\ + channel 1 residuals : & \texttt{[~31,~~32,~~36,~37,~35,~31,~25,~18,~10,~~0]}\\ + \end{tabular} +} +\end{table} + +\clearpage + +\subsubsection{2nd Residual Decoding Example} +Given a 1 channel block with $\text{entropies}_0 = \texttt{[0, 0, 0]}$: + +\begin{figure}[h] +\includegraphics{wavpack/figures/residuals_parse2.pdf} +\end{figure} +\par +\noindent +This residual block demonstrates how a long run of 0 residuals is read +when $\text{entropy}_{0~0}$ falls to a low enough value. +Calculation of $\text{entropies}_{0~1}$ and $\text{entropies}_{0~2}$ +are left as an exercise for the reader. + +\clearpage + +\begin{table}[h] +{\relsize{-3} +\renewcommand{\arraystretch}{1.5} +\begin{tabular}{|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|} +i & u_i & m_i & +\text{base} & \text{add} & \text{entropy}_{0~0} \\ +\hline +& \multicolumn{5}{c|}{read modified Elias gamma code: $t \leftarrow 0~,~\text{zeroes} \leftarrow 0$} \\ +0 & +3 & \lfloor 3 \div 2\rfloor = 1 & +1 + \left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 1 & +\left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 0 & +0 + 5 = 5 %% & 0 - 0 = 0 & 0 +\\ +1 & +3 & \lfloor 3 \div 2\rfloor + 1 = 2 & +2 + \left\lfloor\frac{5}{2 ^ 4}\right\rfloor + \left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 2 & +\left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 0 & +5 + 5 = 10 %% & 0 + 5 = 5 & 0 - 0 = 0 +\\ +2 & +5 & \lfloor 5 \div 2\rfloor + 1 = 3 & +2 + \left\lfloor\frac{10}{2 ^ 4}\right\rfloor + \left\lfloor\frac{5}{2 ^ 4}\right\rfloor + \left(\left\lfloor\frac{0}{2 ^ 4}\right\rfloor \times 1\right) = 3 & +\left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 0 & +10 + 5 = 15 %% & 5 + 5 = 10 & 0 + 5 = 5 +\\ +3 & +2 & \lfloor 2 \div 2\rfloor + 1 = 2 & +2 + \left\lfloor\frac{15}{2 ^ 4}\right\rfloor + \left\lfloor\frac{10}{2 ^ 4}\right\rfloor = 2 & +\left\lfloor\frac{5}{2 ^ 4}\right\rfloor = 0 & +15 + 5 = 20 %% & 10 + 5 = 15 & 5 - 2 = 3 +\\ +4 & +\textit{undefined} & 0 & +0 & \left\lfloor\frac{20}{2 ^ 4}\right\rfloor = 1 & +20 - 2 = 18 %% & 15 & 3 +\\ +5 & +0 & \lfloor 0 \div 2\rfloor = 0 & +0 & \left\lfloor\frac{18}{2 ^ 4}\right\rfloor = 1 & +18 - 2 = 16 %% & 15 & 3 +\\ +6 & +\textit{undefined} & 0 & +0 & \left\lfloor\frac{16}{2 ^ 4}\right\rfloor = 1 & +16 - 2 = 14 %% & 15 & 3 +\\ +7 & +0 & \lfloor 0 \div 2\rfloor = 0 & +0 & \left\lfloor\frac{14}{2 ^ 4}\right\rfloor = 0 & +14 - 2 = 12 %% & 15 & 3 +\\ +8 & +\textit{undefined} & 0 & +0 & \left\lfloor\frac{12}{2 ^ 4}\right\rfloor = 0 & +12 - 2 = 10 %% & 15 & 3 +\\ +9 & +0 & \lfloor 0 \div 2\rfloor = 0 & +0 & \left\lfloor\frac{10}{2 ^ 4}\right\rfloor = 0 & +10 - 2 = 8 %% & 15 & 3 +\\ +10 & +\textit{undefined} & 0 & +0 & \left\lfloor\frac{8}{2 ^ 4}\right\rfloor = 0 & +8 - 2 = 6 %% & 15 & 3 +\\ +11 & +0 & \lfloor 0 \div 2\rfloor = 0 & +0 & \left\lfloor\frac{6}{2 ^ 4}\right\rfloor = 0 & +6 - 2 = 4 %% & 15 & 3 +\\ +12 & +\textit{undefined} & 0 & +0 & \left\lfloor\frac{4}{2 ^ 4}\right\rfloor = 0 & +4 - 2 = 2 %% & 15 & 3 +\\ +13 & +0 & \lfloor 0 \div 2\rfloor = 0 & +0 & \left\lfloor\frac{2}{2 ^ 4}\right\rfloor = 0 & +2 - 2 = 0 %% & 15 & 3 +\\ +14 & +\textit{\color{red}undefined} & 0 & +0 & \left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 0 & +0 - 0 = {\color{red}0} %% & 15 & 3 +\\ +15-24 & \multicolumn{5}{c|}{read modified Elias gamma code: $t \leftarrow 4~,~p \leftarrow 2~,~\text{zeroes} \leftarrow 2 ^ {(4 - 1)} + 2 = 10$} \\ +25 & +1 & \lfloor 1 \div 2\rfloor = 0 & +0 & \left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 0 & +0 - 0 = 0 %% & 0 & 0 +\\ +26 & +1 & \lfloor 1 \div 2\rfloor + 1 = 1 & +1 + \left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 1 & +\left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 0 & +0 + 5 = 5 %% & 0 - 0 = 0 & 0 +\\ +27 & +3 & \lfloor 3 \div 2\rfloor + 1 = 2 & +2 + \left\lfloor\frac{5}{2 ^ 4}\right\rfloor + \left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 2 & +\left\lfloor\frac{0}{2 ^ 4}\right\rfloor = 0 & +5 + 5 = 10 %% & 0 + 5 = 5 & 0 - 0 = 0 +\\ +28 & +0 & \lfloor 0 \div 2\rfloor + 1 = 1 & +1 + \left\lfloor\frac{10}{2 ^ 4}\right\rfloor = 1 & +\left\lfloor\frac{5}{2 ^ 4}\right\rfloor = 0 & +10 + 5 = 15 %% & 5 - 2 = 3 & 0 +\\ +29 & +\textit{undefined} & 0 & +0 & \left\lfloor\frac{15}{2 ^ 4}\right\rfloor = 0 & +15 - 2 = 13 %% & 3 & 0 +\\ +\hline +\end{tabular} +} +\vskip .25in +{\relsize{-3} +\begin{tabular}{|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|} +i & \text{base} & \text{add} & p & e & r_i & b_i & unsigned & sign_i & residual_i \\ +\hline +0 & +1 & 0 & +& & & & 1 & +0 & 1 +\\ +1 & +2 & 0 & +& & & & 2 & +0 & 2 +\\ +2 & +3 & 0 & +& & & & 3 & +0 & 3 +\\ +3 & +2 & 0 & +& & & & 2 & +0 & 2 +\\ +4 & +0 & 1 & +\lfloor\log_2(1)\rfloor = 0 & +2 ^ {0 + 1} - 1 - 1 = 0 & +0 & +1 & 0 + (0 \times 2) - 0 + 1 = 1 & +0 & 1 +\\ +5 & +0 & 1 & +\lfloor\log_2(1)\rfloor = 0 & +2 ^ {0 + 1} - 1 - 1 = 0 & +0 & +0 & 0 + (0 \times 2) - 0 + 0 = 0 & +0 & 0 +\\ +6 & +0 & 1 & +\lfloor\log_2(1)\rfloor = 0 & +2 ^ {0 + 1} - 1 - 1 = 0 & +0 & +0 & 0 + (0 \times 2) - 0 + 0 = 0 & +0 & 0 +\\ +7 & +0 & 0 & +& & & & 0 & +0 & 0 +\\ +8 & +0 & 0 & +& & & & 0 & +0 & 0 +\\ +9 & +0 & 0 & +& & & & 0 & +0 & 0 +\\ +10 & +0 & 0 & +& & & & 0 & +0 & 0 +\\ +11 & +0 & 0 & +& & & & 0 & +0 & 0 +\\ +12 & +0 & 0 & +& & & & 0 & +0 & 0 +\\ +13 & +0 & 0 & +& & & & 0 & +0 & 0 +\\ +14 & +0 & 0 & +& & & & 0 & +0 & 0 +\\ +15-24 & \multicolumn{8}{c|}{long run of 10, 0 residual values} & 0 \\ +25 & +0 & 0 & +& & & & 0 & +1 & -0 - 1 = -1 +\\ +26 & +1 & 0 & +& & & & 1 & +1 & -1 - 1 = -2 +\\ +27 & +2 & 0 & +& & & & 2 & +1 & -2 - 1 = -3 +\\ +28 & +1 & 0 & +& & & & 1 & +1 & -1 - 1 = -2 +\\ +29 & +0 & 0 & +& & & & 0 & +1 & -0 - 1 = -1 +\\ +\hline +\end{tabular} +} +{\relsize{-3} + \vskip .2in + Resulting in channel 0 residuals: + \newline + \texttt{[~~1,~~2,~~3,~~2,~~1,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~~0,~-1,~-2,~-3,~-2,~-1]} +} +\end{table}
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/decode/decorrelation.tex
Added
@@ -0,0 +1,611 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\subsection{Channel Decorrelation} +\label{wavpack:decorrelate_channels} +\ALGORITHM{a list of signed residuals per channel, a list of decorrelation terms and deltas, a decorrelation weight per term and channel, a list of decorrelation samples per term and channel}{a list of signed samples per channel} +\SetKwData{TERMCOUNT}{term count} +\SetKwData{PASS}{pass} +\SetKwData{TERM}{term} +\SetKwData{DELTA}{delta} +\SetKwData{WEIGHT}{weight} +\SetKwData{SAMPLES}{sample} +\SetKwData{RESIDUALS}{residual} +\eIf{$\text{channel count} = 1$}{ + $\text{\PASS}_{(-1)~0} \leftarrow \text{\RESIDUALS}_{0}$\; + \For{$p \leftarrow 0$ \emph{\KwTo}\TERMCOUNT}{ + $\text{\PASS}_{p} \leftarrow$ \hyperref[wavpack:decorr_pass_1ch]{decorrelate 1 channel $\text{\PASS}_{(p - 1)}$} using $\left\lbrace\begin{tabular}{l} + $\text{\TERM}_{p}$ \\ + $\text{\DELTA}_{p}$ \\ + $\text{\WEIGHT}_{p~0}$ \\ + $\text{\SAMPLES}_{p~0}$ \\ + \end{tabular}\right.$\; + } + \Return $\text{\PASS}_{(\text{\TERMCOUNT} - 1)~0}$\; +}{ + $\text{\PASS}_{-1~0} \leftarrow \text{\RESIDUALS}_{0}$\; + $\text{\PASS}_{-1~1} \leftarrow \text{\RESIDUALS}_{1}$\; + \For{$p \leftarrow 0$ \emph{\KwTo}\TERMCOUNT}{ + $\text{\PASS}_{p} \leftarrow$ \hyperref[wavpack:decorr_pass_2ch]{decorrelate 2 channel $\text{\PASS}_{(p - 1)}$} using $\left\lbrace\begin{tabular}{l} + $\text{\TERM}_{p}$ \\ + $\text{\DELTA}_{p}$ \\ + $\text{\WEIGHT}_{p~0}$ \\ + $\text{\WEIGHT}_{p~1}$ \\ + $\text{\SAMPLES}_{p~0}$ \\ + $\text{\SAMPLES}_{p~1}$ \\ + \end{tabular}\right.$\; + } + \Return $\text{\PASS}_{(\text{\TERMCOUNT} - 1)~0}$ and $\text{\PASS}_{(\text{\TERMCOUNT} - 1)~1}$\; +} +\EALGORITHM + +\clearpage + +\subsubsection{Visualizing Decorrelation Passes} + +This is an example of a relatively small set of residuals +being transformed back into a set of samples +over the course of 5 decorrelation passes. +What's important to note is that each pass +adjusts the residual values' overall range. +The number of these passes and their decorrelation terms +are what separate high levels of WavPack compression from low levels. + +\begin{figure}[h] + \subfloat{ + \includegraphics{wavpack/figures/decorrelation0.pdf} + } + \subfloat{ + \includegraphics{wavpack/figures/decorrelation1.pdf} + } + \newline + \subfloat{ + \includegraphics{wavpack/figures/decorrelation2.pdf} + } + \subfloat{ + \includegraphics{wavpack/figures/decorrelation3.pdf} + } + \newline + \subfloat{ + \includegraphics{wavpack/figures/decorrelation4.pdf} + } + \subfloat{ + \includegraphics{wavpack/figures/decorrelation5.pdf} + } +\end{figure} + + +\clearpage + +\subsubsection{1 Channel Decorrelation Pass} +\label{wavpack:decorr_pass_1ch} +{\relsize{-1} +\ALGORITHM{a list of signed correlated samples, decorrelation term and delta, decorrelation weight, list of decorrelation samples}{a list of signed decorrelated samples} +\SetKwData{CORRELATED}{correlated} +\SetKwData{DECORRELATED}{decorrelated} +\SetKwData{DECORRSAMPLE}{decorrelation sample} +\SetKwData{WEIGHT}{weight} +\SetKwData{TEMP}{temp} +\SetKw{OR}{or} +\SetKw{XOR}{xor} +\SetKwFunction{APPLYWEIGHT}{apply\_weight} +\SetKwFunction{UPDATEWEIGHT}{update\_weight} +$\text{\WEIGHT}_0 \leftarrow$ decorrelation weight\; +\BlankLine +\uIf{$term = 18$}{ + $\text{\DECORRELATED}_0 \leftarrow \text{\DECORRSAMPLE}_1$\; + $\text{\DECORRELATED}_1 \leftarrow \text{\DECORRSAMPLE}_0$\; + \For{$i \leftarrow 0$ \emph{\KwTo}correlated samples length}{ + $\text{\TEMP}_{i} \leftarrow \lfloor(3 \times \text{\DECORRELATED}_{i + 1} - \text{\DECORRELATED}_{i}) \div 2 \rfloor$\; + $\text{\DECORRELATED}_{i + 2} \leftarrow \APPLYWEIGHT(\text{\WEIGHT}_i~,~\text{\TEMP}_{i}) + \text{\CORRELATED}_i$\; + $\text{\WEIGHT}_{i + 1} \leftarrow \text{\WEIGHT}_i + \UPDATEWEIGHT(\text{\TEMP}_{i}~,~\text{\CORRELATED}_i~,~delta)$\; + } + \Return \DECORRELATED samples starting from 2\; +} +\uElseIf{$term = 17$}{ + $\text{\DECORRELATED}_0 \leftarrow \text{\DECORRSAMPLE}_1$\; + $\text{\DECORRELATED}_1 \leftarrow \text{\DECORRSAMPLE}_0$\; + \For{$i \leftarrow 0$ \emph{\KwTo}correlated samples length}{ + $\text{\TEMP}_{i} \leftarrow 2 \times \text{\DECORRELATED}_{i + 1} - \text{\DECORRELATED}_{i}$\; + $\text{\DECORRELATED}_{i + 2} \leftarrow \APPLYWEIGHT(\text{\WEIGHT}_i~,~\text{\TEMP}_{i}) + \text{\CORRELATED}_i$\; + $\text{\WEIGHT}_{i + 1} \leftarrow \text{\WEIGHT}_i + \UPDATEWEIGHT(\text{\TEMP}_{i}~,~\text{\CORRELATED}_i~,~delta)$\; + } + \Return \DECORRELATED samples starting from 2\; +} +\uElseIf{$1 \leq term \leq 8$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}term}{ + $\text{\DECORRELATED}_i \leftarrow \text{\DECORRSAMPLE}_i$\; + } + \For{$i \leftarrow 0$ \emph{\KwTo}correlated samples length}{ + $\text{\DECORRELATED}_{i + term} \leftarrow \APPLYWEIGHT(\text{\WEIGHT}_i~,~\text{\DECORRELATED}_{i}) + \text{\CORRELATED}_i$\; + $\text{\WEIGHT}_{i + 1} \leftarrow \text{\WEIGHT}_i + \UPDATEWEIGHT(\text{\DECORRELATED}_{i}~,~\text{\CORRELATED}_i~,~delta)$\; + } + \BlankLine + \Return \DECORRELATED samples starting from $term$\; +} +\Else{ + invalid decorrelation term\; +} +\EALGORITHM +\par +\noindent +\begin{align*} +\intertext{where \texttt{apply\_weight} is defined as:} +\texttt{apply\_weight}(weight~,~sample) &= \left\lfloor\frac{weight \times sample + 2 ^ 9}{2 ^ {10}}\right\rfloor \\ +\intertext{and \texttt{update\_weight} is defined as:} +\texttt{update\_weight}(source~,~result~,~delta) &= +\begin{cases} +0 & \text{ if } source = 0 \text{ or } result = 0 \\ +delta & \text{ if } (source \textbf{ xor } result ) \geq 0 \\ +-delta & \text{ if } (source \textbf{ xor } result) < 0 +\end{cases} +\end{align*} + +} + +\clearpage + +\subsubsection{2 Channel Decorrelation Pass} +\label{wavpack:decorr_pass_2ch} +{\relsize{-1} +\ALGORITHM{2 lists of signed correlated samples, decorrelation term and delta, 2 decorrelation weights, 2 lists of decorrelation samples}{2 lists of signed decorrelated samples} +\SetKwData{CORRELATED}{correlated} +\SetKwData{DECORRELATED}{decorrelated} +\SetKwData{DECORRSAMPLE}{decorrelation sample} +\SetKwData{WEIGHT}{weight} +\SetKw{OR}{or} +\SetKw{XOR}{xor} +\SetKwFunction{MIN}{min} +\SetKwFunction{MAX}{max} +\SetKwFunction{APPLYWEIGHT}{apply\_weight} +\SetKwFunction{UPDATEWEIGHT}{update\_weight} +\uIf{$17 \leq term \leq 18$ \OR $1 \leq term \leq 8$}{ + $\text{\DECORRELATED}_0 \leftarrow$ 1 channel decorrelation pass of $\text{\CORRELATED}_0$\; + $\text{\DECORRELATED}_1 \leftarrow$ 1 channel decorrelation pass of $\text{\CORRELATED}_1$\; + \Return $\text{\DECORRELATED}_0$ and $\text{\DECORRELATED}_1$\; +} +\uElseIf{$-3 \leq term \leq -1$}{ + $\text{\WEIGHT}_{0~0} \leftarrow$ decorrelation weight 0\; + $\text{\WEIGHT}_{1~0} \leftarrow$ decorrelation weight 1\; + $\text{\DECORRELATED}_{0~0} \leftarrow \text{\DECORRSAMPLE}_{1~0}$\; + $\text{\DECORRELATED}_{1~0} \leftarrow \text{\DECORRSAMPLE}_{0~0}$\; + \uIf{$term = -1$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}correlated samples length}{ + $\text{\DECORRELATED}_{0~(i + 1)} \leftarrow \APPLYWEIGHT(\text{\WEIGHT}_{0~i}~,~\text{\DECORRELATED}_{1~i}) + \text{\CORRELATED}_{0~i}$\; + $\text{\DECORRELATED}_{1~(i + 1)} \leftarrow \APPLYWEIGHT(\text{\WEIGHT}_{1~i}~,~\text{\DECORRELATED}_{0~(i + 1)}) + \text{\CORRELATED}_{1~i}$\; + $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \text{\WEIGHT}_{0~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{1~i}~,~\text{\CORRELATED}_{0~i}~,~delta)$\; + $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \text{\WEIGHT}_{1~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{0~(i + 1)}~,~\text{\CORRELATED}_{1~i}~,~delta)$\; + $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{0~(i + 1)}~,~1024)~,~-1024)$\; + $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{1~(i + 1)}~,~1024)~,~-1024)$\; + } + } + \uElseIf{$term = -2$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}correlated samples length}{ + $\text{\DECORRELATED}_{1~(i + 1)} \leftarrow \APPLYWEIGHT(\text{\WEIGHT}_{1~i}~,~\text{\DECORRELATED}_{0~i}) + \text{\CORRELATED}_{1~i}$\; + $\text{\DECORRELATED}_{0~(i + 1)} \leftarrow \APPLYWEIGHT(\text{\WEIGHT}_{0~i}~,~\text{\DECORRELATED}_{1~(i + 1)}) + \text{\CORRELATED}_{0~i}$\; + $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \text{\WEIGHT}_{1~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{0~i}~,~\text{\CORRELATED}_{1~i}~,~delta)$\; + $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \text{\WEIGHT}_{0~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{1~(i + 1)},~\text{\CORRELATED}_{0~i}~,~delta)$\; + $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{1~(i + 1)}~,~1024)~,~-1024)$\; + $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{0~(i + 1)}~,~1024)~,~-1024)$\; + } + } + \ElseIf{$term = -3$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}correlated samples length}{ + $\text{\DECORRELATED}_{0~(i + 1)} \leftarrow \APPLYWEIGHT(\text{\WEIGHT}_{0~i}~,~\text{\DECORRELATED}_{1~i}) + \text{\CORRELATED}_{0~i}$\; + $\text{\DECORRELATED}_{1~(i + 1)} \leftarrow \APPLYWEIGHT(\text{\WEIGHT}_{1~i}~,~\text{\DECORRELATED}_{0~i}) + \text{\CORRELATED}_{1~i}$\; + $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \text{\WEIGHT}_{0~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{1~i}~,~\text{\CORRELATED}_{0~i}~,~delta)$\; + $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \text{\WEIGHT}_{1~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{0~i}~,~\text{\CORRELATED}_{1~i}~,~delta)$\; + $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{0~(i + 1)}~,~1024)~,~-1024)$\; + $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{1~(i + 1)}~,~1024)~,~-1024)$\; + } + } + \Return $\text{\DECORRELATED}_0$ starting from 1 and $\text{\DECORRELATED}_1$ starting from 1\; +} +\Else{ + invalid decorrelation term\; +} +\EALGORITHM +} + +\clearpage + +\subsubsection{Channel Decorrelation Example} +Given the values from the \VAR{Decorrelation Terms}, +\VAR{Decorrelation Weights} and \VAR{Decorrelation Samples} +sub blocks: +\begin{figure}[h] +{\relsize{-1} + \subfloat{ + \begin{tabular}{|r|r|r|} + \multicolumn{3}{c}{Decorrelation Terms} \\ + \hline + $p$ & $\textsf{term}_p$ & $\textsf{delta}_p$ \\ + \hline + 4 & 18 & 2 \\ + 3 & 18 & 2 \\ + 2 & 2 & 2 \\ + 1 & 17 & 2 \\ + 0 & 3 & 2 \\ + \hline + \end{tabular} + } + \subfloat{ + \begin{tabular}{|r|r|r|} + \multicolumn{3}{c}{Decorrelation Weights} \\ + \hline + $p$ & $\textsf{weight}_{p~0}$ & $\textsf{weight}_{p~1}$ \\ + \hline + 4 & 48 & 48 \\ + 3 & 48 & 48 \\ + 2 & 32 & 32 \\ + 1 & 48 & 48 \\ + 0 & 16 & 24 \\ + \hline + \end{tabular} + } + \subfloat{ + \begin{tabular}{|r|r|r|} + \multicolumn{3}{c}{Decorrelation Samples} \\ + \hline + $p$ & $\textsf{sample}_{p~0~s}$ & $\textsf{sample}_{p~1~s}$ \\ + \hline + 4 & \texttt{[-73, -78]} & \texttt{[28, 26]} \\ + 3 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ + 2 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ + 1 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ + 0 & \texttt{[0, 0, 0]} & \texttt{[0, 0, 0]} \\ + \hline + \end{tabular} + } +} +\end{figure} +\par +\noindent +we combine them into a single set of arguments for each decorrelation pass: +\begin{table}[h] +{\relsize{-1} + \begin{tabular}{|r|r|r|r|r|r|} + \hline + & $\textbf{pass}_0$ & $\textbf{pass}_1$ & $\textbf{pass}_2$ & + $\textbf{pass}_3$ & $\textbf{pass}_4$ \\ + \hline + $\textsf{term}_p$ & 3 & 17 & 2 & 18 & 18 \\ + $\textsf{delta}_p$ & 2 & 2 & 2 & 2 & 2 \\ + $\textsf{weight}_{p~0}$ & 16 & 48 & 32 & 48 & 48 \\ + $\textsf{sample}_{p~0~s}$ & \texttt{[0, 0, 0]} & \texttt{[0, 0]} & + \texttt{[0, 0]} & \texttt{[0, 0]} & \texttt{[-73, -78]} \\ + $\textsf{weight}_{p~1}$ & 24 & 48 & 32 & 48 & 48 \\ + $\textsf{sample}_{p~1~s}$ & \texttt{[0, 0, 0]} & \texttt{[0, 0]} & + \texttt{[0, 0]} & \texttt{[0, 0]} & \texttt{[28, 26]} \\ + \hline + \end{tabular} +} +\end{table} +\par +\noindent +which we apply to the residuals from the bitstream sub-block: +\par +\noindent +{\relsize{-1} + \begin{tabular}{|r|r|r|r|r|r|} + \hline + $\textsf{residual}_{0~i}$ & + after $\textbf{pass}_0$ & + after $\textbf{pass}_1$ & + after $\textbf{pass}_2$ & + after $\textbf{pass}_3$ & + after $\textbf{pass}_4$ \\ + \hline + -61 & -61 & -61 & -61 & -61 & -64 \\ + -33 & -33 & -39 & -39 & -43 & -46 \\ + -18 & -18 & -19 & -21 & -23 & -25 \\ + 1 & 0 & 0 & -1 & -2 & -3 \\ + 20 & 20 & 21 & 20 & 20 & 20 \\ + 35 & 35 & 37 & 37 & 39 & 41 \\ + 50 & 50 & 53 & 54 & 57 & 60 \\ + 62 & 62 & 66 & 67 & 71 & 75 \\ + 68 & 68 & 73 & 75 & 80 & 85 \\ + 71 & 72 & 77 & 79 & 84 & 90 \\ + \hline + \hline + $\textsf{residual}_{1~i}$ & + after $\textbf{pass}_0$ & + after $\textbf{pass}_1$ & + after $\textbf{pass}_2$ & + after $\textbf{pass}_3$ & + after $\textbf{pass}_4$ \\ + \hline + 31 & 31 & 31 & 31 & 31 & 32 \\ + 32 & 32 & 35 & 35 & 37 & 39 \\ + 36 & 36 & 38 & 39 & 41 & 43 \\ + 37 & 38 & 40 & 41 & 43 & 45 \\ + 35 & 36 & 38 & 39 & 41 & 44 \\ + 31 & 32 & 34 & 36 & 38 & 40 \\ + 25 & 26 & 28 & 30 & 32 & 34 \\ + 18 & 19 & 20 & 21 & 23 & 25 \\ + 10 & 11 & 12 & 13 & 14 & 15 \\ + 0 & 1 & 1 & 2 & 3 & 4 \\ + \hline + \end{tabular} +} +\par +\noindent +Resulting in final decorrelated samples: +\newline +\begin{tabular}{rr} +$\textsf{channel}_0$ : & \texttt{[-64,~-46,~-25,~-3,~20,~41,~60,~75,~85,~90]} \\ +$\textsf{channel}_1$ : & \texttt{[~32,~~39,~~43,~45,~44,~40,~34,~25,~15,~~4]} \\ +\end{tabular} + +\clearpage + +{\relsize{-2} +\begin{tabular}{r||r|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}} +%% \multicolumn{5}{c}{\textbf{pass 0} - term 3 - delta 2 - weight 16} \\ +& $i$ & \textsf{correlated}_i & \textsf{temp}_i & \textsf{decorrelated}_{i + 3} & \textsf{weight}_{i + 1} \\ +\hline +\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_0$ - term 3\end{sideways}} +& 0 & -61 & & +\lfloor(16 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor - 61 = -61 & +16 + 0 = 16 +\\ +& 1 & -33 & & +\lfloor(16 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor - 33 = -33 & +16 + 0 = 16 +\\ +& 2 & -18 & & +\lfloor(16 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor - 18 = -18 & +16 + 0 = 16 +\\ +& 3 & 1 & & +\lfloor(16 \times -61 + 2 ^ 9) \div 2 ^ {10}\rfloor + 1 = 0 & +16 - 2 = 14 +\\ +& 4 & 20 & & +\lfloor(14 \times -33 + 2 ^ 9) \div 2 ^ {10}\rfloor + 20 = 20 & +14 - 2 = 12 +\\ +& 5 & 35 & & +\lfloor(12 \times -18 + 2 ^ 9) \div 2 ^ {10}\rfloor + 35 = 35 & +12 - 2 = 10 +\\ +& 6 & 50 & & +\lfloor(10 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor + 50 = 50 & +10 + 0 = 10 +\\ +& 7 & 62 & & +\lfloor(10 \times 20 + 2 ^ 9) \div 2 ^ {10}\rfloor + 62 = 62 & +10 + 2 = 12 +\\ +& 8 & 68 & & +\lfloor(12 \times 35 + 2 ^ 9) \div 2 ^ {10}\rfloor + 68 = 68 & +12 + 2 = 14 +\\ +& 9 & 71 & & +\lfloor(14 \times 50 + 2 ^ 9) \div 2 ^ {10}\rfloor + 71 = 72 & +14 + 2 = 16 +\\ +\hline +\hline +%% \multicolumn{5}{c}{\textbf{pass 1 } - term 17 - delta 2 - weight 48} \\ +& $i$ & \textsf{correlated}_i & \textsf{temp}_i & \textsf{decorrelated}_{i + 2} & \textsf{weight}_{i + 1} \\ +\hline +\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_1$ - term 17\end{sideways}} +& 0 & -61 & +2 \times 0 - 0 = 0 & +\lfloor(48 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor - 61 = -61 & +48 + 0 = 48 +\\ +& 1 & -33 & +2 \times -61 - 0 = -122 & +\lfloor(48 \times -122 + 2 ^ 9) \div 2 ^ {10}\rfloor - 33 = -39 & +48 + 2 = 50 +\\ +& 2 & -18 & +2 \times -39 + 61 = -17 & +\lfloor(50 \times -17 + 2 ^ 9) \div 2 ^ {10}\rfloor - 18 = -19 & +50 + 2 = 52 +\\ +& 3 & 0 & +2 \times -19 + 39 = 1 & +\lfloor(52 \times 1 + 2 ^ 9) \div 2 ^ {10}\rfloor + 0 = 0 & +52 + 0 = 52 +\\ +& 4 & 20 & +2 \times 0 + 19 = 19 & +\lfloor(52 \times 19 + 2 ^ 9) \div 2 ^ {10}\rfloor + 20 = 21 & +52 + 2 = 54 +\\ +& 5 & 35 & +2 \times 21 - 0 = 42 & +\lfloor(54 \times 42 + 2 ^ 9) \div 2 ^ {10}\rfloor + 35 = 37 & +54 + 2 = 56 +\\ +& 6 & 50 & +2 \times 37 - 21 = 53 & +\lfloor(56 \times 53 + 2 ^ 9) \div 2 ^ {10}\rfloor + 50 = 53 & +56 + 2 = 58 +\\ +& 7 & 62 & +2 \times 53 - 37 = 69 & +\lfloor(58 \times 69 + 2 ^ 9) \div 2 ^ {10}\rfloor + 62 = 66 & +58 + 2 = 60 +\\ +& 8 & 68 & +2 \times 66 - 53 = 79 & +\lfloor(60 \times 79 + 2 ^ 9) \div 2 ^ {10}\rfloor + 68 = 73 & +60 + 2 = 62 +\\ +& 9 & 72 & +2 \times 73 - 66 = 80 & +\lfloor(62 \times 80 + 2 ^ 9) \div 2 ^ {10}\rfloor + 72 = 77 & +62 + 2 = 64 +\\ +\hline +\hline +%% \multicolumn{5}{c}{\textbf{pass 2 } - term 2 - delta 2 - weight 32} \\ +& $i$ & \textsf{correlated}_i & \textsf{temp}_i & \textsf{decorrelated}_{i + 2} & \textsf{weight}_{i + 1} \\ +\hline +\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_2$ - term 2\end{sideways}} +& 0 & -61 & & +\lfloor(32 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor - 61 = -61 & +32 + 0 = 32 +\\ +& 1 & -39 & & +\lfloor(32 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor - 39 = -39 & +32 + 0 = 32 +\\ +& 2 & -19 & & +\lfloor(32 \times -61 + 2 ^ 9) \div 2 ^ {10}\rfloor - 19 = -21 & +32 + 2 = 34 +\\ +& 3 & 0 & & +\lfloor(34 \times -39 + 2 ^ 9) \div 2 ^ {10}\rfloor + 0 = -1 & +34 + 0 = 34 +\\ +& 4 & 21 & & +\lfloor(34 \times -21 + 2 ^ 9) \div 2 ^ {10}\rfloor + 21 = 20 & +34 - 2 = 32 +\\ +& 5 & 37 & & +\lfloor(32 \times -1 + 2 ^ 9) \div 2 ^ {10}\rfloor + 37 = 37 & +32 - 2 = 30 +\\ +& 6 & 53 & & +\lfloor(30 \times 20 + 2 ^ 9) \div 2 ^ {10}\rfloor + 53 = 54 & +30 + 2 = 32 +\\ +& 7 & 66 & & +\lfloor(32 \times 37 + 2 ^ 9) \div 2 ^ {10}\rfloor + 66 = 67 & +32 + 2 = 34 +\\ +& 8 & 73 & & +\lfloor(34 \times 54 + 2 ^ 9) \div 2 ^ {10}\rfloor + 73 = 75 & +34 + 2 = 36 +\\ +& 9 & 77 & & +\lfloor(36 \times 67 + 2 ^ 9) \div 2 ^ {10}\rfloor + 77 = 79 & +36 + 2 = 38 +\\ +\hline +\hline +%% \multicolumn{5}{c}{\textbf{pass 3 } - term 18 - delta 2 - weight 48} \\ +& $i$ & \textsf{correlated}_i & \textsf{temp}_i & \textsf{decorrelated}_{i + 2} & \textsf{weight}_{i + 1} \\ +\hline +\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_3$ - term 18\end{sideways}} +& 0 & -61 & +\lfloor(3 \times 0 - 0) \div 2\rfloor = 0 & +\lfloor(48 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor - 61 = -61 & +48 + 0 = 48 +\\ +& 1 & -39 & +\lfloor(3 \times -61 - 0) \div 2\rfloor = -92 & +\lfloor(48 \times -92 + 2 ^ 9) \div 2 ^ {10}\rfloor - 39 = -43 & +48 + 2 = 50 +\\ +& 2 & -21 & +\lfloor(3 \times -43 + 61) \div 2\rfloor = -34 & +\lfloor(50 \times -34 + 2 ^ 9) \div 2 ^ {10}\rfloor - 21 = -23 & +50 + 2 = 52 +\\ +& 3 & -1 & +\lfloor(3 \times -23 + 43) \div 2\rfloor = -13 & +\lfloor(52 \times -13 + 2 ^ 9) \div 2 ^ {10}\rfloor - 1 = -2 & +52 + 2 = 54 +\\ +& 4 & 20 & +\lfloor(3 \times -2 + 23) \div 2\rfloor = 8 & +\lfloor(54 \times 8 + 2 ^ 9) \div 2 ^ {10}\rfloor + 20 = 20 & +54 + 2 = 56 +\\ +& 5 & 37 & +\lfloor(3 \times 20 + 2) \div 2\rfloor = 31 & +\lfloor(56 \times 31 + 2 ^ 9) \div 2 ^ {10}\rfloor + 37 = 39 & +56 + 2 = 58 +\\ +& 6 & 54 & +\lfloor(3 \times 39 - 20) \div 2\rfloor = 48 & +\lfloor(58 \times 48 + 2 ^ 9) \div 2 ^ {10}\rfloor + 54 = 57 & +58 + 2 = 60 +\\ +& 7 & 67 & +\lfloor(3 \times 57 - 39) \div 2\rfloor = 66 & +\lfloor(60 \times 66 + 2 ^ 9) \div 2 ^ {10}\rfloor + 67 = 71 & +60 + 2 = 62 +\\ +& 8 & 75 & +\lfloor(3 \times 71 - 57) \div 2\rfloor = 78 & +\lfloor(62 \times 78 + 2 ^ 9) \div 2 ^ {10}\rfloor + 75 = 80 & +62 + 2 = 64 +\\ +& 9 & 79 & +\lfloor(3 \times 80 - 71) \div 2\rfloor = 84 & +\lfloor(64 \times 84 + 2 ^ 9) \div 2 ^ {10}\rfloor + 79 = 84 & +64 + 2 = 66 +\\ +\hline +\hline +%% \multicolumn{5}{c}{\textbf{pass 4 } - term 18 - delta 2 - weight 48} \\ +& $i$ & \textsf{correlated}_i & \textsf{temp}_i & \textsf{decorrelated}_{i + 2} & \textsf{weight}_{i + 1} \\ +\hline +\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_4$ - term 18\end{sideways}} +& 0 & -61 & +\lfloor(3 \times -73 + 78) \div 2\rfloor = -71 & +\lfloor(48 \times -71 + 2 ^ 9) \div 2 ^ {10}\rfloor - 61 = -64 & +48 + 2 = 50 +\\ +& 1 & -43 & +\lfloor(3 \times -64 + 73) \div 2\rfloor = -60 & +\lfloor(50 \times -60 + 2 ^ 9) \div 2 ^ {10}\rfloor - 43 = -46 & +50 + 2 = 52 +\\ +& 2 & -23 & +\lfloor(3 \times -46 + 64) \div 2\rfloor = -37 & +\lfloor(52 \times -37 + 2 ^ 9) \div 2 ^ {10}\rfloor - 23 = -25 & +52 + 2 = 54 +\\ +& 3 & -2 & +\lfloor(3 \times -25 + 46) \div 2\rfloor = -15 & +\lfloor(54 \times -15 + 2 ^ 9) \div 2 ^ {10}\rfloor - 2 = -3 & +54 + 2 = 56 +\\ +& 4 & 20 & +\lfloor(3 \times -3 + 25) \div 2\rfloor = 8 & +\lfloor(56 \times 8 + 2 ^ 9) \div 2 ^ {10}\rfloor + 20 = 20 & +56 + 2 = 58 +\\ +& 5 & 39 & +\lfloor(3 \times 20 + 3) \div 2\rfloor = 31 & +\lfloor(58 \times 31 + 2 ^ 9) \div 2 ^ {10}\rfloor + 39 = 41 & +58 + 2 = 60 +\\ +& 6 & 57 & +\lfloor(3 \times 41 - 20) \div 2\rfloor = 51 & +\lfloor(60 \times 51 + 2 ^ 9) \div 2 ^ {10}\rfloor + 57 = 60 & +60 + 2 = 62 +\\ +& 7 & 71 & +\lfloor(3 \times 60 - 41) \div 2\rfloor = 69 & +\lfloor(62 \times 69 + 2 ^ 9) \div 2 ^ {10}\rfloor + 71 = 75 & +62 + 2 = 64 +\\ +& 8 & 80 & +\lfloor(3 \times 75 - 60) \div 2\rfloor = 82 & +\lfloor(64 \times 82 + 2 ^ 9) \div 2 ^ {10}\rfloor + 80 = 85 & +64 + 2 = 66 +\\ +& 9 & 84 & +\lfloor(3 \times 85 - 75) \div 2\rfloor = 90 & +\lfloor(66 \times 90 + 2 ^ 9) \div 2 ^ {10}\rfloor + 84 = 90 & +66 + 2 = 68 +\\ +\end{tabular} +} +\begin{center} +$\text{channel}_0$ decorrelation passes +\end{center}
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/decode/entropy.tex
Added
@@ -0,0 +1,68 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\subsection{Decoding Entropy Variables} +\label{wavpack:decode_entropy_variables} +{\relsize{-1} +\ALGORITHM{\VAR{mono output} and \VAR{false stereo} from block header, \VAR{sub block size}, \VAR{actual size 1 less}, sub block data}{2 lists of 3 signed entropy value integers\footnote{$\text{entropy}_{c~i}$ indicates the $i$th entropy of channel $c$}} +\SetKwData{ENTROPY}{entropy} +\SetKwFunction{EXP}{wv\_exp2} +\SetKwData{MONO}{mono output} +\SetKwData{FALSESTEREO}{false stereo} +\SetKwData{ONELESS}{actual size 1 less} +\SetKwData{SUBBLOCKSIZE}{sub block size} +\SetKw{AND}{and} +\ASSERT $\text{\ONELESS} = 0$\; +\eIf(\tcc*[f]{2 channels}){$\text{\MONO} = 0$ \AND $\text{\FALSESTEREO} = 0$}{ + \ASSERT $\text{\SUBBLOCKSIZE} = 6$\; + $\text{\ENTROPY}_{0~0} \leftarrow \text{read \EXP value}$\; + $\text{\ENTROPY}_{0~1} \leftarrow \text{read \EXP value}$\; + $\text{\ENTROPY}_{0~2} \leftarrow \text{read \EXP value}$\; + $\text{\ENTROPY}_{1~0} \leftarrow \text{read \EXP value}$\; + $\text{\ENTROPY}_{1~1} \leftarrow \text{read \EXP value}$\; + $\text{\ENTROPY}_{1~2} \leftarrow \text{read \EXP value}$\; +}(\tcc*[f]{1 channel}){ + \ASSERT $\text{\SUBBLOCKSIZE} = 3$\; + $\text{\ENTROPY}_{0~0} \leftarrow \text{read \EXP value}$\; + $\text{\ENTROPY}_{0~1} \leftarrow \text{read \EXP value}$\; + $\text{\ENTROPY}_{0~2} \leftarrow \text{read \EXP value}$\; + $\text{\ENTROPY}_{1} \leftarrow \texttt{[0, 0, 0]}$\; +} +\Return $\text{\ENTROPY}_0$ list and $\text{\ENTROPY}_1$ list\; +\EALGORITHM + +\begin{figure}[h] + \includegraphics{wavpack/figures/entropy_vars.pdf} +\end{figure} + +\clearpage + +\subsubsection{Reading Entropy Variables Example} +Given a 2 channel block containing the subframe: +\begin{figure}[h] +\includegraphics{wavpack/figures/entropy_vars_parse.pdf} +\end{figure} +\begin{center} +{\relsize{-1} +\renewcommand{\arraystretch}{1.25} +\begin{tabular}{r|>{$}r<{$}|>{$}r<{$}} +$i$ & \text{entropy}_{0~i} & \text{entropy}_{1~i} \\ +\hline +0 & +\lfloor \EXP(2018 \bmod{256}) \div 2 ^ {9 - \lfloor 2018 \div 2 ^ 8 \rfloor} \rfloor = 118 & +\lfloor \EXP(2018 \bmod{256}) \div 2 ^ {9 - \lfloor 2018 \div 2 ^ 8 \rfloor} \rfloor = 118 \\ +1 & +\lfloor \EXP(2203 \bmod{256}) \div 2 ^ {9 - \lfloor 2203 \div 2 ^ 8 \rfloor} \rfloor = 194 & +\lfloor \EXP(2166 \bmod{256}) \div 2 ^ {9 - \lfloor 2166 \div 2 ^ 8 \rfloor} \rfloor = 176 \\ +2 & +\EXP(2389 \bmod{256}) \times 2 ^ {\lfloor 2389 \div 2 ^ 8 \rfloor - 9} = 322 & +\lfloor \EXP(2234 \bmod{256}) \div 2 ^ {9 - \lfloor 2234 \div 2 ^ 8 \rfloor} \rfloor = 212 \\ +\end{tabular} +\renewcommand{\arraystretch}{1.0} +} +\end{center}
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/decode/samples.tex
Added
@@ -0,0 +1,162 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\subsection{Decoding Decorrelation Samples} +\label{wavpack:decode_decorrelation_samples} +{\relsize{-2} +\ALGORITHM{\VAR{mono output} and \VAR{false stereo} from block header, decorrelation terms, sub block size and data}{a list of signed decorrelation sample lists per channel per decorrelation term\footnote{\relsize{-1}$\text{sample}_{p~c~s}$ indicates the $s$th sample of decorrelation pass $p$ for channel $c$}} +\SetKwData{MONO}{mono output} +\SetKwData{FALSESTEREO}{false stereo} +\SetKwData{CHANNELS}{channel count} +\SetKwData{SAMPLE}{sample} +\SetKwData{TOTALSAMPLES}{sample count} +\SetKwData{TERMCOUNT}{term count} +\SetKwData{TERM}{term} +\SetKwFunction{EXP}{wv\_exp2} +\SetKw{KwDownTo}{downto} +\SetKw{AND}{and} +\eIf{$(\MONO = 0)$ \AND $(\FALSESTEREO = 0)$}{ + $\CHANNELS \leftarrow 2$\; +}{ + $\CHANNELS \leftarrow 1$\; +} +\For{$p \leftarrow \TERMCOUNT$ \emph{\KwDownTo}0}{ + \uIf(\tcc*[f]{2 samples per channel}){$17 \leq \text{\TERM}_p \leq 18$}{ + \eIf{$\text{sub block bytes remaining} \geq (\CHANNELS \times 4)$}{ + \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELS}{ + $\text{\SAMPLE}_{p~c~0} \leftarrow \text{read \EXP value}$\; + $\text{\SAMPLE}_{p~c~1} \leftarrow \text{read \EXP value}$\; + } + }{ + \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELS}{ + $\text{\SAMPLE}_{p~c} \leftarrow \texttt{[0, 0]}$\; + } + } + } + \uElseIf(\tcc*[f]{"term" samples per channel}){$1 \leq \text{\TERM}_p \leq 8$}{ + \eIf{$\text{sub block bytes remaining} \geq (\CHANNELS \times \text{\TERM}_p \times 2)$}{ + \For{$s \leftarrow 0$ \emph{\KwTo}$\text{\TERM}_p$}{ + \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELS}{ + $\text{\SAMPLE}_{p~c~s} \leftarrow \text{read \EXP value}$\; + } + } + }{ + \For{$s \leftarrow 0$ \emph{\KwTo}$\text{\TERM}_p$}{ + \For{$c \leftarrow 0$ \emph{\KwTo}\CHANNELS}{ + $\text{\SAMPLE}_{p~c~s} \leftarrow 0$\; + } + } + } + } + \ElseIf(\tcc*[f]{1 sample per channel}){$-3 \leq \text{\TERM}_p \leq -1$}{ + \eIf{$\text{sub block bytes remaining} \geq 4$}{ + $\text{\SAMPLE}_{p~0~0} \leftarrow \text{read \EXP value}$\; + $\text{\SAMPLE}_{p~1~0} \leftarrow \text{read \EXP value}$\; + }{ + $\text{\SAMPLE}_{p~0~0} \leftarrow 0$\; + $\text{\SAMPLE}_{p~1~0} \leftarrow 0$\; + } + } +} +\Return $\text{\SAMPLE}$ lists per pass, per channel\; +\EALGORITHM +} +\begin{figure}[h] + \includegraphics{wavpack/figures/decorr_samples.pdf} +\end{figure} + +\clearpage + +\subsubsection{Reading wv\_exp2 Values} +\label{wavpack_wvexp2} +{\relsize{-1} +\ALGORITHM{2 bytes of sub block data}{a signed value} +\SetKwFunction{EXP}{wexp} +$value \leftarrow$ \READ 16 signed bits\; +\BlankLine +\uIf{$-32768 \leq value < -2304$}{ + \Return $-(\EXP(-value \bmod{256}) \times 2 ^ {\lfloor -value \div 2 ^ 8 \rfloor - 9})$\; +} +\uElseIf{$-2304 \leq value < 0$}{ + \Return $-\lfloor \EXP(-value \bmod{256}) \div 2 ^ {9 - \lfloor -value \div 2 ^ 8 \rfloor} \rfloor$\; +} +\uElseIf{$0 \leq value \leq 2304$}{ + \Return $\lfloor \EXP(value \bmod{256}) \div 2 ^ {9 - \lfloor value \div 2 ^ 8 \rfloor} \rfloor$\; +} +\ElseIf{$2304 < value \leq 32767$}{ + \Return $\EXP(value \bmod{256}) \times 2 ^ {\lfloor value \div 2 ^ 8 \rfloor - 9}$\; +} +\EALGORITHM +} +\par +\noindent +where \texttt{wexp}(\textit{x}) is defined from the following table: +\vskip .10in +\par +\noindent +{\relsize{-3}\ttfamily +\begin{tabular}{| c | c | c | c | c | c | c | c | c | c | c | c | c | c | c | c | c |} +\hline +& 0x?0 & 0x?1 & 0x?2 & 0x?3 & 0x?4 & 0x?5 & 0x?6 & 0x?7 & 0x?8 & 0x?9 & 0x?A & 0x?B & 0x?C & 0x?D & 0x?E & 0x?F \\ +\hline +0x0? & 256 & 257 & 257 & 258 & 259 & 259 & 260 & 261 & 262 & 262 & 263 & 264 & 264 & 265 & 266 & 267 \\ +0x1? & 267 & 268 & 269 & 270 & 270 & 271 & 272 & 272 & 273 & 274 & 275 & 275 & 276 & 277 & 278 & 278 \\ +0x2? & 279 & 280 & 281 & 281 & 282 & 283 & 284 & 285 & 285 & 286 & 287 & 288 & 288 & 289 & 290 & 291 \\ +0x3? & 292 & 292 & 293 & 294 & 295 & 296 & 296 & 297 & 298 & 299 & 300 & 300 & 301 & 302 & 303 & 304 \\ +0x4? & 304 & 305 & 306 & 307 & 308 & 309 & 309 & 310 & 311 & 312 & 313 & 314 & 314 & 315 & 316 & 317 \\ +0x5? & 318 & 319 & 320 & 321 & 321 & 322 & 323 & 324 & 325 & 326 & 327 & 328 & 328 & 329 & 330 & 331 \\ +0x6? & 332 & 333 & 334 & 335 & 336 & 337 & 337 & 338 & 339 & 340 & 341 & 342 & 343 & 344 & 345 & 346 \\ +0x7? & 347 & 348 & 349 & 350 & 350 & 351 & 352 & 353 & 354 & 355 & 356 & 357 & 358 & 359 & 360 & 361 \\ +0x8? & 362 & 363 & 364 & 365 & 366 & 367 & 368 & 369 & 370 & 371 & 372 & 373 & 374 & 375 & 376 & 377 \\ +0x9? & 378 & 379 & 380 & 381 & 382 & 383 & 384 & 385 & 386 & 387 & 388 & 389 & 391 & 392 & 393 & 394 \\ +0xA? & 395 & 396 & 397 & 398 & 399 & 400 & 401 & 402 & 403 & 405 & 406 & 407 & 408 & 409 & 410 & 411 \\ +0xB? & 412 & 413 & 415 & 416 & 417 & 418 & 419 & 420 & 421 & 422 & 424 & 425 & 426 & 427 & 428 & 429 \\ +0xC? & 431 & 432 & 433 & 434 & 435 & 436 & 438 & 439 & 440 & 441 & 442 & 444 & 445 & 446 & 447 & 448 \\ +0xD? & 450 & 451 & 452 & 453 & 454 & 456 & 457 & 458 & 459 & 461 & 462 & 463 & 464 & 466 & 467 & 468 \\ +0xE? & 470 & 471 & 472 & 473 & 475 & 476 & 477 & 478 & 480 & 481 & 482 & 484 & 485 & 486 & 488 & 489 \\ +0xF? & 490 & 492 & 493 & 494 & 496 & 497 & 498 & 500 & 501 & 502 & 504 & 505 & 506 & 508 & 509 & 511 \\ +\hline +\end{tabular} +} + +\subsubsection{Reading Decorrelation Samples Example} +Given a stereo block containing the sub-block: +\begin{figure}[h] +\includegraphics{wavpack/figures/decorr_samples_parse.pdf} +\end{figure} +\begin{center} +{\relsize{-2} +\begin{tabular}{r|r|r|>{$}r<{$}|>{$}r<{$}} +$p$ & $\text{term}_p$ & $s$ & +\text{sample}_{p~0~s} & +\text{sample}_{p~1~s} \\ +\hline +4 & 18 & 0 & +-\lfloor \texttt{wexp}(1841 \bmod{256}) \div 2 ^ {9 - \lfloor 1841 \div 2 ^ 8 \rfloor} \rfloor = -73 & +\lfloor \EXP(1487 \bmod{256}) \div 2 ^ {9 - \lfloor 1487 \div 2 ^ 8 \rfloor} \rfloor = 28 \\ +& & 1 & +-\lfloor \EXP(1865 \bmod{256}) \div 2 ^ {9 - \lfloor 1865 \div 2 ^ 8 \rfloor} \rfloor = -78 & +\lfloor \EXP(1459 \bmod{256}) \div 2 ^ {9 - \lfloor 1459 \div 2 ^ 8 \rfloor} \rfloor = 26 \\ +\hline +3 & 18 & 0 & 0 & 0 \\ +& & 1 & 0 & 0 \\ +\hline +2 & 2 & 0 & 0 & 0 \\ +& & 1 & 0 & 0 \\ +\hline +1 & 17 & 0 & 0 & 0 \\ +& & 1 & 0 & 0 \\ +\hline +0 & 3 & 0 & 0 & 0 \\ +& & 1 & 0 & 0 \\ +& & 2 & 0 & 0 \\ +\hline +\end{tabular} +\renewcommand{\arraystretch}{1.0} +} +\end{center}
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/decode/terms.tex
Added
@@ -0,0 +1,65 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\subsection{Decode Decorrelation Terms} +\label{wavpack:decode_decorrelation_terms} +\ALGORITHM{\VAR{actual size 1 less} and \VAR{sub block size} values from sub block header, sub block data}{a list of signed decorrelation term integers, a list of unsigned decorrelation delta integers\footnote{$\text{term}_p$ and $\text{delta}_p$ indicate the term and delta values for decorrelation pass $p$}} +\SetKwData{PASSES}{passes} +\SetKwData{SUBBLOCKSIZE}{sub block size} +\SetKwData{ACTUALSIZEONELESS}{actual size 1 less} +\SetKwData{TERM}{term} +\SetKwData{DELTA}{delta} +\SetKw{OR}{or} +\SetKw{KwDownTo}{downto} +\eIf{$\text{\ACTUALSIZEONELESS} = 0$}{ + \PASSES $\leftarrow \text{\SUBBLOCKSIZE} \times 2$\; +}{ + \PASSES $\leftarrow \text{\SUBBLOCKSIZE} \times 2 - 1$\; +} +\ASSERT $\text{\PASSES} \leq 16$\; +\BlankLine +\For(\tcc*[f]{populate in reverse order}){$p \leftarrow \PASSES$ \emph{\KwDownTo}0}{ + $\text{\TERM}_p \leftarrow$ (\READ 5 unsigned bits) - 5\; + \ASSERT $\text{\TERM}_p$ \IN \texttt{[-3, -2, -1, 1, 2, 3, 4, 5, 6, 7, 8, 17, 18]} + \BlankLine + $\text{\DELTA}_p \leftarrow$ \READ 3 unsigned bits\; +} +\Return $\left\lbrace\begin{tabular}{l} +decorrelation \TERM \\ +decorrelation \DELTA \\ +\end{tabular}\right.$\; +\EALGORITHM + +\begin{figure}[h] + \includegraphics{wavpack/figures/decorr_terms.pdf} +\end{figure} + +\clearpage + +\subsubsection{Reading Decorrelation Terms Example} + +\begin{figure}[h] +\includegraphics{wavpack/figures/terms_parse.pdf} +\end{figure} +\begin{center} +{\renewcommand{\arraystretch}{1.25} +\begin{tabular}{>{$}r<{$}>{$}c<{$}>{$}r<{$}|>{$}r<{$}>{$}r<{$}>{$}r<{$}} +\text{decorrelation term}_4 & \leftarrow & 23 - 5 = 18 & +\text{decorrelation delta}_4 & \leftarrow & 2 \\ +\text{decorrelation term}_3 & \leftarrow & 23 - 5 = 18 & +\text{decorrelation delta}_3 & \leftarrow & 2 \\ +\text{decorrelation term}_2 & \leftarrow & 7 - 5 = 2 & +\text{decorrelation delta}_2 & \leftarrow & 2 \\ +\text{decorrelation term}_1 & \leftarrow & 22 - 5 = 17 & +\text{decorrelation delta}_1 & \leftarrow & 2 \\ +\text{decorrelation term}_0 & \leftarrow & 8 - 5 = 3 & +\text{decorrelation delta}_0 & \leftarrow & 2 \\ +\end{tabular} +\renewcommand{\arraystretch}{1.0} +} +\end{center}
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/decode/weights.tex
Added
@@ -0,0 +1,104 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\subsection{Decode Decorrelation Weights} +\label{wavpack:decode_decorrelation_weights} +{\relsize{-1} +\ALGORITHM{\VAR{mono output} and \VAR{false stereo} from block header, decorrelation terms count\footnote{from the decorrelation terms sub block, which must be read prior to this sub block}, \VAR{actual size 1 less} and \VAR{sub block size} values from sub block header, sub block data}{a list of signed weight integers per channel\footnote{$\text{weight}_{p~c}$ indicates weight value for decorrelation pass $p$, channel $c$}} +\SetKwData{MONO}{mono output} +\SetKwData{FALSESTEREO}{false stereo} +\SetKwData{SUBBLOCKSIZE}{sub block size} +\SetKwData{ACTUALSIZEONELESS}{actual size 1 less} +\SetKwData{WEIGHTCOUNT}{weight count} +\SetKwData{TERMCOUNT}{term count} +\SetKwData{WEIGHTVAL}{weight value} +\SetKwData{WEIGHT}{weight} +\SetKw{AND}{and} +\tcc{read as many 8 bit weight values as possible} +\eIf{$\text{\ACTUALSIZEONELESS} = 0$}{ + \WEIGHTCOUNT $\leftarrow \text{\SUBBLOCKSIZE} \times 2$\; +}{ + \WEIGHTCOUNT $\leftarrow \text{\SUBBLOCKSIZE} \times 2 - 1$\; +} +\For{$i \leftarrow 0$ \emph{\KwTo}\WEIGHTCOUNT}{ + $\text{value}_i \leftarrow$ \READ 8 signed bits\; + $\text{\WEIGHTVAL}_i \leftarrow\begin{cases} +\text{value}_i \times 2 ^ 3 + \left\lfloor\frac{\text{value}_i \times 2 ^ 3 + 2 ^ 6}{2 ^ 7}\right\rfloor & \text{if }\text{value}_i > 0 \\ +0 & \text{if }\text{value}_i = 0 \\ +\text{value}_i \times 2 ^ 3 & \text{if }\text{value}_i < 0 +\end{cases}$\; +} +\BlankLine +\tcc{populate weight values by channel, in reverse order} +\eIf(\tcc*[f]{two channels}){$\text{\MONO} = 0$ \AND $\text{\FALSESTEREO} = 0$}{ + \ASSERT $\lfloor\WEIGHTCOUNT \div 2\rfloor \leq \TERMCOUNT$\; + \For{$i \leftarrow 0$ \emph{\KwTo}$\lfloor\WEIGHTCOUNT \div 2\rfloor$}{ + $\text{\WEIGHT}_{(\TERMCOUNT - i - 1)~0} \leftarrow \text{\WEIGHTVAL}_{i \times 2}$\; + $\text{\WEIGHT}_{(\TERMCOUNT - i - 1)~1} \leftarrow \text{\WEIGHTVAL}_{i \times 2 + 1}$\; + } + \For{$i \leftarrow \lfloor\WEIGHTCOUNT \div 2\rfloor$ \emph{\KwTo}\TERMCOUNT}{ + $\text{\WEIGHT}_{(\TERMCOUNT - i - 1)~0} \leftarrow 0$\; + $\text{\WEIGHT}_{(\TERMCOUNT - i - 1)~1} \leftarrow 0$\; + } + \Return a \WEIGHT value per pass, per channel\; +}(\tcc*[f]{one channel}){ + \ASSERT $\WEIGHTCOUNT \leq \TERMCOUNT$\; + \For{$i \leftarrow 0$ \emph{\KwTo}\WEIGHTCOUNT}{ + $\text{\WEIGHT}_{(\TERMCOUNT - i - 1)~0} \leftarrow \text{\WEIGHTVAL}_{i}$\; + } + \For{$i \leftarrow \WEIGHTCOUNT$ \emph{\KwTo}\TERMCOUNT}{ + $\text{\WEIGHT}_{(\TERMCOUNT - i - 1)~0} \leftarrow 0$\; + } + \Return a \WEIGHT value per pass\; +} +\EALGORITHM +} +\begin{figure}[h] + \includegraphics{wavpack/figures/decorr_weights.pdf} +\end{figure} + +\clearpage + +\subsubsection{Reading Decorrelation Weights Example} +Given a 2 channel block containing 5 decorrelation terms: +\begin{figure}[h] +\includegraphics{wavpack/figures/decorr_weights_parse.pdf} +\end{figure} +\begin{center} +{\renewcommand{\arraystretch}{1.25} +\begin{tabular}{r|r|>{$}r<{$}} +$i$ & $\text{value}_i$ & \text{weight value}_i \\ +\hline +0 & 6 & 6 \times 2 ^ 3 + \lfloor(6 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 48 \\ +1 & 6 & 6 \times 2 ^ 3 + \lfloor(6 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 48 \\ +2 & 6 & 6 \times 2 ^ 3 + \lfloor(6 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 48 \\ +3 & 6 & 6 \times 2 ^ 3 + \lfloor(6 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 48 \\ +4 & 4 & 4 \times 2 ^ 3 + \lfloor(4 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 32 \\ +5 & 4 & 4 \times 2 ^ 3 + \lfloor(4 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 32 \\ +6 & 6 & 6 \times 2 ^ 3 + \lfloor(6 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 48 \\ +7 & 6 & 6 \times 2 ^ 3 + \lfloor(6 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 48 \\ +8 & 2 & 2 \times 2 ^ 3 + \lfloor(2 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 16 \\ +9 & 3 & 3 \times 2 ^ 3 + \lfloor(3 \times 2 ^ 3 + 2 ^ 6) \div 2 ^ 7\rfloor = 24 \\ +\end{tabular} +\renewcommand{\arraystretch}{1.0} +} +\end{center} +\begin{center} +\begin{tabular}{>{$}r<{$}||>{$}r<{$}} +\text{weight}_{4~0} = \text{weight value}_0 = 48 & +\text{weight}_{4~1} = \text{weight value}_1 = 48 \\ +\text{weight}_{3~0} = \text{weight value}_2 = 48 & +\text{weight}_{3~1} = \text{weight value}_3 = 48 \\ +\text{weight}_{2~0} = \text{weight value}_4 = 32 & +\text{weight}_{2~1} = \text{weight value}_5 = 32 \\ +\text{weight}_{1~0} = \text{weight value}_6 = 48 & +\text{weight}_{1~1} = \text{weight value}_7 = 48 \\ +\text{weight}_{0~0} = \text{weight value}_8 = 16 & +\text{weight}_{0~1} = \text{weight value}_9 = 24 \\ +\end{tabular} +\end{center}
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/encode
Added
+(directory)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/encode.tex
Added
@@ -0,0 +1,901 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\section{WavPack Encoding} + +\ALGORITHM{PCM frames, Wave header\footnote{Everything between the file's start and the start of the \texttt{data} chunk's contents. If one is encoding a WavPack from raw PCM input, this header will need to be generated.}, optional Wave footer\footnote{Everything between the end of the \texttt{data} chunk's contents and the file's end, if anything.}, encoding parameters: +\newline +{\relsize{-1} +\begin{tabular}{rll} +parameter & possible values & typical values \\ +\hline +block size & a positive number of PCM frames & 22050 \\ +correlation passes & 0, 1, 2, 5, 10 or 16 & 5 \\ +\end{tabular} +} +}{an encoded WavPack file} +\SetKwData{BLOCKSIZE}{block size} +\SetKwData{BLOCKINDEX}{block index} +\SetKwData{BLOCKSETCOUNT}{block count} +\SetKwData{BLOCKSETCHANNELS}{block channels} +\SetKwData{PASSES}{correlation passes} +\SetKwData{PARAMS}{block params} +\SetKwData{FIRST}{first} +\SetKwData{LAST}{last} +\SetKwData{BLOCK}{block} +\SetKwData{CHANNELS}{channels} +\SetKwData{CHANNEL}{channel} +$(\text{\BLOCKSETCOUNT}~,~\text{\BLOCKSETCHANNELS}) \leftarrow$ \hyperref[wavpack:block_split]{determine block split}\; +\For{$b \leftarrow 0$ \emph{\KwTo}\BLOCKSETCOUNT}{ + $\text{\PARAMS}_{0~b} \leftarrow$ \hyperref[wavpack:initial_correlation_parameters]{determine initial correlation parameters and entropy variables from correlation passes and $\text{\BLOCKSETCHANNELS}_b$}\; +} +\BlankLine +$\text{\BLOCKINDEX} \leftarrow 0$\; +$s \leftarrow 0$\tcc*[r]{the number of block sets written} +\While{PCM frames remain}{ + $\text{\CHANNELS} \leftarrow$ take up to \BLOCKSIZE PCM frames from the input\; + update the stream's MD5 sum with that PCM data\; + $c \leftarrow 0$\; + \For(\tcc*[f]{blocks in each set}){$b \leftarrow 0$ \emph{\KwTo}\BLOCKSETCOUNT}{ + \lIf{$b = 0$}{$\text{\FIRST} = 1$} + \lElse{$\text{\FIRST} = 0$}\; + \lIf{$b = \text{\BLOCKSETCOUNT} - 1$}{$\text{\LAST} = 1$} + \lElse{$\text{\LAST} = 0$}\; + \uIf{$\text{\BLOCKSETCHANNELS}_b = 1$}{ + $(\text{\BLOCK}_{(s \times \text{\BLOCKSETCHANNELS}) + b}~,~\text{\PARAMS}_{(s + 1)~b}) \leftarrow$ \hyperref[wavpack:write_block]{write block}\newline + using $\text{\CHANNEL}_c$, \BLOCKINDEX, \FIRST, \LAST and $\text{\PARAMS}_{s~b}$\; + $c \leftarrow c + 1$\; + } + \ElseIf{$\text{\BLOCKSETCHANNELS}_b = 2$}{ + $(\text{\BLOCK}_{(s \times \text{\BLOCKSETCHANNELS}) + b}~,~\text{\PARAMS}_{(s + 1)~b}) \leftarrow$ \hyperref[wavpack:write_block]{write block}\newline + using $\text{\CHANNEL}_c/\text{\CHANNEL}_{c + 1}$, \BLOCKINDEX, \FIRST, \LAST and $\text{\PARAMS}_{s~b}$\; + $c \leftarrow c + 2$\; + } + } + $s \leftarrow s + 1$\; + $\text{\BLOCKINDEX} \leftarrow \text{\BLOCKINDEX} + \text{PCM data's frame count}$\; +} +\BlankLine +write final block containing optional \hyperref[wavpack:write_wave_header]{Wave footer} and \hyperref[wavpack:write_md5]{MD5 sum} sub blocks\; +update Wave header's \texttt{data} chunk size, if generated from scratch\; +update \VAR{total samples} field in all block headers with \BLOCKINDEX\; +\EALGORITHM + +\clearpage + +\subsection{Determine Block Split} +\label{wavpack:block_split} +\ALGORITHM{input stream's channel assignment}{number of blocks per set, list of channel counts per block} +\SetKwData{BLOCKCOUNT}{block count} +\SetKwData{BLOCKCHANNELS}{block channels} +\Switch(\tcc*[f]{split channels by left/right pairs}){channel assignment}{ + \uCase{mono}{ + $\text{\BLOCKCOUNT} \leftarrow 1$\; + $\text{\BLOCKCHANNELS} \leftarrow \texttt{[1]}$\; + } + \uCase{front left, front right}{ + $\text{\BLOCKCOUNT} \leftarrow 1$\; + $\text{\BLOCKCHANNELS} \leftarrow \texttt{[2]}$\; + } + \uCase{front left, front right, front center}{ + $\text{\BLOCKCOUNT} \leftarrow 2$\; + $\text{\BLOCKCHANNELS} \leftarrow \texttt{[2, 1]}$\; + } + \uCase{front left, front right, back left, back right}{ + $\text{\BLOCKCOUNT} \leftarrow 2$\; + $\text{\BLOCKCHANNELS} \leftarrow \texttt{[2, 2]}$\; + } + \uCase{front left, front right, front center, back center}{ + $\text{\BLOCKCOUNT} \leftarrow 3$\; + $\text{\BLOCKCHANNELS} \leftarrow \texttt{[2, 1, 1]}$\; + } + \uCase{front left, front right, front center, back left, back right}{ + $\text{\BLOCKCOUNT} \leftarrow 3$\; + $\text{\BLOCKCHANNELS} \leftarrow \texttt{[2, 1, 2]}$\; + } + \uCase{front left, front right, front center, LFE, back left, back right}{ + $\text{\BLOCKCOUNT} \leftarrow 4$\; + $\text{\BLOCKCHANNELS} \leftarrow \texttt{[2, 1, 1, 2]}$\; + } + \Other(\tcc*[f]{save them independently}){ + $\text{\BLOCKCOUNT} \leftarrow$ channel count\; + $\text{\BLOCKCHANNELS} \leftarrow$ 1 per channel\; + } +} +\Return \BLOCKCOUNT and \BLOCKCHANNELS +\EALGORITHM +\vskip 1ex +\par +\noindent +One could invent alternate channel splits for other obscure assignments. +WavPack's only requirement is that all channels must be in +Wave order\footnote{see page \pageref{wave_channel_assignment}} +and each block must contain 1 or 2 channels. + +\begin{figure}[h] +\includegraphics{wavpack/figures/block_channels.pdf} +\end{figure} + +\begin{landscape} + +\subsection{Determine Correlation Parameters and Entropy Variables} +\label{wavpack:initial_correlation_parameters} +{\relsize{-1} +\begin{description} +\item[$\text{term}_{b~p}$] correlation term for block $b$, correlation pass $p$ +\item[$\text{delta}_{b~p}$] correlation delta for block $b$, correlation pass $p$ +\item[$\text{weight}_{b~p~c}$] correlation weight for block $b$, correlation pass $p$, channel $c$ +\item[$\text{sample}_{b~p~c~s}$] correlation sample $s$ for block $b$, correlation pass $p$, channel $c$ +\item[$\text{entropy}_{b~c~m}$] median $m$ for block $b$, channel $c$ +\end{description} +\par +\noindent +We'll omit the block $b$ parameter since it will be the same +throughout the block encode, but one must keep it in mind +when transferring parameters from the block of one set of channels +to the next block of those same channels. +} +\vskip .10in +\par +\noindent +\ALGORITHM{correlation pass count, block's channel count of 1 or 2}{correlation term, delta, weights and samples for each pass; 3 entropy variables for each channel} +{\relsize{-2} +$\text{entropy}_0 \leftarrow \texttt{[0, 0, 0]}$\; +$\text{entropy}_1 \leftarrow \texttt{[0, 0, 0]}$\; +\BlankLine +\If{$\text{channel count} = 1$}{ +\Switch{correlation pass count}{ +\uCase{1}{ +\begin{tabular}{r|rrrl} +$\text{pass}~p$ & $\text{term}_p$ & $\text{delta}_p$ & $\text{weight}_{p~0}$ & $\text{samples}_{p~0}$ \\ +\hline +0 & 18 & 2 & 0 & \texttt{[0, 0]} \\ +\end{tabular} +} +\uCase{2}{ +\begin{tabular}{r|rrrl} +$\text{pass}~p$ & $\text{term}_p$ & $\text{delta}_p$ & $\text{weight}_{p~0}$ & $\text{samples}_{p~0}$ \\ +\hline +0 & 17 & 2 & 0 & \texttt{[0, 0]} \\ +1 & 18 & 2 & 0 & \texttt{[0, 0]} \\ +\end{tabular} +} +\uCase(\tcc*[f]{one channel blocks don't use negative terms}){5, 10, or 16}{ +\begin{tabular}{r|rrrl} +$\text{pass}~p$ & $\text{term}_p$ & $\text{delta}_p$ & $\text{weight}_{p~0}$ & $\text{samples}_{p~0}$ \\ +\hline +0 & 3 & 2 & 0 & \texttt{[0, 0, 0]} \\ +1 & 17 & 2 & 0 & \texttt{[0, 0]} \\ +2 & 2 & 2 & 0 & \texttt{[0, 0]} \\ +3 & 18 & 2 & 0 & \texttt{[0, 0]} \\ +4 & 18 & 2 & 0 & \texttt{[0, 0]} \\ +\end{tabular} +}}}} +\EALGORITHM + +\clearpage + +\begin{algorithm} +{\relsize{-2} +\ElseIf{$\text{channel count} = 2$}{ +\Switch{correlation pass count}{ +\uCase{1}{ +\begin{tabular}{r|rrrrll} +$\text{pass}~p$ & $\text{term}_p$ & $\text{delta}_p$ & $\text{weight}_{p~0}$ & $\text{weight}_{p~1}$ & $\text{samples}_{p~0}$ & $\text{samples}_{p~1}$ \\ +\hline +0 & 18 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +\end{tabular} +} +\uCase{2}{ +\begin{tabular}{r|rrrrll} +$\text{pass}~p$ & $\text{term}_p$ & $\text{delta}_p$ & $\text{weight}_{p~0}$ & $\text{weight}_{p~1}$ & $\text{samples}_{p~0}$ & $\text{samples}_{p~1}$ \\ +\hline +0 & 17 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +1 & 18 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +\end{tabular} +} +\uCase{5}{ +\begin{tabular}{r|rrrrll} +$\text{pass}~p$ & $\text{term}_p$ & $\text{delta}_p$ & $\text{weight}_{p~0}$ & $\text{weight}_{p~1}$ & $\text{samples}_{p~0}$ & $\text{samples}_{p~1}$ \\ +\hline +0 & 3 & 2 & 48 & 48 & \texttt{[0, 0, 0]} & \texttt{[0, 0, 0]} \\ +1 & 17 & 2 & 48 & 48 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +2 & 2 & 2 & 32 & 32 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +3 & 18 & 2 & 48 & 48 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +4 & 18 & 2 & 16 & 24 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +\end{tabular} +} +\uCase{10}{ +\begin{tabular}{r|rrrrll} +$\text{pass}~p$ & $\text{term}_p$ & $\text{delta}_p$ & $\text{weight}_{p~0}$ & $\text{weight}_{p~1}$ & $\text{samples}_{p~0}$ & $\text{samples}_{p~1}$ \\ +\hline +0 & 4 & 2 & 0 & 0 & \texttt{[0, 0, 0, 0]} & \texttt{[0, 0, 0, 0]} \\ +1 & 17 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +2 & -1 & 2 & 0 & 0 & \texttt{[0]} & \texttt{[0]} \\ +3 & 5 & 2 & 0 & 0 & \texttt{[0, 0, 0, 0, 0]} & \texttt{[0, 0, 0, 0, 0]} \\ +4 & 3 & 2 & 0 & 0 & \texttt{[0, 0, 0]} & \texttt{[0, 0, 0]} \\ +5 & 2 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +6 & -2 & 2 & 0 & 0 & \texttt{[0]} & \texttt{[0]} \\ +7 & 18 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +8 & 18 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +9 & 18 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +\end{tabular} +} +\Case{16}{ +\begin{tabular}{r|rrrrll} +$\text{pass}~p$ & $\text{term}_p$ & $\text{delta}_p$ & $\text{weight}_{p~0}$ & $\text{weight}_{p~1}$ & $\text{samples}_{p~0}$ & $\text{samples}_{p~1}$ \\ +\hline +0 & 2 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +1 & 18 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +2 & -1 & 2 & 0 & 0 & \texttt{[0]} & \texttt{[0]} \\ +3 & 8 & 2 & 0 & 0 & \texttt{[0, 0, 0, 0, 0, 0, 0, 0]} & \texttt{[0, 0, 0, 0, 0, 0, 0, 0]} \\ +4 & 6 & 2 & 0 & 0 & \texttt{[0, 0, 0, 0, 0, 0]} & \texttt{[0, 0, 0, 0, 0, 0]} \\ +5 & 3 & 2 & 0 & 0 & \texttt{[0, 0, 0]} & \texttt{[0, 0, 0]} \\ +6 & 5 & 2 & 0 & 0 & \texttt{[0, 0, 0, 0, 0]} & \texttt{[0, 0, 0, 0, 0]} \\ +7 & 7 & 2 & 0 & 0 & \texttt{[0, 0, 0, 0, 0, 0, 0]} & \texttt{[0, 0, 0, 0, 0, 0, 0]} \\ +8 & 4 & 2 & 0 & 0 & \texttt{[0, 0, 0, 0]} & \texttt{[0, 0, 0, 0]} \\ +9 & 2 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +10 & 18 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +11 & -2 & 2 & 0 & 0 & \texttt{[0]} & \texttt{[0]} \\ +12 & 3 & 2 & 0 & 0 & \texttt{[0, 0, 0]} & \texttt{[0, 0, 0]} \\ +13 & 2 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +14 & 18 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +15 & 18 & 2 & 0 & 0 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ +\end{tabular} +}}}} +\end{algorithm} + +\end{landscape} + +%% \subsection{Writing Block Set} +%% {\relsize{-1} +%% \ALGORITHM{PCM frames and their channel assignment, block index, encoding parameters}{one or more WavPack blocks} +%% \SetKwData{CHANNEL}{channel} +%% \SetKwData{FIRST}{first} +%% \SetKwData{LAST}{last} +%% \Switch(\tcc*[f]{split channels by left/right pairs}){channel assignment}{ +%% \uCase{mono}{ +%% \begin{tabular}{lrr} +%% write $\text{\CHANNEL}_0$ & $\text{\FIRST} = 1$ & $\text{\LAST} = 1$ \\ +%% \end{tabular}\; +%% } +%% \uCase{front left, front right}{ +%% \begin{tabular}{lrr} +%% write $\text{\CHANNEL}_0/\text{\CHANNEL}_1$ & $\text{\FIRST} = 1$ & $\text{\LAST} = 1$ \\ +%% \end{tabular}\; +%% } +%% \uCase{front left, front right, front center}{ +%% \begin{tabular}{lrr} +%% write $\text{\CHANNEL}_0/\text{\CHANNEL}_1$ & $\text{\FIRST} = 1$ & $\text{\LAST} = 0$ \\ +%% write $\text{\CHANNEL}_2$ & $\text{\FIRST} = 0$ & $\text{\LAST} = 1$ \\ +%% \end{tabular}\; +%% } +%% \uCase{front left, front right, back left, back right}{ +%% \begin{tabular}{lrr} +%% write $\text{\CHANNEL}_0/\text{\CHANNEL}_1$ & $\text{\FIRST} = 1$ & $\text{\LAST} = 0$ \\ +%% write $\text{\CHANNEL}_2/\text{\CHANNEL}_3$ & $\text{\FIRST} = 0$ & $\text{\LAST} = 1$ \\ +%% \end{tabular}\; +%% } +%% \uCase{front left, front right, front center, back center}{ +%% \begin{tabular}{lrr} +%% write $\text{\CHANNEL}_0/\text{\CHANNEL}_1$ & $\text{\FIRST} = 1$ & $\text{\LAST} = 0$ \\ +%% write $\text{\CHANNEL}_2$ & $\text{\FIRST} = 0$ & $\text{\LAST} = 0$ \\ +%% write $\text{\CHANNEL}_3$ & $\text{\FIRST} = 0$ & $\text{\LAST} = 1$ \\ +%% \end{tabular}\; +%% } +%% \uCase{front left, front right, front center, back left, back right}{ +%% \begin{tabular}{lrr} +%% write $\text{\CHANNEL}_0/\text{\CHANNEL}_1$ & $\text{\FIRST} = 1$ & $\text{\LAST} = 0$ \\ +%% write $\text{\CHANNEL}_2$ & $\text{\FIRST} = 0$ & $\text{\LAST} = 0$ \\ +%% write $\text{\CHANNEL}_3/\text{\CHANNEL}_4$ & $\text{\FIRST} = 0$ & $\text{\LAST} = 1$ \\ +%% \end{tabular}\; +%% } +%% \uCase{front left, front right, front center, LFE, back left, back right}{ +%% \begin{tabular}{lrr} +%% write $\text{\CHANNEL}_0/\text{\CHANNEL}_1$ & $\text{\FIRST} = 1$ & $\text{\LAST} = 0$ \\ +%% write $\text{\CHANNEL}_2$ & $\text{\FIRST} = 0$ & $\text{\LAST} = 0$ \\ +%% write $\text{\CHANNEL}_3$ & $\text{\FIRST} = 0$ & $\text{\LAST} = 0$ \\ +%% write $\text{\CHANNEL}_4/\text{\CHANNEL}_5$ & $\text{\FIRST} = 0$ & $\text{\LAST} = 1$ \\ +%% \end{tabular}\; +%% } +%% \Other(\tcc*[f]{save them independently}){ +%% \For{$i \leftarrow 0$ \emph{\KwTo}channel count}{ +%% \lIf{$i = 0$}{$\text{\FIRST} = 1$} +%% \lElse{$\text{\FIRST} = 0$}\; +%% \lIf{$i = \text{channel count} - 1$}{$\text{\LAST} = 1$} +%% \lElse{$\text{\LAST} = 0$}\; +%% write $\text{\CHANNEL}_i$ to block\; +%% } +%% } +%% } +%% \Return set of encoded WavPack blocks\; +%% \EALGORITHM + + +%% \clearpage + +\subsection{Writing Block} +\label{wavpack:write_block} +{\relsize{-1} +\ALGORITHM{1 or 2 channels of PCM frames, block index, first block, last block, encoding parameters from previous block}{a WavPack block, encoding parameters for next block} +\SetKwData{CHANNEL}{channel} +\SetKwData{MONO}{mono output} +\SetKwData{JOINTSTEREO}{joint stereo} +\SetKwData{FALSESTEREO}{false stereo} +\SetKwData{MAGNITUDE}{magnitude} +\SetKwData{WASTEDBPS}{wasted bps} +\SetKwData{SHIFTED}{shifted} +\SetKwData{CRC}{CRC} +\SetKwData{CORRELATED}{correlated} +\SetKwData{MID}{mid} +\SetKwData{SIDE}{side} +\SetKwData{TERMS}{terms} +\SetKwData{DELTAS}{deltas} +\SetKwData{WEIGHTS}{weights} +\SetKwData{SAMPLES}{samples} +\SetKwData{BITSTREAM}{bitstream} +\SetKwData{ENTROPY}{entropy} +\SetKwData{SUBBLOCK}{sub block} +\SetKw{OR}{or} +\SetKwFunction{MAX}{max} +\SetKwFunction{MIN}{min} +\eIf(\tcc*[f]{1 channel block}){$\text{channel count} = 1$ \OR $\text{\CHANNEL}_0 = \text{\CHANNEL}_1$}{ + $\text{\JOINTSTEREO} \leftarrow 0$\; + \eIf{$\text{channel count} = 1$}{ + $\text{\MONO} \leftarrow 1$\; + $\text{\FALSESTEREO} \leftarrow 0$\; + }{ + $\text{\MONO} \leftarrow 0$\; + $\text{\FALSESTEREO} \leftarrow 1$\; + } + $\text{\MAGNITUDE} \leftarrow$ \hyperref[wavpack:maximum_magnitude]{maximum magnitude of $\text{\CHANNEL}_0$}\; + $\text{\WASTEDBPS} \leftarrow$ \hyperref[wavpack:wasted_bps]{wasted bps of $\text{\CHANNEL}_0$}\; + \uIf{$\text{\WASTEDBPS} > 0$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}block size}{ + $\text{\SHIFTED}_{0~i} \leftarrow \lfloor\text{\CHANNEL}_{0~i} \div 2 ^ \text{\WASTEDBPS}\rfloor$\; + } + } + \lElse{ + $\text{\SHIFTED}_0 \leftarrow \text{\CHANNEL}_0$\; + } + $\text{\CRC} \leftarrow$ \hyperref[wavpack:calc_crc]{calculate CRC of $\text{\SHIFTED}_0$}\; +}(\tcc*[f]{2 channel block}){ + $\text{\JOINTSTEREO} \leftarrow 1$\; + $\text{\MONO} \leftarrow \text{\FALSESTEREO} \leftarrow 0$\; + $\text{\MAGNITUDE} \leftarrow \hyperref[wavpack:maximum_magnitude]{\MAX(\text{maximum magnitude of \CHANNEL}_0~,~\text{maximum magnitude of \CHANNEL}_1)}$\; + $\text{\WASTEDBPS} \leftarrow \hyperref[wavpack:wasted_bps]{\MIN(\text{wasted bps of \CHANNEL}_0~,~\text{wasted bps of \CHANNEL}_1)}$\; + \eIf{$\text{\WASTEDBPS} > 0$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}block size}{ + $\text{\SHIFTED}_{0~i} \leftarrow \lfloor\text{\CHANNEL}_{0~i} \div 2 ^ \text{\WASTEDBPS}\rfloor$\; + $\text{\SHIFTED}_{1~i} \leftarrow \lfloor\text{\CHANNEL}_{1~i} \div 2 ^ \text{\WASTEDBPS}\rfloor$\; + } + }{ + $\text{\SHIFTED}_0 \leftarrow \text{\CHANNEL}_0$\; + $\text{\SHIFTED}_1 \leftarrow \text{\CHANNEL}_1$\; + } + $\text{\CRC} \leftarrow$ \hyperref[wavpack:calc_crc]{calculate CRC of $\text{\SHIFTED}_0$ and $\text{\SHIFTED}_1$}\; + $\left.\begin{tabular}{r} + \MID \\ + \SIDE \\ + \end{tabular}\right\rbrace \leftarrow$ + \hyperref[wavpack:calc_joint_stereo]{apply joint stereo} + $\left\lbrace\begin{tabular}{l} + $\text{\SHIFTED}_0$ \\ + $\text{\SHIFTED}_1$ \\ + \end{tabular}\right.$\; +} +$i \leftarrow 0$\; +\If{first block in file}{ + $\text{\SUBBLOCK}_i \leftarrow$ \hyperref[wavpack:write_wave_header]{wave header}\; + $i \leftarrow i + 1$\; +} +\If{$\text{decorrelation passes} > 0$}{ + $\text{\SUBBLOCK}_i \leftarrow$ \hyperref[wavpack:write_decorr_terms]{decorrelation terms sub block from \TERMS and \DELTAS}\; + $\text{\SUBBLOCK}_{i + 1} \leftarrow$ \hyperref[wavpack:write_decorr_weights]{decorrelation weights sub block from \WEIGHTS}\; + $\text{\SUBBLOCK}_{i + 2} \leftarrow$ \hyperref[wavpack:write_decorr_samples]{decorrelation samples sub block from \SAMPLES}\; + $i \leftarrow i + 3$\; +} +\If{$\text{\WASTEDBPS} > 0$}{ + $\text{\SUBBLOCK}_i \leftarrow$ \hyperref[wavpack:write_extended_integers]{extended integers}\; + $i \leftarrow i + 1$\; +} +\If{$\text{total channel count} > 2$}{ + $\text{\SUBBLOCK}_i \leftarrow$ \hyperref[wavpack:write_channel_info]{channel info}\; + $i \leftarrow i + 1$\; +} +\If{sample rate not defined in block header}{ + $\text{\SUBBLOCK}_i \leftarrow$ \hyperref[wavpack:write_sample_rate]{sample rate}\; + $i \leftarrow i + 1$\; +} +\EALGORITHM +} + +%% \begin{figure}[h] +%% \includegraphics{wavpack/figures/typical_block.pdf} +%% \end{figure} + +\clearpage +{\relsize{-1} +\begin{algorithm}[H] +\DontPrintSemicolon +\SetKwData{CHANNEL}{channel} +\SetKwData{CORRELATED}{correlated} +\SetKwData{SHIFTED}{shifted} +\SetKwData{MID}{mid} +\SetKwData{SIDE}{side} +\SetKwData{SUBBLOCK}{sub block} +\SetKwData{WASTEDBPS}{wasted bps} +\SetKwData{BITSTREAM}{bitstream} +\SetKwData{TERMS}{terms} +\SetKwData{DELTAS}{deltas} +\SetKwData{WEIGHTS}{weights} +\SetKwData{SAMPLES}{samples} +\SetKwData{ENTROPY}{entropy} +\SetKw{NOT}{not} +\SetKw{IN}{in} +\SetKw{OR}{or} +$\text{\SUBBLOCK}_i \leftarrow$ \hyperref[wavpack:write_entropy]{entropy variables sub block from \ENTROPY}\; +\eIf(\tcc*[f]{1 channel block}){$\text{channel count} = 1$ \OR $\text{\CHANNEL}_0 = \text{\CHANNEL}_1$}{ + $\left.\begin{tabular}{r} + $\text{\CORRELATED}_0$ \\ + $\text{\WEIGHTS}'$ \\ + $\text{\SAMPLES}'$ \\ + \end{tabular}\right\rbrace \leftarrow$ + \hyperref[wavpack:correlate_channels]{correlate channel} + $\left\lbrace\begin{tabular}{l} + $\text{\SHIFTED}_0$ \\ + \TERMS \\ + \DELTAS \\ + \WEIGHTS \\ + \SAMPLES \\ + \end{tabular}\right.$\; + $\left.\begin{tabular}{r} + \BITSTREAM \\ + $\text{\ENTROPY}'$ \\ + \end{tabular}\right\rbrace \leftarrow$ + \hyperref[wavpack:write_bitstream]{calculate bitstream} + $\left\lbrace\begin{tabular}{l} + $\text{\CORRELATED}_0$ \\ + $\text{\ENTROPY}$ \\ + \end{tabular}\right.$\; +}(\tcc*[f]{2 channel block}){ + $\left.\begin{tabular}{r} + $\text{\CORRELATED}_0$ \\ + $\text{\CORRELATED}_1$ \\ + $\text{\WEIGHTS}'$ \\ + $\text{\SAMPLES}'$ \\ + \end{tabular}\right\rbrace \leftarrow$ + \hyperref[wavpack:correlate_channels]{correlate channels} + $\left\lbrace\begin{tabular}{l} + \MID \\ + \SIDE \\ + \TERMS \\ + \DELTAS \\ + \WEIGHTS \\ + \SAMPLES \\ + \end{tabular}\right.$\; + $\left.\begin{tabular}{r} + \BITSTREAM \\ + $\text{\ENTROPY}'$ \\ + \end{tabular}\right\rbrace \leftarrow$ + \hyperref[wavpack:write_bitstream]{calculate bitstream} + $\left\lbrace\begin{tabular}{l} + $\text{\CORRELATED}_0$ \\ + $\text{\CORRELATED}_1$ \\ + $\text{\ENTROPY}$ \\ + \end{tabular}\right.$\; +} +$\text{\SUBBLOCK}_{i + 1} \leftarrow$ bitstream sub block from \BITSTREAM\; +$i \leftarrow i + 2$\; +\BlankLine +\hyperref[wavpack:write_block_header]{write block header to block data} +$\left\lbrace\begin{tabular}{l} +\textsf{total sub blocks size} \\ +\textsf{block index} \\ +\textsf{block samples} \\ +\textsf{bits per sample} \\ +\textsf{channel count} \\ +\textsf{joint stereo} \\ +\textsf{correlation pass count} \\ +\textsf{wasted bps} \\ +\textsf{initial block} \\ +\textsf{final block} \\ +\textsf{magnitude} \\ +\textsf{sample rate} \\ +\textsf{false stereo} \\ +\textsf{CRC} \\ +\end{tabular}\right.$\; +\For{$j \leftarrow 0$ \emph{\KwTo}i}{ + write $\text{\SUBBLOCK}_j$ to block data\; +} +\Return $\left\lbrace\begin{tabular}{l} +block data \\ +$\text{\WEIGHTS}'$ \\ +$\text{\SAMPLES}'$ \\ +$\text{\ENTROPY}'$ \\ +\end{tabular}\right.$ +\end{algorithm} +} + +\clearpage + +\subsection{Calculating Maximum Magnitude} +\label{wavpack:maximum_magnitude} +{\relsize{-1} +\ALGORITHM{a list of signed PCM samples for a single channel}{an unsigned integer} +\SetKwData{MAXMAGNITUDE}{maximum magnitude} +\SetKwData{SAMPLE}{sample} +\SetKwFunction{MAX}{max} +\SetKwFunction{BITS}{bits} +$\text{\MAXMAGNITUDE} \leftarrow 0$\; +\For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ + $\text{\MAXMAGNITUDE} \leftarrow \MAX(\BITS(|\text{\SAMPLE}_i|)~,~\text{\MAXMAGNITUDE})$\; +} +\Return \MAXMAGNITUDE\; +\EALGORITHM +where the \texttt{bits} function is defined as: +\begin{equation*} +\texttt{bits}(x) = +\begin{cases} +0 & \text{if } x = 0 \\ +1 + \texttt{bits}(\lfloor x \div 2 \rfloor) & \text{if } x > 0 +\end{cases} +\end{equation*} +} +\subsubsection{Maximum Magnitude Example} +\begin{table}[h] +{\relsize{-1} +\begin{tabular}{r|rrrrrrrrrr} +$i$ & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\ +\hline +$\text{sample}_i$ & 0 & 16 & 31 & 44 & 54 & 61 & 64 & 63 & 58 & 49 \\ +$\texttt{bits}(|\text{sample}_i|)$ & 0 & 5 & 5 & 6 & 6 & 6 & 7 & 6 & 6 & 6 +\end{tabular} +} +\end{table} +\par +\noindent +for a maximum magnitude of 7. + +\subsection{Calculating Wasted Bits Per Sample} +\label{wavpack:wasted_bps} +{\relsize{-1} +\ALGORITHM{a list of signed PCM samples for a single channel}{an unsigned integer} +\SetKwData{WASTEDBPS}{wasted bps} +\SetKwData{SAMPLE}{sample} +\SetKwFunction{MIN}{min} +\SetKwFunction{WASTED}{wasted} +$\text{\WASTEDBPS} \leftarrow \infty$\tcc*[r]{maximum unsigned integer} +\For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ + $\text{\WASTEDBPS} \leftarrow \MIN(\WASTED(\text{\SAMPLE}_i)~,~\text{\WASTEDBPS})$\; + \If{$\text{\WASTEDBPS} = 0$}{ + \Return 0\; + } +} +\eIf(\tcc*[f]{all samples are 0}){$\WASTEDBPS = \infty$}{ + \Return 0\; +}{ + \Return \WASTEDBPS\; +} +\EALGORITHM +where the \texttt{wasted} function is defined as: +\begin{equation*} +\texttt{wasted}(x) = +\begin{cases} +\infty & \text{if } x = 0 \\ +0 & \text{if } x \bmod 2 = 1 \\ +1 + \texttt{wasted}(x \div 2) & \text{if } x \bmod 2 = 0 \\ +\end{cases} +\end{equation*} +} +\subsubsection{Wasted Bits Example} +\begin{table}[h] +{\relsize{-1} +\begin{tabular}{r|rrrrrrrrrr} +$i$ & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\ +\hline +$\text{sample}_i$ & 0 & 16 & 31 & 44 & 54 & 61 & 64 & 63 & 58 & 49 \\ +$\texttt{wasted}(\text{sample}_i)$ & $\infty$ & 4 & 0 & 2 & 1 & 0 & 6 & 0 & 1 & 0 \\ +\end{tabular} +} +\end{table} +\par +\noindent +for a wasted bps of 0 (which is typical). + +\subsection{Calculating CRC} +\label{wavpack:calc_crc} +\ALGORITHM{one or two channels of signed audio samples}{an unsigned 32-bit CRC integer} +\SetKwData{MONO}{mono output} +\SetKwData{CRC}{CRC} +\SetKwData{LCRC}{LCRC} +\SetKwData{SCRC}{SCRC} +\SetKwData{CHANNEL}{channel} +$\text{\CRC}_{-1} \leftarrow \texttt{0xFFFFFFFF}$\; +\For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ + \eIf{$\text{\MONO} = 0$}{ + $\text{\LCRC}_i \leftarrow (3 \times \text{\CRC}_{i - 1}) + \text{\CHANNEL}_{0~i}$\tcc*[r]{calculate signed CRC of left channel} + $\text{\SCRC}_i \leftarrow (3 \times \text{\LCRC}_{i - 1}) + \text{\CHANNEL}_{1~i}$\tcc*[r]{calculate signed CRC of right channel} + }{ + $\text{\SCRC}_i \leftarrow (3 \times \text{\CRC}_{i - 1}) + \text{\CHANNEL}_{0~i}$\tcc*[r]{calculate signed CRC of channel} + } + \BlankLine + \eIf(\tcc*[f]{convert signed CRC to unsigned, 32-bit integer}){$\text{\SCRC}_i \geq 0$}{ + $\text{\CRC}_i \leftarrow \text{\SCRC}_i \bmod \texttt{0x100000000}$\; + }{ + $\text{\CRC}_i \leftarrow (2 ^ {32} - (-\text{\SCRC}_i)) \bmod \texttt{0x100000000}$\; + } +} +\Return $\text{\CRC}_{\text{sample count} - 1}$\; +\EALGORITHM + +\subsubsection{Checksum Calculation Example} +{\relsize{-1} +\begin{tabular}{|r|r|r||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|} +$i$ & $\textsf{channel}_{0~i}$ & $\textsf{channel}_{1~i}$ & \textsf{LCRC}_i & \textsf{SCRC}_i & \textsf{CRC}_i \\ +\hline +0 & 0 & 64 & +(3 \times \texttt{FFFFFFFF}) + 0 = \texttt{2FFFFFFFD} & +(3 \times \texttt{2FFFFFFFD}) + 64 = \texttt{900000037} & +\texttt{00000037} \\ +1 & 16 & 62 & +(3 \times \texttt{00000037}) + 16 = \texttt{000000B5} & +(3 \times \texttt{000000B5}) + 62 = \texttt{0000025D} & +\texttt{0000025D} \\ +2 & 31 & 56 & +(3 \times \texttt{0000025D}) + 31 = \texttt{00000736} & +(3 \times \texttt{00000736}) + 56 = \texttt{000015DA} & +\texttt{000015DA} \\ +3 & 44 & 47 & +(3 \times \texttt{000015DA}) + 44 = \texttt{000041BA} & +(3 \times \texttt{000041BA}) + 47 = \texttt{0000C55D} & +\texttt{0000C55D} \\ +4 & 54 & 34 & +(3 \times \texttt{0000C55D}) + 54 = \texttt{0002504D} & +(3 \times \texttt{0002504D}) + 34 = \texttt{0006F109} & +\texttt{0006F109} \\ +5 & 61 & 20 & +(3 \times \texttt{0006F109}) + 61 = \texttt{0014D358} & +(3 \times \texttt{0014D358}) + 20 = \texttt{003E7A1C} & +\texttt{003E7A1C} \\ +6 & 64 & 4 & +(3 \times \texttt{003E7A1C}) + 64 = \texttt{00BB6E94} & +(3 \times \texttt{00BB6E94}) + 4 = \texttt{02324BC0} & +\texttt{02324BC0} \\ +7 & 63 & -12 & +(3 \times \texttt{02324BC0}) + 63 = \texttt{0696E37F} & +(3 \times \texttt{0696E37F}) - 12 = \texttt{13C4AA71} & +\texttt{13C4AA71} \\ +8 & 58 & -27 & +(3 \times \texttt{13C4AA71}) + 58 = \texttt{3B4DFF8D} & +(3 \times \texttt{3B4DFF8D}) - 27 = \texttt{B1E9FE8C} & +\texttt{B1E9FE8C} \\ +9 & 49 & -41 & +(3 \times \texttt{B1E9FE8C}) + 49 = \texttt{215BDFBD5} & +(3 \times \texttt{215BDFBD5}) - 41 = \texttt{64139F356} & +\texttt{4139F356} \\ +\end{tabular} +} +\vskip 1em +\par +\noindent +Resulting in a final CRC of \texttt{0x4139F356} + +\clearpage + +\subsection{Joint Stereo Conversion} +\label{wavpack:calc_joint_stereo} +\ALGORITHM{left and right channels of signed integers}{mid and side channels of signed integers} +\SetKwData{LEFT}{left} +\SetKwData{RIGHT}{right} +\SetKwData{MID}{mid} +\SetKwData{SIDE}{side} +\For{$i \leftarrow 0$ \emph{\KwTo}sample count}{ + $\text{\MID}_i \leftarrow \text{\LEFT}_i - \text{\RIGHT}_i$\; + $\text{\SIDE}_i \leftarrow \lfloor(\text{\LEFT}_i + \text{\RIGHT}_i) \div 2\rfloor$\; +} +\Return $\left\lbrace\begin{tabular}{l} +\MID \\ +\SIDE \\ +\end{tabular}\right.$\; +\EALGORITHM + +\subsubsection{Joint Stereo Example} +\begin{table}[h] +{\relsize{-1} +\begin{tabular}{|r|r|r||>{$}r<{$}|>{$}r<{$}|} +$i$ & $\textsf{left}_i$ & $\textsf{right}_i$ & \textsf{mid}_i & \textsf{side}_i \\ +\hline +0 & 0 & 64 & 0 - 64 = -64 & \lfloor(0 + 64) \div 2\rfloor = 32 \\ +1 & 16 & 62 & 16 - 62 = -46 & \lfloor(16 + 62) \div 2\rfloor = 39 \\ +2 & 31 & 56 & 31 - 56 = -25 & \lfloor(31 + 56) \div 2\rfloor = 43 \\ +3 & 44 & 47 & 44 - 47 = -3 & \lfloor(44 + 47) \div 2\rfloor = 45 \\ +4 & 54 & 34 & 54 - 34 = 20 & \lfloor(54 + 34) \div 2\rfloor = 44 \\ +5 & 61 & 20 & 61 - 20 = 41 & \lfloor(61 + 20) \div 2\rfloor = 40 \\ +6 & 64 & 4 & 64 - 4 = 60 & \lfloor(64 + 4) \div 2\rfloor = 34 \\ +7 & 63 & -12 & 63 - -12 = 75 & \lfloor(63 + -12) \div 2\rfloor = 25 \\ +8 & 58 & -27 & 58 - -27 = 85 & \lfloor(58 + -27) \div 2\rfloor = 15 \\ +9 & 49 & -41 & 49 - -41 = 90 & \lfloor(49 + -41) \div 2\rfloor = 4 \\ +\end{tabular} +} +\end{table} + +\clearpage + +\input{wavpack/encode/correlation} + +\clearpage + +\input{wavpack/encode/terms} + +\clearpage + +\input{wavpack/encode/weights} + +\clearpage + +\input{wavpack/encode/samples} + +\clearpage + +\input{wavpack/encode/entropy} + +\clearpage + +\input{wavpack/encode/bitstream} + +\clearpage + +\subsection{Writing RIFF WAVE Header and Footer} +\label{wavpack:write_wave_header} +\begin{figure}[h] + \includegraphics{wavpack/figures/pcm_sandwich.pdf} +\end{figure} + + +\subsection{Writing MD5 Sum} +\label{wavpack:write_md5} +MD5 sum is calculated as if the PCM data had been read from +a wave file's \texttt{data} chunk. +That is, the samples are converted to little-endian format +and are signed if the stream's bits-per-sample is greater than 8. +\begin{figure}[h] + \includegraphics{wavpack/figures/md5sum.pdf} +\end{figure} + +\subsection{Writing Extended Integers} +\label{wavpack:write_extended_integers} +\begin{figure}[h] + \includegraphics{wavpack/figures/extended_integers.pdf} +\end{figure} + +\clearpage + +\subsection{Writing Channel Info} +\label{wavpack:write_channel_info} +\begin{figure}[h] + \includegraphics{wavpack/figures/channel_info.pdf} +\end{figure} + + +\subsection{Writing Sample Rate} +\label{wavpack:write_sample_rate} +\begin{figure}[h] + \includegraphics{wavpack/figures/sample_rate.pdf} +\end{figure} + +\clearpage + +\subsection{Writing Sub Block} +{\relsize{-1} +\ALGORITHM{metadata function, nondecoder data flag, sub block data}{sub block header data} +\SetKwData{DATA}{sub block data} +\SetKwFunction{LEN}{len} +$\text{metadata function} \rightarrow$ \WRITE 5 unsigned bits\; +$\text{nondecoder data} \rightarrow$ \WRITE 1 unsigned bit\; +$\LEN(\text{\DATA}) \bmod 2 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{actual size 1 less} +\eIf(\tcc*[f]{large block}){$\LEN(\text{\DATA}) > 255 \times 2$}{ + $1 \rightarrow$ \WRITE 1 unsigned bit\; + $\lceil\LEN(\text{\DATA}) \div 2\rceil \rightarrow$ \WRITE 24 unsigned bits\; +}{ + $0 \rightarrow$ \WRITE 1 unsigned bit\; + $\lceil\LEN(\text{\DATA}) \div 2\rceil \rightarrow$ \WRITE 8 unsigned bits\; +} +write \DATA\; +\If{$\LEN(\text{\DATA}) \bmod 2 = 1$}{ + $0 \rightarrow$ \WRITE 8 unsigned bits\; +} +\EALGORITHM +} + +\begin{figure}[h] +\includegraphics{wavpack/figures/block_header2.pdf} +\end{figure} + +\clearpage + +\subsection{Writing Block Header} +\label{wavpack:write_block_header} +{\relsize{-1} +\ALGORITHM{total sub blocks size, block index, block samples, bits per sample, channel count, joint stereo, correlation pass count, wasted BPS, initial block, final block, maximum magnitude, sample rate, false stereo, CRC}{block header data} +\SetKwData{SUBBLOCKSSIZE}{sub blocks size} +\SetKwData{BLOCKINDEX}{block index} +\SetKwData{BLOCKSAMPLES}{block samples} +\SetKwData{BITSPERSAMPLE}{bits per sample} +\SetKwData{CHANNELCOUNT}{channel count} +\SetKwData{PASSES}{correlation passes} +\SetKwData{WASTEDBPS}{wasted BPS} +\SetKwData{JOINTSTEREO}{joint stereo} +\SetKwData{INITIALBLOCK}{initial block} +\SetKwData{FINALBLOCK}{final block} +\SetKwData{MAXMAGNITUDE}{maximum magnitude} +\SetKwData{FALSESTEREO}{false stereo} +$\texttt{"wvpk"} \rightarrow$ \WRITE 4 bytes\; +$\SUBBLOCKSSIZE + 24 \rightarrow$ \WRITE 32 unsigned bits\; +$\texttt{0x0410} \rightarrow$ \WRITE 16 unsigned bits\tcc*[r]{version} +$0 \rightarrow$ \WRITE 8 unsigned bits\tcc*[r]{track number} +$0 \rightarrow$ \WRITE 8 unsigned bits\tcc*[r]{index number} +$\texttt{0xFFFFFFFF} \rightarrow$ \WRITE 32 unsigned bits\tcc*[r]{total samples placeholder} +$\BLOCKINDEX \rightarrow$ \WRITE 32 unsigned bits\; +$\BLOCKSAMPLES \rightarrow$ \WRITE 32 unsigned bits\; +$\text{\BITSPERSAMPLE} \div 8 - 1 \rightarrow$ \WRITE 2 unsigned bits\; +$2 - \text{\CHANNELCOUNT} \rightarrow$ \WRITE 1 unsigned bit\; +$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{hybrid mode} +\eIf(\tcc*[f]{joint stereo}){$\text{\CHANNELCOUNT} = 2$}{ + $1 \rightarrow$ \WRITE 1 unsigned bit +}{ + $0 \rightarrow$ \WRITE 1 unsigned bit +} +\eIf(\tcc*[f]{cross channel decorrelation}){$\text{\PASSES} > 5$}{ + $1 \rightarrow$ \WRITE 1 unsigned bit\; +}{ + $0 \rightarrow$ \WRITE 1 unsigned bit\; +} +$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{hybrid noise shaping} +$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{floating point data} +\eIf(\tcc*[f]{extended size integers}){$\WASTEDBPS > 0$}{ + $1 \rightarrow$ \WRITE 1 unsigned bit\; +}{ + $0 \rightarrow$ \WRITE 1 unsigned bit\; +} +$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{hybrid controls bitrate} +$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{hybrid noise balanced} +$\INITIALBLOCK \rightarrow$ \WRITE 1 unsigned bit\; +$\FINALBLOCK \rightarrow$ \WRITE 1 unsigned bit\; +$0 \rightarrow$ \WRITE 5 unsigned bits\tcc*[r]{left shift data} +$\MAXMAGNITUDE \rightarrow$ \WRITE 5 unsigned bits\; +$\textit{encoded sample rate} \rightarrow$ \WRITE 4 unsigned bits\; +\begin{tabular}{rr|rr|rr} + sample rate & \textit{encoded} & + sample rate & \textit{encoded} & + sample rate & \textit{encoded} \\ + \hline + 6000 Hz & \texttt{0} & + 22050 Hz & \texttt{6} & + 64000 Hz & \texttt{11} \\ + 8000 Hz & \texttt{1} & + 24000 Hz & \texttt{7} & + 88200 Hz & \texttt{12} \\ + 9600 Hz & \texttt{2} & + 32000 Hz & \texttt{8} & + 96000 Hz & \texttt{13} \\ + 11025 Hz & \texttt{3} & + 44100 Hz & \texttt{9} & + 192000 Hz & \texttt{14} \\ + 12000 Hz & \texttt{4} & + 48000 Hz & \texttt{10} & + otherwise & \texttt{15} \\ + 16000 Hz & \texttt{5} \\ +\end{tabular} \\ +$0 \rightarrow$ \WRITE 2 unsigned bits\tcc*[r]{reserved} +$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{use IIR} +$\FALSESTEREO \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{if $\text{channel}_0 = \text{channel}_1$} +$0 \rightarrow$ \WRITE 1 unsigned bit\tcc*[r]{reserved} +$CRC \rightarrow$ \WRITE 32 unsigned bits\; +\EALGORITHM +}
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/encode/bitstream.tex
Added
@@ -0,0 +1,637 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\subsection{Encoding Bitstream} +\label{wavpack:write_bitstream} +{\relsize{-1} +\ALGORITHM{channel count, entropy values, a list of signed residual values per channel}{bitstream sub block data} +\SetKwData{UNDEFINED}{undefined} +\SetKwData{RESIDUAL}{residual} +\SetKwData{ENTROPY}{entropy} +\SetKwData{BLOCKSAMPLES}{block samples} +\SetKwData{CHANNELCOUNT}{channel count} +\SetKwData{ZEROES}{zeroes} +\SetKwData{OFFSET}{offset} +\SetKwData{ADD}{add} +\SetKwData{SIGN}{sign} +\SetKw{AND}{and} +\SetKw{OR}{or} +\SetKw{IS}{is} +\SetKwFunction{FLUSH}{flush} +\SetKwFunction{UNARYUNDEFINED}{unary\_undefined} +\SetKwFunction{ENCODERESIDUAL}{encode\_residual} +\SetKwFunction{UNARY}{unary} +\SetKwFunction{WRITERESIDUAL}{write\_residual} +$u_{(-2)} \leftarrow \text{\UNDEFINED}$\; +$\text{\ZEROES}_{(-1)} \leftarrow m_{(-1)} \leftarrow \text{\OFFSET}_{(-1)} \leftarrow \text{\ADD}_{(-1)} \leftarrow \text{\SIGN}_{(-1)} \leftarrow \text{\UNDEFINED}$\tcc*[r]{buffered $\text{residual}_{i - 1}$} +$i \leftarrow 0$\; +\BlankLine +\While{$i < (\text{\BLOCKSAMPLES} \times \text{\CHANNELCOUNT})$}{ + $c \leftarrow i \bmod \text{\CHANNELCOUNT}$\; + $r_i \leftarrow \text{\RESIDUAL}_{c~\lfloor i \div \text{\CHANNELCOUNT}\rfloor}$\; + \eIf{$(\text{\ENTROPY}_{0~0} < 2)$ \AND $(\text{\ENTROPY}_{1~0} < 2)$ \AND $\UNARYUNDEFINED(u_{i - 2}~,~m_{i - 1})$}{ + \eIf(\tcc*[f]{in a block of zeroes}){$(\text{\ZEROES}_{i - 1} \neq \text{\UNDEFINED})$ \AND $(m_{i - 1} = \text{\UNDEFINED})$}{ + \eIf(\tcc*[f]{continue block of zeroes}){$r_i = 0$}{ + $\text{\ZEROES}_i \leftarrow \text{\ZEROES}_{i - 1} + 1$\; + $(u_{i - 1}~,~m_i~,~\text{\OFFSET}_i~,~\text{\ADD}_i~,~\text{\SIGN}_i) \leftarrow (u_{i - 2}~,~m_{i - 1}~,~\text{\OFFSET}_{i - 1}~,~\text{\ADD}_{i - 1}~,~\text{\SIGN}_{i - 1})$\; + }(\tcc*[f]{end block of zeroes}){ + $\text{\ZEROES}_i \leftarrow \text{\ZEROES}_{i - 1}$\; + $\left.\begin{tabular}{r} + $m_i$ \\ + $\text{\OFFSET}_i$ \\ + $\text{\ADD}_i$ \\ + $\text{\SIGN}_i$ \\ + $\text{\ENTROPY}'_{c}$ \\ + \end{tabular}\right\rbrace \leftarrow + \hyperref[wavpack:encode_residual]{\ENCODERESIDUAL(r_i~,~\text{\ENTROPY}_{c})}$\; + } + }(\tcc*[f]{start a new block of zeroes}){ + \eIf{$r_i = 0$}{ + $\text{\ZEROES}_i \leftarrow 1$\; + $m_i \leftarrow \text{\OFFSET}_i \leftarrow \text{\ADD}_i \leftarrow \text{\SIGN}_i \leftarrow \text{\UNDEFINED}$\; + $u_{i - 1} \leftarrow \hyperref[wavpack:flush_residual]{\FLUSH(u_{i - 2}~,~\text{\ZEROES}_{i - 1}~,~m_{i - 1}~,~\text{\OFFSET}_{i - 1}~,~\text{\ADD}_{i - 1}~,~\text{\SIGN}_{i - 1}~,~0)}$\; + $\text{\ENTROPY}_{0~0} \leftarrow \text{\ENTROPY}_{0~1} \leftarrow \text{\ENTROPY}_{0~2} \leftarrow \text{\ENTROPY}_{1~0} \leftarrow \text{\ENTROPY}_{1~1} \leftarrow \text{\ENTROPY}_{1~2} \leftarrow 0$\; + }(\tcc*[f]{false-alarm block of zeroes}){ + $\text{\ZEROES}_i \leftarrow 0$\; + $\left.\begin{tabular}{r} + $m_i$ \\ + $\text{\OFFSET}_i$ \\ + $\text{\ADD}_i$ \\ + $\text{\SIGN}_i$ \\ + $\text{\ENTROPY}'_{c}$ \\ + \end{tabular}\right\rbrace \leftarrow + \hyperref[wavpack:encode_residual]{\ENCODERESIDUAL(r_i~,~\text{\ENTROPY}_{c})}$\; + $u_{i - 1} \leftarrow \hyperref[wavpack:flush_residual]{\FLUSH(u_{i - 2}~,~\text{\ZEROES}_{i - 1}~,~m_{i - 1}~,~\text{\OFFSET}_{i - 1}~,~\text{\ADD}_{i - 1}~,~\text{\SIGN}_{i - 1}~,~m_i)}$\; + } + } + }(\tcc*[f]{encode regular residual}){ + $\text{\ZEROES}_i \leftarrow \text{\UNDEFINED}$\; + $\left.\begin{tabular}{r} + $m_i$ \\ + $\text{\OFFSET}_i$ \\ + $\text{\ADD}_i$ \\ + $\text{\SIGN}_i$ \\ + $\text{\ENTROPY}'_{c}$ \\ + \end{tabular}\right\rbrace \leftarrow + \hyperref[wavpack:encode_residual]{\ENCODERESIDUAL(r_i~,~\text{\ENTROPY}_{c})}$\; + $u_{i - 1} \leftarrow \hyperref[wavpack:flush_residual]{\FLUSH(u_{i - 2}~,~\text{\ZEROES}_{i - 1}~,~m_{i - 1}~,~\text{\OFFSET}_{i - 1}~,~\text{\ADD}_{i - 1}~,~\text{\SIGN}_{i - 1}~,~m_i)}$\; + } + $i \leftarrow i + 1$\; +} +\BlankLine +\tcc{flush final residual} +$u_{i - 1} \leftarrow \hyperref[wavpack:flush_residual]{\FLUSH(u_{i - 2}~,~\text{\ZEROES}_{i - 1}~,~m_{i - 1}~,~\text{\OFFSET}_{i - 1}~,~\text{\ADD}_{i - 1}~,~\text{\SIGN}_{i - 1}~,~0)}$\; +\BlankLine +\Return encoded bitstream sub block\; +\EALGORITHM + +\clearpage + +\subsubsection{\texttt{unary\_undefined} Function} +\ALGORITHM{$u_{i - 2}$, $m_{i - 1}$}{whether $u_{i - 1}$ is defined} +\SetKw{AND}{and} +\SetKw{IS}{is} +\SetKw{NOT}{not} +\SetKwData{TRUE}{true} +\SetKwData{FALSE}{false} +\SetKwData{UNDEFINED}{undefined} +\uIf{$m_{i - 1} = \text{\UNDEFINED}$}{ + \Return \TRUE\tcc*[r]{$u_{i - 1}$ \IS \UNDEFINED} +} +\uElseIf{$(m_{i - 1} = 0)$ \AND $(u_{i - 2} \neq \UNDEFINED)$ \AND $(u_{i - 2} \bmod 2 = 0)$}{ + \Return \TRUE\tcc*[r]{$u_{i - 1}$ \IS \UNDEFINED} +} +\Else{ + \Return \FALSE\tcc*[r]{$u_{i - 1}$ \IS \NOT \UNDEFINED} +} +\EALGORITHM +} + +\subsubsection{Writing Modified Elias Gamma Code} +\label{wavpack:write_egc} +\ALGORITHM{a non-negative integer value}{an encoded value} +\eIf{$v \leq 1$}{ + $v \rightarrow$ \WUNARY with stop bit 0\; +}{ + $t \leftarrow \lfloor\log_2(v)\rfloor + 1$\; + $t \rightarrow$ \WUNARY with stop bit 0\; + $v \bmod 2 ^ {t - 1} \rightarrow$ \WRITE $(t - 1)$ unsigned bits\; +} +\Return encoded value\; +\EALGORITHM + +\begin{figure}[h] + \includegraphics{wavpack/figures/residuals.pdf} +\end{figure} + +\clearpage + +\subsubsection{\texttt{encode\_residual} Function} +\label{wavpack:encode_residual} +{\relsize{-1} + \ALGORITHM{signed residual value, entropy variables for channel}{$\text{m}_i$, $\text{offset}_i$, $\text{add}_i$, $\text{sign}_i$; updated entropy values} + \SetKwData{RESIDUAL}{residual} + \SetKwData{UNSIGNED}{unsigned} + \SetKwData{SIGN}{sign} + \SetKwData{ENTROPY}{entropy} + \SetKwData{MEDIAN}{median} + \SetKwData{MED}{m} + \SetKwData{ADD}{add} + \SetKwData{OFFSET}{offset} + \eIf{$\RESIDUAL \geq 0$}{ + $\UNSIGNED \leftarrow \RESIDUAL$\; + $\text{\SIGN}_i \leftarrow 0$\; + }{ + $\UNSIGNED \leftarrow -\RESIDUAL - 1$\; + $\text{\SIGN}_i \leftarrow 1$\; + } + $\text{\MEDIAN}_{c~0} \leftarrow \lfloor\text{\ENTROPY}_{c~0} \div 2 ^ 4\rfloor + 1$\; + $\text{\MEDIAN}_{c~1} \leftarrow \lfloor\text{\ENTROPY}_{c~1} \div 2 ^ 4\rfloor + 1$\; + $\text{\MEDIAN}_{c~2} \leftarrow \lfloor\text{\ENTROPY}_{c~2} \div 2 ^ 4\rfloor + 1$\; + \uIf{$\UNSIGNED < \text{\MEDIAN}_{c~0}$}{ + $\text{\MED}_i \leftarrow 0$\; + $\text{\OFFSET}_i \leftarrow \text{\UNSIGNED}$\tcc*[r]{offset is unsigned - base} + $\text{\ADD}_i \leftarrow \text{\MEDIAN}_{c~0} - 1$\; + $\text{\ENTROPY}_{c~0} \leftarrow \text{\ENTROPY}_{c~0} - \lfloor(\text{\ENTROPY}_{c~0} + 126) \div 128\rfloor \times 2$\; + } + \uElseIf{$(\UNSIGNED - \text{\MEDIAN}_{c~0}) < \text{\MEDIAN}_{c~1}$}{ + $\text{\MED}_i \leftarrow 1$\; + $\text{\OFFSET}_i \leftarrow \text{\UNSIGNED} - \text{\MEDIAN}_{c~0}$\; + $\text{\ADD}_i \leftarrow \text{\MEDIAN}_{c~1} - 1$\; + $\text{\ENTROPY}_{c~0} \leftarrow \text{\ENTROPY}_{c~0} + \lfloor(\text{\ENTROPY}_{c~0} + 128) \div 128\rfloor \times 5$\; + $\text{\ENTROPY}_{c~1} \leftarrow \text{\ENTROPY}_{c~1} - \lfloor(\text{\ENTROPY}_{c~1} + 62) \div 64\rfloor \times 2$\; + } + \uElseIf{$(\UNSIGNED - (\text{\MEDIAN}_{c~0} + \text{\MEDIAN}_{c~1})) < \text{\MEDIAN}_{c~2}$}{ + $\text{\MED}_i \leftarrow 2$\; + $\text{\OFFSET}_i \leftarrow \text{\UNSIGNED} - (\text{\MEDIAN}_{c~0} + \text{\ENTROPY}_{c~1})$\; + $\text{\ADD}_i \leftarrow \text{\MEDIAN}_{c~2} - 1$\; + $\text{\ENTROPY}_{c~0} \leftarrow \text{\ENTROPY}_{c~0} + \lfloor(\text{\ENTROPY}_{c~0} + 128) \div 128\rfloor \times 5$\; + $\text{\ENTROPY}_{c~1} \leftarrow \text{\ENTROPY}_{c~1} + \lfloor(\text{\ENTROPY}_{c~1} + 64) \div 64\rfloor \times 5$\; + $\text{\ENTROPY}_{c~2} \leftarrow \text{\ENTROPY}_{c~2} - \lfloor(\text{\ENTROPY}_{c~2} + 30) \div 32\rfloor \times 2$\; + } + \Else{ + $\text{\MED}_i \leftarrow \lfloor(\UNSIGNED - (\text{\MEDIAN}_{c~0} + \text{\MEDIAN}_{c~1})) \div \text{\MEDIAN}_{c~2}\rfloor + 2$\; + $\text{\OFFSET}_i \leftarrow \text{\UNSIGNED} - (\text{\MEDIAN}_{c~0} + \text{\MEDIAN}_{c~1} + ((\text{\MED}_i - 2) \times \text{\MEDIAN}_{c~2}))$\; + $\text{\ADD}_i \leftarrow \text{\MEDIAN}_{c~2} - 1$\; + $\text{\ENTROPY}_{c~0} \leftarrow \text{\ENTROPY}_{c~0} + \lfloor(\text{\ENTROPY}_{c~0} + 128) \div 128\rfloor \times 5$\; + $\text{\ENTROPY}_{c~1} \leftarrow \text{\ENTROPY}_{c~1} + \lfloor(\text{\ENTROPY}_{c~1} + 64) \div 64\rfloor \times 5$\; + $\text{\ENTROPY}_{c~2} \leftarrow \text{\ENTROPY}_{c~2} + \lfloor(\text{\ENTROPY}_{c~2} + 32) \div 32\rfloor \times 5$\; + } + \Return $\left\lbrace\begin{tabular}{l} + $\text{\MED}_i$ \\ + $\text{\OFFSET}_i$ \\ + $\text{\ADD}_i$ \\ + $\text{\SIGN}_i$ \\ + $\text{\ENTROPY}_c$ \\ + \end{tabular}\right.$\; + \EALGORITHM +} + +\clearpage + +\subsubsection{\texttt{flush} Function} +\label{wavpack:flush_residual} +{\relsize{-1} + \ALGORITHM{$u_{i - 2}$, $\text{zeroes}_{i - 1}$, $m_{i - 1}$, $\text{offset}_{i - 1}$, $\text{add}_{i - 1}$, $\text{sign}_{i - 1}$, $m_i$}{$u_{i - 1}$} + \SetKwData{UNDEFINED}{undefined} + \SetKwData{ZEROES}{zeroes} + \SetKwData{OFFSET}{offset} + \SetKwData{ADD}{add} + \SetKwData{SIGN}{sign} + \SetKwFunction{UNARY}{unary} + \SetKwFunction{WRITERESIDUAL}{write\_residual} + \SetKw{AND}{and} + \SetKw{OR}{or} + \If{$\text{\ZEROES}_{i - 1} \neq \text{\UNDEFINED}$}{ + write $\text{\ZEROES}_{i - 1}$ \hyperref[wavpack:write_egc]{in modified Elias gamma code}\; + } + \eIf{$m_{i - 1} \neq \text{\UNDEFINED}$}{ + \tcc{calculate $\text{unary}_{i - 1}$ value for $m_{i - 1}$ based on $m_i$} + \uIf{$(m_{i - 1} > 0)$ \AND $(m_i > 0)$}{ + \eIf{$(u_{i - 2} = \UNDEFINED)$ \OR $(u_{i - 2} \bmod 2 = 0)$}{ + $u_{i - 1} \leftarrow (m_{i - 1} \times 2) + 1$\; + }{ + $u_{i - 1} \leftarrow (m_{i - 1} \times 2) - 1$\; + } + } + \uElseIf{$(m_{i - 1} = 0)$ \AND $(m_i > 0)$}{ + \eIf{$(u_{i - 2} = \UNDEFINED)$ \OR $(u_{i - 2} \bmod 2 = 1)$}{ + $u_{i - 1} \leftarrow 1$\; + }{ + $u_{i - 1} \leftarrow \UNDEFINED$\; + } + } + \uElseIf{$(m_{i - 1} > 0)$ \AND $(m_i = 0)$}{ + \eIf{$(u_{i - 2} = \UNDEFINED)$ \OR $(u_{i - 2} \bmod 2 = 0)$}{ + $u_{i - 1} \leftarrow m_{i - 1} \times 2$\; + }{ + $u_{i - 1} \leftarrow (m_{i - 1} - 1) \times 2$\; + } + } + \ElseIf{$(m_{i - 1} = 0)$ \AND $(m_i = 0)$}{ + \eIf{$(u_{i - 2} = \UNDEFINED)$ \OR $(u_{i - 2} \bmod 2 = 1)$}{ + $u_{i - 1} \leftarrow 0$\; + }{ + $u_{i - 1} \leftarrow \UNDEFINED$\; + } + } + \BlankLine + \tcc{write $\text{residual}_{i - 1}$ to disk using $\text{unary}_{i - 1}$} + \If{$u_{i - 1} \neq \UNDEFINED$}{ + \eIf{$u_{i - 1} < 16$}{ + $u_{i - 1} \rightarrow$ \WUNARY with stop bit 0\; + }{ + $16 \rightarrow$ \WUNARY with stop bit 0\; + $u_{i - 1} - 16 \rightarrow$ \hyperref[wavpack:write_egc]{write in modified Elias gamma code}\; + } + } + \If{$\text{\ADD}_{i - 1} > 0$}{ + $p \leftarrow \lfloor\log_2(\text{\ADD}_{i - 1})\rfloor$\; + $e \leftarrow 2 ^ {p + 1} - \text{\ADD}_{i - 1} - 1$\; + \eIf{$\text{\OFFSET}_{i - 1} < e$}{ + $r \leftarrow \text{\OFFSET}_{i - 1}$\; + $r \rightarrow$ \WRITE $p$ unsigned bits\; + }{ + $r \leftarrow \lfloor(\text{\OFFSET}_{i - 1} + e) \div 2\rfloor$\; + $b \leftarrow (\text{\OFFSET}_{i - 1} + e) \bmod 2$\; + $r \rightarrow$ \WRITE $p$ unsigned bits\; + $b \rightarrow$ \WRITE 1 unsigned bit\; + } + } + $\text{\SIGN}_{i - 1} \rightarrow$ \WRITE 1 unsigned bit\; + }{ + $u_{i - 1} \leftarrow \text{\UNDEFINED}$\; + } + \Return $u_{i - 1}$\; + \EALGORITHM +} + +\clearpage + + +\subsubsection{Residual Encoding Example} +{\relsize{-1} + Given the residuals: + \newline + \begin{tabular}{rr} + channel 0 residuals : & \texttt{[-61,~-33,~-18,~~1,~20,~35,~50,~62,~68,~71]}\\ + channel 1 residuals : & \texttt{[~31,~~32,~~36,~37,~35,~31,~25,~18,~10,~~0]}\\ + \end{tabular} + \newline + And entropies: + \newline + \begin{tabular}{rr} + channel 0 entropy : & \texttt{[118, 194, 322]} \\ + channel 1 entropy : & \texttt{[118, 176, 212]} \\ + \end{tabular} +} + +\begin{table}[h] +{\relsize{-2} +\begin{tabular}{|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|} +i & r_i & \text{unsigned}_i &\text{sign}_i & +%% \text{median}_{c~0} & \text{median}_{c~1} & \text{median}_{c~2} & +m_i & \text{offset}_i & \text{add}_i \\ +\hline +0 & -61 & +60 & 1 & +%% \left\lfloor\frac{118}{2 ^ 4}\right\rfloor + 1 = 8 & \left\lfloor\frac{194}{2 ^ 4}\right\rfloor + 1 = 13 & \left\lfloor\frac{322}{2 ^ 4}\right\rfloor + 1 = 21 & +3 & 60 - (8 + 13 + ((3 - 2) \times 21)) = 18 & 21 - 1 = 20 +\\ +1 & 31 & +31 & 0 & +%% \left\lfloor\frac{118}{2 ^ 4}\right\rfloor + 1 = 8 & \left\lfloor\frac{176}{2 ^ 4}\right\rfloor + 1 = 12 & \left\lfloor\frac{212}{2 ^ 4}\right\rfloor + 1 = 14 & +2 & 31 - (8 + 12) = 11 & 14 - 1 = 13 +\\ +\hline +2 & -33 & +32 & 1 & +%% \left\lfloor\frac{123}{2 ^ 4}\right\rfloor + 1 = 8 & \left\lfloor\frac{214}{2 ^ 4}\right\rfloor + 1 = 14 & \left\lfloor\frac{377}{2 ^ 4}\right\rfloor + 1 = 24 & +2 & 32 - (8 + 14) = 10 & 24 - 1 = 23 +\\ +3 & 32 & +32 & 0 & +%% \left\lfloor\frac{123}{2 ^ 4}\right\rfloor + 1 = 8 & \left\lfloor\frac{191}{2 ^ 4}\right\rfloor + 1 = 12 & \left\lfloor\frac{198}{2 ^ 4}\right\rfloor + 1 = 13 & +2 & 32 - (8 + 12) = 12 & 13 - 1 = 12 +\\ +\hline +4 & -18 & +17 & 1 & +%% \left\lfloor\frac{128}{2 ^ 4}\right\rfloor + 1 = 9 & \left\lfloor\frac{234}{2 ^ 4}\right\rfloor + 1 = 15 & \left\lfloor\frac{353}{2 ^ 4}\right\rfloor + 1 = 23 & +1 & 17 - 9 = 8 & 15 - 1 = 14 +\\ +5 & 36 & +36 & 0 & +%% \left\lfloor\frac{128}{2 ^ 4}\right\rfloor + 1 = 9 & \left\lfloor\frac{206}{2 ^ 4}\right\rfloor + 1 = 13 & \left\lfloor\frac{184}{2 ^ 4}\right\rfloor + 1 = 12 & +3 & 36 - (9 + 13 + ((3 - 2) \times 12)) = 2 & 12 - 1 = 11 +\\ +\hline +6 & 1 & +1 & 0 & +%% \left\lfloor\frac{138}{2 ^ 4}\right\rfloor + 1 = 9 & \left\lfloor\frac{226}{2 ^ 4}\right\rfloor + 1 = 15 & \left\lfloor\frac{353}{2 ^ 4}\right\rfloor + 1 = 23 & +0 & 1 & 9 - 1 = 8 +\\ +7 & 37 & +37 & 0 & +%% \left\lfloor\frac{138}{2 ^ 4}\right\rfloor + 1 = 9 & \left\lfloor\frac{226}{2 ^ 4}\right\rfloor + 1 = 15 & \left\lfloor\frac{214}{2 ^ 4}\right\rfloor + 1 = 14 & +2 & 37 - (9 + 15) = 13 & 14 - 1 = 13 +\\ +\hline +8 & 20 & +20 & 0 & +%% \left\lfloor\frac{134}{2 ^ 4}\right\rfloor + 1 = 9 & \left\lfloor\frac{226}{2 ^ 4}\right\rfloor + 1 = 15 & \left\lfloor\frac{353}{2 ^ 4}\right\rfloor + 1 = 23 & +1 & 20 - 9 = 11 & 15 - 1 = 14 +\\ +9 & 35 & +35 & 0 & +%% \left\lfloor\frac{148}{2 ^ 4}\right\rfloor + 1 = 10 & \left\lfloor\frac{246}{2 ^ 4}\right\rfloor + 1 = 16 & \left\lfloor\frac{200}{2 ^ 4}\right\rfloor + 1 = 13 & +2 & 35 - (10 + 16) = 9 & 13 - 1 = 12 +\\ +\hline +10 & 35 & +35 & 0 & +%% \left\lfloor\frac{144}{2 ^ 4}\right\rfloor + 1 = 10 & \left\lfloor\frac{218}{2 ^ 4}\right\rfloor + 1 = 14 & \left\lfloor\frac{353}{2 ^ 4}\right\rfloor + 1 = 23 & +2 & 35 - (10 + 14) = 11 & 23 - 1 = 22 +\\ +11 & 31 & +31 & 0 & +%% \left\lfloor\frac{158}{2 ^ 4}\right\rfloor + 1 = 10 & \left\lfloor\frac{266}{2 ^ 4}\right\rfloor + 1 = 17 & \left\lfloor\frac{186}{2 ^ 4}\right\rfloor + 1 = 12 & +2 & 31 - (10 + 17) = 4 & 12 - 1 = 11 +\\ +\hline +12 & 50 & +50 & 0 & +%% \left\lfloor\frac{154}{2 ^ 4}\right\rfloor + 1 = 10 & \left\lfloor\frac{238}{2 ^ 4}\right\rfloor + 1 = 15 & \left\lfloor\frac{331}{2 ^ 4}\right\rfloor + 1 = 21 & +3 & 50 - (10 + 15 + ((3 - 2) \times 21)) = 4 & 21 - 1 = 20 +\\ +13 & 25 & +25 & 0 & +%% \left\lfloor\frac{168}{2 ^ 4}\right\rfloor + 1 = 11 & \left\lfloor\frac{291}{2 ^ 4}\right\rfloor + 1 = 19 & \left\lfloor\frac{174}{2 ^ 4}\right\rfloor + 1 = 11 & +1 & 25 - 11 = 14 & 19 - 1 = 18 +\\ +\hline +14 & 62 & +62 & 0 & +%% \left\lfloor\frac{164}{2 ^ 4}\right\rfloor + 1 = 11 & \left\lfloor\frac{258}{2 ^ 4}\right\rfloor + 1 = 17 & \left\lfloor\frac{386}{2 ^ 4}\right\rfloor + 1 = 25 & +3 & 62 - (11 + 17 + ((3 - 2) \times 25)) = 9 & 25 - 1 = 24 +\\ +15 & 18 & +18 & 0 & +%% \left\lfloor\frac{178}{2 ^ 4}\right\rfloor + 1 = 12 & \left\lfloor\frac{281}{2 ^ 4}\right\rfloor + 1 = 18 & \left\lfloor\frac{174}{2 ^ 4}\right\rfloor + 1 = 11 & +1 & 18 - 12 = 6 & 18 - 1 = 17 +\\ +\hline +16 & 68 & +68 & 0 & +%% \left\lfloor\frac{174}{2 ^ 4}\right\rfloor + 1 = 11 & \left\lfloor\frac{283}{2 ^ 4}\right\rfloor + 1 = 18 & \left\lfloor\frac{451}{2 ^ 4}\right\rfloor + 1 = 29 & +3 & 68 - (11 + 18 + ((3 - 2) \times 29)) = 10 & 29 - 1 = 28 +\\ +17 & 10 & +10 & 0 & +%% \left\lfloor\frac{188}{2 ^ 4}\right\rfloor + 1 = 12 & \left\lfloor\frac{271}{2 ^ 4}\right\rfloor + 1 = 17 & \left\lfloor\frac{174}{2 ^ 4}\right\rfloor + 1 = 11 & +0 & 10 & 12 - 1 = 11 +\\ +\hline +18 & 71 & +71 & 0 & +%% \left\lfloor\frac{184}{2 ^ 4}\right\rfloor + 1 = 12 & \left\lfloor\frac{308}{2 ^ 4}\right\rfloor + 1 = 20 & \left\lfloor\frac{526}{2 ^ 4}\right\rfloor + 1 = 33 & +3 & 71 - (12 + 20 + ((3 - 2) \times 33)) = 6 & 33 - 1 = 32 +\\ +19 & 0 & +0 & 0 & +%% \left\lfloor\frac{184}{2 ^ 4}\right\rfloor + 1 = 12 & \left\lfloor\frac{271}{2 ^ 4}\right\rfloor + 1 = 17 & \left\lfloor\frac{174}{2 ^ 4}\right\rfloor + 1 = 11 & +0 & 0 & 12 - 1 = 11 +\\ +\hline +\end{tabular} +\vskip .25in +\begin{tabular}{|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}||>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}>{$}r<{$}|} +i & m_i & \text{offset}_i & \text{add}_i & u_i & p_i & e_i & r_i & b_i \\ +\hline +0 & 3 & +18 & 20 & +(3 \times 2) + 1 = 7 & +\lfloor\log_2(20)\rfloor = 4 & +2 ^ {(4 + 1)} - 20 - 1 = 11 & +(18 + 11) \div 2 = 14 & +1 \\ +1 & 2 & +11 & 13 & +(2 \times 2) - 1 = 3 & +\lfloor\log_2(13)\rfloor = 3 & +2 ^ {(3 + 1)} - 13 - 1 = 2 & +(11 + 2) \div 2 = 6 & +1 \\ +\hline +2 & 2 & +10 & 23 & +(2 \times 2) - 1 = 3 & +\lfloor\log_2(23)\rfloor = 4 & +2 ^ {(4 + 1)} - 23 - 1 = 8 & +(10 + 8) \div 2 = 9 & +0 \\ +3 & 2 & +12 & 12 & +(2 \times 2) - 1 = 3 & +\lfloor\log_2(12)\rfloor = 3 & +2 ^ {(3 + 1)} - 12 - 1 = 3 & +(12 + 3) \div 2 = 7 & +1 \\ +\hline +4 & 1 & +8 & 14 & +(1 \times 2) - 1 = 1 & +\lfloor\log_2(14)\rfloor = 3 & +2 ^ {(3 + 1)} - 14 - 1 = 1 & +(8 + 1) \div 2 = 4 & +1 \\ +5 & 3 & +2 & 11 & +(3 - 1) \times 2 = 4 & +\lfloor\log_2(11)\rfloor = 3 & +2 ^ {(3 + 1)} - 11 - 1 = 4 & +2 & \\ +\hline +6 & 0 & +1 & 8 & +\textit{undefined} & +\lfloor\log_2(8)\rfloor = 3 & +2 ^ {(3 + 1)} - 8 - 1 = 7 & +1 & \\ +7 & 2 & +13 & 13 & +(2 \times 2) + 1 = 5 & +\lfloor\log_2(13)\rfloor = 3 & +2 ^ {(3 + 1)} - 13 - 1 = 2 & +(13 + 2) \div 2 = 7 & +1 \\ +\hline +8 & 1 & +11 & 14 & +(1 \times 2) - 1 = 1 & +\lfloor\log_2(14)\rfloor = 3 & +2 ^ {(3 + 1)} - 14 - 1 = 1 & +(11 + 1) \div 2 = 6 & +0 \\ +9 & 2 & +9 & 12 & +(2 \times 2) - 1 = 3 & +\lfloor\log_2(12)\rfloor = 3 & +2 ^ {(3 + 1)} - 12 - 1 = 3 & +(9 + 3) \div 2 = 6 & +0 \\ +\hline +10 & 2 & +11 & 22 & +(2 \times 2) - 1 = 3 & +\lfloor\log_2(22)\rfloor = 4 & +2 ^ {(4 + 1)} - 22 - 1 = 9 & +(11 + 9) \div 2 = 10 & +0 \\ +11 & 2 & +4 & 11 & +(2 \times 2) - 1 = 3 & +\lfloor\log_2(11)\rfloor = 3 & +2 ^ {(3 + 1)} - 11 - 1 = 4 & +(4 + 4) \div 2 = 4 & +0 \\ +\hline +12 & 3 & +4 & 20 & +(3 \times 2) - 1 = 5 & +\lfloor\log_2(20)\rfloor = 4 & +2 ^ {(4 + 1)} - 20 - 1 = 11 & +4 & \\ +13 & 1 & +14 & 18 & +(1 \times 2) - 1 = 1 & +\lfloor\log_2(18)\rfloor = 4 & +2 ^ {(4 + 1)} - 18 - 1 = 13 & +(14 + 13) \div 2 = 13 & +1 \\ +\hline +14 & 3 & +9 & 24 & +(3 \times 2) - 1 = 5 & +\lfloor\log_2(24)\rfloor = 4 & +2 ^ {(4 + 1)} - 24 - 1 = 7 & +(9 + 7) \div 2 = 8 & +0 \\ +15 & 1 & +6 & 17 & +(1 \times 2) - 1 = 1 & +\lfloor\log_2(17)\rfloor = 4 & +2 ^ {(4 + 1)} - 17 - 1 = 14 & +6 & \\ +\hline +16 & 3 & +10 & 28 & +(3 - 1) \times 2 = 4 & +\lfloor\log_2(28)\rfloor = 4 & +2 ^ {(4 + 1)} - 28 - 1 = 3 & +(9 + 3) \div 2 = 6 & +0 \\ +17 & 0 & +10 & 11 & +\textit{undefined} & +\lfloor\log_2(11)\rfloor = 3 & +2 ^ {(3 + 1)} - 11 - 1 = 4 & +(10 + 4) \div 2 = 7 & +0 \\ +\hline +18 & 3 & +6 & 32 & +3 \times 2 = 6 & +\lfloor\log_2(32)\rfloor = 5 & +2 ^ {(5 + 1)} - 32 - 1 = 31 & +6 & \\ +19 & 0 & +0 & 11 & +\textit{undefined} & +\lfloor\log_2(11)\rfloor = 3 & +2 ^ {(3 + 1)} - 11 - 1 = 4 & +0 & \\ +\hline +\end{tabular} +} +\end{table} + +\clearpage + +\begin{figure}[h] + \includegraphics[width=6in,keepaspectratio]{wavpack/figures/residuals_parse.pdf} +\end{figure} + +\clearpage + +\subsubsection{2nd Residual Encoding Example} +This example is more simplified to demonstrate how the \VAR{zeroes} +value propagates in two instances. +\vskip .25in +{\relsize{-2} +\renewcommand{\arraystretch}{1.75} +\begin{tabular}{|r|r|>{$}r<{$}>{$}r<{$}>{$}r<{$}||>{$}r<{$}|>{$}r<{$}>{$}r<{$}>{$}r<{$}>{$}r<{$}>{$}r<{$}|l} + $i$ & $r_i$ & \text{entropy}_{0~0} & \text{entropy}_{0~1} & \text{entropy}_{0~2} & u_i & \text{zeroes}_i & m_i & \text{offset}_i & \text{add}_i & \text{sign}_i \\ +\cline{0-10} +-2 & & & & & \textit{und.} & & & & & \\ +-1 & & {\color{red}0} & 0 & 0 & {\color{red}\textit{und.}} & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ +0 & 1 & 0 & 0 & 0 & 3 & {\color{blue}0} & 1 & 0 & 0 & 0 \\ +1 & 2 & 5 & 0 & 0 & 3 & \textit{und.} & 2 & 0 & 0 & 0 \\ +2 & 3 & 10 & 5 & 0 & 5 & \textit{und.} & 3 & 0 & 0 & 0 \\ +3 & 2 & 15 & 10 & 5 & 2 & \textit{und.} & 2 & 0 & 0 & 0 \\ +4 & 1 & 20 & 15 & 3 & \textit{und.} & \textit{und.} & 0 & 1 & 1 & 0 \\ +5 & 0 & 18 & 15 & 3 & 0 & \textit{und.} & 0 & 0 & 1 & 0 \\ +6 & 0 & 16 & 15 & 3 & \textit{und.} & \textit{und.} & 0 & 0 & 1 & 0 \\ +7 & 0 & 14 & 15 & 3 & 0 & \textit{und.} & 0 & 0 & 0 & 0 \\ +8 & 0 & 12 & 15 & 3 & \textit{und.} & \textit{und.} & 0 & 0 & 0 & 0 \\ +9 & 0 & 10 & 15 & 3 & 0 & \textit{und.} & 0 & 0 & 0 & 0 \\ +10 & 0 & 8 & 15 & 3 & \textit{und.} & \textit{und.} & 0 & 0 & 0 & 0 \\ +11 & 0 & 6 & 15 & 3 & 0 & \textit{und.} & 0 & 0 & 0 & 0 \\ +12 & 0 & 4 & 15 & 3 & \textit{und.} & \textit{und.} & 0 & 0 & 0 & 0 \\ +13 & 0 & 2 & 15 & 3 & 0 & \textit{und.} & 0 & 0 & 0 & 0 \\ +14 & 0 & {\color{red}0} & 15 & 3 & {\color{red}\textit{und.}} & \textit{und.} & 0 & 0 & 0 & 0 \\ +\cline{0-10} +15 & 0 & 0 & 15 & 3 & \textit{und.} & 1 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} & \multirow{10}{1em}{\begin{sideways}block of 10 zero residuals\end{sideways}} \\ +16 & 0 & 0 & 0 & 0 & \textit{und.} & 2 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ +17 & 0 & 0 & 0 & 0 & \textit{und.} & 3 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ +18 & 0 & 0 & 0 & 0 & \textit{und.} & 4 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ +19 & 0 & 0 & 0 & 0 & \textit{und.} & 5 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ +20 & 0 & 0 & 0 & 0 & \textit{und.} & 6 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ +21 & 0 & 0 & 0 & 0 & \textit{und.} & 7 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ +22 & 0 & 0 & 0 & 0 & \textit{und.} & 8 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ +23 & 0 & 0 & 0 & 0 & \textit{und.} & 9 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ +24 & 0 & 0 & 0 & 0 & \textit{und.} & 10 & \textit{und.} & \textit{und.} & \textit{und.} & \textit{und.} \\ +\cline{0-10} +25 & -1 & 0 & 0 & 0 & 1 & {\color{blue}10} & 0 & 0 & 0 & 1 \\ +26 & -2 & 0 & 0 & 0 & 1 & \textit{und.} & 1 & 0 & 0 & 1 \\ +27 & -3 & 5 & 0 & 0 & 3 & \textit{und.} & 2 & 0 & 0 & 1 \\ +28 & -2 & 10 & 5 & 0 & 0 & \textit{und.} & 1 & 0 & 0 & 1 \\ +29 & -1 & 15 & 3 & 0 & \textit{und.} & \textit{und.} & 0 & 0 & 0 & 1 \\ +\cline{0-10} +\end{tabular} +} + +\clearpage + +For $r_0$, because $\text{entropy}_{0~0} = 0$ and +$u_{(-1)} = \textit{undefined}$\footnote{as determined by the \texttt{unary\_undefined} function}, +we must handle a block of zeroes in some way. +But because $r_0 \neq 0$, we prepend a ``false alarm'' block of zeroes +and encode the residual normally. + +For $r_{15}$, because $\text{entropy}_{0~0} = 0$, +$u_{14} = \textit{undefined}$ and $r_{15} = 0$, +we flush $\text{residual}_{14}$'s values and begin a block of zeroes +which continues until $r_{25} \neq 0$. +This block of zeroes is prepended to $\text{residual}_{25}$'s +values, which are flushed once $\text{residual}_{26}$ is encoded +and $u_{25}$ can be calculated from $u_{24}$ and $m_{26}$. + +\begin{figure}[h] + \includegraphics{wavpack/figures/residuals_parse2.pdf} +\end{figure}
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/encode/correlation.tex
Added
@@ -0,0 +1,642 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\subsection{1 Channel Correlation Passes} +\label{wavpack:correlate_channels} +\ALGORITHM{a list of signed samples; correlation terms, deltas, weights and samples}{a list of signed residuals; correlation weights and samples for the next block} +\SetKwData{PASS}{pass} +\SetKwData{CHANNEL}{channel} +\SetKwData{TERMCOUNT}{term count} +\SetKwData{TERM}{term} +\SetKwData{DELTA}{delta} +\SetKwData{WEIGHT}{weight} +\SetKwData{SAMPLES}{sample} +\SetKw{KwDownTo}{downto} +$\text{\PASS}_{-1~0} \leftarrow \text{\CHANNEL}_0$\; +\For(\tcc*[f]{perform passes in reverse order}){$i \leftarrow 0$ \emph{\KwTo}\TERMCOUNT}{ + $p \leftarrow \TERMCOUNT - i - 1$\; + $\left.\begin{tabular}{r} + $\text{\PASS}_{i}$ \\ + $\text{\WEIGHT}'_{p~0}$ \\ + $\text{\SAMPLES}'_{p~0}$ \\ + \end{tabular}\right\rbrace \leftarrow$ \hyperref[wavpack:correlate_1ch]{correlate 1 channel} + $\left\lbrace\begin{tabular}{l} + $\text{\PASS}_{i - 1}$ \\ + $\text{\TERM}_{i}$ \\ + $\text{\DELTA}_{i}$ \\ + $\text{\WEIGHT}_{p~0}$ \\ + $\text{\SAMPLES}_{p~0}$ \\ + \end{tabular}\right.$\; +} +\Return $\left\lbrace\begin{tabular}{l} +$\text{\PASS}_{(\TERMCOUNT - 1)}$ \\ +$\text{\WEIGHT}'$ \\ +$\text{\SAMPLES}'$ \\ +\end{tabular}\right.$\; +\EALGORITHM + +\subsection{2 Channel Correlation Passes} +\ALGORITHM{a list of signed samples per channel; correlation terms, deltas, weights and samples}{a list of signed residuals per channel; correlation weights and samples for the next block} +\SetKwData{PASS}{pass} +\SetKwData{CHANNEL}{channel} +\SetKwData{TERMCOUNT}{term count} +\SetKwData{TERM}{term} +\SetKwData{DELTA}{delta} +\SetKwData{WEIGHT}{weight} +\SetKwData{SAMPLES}{sample} +\SetKw{KwDownTo}{downto} +$\text{\PASS}_{-1~0} \leftarrow \text{\CHANNEL}_0$\; +$\text{\PASS}_{-1~1} \leftarrow \text{\CHANNEL}_1$\; +\For(\tcc*[f]{perform passes in reverse order}){$i \leftarrow 0$ \emph{\KwTo}\TERMCOUNT}{ + $p \leftarrow \TERMCOUNT - i - 1$\; + $\left.\begin{tabular}{r} + $\text{\PASS}_{i~0}$ \\ + $\text{\PASS}_{i~1}$ \\ + $\text{\WEIGHT}'_{p~0}$ \\ + $\text{\WEIGHT}'_{p~1}$ \\ + $\text{\SAMPLES}'_{p~0}$ \\ + $\text{\SAMPLES}'_{p~1}$ \\ + \end{tabular}\right\rbrace \leftarrow$ \hyperref[wavpack:correlate_2ch]{correlate 2 channel} + $\left\lbrace\begin{tabular}{l} + $\text{\PASS}_{i - 1~0}$ \\ + $\text{\PASS}_{i - 1~1}$ \\ + $\text{\TERM}_{i}$ \\ + $\text{\DELTA}_{i}$ \\ + $\text{\WEIGHT}_{p~0}$ \\ + $\text{\WEIGHT}_{p~1}$ \\ + $\text{\SAMPLES}_{p~0}$ \\ + $\text{\SAMPLES}_{p~1}$ \\ + \end{tabular}\right.$\; +} +\Return $\left\lbrace\begin{tabular}{l} +$\text{\PASS}_{(\TERMCOUNT - 1)}$ \\ +$\text{\WEIGHT}'$ \\ +$\text{\SAMPLES}'$ \\ +\end{tabular}\right.$\; +\EALGORITHM + +\clearpage + +\subsection{1 Channel Correlation Pass} +\label{wavpack:correlate_1ch} +{\relsize{-1} +\ALGORITHM{a list of signed uncorrelated samples; correlation term, delta, weight and samples}{a list of signed correlated samples; updated weight and sample values} +\SetKwData{CORRELATED}{correlated} +\SetKwData{DECORRELATED}{uncorrelated} +\SetKwData{DECORRSAMPLE}{correlation sample} +\SetKwData{WEIGHT}{weight} +\SetKwData{DELTA}{delta} +\SetKwData{TEMP}{temp} +\SetKw{OR}{or} +\SetKw{XOR}{xor} +\SetKwFunction{APPLYWEIGHT}{apply\_weight} +\SetKwFunction{UPDATEWEIGHT}{update\_weight} +$\text{\WEIGHT}_0 \leftarrow$ decorrelation weight\; +\BlankLine +\uIf{$term = 18$}{ + $\text{\DECORRELATED}_{-2} \leftarrow \text{\DECORRSAMPLE}_1$\; + $\text{\DECORRELATED}_{-1} \leftarrow \text{\DECORRSAMPLE}_0$\; + \For{$i \leftarrow 0$ \emph{\KwTo}uncorrelated samples length}{ + $\text{\TEMP}_{i} \leftarrow \lfloor(3 \times \text{\DECORRELATED}_{i - 1} - \text{\DECORRELATED}_{i - 2}) \div 2 \rfloor$\; + $\text{\CORRELATED}_i \leftarrow \text{\DECORRELATED}_i - \APPLYWEIGHT(\text{\WEIGHT}_i~,~\text{\TEMP}_{i})$\; + $\text{\WEIGHT}_{i + 1} \leftarrow \text{\WEIGHT}_i + \UPDATEWEIGHT(\text{\TEMP}_{i}~,~\text{\CORRELATED}_i~,~\DELTA)$\; + } + \Return $\left\lbrace\begin{tabular}{l} + \CORRELATED \\ + $\text{\WEIGHT}_i$ \\ + $\DECORRSAMPLE \leftarrow$ \texttt{[}$\text{\DECORRELATED}_{(i - 2)}$, $\text{\DECORRELATED}_{(i - 1)}$ \texttt{]} \\ + \end{tabular}\right.$\; +} +\uElseIf{$term = 17$}{ + $\text{\DECORRELATED}_{-2} \leftarrow \text{\DECORRSAMPLE}_1$\; + $\text{\DECORRELATED}_{-1} \leftarrow \text{\DECORRSAMPLE}_0$\; + \For{$i \leftarrow 0$ \emph{\KwTo}uncorrelated samples length}{ + $\text{\TEMP}_{i} \leftarrow 2 \times \text{\DECORRELATED}_{i - 1} - \text{\DECORRELATED}_{i - 2}$\; + $\text{\CORRELATED}_i \leftarrow \text{\DECORRELATED}_i - \APPLYWEIGHT(\text{\WEIGHT}_i~,~\text{\TEMP}_{i})$\; + $\text{\WEIGHT}_{i + 1} \leftarrow \text{\WEIGHT}_i + \UPDATEWEIGHT(\text{\TEMP}_{i}~,~\text{\CORRELATED}_i~,~\DELTA)$\; + } + \Return $\left\lbrace\begin{tabular}{l} + \CORRELATED \\ + $\text{\WEIGHT}_i$ \\ + $\DECORRSAMPLE \leftarrow$ \texttt{[}$\text{\DECORRELATED}_{(i - 2)}$, $\text{\DECORRELATED}_{(i - 1)}$ \texttt{]} \\ + \end{tabular}\right.$\; +} +\uElseIf{$1 \leq term \leq 8$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}term}{ + $\text{\DECORRELATED}_{i - \text{term}} \leftarrow \text{\DECORRSAMPLE}_i$\; + } + \For{$i \leftarrow 0$ \emph{\KwTo}correlated samples length}{ + $\text{\CORRELATED}_i \leftarrow \text{\DECORRELATED}_i - \APPLYWEIGHT(\text{\WEIGHT}_i~,~\text{\DECORRELATED}_{i - \text{term}})$\; + $\text{\WEIGHT}_{i + 1} \leftarrow \text{\WEIGHT}_i + \UPDATEWEIGHT(\text{\DECORRELATED}_{i - \text{term}}~,~\text{\CORRELATED}_i~,~\DELTA)$\; + } + \Return $\left\lbrace\begin{tabular}{l} + \CORRELATED \\ + $\text{\WEIGHT}_i$ \\ + $\DECORRSAMPLE \leftarrow$ last $term$ \DECORRELATED samples \\ + \end{tabular}\right.$\; +} +\Else{ + invalid decorrelation term\; +} +\EALGORITHM +\par +\noindent +\begin{align*} +\intertext{where \texttt{apply\_weight} is defined as:} +\texttt{apply\_weight}(weight~,~sample) &= \left\lfloor\frac{weight \times sample + 2 ^ 9}{2 ^ {10}}\right\rfloor \\ +\intertext{and \texttt{update\_weight} is defined as:} +\texttt{update\_weight}(source~,~result~,~delta) &= +\begin{cases} +0 & \text{ if } source = 0 \text{ or } result = 0 \\ +delta & \text{ if } (source \textbf{ xor } result ) \geq 0 \\ +-delta & \text{ if } (source \textbf{ xor } result) < 0 +\end{cases} +\end{align*} +} + +\clearpage + +\subsection{2 Channel Correlation Pass} +\label{wavpack:correlate_2ch} +{\relsize{-1} +\ALGORITHM{2 lists of signed uncorrelated samples; correlation term and delta, 2 correlation weights, 2 lists of correlation samples}{2 lists of signed correlated samples; updated weight and sample values per channel} +\SetKwData{TERM}{term} +\SetKwData{DELTA}{delta} +\SetKwData{CORRELATED}{correlated} +\SetKwData{DECORRELATED}{uncorrelated} +\SetKwData{DECORRSAMPLE}{correlation sample} +\SetKwData{WEIGHT}{weight} +\SetKw{OR}{or} +\SetKw{XOR}{xor} +\SetKwFunction{MIN}{min} +\SetKwFunction{MAX}{max} +\SetKwFunction{APPLYWEIGHT}{apply\_weight} +\SetKwFunction{UPDATEWEIGHT}{update\_weight} +\uIf{$(17 \leq \TERM \leq 18)$ \OR $(1 \leq \TERM \leq 8)$}{ + $\left.\begin{tabular}{r} + $\text{\CORRELATED}_{0}$ \\ + $\text{\WEIGHT}'_{0}$ \\ + $\text{\DECORRSAMPLE}'_{0}$ \\ + \end{tabular}\right\rbrace \leftarrow$ \hyperref[wavpack:correlate_1ch]{correlate 1 channel} + $\left\lbrace\begin{tabular}{l} + $\text{\DECORRELATED}_{0}$ + $\text{\TERM}$ \\ + $\text{\DELTA}$ \\ + $\text{\WEIGHT}_{0}$ \\ + $\text{\DECORRSAMPLE}_{0}$ \\ + \end{tabular}\right.$\; + $\left.\begin{tabular}{r} + $\text{\CORRELATED}_{1}$ \\ + $\text{\WEIGHT}'_{1}$ \\ + $\text{\DECORRSAMPLE}'_{1}$ \\ + \end{tabular}\right\rbrace \leftarrow$ \hyperref[wavpack:correlate_1ch]{correlate 1 channel} + $\left\lbrace\begin{tabular}{l} + $\text{\DECORRELATED}_{1}$ + $\text{\TERM}$ \\ + $\text{\DELTA}$ \\ + $\text{\WEIGHT}_{1}$ \\ + $\text{\DECORRSAMPLE}_{1}$ \\ + \end{tabular}\right.$\; + \Return $\left\lbrace\begin{tabular}{l} + $\text{\CORRELATED}$ \\ + $\text{\WEIGHT}'$ \\ + $\text{\DECORRSAMPLE}'$ \\ + \end{tabular}\right.$\; +} +\uElseIf{$-3 \leq \TERM \leq -1$}{ + $\text{\WEIGHT}_{0~0} \leftarrow$ correlation weight 0\; + $\text{\WEIGHT}_{1~0} \leftarrow$ correlation weight 1\; + $\text{\DECORRELATED}_{0~-1} \leftarrow \text{\DECORRSAMPLE}_{1~0}$\; + $\text{\DECORRELATED}_{1~-1} \leftarrow \text{\DECORRSAMPLE}_{0~0}$\; + \uIf{$\TERM = -1$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}uncorrelated samples length}{ + $\text{\CORRELATED}_{0~i} \leftarrow \text{\DECORRELATED}_{0~i} - \APPLYWEIGHT(\text{\WEIGHT}_{0~i}~,~\text{\DECORRELATED}_{1~(i - 1)})$\; + $\text{\CORRELATED}_{1~i} \leftarrow \text{\DECORRELATED}_{1~i} - \APPLYWEIGHT(\text{\WEIGHT}_{1~i}~,~\text{\DECORRELATED}_{0~i})$\; + $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \text{\WEIGHT}_{0~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{1~(i - 1)}~,~\text{\CORRELATED}_{0~i}~,~\DELTA)$\; + $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \text{\WEIGHT}_{1~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{0~i}~,~\text{\CORRELATED}_{1~i}~,~\DELTA)$\; + $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{0~(i + 1)}~,~1024)~,~-1024)$\; + $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{1~(i + 1)}~,~1024)~,~-1024)$\; + } + } + \uElseIf{$\TERM = -2$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}uncorrelated samples length}{ + $\text{\CORRELATED}_{0~i} \leftarrow \text{\DECORRELATED}_{0~i} - \APPLYWEIGHT(\text{\WEIGHT}_{0~i}~,~\text{\DECORRELATED}_{1~i})$\; + $\text{\CORRELATED}_{1~i} \leftarrow \text{\DECORRELATED}_{1~i} - \APPLYWEIGHT(\text{\WEIGHT}_{1~i}~,~\text{\DECORRELATED}_{0~({i - 1})})$\; + $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \text{\WEIGHT}_{0~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{1~i}~,~\text{\CORRELATED}_{0~i}~,~\DELTA)$\; + $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \text{\WEIGHT}_{1~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{0~(i - 1)}~,~\text{\CORRELATED}_{1~i}~,~\DELTA)$\; + $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{0~(i + 1)}~,~1024)~,~-1024)$\; + $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{1~(i + 1)}~,~1024)~,~-1024)$\; + } + } + \ElseIf{$\TERM = -3$}{ + \For{$i \leftarrow 0$ \emph{\KwTo}uncorrelated samples length}{ + $\text{\CORRELATED}_{0~i} \leftarrow \text{\DECORRELATED}_{0~i} - \APPLYWEIGHT(\text{\WEIGHT}_{0~i}~,~\text{\DECORRELATED}_{1~(i - 1)})$\; + $\text{\CORRELATED}_{1~i} \leftarrow \text{\DECORRELATED}_{1~i} - \APPLYWEIGHT(\text{\WEIGHT}_{1~i}~,~\text{\DECORRELATED}_{0~(i - 1)})$\; + $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \text{\WEIGHT}_{0~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{1~(i - 1)}~,~\text{\CORRELATED}_{0~i}~,~\DELTA)$\; + $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \text{\WEIGHT}_{1~i} + \UPDATEWEIGHT(\text{\DECORRELATED}_{0~(i - 1)}~,~\text{\CORRELATED}_{1~i}~,~\DELTA)$\; + $\text{\WEIGHT}_{0~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{0~(i + 1)}~,~1024)~,~-1024)$\; + $\text{\WEIGHT}_{1~(i + 1)} \leftarrow \MAX(\MIN(\text{\WEIGHT}_{1~(i + 1)}~,~1024)~,~-1024)$\; + } + } + \Return $\left\lbrace\begin{tabular}{l} + $\text{\CORRELATED}$ \\ + $\text{\WEIGHT}_{0~i}$ \\ + $\text{\WEIGHT}_{1~i}$ \\ + $\text{\DECORRSAMPLE}_{0~0} \leftarrow \text{\DECORRELATED}_{1~i}$ \\ + $\text{\DECORRSAMPLE}_{1~0} \leftarrow \text{\DECORRELATED}_{0~i}$ \\ + \end{tabular}\right.$\; +} +\Else{ + invalid decorrelation term\; +} +\EALGORITHM +} + +\clearpage + +\subsection{Channel Correlation Example} +\begin{figure}[h] +{\relsize{-1} + \subfloat{ + \begin{tabular}{|r|r|r|} + \multicolumn{3}{c}{Correlation Terms} \\ + \hline + $p$ & $\textsf{term}_p$ & $\textsf{delta}_p$ \\ + \hline + 0 & 3 & 2 \\ + 1 & 17 & 2 \\ + 2 & 2 & 2 \\ + 3 & 18 & 2 \\ + 4 & 18 & 2 \\ + \hline + \end{tabular} + } + \subfloat{ + \begin{tabular}{|r|r|r|} + \multicolumn{3}{c}{Correlation Weights} \\ + \hline + $p$ & $\textsf{weight}_{p~0}$ & $\textsf{weight}_{p~1}$ \\ + \hline + 0 & 16 & 24 \\ + 1 & 48 & 48 \\ + 2 & 32 & 32 \\ + 3 & 48 & 48 \\ + 4 & 48 & 48 \\ + \hline + \end{tabular} + } + \subfloat{ + \begin{tabular}{|r|r|r|} + \multicolumn{3}{c}{Correlation Samples} \\ + \hline + $p$ & $\textsf{sample}_{p~0~s}$ & $\textsf{sample}_{p~1~s}$ \\ + \hline + 0 & \texttt{[0, 0, 0]} & \texttt{[0, 0, 0]} \\ + 1 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ + 2 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ + 3 & \texttt{[0, 0]} & \texttt{[0, 0]} \\ + 4 & \texttt{[-73, -78]} & \texttt{[28, 26]} \\ + \hline + \end{tabular} + } +} +\end{figure} +\par +\noindent +we combine them into a single set of arguments for each correlation pass: +\begin{table}[h] +{\relsize{-1} + \begin{tabular}{|r|r|r|r|r|r|} + \hline + & $\textbf{pass}_0$ & $\textbf{pass}_1$ & $\textbf{pass}_2$ & + $\textbf{pass}_3$ & $\textbf{pass}_3$ \\ + \hline + $\textsf{term}_p$ & 18 & 18 & 2 & 17 & 3 \\ + $\textsf{delta}_p$ & 2 & 2 & 2 & 2 & 2 \\ + $\textsf{weight}_{p~0}$ & 48 & 48 & 32 & 48 & 16 \\ + $\textsf{sample}_{p~0~s}$ & \texttt{[-73, -78]} & \texttt{[0, 0]} & + \texttt{[0, 0]} & \texttt{[0, 0]} & \texttt{[0, 0, 0]} \\ + $\textsf{weight}_{p~1}$ & 48 & 48 & 32 & 48 & 24 \\ + $\textsf{sample}_{p~1~s}$ & \texttt{[28, 26]} & \texttt{[0, 0]} & + \texttt{[0, 0]} & \texttt{[0, 0]} & \texttt{[0, 0, 0]} \\ + \hline + \end{tabular} +} +\end{table} +\par +\noindent +which we apply to the residuals from the bitstream sub-block: +\par +\noindent +{\relsize{-1} + \begin{tabular}{|r|r|r|r|r|r|} + \hline + $\textsf{channel}_{0~i}$ & + after $\textbf{pass}_0$ & + after $\textbf{pass}_1$ & + after $\textbf{pass}_2$ & + after $\textbf{pass}_3$ & + after $\textbf{pass}_4$ \\ + \hline + -64 & -61 & -61 & -61 & -61 & -61 \\ + -46 & -43 & -39 & -39 & -33 & -33 \\ + -25 & -23 & -21 & -19 & -18 & -18 \\ + -3 & -2 & -1 & 0 & 0 & 1 \\ + 20 & 20 & 20 & 21 & 20 & 20 \\ + 41 & 39 & 37 & 37 & 35 & 35 \\ + 60 & 57 & 54 & 53 & 50 & 50 \\ + 75 & 71 & 67 & 66 & 62 & 62 \\ + 85 & 80 & 75 & 73 & 68 & 68 \\ + 90 & 84 & 79 & 77 & 72 & 71 \\ + \hline + \hline + $\textsf{channel}_{1~i}$ & + after $\textbf{pass}_0$ & + after $\textbf{pass}_1$ & + after $\textbf{pass}_2$ & + after $\textbf{pass}_3$ & + after $\textbf{pass}_4$ \\ + \hline + 32 & 31 & 31 & 31 & 31 & 31 \\ + 39 & 37 & 35 & 35 & 32 & 32 \\ + 43 & 41 & 39 & 38 & 36 & 36 \\ + 45 & 43 & 41 & 40 & 38 & 37 \\ + 44 & 41 & 39 & 38 & 36 & 35 \\ + 40 & 38 & 36 & 34 & 32 & 31 \\ + 34 & 32 & 30 & 28 & 26 & 25 \\ + 25 & 23 & 21 & 20 & 19 & 18 \\ + 15 & 14 & 13 & 12 & 11 & 10 \\ + 4 & 3 & 2 & 1 & 1 & 0 \\ + \hline + \end{tabular} +} +\par +\noindent +Resulting in final correlated samples: +\newline +\begin{tabular}{rr} +$\textsf{residual}_0$ : & \texttt{[-61,~-33,~-18,~~1,~20,~35,~50,~62,~68,~71]} \\ +$\textsf{residual}_1$ : & \texttt{[~31,~~32,~~36,~37,~35,~31,~25,~18,~10,~~0]} \\ +\end{tabular} + +\clearpage + +{\relsize{-2} +\begin{tabular}{r||r|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}} +& $i$ & \textsf{uncorrelated}_i & \textsf{temp}_i & \textsf{correlated}_i & \textsf{weight}_{i + 1} \\ +\hline +%%START +\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_0$ - term 18\end{sideways}} +& 0 & -64 & +\lfloor(3 \times -73 + 78) \div 2\rfloor = -71 & +-64 - \lfloor(48 \times -71 + 2 ^ 9) \div 2 ^ {10}\rfloor = -61 & +48 + 2 = 50 +\\ +& 1 & -46 & +\lfloor(3 \times -64 + 73) \div 2\rfloor = -60 & +-46 - \lfloor(50 \times -60 + 2 ^ 9) \div 2 ^ {10}\rfloor = -43 & +50 + 2 = 52 +\\ +& 2 & -25 & +\lfloor(3 \times -46 + 64) \div 2\rfloor = -37 & +-25 - \lfloor(52 \times -37 + 2 ^ 9) \div 2 ^ {10}\rfloor = -23 & +52 + 2 = 54 +\\ +& 3 & -3 & +\lfloor(3 \times -25 + 46) \div 2\rfloor = -15 & +-3 - \lfloor(54 \times -15 + 2 ^ 9) \div 2 ^ {10}\rfloor = -2 & +54 + 2 = 56 +\\ +& 4 & 20 & +\lfloor(3 \times -3 + 25) \div 2\rfloor = 8 & +20 - \lfloor(56 \times 8 + 2 ^ 9) \div 2 ^ {10}\rfloor = 20 & +56 + 2 = 58 +\\ +& 5 & 41 & +\lfloor(3 \times 20 + 3) \div 2\rfloor = 31 & +41 - \lfloor(58 \times 31 + 2 ^ 9) \div 2 ^ {10}\rfloor = 39 & +58 + 2 = 60 +\\ +& 6 & 60 & +\lfloor(3 \times 41 - 20) \div 2\rfloor = 51 & +60 - \lfloor(60 \times 51 + 2 ^ 9) \div 2 ^ {10}\rfloor = 57 & +60 + 2 = 62 +\\ +& 7 & 75 & +\lfloor(3 \times 60 - 41) \div 2\rfloor = 69 & +75 - \lfloor(62 \times 69 + 2 ^ 9) \div 2 ^ {10}\rfloor = 71 & +62 + 2 = 64 +\\ +& 8 & 85 & +\lfloor(3 \times 75 - 60) \div 2\rfloor = 82 & +85 - \lfloor(64 \times 82 + 2 ^ 9) \div 2 ^ {10}\rfloor = 80 & +64 + 2 = 66 +\\ +& 9 & 90 & +\lfloor(3 \times 85 - 75) \div 2\rfloor = 90 & +90 - \lfloor(66 \times 90 + 2 ^ 9) \div 2 ^ {10}\rfloor = 84 & +66 + 2 = 68 +\\ +\hline +\hline +\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_1$ - term 18\end{sideways}} +& 0 & -61 & +\lfloor(3 \times 0 - 0) \div 2\rfloor = 0 & +-61 - \lfloor(48 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor = -61 & +48 + 0 = 48 +\\ +& 1 & -43 & +\lfloor(3 \times -61 - 0) \div 2\rfloor = -92 & +-43 - \lfloor(48 \times -92 + 2 ^ 9) \div 2 ^ {10}\rfloor = -39 & +48 + 2 = 50 +\\ +& 2 & -23 & +\lfloor(3 \times -43 + 61) \div 2\rfloor = -34 & +-23 - \lfloor(50 \times -34 + 2 ^ 9) \div 2 ^ {10}\rfloor = -21 & +50 + 2 = 52 +\\ +& 3 & -2 & +\lfloor(3 \times -23 + 43) \div 2\rfloor = -13 & +-2 - \lfloor(52 \times -13 + 2 ^ 9) \div 2 ^ {10}\rfloor = -1 & +52 + 2 = 54 +\\ +& 4 & 20 & +\lfloor(3 \times -2 + 23) \div 2\rfloor = 8 & +20 - \lfloor(54 \times 8 + 2 ^ 9) \div 2 ^ {10}\rfloor = 20 & +54 + 2 = 56 +\\ +& 5 & 39 & +\lfloor(3 \times 20 + 2) \div 2\rfloor = 31 & +39 - \lfloor(56 \times 31 + 2 ^ 9) \div 2 ^ {10}\rfloor = 37 & +56 + 2 = 58 +\\ +& 6 & 57 & +\lfloor(3 \times 39 - 20) \div 2\rfloor = 48 & +57 - \lfloor(58 \times 48 + 2 ^ 9) \div 2 ^ {10}\rfloor = 54 & +58 + 2 = 60 +\\ +& 7 & 71 & +\lfloor(3 \times 57 - 39) \div 2\rfloor = 66 & +71 - \lfloor(60 \times 66 + 2 ^ 9) \div 2 ^ {10}\rfloor = 67 & +60 + 2 = 62 +\\ +& 8 & 80 & +\lfloor(3 \times 71 - 57) \div 2\rfloor = 78 & +80 - \lfloor(62 \times 78 + 2 ^ 9) \div 2 ^ {10}\rfloor = 75 & +62 + 2 = 64 +\\ +& 9 & 84 & +\lfloor(3 \times 80 - 71) \div 2\rfloor = 84 & +84 - \lfloor(64 \times 84 + 2 ^ 9) \div 2 ^ {10}\rfloor = 79 & +64 + 2 = 66 +\\ +\hline +\hline +\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_2$ - term 2\end{sideways}} +& 0 & -61 & & +-61 - \lfloor(32 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor = -61 & +32 + 0 = 32 +\\ +& 1 & -39 & & +-39 - \lfloor(32 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor = -39 & +32 + 0 = 32 +\\ +& 2 & -21 & & +-21 - \lfloor(32 \times -61 + 2 ^ 9) \div 2 ^ {10}\rfloor = -19 & +32 + 2 = 34 +\\ +& 3 & -1 & & +-1 - \lfloor(34 \times -39 + 2 ^ 9) \div 2 ^ {10}\rfloor = 0 & +34 + 0 = 34 +\\ +& 4 & 20 & & +20 - \lfloor(34 \times -21 + 2 ^ 9) \div 2 ^ {10}\rfloor = 21 & +34 - 2 = 32 +\\ +& 5 & 37 & & +37 - \lfloor(32 \times -1 + 2 ^ 9) \div 2 ^ {10}\rfloor = 37 & +32 - 2 = 30 +\\ +& 6 & 54 & & +54 - \lfloor(30 \times 20 + 2 ^ 9) \div 2 ^ {10}\rfloor = 53 & +30 + 2 = 32 +\\ +& 7 & 67 & & +67 - \lfloor(32 \times 37 + 2 ^ 9) \div 2 ^ {10}\rfloor = 66 & +32 + 2 = 34 +\\ +& 8 & 75 & & +75 - \lfloor(34 \times 54 + 2 ^ 9) \div 2 ^ {10}\rfloor = 73 & +34 + 2 = 36 +\\ +& 9 & 79 & & +79 - \lfloor(36 \times 67 + 2 ^ 9) \div 2 ^ {10}\rfloor = 77 & +36 + 2 = 38 +\\ +\hline +\hline +\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_3$ - term 17\end{sideways}} +& 0 & -61 & +2 \times 0 - 0 = 0 & +-61 - \lfloor(48 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor = -61 & +48 + 0 = 48 +\\ +& 1 & -39 & +2 \times -61 - 0 = -122 & +-39 - \lfloor(48 \times -122 + 2 ^ 9) \div 2 ^ {10}\rfloor = -33 & +48 + 2 = 50 +\\ +& 2 & -19 & +2 \times -39 + 61 = -17 & +-19 - \lfloor(50 \times -17 + 2 ^ 9) \div 2 ^ {10}\rfloor = -18 & +50 + 2 = 52 +\\ +& 3 & 0 & +2 \times -19 + 39 = 1 & +0 - \lfloor(52 \times 1 + 2 ^ 9) \div 2 ^ {10}\rfloor = 0 & +52 + 0 = 52 +\\ +& 4 & 21 & +2 \times 0 + 19 = 19 & +21 - \lfloor(52 \times 19 + 2 ^ 9) \div 2 ^ {10}\rfloor = 20 & +52 + 2 = 54 +\\ +& 5 & 37 & +2 \times 21 - 0 = 42 & +37 - \lfloor(54 \times 42 + 2 ^ 9) \div 2 ^ {10}\rfloor = 35 & +54 + 2 = 56 +\\ +& 6 & 53 & +2 \times 37 - 21 = 53 & +53 - \lfloor(56 \times 53 + 2 ^ 9) \div 2 ^ {10}\rfloor = 50 & +56 + 2 = 58 +\\ +& 7 & 66 & +2 \times 53 - 37 = 69 & +66 - \lfloor(58 \times 69 + 2 ^ 9) \div 2 ^ {10}\rfloor = 62 & +58 + 2 = 60 +\\ +& 8 & 73 & +2 \times 66 - 53 = 79 & +73 - \lfloor(60 \times 79 + 2 ^ 9) \div 2 ^ {10}\rfloor = 68 & +60 + 2 = 62 +\\ +& 9 & 77 & +2 \times 73 - 66 = 80 & +77 - \lfloor(62 \times 80 + 2 ^ 9) \div 2 ^ {10}\rfloor = 72 & +62 + 2 = 64 +\\ +\hline +\hline +\multirow{10}{1em}{\begin{sideways}$\textbf{pass}_4$ - term 3\end{sideways}} +& 0 & -61 & & +-61 - \lfloor(16 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor = -61 & +16 + 0 = 16 +\\ +& 1 & -33 & & +-33 - \lfloor(16 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor = -33 & +16 + 0 = 16 +\\ +& 2 & -18 & & +-18 - \lfloor(16 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor = -18 & +16 + 0 = 16 +\\ +& 3 & 0 & & +0 - \lfloor(16 \times -61 + 2 ^ 9) \div 2 ^ {10}\rfloor = 1 & +16 - 2 = 14 +\\ +& 4 & 20 & & +20 - \lfloor(14 \times -33 + 2 ^ 9) \div 2 ^ {10}\rfloor = 20 & +14 - 2 = 12 +\\ +& 5 & 35 & & +35 - \lfloor(12 \times -18 + 2 ^ 9) \div 2 ^ {10}\rfloor = 35 & +12 - 2 = 10 +\\ +& 6 & 50 & & +50 - \lfloor(10 \times 0 + 2 ^ 9) \div 2 ^ {10}\rfloor = 50 & +10 + 0 = 10 +\\ +& 7 & 62 & & +62 - \lfloor(10 \times 20 + 2 ^ 9) \div 2 ^ {10}\rfloor = 62 & +10 + 2 = 12 +\\ +& 8 & 68 & & +68 - \lfloor(12 \times 35 + 2 ^ 9) \div 2 ^ {10}\rfloor = 68 & +12 + 2 = 14 +\\ +& 9 & 72 & & +72 - \lfloor(14 \times 50 + 2 ^ 9) \div 2 ^ {10}\rfloor = 71 & +14 + 2 = 16 +\\ +%%END +\end{tabular} +} +\begin{center} +$\text{channel}_0$ correlation passes +\end{center}
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/encode/entropy.tex
Added
@@ -0,0 +1,69 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\subsection{Writing Entropy Variables} +\label{wavpack:write_entropy} +\ALGORITHM{3 entropy variables per channel, channel count}{entropy variables sub block data} +\SetKwData{ENTROPY}{entropy} +\SetKwFunction{WVLOG}{wv\_log2} +$\WVLOG(\text{\ENTROPY}_{0~0}) \rightarrow$ \WRITE 16 signed bits\; +$\WVLOG(\text{\ENTROPY}_{0~1}) \rightarrow$ \WRITE 16 signed bits\; +$\WVLOG(\text{\ENTROPY}_{0~2}) \rightarrow$ \WRITE 16 signed bits\; +\If{$\text{channel count} = 2$}{ + $\WVLOG(\text{\ENTROPY}_{1~0}) \rightarrow$ \WRITE 16 signed bits\; + $\WVLOG(\text{\ENTROPY}_{1~1}) \rightarrow$ \WRITE 16 signed bits\; + $\WVLOG(\text{\ENTROPY}_{1~2}) \rightarrow$ \WRITE 16 signed bits\; +} +\Return entropy variables sub block data\; +\EALGORITHM + +\begin{figure}[h] + \includegraphics{wavpack/figures/entropy_vars.pdf} +\end{figure} + +\clearpage + +\subsubsection{Writing Entropy Variables Example} + +\begin{table}[h] +{\relsize{-2} + \renewcommand{\arraystretch}{1.5} +\begin{tabular}{r|>{$}r<{$}|>{$}r<{$}|>{$}r<{$}} + $\text{entropy}_{c~i}$ & $a$ & $c$ & \text{value}_{c~i} \\ + \hline + 118 & + |118| + \lfloor |118| \div 2 ^ 9\rfloor = 118 & + \lfloor\log_2(118)\rfloor + 1 = 7 & + 7 \times 2 ^ 8 + \texttt{wlog}((118 \times 2 ^ {9 - 7}) \bmod 256) = 2018 \\ + 194 & + |194| + \lfloor |194| \div 2 ^ 9\rfloor = 194 & + \lfloor\log_2(194)\rfloor + 1 = 8 & + 8 \times 2 ^ 8 + \texttt{wlog}((194 \times 2 ^ {9 - 8}) \bmod 256) = 2202 \\ + 322 & + |322| + \lfloor |322| \div 2 ^ 9\rfloor = 322 & + \lfloor\log_2(322)\rfloor + 1 = 9 & + 9 \times 2 ^ 8 + \LOG(\lfloor 322 \div 2 ^ {9 - 9}\rfloor \bmod 256) = 2389 \\ + \hline + 118 & + |118| + \lfloor |118| \div 2 ^ 9\rfloor = 118 & + \lfloor\log_2(118)\rfloor + 1 = 7 & + 7 \times 2 ^ 8 + \texttt{wlog}((118 \times 2 ^ {9 - 7}) \bmod 256) = 2018 \\ + 176 & + |176| + \lfloor |176| \div 2 ^ 9\rfloor = 176 & + \lfloor\log_2(176)\rfloor + 1 = 8 & + 8 \times 2 ^ 8 + \texttt{wlog}((176 \times 2 ^ {9 - 8}) \bmod 256) = 2166 \\ + 212 & + |212| + \lfloor |212| \div 2 ^ 9\rfloor = 212 & + \lfloor\log_2(212)\rfloor + 1 = 8 & + 8 \times 2 ^ 8 + \texttt{wlog}((212 \times 2 ^ {9 - 8}) \bmod 256) = 2234 \\ +\end{tabular} +} +\end{table} +\begin{figure}[h] + \includegraphics{wavpack/figures/entropy_vars_parse.pdf} +\end{figure}
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/encode/samples.tex
Added
@@ -0,0 +1,121 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\subsection{Writing Decorrelation Samples} +\label{wavpack:write_decorr_samples} +\ALGORITHM{a list of decorrelation sample values per pass, per channel}{decorrelation samples sub block data} +\SetKwData{TERM}{term} +\SetKwData{SAMPLE}{sample} +\SetKwFunction{WVLOG}{wv\_log2} +\SetKw{KwDownTo}{downto} +\For{$p \leftarrow \text{decorrelation pass count}$ \emph{\KwDownTo}0}{ + \uIf{$17 \leq \text{\TERM}_p \leq 18$}{ + \For{$c \leftarrow 0$ \emph{\KwTo}channel count}{ + $\WVLOG(\text{\SAMPLE}_{p~c~0}) \rightarrow$ \WRITE 16 signed bits\; + $\WVLOG(\text{\SAMPLE}_{p~c~1}) \rightarrow$ \WRITE 16 signed bits\; + } + } + \uElseIf{$1 \leq \text{\TERM}_p \leq 8$}{ + \For{$s \leftarrow 0$ \emph{\KwTo}$\text{\TERM}_p$}{ + \For{$c \leftarrow 0$ \emph{\KwTo}channel count}{ + $\WVLOG(\text{\SAMPLE}_{p~c~s}) \rightarrow$ \WRITE 16 signed bits\; + } + } + } + \ElseIf{$-3 \leq \text{\TERM}_p \leq -1$}{ + $\WVLOG(\text{\SAMPLE}_{p~0~0}) \rightarrow$ \WRITE 16 signed bits\; + $\WVLOG(\text{\SAMPLE}_{p~1~0}) \rightarrow$ \WRITE 16 signed bits\; + } +} +\Return decorrelation samples data\; +\EALGORITHM + +\begin{figure}[h] + \includegraphics{wavpack/figures/decorr_samples.pdf} +\end{figure} + +\subsubsection{The wv\_log2 Function} +\ALGORITHM{a signed value}{a signed 16 bit value} +\SetKwFunction{LOG}{wlog} +$a \leftarrow |value| + \lfloor |value| \div 2 ^ 9\rfloor$\; +$c \leftarrow $\lIf{$a \neq 0$}{$\lfloor\log_2(a)\rfloor + 1$} +\lElse{$0$}\; +\eIf{$value \geq 0$}{ + \eIf{$0 \leq a < 256$}{ + \Return $c \times 2 ^ 8 + \LOG((a \times 2 ^ {9 - c}) \bmod 256)$\; + }{ + \Return $c \times 2 ^ 8 + \LOG(\lfloor a \div 2 ^ {c - 9}\rfloor \bmod 256)$\; + } +}{ + \eIf{$0 \leq a < 256$}{ + \Return $-(c \times 2 ^ 8 + \LOG((a \times 2 ^ {9 - c}) \bmod 256))$\; + }{ + \Return $-(c \times 2 ^ 8 + \LOG(\lfloor a \div 2 ^ {c - 9}\rfloor \bmod 256))$\; + } +} +\EALGORITHM + +\clearpage + +where \texttt{wlog} is defined from the following table: +\par +\noindent +{\relsize{-3}\ttfamily +\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} +\hline +& \texttt{0x?0} & \texttt{0x?1} & \texttt{0x?2} & \texttt{0x?3} & \texttt{0x?4} & \texttt{0x?5} & \texttt{0x?6} & \texttt{0x?7} & \texttt{0x?8} & \texttt{0x?9} & \texttt{0x?A} & \texttt{0x?B} & \texttt{0x?C} & \texttt{0x?D} & \texttt{0x?E} & \texttt{0x?F} \\ +\hline +\texttt{0x0?} & 0 & 1 & 3 & 4 & 6 & 7 & 9 & 10 & 11 & 13 & 14 & 16 & 17 & 18 & 20 & 21 \\ +\texttt{0x1?} & 22 & 24 & 25 & 26 & 28 & 29 & 30 & 32 & 33 & 34 & 36 & 37 & 38 & 40 & 41 & 42 \\ +\texttt{0x2?} & 44 & 45 & 46 & 47 & 49 & 50 & 51 & 52 & 54 & 55 & 56 & 57 & 59 & 60 & 61 & 62 \\ +\texttt{0x3?} & 63 & 65 & 66 & 67 & 68 & 69 & 71 & 72 & 73 & 74 & 75 & 77 & 78 & 79 & 80 & 81 \\ +\texttt{0x4?} & 82 & 84 & 85 & 86 & 87 & 88 & 89 & 90 & 92 & 93 & 94 & 95 & 96 & 97 & 98 & 99 \\ +\texttt{0x5?} & 100 & 102 & 103 & 104 & 105 & 106 & 107 & 108 & 109 & 110 & 111 & 112 & 113 & 114 & 116 & 117 \\ +\texttt{0x6?} & 118 & 119 & 120 & 121 & 122 & 123 & 124 & 125 & 126 & 127 & 128 & 129 & 130 & 131 & 132 & 133 \\ +\texttt{0x7?} & 134 & 135 & 136 & 137 & 138 & 139 & 140 & 141 & 142 & 143 & 144 & 145 & 146 & 147 & 148 & 149 \\ +\texttt{0x8?} & 150 & 151 & 152 & 153 & 154 & 155 & 155 & 156 & 157 & 158 & 159 & 160 & 161 & 162 & 163 & 164 \\ +\texttt{0x9?} & 165 & 166 & 167 & 168 & 169 & 169 & 170 & 171 & 172 & 173 & 174 & 175 & 176 & 177 & 178 & 178 \\ +\texttt{0xA?} & 179 & 180 & 181 & 182 & 183 & 184 & 185 & 185 & 186 & 187 & 188 & 189 & 190 & 191 & 192 & 192 \\ +\texttt{0xB?} & 193 & 194 & 195 & 196 & 197 & 198 & 198 & 199 & 200 & 201 & 202 & 203 & 203 & 204 & 205 & 206 \\ +\texttt{0xC?} & 207 & 208 & 208 & 209 & 210 & 211 & 212 & 212 & 213 & 214 & 215 & 216 & 216 & 217 & 218 & 219 \\ +\texttt{0xD?} & 220 & 220 & 221 & 222 & 223 & 224 & 224 & 225 & 226 & 227 & 228 & 228 & 229 & 230 & 231 & 231 \\ +\texttt{0xE?} & 232 & 233 & 234 & 234 & 235 & 236 & 237 & 238 & 238 & 239 & 240 & 241 & 241 & 242 & 243 & 244 \\ +\texttt{0xF?} & 244 & 245 & 246 & 247 & 247 & 248 & 249 & 249 & 250 & 251 & 252 & 252 & 253 & 254 & 255 & 255 \\ +\hline +\end{tabular} +} + +\subsubsection{Writing Decorrelation Samples Example} +Given a 2 channel subframe with 5 correlation passes containing +the following correlation samples: +\begin{table}[h] +\begin{tabular}{rrrr} +pass $p$ & $\text{term}_p$ & $\text{sample}_{p~0~s}$ & $\text{sample}_{p~1~s}$ \\ +\hline +0 & 3 & \texttt{[62, 68, 71]} & \texttt{[0, 10, 18]} \\ +1 & 17 & \texttt{[72, 68]} & \texttt{[11, 1]} \\ +2 & 2 & \texttt{[73, 77]} & \texttt{[1, 12]} \\ +3 & 18 & \texttt{[79, 75]} & \texttt{[13, 2]} \\ +4 & 18 & \texttt{[84, 80]} & \texttt{[14, 3]} \\ +\end{tabular} +\end{table} +\par +\noindent +$\text{sample}_{4~0~0}$ (pass 4, channel 0, sample 0) is encoded as: +\begin{align*} +a &\leftarrow |84| + \lfloor|84| \div 2 ^ 9\rfloor = 84 \\ +c &\leftarrow \lfloor\log_2(84)\rfloor + 1 = 7 \\ +value &\leftarrow 7 \times 2 ^ 8 + \texttt{wlog}((84 \times 2 ^ 2) \bmod 256) \\ +&\leftarrow 1792 + \texttt{wlog}(\texttt{0x50}) = 1892 = \texttt{0x764} +\end{align*} +\par +\noindent +and the entire sub block is written as: +\begin{figure}[h] +\includegraphics{wavpack/figures/decorr_samples_encode.pdf} +\end{figure}
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/encode/terms.tex
Added
@@ -0,0 +1,43 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\subsection{Writing Decorrelation Terms} +\label{wavpack:write_decorr_terms} +\ALGORITHM{a list of decorrelation terms, a list of decorrelation deltas}{decorrelation terms sub block data} +\SetKwData{TERM}{term} +\SetKwData{DELTA}{delta} +\SetKwData{KwDownTo}{downto} +\For(\tcc*[f]{populate in reverse order}){$p \leftarrow \text{decorrelation pass count}$ \emph{\KwDownTo}0}{ + $\text{\TERM}_p + 5 \rightarrow$ \WRITE 5 unsigned bits\; + $\text{\DELTA}_p \rightarrow$ \WRITE 3 unsigned bits\; +} +\Return decorrelation terms sub block data\; +\EALGORITHM +\begin{figure}[h] + \includegraphics{wavpack/figures/decorr_terms.pdf} +\end{figure} + +\clearpage +For example, given decorrelation terms and deltas: +\begin{table}[h] +\begin{tabular}{rrr} +$p$ & $\textsf{term}_p$ & $\textsf{delta}_p$ \\ +\hline +0 & 3 & 2 \\ +1 & 17 & 2 \\ +2 & 2 & 2 \\ +3 & 18 & 2 \\ +4 & 18 & 2 \\ +\end{tabular} +\end{table} +\par +\noindent +the decorrelation terms sub block is written as: +\begin{figure}[h] +\includegraphics{wavpack/figures/terms_parse.pdf} +\end{figure}
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/encode/weights.tex
Added
@@ -0,0 +1,57 @@ +%This work is licensed under the +%Creative Commons Attribution-Share Alike 3.0 United States License. +%To view a copy of this license, visit +%http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter to +%Creative Commons, +%171 Second Street, Suite 300, +%San Francisco, California, 94105, USA. + +\subsection{Writing Decorrelation Weights} +\label{wavpack:write_decorr_weights} +\ALGORITHM{a list of decorrelation weights per channel}{decorrelation weights sub block data} +\SetKwData{WEIGHT}{weight} +\SetKwFunction{STOREWEIGHT}{store\_weight} +\SetKwData{KwDownTo}{downto} +\For(\tcc*[f]{populate in reverse order}){$p \leftarrow \text{decorrelation pass count}$ \emph{\KwDownTo}0}{ + $\texttt{\STOREWEIGHT}(\text{\WEIGHT}_{p~0}) \rightarrow$ \WRITE 8 signed bits\; + \If{$\text{channel count} = 2$}{ + $\texttt{\STOREWEIGHT}(\text{\WEIGHT}_{p~1}) \rightarrow$ \WRITE 8 signed bits\; + } +} +\Return decorrelation weights sub block data\; +\EALGORITHM +\par +\noindent +where \texttt{store\_weight} is defined as: +\begin{equation*} +\texttt{store\_weight}(w) = +\begin{cases} +\left\lfloor\frac{\texttt{min}(w, 1024) - \lfloor(\texttt{min}(w,1024) + 2 ^ 6) \div 2 ^ 7\rfloor + 4}{2 ^ 3}\right\rfloor & \text{ if } w > 0 \\ +0 & \text{ if } w = 0 \\ +\left\lfloor \frac{\texttt{max}(w, -1024) + 4}{2 ^ 3} \right\rfloor & \text{ if } w < 0 \\ +\end{cases} +\end{equation*} +\begin{figure}[h] + \includegraphics{wavpack/figures/decorr_weights.pdf} +\end{figure} +\clearpage +For example, given the decorrelation weight values: +\begin{table}[h] +\begin{tabular}{rrrrr} +$p$ & $\textsf{weight}_{p~0}$ & $\textsf{weight}_{p~1}$ & +$\texttt{store\_weight}(\textsf{weight}_{p~0})$ & +$\texttt{store\_weight}(\textsf{weight}_{p~1})$ \\ +\hline +0 & 16 & 24 & 2 & 3 \\ +1 & 48 & 48 & 6 & 6 \\ +2 & 32 & 32 & 4 & 4 \\ +3 & 48 & 48 & 6 & 6 \\ +4 & 48 & 48 & 6 & 6 \\ +\end{tabular} +\end{table} +\par +\noindent +the decorrelation weights subframe is written as: +\begin{figure}[h] +\includegraphics{wavpack/figures/decorr_weights_parse.pdf} +\end{figure}
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures
Added
+(directory)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/block_channels.bdx
Changed
(renamed from docs/reference/figures/wavpack/block_channels.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/block_header.bdx
Changed
(renamed from docs/reference/figures/wavpack/block_header.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/block_header2.bdx
Changed
(renamed from docs/reference/figures/wavpack/block_header2.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/block_header_parse.bpx
Changed
(renamed from docs/reference/figures/wavpack/block_header_parse.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/channel_info.bdx
Changed
(renamed from docs/reference/figures/wavpack/channel_info.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decoding_params.bdx
Changed
(renamed from docs/reference/figures/wavpack/decoding_params.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorr_pass0.dat
Changed
(renamed from docs/reference/figures/wavpack/decorr_pass0.dat)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorr_pass1.dat
Changed
(renamed from docs/reference/figures/wavpack/decorr_pass1.dat)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorr_pass2.dat
Changed
(renamed from docs/reference/figures/wavpack/decorr_pass2.dat)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorr_pass3.dat
Changed
(renamed from docs/reference/figures/wavpack/decorr_pass3.dat)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorr_pass4.dat
Changed
(renamed from docs/reference/figures/wavpack/decorr_pass4.dat)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorr_pass5.dat
Changed
(renamed from docs/reference/figures/wavpack/decorr_pass5.dat)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorr_samples.bdx
Changed
(renamed from docs/reference/figures/wavpack/decorr_samples.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorr_samples2.dot
Changed
(renamed from docs/reference/figures/wavpack/decorr_samples2.dot)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorr_samples_encode.bpx
Changed
(renamed from docs/reference/figures/wavpack/decorr_samples_encode.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorr_samples_parse.bpx
Changed
(renamed from docs/reference/figures/wavpack/decorr_samples_parse.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorr_terms.bdx
Changed
(renamed from docs/reference/figures/wavpack/decorr_terms.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorr_weights.bdx
Changed
(renamed from docs/reference/figures/wavpack/decorr_weights.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorr_weights_parse.bpx
Changed
(renamed from docs/reference/figures/wavpack/decorr_weights_parse.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorrelation0.plot
Added
@@ -0,0 +1,11 @@ +#!/usr/bin/gnuplot + +set terminal fig color +set title "Bitstream Values" +set xlabel "i" +set ylabel "value" +set border 3 +set xtics nomirror +set ytics nomirror + +plot "wavpack/figures/decorr_pass0.dat" title 'residual' with lines;
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorrelation1.plot
Added
@@ -0,0 +1,11 @@ +#!/usr/bin/gnuplot + +set terminal fig color +set title "Decorrelation Pass 1" +set xlabel "i" +set ylabel "value" +set border 3 +set xtics nomirror +set ytics nomirror + +plot "wavpack/figures/decorr_pass1.dat" title 'pass 1' with lines;
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorrelation2.plot
Added
@@ -0,0 +1,11 @@ +#!/usr/bin/gnuplot + +set terminal fig color +set title "Decorrelation Pass 2" +set xlabel "i" +set ylabel "value" +set border 3 +set xtics nomirror +set ytics nomirror + +plot "wavpack/figures/decorr_pass2.dat" title 'pass 2' with lines;
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorrelation3.plot
Added
@@ -0,0 +1,11 @@ +#!/usr/bin/gnuplot + +set terminal fig color +set title "Decorrelation Pass 3" +set xlabel "i" +set ylabel "value" +set border 3 +set xtics nomirror +set ytics nomirror + +plot "wavpack/figures/decorr_pass3.dat" title 'pass 3' with lines;
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorrelation4.plot
Added
@@ -0,0 +1,11 @@ +#!/usr/bin/gnuplot + +set terminal fig color +set title "Decorrelation Pass 4" +set xlabel "i" +set ylabel "value" +set border 3 +set xtics nomirror +set ytics nomirror + +plot "wavpack/figures/decorr_pass4.dat" title 'pass 4' with lines;
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorrelation5.plot
Added
@@ -0,0 +1,11 @@ +#!/usr/bin/gnuplot + +set terminal fig color +set title "Decorrelation Pass 5" +set xlabel "i" +set ylabel "value" +set border 3 +set xtics nomirror +set ytics nomirror + +plot "wavpack/figures/decorr_pass5.dat" title 'pass 5' with lines;
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/decorrelation_passes.plot
Added
@@ -0,0 +1,11 @@ +#!/usr/bin/gnuplot + +set terminal fig color +set title "decorrelation passes" +set xlabel "time" +set ylabel "intensity" +set border 3 +set xtics nomirror +set ytics nomirror + +plot "wavpack/figures/decorrelation0.dat" title 'residuals' with steps,"wavpack/figures/decorrelation1.dat" title 'pass 1' with steps, "wavpack/figures/decorrelation2.dat" title 'pass 2' with steps, "wavpack/figures/decorrelation3.dat" title 'pass 3' with steps, "wavpack/figures/decorrelation4.dat" title 'pass 4' with steps;
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/entropy_vars.bdx
Changed
(renamed from docs/reference/figures/wavpack/entropy_vars.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/entropy_vars_parse.bpx
Changed
(renamed from docs/reference/figures/wavpack/entropy_vars_parse.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/extended_integers.bdx
Added
@@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<diagram> + <row> + <col end="255" start="0" width=".20">Block Header</col> + <col start="256" width=".16">Sub Block₀</col> + <col width=".16">Sub Block₁</col> + <col width=".16">Sub Block₂</col> + <col width=".16">Sub Block₃</col> + <col width=".16" id="subblock">Sub Block₄</col> + </row> + <spacer/> + <row> + <col start="0" end="4" width=".34" + id="subblock_s">metadata function (9)</col> + <col start="5" end="5" width=".22">nondecoder (0)</col> + <col start="6" end="6" width=".22">actual size 1 less</col> + <col start="7" end="7" width=".22" id="subblock_e">large sub block (0)</col> + </row> + <row> + <col start="8" end="15">sub block size (16)</col> + </row> + <row> + <col start="16" end="23" width=".25">Sent Bits</col> + <col start="24" end="31" width=".25">Zero Bits</col> + <col start="32" end="39" width=".25">One Bits</col> + <col start="40" end="47" width=".25">Duplicate Bits</col> + </row> + <line style="dotted"> + <start id="subblock" corner="sw"/> + <end id="subblock_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="subblock" corner="se"/> + <end id="subblock_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/md5sum.bdx
Added
@@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<diagram> + <row> + <col end="255" start="0" width=".20">Block Header</col> + <col start="256" width=".16">Sub Block₀</col> + <col width=".16">Sub Block₁</col> + <col width=".16">Sub Block₂</col> + <col width=".16">Sub Block₃</col> + <col width=".16" id="subblock">Sub Block₄</col> + </row> + <spacer/> + <row> + <col start="0" end="4" width=".34" + id="subblock_s">metadata function (6)</col> + <col start="5" end="5" width=".22">nondecoder (0)</col> + <col start="6" end="6" width=".22">actual size 1 less</col> + <col start="7" end="7" width=".22" id="subblock_e">large sub block (0)</col> + </row> + <row> + <col start="8" end="15">block size (16)</col> + </row> + <row> + <col start="16" end="271">MD5 sum</col> + </row> + <line style="dotted"> + <start id="subblock" corner="sw"/> + <end id="subblock_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="subblock" corner="se"/> + <end id="subblock_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/pcm_sandwich.bdx
Changed
(renamed from docs/reference/figures/wavpack/pcm_sandwich.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/residuals.bdx
Changed
(renamed from docs/reference/figures/wavpack/residuals.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/residuals_parse.bpx
Changed
(renamed from docs/reference/figures/wavpack/residuals_parse.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/residuals_parse2.bpx
Changed
(renamed from docs/reference/figures/wavpack/residuals_parse2.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/sample_rate.bdx
Changed
(renamed from docs/reference/figures/wavpack/sample_rate.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/stream.bdx
Changed
(renamed from docs/reference/figures/wavpack/stream.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/subblock.bdx
Added
@@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<diagram> + <row> + <col end="255" start="0" width=".22">Block Header</col> + <col start="256" width=".22" id="subblock">Sub Block₀</col> + <col width=".22">Sub Block₁</col> + <col style="dashed" width=".22">...</col> + </row> + <spacer/> + <row> + <col start="0" end="4" width=".34" id="subblock_s">metadata function</col> + <col start="5" end="5" width=".22">nondecoder data</col> + <col start="6" end="6" width=".22">actual size 1 less</col> + <col start="7" end="7" width=".22" id="subblock_e">large sub block</col> + </row> + <row> + <col start="8" end="15/31" width=".45">sub block size</col> + <col width=".45" style="dashed" + start="(sub block size × 2) bytes" + end="(sub block size × 2) bytes">sub block data</col> + <col width=".1" style="dashed" start="0" end="7">pad</col> + </row> + <line style="dotted"> + <start id="subblock" corner="sw"/> + <end id="subblock_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="subblock" corner="se"/> + <end id="subblock_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/subblock_header.bdx
Changed
(renamed from docs/reference/figures/wavpack/subblock_header.bdx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/subblock_parse.bpx
Changed
(renamed from docs/reference/figures/wavpack/subblock_parse.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/terms_parse.bpx
Changed
(renamed from docs/reference/figures/wavpack/terms_parse.bpx)
View file
audiotools-2.19.tar.gz/docs/reference/wavpack/figures/typical_block.bdx
Added
@@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="utf-8"?> +<diagram> + <row> + <col width=".20">Block₀</col> + <col width=".20" id="block">Block₁</col> + <col width=".20">Block₂</col> + <col width=".20">Block₃</col> + <col width=".20" style="dashed"/> + </row> + <spacer/> + <row> + <col width=".25" start="0" end="255" id="block_header">Block Header</col> + <col width=".15" id="subblock_0">Sub Block₀</col> + <col width=".15">Sub Block₁</col> + <col width=".15">Sub Block₂</col> + <col width=".15">Sub Block₃</col> + <col width=".15" id="subblock_4">Sub Block₄</col> + </row> + <spacer height=".5"/> + <row> + <col width=".2" style="blank"/> + <col width=".3" id="subblocks_s">Sub Block Header</col> + <col width=".5" id="subblocks_e">Decorrelation Terms and Deltas</col> + </row> + <row> + <col width=".2" style="blank"/> + <col width=".3">Sub Block Header</col> + <col width=".5">Decorrelation Weights</col> + </row> + <row> + <col width=".2" style="blank"/> + <col width=".3">Sub Block Header</col> + <col width=".5">Decorrelation Samples</col> + </row> + <row> + <col width=".2" style="blank"/> + <col width=".3">Sub Block Header</col> + <col width=".5">Entropy Variables</col> + </row> + <row> + <col width=".2" style="blank"/> + <col width=".3">Sub Block Header</col> + <col width=".5">Bitstream</col> + </row> + <line style="dotted"> + <start id="block" corner="sw"/> + <end id="block_header" corner="nw"/> + </line> + <line style="dotted"> + <start id="block" corner="se"/> + <end id="subblock_4" corner="ne"/> + </line> + <line style="dotted"> + <start id="subblock_0" corner="sw"/> + <end id="subblocks_s" corner="nw"/> + </line> + <line style="dotted"> + <start id="subblock_4" corner="se"/> + <end id="subblocks_e" corner="ne"/> + </line> +</diagram>
View file
audiotools-2.18.tar.gz/docs/track2cd.xml -> audiotools-2.19.tar.gz/docs/track2cd.xml
Changed
@@ -12,10 +12,6 @@ </description> <options> <option short="h" long="help">show a list of options and exit</option> - <option short="V" long="verbose" arg="verbosity"> - The level of output to display. - Choose between 'normal', 'quiet' and 'debug. - </option> <option short="c" long="cdrom" arg="cdrom"> the cdrom device to write the CD to </option> @@ -35,6 +31,10 @@ to use all of them simultaneously can reduce the time needed to decode tracks prior to CD burning. </option> + <option short="V" long="verbose" arg="verbosity"> + The level of output to display. + Choose between 'normal', 'quiet' and 'debug'. + </option> </options> <examples> <example>
View file
audiotools-2.18.tar.gz/docs/track2track.xml -> audiotools-2.19.tar.gz/docs/track2track.xml
Changed
@@ -11,9 +11,13 @@ </description> <options> <option short="h" long="help">show a list of options and exit</option> + <option short="I" long="interactive"> + edit metadata and encoding options in interactive mode + prior to converting tracks + </option> <option short="V" long="verbose" arg="verbosity"> The level of output to display. - Choose between 'normal', 'quiet' and 'debug. + Choose between 'normal', 'quiet' and 'debug'. </option> </options> <options category="conversion"> @@ -50,10 +54,35 @@ to use all of them simultaneously can greatly increase encoding speed. </option> </options> - <options category="metadata"> - <option short="T" long="thumbnail"> - convert embedded images to smaller thumbnails during conversion + <options category="CD lookup"> + <option short="M" long="metadata-lookup"> + perform metadata lookup for converted tracks + </option> + <option long="musicbrainz-server" arg="hostname"> + the MusicBrainz server name to query for metadata + </option> + <option long="musicbrainz-port" arg="port"> + the MusicBrainz port to query for metadata + </option> + <option long="no-musicbrainz"> + don't query MusicBrainz for metadata + </option> + <option long="freedb-server" arg="hostname"> + the FreeDB server name to query for metadata </option> + <option long="freedb-port" arg="port"> + the FreeDB port to query for metadata + </option> + <option long="no-freedb"> + don't query FreeDB for metadata + </option> + <option short="D" long="default"> + When multiple metadata choices are available, + select the first one automatically. + This option has no effect when used with -I + </option> + </options> + <options category="metadata"> <option long="replay-gain"> add ReplayGain metadata to newly created tracks </option>
View file
audiotools-2.18.tar.gz/docs/trackcat.xml -> audiotools-2.19.tar.gz/docs/trackcat.xml
Changed
@@ -4,7 +4,7 @@ <author>Brian Langenberger</author> <section>1</section> <name>concatenate two or more audio tracks</name> - <title>Audio Concatenater</title> + <title>Audio Concatenator</title> <synopsis>[OPTIONS] <track 1> [track 2] [track 3] ...</synopsis> <description> trackcat combines the audio data from two or more audio tracks @@ -12,15 +12,22 @@ </description> <options> <option short="h" long="help">show a list of options and exit</option> - <option short="V" long="verbose" arg="verbosity"> - The level of output to display. - Choose between 'normal', 'quiet' and 'debug. + <option short="I" long="interactive"> + edit metadata and encoding options in interactive mode + prior to converting tracks </option> <option long="cue" arg="filename"> a cuesheet to embed in the output file </option> + <option short="V" long="verbose" arg="verbosity"> + The level of output to display. + Choose between 'normal', 'quiet' and 'debug'. + </option> </options> <options category="encoding"> + <option short="o" long="output" arg="filename"> + the output filename of the concatenated track + </option> <option short="t" long="type" arg="type"> the audio format of the concatenated track; for a list of available audio formats, try: -t help @@ -29,8 +36,33 @@ the desired quality of the concatenated track; for a list of available quality modes for a given format, try: -q help </option> - <option short="o" long="output" arg="filename"> - the output filename of the concatenated track + </options> + <options category="CD lookup"> + <option short="M" long="metadata-lookup"> + perform metadata lookup for converted tracks + </option> + <option long="musicbrainz-server" arg="hostname"> + the MusicBrainz server name to query for metadata + </option> + <option long="musicbrainz-port" arg="port"> + the MusicBrainz port to query for metadata + </option> + <option long="no-musicbrainz"> + don't query MusicBrainz for metadata + </option> + <option long="freedb-server" arg="hostname"> + the FreeDB server name to query for metadata + </option> + <option long="freedb-port" arg="port"> + the FreeDB port to query for metadata + </option> + <option long="no-freedb"> + don't query FreeDB for metadata + </option> + <option short="D" long="default"> + When multiple metadata choices are available, + select the first one automatically. + This option has no effect when used with -I </option> </options> <examples>
View file
audiotools-2.18.tar.gz/docs/trackcmp.xml -> audiotools-2.19.tar.gz/docs/trackcmp.xml
Changed
@@ -19,10 +19,6 @@ </description> <options> <option short="h" long="help">show a list of options and exit</option> - <option short="V" long="verbose" arg="verbosity"> - The level of output to display. - Choose between 'normal', 'quiet' and 'debug. - </option> <option short="j" long="joint" arg="processes"> The maximum number of tracks to compare at one time. If one has multiple CPUs or CPU cores, allowing @@ -32,5 +28,9 @@ <option short="R" long="no-summary"> suppress summary output </option> + <option short="V" long="verbose" arg="verbosity"> + The level of output to display. + Choose between 'normal', 'quiet' and 'debug. + </option> </options> </manpage>
View file
audiotools-2.18.tar.gz/docs/tracklint.xml -> audiotools-2.19.tar.gz/docs/tracklint.xml
Changed
@@ -17,10 +17,6 @@ </description> <options> <option short="h" long="help">show a list of options and exit</option> - <option short="V" long="verbose" arg="verbosity"> - The level of output to display. - Choose between 'normal', 'quiet' and 'debug. - </option> <option long="fix"> perform tracklint's suggested fixes on the given audio tracks </option> @@ -40,6 +36,10 @@ prior to using this option or else tracklint's undo information will no longer be applicable. </option> + <option short="V" long="verbose" arg="verbosity"> + The level of output to display. + Choose between 'normal', 'quiet' and 'debug'. + </option> </options> <element name="fixes"> <p>The following fixes are performed on each track:</p>
View file
audiotools-2.18.tar.gz/docs/trackplay.xml -> audiotools-2.19.tar.gz/docs/trackplay.xml
Changed
@@ -12,10 +12,6 @@ </description> <options> <option short="h" long="help">show a list of options and exit</option> - <option short="V" long="verbose" arg="verbosity"> - The level of output to display. - Choose between 'normal', 'quiet' and 'debug. - </option> <option short="I" long="interactive"> run in interactive mode; this provides a text-based GUI and allows one to operate @@ -33,6 +29,10 @@ <option long="shuffle"> shuffle tracks randomly before playback </option> + <option short="V" long="verbose" arg="verbosity"> + The level of output to display. + Choose between 'normal', 'quiet' and 'debug'. + </option> </options> <element name="playback controls"> <table>
View file
audiotools-2.18.tar.gz/docs/trackrename.xml -> audiotools-2.19.tar.gz/docs/trackrename.xml
Changed
@@ -12,9 +12,8 @@ </description> <options> <option short="h" long="help">show a list of options and exit</option> - <option short="V" long="verbose" arg="verbosity"> - The level of output to display. - Choose between 'normal', 'quiet' and 'debug. + <option short="I" long="interactive"> + edit format string in interactive mode prior to renaming tracks </option> <option long="format" arg="string"> The format string to use for new filenames. @@ -22,6 +21,10 @@ new tracks are created. All other text is left as-is. If this option is omitted, a default format string is used. </option> + <option short="V" long="verbose" arg="verbosity"> + The level of output to display. + Choose between 'normal', 'quiet' and 'debug'. + </option> </options> <examples> <example>
View file
audiotools-2.18.tar.gz/docs/tracksplit.xml -> audiotools-2.19.tar.gz/docs/tracksplit.xml
Changed
@@ -11,13 +11,16 @@ </description> <options> <option short="h" long="help">show a list of options and exit</option> - <option short="V" long="verbose" arg="verbosity"> - The level of output to display. - Choose between 'normal', 'quiet' and 'debug. + <option short="I" long="interactive"> + edit metadata in interactive mode prior to splitting file </option> <option long="cue" arg="filename"> the cuesheet to use for splitting track </option> + <option short="V" long="verbose" arg="verbosity"> + The level of output to display. + Choose between 'normal', 'quiet' and 'debug'. + </option> </options> <options category="encoding"> <option short="t" long="type" arg="type"> @@ -60,9 +63,6 @@ <option long="no-freedb"> don't query FreeDB for metadata </option> - <option short="I" long="interactive"> - edit metadata in interactive mode prior to splitting file - </option> <option short="D" long="default"> When multiple metadata choices are available, select the first one automatically.
View file
audiotools-2.18.tar.gz/docs/tracktag.xml -> audiotools-2.19.tar.gz/docs/tracktag.xml
Changed
@@ -12,10 +12,6 @@ </description> <options> <option short="h" long="help">show a list of options and exit</option> - <option short="V" long="verbose" arg="verbosity"> - The level of output to display. - Choose between 'normal', 'quiet' and 'debug. - </option> <option short="I" long="interactive"> Once any other command-line arguments are processed, edit the given tracks in interactive mode. @@ -24,20 +20,16 @@ this options erases all existing metadata and replaces it with any specified values </option> - <option long="cue" arg="filename"> - If given along with multiple audio tracks, - this file's ISRC metadata will be added to those tracks; - if given with a single CD image track, this file will - be embedded in that image. - Note that only FLAC and WavPack currently support embedded cuesheets - and not all CDs contain ISRC metadata. - </option> <option long="replay-gain"> add ReplayGain metadata to tracks </option> <option short="j" long="joint" arg="processes"> The maximum number of albums to calculate ReplayGain for at one time. </option> + <option short="V" long="verbose" arg="verbosity"> + The level of output to display. + Choose between 'normal', 'quiet' and 'debug'. + </option> </options> <options category="text"> <option long="name" arg="name"> @@ -99,30 +91,6 @@ a file containing comment text, for particularly long comments </option> </options> - <options category="image"> - <option long="remove-images"> - remove existing images; - if new images are added, existing images will be removed beforehand - </option> - <option long="front-cover" arg="filename"> - an image file of the album's front cover - </option> - <option long="back-cover" arg="filename"> - an image file of the album's back cover - </option> - <option long="leaflet" arg="filename"> - an image file of one of the album's leaflet pages - </option> - <option long="media" arg="filename"> - an image file of the album's media - </option> - <option long="other-image" arg="filename"> - an image file related to the album - </option> - <option long="thumbnail"> - convert images to smaller thumbnails before adding - </option> - </options> <options category="removal"> <option long="remove-name"/> <option long="remove-artist"/> @@ -143,6 +111,34 @@ <option long="remove-copyright"/> <option long="remove-comment"/> </options> + <options category="CD lookup"> + <option short="M" long="metadata-lookup"> + perform metadata lookup for tracks as if they were a single CD + </option> + <option long="musicbrainz-server" arg="hostname"> + the MusicBrainz server name to query for metadata + </option> + <option long="musicbrainz-port" arg="port"> + the MusicBrainz port to query for metadata + </option> + <option long="no-musicbrainz"> + don't query MusicBrainz for metadata + </option> + <option long="freedb-server" arg="hostname"> + the FreeDB server name to query for metadata + </option> + <option long="freedb-port" arg="port"> + the FreeDB port to query for metadata + </option> + <option long="no-freedb"> + don't query FreeDB for metadata + </option> + <option short="D" long="default"> + When multiple metadata choices are available, + select the first one automatically. + This option has no effect when used with -I + </option> + </options> <examples> <example> <description> @@ -155,16 +151,6 @@ </example> <example> <description> - Add several JPEG images to track.flac - </description> - <command> - tracktag --front-cover=front.jpg --back-cover=back.jpg - --leaflet=page1.jpg --leaflet=page2.jpg --leaflet=page3.jpg - track.flac - </command> - </example> - <example> - <description> Add ISRC metadata from the disc in /dev/cdrom to all MP3 files in the current directory </description>
View file
audiotools-2.18.tar.gz/docs/trackverify.xml -> audiotools-2.19.tar.gz/docs/trackverify.xml
Changed
@@ -20,6 +20,9 @@ May be used multiple times. For a list of available audio formats, try: -t help </option> + <option short="R" long="no-summary"> + do not display summary information when verification is complete + </option> <option short="j" long="joint" arg="processes"> The maximum number of tracks to verify at one time. If one has multiple CPUs or CPU cores, allowing @@ -28,8 +31,9 @@ However, the maximum speed is likely to be limited by I/O-bound rather than CPU-bound. </option> - <option short="R" long="no-summary"> - do not display summary information when verification is complete + <option short="V" long="verbose" arg="verbosity"> + The level of output to display. + Choose between 'normal', 'quiet' and 'debug'. </option> </options> <element name="verification">
View file
audiotools-2.18.tar.gz/dvda2track -> audiotools-2.19.tar.gz/dvda2track
Changed
@@ -20,47 +20,33 @@ import sys import os -import audiotools -import audiotools.ui -import gettext import tempfile from itertools import izip +import audiotools +import audiotools.ui +import audiotools.text as _ +import termios -gettext.install("audiotools", unicode=True) - - -class DummyTrack: - def __init__(self, sample_rate, channels, channel_mask, bits_per_sample): - self.__sample_rate__ = sample_rate - self.__channels__ = channels - self.__channel_mask__ = channel_mask - self.__bits_per_sample__ = bits_per_sample - - def sample_rate(self): - return self.__sample_rate__ - - def channels(self): - return self.__channels__ - def channel_mask(self): - return self.__channel_mask__ +def limit(l, indexes): + """given a list and set of indexes (starting from 1) + returns a new list with only those items""" - def bits_per_sample(self): - return self.__bits_per_sample__ + return [item for (index, item) in enumerate(l) + if ((index + 1) in indexes)] if (__name__ == '__main__'): parser = audiotools.OptionParser( - usage=_(u"%prog [options] [track #] [track #] ..."), + usage=_.USAGE_DVDA2TRACK, version="Python Audio Tools %s" % (audiotools.VERSION)) parser.add_option( - '-V', '--verbose', - action='store', - dest='verbosity', - choices=audiotools.VERBOSITY_LEVELS, - default=audiotools.DEFAULT_VERBOSITY, - help=_(u'the verbosity level to execute at')) + '-I', '--interactive', + action='store_true', + default=False, + dest='interactive', + help=_.OPT_INTERACTIVE_OPTIONS) parser.add_option( '-c', '--cdrom', action='store', @@ -69,28 +55,36 @@ parser.add_option('-A', '--audio-ts', action='store', default=None, type='string', dest='audio_ts', metavar='DIR', - help='location of AUDIO_TS directory') + help=_.OPT_AUDIO_TS) parser.add_option('--title', action='store', default=1, type='int', dest='title', - help='DVD-Audio title number to extract tracks from') + help=_.OPT_DVDA_TITLE) - conversion = audiotools.OptionGroup(parser, _(u"Extraction Options")) + parser.add_option( + '-V', '--verbose', + action='store', + dest='verbosity', + choices=audiotools.VERBOSITY_LEVELS, + default=audiotools.DEFAULT_VERBOSITY, + help=_.OPT_VERBOSE) + + conversion = audiotools.OptionGroup(parser, _.OPT_CAT_EXTRACTION) conversion.add_option( '-t', '--type', action='store', dest='type', - choices=audiotools.TYPE_MAP.keys(), + choices=sorted(audiotools.TYPE_MAP.keys() + ['help']), default=audiotools.DEFAULT_TYPE, - help=_(u'the type of audio track to create')) + help=_.OPT_TYPE) conversion.add_option( '-q', '--quality', action='store', type='string', dest='quality', - help=_(u'the quality to store audio tracks at')) + help=_.OPT_QUALITY) conversion.add_option( '-d', '--dir', @@ -98,19 +92,19 @@ type='string', dest='dir', default='.', - help=_(u"the directory to store extracted audio tracks")) + help=_.OPT_DIR) conversion.add_option( '--format', action='store', type='string', - default=None, + default=audiotools.FILENAME_FORMAT, dest='format', - help=_(u'the format string for new filenames')) + help=_.OPT_FORMAT) parser.add_option_group(conversion) - lookup = audiotools.OptionGroup(parser, _(u"CD Lookup Options")) + lookup = audiotools.OptionGroup(parser, _.OPT_CAT_DVDA_LOOKUP) lookup.add_option( '--musicbrainz-server', action='store', @@ -126,7 +120,7 @@ '--no-musicbrainz', action='store_false', dest='use_musicbrainz', default=audiotools.MUSICBRAINZ_SERVICE, - help='do not query MusicBrainz for metadata') + help=_.OPT_NO_MUSICBRAINZ) lookup.add_option( '--freedb-server', action='store', @@ -142,32 +136,24 @@ '--no-freedb', action='store_false', dest='use_freedb', default=audiotools.FREEDB_SERVICE, - help='do not query FreeDB for metadata') - - lookup.add_option( - '-I', '--interactive', - action='store_true', - default=False, - dest='interactive', - help=_(u'edit metadata in interactive mode')) + help=_.OPT_NO_FREEDB) lookup.add_option( '-D', '--default', dest='use_default', action='store_true', default=False, - help=_(u'when multiple choices are available, ' + - u'select the first one automatically')) + help=_.OPT_DEFAULT) parser.add_option_group(lookup) - metadata = audiotools.OptionGroup(parser, _(u"Metadata Options")) + metadata = audiotools.OptionGroup(parser, _.OPT_CAT_METADATA) metadata.add_option( '--track-start', action='store', type='int', dest='track_start', - default=1, - help=_(u'the starting track number of the title being extracted')) + default=None, + help=_.OPT_TRACK_START) metadata.add_option( '--track-total', @@ -175,26 +161,23 @@ type='int', dest='track_total', default=None, - help=_(u'the total number of tracks, ' + - u'if the extracted title is only a subset')) + help=_.OPT_TRACK_TOTAL) metadata.add_option( '--album-number', dest='album_number', action='store', type='int', - default=0, - help=_(u'the album number of this disc, ' + - u'if it is one of a series of albums')) + default=None, + help=_.OPT_ALBUM_NUMBER) metadata.add_option( '--album-total', dest='album_total', action='store', type='int', - default=0, - help=_(u'the total albums of this disc\'s set, ' + - u'if it is one of a series of albums')) + default=None, + help=_.OPT_ALBUM_TOTAL) #if adding ReplayGain is a lossless process #(i.e. added as tags rather than modifying track data) @@ -205,13 +188,13 @@ '--replay-gain', action='store_true', dest='add_replay_gain', - help=_(u'add ReplayGain metadata to newly created tracks')) + help=_.OPT_REPLAY_GAIN) metadata.add_option( '--no-replay-gain', action='store_false', dest='add_replay_gain', - help=_(u'do not add ReplayGain metadata in newly created tracks')) + help=_.OPT_NO_REPLAY_GAIN) parser.add_option_group(metadata) @@ -224,47 +207,29 @@ sys.exit(1) #get the AudioFile class we are converted to - AudioType = audiotools.TYPE_MAP[options.type] + if (options.type == 'help'): + audiotools.ui.show_available_formats(msg) + sys.exit(0) + else: + AudioType = audiotools.TYPE_MAP[options.type] #ensure the selected compression is compatible with that class if (options.quality == 'help'): - if (len(AudioType.COMPRESSION_MODES) > 1): - msg.info(_(u"Available compression types for %s:") % \ - (AudioType.NAME)) - for mode in AudioType.COMPRESSION_MODES: - msg.new_row() - if (mode == audiotools.__default_quality__(AudioType.NAME)): - msg.output_column(msg.ansi(mode.decode('ascii'), - [msg.BOLD, - msg.UNDERLINE]), True) - else: - msg.output_column(mode.decode('ascii'), True) - if (mode in AudioType.COMPRESSION_DESCRIPTIONS): - msg.output_column(u" : ") - else: - msg.output_column(u" ") - msg.output_column( - AudioType.COMPRESSION_DESCRIPTIONS.get(mode, u"")) - msg.output_rows() - else: - msg.error(_(u"Audio type %s has no compression modes") % \ - (AudioType.NAME)) + audiotools.ui.show_available_qualities(msg, AudioType) sys.exit(0) elif (options.quality is None): options.quality = audiotools.__default_quality__(AudioType.NAME) elif (options.quality not in AudioType.COMPRESSION_MODES): - msg.error(_(u"\"%(quality)s\" is not a supported " + - u"compression mode for type \"%(type)s\"") % \ - {"quality": options.quality, - "type": AudioType.NAME}) + msg.error(_.ERR_UNSUPPORTED_COMPRESSION_MODE % + {"quality": options.quality, + "type": AudioType.NAME}) sys.exit(1) quality = options.quality base_directory = options.dir if (options.audio_ts is None): - msg.error( - _(u"You must specify the DVD-Audio's AUDIO_TS directory with -A")) + msg.error(_.ERR_NO_AUDIO_TS) sys.exit(1) #get main DVDAudio object @@ -279,7 +244,7 @@ #get selected DVDATitle from DVDAudio object if (options.title < 1): - msg.error(_(u"title number must be greater than 0")) + msg.error(ERR_INVALID_TITLE_NUMBER) sys.exit(1) title = dvda[0][options.title - 1] @@ -292,28 +257,21 @@ use_musicbrainz=options.use_musicbrainz, use_freedb=options.use_freedb) - #decide which metadata to use to tag extracted tracks - if (options.interactive): - #pick choice using interactive widget - metadata_widget = audiotools.ui.MetaDataFiller(metadata_choices) - loop = audiotools.ui.urwid.MainLoop( - metadata_widget, - [('key', 'white', 'dark blue')], - unhandled_input=metadata_widget.handle_text) - loop.run() - - track_metadatas = dict([(m.track_number, m) for m in - metadata_widget.populated_metadata()]) - else: - if ((len(metadata_choices) == 1) or options.use_default): - #use default choice - track_metadatas = dict([(m.track_number, m) for m in - metadata_choices[0]]) - else: - #pick choice using raw stdin/stdout - track_metadatas = \ - dict([(m.track_number, m) for m in - audiotools.ui.select_metadata(metadata_choices, msg)]) + #update MetaData with command-line + #track_start, track_total, album_number, album_total - if given + for c in metadata_choices: + for m in c: + if (((options.track_start is not None) and + (m.track_number is not None))): + #track_number and track_start both start at 1 + #so minus 1 to take care of the offset + m.track_number += (options.track_start - 1) + if (options.track_total is not None): + m.track_total = options.track_total + if (options.album_number is not None): + m.album_number = options.album_number + if (options.album_total is not None): + m.album_total = options.album_total #determine which tracks to extract from the DVDATitle if (len(args) == 0): @@ -328,110 +286,130 @@ except ValueError: continue - (sample_rate, - channels, - channel_mask, - bits_per_sample, - stream_type) = title.info() - - #determine whether to add ReplayGain by default - if (options.add_replay_gain is None): - options.add_replay_gain = ( - audiotools.ADD_REPLAYGAIN and - AudioType.lossless_replay_gain() and - audiotools.applicable_replay_gain( - [DummyTrack(sample_rate=sample_rate, - channels=channels, - channel_mask=channel_mask, - bits_per_sample=bits_per_sample)])) + #decide which metadata and output options use when extracting tracks + if (options.interactive): + #pick choice using interactive widget + output_widget = audiotools.ui.OutputFiller( + track_labels=[_.LAB_TRACK_X_OF_Y % (i, len(title)) + for i in sorted(to_rip.keys())], + metadata_choices=[limit(c, to_rip.keys()) + for c in metadata_choices], + input_filenames=[ + audiotools.Filename("track-%d-%2.2d.dvda.wav" % + (options.title, i)) + for i in sorted(to_rip.keys())], + output_directory=options.dir, + format_string=options.format, + output_class=AudioType, + quality=options.quality, + completion_label=_.LAB_DVDA2TRACK_APPLY) - encoded = [] + loop = audiotools.ui.urwid.MainLoop( + output_widget, + audiotools.ui.style(), + unhandled_input=output_widget.handle_text, + pop_ups=True) + try: + loop.run() + msg.ansi_clearscreen() + except (termios.error, IOError): + msg.error(_.ERR_TERMIOS_ERROR) + msg.info(_.ERR_TERMIOS_SUGGESTION) + msg.info(audiotools.ui.xargs_suggestion(sys.argv)) + sys.exit(1) + if (not output_widget.cancelled()): + output_tracks = list(output_widget.output_tracks()) + else: + sys.exit(0) + else: + #pick choice without using GUI + try: + output_tracks = list( + audiotools.ui.process_output_options( + metadata_choices=[limit(c, to_rip.keys()) + for c in metadata_choices], + input_filenames=[ + audiotools.Filename("track-%d-%2.2d.dvda.wav" % + (options.title, i)) + for i in sorted(to_rip.keys())], + output_directory=options.dir, + format_string=options.format, + output_class=AudioType, + quality=options.quality, + msg=msg, + use_default=options.use_default)) + except audiotools.UnsupportedTracknameField, err: + err.error_msg(msg) + sys.exit(1) + except (audiotools.InvalidFilenameFormat, + audiotools.OutputFileIsInput, + audiotools.DuplicateOutputFile), err: + msg.error(unicode(err)) + sys.exit(1) + + #perform actual track extraction + encoded = [] pcmreader = title.to_pcm() for (i, pts_ticks) in enumerate([t.pts_length for t in title]): pcmreader.next_track(pts_ticks) pcm_frames = pcmreader.pcm_frames - title_track_number = i + 1 - logical_track_number = i + options.track_start - if (options.track_total is None): - track_total = len(title) - else: - track_total = options.track_total - - if (title_track_number in to_rip.keys()): - dvda_track = to_rip[title_track_number] - - basename = "track%2d%2.2d" % (options.album_number, - logical_track_number) + if ((i + 1) in to_rip): + (output_class, + output_filename, + output_quality, + output_metadata) = output_tracks.pop(0) try: - metadata = track_metadatas.get(dvda_track.track, None) - if (metadata is not None): - #update track metadata with additional fields - metadata.track_number = logical_track_number - if (options.track_total is not None): - metadata.track_total = options.track_total - if (options.album_number != 0): - metadata.album_number = options.album_number - if (options.album_total != 0): - metadata.album_total = options.album_total - - filename = os.path.join( - base_directory, - AudioType.track_name(file_path=basename, - track_metadata=metadata, - format=options.format)) - - audiotools.make_dirs(filename) - - progress = audiotools.SingleProgressDisplay( - msg, msg.filename(filename)) - - track = AudioType.from_pcm( - filename, - audiotools.PCMReaderProgress(pcmreader, - pcm_frames, - progress.update), - quality) - - track.set_metadata(metadata) - encoded.append(track) - - progress.clear() - - msg.info( - _(u"title %(title_number)d - " + - u"track %(track_number)2.2d -> %(filename)s") % \ - {"title_number": options.title, - "track_number": title_track_number, - "filename": msg.filename(track.filename)}) - except audiotools.UnsupportedTracknameField, err: - err.error_msg(msg) - sys.exit(1) - except KeyError: - continue + audiotools.make_dirs(str(output_filename)) except OSError, err: msg.os_error(err) sys.exit(1) - except audiotools.InvalidFormat, err: - msg.error(unicode(err)) - sys.exit(1) + + progress = audiotools.SingleProgressDisplay( + msg, unicode(output_filename)) + + try: + encoded.append( + output_class.from_pcm( + str(output_filename), + audiotools.PCMReaderProgress(pcmreader, + pcm_frames, + progress.update), + output_quality)) except audiotools.EncodingError, err: - msg.error(_(u"Unable to write \"%s\"") % \ - (msg.filename(filename))) + msg.error(_.ERR_ENCODING_ERROR % (output_filename,)) sys.exit(1) + + encoded[-1].set_metadata(output_metadata) + progress.clear() + + msg.info( + audiotools.output_progress( + _.LAB_ENCODE % { + "source": _.LAB_DVDA_TRACK % + {"title_number": options.title, + "track_number": i + 1}, + "destination": output_filename}, + len(to_rip) - len(output_tracks), + len(to_rip))) else: + #bypass skipped tracks by decoding them to null audiotools.transfer_framelist_data(pcmreader, lambda f: f) - if (options.add_replay_gain and AudioType.can_add_replay_gain()): + #add ReplayGain to ripped tracks, if necessary + if ((audiotools.ADD_REPLAYGAIN and + (options.add_replay_gain if (options.add_replay_gain is not None) + else output_class.lossless_replay_gain()) and + output_class.can_add_replay_gain(encoded))): rg_progress = audiotools.ReplayGainProgressDisplay( - msg, AudioType.lossless_replay_gain()) + msg, output_class.lossless_replay_gain()) rg_progress.initial_message() try: #all audio files must belong to the same album, by definition - AudioType.add_replay_gain([f.filename for f in encoded], - rg_progress.update) + output_class.add_replay_gain([f.filename for f in encoded], + rg_progress.update) except ValueError, err: rg_progress.clear() msg.error(unicode(err))
View file
audiotools-2.18.tar.gz/dvdainfo -> audiotools-2.19.tar.gz/dvdainfo
Changed
@@ -19,28 +19,25 @@ import sys -import audiotools -import gettext import os.path - -gettext.install("audiotools", unicode=True) +import audiotools +import audiotools.text as _ if (__name__ == '__main__'): parser = audiotools.OptionParser( - usage=_(u"%prog [options]"), + usage=_.USAGE_DVDAINFO, version="Python Audio Tools %s" % (audiotools.VERSION)) parser.add_option('-A', '--audio-ts', action='store', default=None, type='string', dest='audio_ts', metavar='DIR', - help='location of AUDIO_TS directory') + help=_.OPT_AUDIO_TS) (options, args) = parser.parse_args() msg = audiotools.Messenger("dvdainfo", options) if (options.audio_ts is None): - msg.error( - _(u"You must specify the DVD-Audio's AUDIO_TS directory with -A")) + msg.error(_.ERR_NO_AUDIO_TS) sys.exit(1) try: @@ -61,16 +58,14 @@ stream_type) = title.info() msg.new_row() - msg.output_column(_(u"Title %d") % (title_num + 1), True) - msg.output_column(_(u" (%d tracks)" % (len(title.tracks))), True) - msg.output_column(_(u" : ")) - msg.output_column(_(u"%(minutes)2.2d:%(seconds)2.2d " + - u"%(channels)dch %(rate)dHz %(bits)d-bit " + - u"%(type)s") % + msg.output_column(_.LAB_DVDA_TITLE % (title_num + 1), True) + msg.output_column(_.LAB_DVDA_TRACKS % (len(title.tracks)), True) + msg.output_column(u" : ") + msg.output_column(_.LAB_DVDA_TITLE_INFO % {"minutes": title.pts_length / 90000 / 60, "seconds": title.pts_length / 90000 % 60, "channels": channels, - "rate": sample_rate, + "rate": audiotools.khz(sample_rate), "bits": bits_per_sample, "type": {0xA0: u"PCM", 0xA1: u"MLP"}.get(stream_type, @@ -79,19 +74,19 @@ msg.output_rows() msg.new_row() - msg.output_column(_(u"Title")) + msg.output_column(_.LAB_DVDAINFO_TITLE) msg.output_column(u" ") - msg.output_column(_(u"Track")) + msg.output_column(_.LAB_DVDAINFO_TRACK) msg.output_column(u" ") - msg.output_column(_(u"Length")) + msg.output_column(_.LAB_DVDAINFO_LENGTH) msg.output_column(u" ") - msg.output_column(_(u"Filename")) + msg.output_column(_.LAB_DVDAINFO_FILENAME) msg.output_column(u" ") - msg.output_column(_(u"Start Sector")) + msg.output_column(_.LAB_DVDAINFO_STARTSECTOR) msg.output_column(u" ") - msg.output_column(_(u"End Sector")) + msg.output_column(_.LAB_DVDAINFO_ENDSECTOR) msg.output_column(u" ") - msg.output_column(_(u"PTS Ticks")) + msg.output_column(_.LAB_DVDAINFO_TICKS) msg.divider_row([u"-", u" ", u"-", u" ", u"-", u" ", u"-", u" ", u"-", u" ", u"-", u" ", u"-"]) @@ -107,9 +102,9 @@ msg.output_column(u" ") msg.output_column(unicode(track_num + 1), True) msg.output_column(u" ") - msg.output_column(u"%d:%2.2d" % \ - (track.pts_length / 90000 / 60, - track.pts_length / 90000 % 60), + msg.output_column(_.LAB_TRACK_LENGTH % + (track.pts_length / 90000 / 60, + track.pts_length / 90000 % 60), True) else: msg.output_column(u"") @@ -118,8 +113,9 @@ msg.output_column(u" ") msg.output_column(u"") msg.output_column(u" ") - msg.output_column(msg.filename(os.path.basename(aob_file)), - True) + msg.output_column( + unicode(audiotools.Filename(os.path.basename(aob_file))), + True) msg.output_column(u" ") msg.output_column(unicode(start_sector), True) msg.output_column(u" ")
View file
audiotools-2.18.tar.gz/setup.py -> audiotools-2.19.tar.gz/setup.py
Changed
@@ -17,7 +17,7 @@ #along with this program; if not, write to the Free Software #Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -VERSION = '2.18' +VERSION = '2.19' import sys @@ -33,14 +33,20 @@ libraries=['cdio', 'cdio_paranoia', 'cdio_cdda', 'm']) -resamplemodule = Extension('audiotools.resample', - sources=['src/resample.c']) - pcmmodule = Extension('audiotools.pcm', sources=['src/pcm.c']) +pcmconvmodule = Extension('audiotools.pcmconverter', + sources=['src/pcmconverter.c', + 'src/pcmconv.c', + 'src/array.c', + 'src/bitstream.c']) + replaygainmodule = Extension('audiotools.replaygain', - sources=['src/replaygain.c']) + sources=['src/replaygain.c', + 'src/pcmconv.c', + 'src/array.c', + 'src/bitstream.c']) decoders_defines = [("VERSION", VERSION)] decoders_sources = ['src/array.c', @@ -99,6 +105,22 @@ 'src/common/ogg_crc.c', 'src/bitstream.c']) +output_sources = ['src/output.c'] +output_defines = [] +output_link_args = [] + +if (sys.platform == 'darwin'): + output_sources.append('src/output/core_audio.c') + output_defines.append(("CORE_AUDIO", "1")) + output_link_args.extend(["-framework", "AudioToolbox", + "-framework", "AudioUnit", + "-framework", "CoreServices"]) + +outputmodule = Extension('audiotools.output', + sources=output_sources, + define_macros=output_defines, + extra_link_args=output_link_args) + setup(name='Python Audio Tools', version=VERSION, description='A collection of audio handling utilities', @@ -106,21 +128,36 @@ author_email='tuffy@users.sourceforge.net', url='http://audiotools.sourceforge.net', packages=["audiotools", - "audiotools.py_decoders", "audiotools.py_encoders"], + "audiotools.py_decoders", + "audiotools.py_encoders"], ext_modules=[cdiomodule, - resamplemodule, pcmmodule, + pcmconvmodule, replaygainmodule, decodersmodule, encodersmodule, bitstreammodule, - verifymodule], + verifymodule, + outputmodule], data_files=[("/etc", ["audiotools.cfg"])], - scripts=["cd2track", "cdinfo", "cdplay", - "track2track", "trackrename", "trackinfo", - "tracklength", "track2cd", "trackcmp", "trackplay", - "tracktag", "audiotools-config", - "trackcat", "tracksplit", - "tracklint", "trackverify", - "coverdump", "coverview", - "dvdainfo", "dvda2track"]) + scripts=["audiotools-config", + "cd2track", + "cdinfo", + "cdplay", + "coverdump", + "covertag", + "coverview", + "dvda2track", + "dvdainfo", + "track2cd", + "track2track", + "trackcat", + "trackcmp", + "trackinfo", + "tracklength", + "tracklint", + "trackplay", + "trackrename", + "tracksplit", + "tracktag", + "trackverify"])
View file
audiotools-2.19.tar.gz/src/Makefile
Added
@@ -0,0 +1,94 @@ +#This makefile is for generating debug standalone executables + +VERSION = "2.19" +OBJS = array.o pcm.o bitstream.o +FLAGS = -Wall -g +BINARIES = alacdec flacdec oggflacdec shndec wvdec \ +alacenc flacenc oggflacdec shnenc wvenc \ +huffman bitstream + +all: $(BINARIES) + +flacdec: $(OBJS) decoders/flac.c decoders/flac.h flac_crc.o md5.o pcmconv.o + $(CC) $(FLAGS) -o flacdec decoders/flac.c $(OBJS) flac_crc.o md5.o pcmconv.o -DSTANDALONE -DEXECUTABLE + +oggflacdec: $(OBJS) decoders/oggflac.c decoders/oggflac.h flac.o flac_crc.o md5.o pcmconv.o ogg.o ogg_crc.o + $(CC) $(FLAGS) -o oggflacdec decoders/oggflac.c $(OBJS) flac.o flac_crc.o md5.o pcmconv.o ogg.o ogg_crc.o -DSTANDALONE + +wvdec: $(OBJS) decoders/wavpack.c decoders/wavpack.h md5.o pcmconv.o + $(CC) $(FLAGS) -o wvdec decoders/wavpack.c $(OBJS) md5.o pcmconv.o -DSTANDALONE + +alacdec: $(OBJS) decoders/alac.c decoders/alac.h pcmconv.o + $(CC) $(FLAGS) -o alacdec decoders/alac.c $(OBJS) pcmconv.o -DSTANDALONE + +shndec: $(OBJS) decoders/shn.c decoders/shn.h pcmconv.o + $(CC) $(FLAGS) -o shndec decoders/shn.c $(OBJS) pcmconv.o -DSTANDALONE -lm + +flacenc: $(OBJS) encoders/flac.c encoders/flac.h flac_crc.o md5.o pcmconv.o + $(CC) $(FLAGS) -DVERSION=$(VERSION) -o flacenc encoders/flac.c $(OBJS) md5.o flac_crc.o pcmconv.o -DSTANDALONE -lm + +alacenc: $(OBJS) encoders/alac.c encoders/alac.h pcmconv.o + $(CC) $(FLAGS) -DVERSION=$(VERSION) -o alacenc encoders/alac.c pcmconv.o $(OBJS) -DSTANDALONE -lm + +shnenc: $(OBJS) encoders/shn.c encoders/shn.h pcmconv.o + $(CC) $(FLAGS) -DVERSION=$(VERSION) -o shnenc encoders/shn.c pcmconv.o $(OBJS) -DSTANDALONE -lm + +wvenc: $(OBJS) encoders/wavpack.c encoders/wavpack.h md5.o pcmconv.o + $(CC) $(FLAGS) -o wvenc encoders/wavpack.c md5.o pcmconv.o $(OBJS) -DSTANDALONE + +aobdec: decoders/aob.c decoders/aob.h $(OBJS) mlp.o aobpcm.o pcmconv.o + $(CC) $(FLAGS) -o aobdec decoders/aob.c $(OBJS) mlp.o aobpcm.o pcmconv.o -DSTANDALONE + +# mlpdec: decoders/mlp.c decoders/mlp.h array.o bitstream.o +# $(CC) $(FLAGS) -o mlpdec decoders/mlp.c array.o bitstream.o -DSTANDALONE + +dvdadec: dvda/dvda.c dvda/dvda.h bitstream.o + $(CC) $(FLAGS) -o dvdadec dvda/dvda.c bitstream.o -DSTANDALONE + +huffman: huffman.c huffman.h parson.o + $(CC) $(FLAGS) -o huffman huffman.c parson.o -DSTANDALONE -DEXECUTABLE + +clean: + rm -f $(BINARIES) *.o + +array.o: array.c array.h + $(CC) $(FLAGS) -c array.c -DSTANDALONE + +pcm.o: pcm.c pcm.h + $(CC) $(FLAGS) -c pcm.c -DSTANDALONE + +pcmconv.o: pcmconv.c pcmconv.h + $(CC) $(FLAGS) -c pcmconv.c -DSTANDALONE + +bitstream.o: bitstream.c bitstream.h + $(CC) $(FLAGS) -c bitstream.c + +md5.o: common/md5.c common/md5.h + $(CC) $(FLAGS) -c common/md5.c -DSTANDALONE + +flac.o: decoders/flac.c decoders/flac.h + $(CC) $(FLAGS) -c decoders/flac.c -DSTANDALONE + +mlp.o: decoders/mlp.c decoders/mlp.h + $(CC) $(FLAGS) -c decoders/mlp.c -DSTANDALONE + +aobpcm.o: decoders/aobpcm.c decoders/aobpcm.h + $(CC) $(FLAGS) -c decoders/aobpcm.c -DSTANDALONE + +ogg.o: decoders/ogg.c decoders/ogg.h + $(CC) $(FLAGS) -c decoders/ogg.c -DSTANDALONE + +ogg_crc.o: common/ogg_crc.c common/ogg_crc.h + $(CC) $(FLAGS) -c common/ogg_crc.c -DSTANDALONE + +flac_crc.o: common/flac_crc.c common/flac_crc.h + $(CC) $(FLAGS) -c common/flac_crc.c -DSTANDALONE + +huffman.o: huffman.c huffman.h + $(CC) $(FLAGS) -c huffman.c -DSTANDALONE + +bitstream: bitstream.c bitstream.h huffman.o + $(CC) $(FLAGS) bitstream.c huffman.o -DEXECUTABLE -o $@ + +parson.o: parson.c parson.h + $(CC) $(FLAGS) -c parson.c
View file
audiotools-2.18.tar.gz/src/array.c -> audiotools-2.19.tar.gz/src/array.c
Changed
@@ -47,7 +47,9 @@ a->del = array_i_del; a->resize = array_i_resize; + a->resize_for = array_i_resize_for; a->reset = array_i_reset; + a->reset_for = array_i_reset_for; a->append = array_i_append; a->vappend = array_i_vappend; a->mappend = array_i_mappend; @@ -98,6 +100,18 @@ ARRAY_RESIZE(array_f_resize, array_f, double) ARRAY_RESIZE(array_o_resize, array_o, void*) + +#define ARRAY_RESIZE_FOR(FUNC_NAME, ARRAY_TYPE) \ + void \ + FUNC_NAME(ARRAY_TYPE *array, unsigned additional_items) \ + { \ + array->resize(array, array->len + additional_items); \ + } +ARRAY_RESIZE_FOR(array_i_resize_for, array_i); +ARRAY_RESIZE_FOR(array_f_resize_for, array_f); +ARRAY_RESIZE_FOR(array_o_resize_for, array_o); + + #define ARRAY_RESET(FUNC_NAME, ARRAY_TYPE) \ void \ FUNC_NAME(ARRAY_TYPE *array) \ @@ -110,6 +124,17 @@ ARRAY_RESET(array_lf_reset, array_lf) +#define ARRAY_RESET_FOR(FUNC_NAME, ARRAY_TYPE) \ + void \ + FUNC_NAME(ARRAY_TYPE *array, unsigned minimum) { \ + array->reset(array); \ + array->resize(array, minimum); \ + } +ARRAY_RESET_FOR(array_i_reset_for, array_i); +ARRAY_RESET_FOR(array_f_reset_for, array_f); +ARRAY_RESET_FOR(array_o_reset_for, array_o); + + #define ARRAY_APPEND(FUNC_NAME, ARRAY_TYPE, ARRAY_DATA_TYPE) \ void \ FUNC_NAME(ARRAY_TYPE *array, ARRAY_DATA_TYPE value) \ @@ -158,14 +183,12 @@ void \ FUNC_NAME(ARRAY_TYPE *array, unsigned count, ...) \ { \ - ARRAY_DATA_TYPE i; \ va_list ap; \ \ - array->reset(array); \ - array->resize(array, count); \ + array->reset_for(array, count); \ va_start(ap, count); \ for (; count > 0; count--) { \ - i = va_arg(ap, ARRAY_DATA_TYPE); \ + const ARRAY_DATA_TYPE i = va_arg(ap, ARRAY_DATA_TYPE); \ array->_[array->len++] = i; \ } \ va_end(ap); \ @@ -178,8 +201,7 @@ void \ FUNC_NAME(ARRAY_TYPE *array, unsigned count, ARRAY_DATA_TYPE value) \ { \ - array->reset(array); \ - array->resize(array, count); \ + array->reset_for(array, count); \ for (; count > 0; count--) { \ array->_[array->len++] = value; \ } \ @@ -191,11 +213,11 @@ void \ FUNC_NAME(ARRAY_TYPE *array, const ARRAY_TYPE *to_add) \ { \ - array->resize(array, array->len + to_add->len); \ - memcpy(array->_ + array->len, \ + array->resize_for(array, to_add->len); \ + memcpy(array->_ + array->len, \ to_add->_, \ - sizeof(ARRAY_DATA_TYPE) * to_add->len); \ - array->len += to_add->len; \ + sizeof(ARRAY_DATA_TYPE) * to_add->len); \ + array->len += to_add->len; \ } ARRAY_EXTEND(array_i_extend, array_i, int) ARRAY_EXTEND(array_f_extend, array_f, double) @@ -206,7 +228,7 @@ { \ assert(array->_ != NULL); \ assert(compare->_ != NULL); \ - if (array->len == compare->len) { \ + if (array->len == compare->len) { \ return (memcmp(array->_, compare->_, \ sizeof(ARRAY_DATA_TYPE) * array->len) == 0); \ } else \ @@ -340,7 +362,7 @@ void \ FUNC_NAME(const ARRAY_TYPE *array, unsigned count, ARRAY_TYPE *head) \ { \ - unsigned to_copy = MIN(count, array->len); \ + const unsigned to_copy = MIN(count, array->len); \ \ if (head != array) { \ head->resize(head, to_copy); \ @@ -380,7 +402,7 @@ void \ FUNC_NAME(const ARRAY_TYPE *array, unsigned count, ARRAY_TYPE *tail) \ { \ - unsigned to_copy = MIN(count, array->len); \ + const unsigned to_copy = MIN(count, array->len); \ \ if (tail != array) { \ tail->resize(tail, to_copy); \ @@ -422,8 +444,8 @@ ARRAY_TYPE *head, ARRAY_TYPE *tail) \ { \ /*ensure we don't try to move too many items*/ \ - unsigned to_head = MIN(count, array->len); \ - unsigned to_tail = array->len - to_head; \ + const unsigned to_head = MIN(count, array->len); \ + const unsigned to_tail = array->len - to_head; \ \ if ((head == array) && (tail == array)) { \ /*do nothing*/ \ @@ -512,12 +534,11 @@ { \ unsigned i; \ unsigned j; \ - ARRAY_DATA_TYPE x; \ ARRAY_DATA_TYPE *data = array->_; \ \ if (array->len > 0) { \ for (i = 0, j = array->len - 1; i < j; i++, j--) { \ - x = data[i]; \ + ARRAY_DATA_TYPE x = data[i]; \ data[i] = data[j]; \ data[j] = x; \ } \ @@ -615,7 +636,7 @@ void \ FUNC_NAME(const ARRAY_TYPE *array, unsigned count, ARRAY_TYPE *head) \ { \ - unsigned to_copy = MIN(count, array->len); \ + const unsigned to_copy = MIN(count, array->len); \ assert(array->_ != NULL); \ head->_ = array->_; \ head->len = to_copy; \ @@ -642,7 +663,7 @@ void \ FUNC_NAME(const ARRAY_TYPE *array, unsigned count, ARRAY_TYPE *tail) \ { \ - unsigned to_copy = MIN(count, array->len); \ + const unsigned to_copy = MIN(count, array->len); \ assert(array->_ != NULL); \ tail->_ = array->_ + (array->len - to_copy); \ tail->len = to_copy; \ @@ -660,29 +681,29 @@ ARRAY_L_DE_TAIL(array_li_de_tail, array_li) ARRAY_L_DE_TAIL(array_lf_de_tail, array_lf) -#define ARRAY_L_SPLIT(FUNC_NAME, ARRAY_TYPE) \ - void \ - FUNC_NAME(const ARRAY_TYPE *array, unsigned count, \ - ARRAY_TYPE *head, ARRAY_TYPE *tail) \ - { \ - /*ensure we don't try to move too many items*/ \ - unsigned to_head = MIN(count, array->len); \ - unsigned to_tail = array->len - to_head; \ - assert(array->_ != NULL); \ - \ - if ((head == array) && (tail == array)) { \ - /*do nothing*/ \ - return; \ - } else if (head == tail) { \ - /*copy all data to head*/ \ - head->_ = array->_; \ - head->len = array->len; \ - } else { \ - head->_ = array->_; \ - head->len = to_head; \ - tail->_ = array->_ + to_head; \ - tail->len = to_tail; \ - } \ +#define ARRAY_L_SPLIT(FUNC_NAME, ARRAY_TYPE) \ + void \ + FUNC_NAME(const ARRAY_TYPE *array, unsigned count, \ + ARRAY_TYPE *head, ARRAY_TYPE *tail) \ + { \ + /*ensure we don't try to move too many items*/ \ + const unsigned to_head = MIN(count, array->len); \ + const unsigned to_tail = array->len - to_head; \ + assert(array->_ != NULL); \ + \ + if ((head == array) && (tail == array)) { \ + /*do nothing*/ \ + return; \ + } else if (head == tail) { \ + /*copy all data to head*/ \ + head->_ = array->_; \ + head->len = array->len; \ + } else { \ + head->_ = array->_; \ + head->len = to_head; \ + tail->_ = array->_ + to_head; \ + tail->len = to_tail; \ + } \ } ARRAY_L_SPLIT(array_li_split, array_li) ARRAY_L_SPLIT(array_lf_split, array_lf) @@ -704,7 +725,9 @@ a->del = array_f_del; a->resize = array_f_resize; + a->resize_for = array_f_resize_for; a->reset = array_f_reset; + a->reset_for = array_f_reset_for; a->append = array_f_append; a->vappend = array_f_vappend; a->mappend = array_f_mappend; @@ -1370,7 +1393,9 @@ a->del = array_o_del; a->resize = array_o_resize; + a->resize_for = array_o_resize_for; a->reset = array_o_reset; + a->reset_for = array_o_reset_for; a->append = array_o_append; a->vappend = array_o_vappend; a->mappend = array_o_mappend; @@ -1453,8 +1478,7 @@ void* i; va_list ap; - array->reset(array); - array->resize(array, count); + array->reset_for(array, count); va_start(ap, count); for (; count > 0; count--) { i = va_arg(ap, void*); @@ -1466,8 +1490,7 @@ void array_o_mset(struct array_o_s *array, unsigned count, void* value) { - array->reset(array); - array->resize(array, count); + array->reset_for(array, count); for (; count > 0; count--) { array->_[array->len++] = array->copy_obj(value); } @@ -1479,7 +1502,7 @@ unsigned i; const unsigned len = to_add->len; - array->resize(array, array->len + to_add->len); + array->resize_for(array, to_add->len); for (i = 0; i < len; i++) { array->_[array->len++] = array->copy_obj(to_add->_[i]); @@ -1492,8 +1515,7 @@ if (array != copy) { unsigned i; - copy->resize(copy, array->len); - copy->reset(copy); + copy->reset_for(copy, array->len); for (i = 0; i < array->len; i++) { copy->_[copy->len++] = array->copy_obj(array->_[i]); } @@ -1504,12 +1526,11 @@ array_o_head(const struct array_o_s *array, unsigned count, struct array_o_s *head) { - unsigned to_copy = MIN(count, array->len); + const unsigned to_copy = MIN(count, array->len); if (head != array) { unsigned i; - head->resize(head, to_copy); - head->reset(head); + head->reset_for(head, to_copy); for (i = 0; i < to_copy; i++) { head->_[head->len++] = array->copy_obj(array->_[i]); } @@ -1524,13 +1545,12 @@ array_o_tail(const struct array_o_s *array, unsigned count, struct array_o_s *tail) { - unsigned to_copy = MIN(count, array->len); + const unsigned to_copy = MIN(count, array->len); if (tail != array) { unsigned i; - tail->resize(tail, to_copy); - tail->reset(tail); + tail->reset_for(tail, to_copy); for (i = array->len - to_copy; i < array->len; i++) { tail->_[tail->len++] = array->copy_obj(array->_[i]); } @@ -1566,8 +1586,8 @@ array_o_split(const struct array_o_s *array, unsigned count, struct array_o_s *head, struct array_o_s *tail) { - unsigned to_head = MIN(count, array->len); - unsigned to_tail = array->len - to_head; + const unsigned to_head = MIN(count, array->len); + const unsigned to_tail = array->len - to_head; if ((head == array) && (tail == array)) { /*do nothing*/
View file
audiotools-2.18.tar.gz/src/array.h -> audiotools-2.19.tar.gz/src/array.h
Changed
@@ -52,8 +52,7 @@ "array" is evaluated twice, while "value" is evaluated only once this presumes array has been resized in advance for additional items: - array->reset(array); - array->resize(array, count); + array->reset_for(array, count); for (i = 0; i < count; i++) a_append(array, data[i]); @@ -81,10 +80,19 @@ if necessary*/ void (*resize)(struct array_i_s *array, unsigned minimum); + /*resizes the array to fit "additional_items" number of new items, + if necessary*/ + void (*resize_for)(struct array_i_s *array, unsigned additional_items); + /*deletes any data in the array and resets its contents so that it can be re-populated with new data*/ void (*reset)(struct array_i_s *array); + /*deletes any data in the array, + resizes its contents to fit "minimum" number of items, + and resets it contents so it can be re-populated with new data*/ + void (*reset_for)(struct array_i_s *array, unsigned minimum); + /*appends a single value to the array*/ void (*append)(struct array_i_s *array, int value); @@ -177,7 +185,9 @@ void array_i_del(struct array_i_s *array); void array_i_resize(struct array_i_s *array, unsigned minimum); +void array_i_resize_for(struct array_i_s *array, unsigned additional_items); void array_i_reset(struct array_i_s *array); +void array_i_reset_for(struct array_i_s *array, unsigned minimum); void array_i_append(struct array_i_s *array, int value); void array_i_vappend(struct array_i_s *array, unsigned count, ...); void array_i_mappend(struct array_i_s *array, unsigned count, int value); @@ -327,10 +337,19 @@ if necessary*/ void (*resize)(struct array_f_s *array, unsigned minimum); + /*resizes the array to fit "additional_items" number of new items, + if necessary*/ + void (*resize_for)(struct array_f_s *array, unsigned additional_items); + /*deletes any data in the array and resets its contents so that it can be re-populated with new data*/ void (*reset)(struct array_f_s *array); + /*deletes any data in the array, + resizes its contents to fit "minimum" number of items, + and resets it contents so it can be re-populated with new data*/ + void (*reset_for)(struct array_f_s *array, unsigned minimum); + /*appends a single value to the array*/ void (*append)(struct array_f_s *array, double value); @@ -424,7 +443,9 @@ void array_f_del(struct array_f_s *array); void array_f_resize(struct array_f_s *array, unsigned minimum); +void array_f_resize_for(struct array_f_s *array, unsigned additional_items); void array_f_reset(struct array_f_s *array); +void array_f_reset_for(struct array_f_s *array, unsigned minimum); void array_f_append(struct array_f_s *array, double value); void array_f_vappend(struct array_f_s *array, unsigned count, ...); void array_f_mappend(struct array_f_s *array, unsigned count, double value); @@ -1050,10 +1071,19 @@ if necessary*/ void (*resize)(struct array_o_s *array, unsigned minimum); + /*resizes the array to fit "additional_items" number of new items, + if necessary*/ + void (*resize_for)(struct array_o_s *array, unsigned additional_items); + /*deletes any data in the array and resets its contents so that it can be re-populated with new data*/ void (*reset)(struct array_o_s *array); + /*deletes any data in the array, + resizes its contents to fit "minimum" number of items, + and resets it contents so it can be re-populated with new data*/ + void (*reset_for)(struct array_o_s *array, unsigned minimum); + /*appends a single value to the array*/ void (*append)(struct array_o_s *array, void* value); @@ -1132,7 +1162,9 @@ void (*print)(void* obj, FILE* output)); void array_o_del(struct array_o_s *array); void array_o_resize(struct array_o_s *array, unsigned minimum); +void array_o_resize_for(struct array_o_s *array, unsigned additional_items); void array_o_reset(struct array_o_s *array); +void array_o_reset_for(struct array_o_s *array, unsigned minimum); void array_o_append(struct array_o_s *array, void* value); void array_o_vappend(struct array_o_s *array, unsigned count, ...); void array_o_mappend(struct array_o_s *array, unsigned count, void* value);
View file
audiotools-2.18.tar.gz/src/bitstream.c -> audiotools-2.19.tar.gz/src/bitstream.c
Changed
@@ -1556,29 +1556,41 @@ br_unmark_f(BitstreamReader* bs) { struct br_mark* mark = bs->marks; - bs->marks = mark->next; - mark->next = bs->marks_used; - bs->marks_used = mark; + if (mark != NULL) { + bs->marks = mark->next; + mark->next = bs->marks_used; + bs->marks_used = mark; + } else { + fprintf(stderr, "No marks on stack to remove!\n"); + } } void br_unmark_s(BitstreamReader* bs) { struct br_mark* mark = bs->marks; - bs->marks = mark->next; - mark->next = bs->marks_used; - bs->marks_used = mark; - bs->input.substream->mark_in_progress = (bs->marks != NULL); + if (mark != NULL) { + bs->marks = mark->next; + mark->next = bs->marks_used; + bs->marks_used = mark; + bs->input.substream->mark_in_progress = (bs->marks != NULL); + } else { + fprintf(stderr, "No marks on stack to remove!\n"); + } } void br_unmark_e(BitstreamReader* bs) { struct br_mark* mark = bs->marks; - bs->marks = mark->next; - mark->next = bs->marks_used; - bs->marks_used = mark; - bs->input.external->buffer->mark_in_progress = (bs->marks != NULL); + if (mark != NULL) { + bs->marks = mark->next; + mark->next = bs->marks_used; + bs->marks_used = mark; + bs->input.external->buffer->mark_in_progress = (bs->marks != NULL); + } else { + fprintf(stderr, "No marks on stack to remove!\n"); + } } void @@ -1597,6 +1609,8 @@ struct bs_callback *callback; uint32_t i; + assert(substream->type == BR_SUBSTREAM); + /*byte align the input stream*/ stream->state = 0; @@ -1747,7 +1761,7 @@ c_node->next = bs->callbacks_used; bs->callbacks_used = c_node; } else { - fprintf(stderr, "warning: no callbacks available to pop\n"); + fprintf(stderr, "Warning: no callbacks available to pop\n"); } } @@ -3048,9 +3062,9 @@ int buf_getc(struct bs_buffer *stream) { - if (stream->buffer_position < stream->buffer_size) { + if (stream->buffer_position < stream->buffer_size) return stream->buffer[stream->buffer_position++]; - } else + else return EOF; } @@ -3110,7 +3124,7 @@ if (!buffer->mark_in_progress) { /*if the stream has no mark, - reset the buffer prior to reading new data*/ + reset the empty buffer prior to reading new data*/ buffer->buffer_size = 0; buffer->buffer_position = 0;
View file
audiotools-2.18.tar.gz/src/bitstream.h -> audiotools-2.19.tar.gz/src/bitstream.h
Changed
@@ -254,15 +254,18 @@ * for FILE objects, performs fclose * for substreams, does nothing - * for Python readers, calls its .close() method + * for external input, calls its .close() method once the substream is closed, the reader's methods are updated to generate errors if called again*/ void (*close_internal_stream)(struct BitstreamReader_s* bs); - /*for substreams, deallocates buffer - for Python readers, decrefs Python object + /*frees the BitstreamReader's allocated data + + for FILE objects, does nothing + for substreams, deallocates buffer + for external input, calls its .free() method deallocates any callbacks/used callbacks deallocates any exceptions/used exceptions @@ -288,12 +291,20 @@ void (*rewind)(struct BitstreamReader_s* bs); - /*pops the previous mark from the mark stack*/ + /*pops the previous mark from the mark stack + + unmarking does not affect the stream's current position*/ void (*unmark)(struct BitstreamReader_s* bs); /*this appends the given length of bytes from the current stream - to the given substream*/ + to the given substream + + the input stream must be byte-aligned + but the substream need not be byte-aligned + + br_abort() is called if insufficient bytes + are available on the input stream*/ void (*substream_append)(struct BitstreamReader_s* bs, struct BitstreamReader_s* substream, @@ -774,6 +785,8 @@ unsigned int count, uint64_t value); + /*writes the given value as "count" number of signed bits + to the current stream, up to 64 bits wide*/ void (*write_signed_64)(struct BitstreamWriter_s* bs, unsigned int count, @@ -1127,6 +1140,7 @@ /*adds a callback function, which is called on every byte written + the function's arguments are the written byte and a generic pointer to some other data structure */ @@ -1170,13 +1184,13 @@ The basic call procudure is as follows: if (!setjmp(*bw_try(bs))) { - - perform reads here - + - perform writes here - } else { - - catch read exception here - + - catch write exception here - } bw_etry(bs); - either way, pop handler off exception stack - - The idea being to avoid cluttering our read code with lots + The idea being to avoid cluttering our write code with lots and lots of error checking tests, but rather assign a spot for errors to go if/when they do occur. */ @@ -1273,7 +1287,8 @@ /*returns a new bs_buffer struct which can be appended to and read from - it must be closed later*/ + + it must be closed with buf_close() when no longer needed*/ struct bs_buffer* buf_new(void); @@ -1284,7 +1299,7 @@ For example: - new_data = buf_extend(buffer, 10); + uint8_t* new_data = buf_extend(buffer, 10); if (fread(new_data, sizeof(uint8_t), 10, input_file) == 10) buffer->buffer_size += 10; else @@ -1329,7 +1344,9 @@ /*takes a user_data pointer and three functions and returns a FILE-like struct which can be read on a byte-by-byte basis - that will call C functions to fetch additional data as needed + via the ext_getc function + that will call the "read" function to fetch additional data as needed + int read(void* user_data, struct bs_buffer* buffer) where "buffer" is where read output will be placed @@ -1341,7 +1358,6 @@ rather than replacing what's already there returns 0 on a successful read, 1 on a read error - "size" will be set to 0 once EOF is reached void close(void* user_data)
View file
audiotools-2.18.tar.gz/src/common/flac_crc.c -> audiotools-2.19.tar.gz/src/common/flac_crc.c
Changed
@@ -1,7 +1,7 @@ #include "flac_crc.h" void -flac_crc8(uint8_t byte, void *checksum) +flac_crc8(uint8_t byte, unsigned *checksum) { const static uint32_t sumtable[0x100] = {0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, @@ -37,11 +37,11 @@ 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3}; - *((int*)checksum) = sumtable[*((int*)checksum) ^ byte]; + *checksum = sumtable[*checksum ^ byte]; } void -flac_crc16(uint8_t byte, void *checksum) +flac_crc16(uint8_t byte, unsigned *checksum) { const static uint32_t sumtable[0x100] = {0x0000, 0x8005, 0x800f, 0x000a, 0x801b, 0x001e, 0x0014, 0x8011, @@ -76,8 +76,6 @@ 0x0270, 0x8275, 0x827f, 0x027a, 0x826b, 0x026e, 0x0264, 0x8261, 0x0220, 0x8225, 0x822f, 0x022a, 0x823b, 0x023e, 0x0234, 0x8231, 0x8213, 0x0216, 0x021c, 0x8219, 0x0208, 0x820d, 0x8207, 0x0202}; - uint32_t old_checksum = *((int*)checksum); - *((int*)checksum) = (sumtable[(old_checksum >> 8) ^ byte] ^ - (old_checksum << 8)) & 0xFFFF; + *checksum = (sumtable[(*checksum >> 8) ^ byte] ^ (*checksum << 8)) & 0xFFFF; }
View file
audiotools-2.18.tar.gz/src/common/flac_crc.h -> audiotools-2.19.tar.gz/src/common/flac_crc.h
Changed
@@ -4,7 +4,7 @@ assigns a new checksum to that value*/ void -flac_crc8(uint8_t byte, void *checksum); +flac_crc8(uint8_t byte, unsigned *checksum); void -flac_crc16(uint8_t byte, void *checksum); +flac_crc16(uint8_t byte, unsigned *checksum);
View file
audiotools-2.18.tar.gz/src/common/md5.c -> audiotools-2.19.tar.gz/src/common/md5.c
Changed
@@ -1,5 +1,5 @@ -#include <stdlib.h> /* for malloc() */ -#include <string.h> /* for memcpy() */ +#include <stdlib.h> /* for malloc() */ +#include <string.h> /* for memcpy() */ #include "md5.h" @@ -185,9 +185,9 @@ t = ctx->bytes[0]; if ((ctx->bytes[0] = t + (uint32_t)len) < t) - ctx->bytes[1]++; /* Carry from low to high */ + ctx->bytes[1]++; /* Carry from low to high */ - t = 64 - (t & 0x3f); /* Space available in ctx->in (at least 1) */ + t = 64 - (t & 0x3f); /* Space available in ctx->in (at least 1) */ if (t > len) { memcpy((unsigned char *)ctx->in + 64 - t, buf, len); return; @@ -238,7 +238,7 @@ void audiotools__MD5Final(unsigned char *digest, audiotools__MD5Context *ctx) { - int count = ctx->bytes[0] & 0x3f; /* Number of bytes in ctx->in */ + int count = ctx->bytes[0] & 0x3f; /* Number of bytes in ctx->in */ unsigned char *p = (unsigned char *)ctx->in + count; /* Set the first char of padding to 0x80. There is always room. */ @@ -247,7 +247,7 @@ /* Bytes of padding needed to make 56 bytes (-8..55) */ count = 56 - 1 - count; - if (count < 0) { /* Padding forces an extra block */ + if (count < 0) { /* Padding forces an extra block */ memset(p, 0, count + 8); byteSwapX16(ctx->in); audiotools__MD5Transform(ctx->buf, ctx->in); @@ -264,12 +264,10 @@ byteSwap(ctx->buf, 4); memcpy(digest, ctx->buf, 16); - memset(ctx, 0, sizeof(ctx)); /* In case it's sensitive */ + /* memset(ctx, 0, sizeof(ctx)); */ /* In case it's sensitive */ if(0 != ctx->internal_buf) { free(ctx->internal_buf); ctx->internal_buf = 0; ctx->capacity = 0; } } - -
View file
audiotools-2.18.tar.gz/src/decoders/alac.c -> audiotools-2.19.tar.gz/src/decoders/alac.c
Changed
@@ -1,5 +1,6 @@ #include "alac.h" #include "../pcmconv.h" +#include <string.h> /******************************************************** Audio Tools, a module and set of tools for manipulating audio data @@ -20,6 +21,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *******************************************************/ +#ifndef STANDALONE int ALACDecoder_init(decoders_ALACDecoder *self, PyObject *args, PyObject *kwds) @@ -77,6 +79,9 @@ if ((self->audiotools_pcm = open_audiotools_pcm()) == NULL) return -1; + /*mark stream as not closed and ready for reading*/ + self->closed = 0; + return 0; } @@ -106,94 +111,6 @@ self->ob_type->tp_free((PyObject*)self); } -static int -parse_decoding_parameters(decoders_ALACDecoder *self) -{ - BitstreamReader* mdia_atom = br_substream_new(BS_BIG_ENDIAN); - BitstreamReader* stsd_atom = br_substream_new(BS_BIG_ENDIAN); - BitstreamReader* mdhd_atom = br_substream_new(BS_BIG_ENDIAN); - uint32_t mdia_atom_size; - uint32_t stsd_atom_size; - uint32_t mdhd_atom_size; - unsigned int total_frames; - - /*find the mdia atom, which is the parent to stsd and mdhd*/ - if (find_sub_atom(self->bitstream, mdia_atom, &mdia_atom_size, - "moov", "trak", "mdia", NULL)) { - PyErr_SetString(PyExc_ValueError, "unable to find mdia atom"); - goto error; - } else { - /*mark the mdia atom so we can parse - two different trees from it*/ - mdia_atom->mark(mdia_atom); - } - - /*find the stsd atom, which contains the alac atom*/ - if (find_sub_atom(mdia_atom, stsd_atom, &stsd_atom_size, - "minf", "stbl", "stsd", NULL)) { - mdia_atom->unmark(mdia_atom); - PyErr_SetString(PyExc_ValueError, "unable to find sdsd atom"); - goto error; - } - - /*parse the alac atom, which contains lots of crucial decoder details*/ - switch (read_alac_atom(stsd_atom, - &(self->max_samples_per_frame), - &(self->bits_per_sample), - &(self->history_multiplier), - &(self->initial_history), - &(self->maximum_k), - &(self->channels), - &(self->sample_rate))) { - case 1: - mdia_atom->unmark(mdia_atom); - PyErr_SetString(PyExc_IOError, "I/O error reading alac atom"); - goto error; - case 2: - mdia_atom->unmark(mdia_atom); - PyErr_SetString(PyExc_ValueError, "invalid alac atom"); - goto error; - default: - break; - } - - /*find the mdhd atom*/ - mdia_atom->rewind(mdia_atom); - if (find_sub_atom(mdia_atom, mdhd_atom, &mdhd_atom_size, - "mdhd", NULL)) { - mdia_atom->unmark(mdia_atom); - PyErr_SetString(PyExc_ValueError, "unable to find mdhd atom"); - goto error; - } else { - mdia_atom->unmark(mdia_atom); - } - - /*parse the mdhd atom, which contains our total frame count*/ - switch (read_mdhd_atom(mdhd_atom, &total_frames)) { - case 1: - PyErr_SetString(PyExc_IOError, "I/O error reading mdhd atom"); - goto error; - case 2: - PyErr_SetString(PyExc_ValueError, "invalid mdhd atom"); - goto error; - default: - self->remaining_frames = total_frames; - break; - } - - mdia_atom->close(mdia_atom); - stsd_atom->close(stsd_atom); - mdhd_atom->close(mdhd_atom); - - return 0; - - error: - mdia_atom->close(mdia_atom); - stsd_atom->close(stsd_atom); - mdhd_atom->close(mdhd_atom); - return -1; -} - PyObject* ALACDecoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds) @@ -248,7 +165,6 @@ } } - static PyObject* ALACDecoder_read(decoders_ALACDecoder* self, PyObject *args) { @@ -257,6 +173,11 @@ array_ia* frameset_channels = self->frameset_channels; PyThreadState *thread_state; + if (self->closed) { + PyErr_SetString(PyExc_ValueError, "cannot read closed stream"); + return NULL; + } + /*return an empty framelist if total samples are exhausted*/ if (self->remaining_frames == 0) { return empty_FrameList(self->audiotools_pcm, @@ -314,6 +235,187 @@ } } +static PyObject* +ALACDecoder_close(decoders_ALACDecoder* self, PyObject *args) +{ + /*mark stream as closed so more calls to read() + generate ValueErrors*/ + self->closed = 1; + + Py_INCREF(Py_None); + return Py_None; +} +#else +#include <errno.h> + +int +ALACDecoder_init(decoders_ALACDecoder *self, char *filename) +{ + unsigned i; + + self->frameset_channels = array_ia_new(); + self->frame_channels = array_ia_new(); + self->uncompressed_LSBs = array_i_new(); + self->residuals = array_i_new(); + + for (i = 0; i < MAX_CHANNELS; i++) { + self->subframe_headers[i].qlp_coeff = array_i_new(); + } + + if ((self->file = fopen(filename, "rb")) == NULL) { + fprintf(stderr, "*** %s: %s\n", filename, strerror(errno)); + return -1; + } else { + self->bitstream = br_open(self->file, BS_BIG_ENDIAN); + } + self->filename = strdup(filename); + + self->bitstream->mark(self->bitstream); + + if (parse_decoding_parameters(self)) { + self->bitstream->unmark(self->bitstream); + return -1; + } else { + self->bitstream->rewind(self->bitstream); + } + + /*seek to the 'mdat' atom, which contains the ALAC stream*/ + if (seek_mdat(self->bitstream) == ERROR) { + self->bitstream->unmark(self->bitstream); + fprintf(stderr, "Unable to locate 'mdat' atom in stream\n"); + return -1; + } else { + self->bitstream->unmark(self->bitstream); + } + + return 0; +} + +void +ALACDecoder_dealloc(decoders_ALACDecoder *self) +{ + int i; + + if (self->filename != NULL) + free(self->filename); + + if (self->bitstream != NULL) + /*this closes self->file also*/ + self->bitstream->close(self->bitstream); + + for (i = 0; i < MAX_CHANNELS; i++) + self->subframe_headers[i].qlp_coeff->del( + self->subframe_headers[i].qlp_coeff); + + self->frameset_channels->del(self->frameset_channels); + self->frame_channels->del(self->frame_channels); + self->uncompressed_LSBs->del(self->uncompressed_LSBs); + self->residuals->del(self->residuals); +} +#endif + +static int +parse_decoding_parameters(decoders_ALACDecoder *self) +{ + BitstreamReader* mdia_atom = br_substream_new(BS_BIG_ENDIAN); + BitstreamReader* stsd_atom = br_substream_new(BS_BIG_ENDIAN); + BitstreamReader* mdhd_atom = br_substream_new(BS_BIG_ENDIAN); + uint32_t mdia_atom_size; + uint32_t stsd_atom_size; + uint32_t mdhd_atom_size; + unsigned int total_frames; + + /*find the mdia atom, which is the parent to stsd and mdhd*/ + if (find_sub_atom(self->bitstream, mdia_atom, &mdia_atom_size, + "moov", "trak", "mdia", NULL)) { +#ifndef STANDALONE + PyErr_SetString(PyExc_ValueError, "unable to find mdia atom"); +#endif + goto error; + } else { + /*mark the mdia atom so we can parse + two different trees from it*/ + mdia_atom->mark(mdia_atom); + } + + /*find the stsd atom, which contains the alac atom*/ + if (find_sub_atom(mdia_atom, stsd_atom, &stsd_atom_size, + "minf", "stbl", "stsd", NULL)) { + mdia_atom->unmark(mdia_atom); +#ifndef STANDALONE + PyErr_SetString(PyExc_ValueError, "unable to find sdsd atom"); +#endif + goto error; + } + + /*parse the alac atom, which contains lots of crucial decoder details*/ + switch (read_alac_atom(stsd_atom, + &(self->max_samples_per_frame), + &(self->bits_per_sample), + &(self->history_multiplier), + &(self->initial_history), + &(self->maximum_k), + &(self->channels), + &(self->sample_rate))) { + case 1: + mdia_atom->unmark(mdia_atom); +#ifndef STANDALONE + PyErr_SetString(PyExc_IOError, "I/O error reading alac atom"); +#endif + goto error; + case 2: + mdia_atom->unmark(mdia_atom); +#ifndef STANDALONE + PyErr_SetString(PyExc_ValueError, "invalid alac atom"); +#endif + goto error; + default: + break; + } + + /*find the mdhd atom*/ + mdia_atom->rewind(mdia_atom); + if (find_sub_atom(mdia_atom, mdhd_atom, &mdhd_atom_size, + "mdhd", NULL)) { + mdia_atom->unmark(mdia_atom); +#ifndef STANDALONE + PyErr_SetString(PyExc_ValueError, "unable to find mdhd atom"); +#endif + goto error; + } else { + mdia_atom->unmark(mdia_atom); + } + + /*parse the mdhd atom, which contains our total frame count*/ + switch (read_mdhd_atom(mdhd_atom, &total_frames)) { + case 1: +#ifndef STANDALONE + PyErr_SetString(PyExc_IOError, "I/O error reading mdhd atom"); +#endif + goto error; + case 2: +#ifndef STANDALONE + PyErr_SetString(PyExc_ValueError, "invalid mdhd atom"); +#endif + goto error; + default: + self->remaining_frames = total_frames; + break; + } + + mdia_atom->close(mdia_atom); + stsd_atom->close(stsd_atom); + mdhd_atom->close(mdhd_atom); + + return 0; + +error: + mdia_atom->close(mdia_atom); + stsd_atom->close(stsd_atom); + mdhd_atom->close(mdhd_atom); + return -1; +} + static void alac_order_to_wave_order(array_ia* alac_ordered) { @@ -497,10 +599,9 @@ /*if uncompressed LSBs, read a block of partial samples to prepend*/ if (uncompressed_LSBs > 0) { LSBs = self->uncompressed_LSBs; - LSBs->reset(LSBs); + LSBs->reset_for(LSBs, channel_count * sample_count); for (i = 0; i < (channel_count * sample_count); i++) - LSBs->append(LSBs, - mdat->read(mdat, uncompressed_LSBs * 8)); + a_append(LSBs, mdat->read(mdat, uncompressed_LSBs * 8)); } sample_size = (self->bits_per_sample - @@ -559,13 +660,6 @@ } } -static PyObject* -ALACDecoder_close(decoders_ALACDecoder* self, PyObject *args) -{ - Py_INCREF(Py_None); - return Py_None; -} - static status seek_mdat(BitstreamReader* alac_stream) { @@ -645,8 +739,7 @@ unsigned int zero_block_size; int i, j; - residuals->reset(residuals); - residuals->resize(residuals, residual_count); + residuals->reset_for(residuals, residual_count); for (i = 0; i < residual_count; i++) { /*get an unsigned residual based on "history" @@ -686,8 +779,7 @@ /*ensure block of zeroes doesn't exceed remaining residual count*/ - zero_block_size = MIN(zero_block_size, - residual_count - i); + zero_block_size = MIN(zero_block_size, residual_count - i); for (j = 0; j < zero_block_size; j++) { a_append(residuals, 0); @@ -775,8 +867,7 @@ int* residuals_data = residuals->_; int i = 0; - samples->reset(samples); - samples->resize(samples, MAX(qlp_coeff->len, residuals->len)); + samples->reset_for(samples, MAX(qlp_coeff->len, residuals->len)); /*first sample always copied verbatim*/ a_append(samples, residuals_data[i++]); @@ -966,6 +1057,8 @@ /*otherwise, return the found atom*/ child_atom->substream_append(child_atom, sub_atom, child_atom_size); *sub_atom_size = child_atom_size; + child_atom->close(child_atom); + parent_atom->close(parent_atom); va_end(ap); return 0; } @@ -1044,3 +1137,113 @@ decoder->error_message = message; return ERROR; } + +#ifdef STANDALONE +int main(int argc, char* argv[]) { + decoders_ALACDecoder decoder; + unsigned channel_count; + BitstreamReader* mdat; + array_ia* frameset_channels; + unsigned frame; + unsigned channel; + unsigned bytes_per_sample; + FrameList_int_to_char_converter converter; + unsigned char* output_data; + unsigned output_data_size; + unsigned pcm_size; + + if (argc < 2) { + fprintf(stderr, "*** Usage: %s <file.m4a>\n", argv[0]); + return 1; + } + + if (ALACDecoder_init(&decoder, argv[1])) { + fprintf(stderr, "*** Error: unable to initialize ALAC file\n"); + ALACDecoder_dealloc(&decoder); + return 1; + } else { + mdat = decoder.bitstream; + frameset_channels = decoder.frameset_channels; + output_data = malloc(1); + output_data_size = 1; + bytes_per_sample = decoder.bits_per_sample / 8; + converter = FrameList_get_int_to_char_converter( + decoder.bits_per_sample, 0, 1); + } + + while (decoder.remaining_frames) { + if (!setjmp(*br_try(mdat))) { + frameset_channels->reset(frameset_channels); + + /*get initial frame's channel count*/ + channel_count = mdat->read(mdat, 3) + 1; + while (channel_count != 8) { + /*read a frame from the frameset into "channels"*/ + if (read_frame(&decoder, + mdat, + frameset_channels, + channel_count) != OK) { + br_etry(mdat); + fprintf(stderr, "*** Error: %s", decoder.error_message); + goto error; + } else { + /*ensure all frames have the same sample count*/ + /*FIXME*/ + + /*read the channel count of the next frame + in the frameset, if any*/ + channel_count = mdat->read(mdat, 3) + 1; + } + } + + /*once all the frames in the frameset are read, + byte-align the output stream*/ + mdat->byte_align(mdat); + br_etry(mdat); + + /*decrement the remaining sample count*/ + decoder.remaining_frames -= MIN(decoder.remaining_frames, + frameset_channels->_[0]->len); + + /*convert ALAC channel assignment to + standard audiotools assignment*/ + alac_order_to_wave_order(frameset_channels); + + /*finally, build and return framelist string from the sample data*/ + pcm_size = (bytes_per_sample * + frameset_channels->len * + frameset_channels->_[0]->len); + if (pcm_size > output_data_size) { + output_data_size = pcm_size; + output_data = realloc(output_data, output_data_size); + } + for (channel = 0; channel < frameset_channels->len; channel++) { + const array_i* channel_data = frameset_channels->_[channel]; + for (frame = 0; frame < channel_data->len; frame++) { + converter(channel_data->_[frame], + output_data + + ((frame * frameset_channels->len) + channel) * + bytes_per_sample); + } + } + + fwrite(output_data, sizeof(unsigned char), pcm_size, stdout); + } else { + br_etry(mdat); + fprintf(stderr, "*** Error: EOF during frame reading\n"); + goto error; + } + } + + ALACDecoder_dealloc(&decoder); + free(output_data); + + return 0; + +error: + ALACDecoder_dealloc(&decoder); + free(output_data); + + return 1; +} +#endif
View file
audiotools-2.18.tar.gz/src/decoders/alac.h -> audiotools-2.19.tar.gz/src/decoders/alac.h
Changed
@@ -1,4 +1,6 @@ +#ifndef STANDALONE #include <Python.h> +#endif #include <stdint.h> #include "../bitstream.h" #include "../array.h" @@ -32,7 +34,9 @@ }; typedef struct { +#ifndef STANDALONE PyObject_HEAD +#endif char* filename; FILE* file; @@ -42,6 +46,7 @@ unsigned int channels; unsigned int bits_per_sample; + int closed; unsigned int remaining_frames; /*a bunch of decoding fields pulled from the stream's 'alac' atom*/ @@ -56,8 +61,10 @@ array_i* residuals; struct alac_subframe_header subframe_headers[MAX_CHANNELS]; +#ifndef STANDALONE /*a framelist generator*/ PyObject* audiotools_pcm; +#endif /*a place to store error messages to be bubbled-up to the interpreter*/ char* error_message; @@ -65,6 +72,7 @@ typedef enum {OK, ERROR} status; +#ifndef STANDALONE /*the ALACDecoder.sample_rate attribute getter*/ static PyObject* ALACDecoder_sample_rate(decoders_ALACDecoder *self, void *closure); @@ -94,67 +102,6 @@ ALACDecoder_init(decoders_ALACDecoder *self, PyObject *args, PyObject *kwds); -static int -parse_decoding_parameters(decoders_ALACDecoder *self); - -/*walks through the open QuickTime stream looking for the 'mdat' atom - or returns ERROR if one cannot be found*/ -static status -seek_mdat(BitstreamReader* alac_stream); - -/*swaps the ALAC-ordered set of channels to Wave order, - depending on the number of ALAC-ordered channels*/ -static void -alac_order_to_wave_order(array_ia* alac_ordered); - -/*appends 1 or 2 channels worth of data from the current bitstream - to the "samples" arrays - returns OK on success - or returns ERROR and sets self->error_message if some problem occurs*/ -static status -read_frame(decoders_ALACDecoder *self, - BitstreamReader *mdat, - array_ia* frameset_channels, - unsigned channel_count); - -/*reads "subframe header" from the current bitstream*/ -static void -read_subframe_header(BitstreamReader *bs, - struct alac_subframe_header *subframe_header); - -/*reads a block of residuals from the current bitstream*/ -static void -read_residuals(BitstreamReader *bs, - array_i* residuals, - unsigned int residual_count, - unsigned int sample_size, - unsigned int initial_history, - unsigned int history_multiplier, - unsigned int maximum_k); - -/*reads an unsigned residual from the current bitstream*/ -static unsigned -read_residual(BitstreamReader *bs, - unsigned int lsb_count, - unsigned int sample_size); - -/*decodes the given residuals, QLP coefficient values and shift needed - to the given samples*/ -static void -decode_subframe(array_i* samples, - unsigned sample_size, - array_i* residuals, - array_i* qlp_coeff, - uint8_t qlp_shift_needed); - -/*decorrelates 2 channels, in-place*/ -static void -decorrelate_channels(array_i* left, - array_i* right, - unsigned interlacing_shift, - unsigned interlacing_leftweight); - - PyGetSetDef ALACDecoder_getseters[] = { {"sample_rate", (getter)ALACDecoder_sample_rate, NULL, "sample rate", NULL}, @@ -170,7 +117,7 @@ PyMethodDef ALACDecoder_methods[] = { {"read", (PyCFunction)ALACDecoder_read, METH_VARARGS, - "Reads the given number of bytes from the ALAC file, if possible"}, + "Reads the given number of PCM frames from the ALAC file, if possible"}, {"close", (PyCFunction)ALACDecoder_close, METH_NOARGS, "Closes the ALAC decoder stream"}, {NULL} @@ -224,6 +171,68 @@ ALACDecoder_new, /* tp_new */ }; +#endif + +static int +parse_decoding_parameters(decoders_ALACDecoder *self); + +/*walks through the open QuickTime stream looking for the 'mdat' atom + or returns ERROR if one cannot be found*/ +static status +seek_mdat(BitstreamReader* alac_stream); + +/*swaps the ALAC-ordered set of channels to Wave order, + depending on the number of ALAC-ordered channels*/ +static void +alac_order_to_wave_order(array_ia* alac_ordered); + +/*appends 1 or 2 channels worth of data from the current bitstream + to the "samples" arrays + returns OK on success + or returns ERROR and sets self->error_message if some problem occurs*/ +static status +read_frame(decoders_ALACDecoder *self, + BitstreamReader *mdat, + array_ia* frameset_channels, + unsigned channel_count); + +/*reads "subframe header" from the current bitstream*/ +static void +read_subframe_header(BitstreamReader *bs, + struct alac_subframe_header *subframe_header); + +/*reads a block of residuals from the current bitstream*/ +static void +read_residuals(BitstreamReader *bs, + array_i* residuals, + unsigned int residual_count, + unsigned int sample_size, + unsigned int initial_history, + unsigned int history_multiplier, + unsigned int maximum_k); + +/*reads an unsigned residual from the current bitstream*/ +static unsigned +read_residual(BitstreamReader *bs, + unsigned int lsb_count, + unsigned int sample_size); + +/*decodes the given residuals, QLP coefficient values and shift needed + to the given samples*/ +static void +decode_subframe(array_i* samples, + unsigned sample_size, + array_i* residuals, + array_i* qlp_coeff, + uint8_t qlp_shift_needed); + +/*decorrelates 2 channels, in-place*/ +static void +decorrelate_channels(array_i* left, + array_i* right, + unsigned interlacing_shift, + unsigned interlacing_leftweight); + /*returns 0 if the given sub atom name is found in the parent and sets "sub_atom" to that atom data and "sub_atom_size" to its size (not including the 64 bit header)
View file
audiotools-2.18.tar.gz/src/decoders/aob.c -> audiotools-2.19.tar.gz/src/decoders/aob.c
Changed
@@ -1,4 +1,6 @@ #include "aob.h" +#include <string.h> +#include <math.h> #include <dirent.h> #include <stdlib.h> #include <ctype.h> @@ -30,6 +32,7 @@ #define PCM_CODEC_ID 0xA0 #define MLP_CODEC_ID 0xA1 +#ifndef STANDALONE PyObject* DVDA_Title_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { decoders_DVDA_Title *self; @@ -52,6 +55,15 @@ unsigned titleset; unsigned start_sector; unsigned end_sector; +#else +int + DVDA_Title_init(decoders_DVDA_Title *self, + char* audio_ts, + unsigned titleset, + unsigned start_sector, + unsigned end_sector) +{ +#endif char* cdrom = NULL; self->sector_reader = NULL; @@ -73,6 +85,7 @@ self->codec_framelist = array_ia_new(); self->output_framelist = array_ia_new(); +#ifndef STANDALONE if ((self->audiotools_pcm = open_audiotools_pcm()) == NULL) return -1; @@ -84,19 +97,23 @@ &end_sector, &cdrom)) return -1; +#endif /*setup a sector reader according to AUDIO_TS and cdrom device*/ if ((self->sector_reader = open_sector_reader(audio_ts, titleset, cdrom)) == NULL) { +#ifndef STANDALONE PyErr_SetFromErrnoWithFilename(PyExc_IOError, audio_ts); +#endif return -1; } /*setup a packet reader according to start and end sector this packet reader will be shared by all returned DVDA_Tracks*/ self->packet_reader = open_packet_reader(self->sector_reader, - start_sector, end_sector); + start_sector, + end_sector); return 0; } @@ -120,11 +137,14 @@ self->codec_framelist->del(self->codec_framelist); self->output_framelist->del(self->output_framelist); +#ifndef STANDALONE Py_XDECREF(self->audiotools_pcm); self->ob_type->tp_free((PyObject*)self); +#endif } +#ifndef STANDALONE static PyObject* DVDA_Title_sample_rate(decoders_DVDA_Title *self, void *closure) { @@ -154,7 +174,6 @@ { return Py_BuildValue("I", self->pcm_frames_remaining); } - static PyObject* DVDA_Title_next_track(decoders_DVDA_Title *self, PyObject *args) { @@ -180,6 +199,25 @@ "I/O error reading initialization packet"); return NULL; } +#else +int +DVDA_Title_next_track(decoders_DVDA_Title *self, unsigned PTS_ticks) +{ + DVDA_Packet next_packet; + struct bs_buffer* packet_data = self->packet_data; + unsigned i; + + if (self->pcm_frames_remaining) { + fprintf(stderr, "current track has not been exhausted\n"); + return 0; + } + + if (read_audio_packet(self->packet_reader, + &next_packet, packet_data)) { + fprintf(stderr, "I/O error reading initialization packet\n"); + return 0; + } +#endif if (next_packet.codec_ID == PCM_CODEC_ID) { /*if the packet is PCM, initialize Title's stream attributes @@ -191,7 +229,8 @@ dvda_bits_per_sample(next_packet.PCM.group_1_bps); self->sample_rate = dvda_sample_rate(next_packet.PCM.group_1_rate); - self->channel_assignment = next_packet.PCM.channel_assignment; + self->channel_assignment = + next_packet.PCM.channel_assignment; self->channel_count = dvda_channel_count(next_packet.PCM.channel_assignment); self->channel_mask = @@ -273,8 +312,12 @@ } } else { +#ifndef STANDALONE PyErr_SetString(PyExc_ValueError, "unknown codec ID"); return NULL; +#else + return 0; +#endif } /*convert PTS ticks to PCM frames based on sample rate*/ @@ -289,10 +332,15 @@ self->codec_framelist->append(self->codec_framelist); } +#ifndef STANDALONE Py_INCREF(Py_None); return Py_None; +#else + return 1; +#endif } +#ifndef STANDALONE static PyObject* DVDA_Title_read(decoders_DVDA_Title *self, PyObject *args) { @@ -371,6 +419,7 @@ Py_INCREF(Py_None); return Py_None; } +#endif static char* @@ -851,3 +900,150 @@ default: return 0; } } + +#ifdef STANDALONE +int main(int argc, char* argv[]) { + decoders_DVDA_Title title; + char* audio_ts; + unsigned titleset; + unsigned start_sector; + unsigned end_sector; + unsigned pts_ticks; + + DVDA_Packet next_packet; + struct bs_buffer* packet_data; + mlp_status mlp_status; + + unsigned output_data_size = 1; + uint8_t* output_data; + + unsigned bytes_per_sample; + FrameList_int_to_char_converter converter; + + if (argc < 6) { + fprintf(stderr, "*** Usage: %s <AUDIO_TS> <titleset> " + "<start sector> <end sector> <PTS ticks>\n", argv[0]); + return 1; + } else { + audio_ts = argv[1]; + titleset = strtoul(argv[2], NULL, 10); + start_sector = strtoul(argv[3], NULL, 10); + end_sector = strtoul(argv[4], NULL, 10); + pts_ticks = strtoul(argv[5], NULL, 10); + } + + if (DVDA_Title_init(&title, audio_ts, titleset, + start_sector, end_sector)) { + fprintf(stderr, "*** Error: unable to initialize DVDA title\n"); + return 1; + } else { + packet_data = title.packet_data; + output_data = malloc(output_data_size); + } + + if (!DVDA_Title_next_track(&title, pts_ticks)) { + fprintf(stderr, "*** Error getting next track\n"); + goto error; + } else { + bytes_per_sample = title.bits_per_sample / 8; + converter = FrameList_get_int_to_char_converter( + title.bits_per_sample, 0, 1); + } + + fprintf(stderr, "frames remaining: %u\n", title.pcm_frames_remaining); + + while (title.pcm_frames_remaining) { + unsigned pcm_size; + unsigned channel; + unsigned frame; + + /*build FrameList from PCM or MLP packet data*/ + switch (title.frame_codec) { + case PCM: + /*if not enough bytes in buffer, read another packet*/ + while (aobpcm_packet_empty(&(title.pcm_decoder), title.frames)) { + if (read_audio_packet(title.packet_reader, + &next_packet, packet_data)) { + fprintf(stderr, "I/O Error reading PCM packet\n"); + goto error; + } + + /*FIXME - ensure packet has same format as PCM*/ + + buf_append(packet_data, title.frames); + } + + /*FIXME - make this thread friendly*/ + read_aobpcm(&(title.pcm_decoder), + title.frames, + title.codec_framelist); + break; + case MLP: + /*if not enough bytes in buffer, read another packet*/ + while (mlp_packet_empty(title.mlp_decoder)) { + if (read_audio_packet(title.packet_reader, + &next_packet, packet_data)) { + fprintf(stderr, "I/O Error reading MLP packet\n"); + goto error; + } + + /*FIXME - ensure packet has same format as MLP*/ + + buf_append(packet_data, title.frames); + } + + /*FIXME - make this thread friendly*/ + if ((mlp_status = read_mlp_frames(title.mlp_decoder, + title.codec_framelist)) != OK) { + fprintf(stderr, "*** MLP Error: %s\n", + mlp_python_exception_msg(mlp_status)); + goto error; + } + } + + /*account for framelists larger than frames remaining*/ + title.codec_framelist->cross_split( + title.codec_framelist, + MIN(title.pcm_frames_remaining, title.codec_framelist->_[0]->len), + title.output_framelist, + title.codec_framelist); + + /*deduct output FrameList's PCM frames from remaining count*/ + title.pcm_frames_remaining -= title.output_framelist->_[0]->len; + + /*convert framelist data to string*/ + pcm_size = (bytes_per_sample * + title.output_framelist->len * + title.output_framelist->_[0]->len); + + if (pcm_size > output_data_size) { + output_data_size = pcm_size; + output_data = realloc(output_data, output_data_size); + } + for (channel = 0; channel < title.output_framelist->len; channel++) { + const array_i* channel_data = title.output_framelist->_[channel]; + for (frame = 0; frame < channel_data->len; frame++) { + converter(channel_data->_[frame], + output_data + + ((frame * title.output_framelist->len) + channel) * + bytes_per_sample); + } + } + + /*output framelist data to stdout*/ + fwrite(output_data, sizeof(unsigned char), pcm_size, stdout); + } + + + DVDA_Title_dealloc(&title); + free(output_data); + + return 0; + +error: + DVDA_Title_dealloc(&title); + free(output_data); + + return 0; +} +#endif
View file
audiotools-2.18.tar.gz/src/decoders/aob.h -> audiotools-2.19.tar.gz/src/decoders/aob.h
Changed
@@ -1,4 +1,6 @@ +#ifndef STANDALONE #include <Python.h> +#endif #include "../array.h" #include "../bitstream.h" #include "aobpcm.h" @@ -35,7 +37,9 @@ /*a title in a given titleset this generates DVDA_Track objects which are actual decoders*/ typedef struct { +#ifndef STANDALONE PyObject_HEAD +#endif /*an AOB sector reader*/ struct DVDA_Sector_Reader_s* sector_reader; @@ -69,16 +73,19 @@ /*a FrameList to be returned by calls to read()*/ array_ia* output_framelist; +#ifndef STANDALONE /*a FrameList generator*/ PyObject* audiotools_pcm; +#endif } decoders_DVDA_Title; -static PyObject* -DVDA_Title_new(PyTypeObject *type, PyObject *args, PyObject *kwds); - void DVDA_Title_dealloc(decoders_DVDA_Title *self); +#ifndef STANDALONE +static PyObject* +DVDA_Title_new(PyTypeObject *type, PyObject *args, PyObject *kwds); + int DVDA_Title_init(decoders_DVDA_Title *self, PyObject *args, PyObject *kwds); @@ -171,7 +178,7 @@ 0, /* tp_alloc */ DVDA_Title_new, /* tp_new */ }; - +#endif /*given a path to the AUDIO_TS directory and a filename to search for, in upper case,
View file
audiotools-2.18.tar.gz/src/decoders/aobpcm.h -> audiotools-2.19.tar.gz/src/decoders/aobpcm.h
Changed
@@ -1,6 +1,8 @@ #ifndef AOBPCM2 #define AOBPCM2 +#ifndef STANDALONE #include <Python.h> +#endif #include <stdint.h> #include "../bitstream.h" #include "../array.h"
View file
audiotools-2.18.tar.gz/src/decoders/flac.c -> audiotools-2.19.tar.gz/src/decoders/flac.c
Changed
@@ -20,6 +20,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *******************************************************/ +#ifndef STANDALONE int FlacDecoder_init(decoders_FlacDecoder *self, PyObject *args, PyObject *kwds) @@ -79,13 +80,13 @@ audiotools__MD5Init(&(self->md5)); self->stream_finalized = 0; - /*add callback for CRC16 calculation*/ - br_add_callback(self->bitstream, flac_crc16, &(self->crc16)); - /*setup a framelist generator function*/ if ((self->audiotools_pcm = open_audiotools_pcm()) == NULL) return -1; + /*mark stream as not closed and ready for reading*/ + self->closed = 0; + return 0; } @@ -93,7 +94,10 @@ FlacDecoder_close(decoders_FlacDecoder* self, PyObject *args) { - self->remaining_samples = 0; + /*mark stream as closed so more calls to read() + generate ValueErrors*/ + self->closed = 1; + Py_INCREF(Py_None); return Py_None; } @@ -127,60 +131,6 @@ return (PyObject *)self; } -int -flacdec_read_metadata(BitstreamReader *bitstream, - struct flac_STREAMINFO *streaminfo) -{ - unsigned int last_block; - unsigned int block_type; - unsigned int block_length; - - if (!setjmp(*br_try(bitstream))) { - if (bitstream->read(bitstream, 32) != 0x664C6143u) { - PyErr_SetString(PyExc_ValueError, "not a FLAC file"); - br_etry(bitstream); - return 1; - } - - do { - last_block = bitstream->read(bitstream, 1); - block_type = bitstream->read(bitstream, 7); - block_length = bitstream->read(bitstream, 24); - - if (block_type == 0) { - streaminfo->minimum_block_size = - bitstream->read(bitstream, 16); - streaminfo->maximum_block_size = - bitstream->read(bitstream, 16); - streaminfo->minimum_frame_size = - bitstream->read(bitstream, 24); - streaminfo->maximum_frame_size = - bitstream->read(bitstream, 24); - streaminfo->sample_rate = - bitstream->read(bitstream, 20); - streaminfo->channels = - bitstream->read(bitstream, 3) + 1; - streaminfo->bits_per_sample = - bitstream->read(bitstream, 5) + 1; - streaminfo->total_samples = - bitstream->read_64(bitstream, 36); - - bitstream->read_bytes(bitstream, streaminfo->md5sum, 16); - } else { - bitstream->skip(bitstream, block_length * 8); - } - } while (!last_block); - - br_etry(bitstream); - return 0; - } else { - PyErr_SetString(PyExc_IOError, - "EOF while reading STREAMINFO block"); - br_etry(bitstream); - return 1; - } -} - static PyObject* FlacDecoder_sample_rate(decoders_FlacDecoder *self, void *closure) { @@ -208,12 +158,18 @@ PyObject* FlacDecoder_read(decoders_FlacDecoder* self, PyObject *args) { + uint32_t crc16 = 0; int channel; struct flac_frame_header frame_header; PyObject* framelist; PyThreadState *thread_state; flac_status error; + if (self->closed) { + PyErr_SetString(PyExc_ValueError, "cannot read closed stream"); + return NULL; + } + self->subframe_data->reset(self->subframe_data); /*if all samples have been read, return an empty FrameList*/ @@ -238,16 +194,20 @@ } thread_state = PyEval_SaveThread(); - self->crc16 = 0; if (!setjmp(*br_try(self->bitstream))) { + /*add callback for CRC16 calculation*/ + br_add_callback(self->bitstream, (bs_callback_func)flac_crc16, &crc16); + /*read frame header*/ if ((error = flacdec_read_frame_header(self->bitstream, &(self->streaminfo), &frame_header)) != OK) { + br_pop_callback(self->bitstream, NULL); PyEval_RestoreThread(thread_state); PyErr_SetString(PyExc_ValueError, FlacDecoder_strerror(error)); - goto error; + br_etry(self->bitstream); + return NULL; } /*read 1 subframe per channel*/ @@ -262,9 +222,11 @@ flacdec_subframe_bits_per_sample(&frame_header, channel), self->subframe_data->append(self->subframe_data))) != OK) { + br_pop_callback(self->bitstream, NULL); PyEval_RestoreThread(thread_state); PyErr_SetString(PyExc_ValueError, FlacDecoder_strerror(error)); - goto error; + br_etry(self->bitstream); + return NULL; } /*handle difference channels, if any*/ @@ -275,19 +237,23 @@ /*check CRC-16*/ self->bitstream->byte_align(self->bitstream); self->bitstream->read(self->bitstream, 16); - if (self->crc16 != 0) { + br_pop_callback(self->bitstream, NULL); + if (crc16 != 0) { PyEval_RestoreThread(thread_state); PyErr_SetString(PyExc_ValueError, "invalid checksum in frame"); - goto error; + br_etry(self->bitstream); + return NULL; } /*decrement remaining samples*/ self->remaining_samples -= frame_header.block_size; } else { /*handle I/O error during read*/ + br_pop_callback(self->bitstream, NULL); PyEval_RestoreThread(thread_state); PyErr_SetString(PyExc_IOError, "EOF reading frame"); - goto error; + br_etry(self->bitstream); + return NULL; } br_etry(self->bitstream); @@ -309,10 +275,6 @@ } else { return NULL; } - - error: - br_etry(self->bitstream); - return NULL; } static PyObject* @@ -389,13 +351,113 @@ PyEval_RestoreThread(thread_state); return offsets; - error: +error: Py_XDECREF(offsets); br_etry(self->bitstream); return NULL; } flac_status +FlacDecoder_update_md5sum(decoders_FlacDecoder *self, + PyObject *framelist) +{ + PyObject *string = PyObject_CallMethod(framelist, + "to_bytes","ii", + 0, + 1); + char *string_buffer; + Py_ssize_t length; + + if (string != NULL) { + if (PyString_AsStringAndSize(string, &string_buffer, &length) == 0) { + audiotools__MD5Update(&(self->md5), + (unsigned char *)string_buffer, + length); + Py_DECREF(string); + return OK; + } else { + Py_DECREF(string); + return ERROR; + } + } else { + return ERROR; + } +} + +int +FlacDecoder_verify_okay(decoders_FlacDecoder *self) +{ + unsigned char stream_md5sum[16]; + const static unsigned char blank_md5sum[16] = {0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0}; + + audiotools__MD5Final(stream_md5sum, &(self->md5)); + + return ((memcmp(self->streaminfo.md5sum, blank_md5sum, 16) == 0) || + (memcmp(stream_md5sum, self->streaminfo.md5sum, 16) == 0)); +} + +#endif + +int +flacdec_read_metadata(BitstreamReader *bitstream, + struct flac_STREAMINFO *streaminfo) +{ + unsigned int last_block; + unsigned int block_type; + unsigned int block_length; + + if (!setjmp(*br_try(bitstream))) { + if (bitstream->read(bitstream, 32) != 0x664C6143u) { +#ifndef STANDALONE + PyErr_SetString(PyExc_ValueError, "not a FLAC file"); +#endif + br_etry(bitstream); + return 1; + } + + do { + last_block = bitstream->read(bitstream, 1); + block_type = bitstream->read(bitstream, 7); + block_length = bitstream->read(bitstream, 24); + + if (block_type == 0) { + streaminfo->minimum_block_size = + bitstream->read(bitstream, 16); + streaminfo->maximum_block_size = + bitstream->read(bitstream, 16); + streaminfo->minimum_frame_size = + bitstream->read(bitstream, 24); + streaminfo->maximum_frame_size = + bitstream->read(bitstream, 24); + streaminfo->sample_rate = + bitstream->read(bitstream, 20); + streaminfo->channels = + bitstream->read(bitstream, 3) + 1; + streaminfo->bits_per_sample = + bitstream->read(bitstream, 5) + 1; + streaminfo->total_samples = + bitstream->read_64(bitstream, 36); + + bitstream->read_bytes(bitstream, streaminfo->md5sum, 16); + } else { + bitstream->skip(bitstream, block_length * 8); + } + } while (!last_block); + + br_etry(bitstream); + return 0; + } else { +#ifndef STANDALONE + PyErr_SetString(PyExc_IOError, + "EOF while reading STREAMINFO block"); +#endif + br_etry(bitstream); + return 1; + } +} + +flac_status flacdec_read_frame_header(BitstreamReader *bitstream, struct flac_STREAMINFO *streaminfo, struct flac_frame_header *header) @@ -404,7 +466,7 @@ uint32_t sample_rate_bits; uint32_t crc8 = 0; - br_add_callback(bitstream, flac_crc8, &crc8); + br_add_callback(bitstream, (bs_callback_func)flac_crc8, &crc8); /*read and verify sync code*/ if (bitstream->read(bitstream, 14) != 0x3FFE) { @@ -636,8 +698,7 @@ { int32_t value = bitstream->read_signed(bitstream, bits_per_sample); - samples->reset(samples); - samples->mappend(samples, block_size, value); + samples->mset(samples, block_size, value); return OK; } @@ -650,8 +711,7 @@ { int32_t i; - samples->reset(samples); - samples->resize(samples, block_size); + samples->reset_for(samples, block_size); for (i = 0; i < block_size; i++) a_append(samples, @@ -673,11 +733,8 @@ int* s_data; int* r_data; - residuals->reset(residuals); - samples->reset(samples); - /*ensure that samples->data won't be realloc'ated*/ - samples->resize(samples, block_size); + samples->reset_for(samples, block_size); s_data = samples->_; /*read "order" number of warm-up samples*/ @@ -754,9 +811,7 @@ flac_status error; qlp_coeffs->reset(qlp_coeffs); - residuals->reset(residuals); - samples->reset(samples); - samples->resize(samples, block_size); + samples->reset_for(samples, block_size); s_data = samples->_; /*read order number of warm-up samples*/ @@ -807,33 +862,31 @@ uint32_t block_size, array_i* residuals) { - uint32_t coding_method = bitstream->read(bitstream, 2); - uint32_t partition_order = bitstream->read(bitstream, 4); - int total_partitions = 1 << partition_order; - int partition; - uint32_t rice_parameter; - uint32_t escape_code; - uint32_t partition_samples; - int32_t msb; - int32_t lsb; - int32_t value; + const unsigned coding_method = bitstream->read(bitstream, 2); + const unsigned partition_order = bitstream->read(bitstream, 4); + const unsigned total_partitions = 1 << partition_order; + unsigned partition; unsigned int (*read)(struct BitstreamReader_s* bs, unsigned int count); unsigned int (*read_unary)(struct BitstreamReader_s* bs, int stop_bit); - void (*append)(array_i* array, int value); read = bitstream->read; read_unary = bitstream->read_unary; - append = residuals->append; residuals->reset(residuals); /*read 2^partition_order number of partitions*/ for (partition = 0; partition < total_partitions; partition++) { + int partition_samples; + unsigned rice_parameter; + unsigned escape_code; + /*each partition after the first contains block_size / (2 ^ partition_order) number of residual values*/ if (partition == 0) { - partition_samples = (block_size / (1 << partition_order)) - order; + partition_samples = (int)(block_size / + (1 << partition_order)) - order; + partition_samples = MAX(partition_samples, 0); } else { partition_samples = block_size / (1 << partition_order); } @@ -857,21 +910,22 @@ return ERR_INVALID_CODING_METHOD; } + residuals->resize_for(residuals, partition_samples); if (!escape_code) { for (;partition_samples; partition_samples--) { - msb = read_unary(bitstream, 1); - lsb = read(bitstream, rice_parameter); - value = (msb << rice_parameter) | lsb; + const unsigned msb = read_unary(bitstream, 1); + const unsigned lsb = read(bitstream, rice_parameter); + const unsigned value = (msb << rice_parameter) | lsb; if (value & 1) { - append(residuals, -(value >> 1) - 1); + a_append(residuals, -((int)value >> 1) - 1); } else { - append(residuals, value >> 1); + a_append(residuals, (int)value >> 1); } } } else { for (;partition_samples; partition_samples--) { - append(residuals, - bitstream->read_signed(bitstream, escape_code)); + a_append(residuals, + bitstream->read_signed(bitstream, escape_code)); } } } @@ -882,98 +936,65 @@ void flacdec_decorrelate_channels(uint8_t channel_assignment, - array_ia* subframes, + const array_ia* subframes, array_i* framelist) { - int* framelist_data; unsigned i,j; - unsigned channel_count = subframes->len; - unsigned block_size = subframes->_[0]->len; + const unsigned channel_count = subframes->len; + const unsigned block_size = subframes->_[0]->len; int64_t mid; int32_t side; - framelist->reset(framelist); - framelist->resize(framelist, channel_count * block_size); - framelist_data = framelist->_; - framelist->len = channel_count * block_size; + framelist->reset_for(framelist, channel_count * block_size); switch (channel_assignment) { case 0x8: /*left-difference*/ + assert(subframes->len == 2); + assert(subframes->_[0]->len == subframes->_[1]->len); for (i = 0; i < block_size; i++) { - framelist_data[i * 2] = subframes->_[0]->_[i]; - framelist_data[i * 2 + 1] = (subframes->_[0]->_[i] - - subframes->_[1]->_[i]); + a_append(framelist, subframes->_[0]->_[i]); + a_append(framelist, (subframes->_[0]->_[i] - + subframes->_[1]->_[i])); } break; case 0x9: /*difference-right*/ + assert(subframes->len == 2); + assert(subframes->_[0]->len == subframes->_[1]->len); for (i = 0; i < block_size; i++) { - framelist_data[i * 2] = (subframes->_[0]->_[i] + - subframes->_[1]->_[i]); - framelist_data[i * 2 + 1] = subframes->_[1]->_[i]; + a_append(framelist, (subframes->_[0]->_[i] + + subframes->_[1]->_[i])); + a_append(framelist, subframes->_[1]->_[i]); } break; case 0xA: /*mid-side*/ + assert(subframes->len == 2); + assert(subframes->_[0]->len == subframes->_[1]->len); for (i = 0; i < block_size; i++) { mid = subframes->_[0]->_[i]; side = subframes->_[1]->_[i]; mid = (mid << 1) | (side & 1); - framelist_data[i * 2] = (int)((mid + side) >> 1); - framelist_data[i * 2 + 1] = (int)((mid - side) >> 1); + a_append(framelist, (int)((mid + side) >> 1)); + a_append(framelist, (int)((mid - side) >> 1)); } break; default: /*independent*/ +#ifndef NDEBUG + for (j = 0; j < channel_count; j++) { + assert(subframes->_[0]->len == subframes->_[j]->len); + } +#endif for (i = 0; i < block_size; i++) { for (j = 0; j < channel_count; j++) { - framelist_data[i * channel_count + j] = - subframes->_[j]->_[i]; + a_append(framelist, subframes->_[j]->_[i]); } } break; } } -flac_status -FlacDecoder_update_md5sum(decoders_FlacDecoder *self, - PyObject *framelist) -{ - PyObject *string = PyObject_CallMethod(framelist, - "to_bytes","ii", - 0, - 1); - char *string_buffer; - Py_ssize_t length; - - if (string != NULL) { - if (PyString_AsStringAndSize(string, &string_buffer, &length) == 0) { - audiotools__MD5Update(&(self->md5), - (unsigned char *)string_buffer, - length); - Py_DECREF(string); - return OK; - } else { - Py_DECREF(string); - return ERROR; - } - } else { - return ERROR; - } -} - -int -FlacDecoder_verify_okay(decoders_FlacDecoder *self) -{ - unsigned char stream_md5sum[16]; - const static unsigned char blank_md5sum[16] = {0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0}; - - audiotools__MD5Final(stream_md5sum, &(self->md5)); - - return ((memcmp(self->streaminfo.md5sum, blank_md5sum, 16) == 0) || - (memcmp(stream_md5sum, self->streaminfo.md5sum, 16) == 0)); -} const char* FlacDecoder_strerror(flac_status error) @@ -1016,11 +1037,195 @@ uint32_t read_utf8(BitstreamReader *stream) { - uint32_t total_bytes = stream->read_unary(stream, 0); - uint32_t value = stream->read(stream, 7 - total_bytes); + unsigned total_bytes = stream->read_unary(stream, 0); + unsigned value = stream->read(stream, 7 - total_bytes); for (;total_bytes > 1; total_bytes--) { value = (value << 6) | (stream->read(stream, 8) & 0x3F); } return value; } + +#ifdef EXECUTABLE +#include <string.h> +#include <errno.h> + +int main(int argc, char* argv[]) { + FILE* file; + BitstreamReader* reader; + struct flac_STREAMINFO streaminfo; + uint64_t remaining_frames; + + array_i* qlp_coeffs; + array_i* residuals; + array_ia* subframe_data; + array_i* framelist_data; + + FrameList_int_to_char_converter converter; + unsigned char *output_data; + unsigned output_data_size; + + audiotools__MD5Context md5; + unsigned char stream_md5sum[16]; + const static unsigned char blank_md5sum[16] = {0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0}; + + if (argc < 2) { + fprintf(stderr, "*** Usage: %s <file.flac>\n", argv[0]); + return 1; + } + + /*open input file for reading*/ + if ((file = fopen(argv[1], "rb")) == NULL) { + fprintf(stderr, "*** %s: %s\n", argv[1], strerror(errno)); + return 1; + } else { + /*open bitstream and setup temporary arrays/buffers*/ + reader = br_open(file, BS_BIG_ENDIAN); + qlp_coeffs = array_i_new(); + residuals = array_i_new(); + subframe_data = array_ia_new(); + framelist_data = array_i_new(); + + output_data = malloc(1); + output_data_size = 1; + } + + /*read initial metadata blocks*/ + if (flacdec_read_metadata(reader, &streaminfo)) { + fprintf(stderr, "*** Error reading streaminfo\n"); + goto error; + } else { + remaining_frames = streaminfo.total_samples; + } + + /*initialize the output MD5 sum*/ + audiotools__MD5Init(&md5); + + /*setup a framelist converter function*/ + converter = FrameList_get_int_to_char_converter(streaminfo.bits_per_sample, + 0, + 1); + + while (remaining_frames) { + unsigned pcm_size; + + qlp_coeffs->reset(qlp_coeffs); + residuals->reset(residuals); + subframe_data->reset(subframe_data); + framelist_data->reset(framelist_data); + + if (!setjmp(*br_try(reader))) { + flac_status error; + struct flac_frame_header frame_header; + unsigned channel; + uint32_t crc16 = 0; + + /*add callback for CRC16 calculation*/ + br_add_callback(reader, (bs_callback_func)flac_crc16, &crc16); + + /*read frame header*/ + if ((error = flacdec_read_frame_header(reader, + &streaminfo, + &frame_header)) != OK) { + br_pop_callback(reader, NULL); + br_etry(reader); + fprintf(stderr, "*** Error: %s\n", FlacDecoder_strerror(error)); + goto error; + } + + /*read 1 subframe per channels*/ + for (channel = 0; channel < frame_header.channel_count; channel++) + if ((error = + flacdec_read_subframe( + reader, + qlp_coeffs, + residuals, + (unsigned)MIN(frame_header.block_size, + remaining_frames), + flacdec_subframe_bits_per_sample(&frame_header, + channel), + subframe_data->append(subframe_data))) != OK) { + br_pop_callback(reader, NULL); + br_etry(reader); + fprintf(stderr, "*** Error: %s\n", + FlacDecoder_strerror(error)); + goto error; + } + + /*handle difference channels, if any*/ + flacdec_decorrelate_channels(frame_header.channel_assignment, + subframe_data, + framelist_data); + + /*check CRC-16*/ + reader->byte_align(reader); + reader->read(reader, 16); + br_pop_callback(reader, NULL); + if (crc16 != 0) { + br_etry(reader); + fprintf(stderr, "*** Error: invalid checksum in frame\n"); + goto error; + } + + /*decrement remaining frames*/ + remaining_frames -= frame_header.block_size; + + br_etry(reader); + } else { + /*handle I/O error during read*/ + br_pop_callback(reader, NULL); + br_etry(reader); + fprintf(stderr, "*** I/O Error reading frame\n"); + goto error; + } + + /*convert framelist to string*/ + pcm_size = (streaminfo.bits_per_sample / 8) * framelist_data->len; + if (pcm_size > output_data_size) { + output_data_size = pcm_size; + output_data = realloc(output_data, output_data_size); + } + FrameList_samples_to_char(output_data, + framelist_data->_, + converter, + framelist_data->len, + streaminfo.bits_per_sample); + + /*update MD5 sum*/ + audiotools__MD5Update(&md5, output_data, pcm_size); + + /*output framelist as string to stdout*/ + fwrite(output_data, sizeof(unsigned char), pcm_size, stdout); + } + + /*verify MD5 sum*/ + audiotools__MD5Final(stream_md5sum, &md5); + + if (!((memcmp(streaminfo.md5sum, blank_md5sum, 16) == 0) || + (memcmp(stream_md5sum, streaminfo.md5sum, 16) == 0))) { + fprintf(stderr, "*** MD5 mismatch at end of stream\n"); + goto error; + } + + reader->close(reader); + qlp_coeffs->del(qlp_coeffs); + residuals->del(residuals); + subframe_data->del(subframe_data); + framelist_data->del(framelist_data); + + free(output_data); + + return 0; +error: + reader->close(reader); + qlp_coeffs->del(qlp_coeffs); + residuals->del(residuals); + subframe_data->del(subframe_data); + framelist_data->del(framelist_data); + + free(output_data); + + return 1; +} +#endif
View file
audiotools-2.18.tar.gz/src/decoders/flac.h -> audiotools-2.19.tar.gz/src/decoders/flac.h
Changed
@@ -1,4 +1,6 @@ +#ifndef STANDALONE #include <Python.h> +#endif #include <stdint.h> #include "../bitstream.h" #include "../array.h" @@ -72,7 +74,7 @@ ERR_INVALID_FIXED_ORDER, ERR_INVALID_SUBFRAME_TYPE} flac_status; -#ifndef OGG_FLAC +#ifndef STANDALONE typedef struct { PyObject_HEAD @@ -83,8 +85,8 @@ struct flac_STREAMINFO streaminfo; uint64_t remaining_samples; + int closed; - uint32_t crc16; audiotools__MD5Context md5; int stream_finalized; @@ -145,7 +147,7 @@ PyMethodDef FlacDecoder_methods[] = { {"read", (PyCFunction)FlacDecoder_read, METH_VARARGS, - "Reads the given number of bytes from the FLAC file, if possible"}, + "Reads the given number of PCM frames from the FLAC file, if possible"}, {"offsets", (PyCFunction)FlacDecoder_offsets, METH_NOARGS, "Returns a list of (offset, PCM frame count) values"}, {"close", (PyCFunction)FlacDecoder_close, @@ -182,12 +184,15 @@ flacdec_read_subframe_header(BitstreamReader *bitstream, struct flac_subframe_header *subframe_header); +/*returns a subframe's effective bits per sample + based on the frame header and whether the subframe + is the side channel in left-side/side-right/mid-side encoding*/ unsigned int flacdec_subframe_bits_per_sample(struct flac_frame_header *frame_header, unsigned int channel_number); -/*reads a FLAC subframe from "bitstream" - with "block_size" and "bits_per_sample" (determined from the frame header) +/*reads a FLAC subframe from bitstream + with block_size and effective bits_per_sample and places the result in "samples" "qlp_coeffs" and "residuals" are temporary buffers @@ -244,13 +249,15 @@ void flacdec_decorrelate_channels(uint8_t channel_assignment, - array_ia* subframes, + const array_ia* subframes, array_i* framelist); const char* FlacDecoder_strerror(flac_status error); -#ifndef OGG_FLAC +uint32_t read_utf8(BitstreamReader *stream); + +#ifndef STANDALONE flac_status FlacDecoder_update_md5sum(decoders_FlacDecoder *self, @@ -259,8 +266,6 @@ int FlacDecoder_verify_okay(decoders_FlacDecoder *self); -uint32_t read_utf8(BitstreamReader *stream); - #ifdef IS_PY3K static PyTypeObject decoders_FlacDecoderType = { @@ -330,12 +335,12 @@ 0, /*tp_as_buffer*/ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ "FlacDecoder objects", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ FlacDecoder_methods, /* tp_methods */ 0, /* tp_members */ FlacDecoder_getseters, /* tp_getset */
View file
audiotools-2.18.tar.gz/src/decoders/mlp.c -> audiotools-2.19.tar.gz/src/decoders/mlp.c
Changed
@@ -1151,6 +1151,7 @@ cd->crc = CRC8[(cd->final_crc = cd->crc ^ byte)]; } +#ifndef STANDALONE PyObject* mlp_python_exception(mlp_status mlp_status) { @@ -1173,6 +1174,7 @@ return PyExc_ValueError; } } +#endif const char* mlp_python_exception_msg(mlp_status mlp_status)
View file
audiotools-2.18.tar.gz/src/decoders/mlp.h -> audiotools-2.19.tar.gz/src/decoders/mlp.h
Changed
@@ -1,6 +1,8 @@ #ifndef MLPDEC2 #define MLPDEC2 +#ifndef STANDALONE #include <Python.h> +#endif #include <stdint.h> #include "../bitstream.h" #include "../array.h" @@ -280,8 +282,10 @@ void mlp_checkdata_callback(uint8_t byte, void* checkdata); +#ifndef STANDALONE PyObject* mlp_python_exception(mlp_status mlp_status); +#endif const char* mlp_python_exception_msg(mlp_status mlp_status);
View file
audiotools-2.18.tar.gz/src/decoders/ogg.c -> audiotools-2.19.tar.gz/src/decoders/ogg.c
Changed
@@ -112,9 +112,6 @@ /*validate page checksum*/ if (reader->current_header.checksum != reader->checksum) { - fprintf(stderr, "0x%4.4X 0x%4.4X\n", - reader->current_header.checksum, - reader->checksum); return OGG_CHECKSUM_MISMATCH; }
View file
audiotools-2.18.tar.gz/src/decoders/oggflac.c -> audiotools-2.19.tar.gz/src/decoders/oggflac.c
Changed
@@ -1,3 +1,7 @@ +#ifdef STANDALONE +#include <string.h> +#include <errno.h> +#endif #include "oggflac.h" #include "../common/flac_crc.h" #include "../pcmconv.h" @@ -21,6 +25,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *******************************************************/ +#ifndef STANDALONE PyObject* OggFlacDecoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { decoders_OggFlacDecoder *self; @@ -60,6 +65,7 @@ self->framelist_data = array_i_new(); self->audiotools_pcm = NULL; self->packet = br_substream_new(BS_BIG_ENDIAN); + self->stream_finalized = 0; if (!PyArg_ParseTuple(args, "si", &filename, &(self->channel_mask))) return -1; @@ -102,12 +108,15 @@ audiotools__MD5Init(&(self->md5)); /*add callback for CRC16 calculation*/ - br_add_callback(self->packet, flac_crc16, &(self->crc16)); + br_add_callback(self->packet, (bs_callback_func)flac_crc16, &(self->crc16)); /*setup a framelist generator function*/ if ((self->audiotools_pcm = open_audiotools_pcm()) == NULL) return -1; + /*mark stream as not closed and ready for reading*/ + self->closed = 0; + return 0; } @@ -140,8 +149,20 @@ PyObject *framelist; PyThreadState *thread_state; + if (self->closed) { + PyErr_SetString(PyExc_ValueError, "cannot read closed stream"); + return NULL; + } + self->subframe_data->reset(self->subframe_data); + /*if all samples have been read, return an empty FrameList*/ + if (self->stream_finalized) { + return empty_FrameList(self->audiotools_pcm, + self->streaminfo.channels, + self->streaminfo.bits_per_sample); + } + thread_state = PyEval_SaveThread(); ogg_status = oggreader_next_packet(self->ogg_stream, self->packet); PyEval_RestoreThread(thread_state); @@ -230,6 +251,7 @@ then return an empty FrameList if it matches correctly*/ if (OggFlacDecoder_verify_okay(self)) { + self->stream_finalized = 1; return empty_FrameList(self->audiotools_pcm, self->streaminfo.channels, self->streaminfo.bits_per_sample); @@ -247,14 +269,55 @@ } static PyObject* -OggFlacDecoder_close(decoders_OggFlacDecoder *self, PyObject *args) { - /*FIXME*/ +OggFlacDecoder_close(decoders_OggFlacDecoder *self, PyObject *args) +{ + /*mark stream as closed so more calls to read() generate ValueErrors*/ + self->closed = 1; Py_INCREF(Py_None); return Py_None; } int +OggFlacDecoder_update_md5sum(decoders_OggFlacDecoder *self, + PyObject *framelist) { + PyObject *string = PyObject_CallMethod(framelist, + "to_bytes","ii", + 0, + 1); + char *string_buffer; + Py_ssize_t length; + + if (string != NULL) { + if (PyString_AsStringAndSize(string, &string_buffer, &length) == 0) { + audiotools__MD5Update(&(self->md5), + (unsigned char *)string_buffer, + length); + Py_DECREF(string); + return 1; + } else { + Py_DECREF(string); + return 0; + } + } else { + return 0; + } +} + +int +OggFlacDecoder_verify_okay(decoders_OggFlacDecoder *self) { + unsigned char stream_md5sum[16]; + const static unsigned char blank_md5sum[16] = {0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0}; + + audiotools__MD5Final(stream_md5sum, &(self->md5)); + + return ((memcmp(self->streaminfo.md5sum, blank_md5sum, 16) == 0) || + (memcmp(stream_md5sum, self->streaminfo.md5sum, 16) == 0)); +} +#endif + +int oggflac_read_streaminfo(BitstreamReader *packet, struct flac_STREAMINFO *streaminfo, uint16_t *header_packets) { @@ -262,29 +325,41 @@ if (!setjmp(*br_try(packet))) { if (packet->read(packet, 8) != 0x7F) { +#ifndef STANDALONE PyErr_SetString(PyExc_ValueError, "invalid packet byte"); +#endif goto error; } if (packet->read_64(packet, 32) != 0x464C4143) { +#ifndef STANDALONE PyErr_SetString(PyExc_ValueError, "invalid Ogg signature"); +#endif goto error; } if (packet->read(packet, 8) != 1) { +#ifndef STANDALONE PyErr_SetString(PyExc_ValueError, "invalid major version"); +#endif goto error; } if (packet->read(packet, 8) != 0) { +#ifndef STANDALONE PyErr_SetString(PyExc_ValueError, "invalid minor version"); +#endif goto error; } *header_packets = packet->read(packet, 16); if (packet->read_64(packet, 32) != 0x664C6143) { +#ifndef STANDALONE PyErr_SetString(PyExc_ValueError, "invalid fLaC signature"); +#endif goto error; } packet->read(packet, 1); /*last block*/ if (packet->read(packet, 7) != 0) { +#ifndef STANDALONE PyErr_SetString(PyExc_ValueError, "invalid block type"); +#endif goto error; } packet->read(packet, 24); /*block length*/ @@ -301,52 +376,206 @@ streaminfo->md5sum[i] = packet->read(packet, 8); } } else { - PyErr_SetString(PyExc_IOError, - "EOF while reading STREAMINFO block"); +#ifndef STANDALONE + PyErr_SetString(PyExc_IOError, "EOF while reading STREAMINFO block"); +#endif goto error; } br_etry(packet); return 1; - error: +error: br_etry(packet); return 0; } -int -OggFlacDecoder_update_md5sum(decoders_OggFlacDecoder *self, - PyObject *framelist) { - PyObject *string = PyObject_CallMethod(framelist, - "to_bytes","ii", - 0, - 1); - char *string_buffer; - Py_ssize_t length; +#ifdef STANDALONE +int main(int argc, char* argv[]) { + FILE* ogg_file; + OggReader* ogg_stream = NULL; + BitstreamReader* packet = NULL; + struct flac_STREAMINFO streaminfo; + uint16_t header_packets; + array_i* residuals = NULL; + array_i* qlp_coeffs = NULL; + array_ia* subframe_data = NULL; + array_i* framelist_data = NULL; + ogg_status result; + uint16_t crc16 = 0; - if (string != NULL) { - if (PyString_AsStringAndSize(string, &string_buffer, &length) == 0) { - audiotools__MD5Update(&(self->md5), - (unsigned char *)string_buffer, - length); - Py_DECREF(string); - return 1; + FrameList_int_to_char_converter converter; + unsigned pcm_size; + unsigned output_data_size = 1; + uint8_t* output_data = NULL; + + audiotools__MD5Context md5; + unsigned char stream_md5sum[16]; + const static unsigned char blank_md5sum[16] = {0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0}; + + if (argc < 2) { + fprintf(stderr, "*** Usage: %s <file.oga>\n", argv[0]); + return 1; + } + + /*open input file for reading*/ + if ((ogg_file = fopen(argv[1], "rb")) == NULL) { + fprintf(stderr, "*** %s: %s\n", argv[1], strerror(errno)); + return 1; + } else { + /*open bitstream and setup temporary arrays/buffers*/ + ogg_stream = oggreader_open(ogg_file); + packet = br_substream_new(BS_BIG_ENDIAN); + subframe_data = array_ia_new(); + residuals = array_i_new(); + qlp_coeffs = array_i_new(); + framelist_data = array_i_new(); + output_data = malloc(output_data_size); + } + + /*the first packet should be the FLAC's STREAMINFO*/ + if ((result = oggreader_next_packet(ogg_stream, packet)) == OGG_OK) { + if (!oggflac_read_streaminfo(packet, &streaminfo, &header_packets)) { + goto error; } else { - Py_DECREF(string); - return 0; + converter = FrameList_get_int_to_char_converter( + streaminfo.bits_per_sample, 0, 1); } } else { - return 0; + fprintf(stderr, "*** Error: %s\n", ogg_strerror(result)); + goto error; } -} -int -OggFlacDecoder_verify_okay(decoders_OggFlacDecoder *self) { - unsigned char stream_md5sum[16]; - const static unsigned char blank_md5sum[16] = {0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0}; + /*skip subsequent header packets*/ + for (; header_packets > 0; header_packets--) { + if ((result = oggreader_next_packet(ogg_stream, packet)) != OGG_OK) { + fprintf(stderr, "*** Error: %s\n", ogg_strerror(result)); + goto error; + } + } - audiotools__MD5Final(stream_md5sum, &(self->md5)); + /*initialize the output MD5 sum*/ + audiotools__MD5Init(&md5); - return ((memcmp(self->streaminfo.md5sum, blank_md5sum, 16) == 0) || - (memcmp(stream_md5sum, self->streaminfo.md5sum, 16) == 0)); + /*add callback for CRC16 calculation*/ + br_add_callback(packet, (bs_callback_func)flac_crc16, &crc16); + + /*decode the next FrameList from the stream*/ + result = oggreader_next_packet(ogg_stream, packet); + + while (result != OGG_STREAM_FINISHED) { + if (result == OGG_OK) { + flac_status flac_status; + struct flac_frame_header frame_header; + unsigned channel; + + subframe_data->reset(subframe_data); + + if (!setjmp(*br_try(packet))) { + /*read frame header*/ + if ((flac_status = + flacdec_read_frame_header(packet, + &streaminfo, + &frame_header)) != OK) { + fprintf(stderr, "*** Error: %s\n", + FlacDecoder_strerror(flac_status)); + br_etry(packet); + goto error; + } + + /*read 1 subframe per channel*/ + for (channel = 0; + channel < frame_header.channel_count; + channel++) + if ((flac_status = flacdec_read_subframe( + packet, + qlp_coeffs, + residuals, + frame_header.block_size, + flacdec_subframe_bits_per_sample(&frame_header, + channel), + subframe_data->append(subframe_data))) != OK) { + fprintf(stderr, "*** Error: %s\n", + FlacDecoder_strerror(flac_status)); + br_etry(packet); + goto error; + } + + br_etry(packet); + } else { + br_etry(packet); + fprintf(stderr, "*** I/O Error reading FLAC frame\n"); + goto error; + } + + /*handle difference channels, if any*/ + flacdec_decorrelate_channels(frame_header.channel_assignment, + subframe_data, + framelist_data); + + /*check CRC-16*/ + packet->byte_align(packet); + packet->read(packet, 16); + if (crc16 != 0) { + fprintf(stderr, "*** Error: invalid checksum in frame\n"); + goto error; + } + + /*turn FrameList into string of output*/ + pcm_size = (streaminfo.bits_per_sample / 8) * framelist_data->len; + if (pcm_size > output_data_size) { + output_data_size = pcm_size; + output_data = realloc(output_data, output_data_size); + } + FrameList_samples_to_char(output_data, + framelist_data->_, + converter, + framelist_data->len, + streaminfo.bits_per_sample); + + /*update MD5 sum*/ + audiotools__MD5Update(&md5, output_data, pcm_size); + + /*output string to stdout*/ + fwrite(output_data, sizeof(unsigned char), pcm_size, stdout); + + result = oggreader_next_packet(ogg_stream, packet); + } else { + /*some error reading Ogg stream*/ + fprintf(stderr, "*** Error: %s\n", ogg_strerror(result)); + goto error; + } + } + + /*Ogg stream is finished so verify stream's MD5 sum*/ + audiotools__MD5Final(stream_md5sum, &md5); + + if (!((memcmp(streaminfo.md5sum, blank_md5sum, 16) == 0) || + (memcmp(stream_md5sum, streaminfo.md5sum, 16) == 0))) { + fprintf(stderr, "*** MD5 mismatch at end of stream\n"); + goto error; + } + + /*close streams, temporary buffers*/ + oggreader_close(ogg_stream); + packet->close(packet); + subframe_data->del(subframe_data); + residuals->del(residuals); + qlp_coeffs->del(qlp_coeffs); + framelist_data->del(framelist_data); + free(output_data); + + return 0; + +error: + oggreader_close(ogg_stream); + packet->close(packet); + subframe_data->del(subframe_data); + residuals->del(residuals); + qlp_coeffs->del(qlp_coeffs); + framelist_data->del(framelist_data); + free(output_data); + + return 1; } +#endif
View file
audiotools-2.18.tar.gz/src/decoders/oggflac.h -> audiotools-2.19.tar.gz/src/decoders/oggflac.h
Changed
@@ -1,10 +1,17 @@ +#ifndef STANDALONE #include <Python.h> +#endif #include <stdint.h> #include "../bitstream.h" #include "../array.h" #include "ogg.h" -#define OGG_FLAC +#ifndef STANDALONE +#define STANDALONE #include "flac.h" +#undef STANDALONE +#else +#include "flac.h" +#endif /******************************************************** Audio Tools, a module and set of tools for manipulating audio data @@ -25,6 +32,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *******************************************************/ +#ifndef STANDALONE typedef struct { PyObject_HEAD @@ -37,6 +45,8 @@ uint32_t crc16; audiotools__MD5Context md5; + int stream_finalized; + int closed; /*temporary buffers we don't want to reallocate each time*/ array_ia* subframe_data; @@ -133,12 +143,14 @@ }; int -oggflac_read_streaminfo(BitstreamReader *bitstream, - struct flac_STREAMINFO *streaminfo, - uint16_t *header_packets); -int OggFlacDecoder_update_md5sum(decoders_OggFlacDecoder *self, PyObject *framelist); int OggFlacDecoder_verify_okay(decoders_OggFlacDecoder *self); +#endif + +int +oggflac_read_streaminfo(BitstreamReader *bitstream, + struct flac_STREAMINFO *streaminfo, + uint16_t *header_packets);
View file
audiotools-2.18.tar.gz/src/decoders/shn.c -> audiotools-2.19.tar.gz/src/decoders/shn.c
Changed
@@ -1,5 +1,7 @@ #include "shn.h" #include "../pcmconv.h" +#include <string.h> +#include <math.h> /******************************************************** Audio Tools, a module and set of tools for manipulating audio data @@ -20,6 +22,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *******************************************************/ +#ifndef STANDALONE PyObject* SHNDecoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds) @@ -38,7 +41,6 @@ char* filename; FILE* fp; - self->filename = NULL; self->bitstream = NULL; self->stream_finished = 0; @@ -57,8 +59,6 @@ if (!PyArg_ParseTuple(args, "s", &filename)) return -1; - self->filename = strdup(filename); - /*open the shn file*/ if ((fp = fopen(filename, "rb")) == NULL) { self->bitstream = NULL; @@ -69,68 +69,24 @@ } /*read Shorten header for basic info*/ - if (!setjmp(*br_try(self->bitstream))) { - uint8_t magic_number[4]; - unsigned version; - unsigned i; - - self->bitstream->parse(self->bitstream, "4b 8u", - magic_number, - &version); - if (memcmp(magic_number, "ajkg", 4)) { - PyErr_SetString(PyExc_ValueError, "invalid magic number"); - br_etry(self->bitstream); - return -1; - } - if (version != 2) { - PyErr_SetString(PyExc_ValueError, "invalid Shorten version"); - br_etry(self->bitstream); - return -1; - } - - self->header.file_type = read_long(self->bitstream); - self->header.channels = read_long(self->bitstream); - self->block_length = read_long(self->bitstream); - self->header.max_LPC = read_long(self->bitstream); - self->header.mean_count = read_long(self->bitstream); - self->bitstream->skip_bytes(self->bitstream, - read_long(self->bitstream)); - - if ((1 <= self->header.file_type) && (self->header.file_type <= 2)) { - self->bits_per_sample = 8; - self->signed_samples = (self->header.file_type == 1); - } else if ((3 <= self->header.file_type) && - (self->header.file_type <= 6)) { - self->bits_per_sample = 16; - self->signed_samples = ((self->header.file_type == 3) || - (self->header.file_type == 5)); - } else { - PyErr_SetString(PyExc_ValueError, "unsupported Shorten file type"); - br_etry(self->bitstream); - return -1; - } - - for (i = 0; i < self->header.channels; i++) { - array_i* means = self->means->append(self->means); - means->mset(means, self->header.mean_count, 0); - self->previous_samples->append(self->previous_samples); - } - - /*process first instruction for wave/aiff header, if present*/ - if (process_header(self->bitstream, - &(self->sample_rate), &(self->channel_mask))) { - br_etry(self->bitstream); - return -1; - } - - br_etry(self->bitstream); - - return 0; - } else { - /*read error in Shorten header*/ - br_etry(self->bitstream); + switch (read_shn_header(self, self->bitstream)) { + case INVALID_MAGIC_NUMBER: + PyErr_SetString(PyExc_ValueError, "invalid magic number"); + return -1; + case INVALID_SHORTEN_VERSION: + PyErr_SetString(PyExc_ValueError, "invalid Shorten version"); + return -1; + case UNSUPPORTED_FILE_TYPE: + PyErr_SetString(PyExc_ValueError, "unsupported Shorten file type"); + return -1; + case IOERROR: PyErr_SetString(PyExc_IOError, "I/O error reading Shorten header"); return -1; + default: + /*mark stream as not closed and ready for reading*/ + self->closed = 0; + + return 0; } } @@ -150,15 +106,15 @@ self->bitstream->close(self->bitstream); } - if (self->filename != NULL) - free(self->filename); - self->ob_type->tp_free((PyObject*)self); } PyObject* SHNDecoder_close(decoders_SHNDecoder* self, PyObject *args) { + /*mark stream as closed so more calls to read() generate ValueErrors*/ + self->closed = 1; + Py_INCREF(Py_None); return Py_None; } @@ -191,11 +147,12 @@ PyObject* SHNDecoder_read(decoders_SHNDecoder* self, PyObject *args) { - unsigned c = 0; PyThreadState *thread_state = NULL; - self->samples->reset(self->samples); - self->unshifted->reset(self->unshifted); + if (self->closed) { + PyErr_SetString(PyExc_ValueError, "cannot read closed stream"); + return NULL; + } if (self->stream_finished) { return empty_FrameList(self->audiotools_pcm, @@ -204,6 +161,207 @@ } thread_state = PyEval_SaveThread(); + self->unshifted->reset(self->unshifted); + + switch (read_framelist(self, self->unshifted)) { + case OK: + PyEval_RestoreThread(thread_state); + return array_ia_to_FrameList(self->audiotools_pcm, + self->unshifted, + self->bits_per_sample); + case END_OF_STREAM: + PyEval_RestoreThread(thread_state); + return empty_FrameList(self->audiotools_pcm, + self->header.channels, + self->bits_per_sample); + case UNKNOWN_COMMAND: + PyEval_RestoreThread(thread_state); + PyErr_SetString(PyExc_ValueError, + "unknown command in Shorten stream"); + return NULL; + case IOERROR: + PyEval_RestoreThread(thread_state); + PyErr_SetString(PyExc_IOError, "I/O error reading Shorten file"); + return NULL; + default: + /*shouldn't get here*/ + PyEval_RestoreThread(thread_state); + PyErr_SetString(PyExc_ValueError, + "unknown value from read_framelist()"); + return NULL; + } +} + +static PyObject* +SHNDecoder_pcm_split(decoders_SHNDecoder* self, PyObject *args) +{ + if (!setjmp(*br_try(self->bitstream))) { + array_i* header = self->pcm_header; + array_i* footer = self->pcm_footer; + array_i* current = header; + uint8_t* header_s; + uint8_t* footer_s; + PyObject* tuple; + + unsigned command; + unsigned i; + + header->reset(header); + footer->reset(footer); + + /*walk through file, processing all commands*/ + do { + unsigned energy; + unsigned LPC_count; + unsigned verbatim_size; + + command = read_unsigned(self->bitstream, COMMAND_SIZE); + + switch (command) { + case FN_DIFF0: + case FN_DIFF1: + case FN_DIFF2: + case FN_DIFF3: + /*all the DIFF commands have the same structure*/ + energy = read_unsigned(self->bitstream, ENERGY_SIZE); + for (i = 0; i < self->block_length; i++) { + skip_signed(self->bitstream, energy); + } + current = footer; + break; + case FN_QUIT: + self->stream_finished = 1; + break; + case FN_BLOCKSIZE: + self->block_length = read_long(self->bitstream); + break; + case FN_BITSHIFT: + skip_unsigned(self->bitstream, SHIFT_SIZE); + break; + case FN_QLPC: + energy = read_unsigned(self->bitstream, ENERGY_SIZE); + LPC_count = read_unsigned(self->bitstream, LPC_COUNT_SIZE); + for (i = 0; i < LPC_count; i++) { + skip_signed(self->bitstream, LPC_COEFF_SIZE); + } + for (i = 0; i < self->block_length; i++) { + skip_signed(self->bitstream, energy); + } + current = footer; + break; + case FN_ZERO: + current = footer; + break; + case FN_VERBATIM: + /*any VERBATIM commands have their data appended + to header or footer*/ + verbatim_size = read_unsigned(self->bitstream, + VERBATIM_CHUNK_SIZE); + for (i = 0; i < verbatim_size; i++) { + current->append(current, + read_unsigned(self->bitstream, + VERBATIM_BYTE_SIZE)); + } + break; + } + } while (command != FN_QUIT); + + br_etry(self->bitstream); + + /*once all commands have been processed, + transform the bytes in header and footer to strings*/ + header_s = malloc(sizeof(uint8_t) * header->len); + for (i = 0; i < header->len; i++) + header_s[i] = (uint8_t)(header->_[i] & 0xFF); + + footer_s = malloc(sizeof(uint8_t) * footer->len); + for (i = 0; i < footer->len; i++) + footer_s[i] = (uint8_t)(footer->_[i] & 0xFF); + + /*generate a tuple from the strings*/ + tuple = Py_BuildValue("(s#s#)", + header_s, header->len, + footer_s, footer->len); + + /*deallocate temporary space before returning tuple*/ + free(header_s); + free(footer_s); + + return tuple; + } else { + br_etry(self->bitstream); + PyErr_SetString(PyExc_IOError, "I/O error reading Shorten file"); + return NULL; + } +} +#endif + +static status +read_shn_header(decoders_SHNDecoder* self, BitstreamReader* reader) +{ + if (!setjmp(*br_try(reader))) { + uint8_t magic_number[4]; + unsigned version; + unsigned i; + + reader->parse(reader, "4b 8u", magic_number, &version); + if (memcmp(magic_number, "ajkg", 4)) { + br_etry(reader); + return INVALID_MAGIC_NUMBER; + } + if (version != 2) { + br_etry(reader); + return INVALID_SHORTEN_VERSION; + } + + self->header.file_type = read_long(reader); + self->header.channels = read_long(reader); + self->block_length = read_long(reader); + self->left_shift = 0; + self->header.max_LPC = read_long(reader); + self->header.mean_count = read_long(reader); + reader->skip_bytes(reader, read_long(reader)); + + if ((1 <= self->header.file_type) && (self->header.file_type <= 2)) { + self->bits_per_sample = 8; + self->signed_samples = (self->header.file_type == 1); + } else if ((3 <= self->header.file_type) && + (self->header.file_type <= 6)) { + self->bits_per_sample = 16; + self->signed_samples = ((self->header.file_type == 3) || + (self->header.file_type == 5)); + } else { + br_etry(reader); + return UNSUPPORTED_FILE_TYPE; + } + + for (i = 0; i < self->header.channels; i++) { + array_i* means = self->means->append(self->means); + means->mset(means, self->header.mean_count, 0); + self->previous_samples->append(self->previous_samples); + } + + /*process first instruction for wave/aiff header, if present*/ + process_iff_header(reader, + &(self->sample_rate), + &(self->channel_mask)); + + br_etry(reader); + + return OK; + } else { + /*read error in Shorten header*/ + br_etry(reader); + return IOERROR; + } +} + +static status +read_framelist(decoders_SHNDecoder* self, array_ia* framelist) +{ + unsigned c = 0; + + self->samples->reset(self->samples); if (!setjmp(*br_try(self->bitstream))) { while (1) { @@ -216,28 +374,39 @@ array_i* means = self->means->_[c]; array_i* previous_samples = self->previous_samples->_[c]; array_i* samples = self->samples->append(self->samples); - array_i* unshifted = self->unshifted->append(self->unshifted); + array_i* unshifted = framelist->append(framelist); switch (command) { case FN_DIFF0: - read_diff0(self->bitstream, self->block_length, means, + read_diff0(self->bitstream, + self->block_length, + means, samples); break; case FN_DIFF1: - read_diff1(self->bitstream, self->block_length, - previous_samples, samples); + read_diff1(self->bitstream, + self->block_length, + previous_samples, + samples); break; case FN_DIFF2: - read_diff2(self->bitstream, self->block_length, - previous_samples, samples); + read_diff2(self->bitstream, + self->block_length, + previous_samples, + samples); break; case FN_DIFF3: - read_diff3(self->bitstream, self->block_length, - previous_samples, samples); + read_diff3(self->bitstream, + self->block_length, + previous_samples, + samples); break; case FN_QLPC: - read_qlpc(self->bitstream, self->block_length, - previous_samples, means, samples); + read_qlpc(self->bitstream, + self->block_length, + previous_samples, + means, + samples); break; case FN_ZERO: samples->mset(samples, self->block_length, 0); @@ -257,9 +426,9 @@ /*apply any left shift to channel*/ if (self->left_shift) { unsigned i; + unshifted->resize_for(unshifted, samples->len); for (i = 0; i < samples->len; i++) - unshifted->append(unshifted, - samples->_[i] << self->left_shift); + a_append(unshifted, samples->_[i] << self->left_shift); } else { samples->copy(samples, unshifted); } @@ -275,14 +444,10 @@ /*move on to next channel*/ c++; - /*once all channels are constructed, - return a complete set of PCM frames*/ + /*return OK once all channels are constructed*/ if (c == self->header.channels) { br_etry(self->bitstream); - PyEval_RestoreThread(thread_state); - return array_ia_to_FrameList(self->audiotools_pcm, - self->unshifted, - self->bits_per_sample); + return OK; } } else if (((FN_QUIT <= command) && (command <= FN_BITSHIFT)) || (command == FN_VERBATIM)) { @@ -294,11 +459,7 @@ case FN_QUIT: self->stream_finished = 1; br_etry(self->bitstream); - PyEval_RestoreThread(thread_state); - return empty_FrameList(self->audiotools_pcm, - self->header.channels, - self->bits_per_sample); - break; + return END_OF_STREAM; case FN_BLOCKSIZE: self->block_length = read_long(self->bitstream); break; @@ -318,17 +479,12 @@ } else { /*unknown command*/ br_etry(self->bitstream); - PyEval_RestoreThread(thread_state); - PyErr_SetString(PyExc_ValueError, - "unknown command in Shorten stream"); - return NULL; + return UNKNOWN_COMMAND; } } } else { br_etry(self->bitstream); - PyEval_RestoreThread(thread_state); - PyErr_SetString(PyExc_IOError, "I/O error reading Shorten file"); - return NULL; + return IOERROR; } } @@ -340,11 +496,11 @@ const unsigned energy = read_unsigned(bs, ENERGY_SIZE); unsigned i; - samples->reset(samples); + samples->reset_for(samples, block_length); for (i = 0; i < block_length; i++) { const int residual = read_signed(bs, energy); - samples->append(samples, residual + offset); + a_append(samples, residual + offset); } } @@ -366,9 +522,10 @@ energy = read_unsigned(bs, ENERGY_SIZE); /*process the residuals to samples*/ + samples->resize_for(samples, block_length); for (i = 1; i < (block_length + 1); i++) { const int residual = read_signed(bs, energy); - samples->append(samples, samples->_[i - 1] + residual); + a_append(samples, samples->_[i - 1] + residual); } /*truncate samples to block length*/ @@ -393,10 +550,11 @@ energy = read_unsigned(bs, ENERGY_SIZE); /*process the residuals to samples*/ + samples->resize_for(samples, block_length); for (i = 2; i < (block_length + 2); i++) { const int residual = read_signed(bs, energy); - samples->append(samples, - (2 * samples->_[i - 1]) - samples->_[i - 2] + residual); + a_append(samples, + (2 * samples->_[i - 1]) - samples->_[i - 2] + residual); } /*truncate samples to block length*/ @@ -421,11 +579,12 @@ energy = read_unsigned(bs, ENERGY_SIZE); /*process the residuals to samples*/ + samples->resize_for(samples, block_length); for (i = 3; i < (block_length + 3); i++) { const int residual = read_signed(bs, energy); - samples->append(samples, - (3 * (samples->_[i - 1] - samples->_[i - 2])) + - samples->_[i - 3] + residual); + a_append(samples, + (3 * (samples->_[i - 1] - samples->_[i - 2])) + + samples->_[i - 3] + residual); } /*truncate samples to block length*/ @@ -477,9 +636,9 @@ } /*reapply offset to unoffset samples*/ - samples->reset(samples); + samples->reset_for(samples, unoffset_samples->len); for (i = 0; i < unoffset_samples->len; i++) { - samples->append(samples, unoffset_samples->_[i] + offset); + a_append(samples, unoffset_samples->_[i] + offset); } /*deallocate temporary arrays before returning successfully*/ @@ -505,109 +664,6 @@ return ((int)(values->len / 2) + values->sum(values)) / (int)(values->len); } -static PyObject* -SHNDecoder_pcm_split(decoders_SHNDecoder* self, PyObject *args) -{ - if (!setjmp(*br_try(self->bitstream))) { - array_i* header = self->pcm_header; - array_i* footer = self->pcm_footer; - array_i* current = header; - uint8_t* header_s; - uint8_t* footer_s; - PyObject* tuple; - - unsigned command; - unsigned i; - - header->reset(header); - footer->reset(footer); - - /*walk through file, processing all commands*/ - do { - unsigned energy; - unsigned LPC_count; - unsigned verbatim_size; - - command = read_unsigned(self->bitstream, COMMAND_SIZE); - - switch (command) { - case FN_DIFF0: - case FN_DIFF1: - case FN_DIFF2: - case FN_DIFF3: - /*all the DIFF commands have the same structure*/ - energy = read_unsigned(self->bitstream, ENERGY_SIZE); - for (i = 0; i < self->block_length; i++) { - skip_signed(self->bitstream, energy); - } - current = footer; - break; - case FN_QUIT: - self->stream_finished = 1; - break; - case FN_BLOCKSIZE: - self->block_length = read_long(self->bitstream); - break; - case FN_BITSHIFT: - skip_unsigned(self->bitstream, SHIFT_SIZE); - break; - case FN_QLPC: - energy = read_unsigned(self->bitstream, ENERGY_SIZE); - LPC_count = read_unsigned(self->bitstream, LPC_COUNT_SIZE); - for (i = 0; i < LPC_count; i++) { - skip_signed(self->bitstream, LPC_COEFF_SIZE); - } - for (i = 0; i < self->block_length; i++) { - skip_signed(self->bitstream, energy); - } - current = footer; - break; - case FN_ZERO: - current = footer; - break; - case FN_VERBATIM: - /*any VERBATIM commands have their data appended - to header or footer*/ - verbatim_size = read_unsigned(self->bitstream, - VERBATIM_CHUNK_SIZE); - for (i = 0; i < verbatim_size; i++) { - current->append(current, - read_unsigned(self->bitstream, - VERBATIM_BYTE_SIZE)); - } - break; - } - } while (command != FN_QUIT); - - br_etry(self->bitstream); - - /*once all commands have been processed, - transform the bytes in header and footer to strings*/ - header_s = malloc(sizeof(uint8_t) * header->len); - for (i = 0; i < header->len; i++) - header_s[i] = (uint8_t)(header->_[i] & 0xFF); - - footer_s = malloc(sizeof(uint8_t) * footer->len); - for (i = 0; i < footer->len; i++) - footer_s[i] = (uint8_t)(footer->_[i] & 0xFF); - - /*generate a tuple from the strings*/ - tuple = Py_BuildValue("(s#s#)", - header_s, header->len, - footer_s, footer->len); - - /*deallocate temporary space before returning tuple*/ - free(header_s); - free(footer_s); - - return tuple; - } else { - br_etry(self->bitstream); - PyErr_SetString(PyExc_IOError, "I/O error reading Shorten file"); - return NULL; - } -} - static unsigned read_unsigned(BitstreamReader* bs, unsigned count) { @@ -648,9 +704,10 @@ bs->skip(bs, count + 1); } -static int -process_header(BitstreamReader* bs, - unsigned* sample_rate, unsigned* channel_mask) +static void +process_iff_header(BitstreamReader* bs, + unsigned* sample_rate, + unsigned* channel_mask) { unsigned command; @@ -672,7 +729,7 @@ bs->rewind(bs); bs->unmark(bs); br_etry(bs); - return 0; + return; } else { verbatim->rewind(verbatim); } @@ -684,7 +741,7 @@ bs->rewind(bs); bs->unmark(bs); br_etry(bs); - return 0; + return; } else { verbatim->rewind(verbatim); } @@ -698,7 +755,7 @@ *sample_rate = 44100; *channel_mask = 0; br_etry(bs); - return 0; + return; } else { /*VERBATIM isn't the first command so rewind and set some dummy values*/ @@ -707,13 +764,15 @@ *sample_rate = 44100; *channel_mask = 0; br_etry(bs); - return 0; + return; } } else { + /*wrap IFF chunk reader in try block + to unmark the stream prior to bubbling up + the read exception to read_shn_header()*/ bs->unmark(bs); br_etry(bs); br_abort(bs); - return 0; /*won't get here*/ } } @@ -951,3 +1010,135 @@ return f; } } + +#ifdef STANDALONE +#include <errno.h> + +int main(int argc, char* argv[]) { + decoders_SHNDecoder decoder; + FILE* file; + array_ia* framelist; + unsigned output_data_size; + unsigned char* output_data; + unsigned bytes_per_sample; + FrameList_int_to_char_converter converter; + + if (argc < 2) { + fprintf(stderr, "*** Usage: %s <file.shn>\n", argv[0]); + return 1; + } + if ((file = fopen(argv[1], "rb")) == NULL) { + fprintf(stderr, "*** %s: %s\n", argv[1], strerror(errno)); + return 1; + } else { + decoder.bitstream = br_open(file, BS_BIG_ENDIAN); + decoder.stream_finished = 0; + + decoder.means = array_ia_new(); + decoder.previous_samples = array_ia_new(); + + decoder.samples = array_ia_new(); + decoder.unshifted = array_ia_new(); + decoder.pcm_header = array_i_new(); + decoder.pcm_footer = array_i_new(); + + framelist = array_ia_new(); + + output_data_size = 1; + output_data = malloc(output_data_size); + } + + /*read Shorten header for basic info*/ + switch (read_shn_header(&decoder, decoder.bitstream)) { + case INVALID_MAGIC_NUMBER: + fprintf(stderr, "invalid magic number"); + goto error; + case INVALID_SHORTEN_VERSION: + fprintf(stderr, "invalid Shorten version"); + goto error; + case UNSUPPORTED_FILE_TYPE: + fprintf(stderr, "unsupported Shorten file type"); + goto error; + case IOERROR: + fprintf(stderr, "I/O error reading Shorten header"); + goto error; + default: + bytes_per_sample = decoder.bits_per_sample / 8; + converter = FrameList_get_int_to_char_converter( + decoder.bits_per_sample, 0, 1); + break; + } + + while (!decoder.stream_finished) { + unsigned pcm_size; + unsigned channel; + unsigned frame; + + framelist->reset(framelist); + /*decode set of commands into a single framelist*/ + switch (read_framelist(&decoder, framelist)) { + case OK: + /*convert framelist to string and output it to stdout*/ + pcm_size = (bytes_per_sample * + framelist->len * + framelist->_[0]->len); + if (pcm_size > output_data_size) { + output_data_size = pcm_size; + output_data = realloc(output_data, output_data_size); + } + for (channel = 0; channel < framelist->len; channel++) { + const array_i* channel_data = framelist->_[channel]; + for (frame = 0; frame < channel_data->len; frame++) { + converter(channel_data->_[frame], + output_data + + ((frame * framelist->len) + channel) * + bytes_per_sample); + } + } + + fwrite(output_data, sizeof(unsigned char), pcm_size, stdout); + break; + case END_OF_STREAM: + /*automatically sets stream_finished to true*/ + break; + case UNKNOWN_COMMAND: + fprintf(stderr, "unknown command in Shorten stream"); + goto error; + case IOERROR: + fprintf(stderr, "I/O error reading Shorten file"); + goto error; + default: + /*shouldn't get here*/ + fprintf(stderr, "unknown value from read_framelist()"); + goto error; + } + } + + decoder.bitstream->close(decoder.bitstream); + + decoder.means->del(decoder.means); + decoder.previous_samples->del(decoder.previous_samples); + decoder.samples->del(decoder.samples); + decoder.unshifted->del(decoder.unshifted); + decoder.pcm_header->del(decoder.pcm_header); + decoder.pcm_footer->del(decoder.pcm_footer); + framelist->del(framelist); + free(output_data); + + return 0; + +error: + decoder.bitstream->close(decoder.bitstream); + + decoder.means->del(decoder.means); + decoder.previous_samples->del(decoder.previous_samples); + decoder.samples->del(decoder.samples); + decoder.unshifted->del(decoder.unshifted); + decoder.pcm_header->del(decoder.pcm_header); + decoder.pcm_footer->del(decoder.pcm_footer); + framelist->del(framelist); + free(output_data); + + return 1; +} +#endif
View file
audiotools-2.18.tar.gz/src/decoders/shn.h -> audiotools-2.19.tar.gz/src/decoders/shn.h
Changed
@@ -1,4 +1,6 @@ +#ifndef STANDALONE #include <Python.h> +#endif #include <stdint.h> #include "../bitstream.h" #include "../array.h" @@ -22,7 +24,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *******************************************************/ -typedef enum {OK, ERROR} status; +typedef enum {OK, + END_OF_STREAM, + IOERROR, + UNKNOWN_COMMAND, + INVALID_MAGIC_NUMBER, + INVALID_SHORTEN_VERSION, + UNSUPPORTED_FILE_TYPE} status; #define COMMAND_SIZE 2 #define ENERGY_SIZE 3 @@ -48,9 +56,10 @@ FN_VERBATIM = 9}; typedef struct { +#ifndef STANDALONE PyObject_HEAD +#endif - char* filename; BitstreamReader* bitstream; /*fixed fields from the Shorten header*/ @@ -82,10 +91,16 @@ array_i* pcm_header; array_i* pcm_footer; +#ifndef STANDALONE /*a framelist generator*/ PyObject* audiotools_pcm; +#endif + + /*a marker to indicate the stream has been explicitly closed*/ + int closed; } decoders_SHNDecoder; +#ifndef STANDALONE PyObject* SHNDecoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds); @@ -186,6 +201,18 @@ 0, /* tp_alloc */ SHNDecoder_new, /* tp_new */ }; +#endif + +static status +read_shn_header(decoders_SHNDecoder* self, BitstreamReader* reader); + +static void +process_iff_header(BitstreamReader* bs, + unsigned* sample_rate, + unsigned* channel_mask); + +static status +read_framelist(decoders_SHNDecoder* self, array_ia* framelist); /*given a list of samples and a set of means for the given channel, reads a DIFF0 command and sets samples to "block_length" values*/ @@ -221,10 +248,6 @@ static int shnmean(const array_i* values); -static int -process_header(BitstreamReader* bs, - unsigned* sample_rate, unsigned* channel_mask); - /*reads the contents of a VERBATIM command into a substream for parsing*/ static BitstreamReader*
View file
audiotools-2.18.tar.gz/src/decoders/sine.c -> audiotools-2.19.tar.gz/src/decoders/sine.c
Changed
@@ -20,6 +20,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *******************************************************/ +#ifndef MIN +#define MIN(x, y) ((x) < (y) ? (x) : (y)) +#endif +#ifndef MAX +#define MAX(x, y) ((x) > (y) ? (x) : (y)) +#endif + int Sine_Mono_init(decoders_Sine_Mono* self, PyObject *args, PyObject *kwds) { double f1; @@ -67,6 +74,8 @@ self->delta2 = 2 * M_PI / (self->sample_rate / f2); self->theta1 = 0.0l; + self->closed = 0; + return 0; } @@ -89,21 +98,22 @@ static PyObject* Sine_Mono_read(decoders_Sine_Mono* self, PyObject* args) { - int byte_count; + int requested_frames; int frames_to_read; - int bytes_per_frame = self->bits_per_sample / 8; int i; double d; int ia; array_i* buffer1; - if (!PyArg_ParseTuple(args, "i", &byte_count)) + if (self->closed) { + PyErr_SetString(PyExc_ValueError, "cannot read closed stream"); return NULL; + } - byte_count -= (byte_count % bytes_per_frame); - frames_to_read = byte_count ? byte_count / bytes_per_frame : 1; - if (frames_to_read > self->remaining_pcm_frames) - frames_to_read = self->remaining_pcm_frames; + if (!PyArg_ParseTuple(args, "i", &requested_frames)) + return NULL; + + frames_to_read = MIN(MAX(requested_frames, 1), self->remaining_pcm_frames); self->buffer->reset(self->buffer); buffer1 = self->buffer->append(self->buffer); @@ -127,7 +137,7 @@ static PyObject* Sine_Mono_close(decoders_Sine_Mono* self, PyObject* args) { - self->remaining_pcm_frames = 0; + self->closed = 1; Py_INCREF(Py_None); return Py_None; @@ -137,6 +147,7 @@ Sine_Mono_reset(decoders_Sine_Mono* self, PyObject* args) { self->remaining_pcm_frames = self->total_pcm_frames; self->theta1 = self->theta2 = 0.0l; + self->closed = 0; Py_INCREF(Py_None); return Py_None; @@ -212,6 +223,8 @@ self->delta2 = 2 * M_PI / (self->sample_rate / f2); self->theta1 = self->theta2 = 0.0l; + self->closed = 0; + return 0; } @@ -233,22 +246,23 @@ static PyObject* Sine_Stereo_read(decoders_Sine_Stereo* self, PyObject* args) { - int byte_count; + int requested_frames; int frames_to_read; - int bytes_per_frame = 2 * (self->bits_per_sample / 8); int i; double d; int ia; array_i* buffer1; array_i* buffer2; - if (!PyArg_ParseTuple(args, "i", &byte_count)) + if (self->closed) { + PyErr_SetString(PyExc_ValueError, "cannot read closed stream"); + return NULL; + } + + if (!PyArg_ParseTuple(args, "i", &requested_frames)) return NULL; - byte_count -= (byte_count % bytes_per_frame); - frames_to_read = byte_count ? byte_count / bytes_per_frame : 1; - if (frames_to_read > self->remaining_pcm_frames) - frames_to_read = self->remaining_pcm_frames; + frames_to_read = MIN(MAX(requested_frames, 1), self->remaining_pcm_frames); self->buffer->reset(self->buffer); buffer1 = self->buffer->append(self->buffer); @@ -277,7 +291,7 @@ static PyObject* Sine_Stereo_close(decoders_Sine_Stereo* self, PyObject* args) { - self->remaining_pcm_frames = 0; + self->closed = 1; Py_INCREF(Py_None); return Py_None; @@ -287,6 +301,7 @@ Sine_Stereo_reset(decoders_Sine_Stereo* self, PyObject* args) { self->remaining_pcm_frames = self->total_pcm_frames; self->theta1 = self->theta2 = 0.0l; + self->closed = 0; Py_INCREF(Py_None); return Py_None; @@ -352,6 +367,8 @@ self->remaining_pcm_frames = self->total_pcm_frames; self->i = 0; + self->closed = 0; + return 0; } @@ -374,24 +391,26 @@ static PyObject* Sine_Simple_read(decoders_Sine_Simple* self, PyObject* args) { - int byte_count; + int requested_frames; int frames_to_read; - int bytes_per_frame = self->bits_per_sample / 8; int i; array_i* buffer; double d; int ia; - if (!PyArg_ParseTuple(args, "i", &byte_count)) + if (self->closed) { + PyErr_SetString(PyExc_ValueError, "cannot read closed stream"); + return NULL; + } + + + if (!PyArg_ParseTuple(args, "i", &requested_frames)) return NULL; self->buffer->reset(self->buffer); buffer = self->buffer->append(self->buffer); - byte_count -= (byte_count % bytes_per_frame); - frames_to_read = byte_count ? byte_count / bytes_per_frame : 1; - if (frames_to_read > self->remaining_pcm_frames) - frames_to_read = self->remaining_pcm_frames; + frames_to_read = MIN(MAX(requested_frames, 1), self->remaining_pcm_frames); for (i = 0; i < frames_to_read; i++) { d = (double)(self->max_value) * @@ -411,7 +430,7 @@ static PyObject* Sine_Simple_close(decoders_Sine_Simple* self, PyObject* args) { - self->remaining_pcm_frames = 0; + self->closed = 1; Py_INCREF(Py_None); return Py_None; @@ -421,6 +440,7 @@ Sine_Simple_reset(decoders_Sine_Simple* self, PyObject* args) { self->i = 0; self->remaining_pcm_frames = self->total_pcm_frames; + self->closed = 0; Py_INCREF(Py_None); return Py_None;
View file
audiotools-2.18.tar.gz/src/decoders/sine.h -> audiotools-2.19.tar.gz/src/decoders/sine.h
Changed
@@ -36,6 +36,8 @@ double theta1; double theta2; + int closed; + array_ia* buffer; PyObject* audiotools_pcm; } decoders_Sine_Mono; @@ -151,6 +153,8 @@ double theta2; double fmult; + int closed; + array_ia* buffer; PyObject* audiotools_pcm; } decoders_Sine_Stereo; @@ -261,6 +265,8 @@ int max_value; int count; + int closed; + array_ia* buffer; PyObject* audiotools_pcm; } decoders_Sine_Simple;
View file
audiotools-2.18.tar.gz/src/decoders/vorbis.c -> audiotools-2.19.tar.gz/src/decoders/vorbis.c
Changed
@@ -40,7 +40,7 @@ /*read identification packet*/ if ((ogg_result = oggreader_next_packet(self->ogg_stream, - self->packet)) == OGG_OK) { + self->packet)) == OGG_OK) { if ((vorbis_result = vorbis_read_identification_packet(self->packet, &(self->identification))) != @@ -56,7 +56,7 @@ /*skip comments packet, but ensure it's positioned properly*/ if ((ogg_result = oggreader_next_packet(self->ogg_stream, - self->packet)) == OGG_OK) { + self->packet)) == OGG_OK) { if (vorbis_read_common_header(self->packet) != 3) { PyErr_SetString(PyExc_ValueError, "comment not second Ogg packet"); @@ -69,7 +69,7 @@ /*read setup header*/ if ((ogg_result = oggreader_next_packet(self->ogg_stream, - self->packet)) == OGG_OK) { + self->packet)) == OGG_OK) { if ((vorbis_result = vorbis_read_setup_packet(self->packet)) != VORBIS_OK) { PyErr_SetString(vorbis_exception(vorbis_result), @@ -227,8 +227,8 @@ int vorbis_read_common_header(BitstreamReader *packet) { - uint8_t vorbis[] = {0x76, 0x6F, 0x72, 0x62, 0x69, 0x73}; - int packet_type = packet->read(packet, 8); + const uint8_t vorbis[] = {0x76, 0x6F, 0x72, 0x62, 0x69, 0x73}; + const int packet_type = packet->read(packet, 8); int i; for (i = 0; i < 6; i++) { @@ -240,63 +240,62 @@ } vorbis_status -vorbis_read_identification_packet( - BitstreamReader *packet, - struct vorbis_identification_header *identification) { +vorbis_read_identification_packet(BitstreamReader *packet, + struct vorbis_identification_header *id) { if (!setjmp(*br_try(packet))) { if (vorbis_read_common_header(packet) != 1) { br_etry(packet); return VORBIS_ID_HEADER_NOT_1ST; } - identification->vorbis_version = packet->read(packet, 32); - if (identification->vorbis_version != 0) { + id->vorbis_version = packet->read(packet, 32); + if (id->vorbis_version != 0) { br_etry(packet); return VORBIS_UNSUPPORTED_VERSION; } - identification->channel_count = packet->read(packet, 8); - if (identification->channel_count < 1) { + id->channel_count = packet->read(packet, 8); + if (id->channel_count < 1) { br_etry(packet); return VORBIS_INVALID_CHANNEL_COUNT; } - identification->sample_rate = packet->read(packet, 32); - if (identification->sample_rate < 1) { + id->sample_rate = packet->read(packet, 32); + if (id->sample_rate < 1) { br_etry(packet); return VORBIS_INVALID_SAMPLE_RATE; } - identification->bitrate_maximum = packet->read(packet, 32); - identification->bitrate_nominal = packet->read(packet, 32); - identification->bitrate_minimum = packet->read(packet, 32); - identification->blocksize_0 = 1 << packet->read(packet, 4); - if ((identification->blocksize_0 != 64) && - (identification->blocksize_0 != 128) && - (identification->blocksize_0 != 256) && - (identification->blocksize_0 != 512) && - (identification->blocksize_0 != 1024) && - (identification->blocksize_0 != 2048) && - (identification->blocksize_0 != 4096) && - (identification->blocksize_0 != 8192)) { + id->bitrate_maximum = packet->read(packet, 32); + id->bitrate_nominal = packet->read(packet, 32); + id->bitrate_minimum = packet->read(packet, 32); + id->blocksize_0 = 1 << packet->read(packet, 4); + if ((id->blocksize_0 != 64) && + (id->blocksize_0 != 128) && + (id->blocksize_0 != 256) && + (id->blocksize_0 != 512) && + (id->blocksize_0 != 1024) && + (id->blocksize_0 != 2048) && + (id->blocksize_0 != 4096) && + (id->blocksize_0 != 8192)) { br_etry(packet); return VORBIS_INVALID_BLOCK_SIZE_0; } - identification->blocksize_1 = 1 << packet->read(packet, 4); - if ((identification->blocksize_1 != 64) && - (identification->blocksize_1 != 128) && - (identification->blocksize_1 != 256) && - (identification->blocksize_1 != 512) && - (identification->blocksize_1 != 1024) && - (identification->blocksize_1 != 2048) && - (identification->blocksize_1 != 4096) && - (identification->blocksize_1 != 8192)) { + id->blocksize_1 = 1 << packet->read(packet, 4); + if ((id->blocksize_1 != 64) && + (id->blocksize_1 != 128) && + (id->blocksize_1 != 256) && + (id->blocksize_1 != 512) && + (id->blocksize_1 != 1024) && + (id->blocksize_1 != 2048) && + (id->blocksize_1 != 4096) && + (id->blocksize_1 != 8192)) { br_etry(packet); return VORBIS_INVALID_BLOCK_SIZE_1; } - if (identification->blocksize_0 > identification->blocksize_1) { + if (id->blocksize_0 > id->blocksize_1) { br_etry(packet); return VORBIS_INVALID_BLOCK_SIZE_1; }
View file
audiotools-2.18.tar.gz/src/decoders/vorbis.h -> audiotools-2.19.tar.gz/src/decoders/vorbis.h
Changed
@@ -180,9 +180,8 @@ /*reads packet data (including the common header) into "identification" performs EOF checking in case the packet is too small*/ vorbis_status -vorbis_read_identification_packet( - BitstreamReader *packet, - struct vorbis_identification_header *identification); +vorbis_read_identification_packet(BitstreamReader *packet, + struct vorbis_identification_header *id); /*reads setup information (including the common header) performs EOF checking in case the packet is too small*/
View file
audiotools-2.18.tar.gz/src/decoders/wavpack.c -> audiotools-2.19.tar.gz/src/decoders/wavpack.c
Changed
@@ -1,5 +1,6 @@ #include "wavpack.h" #include "../pcmconv.h" +#include <string.h> /******************************************************** Audio Tools, a module and set of tools for manipulating audio data @@ -20,11 +21,17 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *******************************************************/ +#ifndef STANDALONE int WavPackDecoder_init(decoders_WavPackDecoder *self, PyObject *args, PyObject *kwds) { - struct block_header header; char* filename; +#else +int +WavPackDecoder_init(decoders_WavPackDecoder *self, + char* filename) { +#endif + struct block_header header; status error; self->filename = NULL; @@ -42,11 +49,13 @@ self->entropies = array_ia_new(); self->residuals = array_ia_new(); self->decorrelated = array_ia_new(); + self->correlated = array_ia_new(); self->left_right = array_ia_new(); self->un_shifted = array_ia_new(); self->block_data = br_substream_new(BS_LITTLE_ENDIAN); self->sub_block_data = br_substream_new(BS_LITTLE_ENDIAN); +#ifndef STANDALONE if ((self->audiotools_pcm = open_audiotools_pcm()) == NULL) return -1; @@ -61,6 +70,13 @@ } else { self->bitstream = br_open(self->file, BS_LITTLE_ENDIAN); } +#else + if ((self->file = fopen(filename, "rb")) == NULL) { + return -1; + } else { + self->bitstream = br_open(self->file, BS_LITTLE_ENDIAN); + } +#endif self->filename = strdup(filename); @@ -74,7 +90,9 @@ sample_rate, bits_per_sample, channels, and channel_mask*/ self->bitstream->mark(self->bitstream); /*beginning of stream*/ if ((error = read_block_header(self->bitstream, &header)) != OK) { +#ifndef STANDALONE PyErr_SetString(wavpack_exception(error), wavpack_strerror(error)); +#endif self->bitstream->unmark(self->bitstream); return -1; } @@ -92,12 +110,16 @@ self->bitstream->unmark(self->bitstream); /*after block header*/ break; case SUB_BLOCK_NOT_FOUND: +#ifndef STANDALONE PyErr_SetString(PyExc_ValueError, "sample rate undefined"); +#endif self->bitstream->unmark(self->bitstream); /*after block header*/ self->bitstream->unmark(self->bitstream); /*beginning of stream*/ return -1; default: +#ifndef STANDALONE PyErr_SetString(wavpack_exception(error), wavpack_strerror(error)); +#endif self->bitstream->unmark(self->bitstream); /*after block header*/ self->bitstream->unmark(self->bitstream); /*beginning of stream*/ return -1; @@ -128,12 +150,16 @@ self->bitstream->unmark(self->bitstream); /*after block header*/ break; case SUB_BLOCK_NOT_FOUND: +#ifndef STANDALONE PyErr_SetString(PyExc_ValueError, "channel count/mask undefined"); +#endif self->bitstream->unmark(self->bitstream); /*after block header*/ self->bitstream->unmark(self->bitstream); /*beginning of stream*/ return -1; default: +#ifndef STANDALONE PyErr_SetString(wavpack_exception(error), wavpack_strerror(error)); +#endif self->bitstream->unmark(self->bitstream); /*after block header*/ self->bitstream->unmark(self->bitstream); /*beginning of stream*/ return -1; @@ -145,6 +171,9 @@ self->bitstream->rewind(self->bitstream); self->bitstream->unmark(self->bitstream); /*beginning of stream*/ + /*mark stream as not closed and ready for reading*/ + self->closed = 0; + return 0; } @@ -158,12 +187,15 @@ self->entropies->del(self->entropies); self->residuals->del(self->residuals); self->decorrelated->del(self->decorrelated); + self->correlated->del(self->correlated); self->left_right->del(self->left_right); self->un_shifted->del(self->un_shifted); self->block_data->close(self->block_data); self->sub_block_data->close(self->sub_block_data); +#ifndef STANDALONE Py_XDECREF(self->audiotools_pcm); +#endif if (self->filename != NULL) free(self->filename); @@ -171,9 +203,12 @@ if (self->bitstream != NULL) self->bitstream->close(self->bitstream); +#ifndef STANDALONE self->ob_type->tp_free((PyObject*)self); +#endif } +#ifndef STANDALONE static PyObject* WavPackDecoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { @@ -206,6 +241,9 @@ static PyObject* WavPackDecoder_close(decoders_WavPackDecoder* self, PyObject *args) { + /*mark stream as closed so more calls to read() generate ValueErrors*/ + self->closed = 1; + Py_INCREF(Py_None); return Py_None; } @@ -220,8 +258,12 @@ PyThreadState *thread_state; PyObject* framelist; - channels_data->reset(channels_data); + if (self->closed) { + PyErr_SetString(PyExc_ValueError, "cannot read closed stream"); + return NULL; + } + channels_data->reset(channels_data); if (self->remaining_pcm_samples > 0) { do { @@ -314,6 +356,60 @@ } } +PyObject* +wavpack_exception(status error) +{ + switch (error) { + case IO_ERROR: + return PyExc_IOError; + case OK: + case INVALID_BLOCK_ID: + case INVALID_RESERVED_BIT: + case EXCESSIVE_DECORRELATION_PASSES: + case INVALID_DECORRELATION_TERM: + case DECORRELATION_TERMS_MISSING: + case DECORRELATION_WEIGHTS_MISSING: + case DECORRELATION_SAMPLES_MISSING: + case ENTROPY_VARIABLES_MISSING: + case RESIDUALS_MISSING: + case EXCESSIVE_DECORRELATION_WEIGHTS: + case INVALID_ENTROPY_VARIABLE_COUNT: + case BLOCK_DATA_CRC_MISMATCH: + case EXTENDED_INTEGERS_MISSING: + default: + return PyExc_ValueError; + } +} + +int +WavPackDecoder_update_md5sum(decoders_WavPackDecoder *self, + PyObject *framelist) +{ + PyObject *string_obj; + char *string_buffer; + Py_ssize_t length; + int sign = self->bits_per_sample >= 16; + + if ((string_obj = + PyObject_CallMethod(framelist, "to_bytes","ii", 0, sign)) != NULL) { + if (PyString_AsStringAndSize(string_obj, + &string_buffer, + &length) == 0) { + audiotools__MD5Update(&(self->md5), + (unsigned char *)string_buffer, + length); + Py_DECREF(string_obj); + return 0; + } else { + Py_DECREF(string_obj); + return 1; + } + } else { + return 1; + } +} +#endif + const char* wavpack_strerror(status error) { @@ -353,31 +449,6 @@ } } -PyObject* -wavpack_exception(status error) -{ - switch (error) { - case IO_ERROR: - return PyExc_IOError; - case OK: - case INVALID_BLOCK_ID: - case INVALID_RESERVED_BIT: - case EXCESSIVE_DECORRELATION_PASSES: - case INVALID_DECORRELATION_TERM: - case DECORRELATION_TERMS_MISSING: - case DECORRELATION_WEIGHTS_MISSING: - case DECORRELATION_SAMPLES_MISSING: - case ENTROPY_VARIABLES_MISSING: - case RESIDUALS_MISSING: - case EXCESSIVE_DECORRELATION_WEIGHTS: - case INVALID_ENTROPY_VARIABLE_COUNT: - case BLOCK_DATA_CRC_MISMATCH: - case EXTENDED_INTEGERS_MISSING: - default: - return PyExc_ValueError; - } -} - static status read_block_header(BitstreamReader* bs, struct block_header* header) { @@ -562,7 +633,8 @@ decorrelation_weights, decorrelation_samples, residuals, - decorrelated); + decorrelated, + decoder->correlated); } else { residuals->swap(residuals, decorrelated); } @@ -605,7 +677,8 @@ decorrelation_weights, decorrelation_samples, residuals, - decorrelated); + decorrelated, + decoder->correlated); } else { residuals->swap(residuals, decorrelated); } @@ -678,6 +751,19 @@ return OK; } +static inline int +pop_decorrelation_weight(BitstreamReader *sub_block) +{ + const int value = sub_block->read_signed(sub_block, 8); + if (value > 0) { + return (value << 3) + (((value << 3) + (1 << 6)) >> 7); + } else if (value == 0) { + return 0; + } else { + return (value << 3); + } +} + static status read_decorrelation_weights(const struct block_header* block_header, const struct sub_block* sub_block, @@ -686,7 +772,6 @@ { unsigned i; unsigned weight_count; - array_i* weight_values = array_i_new(); if (sub_block->actual_size_1_less == 0) { weight_count = sub_block->size * 2; @@ -694,31 +779,19 @@ weight_count = sub_block->size * 2 - 1; } - for (i = 0; i < weight_count; i++) { - int value = sub_block->data->read_signed(sub_block->data, 8); - if (value > 0) { - weight_values->append(weight_values, - (value << 3) + - (((value << 3) + (1 << 6)) >> 7)); - } else if (value == 0) { - weight_values->append(weight_values, 0); - } else { - weight_values->append(weight_values, value << 3); - } - } - weights->reset(weights); if ((block_header->mono_output == 0) && (block_header->false_stereo == 0)) { /*two channels*/ if ((weight_count / 2) > term_count) { - weight_values->del(weight_values); return EXCESSIVE_DECORRELATION_WEIGHTS; } for (i = 0; i < weight_count / 2; i++) { array_i* weights_pass = weights->append(weights); - weights_pass->append(weights_pass, weight_values->_[i * 2]); - weights_pass->append(weights_pass, weight_values->_[i * 2 + 1]); + weights_pass->append(weights_pass, + pop_decorrelation_weight(sub_block->data)); + weights_pass->append(weights_pass, + pop_decorrelation_weight(sub_block->data)); } for (; i < term_count; i++) { array_i* weights_pass = weights->append(weights); @@ -726,18 +799,17 @@ } weights->reverse(weights); - weight_values->del(weight_values); return OK; } else { /*one channel*/ if (weight_count > term_count) { - weight_values->del(weight_values); return EXCESSIVE_DECORRELATION_WEIGHTS; } for (i = 0; i < weight_count; i++) { array_i* weights_pass = weights->append(weights); - weights_pass->append(weights_pass, weight_values->_[i]); + weights_pass->append(weights_pass, + pop_decorrelation_weight(sub_block->data)); } for (; i < term_count; i++) { array_i* weights_pass = weights->append(weights); @@ -745,7 +817,6 @@ } weights->reverse(weights); - weight_values->del(weight_values); return OK; } } @@ -1129,9 +1200,9 @@ if (add == 0) { u = base; } else { - unsigned p = LOG2(add); - int e = (1 << (p + 1)) - add - 1; - unsigned r = bs->read(bs, p); + const unsigned p = LOG2(add); + const int e = (1 << (p + 1)) - add - 1; + const unsigned r = bs->read(bs, p); if (r >= e) { u = base + (r * 2) - e + bs->read(bs, 1); } else { @@ -1152,11 +1223,13 @@ const array_ia* decorrelation_weights, const array_iaa* decorrelation_samples, const array_ia* residuals, - array_ia* decorrelated) + array_ia* decorrelated, + array_ia* correlated) { status status; unsigned pass; - array_ia* correlated = array_ia_new(); + + correlated->reset(correlated); if (residuals->len == 1) { residuals->copy(residuals, decorrelated); @@ -1172,7 +1245,6 @@ decorrelation_samples->_[pass]->_[0], correlated->_[0], decorrelated->_[0])) != OK) { - correlated->del(correlated); return status; } } @@ -1192,7 +1264,6 @@ decorrelation_samples->_[pass]->_[1], correlated, decorrelated)) != OK) { - correlated->del(correlated); return status; } } @@ -1201,7 +1272,6 @@ abort(); } - correlated->del(correlated); return OK; } @@ -1236,17 +1306,17 @@ decorrelated->reset(decorrelated); - switch (decorrelation_term) { case 18: decorrelation_samples->copy(decorrelation_samples, decorrelated); decorrelated->reverse(decorrelated); + decorrelated->resize_for(decorrelated, correlated->len); for (i = 0; i < correlated->len; i++) { const int64_t temp = (3 * decorrelated->_[i + 1] - decorrelated->_[i]) >> 1; - decorrelated->append(decorrelated, - apply_weight(decorrelation_weight, temp) + - correlated->_[i]); + a_append(decorrelated, + apply_weight(decorrelation_weight, temp) + + correlated->_[i]); decorrelation_weight += update_weight(temp, correlated->_[i], decorrelation_delta); @@ -1256,12 +1326,13 @@ case 17: decorrelation_samples->copy(decorrelation_samples, decorrelated); decorrelated->reverse(decorrelated); + decorrelated->resize_for(decorrelated, correlated->len); for (i = 0; i < correlated->len; i++) { const int64_t temp = 2 * decorrelated->_[i + 1] - decorrelated->_[i]; - decorrelated->append(decorrelated, - apply_weight(decorrelation_weight, temp) + - correlated->_[i]); + a_append(decorrelated, + apply_weight(decorrelation_weight, temp) + + correlated->_[i]); decorrelation_weight += update_weight(temp, correlated->_[i], decorrelation_delta); @@ -1277,17 +1348,16 @@ case 2: case 1: decorrelation_samples->copy(decorrelation_samples, decorrelated); + decorrelated->resize_for(decorrelated, correlated->len); for (i = 0; i < correlated->len; i++) { - decorrelated->append(decorrelated, - apply_weight(decorrelation_weight, - decorrelated->_[i]) + - correlated->_[i]); + a_append(decorrelated, + apply_weight(decorrelation_weight, + decorrelated->_[i]) + correlated->_[i]); decorrelation_weight += update_weight(decorrelated->_[i], correlated->_[i], decorrelation_delta); } - decorrelated->de_head(decorrelated, decorrelation_term, - decorrelated); + decorrelated->de_head(decorrelated, decorrelation_term, decorrelated); return OK; default: return INVALID_DECORRELATION_TERM; @@ -1341,16 +1411,18 @@ decorr_1 = decorrelated->append(decorrelated); decorr_0->extend(decorr_0, samples_1); decorr_1->extend(decorr_1, samples_0); + decorr_0->resize_for(decorr_0, corr_0->len); + decorr_1->resize_for(decorr_1, corr_1->len); switch (decorrelation_term) { case -1: for (i = 0; i < corr_0->len; i++) { - decorr_0->append(decorr_0, - apply_weight(weight_0, decorr_1->_[i]) + - corr_0->_[i]); - decorr_1->append(decorr_1, - apply_weight(weight_1, decorr_0->_[i + 1]) + - corr_1->_[i]); + a_append(decorr_0, + apply_weight(weight_0, decorr_1->_[i]) + + corr_0->_[i]); + a_append(decorr_1, + apply_weight(weight_1, decorr_0->_[i + 1]) + + corr_1->_[i]); weight_0 += update_weight(decorr_1->_[i], corr_0->_[i], decorrelation_delta); @@ -1363,12 +1435,12 @@ break; case -2: for (i = 0; i < corr_0->len; i++) { - decorr_1->append(decorr_1, - apply_weight(weight_1, decorr_0->_[i]) + - corr_1->_[i]); - decorr_0->append(decorr_0, - apply_weight(weight_0, decorr_1->_[i + 1]) + - corr_0->_[i]); + a_append(decorr_1, + apply_weight(weight_1, decorr_0->_[i]) + + corr_1->_[i]); + a_append(decorr_0, + apply_weight(weight_0, decorr_1->_[i + 1]) + + corr_0->_[i]); weight_1 += update_weight(decorr_0->_[i], corr_1->_[i], decorrelation_delta); @@ -1381,12 +1453,12 @@ break; case -3: for (i = 0; i < corr_0->len; i++) { - decorr_0->append(decorr_0, - apply_weight(weight_0, decorr_1->_[i]) + - corr_0->_[i]); - decorr_1->append(decorr_1, - apply_weight(weight_1, decorr_0->_[i]) + - corr_1->_[i]); + a_append(decorr_0, + apply_weight(weight_0, decorr_1->_[i]) + + corr_0->_[i]); + a_append(decorr_1, + apply_weight(weight_1, decorr_0->_[i]) + + corr_1->_[i]); weight_0 += update_weight(decorr_1->_[i], corr_0->_[i], decorrelation_delta); @@ -1693,7 +1765,7 @@ for (i = 0; i < extended->len; i++) un_extended->append(un_extended, (extended->_[i] << params->one_bits) | - ((1 < params->one_bits) - 1)); + ((1 << params->one_bits) - 1)); } else if (params->duplicate_bits > 0) { for (i = 0; i < extended->len; i++) { int shifted = extended->_[i]; @@ -1712,30 +1784,146 @@ } } -int -WavPackDecoder_update_md5sum(decoders_WavPackDecoder *self, - PyObject *framelist) -{ - PyObject *string_obj; - char *string_buffer; - Py_ssize_t length; - int sign = self->bits_per_sample >= 16; +#ifdef STANDALONE +int main(int argc, char* argv[]) { + decoders_WavPackDecoder decoder; + unsigned bytes_per_sample; + unsigned char *output_data; + unsigned output_data_size; + FrameList_int_to_char_converter converter; - if ((string_obj = - PyObject_CallMethod(framelist, "to_bytes","ii", 0, sign)) != NULL) { - if (PyString_AsStringAndSize(string_obj, - &string_buffer, - &length) == 0) { - audiotools__MD5Update(&(self->md5), - (unsigned char *)string_buffer, - length); - Py_DECREF(string_obj); - return 0; - } else { - Py_DECREF(string_obj); - return 1; - } - } else { + struct block_header block_header; + struct sub_block md5_sub_block; + unsigned char sub_block_md5sum[16]; + unsigned char stream_md5sum[16]; + + if (argc < 2) { + fprintf(stderr, "*** Usage: %s <file.wv>\n", argv[0]); + return 1; + } + + /*initialize reader object*/ + if (WavPackDecoder_init(&decoder, argv[1])) { + fprintf(stderr, "*** Error initializing WavPack decoder\n"); return 1; + } else { + bytes_per_sample = decoder.bits_per_sample / 8; + output_data = malloc(1); + output_data_size = 1; + converter = FrameList_get_int_to_char_converter( + decoder.bits_per_sample, 0, 1); + } + + while (decoder.remaining_pcm_samples) { + BitstreamReader* bs = decoder.bitstream; + array_ia* channels_data = decoder.channels_data; + status error; + BitstreamReader* block_data = decoder.block_data; + unsigned pcm_size; + unsigned channel; + unsigned frame; + + channels_data->reset(channels_data); + + do { + /*read block header*/ + if ((error = read_block_header(bs, &block_header)) != OK) { + fprintf(stderr, "*** Error: %s\n", + wavpack_strerror(error)); + goto error; + } + + /*FIXME - ensure block header is consistent + with the starting block header*/ + + br_substream_reset(block_data); + + /*read block data*/ + if (!setjmp(*br_try(bs))) { + bs->substream_append(bs, block_data, + block_header.block_size - 24); + br_etry(bs); + } else { + br_etry(bs); + fprintf(stderr, "I/O error reading block data"); + goto error; + } + + /*decode block to 1 or 2 channels of PCM data*/ + if ((error = decode_block(&decoder, + &block_header, + block_data, + block_header.block_size - 24, + channels_data)) != OK) { + fprintf(stderr, "*** Error: %s\n", + wavpack_strerror(error)); + goto error; + } + } while (block_header.final_block == 0); + + /*deduct frame count from total remaining*/ + decoder.remaining_pcm_samples -= MIN(channels_data->_[0]->len, + decoder.remaining_pcm_samples); + + /*convert all channels to single PCM string*/ + pcm_size = (bytes_per_sample * + channels_data->len * + channels_data->_[0]->len); + if (pcm_size > output_data_size) { + output_data_size = pcm_size; + output_data = realloc(output_data, output_data_size); + } + for (channel = 0; channel < channels_data->len; channel++) { + const array_i* channel_data = channels_data->_[channel]; + for (frame = 0; frame < channel_data->len; frame++) { + converter(channel_data->_[frame], + output_data + + ((frame * channels_data->len) + channel) * + bytes_per_sample); + } + } + + /*update stream's MD5 sum with framelist data*/ + audiotools__MD5Update(&(decoder.md5), output_data, pcm_size); + + /*output PCM string to stdout*/ + fwrite(output_data, sizeof(unsigned char), pcm_size, stdout); } + + /*check for final MD5 sub block, if present*/ + md5_sub_block.data = decoder.sub_block_data; + + /*check for final MD5 block, which may not be present*/ + if ((read_block_header(decoder.bitstream, &block_header) == OK) && + (find_sub_block(&block_header, + decoder.bitstream, + 6, 1, &md5_sub_block) == OK) && + (sub_block_data_size(&md5_sub_block) == 16)) { + + /*have valid MD5 block, so check it*/ + md5_sub_block.data->read_bytes(md5_sub_block.data, + (uint8_t*)sub_block_md5sum, + 16); + + audiotools__MD5Final(stream_md5sum, &(decoder.md5)); + + if (memcmp(sub_block_md5sum, stream_md5sum, 16)) { + fprintf(stderr, "*** MD5 mismatch at end of stream\n"); + goto error; + } + } + + /*deallocate reader object*/ + WavPackDecoder_dealloc(&decoder); + free(output_data); + + return 0; + +error: + /*deallocate reader object*/ + WavPackDecoder_dealloc(&decoder); + free(output_data); + + return 1; } +#endif
View file
audiotools-2.18.tar.gz/src/decoders/wavpack.h -> audiotools-2.19.tar.gz/src/decoders/wavpack.h
Changed
@@ -1,4 +1,6 @@ +#ifndef STANDALONE #include <Python.h> +#endif #include <stdint.h> #include "../bitstream.h" #include "../array.h" @@ -50,9 +52,11 @@ WV_MD5 = 0x26} wv_metadata_function; typedef struct { +#ifndef STANDALONE PyObject_HEAD PyObject* audiotools_pcm; +#endif FILE* file; char* filename; @@ -68,6 +72,7 @@ int channels; int channel_mask; unsigned remaining_pcm_samples; + int closed; /*reusable buffers*/ array_ia* channels_data; @@ -78,10 +83,12 @@ array_ia* entropies; array_ia* residuals; array_ia* decorrelated; + array_ia* correlated; array_ia* left_right; array_ia* un_shifted; } decoders_WavPackDecoder; +#ifndef STANDALONE /*the WavPackDecoder.__init__() method*/ int WavPackDecoder_init(decoders_WavPackDecoder *self, @@ -138,9 +145,6 @@ WavPackDecoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds); -const char* -wavpack_strerror(status error); - PyObject* wavpack_exception(status error); @@ -186,6 +190,21 @@ WavPackDecoder_new, /* tp_new */ }; +int +WavPackDecoder_update_md5sum(decoders_WavPackDecoder *self, + PyObject *framelist); +#else +int +WavPackDecoder_init(decoders_WavPackDecoder *self, + char* filename); + +void +WavPackDecoder_dealloc(decoders_WavPackDecoder *self); +#endif + +const char* +wavpack_strerror(status error); + struct block_header { /*block ID 32 bits*/ unsigned block_size; /*32 bits*/ @@ -310,7 +329,9 @@ const array_ia* decorrelation_weights, const array_iaa* decorrelation_samples, const array_ia* residuals, - array_ia* decorrelated); + array_ia* decorrelated, + array_ia* correlated /*a temporary buffer*/ + ); static status decorrelate_1ch_pass(int decorrelation_term, @@ -331,7 +352,7 @@ array_ia* decorrelated); static void -undo_joint_stereo(const array_ia* mid_size, array_ia* left_right); +undo_joint_stereo(const array_ia* mid_side, array_ia* left_right); static uint32_t calculate_crc(const array_ia* channels); @@ -377,7 +398,3 @@ undo_extended_integers(const struct extended_integers* params, const array_ia* extended_integers, array_ia* un_extended_integers); - -int -WavPackDecoder_update_md5sum(decoders_WavPackDecoder *self, - PyObject *framelist);
View file
audiotools-2.19.tar.gz/src/dither.c
Added
@@ -0,0 +1,92 @@ +/******************************************************** + Audio Tools, a module and set of tools for manipulating audio data + Copyright (C) 2007-2012 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 +*******************************************************/ + +/*This is a set of reusable routines for opening a BitstreamReader + wrapped around the os.urandom function call + for generating individual bits of dither for an audio stream.*/ + +static int +read_os_random(PyObject* os_module, + struct bs_buffer* buffer) +{ + PyObject* string; + + /*call os.urandom() and retrieve a Python object*/ + if ((string = PyObject_CallMethod(os_module, + "urandom", "(i)", 4096)) != NULL) { + char *buffer_s; + Py_ssize_t buffer_len; + + /*convert Python object to string and length*/ + if (PyString_AsStringAndSize(string, &buffer_s, &buffer_len) != -1) { + /*extend buffer for additional data*/ + uint8_t* new_data = buf_extend(buffer, (uint32_t)buffer_len); + + /*copy string to buffer and extend buffer length*/ + memcpy(new_data, buffer_s, (size_t)buffer_len); + + buffer->buffer_size += (uint32_t)buffer_len; + + /*DECREF Python object and return OK*/ + Py_DECREF(string); + return 0; + } else { + /*os.urandom() didn't return a string + so print error and clear it*/ + Py_DECREF(string); + PyErr_Print(); + return 1; + } + } else { + /*error occured in os.urandom() call + so print error and clear it*/ + PyErr_Print(); + return 1; + } +} + +static void +close_os_random(PyObject* os_module) +{ + return; /* does nothing*/ +} + +static void +free_os_random(PyObject* os_module) +{ + Py_XDECREF(os_module); +} + +/*returns a BitstreamReader for reading 1 bit white noise dither values + or NULL if an error occurs opening the os module*/ +static BitstreamReader* +open_dither(void) +{ + PyObject* os_module; + + if ((os_module = PyImport_ImportModule("os")) != NULL) { + return br_open_external(os_module, + BS_BIG_ENDIAN, + (EXT_READ)read_os_random, + (EXT_CLOSE)close_os_random, + (EXT_FREE)free_os_random); + } else { + return NULL; + } +}
View file
audiotools-2.18.tar.gz/src/encoders/alac.c -> audiotools-2.19.tar.gz/src/encoders/alac.c
Changed
@@ -44,7 +44,6 @@ PyObject *file_obj; FILE *output_file; BitstreamWriter *output = NULL; - PyObject *pcmreader_obj; pcmreader* pcmreader; struct alac_context encoder; array_ia* channels = array_ia_new(); @@ -59,10 +58,11 @@ /*extract a file object, PCMReader-compatible object and encoding options*/ if (!PyArg_ParseTupleAndKeywords( - args, keywds, "OOiiii|ii", + args, keywds, "OO&iiii|ii", kwlist, &file_obj, - &pcmreader_obj, + pcmreader_converter, + &pcmreader, &(encoder.options.block_size), &(encoder.options.initial_history), &(encoder.options.history_multiplier), @@ -71,11 +71,6 @@ &(encoder.options.maximum_interlacing_leftweight))) return NULL; - /*transform the Python PCMReader-compatible object to a pcm_reader struct*/ - if ((pcmreader = open_pcmreader(pcmreader_obj)) == NULL) { - return NULL; - } - encoder.bits_per_sample = pcmreader->bits_per_sample; /*determine if the PCMReader is compatible with ALAC*/ @@ -101,7 +96,7 @@ int ALACEncoder_encode_alac(char *filename, - FILE *input, + pcmreader* pcmreader, int block_size, int initial_history, int history_multiplier, @@ -109,7 +104,6 @@ { FILE *output_file; BitstreamWriter *output = NULL; - pcmreader *pcmreader; struct alac_context encoder; array_ia* channels = array_ia_new(); unsigned frame_file_offset; @@ -123,10 +117,8 @@ encoder.options.minimum_interlacing_leftweight = 0; encoder.options.maximum_interlacing_leftweight = 4; + /*FIXME - make sure this opens correctly*/ output_file = fopen(filename, "wb"); - /*assume CD quality for now*/ - pcmreader = open_pcmreader(input, 44100, 2, 0x3, 16, 0, 1); - encoder.bits_per_sample = pcmreader->bits_per_sample; /*convert file object to bitstream writer*/ @@ -205,14 +197,14 @@ #else pcmreader->del(pcmreader); - output->free(output); + output->close(output); free_encoder(&encoder); channels->del(channels); return 0; error: pcmreader->del(pcmreader); - output->free(output); + output->close(output); free_encoder(&encoder); channels->del(channels); @@ -291,7 +283,7 @@ { pair->reset(pair); frameset->_[channel]->swap(frameset->_[channel], - pair->append(pair)); + pair->append(pair)); return pair; } @@ -791,8 +783,7 @@ } } else { /*all samples are 0, so use a special case*/ - qlp_coefficients->reset(qlp_coefficients); - qlp_coefficients->mappend(qlp_coefficients, 4, 0); + qlp_coefficients->mset(qlp_coefficients, 4, 0); calculate_residuals(samples, sample_size, qlp_coefficients, residual_values4); @@ -815,8 +806,7 @@ unsigned window2; if (tukey_window->len != samples->len) { - tukey_window->resize(tukey_window, samples->len); - tukey_window->reset(tukey_window); + tukey_window->reset_for(tukey_window, samples->len); window1 = (unsigned)(alpha * (N - 1)) / 2; window2 = (unsigned)((N - 1) * (1.0 - (alpha / 2.0))); @@ -839,8 +829,7 @@ } } - windowed_signal->resize(windowed_signal, samples->len); - windowed_signal->reset(windowed_signal); + windowed_signal->reset_for(windowed_signal, samples->len); for (n = 0; n < N; n++) { a_append(windowed_signal, samples->_[n] * tukey_window->_[n]); } @@ -979,8 +968,7 @@ const unsigned coeff_count = qlp_coefficients->len; qlp_coefficients->copy(qlp_coefficients, coefficients); - residuals->reset(residuals); - residuals->resize(residuals, samples->len); + residuals->reset_for(residuals, samples->len); /*first sample always copied verbatim*/ a_append(residuals, samples->_[i++]); @@ -1153,22 +1141,7 @@ } } -#ifdef STANDALONE -int main(int argc, char *argv[]) { - if (ALACEncoder_encode_alac(argv[1], - stdin, - 4096, - 10, - 40, - 14)) { - fprintf(stderr, "Error during encoding\n"); - return 1; - } else { - return 0; - } -} -#else - +#ifndef STANDALONE PyObject *alac_log_output(struct alac_context *encoder) { @@ -1222,5 +1195,166 @@ Py_XDECREF(file_offset_list); return NULL; } +#else +#include <getopt.h> +#include <errno.h> + +static unsigned +count_bits(unsigned value) +{ + unsigned bits = 0; + while (value) { + bits += value & 0x1; + value >>= 1; + } + return bits; +} +int main(int argc, char *argv[]) { + char* output_file = NULL; + unsigned channels = 2; + unsigned channel_mask = 0x3; + unsigned sample_rate = 44100; + unsigned bits_per_sample = 16; + + int block_size = 4096; + int initial_history = 10; + int history_multiplier = 40; + int maximum_k = 14; + + char c; + const static struct option long_opts[] = { + {"help", no_argument, NULL, 'h'}, + {"channels", required_argument, NULL, 'c'}, + {"sample-rate", required_argument, NULL, 'r'}, + {"bits-per-sample", required_argument, NULL, 'b'}, + {"block-size", required_argument, NULL, 'B'}, + {"initial-history", required_argument, NULL, 'I'}, + {"history-multiplier", required_argument, NULL, 'M'}, + {"maximum-K", required_argument, NULL, 'K'}, + {NULL, no_argument, NULL, 0}}; + const static char* short_opts = "-hc:r:b:B:M:K:"; + + while ((c = getopt_long(argc, + argv, + short_opts, + long_opts, + NULL)) != -1) { + switch (c) { + case 1: + if (output_file == NULL) { + output_file = optarg; + } else { + printf("only one output file allowed\n"); + return 1; + } + break; + case 'c': + if (((channels = strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --channel \"%s\"\n", optarg); + return 1; + } + break; + case 'm': + if (((channel_mask = strtoul(optarg, NULL, 16)) == 0) && errno) { + printf("invalid --channel-mask \"%s\"\n", optarg); + return 1; + } + break; + case 'r': + if (((sample_rate = strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --sample-rate \"%s\"\n", optarg); + return 1; + } + break; + case 'b': + if (((bits_per_sample = strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --bits-per-sample \"%s\"\n", optarg); + return 1; + } + break; + case 'B': + if (((block_size = strtol(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --block-size \"%s\"\n", optarg); + return 1; + } + break; + case 'I': + if (((initial_history = strtol(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --initial-history \"%s\"\n", optarg); + return 1; + } + break; + case 'M': + if (((history_multiplier = + strtol(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --history-multiplier \"%s\"\n", optarg); + return 1; + } + break; + case 'K': + if (((maximum_k = strtol(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --maximum-K \"%s\"\n", optarg); + return 1; + } + break; + case 'h': /*fallthrough*/ + case ':': + case '?': + printf("*** Usage: alacenc [options] <output.m4a>\n"); + printf("-c, --channels=# number of input channels\n"); + printf("-r, --sample_rate=# input sample rate in Hz\n"); + printf("-b, --bits-per-sample=# bits per input sample\n"); + printf("\n"); + printf("-B, --block-size=# block size\n"); + printf("-I, --initial-history=# initial history\n"); + printf("-M, --history-multiplier=# history multiplier\n"); + printf("-K, --maximum-K=# maximum K\n"); + return 0; + default: + break; + } + } + if (output_file == NULL) { + printf("exactly 1 output file required\n"); + return 1; + } + + assert(channels > 0); + assert((bits_per_sample == 8) || + (bits_per_sample == 16) || + (bits_per_sample == 24)); + assert(sample_rate > 0); + assert(count_bits(channel_mask) == channels); + + printf("Encoding from stdin using parameters:\n"); + printf("channels %u\n", channels); + printf("channel mask 0x%X\n", channel_mask); + printf("sample rate %u\n", sample_rate); + printf("bits per sample %u\n", bits_per_sample); + printf("little-endian, signed samples\n"); + printf("\n"); + printf("block size %d\n", block_size); + printf("initial history %d\n", initial_history); + printf("history multiplier %d\n", history_multiplier); + printf("maximum K %d\n", maximum_k); + + if (ALACEncoder_encode_alac(output_file, + open_pcmreader(stdin, + sample_rate, + channels, + channel_mask, + bits_per_sample, + 0, + 1), + block_size, + initial_history, + history_multiplier, + maximum_k)) { + fprintf(stderr, "Error during encoding\n"); + return 1; + } else { + return 0; + } +} #endif
View file
audiotools-2.18.tar.gz/src/encoders/flac.c -> audiotools-2.19.tar.gz/src/encoders/flac.c
Changed
@@ -47,7 +47,6 @@ FILE *output_file; BitstreamWriter* output_stream; struct flac_context encoder; - PyObject *pcmreader_obj; pcmreader* pcmreader; char version_string[0xFF]; static char *kwlist[] = {"filename", @@ -69,6 +68,7 @@ array_ia* samples; + uint64_t current_offset = 0; PyObject *frame_offsets = NULL; PyObject *offset = NULL; @@ -86,10 +86,11 @@ /*extract a filename, PCMReader-compatible object and encoding options: blocksize int*/ if (!PyArg_ParseTupleAndKeywords( - args, keywds, "sOIIII|iiiiiii", + args, keywds, "sO&IIII|iiiiiii", kwlist, &filename, - &pcmreader_obj, + pcmreader_converter, + &pcmreader, &(encoder.options.block_size), &(encoder.options.max_lpc_order), &(encoder.options.min_residual_partition_order), @@ -112,12 +113,6 @@ return NULL; } - /*transform the Python PCMReader-compatible object to a pcm_reader struct*/ - if ((pcmreader = open_pcmreader(pcmreader_obj)) == NULL) { - fclose(output_file); - return NULL; - } - /*build a list for frame offset info*/ frame_offsets = PyList_New(0); @@ -125,7 +120,7 @@ int encoders_encode_flac(char *filename, - FILE *input, + pcmreader* pcmreader, unsigned block_size, unsigned max_lpc_order, unsigned min_residual_partition_order, @@ -135,8 +130,8 @@ int exhaustive_model_search) { FILE* output_file; BitstreamWriter* output_stream; + uint64_t current_offset = 0; struct flac_context encoder; - pcmreader* pcmreader; char version_string[0xFF]; audiotools__MD5Context md5sum; array_ia* samples; @@ -157,9 +152,8 @@ encoder.options.no_fixed_subframes = 0; encoder.options.no_lpc_subframes = 0; + /*FIXME - check for invalid output file*/ output_file = fopen(filename, "wb"); - /*FIXME - assume CD quality for now*/ - pcmreader = open_pcmreader(input, 44100, 2, 0x3, 16, 0, 1); #endif @@ -248,8 +242,8 @@ while (samples->_[0]->len > 0) { #ifndef STANDALONE - offset = Py_BuildValue("(i, i)", - bw_ftell(output_stream), + offset = Py_BuildValue("(K, i)", + current_offset, samples->_[0]->len); PyList_Append(frame_offsets, offset); Py_DECREF(offset); @@ -265,6 +259,7 @@ encoder.streaminfo.maximum_frame_size = MAX(encoder.streaminfo.maximum_frame_size, encoder.frame->bits_written(encoder.frame) / 8); + current_offset += encoder.frame->bytes_written(encoder.frame); bw_rec_copy(output_stream, encoder.frame); #ifndef STANDALONE Py_END_ALLOW_THREADS @@ -422,7 +417,7 @@ unsigned bits_per_sample_bits; int crc8 = 0; - bw_add_callback(bs, flac_crc8, &crc8); + bw_add_callback(bs, (bs_callback_func)flac_crc8, &crc8); /*determine the block size bits from the given amount of samples*/ switch (block_size) { @@ -528,7 +523,7 @@ unsigned channel; int crc16 = 0; - bw_add_callback(bs, flac_crc16, &crc16); + bw_add_callback(bs, (bs_callback_func)flac_crc16, &crc16); if ((encoder->streaminfo.channels == 2) && ((encoder->options.mid_side || encoder->options.adaptive_mid_side))) { @@ -698,8 +693,7 @@ if (wasted_bps > 0) { unsigned i; - subframe_samples->resize(subframe_samples, samples->len); - subframe_samples->reset(subframe_samples); + subframe_samples->reset_for(subframe_samples, samples->len); for (i = 0; i < samples->len; i++) a_append(subframe_samples, samples->_[i] >> wasted_bps); } else { @@ -925,8 +919,7 @@ int* order_data = order->_; assert(order_size > 1); - next_order->resize(next_order, order_size - 1); - next_order->reset(next_order); + next_order->reset_for(next_order, order_size - 1); for (i = 1; i < order_size; i++) { a_append(next_order, order_data[i] - order_data[i - 1]); } @@ -1001,8 +994,7 @@ bs->write_signed(bs, qlp_precision, qlp_coefficients->_[i]); /*calculate signed residuals*/ - lpc_residual->reset(lpc_residual); - lpc_residual->resize(lpc_residual, samples->len - order); + lpc_residual->reset_for(lpc_residual, samples->len - order); for (i = 0; i < samples->len - order; i++) { accumulator = 0; for (j = 0; j < order; j++) @@ -1126,8 +1118,7 @@ } } else { /*use a set of dummy coefficients*/ - qlp_coefficients->reset(qlp_coefficients); - qlp_coefficients->vappend(qlp_coefficients, 1, 1); + qlp_coefficients->vset(qlp_coefficients, 1, 1); *qlp_precision = 2; *qlp_shift_needed = 0; } @@ -1146,8 +1137,7 @@ unsigned window2; if (tukey_window->len != samples->len) { - tukey_window->resize(tukey_window, samples->len); - tukey_window->reset(tukey_window); + tukey_window->reset_for(tukey_window, samples->len); window1 = (unsigned)(alpha * (N - 1)) / 2; window2 = (unsigned)((N - 1) * (1.0 - (alpha / 2.0))); @@ -1170,8 +1160,7 @@ } } - windowed_signal->resize(windowed_signal, samples->len); - windowed_signal->reset(windowed_signal); + windowed_signal->reset_for(windowed_signal, samples->len); for (n = 0; n < N; n++) { a_append(windowed_signal, samples->_[n] * tukey_window->_[n]); } @@ -1530,10 +1519,8 @@ assert(samples->_[0]->len == samples->_[1]->len); - average->reset(average); - average->resize(average, sample_count); - difference->reset(difference); - difference->resize(difference, sample_count); + average->reset_for(average, sample_count); + difference->reset_for(difference, sample_count); channel0 = samples->_[0]->_; channel1 = samples->_[1]->_; @@ -1650,12 +1637,174 @@ } #ifdef STANDALONE +#include <getopt.h> +#include <errno.h> int main(int argc, char *argv[]) { - encoders_encode_flac(argv[1], - stdin, - 4096, 12, 0, 6, 1, 1, 1); + char* output_file = NULL; + unsigned channels = 2; + unsigned sample_rate = 44100; + unsigned bits_per_sample = 16; + + unsigned block_size = 4096; + unsigned max_lpc_order = 12; + unsigned min_partition_order = 0; + unsigned max_partition_order = 6; + int mid_side = 0; + int adaptive_mid_side = 0; + int exhaustive_model_search = 0; + + char c; + const static struct option long_opts[] = { + {"help", no_argument, NULL, 'h'}, + {"channels", required_argument, NULL, 'c'}, + {"sample-rate", required_argument, NULL, 'r'}, + {"bits-per-sample", required_argument, NULL, 'b'}, + {"block-size", required_argument, NULL, 'B'}, + {"max-lpc-order", required_argument, NULL, 'l'}, + {"min-partition-order", required_argument, NULL, 'P'}, + {"max-partition-order", required_argument, NULL, 'R'}, + {"mid-side", no_argument, NULL, 'm'}, + {"adaptive-mid-side", no_argument, NULL, 'M'}, + {"exhaustive-model-search", no_argument, NULL, 'e'}, + {NULL, no_argument, NULL, 0} + }; + const static char* short_opts = "-hc:r:b:B:l:P:R:mMe"; + + while ((c = getopt_long(argc, + argv, + short_opts, + long_opts, + NULL)) != -1) { + switch (c) { + case 1: + if (output_file == NULL) { + output_file = optarg; + } else { + printf("only one output file allowed\n"); + return 1; + } + break; + case 'c': + if (((channels = strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --channel \"%s\"\n", optarg); + return 1; + } + break; + case 'r': + if (((sample_rate = strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --sample-rate \"%s\"\n", optarg); + return 1; + } + break; + case 'b': + if (((bits_per_sample = strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --bits-per-sample \"%s\"\n", optarg); + return 1; + } + break; + case 'B': + if (((block_size = strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --block-size \"%s\"\n", optarg); + return 1; + } + break; + case 'l': + if (((max_lpc_order = strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --max-lpc-order \"%s\"\n", optarg); + return 1; + } + break; + case 'P': + if (((min_partition_order = + strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --min-partition-order \"%s\"\n", optarg); + return 1; + } + break; + case 'R': + if (((max_partition_order = + strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --max-partition-order \"%s\"\n", optarg); + return 1; + } + break; + case 'm': + mid_side = 1; + break; + case 'M': + adaptive_mid_side = 1; + break; + case 'e': + exhaustive_model_search = 1; + break; + case 'h': /*fallthrough*/ + case ':': + case '?': + printf("*** Usage: flacenc [options] <output.flac>\n"); + printf("-c, --channels=# number of input channels\n"); + printf("-r, --sample_rate=# input sample rate in Hz\n"); + printf("-b, --bits-per-sample=# bits per input sample\n"); + printf("\n"); + printf("-B, --block-size=# block size\n"); + printf("-l, --max-lpc-order=# maximum LPC order\n"); + printf("-P, --min-partition-order=# minimum partition order\n"); + printf("-R, --max-partition-order=# maximum partition order\n"); + printf("-m, --mid-side use mid-side encoding\n"); + printf("-M, --adaptive-mid-side " + "use adaptive mid-side encoding\n"); + printf("-m, --mid-side use mid-side encoding\n"); + printf("-e, --exhaustive-model-search " + "search for best subframe exhaustively\n"); + return 0; + default: + break; + } + } + if (output_file == NULL) { + printf("exactly 1 output file required\n"); + return 1; + } - return 0; + assert((channels > 0) && (channels <= 8)); + assert((bits_per_sample == 8) || + (bits_per_sample == 16) || + (bits_per_sample == 24)); + assert(sample_rate > 0); + + printf("Encoding from stdin using parameters:\n"); + printf("channels %u\n", channels); + printf("sample rate %u\n", sample_rate); + printf("bits per sample %u\n", bits_per_sample); + printf("little-endian, signed samples\n"); + printf("\n"); + printf("block size %u\n", block_size); + printf("max LPC order %u\n", max_lpc_order); + printf("min partition order %u\n", min_partition_order); + printf("max partition order %u\n", max_partition_order); + printf("mid side %d\n", mid_side); + printf("adaptive mid side %d\n", adaptive_mid_side); + printf("exhaustive model search %d\n", exhaustive_model_search); + + if (encoders_encode_flac(output_file, + open_pcmreader(stdin, + sample_rate, + channels, + 0, + bits_per_sample, + 0, + 1), + block_size, + max_lpc_order, + min_partition_order, + max_partition_order, + mid_side, + adaptive_mid_side, + exhaustive_model_search)) { + return 0; + } else { + fprintf(stderr, "*** Error encoding FLAC file \"%s\"\n", output_file); + return 1; + } } #endif
View file
audiotools-2.18.tar.gz/src/encoders/shn.c -> audiotools-2.19.tar.gz/src/encoders/shn.c
Changed
@@ -1,3 +1,6 @@ +#ifndef STANDALONE +#include <Python.h> +#endif #include "shn.h" /******************************************************** @@ -19,6 +22,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *******************************************************/ +#ifndef STANDALONE PyObject* encoders_encode_shn(PyObject *dummy, PyObject *args, PyObject *keywds) @@ -34,7 +38,6 @@ char *filename; FILE *output_file; BitstreamWriter* writer; - PyObject *pcmreader_obj; pcmreader* pcmreader; int is_big_endian = 0; int signed_samples = 0; @@ -55,10 +58,11 @@ unsigned i; /*fetch arguments*/ - if (!PyArg_ParseTupleAndKeywords(args, keywds, "sOiis#|s#I", + if (!PyArg_ParseTupleAndKeywords(args, keywds, "sO&iis#|s#I", kwlist, &filename, - &pcmreader_obj, + pcmreader_converter, + &pcmreader, &is_big_endian, &signed_samples, &header_data, @@ -69,11 +73,6 @@ &block_size)) return NULL; - /*convert PCMReader object*/ - if ((pcmreader = open_pcmreader(pcmreader_obj)) == NULL) { - return NULL; - } - /*ensure PCMReader is compatible with Shorten*/ if ((pcmreader->bits_per_sample != 8) && (pcmreader->bits_per_sample != 16)) { @@ -110,7 +109,7 @@ for (i = 0; i < header_size; i++) write_unsigned(writer, VERBATIM_BYTE_SIZE, (uint8_t)header_data[i]); - /*process PCM frames */ + /*process PCM frames*/ if (encode_audio(writer, pcmreader, signed_samples, block_size)) goto error; @@ -125,7 +124,7 @@ /*issue QUIT command*/ write_unsigned(writer, COMMAND_SIZE, FN_QUIT); - /*pad output (non including header) to a multiple of 4 bytes*/ + /*pad output (not including header) to a multiple of 4 bytes*/ writer->byte_align(writer); while ((bytes_written % 4) != 0) { writer->write(writer, 8, 0); @@ -141,8 +140,9 @@ pcmreader->del(pcmreader); writer->close(writer); - return 0; + return NULL; } +#endif static void write_header(BitstreamWriter* bs, @@ -245,8 +245,7 @@ /*apply left shift to channel data*/ if (left_shift > 0) { - shifted->reset(shifted); - shifted->resize(shifted, channel->len); + shifted->reset_for(shifted, channel->len); for (i = 0; i < channel->len; i++) a_append(shifted, channel->_[i] >> left_shift); } else { @@ -382,20 +381,23 @@ samples->_[0] - prev_samples->_[prev_samples->len - 1]); break; } + delta1->resize_for(delta1, samples->len - 1); for (i = 1; i < samples->len; i++) - delta1->append(delta1, samples->_[i] - samples->_[i - 1]); + a_append(delta1, samples->_[i] - samples->_[i - 1]); assert(delta1->len == (samples->len + 2)); /*determine delta2 from delta1*/ delta2 = deltas->append(deltas); + delta2->resize_for(delta2, delta1->len - 1); for (i = 1; i < delta1->len; i++) - delta2->append(delta2, delta1->_[i] - delta1->_[i - 1]); + a_append(delta2, delta1->_[i] - delta1->_[i - 1]); assert(delta2->len == (samples->len + 1)); /*determine delta3 from delta2*/ delta3 = deltas->append(deltas); + delta3->resize_for(delta3, delta2->len - 1); for (i = 1; i < delta2->len; i++) - delta3->append(delta3, delta2->_[i] - delta2->_[i - 1]); + a_append(delta3, delta2->_[i] - delta2->_[i - 1]); assert(delta3->len == samples->len); /*determine delta sums from non-negative deltas*/ @@ -485,3 +487,229 @@ write_unsigned(bs, LSBs, value); } } + +#ifdef STANDALONE +#include <getopt.h> +#include <errno.h> +#include <string.h> + +int main(int argc, char* argv[]) { + char* output_filename = NULL; + unsigned channels = 2; + unsigned sample_rate = 44100; + unsigned bits_per_sample = 16; + + unsigned block_size = 256; + BitstreamWriter* header = bw_open_recorder(BS_BIG_ENDIAN); + BitstreamWriter* footer = bw_open_recorder(BS_BIG_ENDIAN); + + pcmreader* pcmreader; + FILE *output_file; + BitstreamWriter* writer; + unsigned bytes_written = 0; + + char c; + const static struct option long_opts[] = { + {"help", no_argument, NULL, 'h'}, + {"channels", required_argument, NULL, 'c'}, + {"sample-rate", required_argument, NULL, 'r'}, + {"bits-per-sample", required_argument, NULL, 'b'}, + {"block-size", required_argument, NULL, 'B'}, + {"header", required_argument, NULL, 'H'}, + {"footer", required_argument, NULL, 'F'}, + {NULL, no_argument, NULL, 0} + }; + const static char* short_opts = "-hc:r:b:B:H:F:"; + + while ((c = getopt_long(argc, + argv, + short_opts, + long_opts, + NULL)) != -1) { + FILE* f; + + switch (c) { + case 1: + if (output_filename == NULL) { + output_filename = optarg; + } else { + printf("only one output file allowed\n"); + return 1; + } + break; + case 'c': + if (((channels = strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --channel \"%s\"\n", optarg); + return 1; + } + break; + case 'r': + if (((sample_rate = strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --sample-rate \"%s\"\n", optarg); + return 1; + } + break; + case 'b': + if (((bits_per_sample = strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --bits-per-sample \"%s\"\n", optarg); + return 1; + } + break; + case 'B': + if (((block_size = strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --block-size \"%s\"\n", optarg); + return 1; + } + break; + case 'H': + bw_reset_recorder(header); + if ((f = fopen(optarg, "rb")) != NULL) { + uint8_t bytes[4096]; + size_t byte_count; + byte_count = fread(bytes, sizeof(uint8_t), 4096, f); + while (byte_count > 0) { + header->write_bytes(header, + bytes, + (unsigned int)byte_count); + byte_count = fread(bytes, sizeof(uint8_t), 4096, f); + } + fclose(f); + } else { + fprintf(stderr, "*** Error: %s: %s\n", + optarg, strerror(errno)); + goto error; + } + break; + case 'F': + bw_reset_recorder(footer); + if ((f = fopen(optarg, "rb")) != NULL) { + uint8_t bytes[4096]; + size_t byte_count; + byte_count = fread(bytes, sizeof(uint8_t), 4096, f); + while (byte_count > 0) { + footer->write_bytes(footer, + bytes, + (unsigned int)byte_count); + byte_count = fread(bytes, sizeof(uint8_t), 4096, f); + } + fclose(f); + } else { + fprintf(stderr, "*** Error: %s: %s\n", + optarg, strerror(errno)); + goto error; + } + break; + case 'h': /*fallthrough*/ + case ':': + case '?': + printf("*** Usage: shnenc [options] <output.shn>\n"); + printf("-c, --channels=# number of input channels\n"); + printf("-r, --sample_rate=# input sample rate in Hz\n"); + printf("-b, --bits-per-sample=# bits per input sample\n"); + printf("\n"); + printf("-B, --block-size=# block size\n"); + printf("-H, --header=<filename> header data\n"); + printf("-F, --footer=<filename> footer data\n"); + goto exit; + default: + break; + } + } + if (output_filename == NULL) { + printf("exactly 1 output file required\n"); + return 1; + } + + assert(channels > 0); + assert((bits_per_sample == 8) || + (bits_per_sample == 16)); + assert(sample_rate > 0); + + printf("Encoding from stdin using parameters:\n"); + printf("channels %u\n", channels); + printf("sample rate %u\n", sample_rate); + printf("bits per sample %u\n", bits_per_sample); + printf("little-endian, signed samples\n"); + printf("\n"); + printf("block size %u\n", block_size); + printf("header size %u bytes\n", header->bytes_written(header)); + printf("footer size %u bytes\n", footer->bytes_written(footer)); + + /*open pcmreader on stdin*/ + pcmreader = open_pcmreader(stdin, + sample_rate, + channels, + 0, + bits_per_sample, + 0, + 1); + + /*open given filename for writing*/ + if ((output_file = fopen(output_filename, "wb")) == NULL) { + fprintf(stderr, "*** %s: %s\n", output_filename, strerror(errno)); + pcmreader->close(pcmreader); + goto error; + } else { + writer = bw_open(output_file, BS_BIG_ENDIAN); + } + + /*write magic number and version*/ + writer->build(writer, "4b 8u", "ajkg", 2); + + bw_add_callback(writer, byte_counter, &bytes_written); + + /*write Shorten header*/ + write_header(writer, + pcmreader->bits_per_sample, + 0, + 1, + pcmreader->channels, + block_size); + + /*issue initial VERBATIM command with header data*/ + write_unsigned(writer, COMMAND_SIZE, FN_VERBATIM); + write_unsigned(writer, VERBATIM_SIZE, header->bytes_written(header)); + while (header->bytes_written(header)) + write_unsigned(writer, + VERBATIM_BYTE_SIZE, + (uint8_t)buf_getc(header->output.buffer)); + + /*process PCM frames*/ + if (encode_audio(writer, pcmreader, 1, block_size)) + goto error; + + /*if there's footer data, issue a VERBATIM command for it*/ + if (footer->bytes_written(footer)) { + write_unsigned(writer, COMMAND_SIZE, FN_VERBATIM); + write_unsigned(writer, VERBATIM_SIZE, footer->bytes_written(footer)); + while (footer->bytes_written(footer)) + write_unsigned(writer, + VERBATIM_BYTE_SIZE, + (uint8_t)buf_getc(footer->output.buffer)); + } + + /*issue QUIT command*/ + write_unsigned(writer, COMMAND_SIZE, FN_QUIT); + + /*pad output (not including header) to a multiple of 4 bytes*/ + writer->byte_align(writer); + while ((bytes_written % 4) != 0) { + writer->write(writer, 8, 0); + } + + /*deallocate temporary buffers and close files*/ + pcmreader->close(pcmreader); + pcmreader->del(pcmreader); + writer->close(writer); + +exit: + header->close(header); + footer->close(footer); + return 0; + +error: + header->close(header); + footer->close(footer); + return 1; +} +#endif
View file
audiotools-2.18.tar.gz/src/encoders/shn.h -> audiotools-2.19.tar.gz/src/encoders/shn.h
Changed
@@ -1,10 +1,6 @@ #ifndef A_SHN_ENCODE #define A_SHN_ENCODE -#ifndef STANDALONE -#include <Python.h> -#endif - #include <stdint.h> #include "../bitstream.h" #include "../array.h"
View file
audiotools-2.18.tar.gz/src/encoders/wavpack.c -> audiotools-2.19.tar.gz/src/encoders/wavpack.c
Changed
@@ -28,7 +28,6 @@ char *filename; FILE *file; BitstreamWriter *stream; - PyObject *pcmreader_obj; pcmreader *pcmreader; struct wavpack_encoder_context context; array_ia* pcm_frames; @@ -50,7 +49,7 @@ "false_stereo", "wasted_bits", "joint_stereo", - "decorrelation_passes", + "correlation_passes", "wave_header", "wave_footer", NULL}; @@ -62,10 +61,11 @@ if (!PyArg_ParseTupleAndKeywords(args, keywds, - "sOI|iiiIs#s#", + "sO&I|iiiIs#s#", kwlist, &filename, - &pcmreader_obj, + pcmreader_converter, + &pcmreader, &block_size, &try_false_stereo, @@ -86,15 +86,10 @@ stream = bw_open(file, BS_LITTLE_ENDIAN); } - /*transform the Python PCMReader-compatible object to a pcm_reader struct*/ - if ((pcmreader = open_pcmreader(pcmreader_obj)) == NULL) { - fclose(file); - return NULL; - } #else void encoders_encode_wavpack(char *filename, - FILE *pcmdata, + pcmreader* pcmreader, unsigned block_size, int try_false_stereo, int try_wasted_bits, @@ -102,7 +97,6 @@ unsigned correlation_passes) { FILE *file; BitstreamWriter *stream; - pcmreader* pcmreader; struct wavpack_encoder_context context; array_ia* pcm_frames; array_ia* block_frames; @@ -113,12 +107,9 @@ context.wave.header_data = NULL; context.wave.footer_data = NULL; + /*FIXME - check for error here*/ file = fopen(filename, "wb"); stream = bw_open(file, BS_LITTLE_ENDIAN); - if ((pcmreader = - open_pcmreader(pcmdata, 44100, 2, 0x3, 16, 0, 1)) == NULL) { - return; - } #endif @@ -144,6 +135,9 @@ while (pcm_frames->_[0]->len > 0) { unsigned pcm_frame_count = pcm_frames->_[0]->len; +#ifndef STANDALONE + Py_BEGIN_ALLOW_THREADS +#endif /*split PCM frames into 1-2 channel blocks*/ for (block = 0; block < context.blocks_per_set; block++) { /*add a fresh block offset based on current file position*/ @@ -163,6 +157,9 @@ block == 0, block == (context.blocks_per_set - 1)); } +#ifndef STANDALONE + Py_END_ALLOW_THREADS +#endif block_index += pcm_frame_count; if (pcmreader->read(pcmreader, block_size, pcm_frames)) @@ -198,6 +195,9 @@ } } +#ifndef STANDALONE + Py_BEGIN_ALLOW_THREADS +#endif /*go back and set block header data as necessary*/ for (i = 0; i < context.offsets->len; i++) { fpos_t* pos = (fpos_t*)(context.offsets->_[i]); @@ -205,6 +205,9 @@ fseek(file, 12, SEEK_CUR); stream->write(stream, 32, block_index); } +#ifndef STANDALONE + Py_END_ALLOW_THREADS +#endif /*close open file handles and deallocate temporary space*/ free_context(&context); @@ -282,6 +285,7 @@ context->cache.shifted = array_ia_new(); context->cache.mid_side = array_ia_new(); context->cache.correlated = array_ia_new(); + context->cache.correlation_temp = array_ia_new(); context->cache.sub_block = bw_open_recorder(BS_LITTLE_ENDIAN); context->cache.sub_blocks = bw_open_recorder(BS_LITTLE_ENDIAN); @@ -314,6 +318,7 @@ context->cache.shifted->del(context->cache.shifted); context->cache.mid_side->del(context->cache.mid_side); context->cache.correlated->del(context->cache.correlated); + context->cache.correlation_temp->del(context->cache.correlation_temp); context->cache.sub_block->close(context->cache.sub_block); context->cache.sub_blocks->close(context->cache.sub_blocks); @@ -612,6 +617,7 @@ array_ia* shifted = context->cache.shifted; array_ia* mid_side = context->cache.mid_side; array_ia* correlated = context->cache.correlated; + array_ia* correlation_temp = context->cache.correlation_temp; BitstreamWriter* sub_blocks = context->cache.sub_blocks; BitstreamWriter* sub_block = context->cache.sub_block; uint32_t crc; @@ -628,6 +634,8 @@ if ((channels->len == 1) || (parameters->try_false_stereo && (channels->_[0]->equals(channels->_[0], channels->_[1])))) { + array_i* channel_0 = channels->_[0]; + if (channels->len == 1) { false_stereo = 0; effective_channel_count = 1; @@ -636,49 +644,52 @@ effective_channel_count = 1; } - /*calculate the maximum magnitude of channel_0 and channel_1*/ - magnitude = maximum_magnitude(channels->_[0]); + /*calculate the maximum magnitude of channel_0 and channel*/ + magnitude = maximum_magnitude(channel_0); /*calculate and apply any wasted least-significant bits*/ if (parameters->try_wasted_bits) { - wasted_bps = wasted_bits(channels->_[0]); + wasted_bps = wasted_bits(channel_0); if (wasted_bps > 0) { unsigned i; - shifted->append(shifted); - shifted->_[0]->resize(shifted->_[0], total_frames); + array_i* shifted_0 = shifted->append(shifted); + shifted_0->resize(shifted_0, total_frames); for (i = 0; i < total_frames; i++) { - a_append(shifted->_[0], channels->_[0]->_[i] >> wasted_bps); + a_append(shifted_0, channel_0->_[i] >> wasted_bps); } } else { - channels->_[0]->copy(channels->_[0], shifted->append(shifted)); + channel_0->copy(channel_0, shifted->append(shifted)); } } else { wasted_bps = 0; - channels->_[0]->copy(channels->_[0], shifted->append(shifted)); + channel_0->copy(channel_0, shifted->append(shifted)); } crc = calculate_crc(shifted); } else { + array_i* channel_0 = channels->_[0]; + array_i* channel_1 = channels->_[1]; + false_stereo = 0; effective_channel_count = 2; /*calculate the maximum magnitude of channel_0 and channel_1*/ - magnitude = MAX(maximum_magnitude(channels->_[0]), - maximum_magnitude(channels->_[1])); + magnitude = MAX(maximum_magnitude(channel_0), + maximum_magnitude(channel_1)); /*calculate and apply any wasted least-significant bits*/ if (parameters->try_wasted_bits) { - wasted_bps = MIN(wasted_bits(channels->_[0]), - wasted_bits(channels->_[1])); + wasted_bps = MIN(wasted_bits(channel_0), + wasted_bits(channel_1)); if (wasted_bps > 0) { unsigned i; - shifted->append(shifted); - shifted->append(shifted); - shifted->_[0]->resize(shifted->_[0], total_frames); - shifted->_[1]->resize(shifted->_[1], total_frames); - for (i = 0; i < channels->_[0]->len; i++) { - a_append(shifted->_[0], channels->_[0]->_[i] >> wasted_bps); - a_append(shifted->_[1], channels->_[1]->_[i] >> wasted_bps); + array_i* shifted_0 = shifted->append(shifted); + array_i* shifted_1 = shifted->append(shifted); + shifted_0->resize(shifted_0, total_frames); + shifted_1->resize(shifted_1, total_frames); + for (i = 0; i < channel_0->len; i++) { + a_append(shifted_0, channel_0->_[i] >> wasted_bps); + a_append(shifted_1, channel_1->_[i] >> wasted_bps); } } else { channels->copy(channels, shifted); @@ -704,7 +715,6 @@ reset_block_parameters(parameters, effective_channel_count); } - /*if first block in file, write wave header*/ if (!context->wave.header_written) { bw_reset_recorder(sub_block); @@ -777,6 +787,7 @@ } if (effective_channel_count == 1) { /*1 channel block*/ + /*perform channel correlation*/ if (parameters->terms->len > 0) { correlate_channels(correlated, shifted, @@ -784,7 +795,8 @@ parameters->deltas, parameters->weights, parameters->samples, - 1); + 1, + correlation_temp); } else { shifted->copy(shifted, correlated); } @@ -797,7 +809,8 @@ parameters->deltas, parameters->weights, parameters->samples, - 2); + 2, + correlation_temp); } else { mid_side->copy(mid_side, correlated); } @@ -992,7 +1005,8 @@ array_i* deltas, array_ia* weights, array_iaa* samples, - unsigned channel_count) + unsigned channel_count, + array_ia* temp) { unsigned pass; unsigned total; @@ -1003,37 +1017,40 @@ assert(uncorrelated_samples->len == channel_count); if (channel_count == 1) { - array_i* input_channel = array_i_new(); - array_i* output_channel = array_i_new(); - input_channel->swap(input_channel, uncorrelated_samples->_[0]); + array_i* input_channel = uncorrelated_samples->_[0]; + array_i* output_channel; + array_i* temp_channel; + correlated_samples->reset(correlated_samples); + output_channel = correlated_samples->append(correlated_samples); + temp->reset(temp); + temp_channel = temp->append(temp); + for (pass = terms->len - 1,total = terms->len; - total > 0; pass--,total--) { + total > 0; + pass--,total--) { correlate_1ch(output_channel, input_channel, terms->_[pass], deltas->_[pass], &(weights->_[pass]->_[0]), - samples->_[pass]->_[0]); + samples->_[pass]->_[0], + temp_channel); if (total > 1) { input_channel->swap(input_channel, output_channel); } } - - correlated_samples->reset(correlated_samples); - output_channel->swap(output_channel, - correlated_samples->append(correlated_samples)); - input_channel->del(input_channel); - output_channel->del(output_channel); } else if (channel_count == 2) { for (pass = terms->len - 1,total = terms->len; - total > 0; pass--,total--) { + total > 0; + pass--,total--) { correlate_2ch(correlated_samples, uncorrelated_samples, terms->_[pass], deltas->_[pass], weights->_[pass], - samples->_[pass]); + samples->_[pass], + temp); if (total > 1) { uncorrelated_samples->swap(uncorrelated_samples, correlated_samples); @@ -1051,7 +1068,7 @@ return (int)(((weight * sample) + 512) >> 10); } -static int +static inline int update_weight(int64_t source, int result, int delta) { if ((source == 0) || (result == 0)) { @@ -1069,67 +1086,61 @@ int term, int delta, int* weight, - array_i* samples) + array_i* samples, + array_i* temp) { unsigned i; - correlated->reset(correlated); + array_i* uncorr = temp; - if (term == 18) { - array_i* uncorr = array_i_new(); + uncorr->reset(uncorr); + if (term == 18) { assert(samples->len == 2); uncorr->vappend(uncorr, 2, samples->_[1], samples->_[0]); uncorr->extend(uncorr, uncorrelated); + correlated->reset_for(correlated, uncorr->len - 2); for (i = 2; i < uncorr->len; i++) { const int64_t temp = (3 * uncorr->_[i - 1] - uncorr->_[i - 2]) >> 1; - correlated->append(correlated, - uncorr->_[i] - apply_weight(*weight, temp)); + a_append(correlated, + uncorr->_[i] - apply_weight(*weight, temp)); *weight += update_weight(temp, correlated->_[i - 2], delta); } /*round-trip the final 2 uncorrelated samples for the next block*/ samples->_[1] = uncorr->_[uncorr->len - 2]; samples->_[0] = uncorr->_[uncorr->len - 1]; - - uncorr->del(uncorr); } else if (term == 17) { - array_i* uncorr = array_i_new(); - assert(samples->len == 2); uncorr->vappend(uncorr, 2, samples->_[1], samples->_[0]); uncorr->extend(uncorr, uncorrelated); + correlated->reset_for(correlated, uncorr->len - 2); for (i = 2; i < uncorr->len; i++) { const int64_t temp = 2 * uncorr->_[i - 1] - uncorr->_[i - 2]; - correlated->append(correlated, - uncorr->_[i] - apply_weight(*weight, temp)); + a_append(correlated, + uncorr->_[i] - apply_weight(*weight, temp)); *weight += update_weight(temp, correlated->_[i - 2], delta); } /*round-trip the final 2 uncorrelated samples for the next block*/ samples->_[1] = uncorr->_[uncorr->len - 2]; samples->_[0] = uncorr->_[uncorr->len - 1]; - - uncorr->del(uncorr); } else if ((1 <= term) && (term <= 8)) { - array_i* uncorr = array_i_new(); - assert(samples->len == term); uncorr->extend(uncorr, samples); uncorr->extend(uncorr, uncorrelated); + correlated->reset_for(correlated, uncorr->len - term); for (i = term; i < uncorr->len; i++) { - correlated->append(correlated, uncorr->_[i] - - apply_weight(*weight, uncorr->_[i - term])); + a_append(correlated, uncorr->_[i] - + apply_weight(*weight, uncorr->_[i - term])); *weight += update_weight(uncorr->_[i - term], correlated->_[i - term], delta); } /*round-trip the final "terms" uncorrelated samples for the next block*/ uncorrelated->tail(uncorrelated, term, samples); - - uncorr->del(uncorr); } else { /*invalid correlation term*/ assert(0); @@ -1144,94 +1155,102 @@ int term, int delta, array_i* weights, - array_ia* samples) + array_ia* samples, + array_ia* temp) { + array_ia* uncorr = temp; + assert(uncorrelated->len == 2); assert(uncorrelated->_[0]->len == uncorrelated->_[1]->len); assert(weights->len == 2); assert(samples->len == 2); + uncorr->reset(uncorr); if (((17 <= term) && (term <= 18)) || ((1 <= term) && (term <= 8))) { correlated->reset(correlated); correlate_1ch(correlated->append(correlated), uncorrelated->_[0], - term, delta, &(weights->_[0]), samples->_[0]); + term, + delta, + &(weights->_[0]), + samples->_[0], + temp->append(temp)); correlate_1ch(correlated->append(correlated), uncorrelated->_[1], - term, delta, &(weights->_[1]), samples->_[1]); + term, + delta, + &(weights->_[1]), + samples->_[1], + temp->append(temp)); } else if ((-3 <= term) && (term <= -1)) { - array_ia* uncorr = array_ia_new(); - array_i* uncorr_0; - array_i* uncorr_1; + array_i* correlated_0; + array_i* correlated_1; + array_i* uncorr_0 = uncorr->append(uncorr); + array_i* uncorr_1 = uncorr->append(uncorr); unsigned i; assert(samples->_[0]->len == 1); assert(samples->_[1]->len == 1); - uncorr_0 = uncorr->append(uncorr); - uncorr_1 = uncorr->append(uncorr); + uncorr_0->extend(uncorr_0, samples->_[1]); uncorr_0->extend(uncorr_0, uncorrelated->_[0]); uncorr_1->extend(uncorr_1, samples->_[0]); uncorr_1->extend(uncorr_1, uncorrelated->_[1]); correlated->reset(correlated); - correlated->append(correlated); - correlated->append(correlated); + correlated_0 = correlated->append(correlated); + correlated_1 = correlated->append(correlated); + correlated_0->resize(correlated_0, uncorr_0->len - 1); + correlated_1->resize(correlated_1, uncorr_0->len - 1); if (term == -1) { for (i = 1; i < uncorr_0->len; i++) { - array_i_append(correlated->_[0], - uncorr_0->_[i] - - apply_weight(weights->_[0], - uncorr_1->_[i - 1])); - array_i_append(correlated->_[1], - uncorr_1->_[i] - - apply_weight(weights->_[1], - uncorr_0->_[i])); + a_append(correlated_0, + uncorr_0->_[i] - + apply_weight(weights->_[0], uncorr_1->_[i - 1])); + a_append(correlated_1, + uncorr_1->_[i] - + apply_weight(weights->_[1], uncorr_0->_[i])); weights->_[0] += update_weight(uncorr_1->_[i - 1], - correlated->_[0]->_[i - 1], + correlated_0->_[i - 1], delta); weights->_[1] += update_weight(uncorr_0->_[i], - correlated->_[1]->_[i - 1], + correlated_1->_[i - 1], delta); weights->_[0] = MAX(MIN(weights->_[0], 1024), -1024); weights->_[1] = MAX(MIN(weights->_[1], 1024), -1024); } } else if (term == -2) { for (i = 1; i < uncorr_0->len; i++) { - array_i_append(correlated->_[0], - uncorr_0->_[i] - - apply_weight(weights->_[0], - uncorr_1->_[i])); - array_i_append(correlated->_[1], - uncorr_1->_[i] - - apply_weight(weights->_[1], - uncorr_0->_[i - 1])); + a_append(correlated_0, + uncorr_0->_[i] - + apply_weight(weights->_[0], uncorr_1->_[i])); + a_append(correlated_1, + uncorr_1->_[i] - + apply_weight(weights->_[1], uncorr_0->_[i - 1])); weights->_[0] += update_weight(uncorr_1->_[i], - correlated->_[0]->_[i - 1], + correlated_0->_[i - 1], delta); weights->_[1] += update_weight(uncorr_0->_[i - 1], - correlated->_[1]->_[i - 1], + correlated_1->_[i - 1], delta); weights->_[0] = MAX(MIN(weights->_[0], 1024), -1024); weights->_[1] = MAX(MIN(weights->_[1], 1024), -1024); } } else if (term == -3) { for (i = 1; i < uncorr_0->len; i++) { - array_i_append(correlated->_[0], - uncorr_0->_[i] - - apply_weight(weights->_[0], - uncorr_1->_[i - 1])); - array_i_append(correlated->_[1], - uncorr_1->_[i] - - apply_weight(weights->_[1], - uncorr_0->_[i - 1])); + a_append(correlated_0, + uncorr_0->_[i] - + apply_weight(weights->_[0], uncorr_1->_[i - 1])); + a_append(correlated_1, + uncorr_1->_[i] - + apply_weight(weights->_[1], uncorr_0->_[i - 1])); weights->_[0] += update_weight(uncorr_1->_[i - 1], - correlated->_[0]->_[i - 1], + correlated_0->_[i - 1], delta); weights->_[1] += update_weight(uncorr_0->_[i - 1], - correlated->_[1]->_[i - 1], + correlated_1->_[i - 1], delta); weights->_[0] = MAX(MIN(weights->_[0], 1024), -1024); weights->_[1] = MAX(MIN(weights->_[1], 1024), -1024); @@ -1244,8 +1263,6 @@ /*round-trip the final uncorrelated sample for the next block*/ samples->_[1]->_[0] = uncorr_0->_[uncorr_0->len - 1]; samples->_[0]->_[0] = uncorr_1->_[uncorr_1->len - 1]; - - uncorr->del(uncorr); } else { /*invalid correlation term*/ assert(0); @@ -1330,13 +1347,13 @@ unsigned c = (a != 0) ? (LOG2(a) + 1) : 0; if (value >= 0) { - if ((0 <= a) && (a < 256)) { + if (a < 256) { return (c << 8) + WLOG[(a << (9 - c)) % 256]; } else { return (c << 8) + WLOG[(a >> (c - 9)) % 256]; } } else { - if ((0 <= a) && (a < 256)) { + if (a < 256) { return -((c << 8) + WLOG[(a << (9 - c)) % 256]); } else { return -((c << 8) + WLOG[(a >> (c - 9)) % 256]); @@ -1882,8 +1899,171 @@ } #ifdef STANDALONE +#include <getopt.h> +#include <errno.h> + +static unsigned +count_bits(unsigned value) +{ + unsigned bits = 0; + while (value) { + bits += value & 0x1; + value >>= 1; + } + return bits; +} + int main(int argc, char *argv[]) { - encoders_encode_wavpack(argv[1], stdin, 22050, 1, 1, 1, 16); + char* output_file = NULL; + unsigned channels = 2; + unsigned channel_mask = 0x3; + unsigned sample_rate = 44100; + unsigned bits_per_sample = 16; + + unsigned block_size = 22050; + unsigned correlation_passes = 5; + int false_stereo = 0; + int wasted_bits = 0; + int joint_stereo = 0; + + char c; + const static struct option long_opts[] = { + {"help", no_argument, NULL, 'h'}, + {"channels", required_argument, NULL, 'c'}, + {"channel-mask", required_argument, NULL, 'm'}, + {"sample-rate", required_argument, NULL, 'r'}, + {"bits-per-sample", required_argument, NULL, 'b'}, + {"block-size", required_argument, NULL, 'B'}, + {"correlation-passes", required_argument, NULL, 'p'}, + {"false-stereo", no_argument, NULL, 'f'}, + {"wasted-bits", no_argument, NULL, 'w'}, + {"joint-stereo", no_argument, NULL, 'j'}}; + const static char* short_opts = "-hc:m:r:b:B:p:fwj"; + + while ((c = getopt_long(argc, + argv, + short_opts, + long_opts, + NULL)) != -1) { + switch (c) { + case 1: + if (output_file == NULL) { + output_file = optarg; + } else { + printf("only one output file allowed\n"); + return 1; + } + break; + case 'c': + if (((channels = strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --channel \"%s\"\n", optarg); + return 1; + } + break; + case 'm': + if (((channel_mask = strtoul(optarg, NULL, 16)) == 0) && errno) { + printf("invalid --channel-mask \"%s\"\n", optarg); + return 1; + } + break; + case 'r': + if (((sample_rate = strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --sample-rate \"%s\"\n", optarg); + return 1; + } + break; + case 'b': + if (((bits_per_sample = strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --bits-per-sample \"%s\"\n", optarg); + return 1; + } + break; + case 'B': + if (((block_size = strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --block-size \"%s\"\n", optarg); + return 1; + } + break; + case 'p': + if (((correlation_passes = + strtoul(optarg, NULL, 10)) == 0) && errno) { + printf("invalid --correlation_passes \"%s\"\n", optarg); + return 1; + } + break; + case 'f': + false_stereo = 1; + break; + case 'w': + wasted_bits = 1; + break; + case 'j': + joint_stereo = 1; + break; + case 'h': /*fallthrough*/ + case ':': + case '?': + printf("*** Usage: wvenc [options] <output.wv>\n"); + printf("-c, --channels=# number of input channels\n"); + printf("-m, --channel-mask=# channel mask as hex value\n"); + printf("-r, --sample_rate=# input sample rate in Hz\n"); + printf("-b, --bits-per-sample=# bits per input sample\n"); + printf("\n"); + printf("-B, --block-size=# block size\n"); + printf("-p, --correlation_passes=# " + "number of correlation passes\n"); + printf("-f, --false-stereo check for false stereo\n"); + printf("-w, --wasted-bits check for wasted bits"); + printf("-j, --joint-stereo use joint stereo\n"); + return 0; + default: + break; + } + } + if (output_file == NULL) { + printf("exactly 1 output file required\n"); + return 1; + } + + assert(channels > 0); + assert((bits_per_sample == 8) || + (bits_per_sample == 16) || + (bits_per_sample == 24)); + assert(sample_rate > 0); + assert((correlation_passes == 0) || + (correlation_passes == 1) || + (correlation_passes == 2) || + (correlation_passes == 5) || + (correlation_passes == 10) || + (correlation_passes == 16)); + assert(count_bits(channel_mask) == channels); + + printf("Encoding from stdin using parameters:\n"); + printf("channels %u\n", channels); + printf("channel mask 0x%X\n", channel_mask); + printf("sample rate %u\n", sample_rate); + printf("bits per sample %u\n", bits_per_sample); + printf("little-endian, signed samples\n"); + printf("\n"); + printf("block size %u\n", block_size); + printf("correlation_passes %u\n", correlation_passes); + printf("false stereo %d\n", false_stereo); + printf("wasted bits %d\n", wasted_bits); + printf("joint stereo %d\n", joint_stereo); + + encoders_encode_wavpack(output_file, + open_pcmreader(stdin, + sample_rate, + channels, + channel_mask, + bits_per_sample, + 0, + 1), + block_size, + false_stereo, + wasted_bits, + joint_stereo, + correlation_passes); return 0; }
View file
audiotools-2.18.tar.gz/src/encoders/wavpack.h -> audiotools-2.19.tar.gz/src/encoders/wavpack.h
Changed
@@ -83,6 +83,7 @@ array_ia* shifted; array_ia* mid_side; array_ia* correlated; + array_ia* correlation_temp; BitstreamWriter* sub_block; BitstreamWriter* sub_blocks; } cache; @@ -249,21 +250,20 @@ array_i* deltas, array_ia* weights, array_iaa* samples, - unsigned channel_count); + unsigned channel_count, + array_ia* temp); static int apply_weight(int weight, int64_t sample); -static int -update_weight(int64_t source, int result, int delta); - static void correlate_1ch(array_i* correlated, const array_i* uncorrelated, int term, int delta, int* weight, - array_i* samples); + array_i* samples, + array_i* temp); static void correlate_2ch(array_ia* correlated, @@ -271,7 +271,8 @@ int term, int delta, array_i* weights, - array_ia* samples); + array_ia* samples, + array_ia* temp); static void write_entropy_variables(BitstreamWriter* bs,
View file
audiotools-2.18.tar.gz/src/huffman.c -> audiotools-2.19.tar.gz/src/huffman.c
Changed
@@ -419,13 +419,13 @@ #ifdef EXECUTABLE -#include <jansson.h> +#include "parson.h" #include <getopt.h> struct huffman_frequency* json_to_frequencies(const char* path, unsigned int* total_frequencies); -struct huffman_frequency parse_json_pair(json_t* bit_list, json_t* value); +struct huffman_frequency parse_json_pair(JSON_Array* bit_list, double value); int main(int argc, char* argv[]) { /*option handling variables*/ @@ -533,48 +533,53 @@ struct huffman_frequency* json_to_frequencies(const char* path, unsigned int* total_frequencies) { - json_error_t error; - json_t* input = json_load_file(path, 0, &error); + JSON_Value* file; + JSON_Array* input; size_t input_size; int o; size_t i; struct huffman_frequency* frequencies; - if (input == NULL) { - fprintf(stderr, "%s %d: %s\n", error.source, error.line, error.text); + if ((file = json_parse_file(path)) == NULL) { + fprintf(stderr, "error parsing input .json file \"%s\"\n", path); exit(1); + } else { + input = json_value_get_array(file); + if (input == NULL) { + fprintf(stderr, "JSON file isn't an array of items\n"); + exit(1); + } } - input_size = json_array_size(input); + input_size = json_array_get_count(input); - frequencies = malloc(sizeof(struct huffman_frequency) * - (input_size / 2)); + frequencies = malloc(sizeof(struct huffman_frequency) * (input_size / 2)); *total_frequencies = input_size / 2; for (i = o = 0; i < input_size; i += 2,o++) { - frequencies[o] = parse_json_pair(json_array_get(input, i), - json_array_get(input, i + 1)); + frequencies[o] = parse_json_pair(json_array_get_array(input, i), + json_array_get_number(input, i + 1)); } - json_decref(input); + json_value_free(file); return frequencies; } -struct huffman_frequency parse_json_pair(json_t* bit_list, json_t* value) { +struct huffman_frequency parse_json_pair(JSON_Array* bit_list, double value) { struct huffman_frequency frequency; size_t i; frequency.bits = 0; frequency.length = 0; - for (i = 0; i < json_array_size(bit_list); i++) { + for (i = 0; i < json_array_get_count(bit_list); i++) { frequency.bits = ((frequency.bits << 1) | - json_integer_value(json_array_get(bit_list, i))); + (int)(json_array_get_number(bit_list, i))); frequency.length++; } - frequency.value = json_integer_value(value); + frequency.value = (int)(value); return frequency; }
View file
audiotools-2.18.tar.gz/src/mod_bitstream.c -> audiotools-2.19.tar.gz/src/mod_bitstream.c
Changed
@@ -544,12 +544,20 @@ return NULL; } - self->bitstream->substream_append(self->bitstream, - substream->bitstream, - bytes); + if (!setjmp(*br_try(self->bitstream))) { + self->bitstream->substream_append(self->bitstream, + substream->bitstream, + bytes); - Py_INCREF(Py_None); - return Py_None; + br_etry(self->bitstream); + Py_INCREF(Py_None); + return Py_None; + } else { + br_etry(self->bitstream); + /*read error occured during substream_append*/ + PyErr_SetString(PyExc_IOError, "I/O error appending substream"); + return NULL; + } } static PyObject*
View file
audiotools-2.19.tar.gz/src/output
Added
+(directory)
View file
audiotools-2.19.tar.gz/src/output.c
Added
@@ -0,0 +1,52 @@ +#include <Python.h> + +/******************************************************** + Audio Tools, a module and set of tools for manipulating audio data + Copyright (C) 2007-2012 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 +*******************************************************/ + +PyMethodDef module_methods[] = { + {NULL} +}; + +#ifdef CORE_AUDIO +extern PyTypeObject output_CoreAudioType; +#endif + +PyMODINIT_FUNC +initoutput(void) +{ + PyObject* m; + +#ifdef CORE_AUDIO + output_CoreAudioType.tp_new = PyType_GenericNew; + if (PyType_Ready(&output_CoreAudioType) < 0) + return; +#endif + + m = Py_InitModule3("output", module_methods, + "System-specific audio output"); + +#ifdef CORE_AUDIO + Py_INCREF(&output_CoreAudioType); + PyModule_AddObject(m, "CoreAudio", + (PyObject *)&output_CoreAudioType); +#else + /*to avoid an unused variable warning if no output types are present*/ + (void)m; +#endif +}
View file
audiotools-2.19.tar.gz/src/output/core_audio.c
Added
@@ -0,0 +1,538 @@ +#include "core_audio.h" + +/******************************************************** + Audio Tools, a module and set of tools for manipulating audio data + Copyright (C) 2007-2012 Brian Langenberger and the mpg123 project + initially written by Guillaume Outters + modified by Nicholas J Humfrey to use SFIFO code + modified by Taihei Monma to use AudioUnit and AudioConverter APIs + further modified by Brian Langenberger for use in Python Audio Tools + + 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 +*******************************************************/ + +PyObject* +CoreAudio_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { + output_CoreAudio *self; + + self = (output_CoreAudio *)type->tp_alloc(type, 0); + + return (PyObject *)self; +} + +void +CoreAudio_dealloc(output_CoreAudio *self) { + /*additional memory deallocation here*/ + if (!self->closed) { + self->ao->flush(self->ao); + self->ao->close(self->ao); + } + + if (self->ao != NULL) { + self->ao->deinit(self->ao); + free(self->ao); + } + + self->ob_type->tp_free((PyObject*)self); +} + +int +CoreAudio_init(output_CoreAudio *self, PyObject *args, PyObject *kwds) { + long sample_rate; + int channels; + int channel_mask; + int bits_per_sample; + + self->ao = NULL; + self->closed = 1; + + if (!PyArg_ParseTuple(args, "liii", + &sample_rate, + &channels, + &channel_mask, + &bits_per_sample)) + return -1; + + if ((bits_per_sample != 8) && + (bits_per_sample != 16) && + (bits_per_sample != 24)) { + PyErr_SetString(PyExc_ValueError, + "bits_per_sample must be 8, 16 or 24"); + return -1; + } + + self->ao = malloc(sizeof(audio_output_t)); + + if (init_coreaudio(self->ao, + sample_rate, + channels, + bits_per_sample / 8, + 1)) { + PyErr_SetString(PyExc_ValueError, + "error initializing CoreAudio"); + return -1; + } else { + PyObject* os_module_obj; + PyObject* devnull_obj; + char* devnull; + int current_stdout; + int devnull_stdout; + int returnval; + + /*because CoreAudio loves spewing text to stdout + at init-time, we'll need to temporarily redirect + stdout to /dev/null*/ + + /*first, determine the location of /dev/null from os.devnull*/ + if ((os_module_obj = PyImport_ImportModule("os")) == NULL) { + return -1; + } + if ((devnull_obj = + PyObject_GetAttrString(os_module_obj, "devnull")) == NULL) { + Py_DECREF(os_module_obj); + return -1; + } + if ((devnull = PyString_AsString(devnull_obj)) == NULL) { + Py_DECREF(os_module_obj); + Py_DECREF(devnull_obj); + return -1; + } + + /*open /dev/null*/ + if ((devnull_stdout = open(devnull, O_WRONLY | O_TRUNC)) == -1) { + Py_DECREF(os_module_obj); + Py_DECREF(devnull_obj); + PyErr_SetFromErrno(PyExc_IOError); + return -1; + } else { + /*close unneeded Python objects once descriptor is open*/ + Py_DECREF(os_module_obj); + Py_DECREF(devnull_obj); + } + + /*swap file descriptors*/ + current_stdout = dup(STDOUT_FILENO); + dup2(devnull_stdout, STDOUT_FILENO); + + /*initialize CoreAudio itself*/ + if (self->ao->open(self->ao)) { + PyErr_SetString(PyExc_ValueError, + "error opening CoreAudio"); + returnval = -1; + } else { + self->closed = 0; + returnval = 0; + } + + /*close /dev/null and swap file descriptors back again*/ + dup2(current_stdout, STDOUT_FILENO); + close(current_stdout); + close(devnull_stdout); + + return returnval; + } +} + +static PyObject* CoreAudio_play(output_CoreAudio *self, PyObject *args) +{ + unsigned char* buffer; +#ifdef PY_SSIZE_T_CLEAN + Py_ssize_t buffer_size; +#else + int buffer_size; +#endif + int write_result; + + if (!PyArg_ParseTuple(args, "s#", &buffer, &buffer_size)) + return NULL; + + Py_BEGIN_ALLOW_THREADS + write_result = self->ao->write(self->ao, buffer, (int)buffer_size); + Py_END_ALLOW_THREADS + + if (write_result == -1) { + PyErr_SetString(PyExc_ValueError, + "error writing data to CoreAudio"); + return NULL; + } else { + Py_INCREF(Py_None); + return Py_None; + } +} + +static PyObject* CoreAudio_flush(output_CoreAudio *self, PyObject *args) +{ + /*ensure pending samples are played to output + by sleeping for the duration of the ring buffer*/ + Py_BEGIN_ALLOW_THREADS + usleep(FIFO_DURATION * 1000000); + Py_END_ALLOW_THREADS + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject* CoreAudio_pause(output_CoreAudio *self, PyObject *args) +{ + self->ao->pause(self->ao); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject* CoreAudio_resume(output_CoreAudio *self, PyObject *args) +{ + self->ao->resume(self->ao); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject* CoreAudio_close(output_CoreAudio *self, PyObject *args) +{ + if (!self->closed) { + self->ao->flush(self->ao); + self->ao->close(self->ao); + self->closed = 1; + } + + Py_INCREF(Py_None); + return Py_None; +} + +static int init_coreaudio(audio_output_t* ao, + long sample_rate, + int channels, + int bytes_per_sample, + int signed_samples) +{ + if (ao==NULL) return -1; + + /* Set callbacks */ + ao->open = open_coreaudio; + ao->flush = flush_coreaudio; + ao->write = write_coreaudio; + ao->pause = pause_coreaudio; + ao->resume = resume_coreaudio; + ao->close = close_coreaudio; + ao->deinit = deinit_coreaudio; + + ao->rate = sample_rate; + ao->channels = channels; + ao->bytes_per_sample = bytes_per_sample; + ao->signed_samples = signed_samples; + + /* Allocate memory for data structure */ + ao->userptr = malloc( sizeof( mpg123_coreaudio_t ) ); + if (ao->userptr==NULL) { + return -1; + } + memset( ao->userptr, 0, sizeof(mpg123_coreaudio_t) ); + + /* Success */ + return 0; +} + +static int open_coreaudio(audio_output_t *ao) +{ + mpg123_coreaudio_t* ca = (mpg123_coreaudio_t*)ao->userptr; + UInt32 size; + AudioComponentDescription desc; + AudioComponent comp; + AudioStreamBasicDescription inFormat; + AudioStreamBasicDescription outFormat; + AURenderCallbackStruct renderCallback; + Boolean outWritable; + + /* Initialize our environment */ + ca->play = 0; + ca->buffer = NULL; + ca->buffer_size = 0; + ca->last_buffer = 0; + ca->play_done = 0; + ca->decode_done = 0; + + + /* Get the default audio output unit */ + desc.componentType = kAudioUnitType_Output; + desc.componentSubType = kAudioUnitSubType_DefaultOutput; + desc.componentManufacturer = kAudioUnitManufacturer_Apple; + desc.componentFlags = 0; + desc.componentFlagsMask = 0; + comp = AudioComponentFindNext(NULL, &desc); + if(comp == NULL) { + return -1; + } + + if(AudioComponentInstanceNew(comp, &(ca->outputUnit))) { + return -1; + } + + if(AudioUnitInitialize(ca->outputUnit)) { + return -1; + } + + /* Specify the output PCM format */ + AudioUnitGetPropertyInfo(ca->outputUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Output, + 0, + &size, + &outWritable); + if(AudioUnitGetProperty(ca->outputUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Output, + 0, + &outFormat, + &size)) { + return -1; + } + + if(AudioUnitSetProperty(ca->outputUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &outFormat, size)) { + return -1; + } + + /* Specify the input PCM format */ + ca->channels = ao->channels; + inFormat.mSampleRate = ao->rate; + inFormat.mChannelsPerFrame = ao->channels; + inFormat.mFormatID = kAudioFormatLinearPCM; +#ifdef _BIG_ENDIAN + inFormat.mFormatFlags = kLinearPCMFormatFlagIsPacked | kLinearPCMFormatFlagIsBigEndian; +#else + inFormat.mFormatFlags = kLinearPCMFormatFlagIsPacked; +#endif + + if (ao->signed_samples) { + inFormat.mFormatFlags |= kLinearPCMFormatFlagIsSignedInteger; + } + + ca->bps = ao->bytes_per_sample; + + inFormat.mBitsPerChannel = ca->bps << 3; + inFormat.mBytesPerPacket = ca->bps*inFormat.mChannelsPerFrame; + inFormat.mFramesPerPacket = 1; + inFormat.mBytesPerFrame = ca->bps*inFormat.mChannelsPerFrame; + + /* Add our callback - but don't start it yet */ + memset(&renderCallback, 0, sizeof(AURenderCallbackStruct)); + renderCallback.inputProc = convertProc; + renderCallback.inputProcRefCon = ao->userptr; + if(AudioUnitSetProperty(ca->outputUnit, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, + 0, + &renderCallback, + sizeof(AURenderCallbackStruct))) { + return -1; + } + + + /* Open an audio I/O stream and create converter */ + if (ao->rate > 0 && ao->channels >0 ) { + int ringbuffer_len; + + if(AudioConverterNew(&inFormat, &outFormat, &(ca->converter))) { + return -1; + } + if(ao->channels == 1) { + SInt32 channelMap[2] = { 0, 0 }; + if(AudioConverterSetProperty(ca->converter, kAudioConverterChannelMap, sizeof(channelMap), channelMap)) { + return -1; + } + } + + /* Initialise FIFO */ + ringbuffer_len = ((int)ao->rate * + FIFO_DURATION * + ca->bps * + ao->channels); + sfifo_init( &ca->fifo, ringbuffer_len ); + } + + return(0); +} + +static void flush_coreaudio(audio_output_t *ao) +{ + mpg123_coreaudio_t* ca = (mpg123_coreaudio_t*)ao->userptr; + + /* Stop playback */ + if(AudioOutputUnitStop(ca->outputUnit)) { + /* error("AudioOutputUnitStop failed"); */ + } + ca->play=0; + + /* Empty out the ring buffer */ + sfifo_flush( &ca->fifo ); +} + +static int write_coreaudio(audio_output_t *ao, unsigned char *buf, int len) +{ + mpg123_coreaudio_t* ca = (mpg123_coreaudio_t*)ao->userptr; + int written; + + /* If there is no room, then sleep for half the length of the FIFO */ + while (sfifo_space( &ca->fifo ) < len ) { + usleep( (FIFO_DURATION/2) * 1000000 ); + } + + /* Store converted audio in ring buffer */ + written = sfifo_write( &ca->fifo, (char*)buf, len); + if (written != len) { + return -1; + } + + /* Start playback now that we have something to play */ + if(!ca->play) + { + if(AudioOutputUnitStart(ca->outputUnit)) { + return -1; + } + ca->play = 1; + } + + return len; +} + +static void pause_coreaudio(audio_output_t *ao) +{ + mpg123_coreaudio_t* ca = (mpg123_coreaudio_t*)ao->userptr; + + if (ca->play) { + ca->play = 0; + AudioOutputUnitStop(ca->outputUnit); + } +} + +static void resume_coreaudio(audio_output_t *ao) +{ + mpg123_coreaudio_t* ca = (mpg123_coreaudio_t*)ao->userptr; + + if (!ca->play) { + AudioOutputUnitStart(ca->outputUnit); + ca->play = 1; + } +} + +static int close_coreaudio(audio_output_t *ao) +{ + mpg123_coreaudio_t* ca = (mpg123_coreaudio_t*)ao->userptr; + + if (ca) { + ca->decode_done = 1; + while(!ca->play_done && ca->play) usleep(10000); + + /* No matter the error code, we want to close it + (by brute force if necessary) */ + AudioConverterDispose(ca->converter); + AudioOutputUnitStop(ca->outputUnit); + AudioUnitUninitialize(ca->outputUnit); + AudioComponentInstanceDispose(ca->outputUnit); + + /* Free the ring buffer */ + sfifo_close( &ca->fifo ); + + /* Free the conversion buffer */ + if (ca->buffer) { + free( ca->buffer ); + ca->buffer = NULL; + } + + } + + return 0; +} + +static int deinit_coreaudio(audio_output_t* ao) +{ + /* Free up memory */ + if (ao->userptr) { + free( ao->userptr ); + ao->userptr = NULL; + } + + /* Success */ + return 0; +} + +static OSStatus convertProc(void *inRefCon, + AudioUnitRenderActionFlags *inActionFlags, + const AudioTimeStamp *inTimeStamp, + UInt32 inBusNumber, + UInt32 inNumFrames, + AudioBufferList *ioData) +{ + AudioStreamPacketDescription* outPacketDescription = NULL; + mpg123_coreaudio_t* ca = (mpg123_coreaudio_t*)inRefCon; + OSStatus err= noErr; + + err = AudioConverterFillComplexBuffer(ca->converter, + playProc, + inRefCon, + &inNumFrames, + ioData, + outPacketDescription); + + return err; +} + +static OSStatus playProc(AudioConverterRef inAudioConverter, + UInt32 *ioNumberDataPackets, + AudioBufferList *outOutputData, + AudioStreamPacketDescription + **outDataPacketDescription, + void* inClientData) +{ + mpg123_coreaudio_t *ca = (mpg123_coreaudio_t *)inClientData; + long n; + + + if(ca->last_buffer) { + ca->play_done = 1; + return noErr; + } + + for(n = 0; n < outOutputData->mNumberBuffers; n++) + { + unsigned int wanted = *ioNumberDataPackets * ca->channels * ca->bps; + unsigned char *dest; + unsigned int read; + if(ca->buffer_size < wanted) { + ca->buffer = realloc( ca->buffer, wanted); + ca->buffer_size = wanted; + } + dest = ca->buffer; + + /* Only play if we have data left */ + if ( sfifo_used( &ca->fifo ) < wanted ) { + if(!ca->decode_done) { + return -1; + } + wanted = sfifo_used( &ca->fifo ); + ca->last_buffer = 1; + } + + /* Read audio from FIFO to SDL's buffer */ + read = sfifo_read( &ca->fifo, dest, wanted ); + + outOutputData->mBuffers[n].mDataByteSize = read; + outOutputData->mBuffers[n].mData = dest; + } + + return noErr; +} + +#include "sfifo.c"
View file
audiotools-2.19.tar.gz/src/output/core_audio.h
Added
@@ -0,0 +1,174 @@ +#include <Python.h> +#include <CoreServices/CoreServices.h> +#include <AudioUnit/AudioUnit.h> +#include <AudioToolbox/AudioToolbox.h> +#include "sfifo.h" + +/******************************************************** + Audio Tools, a module and set of tools for manipulating audio data + Copyright (C) 2007-2012 Brian Langenberger and the mpg123 project + initially written by Guillaume Outters + modified by Nicholas J Humfrey to use SFIFO code + modified by Taihei Monma to use AudioUnit and AudioConverter APIs + further modified by Brian Langenberger for use in Python Audio Tools + + 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 +*******************************************************/ + + +/* Duration of the ring buffer in seconds */ +#define FIFO_DURATION (1.0f) + +typedef struct audio_output_struct +{ + int fn; /* filenumber */ + void *userptr; /* driver specific pointer */ + + /* Callbacks */ + int (*open)(struct audio_output_struct *); + int (*write)(struct audio_output_struct *, unsigned char *,int); + void (*pause)(struct audio_output_struct *); + void (*resume)(struct audio_output_struct *); + void (*flush)(struct audio_output_struct *); + int (*close)(struct audio_output_struct *); + int (*deinit)(struct audio_output_struct *); + + long rate; /* sample rate */ + int channels; /* number of channels */ + int bytes_per_sample; + int signed_samples; +} audio_output_t; + +typedef struct mpg123_coreaudio +{ + AudioConverterRef converter; + AudioUnit outputUnit; + int open; + char play; + int channels; + int bps; + int last_buffer; + int play_done; + int decode_done; + + /* Convertion buffer */ + unsigned char * buffer; + size_t buffer_size; + + /* Ring buffer */ + sfifo_t fifo; +} mpg123_coreaudio_t; + + +typedef struct { + PyObject_HEAD + + audio_output_t* ao; + int closed; +} output_CoreAudio; + +static PyObject* CoreAudio_play(output_CoreAudio *self, PyObject *args); +static PyObject* CoreAudio_pause(output_CoreAudio *self, PyObject *args); +static PyObject* CoreAudio_resume(output_CoreAudio *self, PyObject *args); +static PyObject* CoreAudio_flush(output_CoreAudio *self, PyObject *args); +static PyObject* CoreAudio_close(output_CoreAudio *self, PyObject *args); + +static PyObject* CoreAudio_new(PyTypeObject *type, + PyObject *args, + PyObject *kwds); +void CoreAudio_dealloc(output_CoreAudio *self); +int CoreAudio_init(output_CoreAudio *self, PyObject *args, PyObject *kwds); + +PyGetSetDef CoreAudio_getseters[] = { + {NULL} +}; + +PyMethodDef CoreAudio_methods[] = { + {"play", (PyCFunction)CoreAudio_play, METH_VARARGS, ""}, + {"pause", (PyCFunction)CoreAudio_pause, METH_NOARGS, ""}, + {"resume", (PyCFunction)CoreAudio_resume, METH_NOARGS, ""}, + {"flush", (PyCFunction)CoreAudio_flush, METH_NOARGS, ""}, + {"close", (PyCFunction)CoreAudio_close, METH_NOARGS, ""}, + {NULL} +}; + +PyTypeObject output_CoreAudioType = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "output.CoreAudio", /*tp_name*/ + sizeof(output_CoreAudio), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + (destructor)CoreAudio_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash */ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + "CoreAudio objects", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + CoreAudio_methods, /* tp_methods */ + 0, /* tp_members */ + CoreAudio_getseters, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)CoreAudio_init, /* tp_init */ + 0, /* tp_alloc */ + CoreAudio_new, /* tp_new */ +}; + +static int init_coreaudio(audio_output_t* ao, + long sample_rate, + int channels, + int bytes_per_sample, + int signed_samples); +static int open_coreaudio(audio_output_t *ao); +static void flush_coreaudio(audio_output_t *ao); +static int write_coreaudio(audio_output_t *ao, unsigned char *buf, int len); +static void pause_coreaudio(audio_output_t *ao); +static void resume_coreaudio(audio_output_t *ao); +static int close_coreaudio(audio_output_t *ao); +static int deinit_coreaudio(audio_output_t* ao); + +static OSStatus convertProc(void *inRefCon, + AudioUnitRenderActionFlags *inActionFlags, + const AudioTimeStamp *inTimeStamp, + UInt32 inBusNumber, + UInt32 inNumFrames, + AudioBufferList *ioData); + +static OSStatus playProc(AudioConverterRef inAudioConverter, + UInt32 *ioNumberDataPackets, + AudioBufferList *outOutputData, + AudioStreamPacketDescription + **outDataPacketDescription, + void* inClientData);
View file
audiotools-2.19.tar.gz/src/output/sfifo.c
Added
@@ -0,0 +1,142 @@ +/* + SFIFO 1.3 Simple portable lock-free FIFO + + (c) 2000-2002, David Olofson - free software under the terms of the LGPL 2.1 +*/ + + +/* +----------------------------------------------------------- +TODO: + * Is there a way to avoid losing one byte of buffer + space to avoid extra variables or locking? + + * Test more compilers and environments. +----------------------------------------------------------- + */ + +#include <string.h> +#include <stdlib.h> + +#include "sfifo.h" + +/* + * Alloc buffer, init FIFO etc... + */ +SFIFO_SCOPE int sfifo_init(sfifo_t *f, int size) +{ + memset(f, 0, sizeof(sfifo_t)); + + if(size > SFIFO_MAX_BUFFER_SIZE) + return -EINVAL; + + /* + * Set sufficient power-of-2 size. + * + * No, there's no bug. If you need + * room for N bytes, the buffer must + * be at least N+1 bytes. (The fifo + * can't tell 'empty' from 'full' + * without unsafe index manipulations + * otherwise.) + */ + f->size = 1; + for(; f->size <= size; f->size <<= 1) + ; + + /* Get buffer */ + if( 0 == (f->buffer = (void *)malloc(f->size)) ) + return -ENOMEM; + + return 0; +} + +/* + * Dealloc buffer etc... + */ +SFIFO_SCOPE void sfifo_close(sfifo_t *f) +{ + if(f->buffer) { + free(f->buffer); + f->buffer = NULL; /* Prevent double free */ + } +} + +/* + * Empty FIFO buffer + */ +SFIFO_SCOPE void sfifo_flush(sfifo_t *f) +{ + /* Reset positions */ + f->readpos = 0; + f->writepos = 0; +} + +/* + * Write bytes to a FIFO + * Return number of bytes written, or an error code + */ +SFIFO_SCOPE int sfifo_write(sfifo_t *f, const void *_buf, int len) +{ + int total; + int i; + const char *buf = (const char *)_buf; + + if(!f->buffer) + return -ENODEV; /* No buffer! */ + + /* total = len = min(space, len) */ + total = sfifo_space(f); + if(len > total) + len = total; + else + total = len; + + i = f->writepos; + if(i + len > f->size) + { + memcpy(f->buffer + i, buf, f->size - i); + buf += f->size - i; + len -= f->size - i; + i = 0; + } + memcpy(f->buffer + i, buf, len); + f->writepos = i + len; + + return total; +} + + +/* + * Read bytes from a FIFO + * Return number of bytes read, or an error code + */ +SFIFO_SCOPE int sfifo_read(sfifo_t *f, void *_buf, int len) +{ + int total; + int i; + char *buf = (char *)_buf; + + if(!f->buffer) + return -ENODEV; /* No buffer! */ + + /* total = len = min(used, len) */ + total = sfifo_used(f); + if(len > total) + len = total; + else + total = len; + + i = f->readpos; + if(i + len > f->size) + { + memcpy(buf, f->buffer + i, f->size - i); + buf += f->size - i; + len -= f->size - i; + i = 0; + } + memcpy(buf, f->buffer + i, len); + f->readpos = i + len; + + return total; +}
View file
audiotools-2.19.tar.gz/src/output/sfifo.h
Added
@@ -0,0 +1,95 @@ +/* + SFIFO 1.3 Simple portable lock-free FIFO + + (c) 2000-2002, David Olofson - free software under the terms of the LGPL 2.1 +*/ + + +/* + * Platform support: + * gcc / Linux / x86: Works + * gcc / Linux / x86 kernel: Works + * gcc / FreeBSD / x86: Works + * gcc / NetBSD / x86: Works + * gcc / Mac OS X / PPC: Works + * gcc / Win32 / x86: Works + * Borland C++ / DOS / x86RM: Works + * Borland C++ / Win32 / x86PM16: Untested + * ? / Various Un*ces / ?: Untested + * ? / Mac OS / PPC: Untested + * gcc / BeOS / x86: Untested + * gcc / BeOS / PPC: Untested + * ? / ? / Alpha: Untested + * + * 1.2: Max buffer size halved, to avoid problems with + * the sign bit... + * + * 1.3: Critical buffer allocation bug fixed! For certain + * requested buffer sizes, older version would + * allocate a buffer of insufficient size, which + * would result in memory thrashing. (Amazing that + * I've manage to use this to the extent I have + * without running into this... *heh*) + */ + +#ifndef _SFIFO_H_ +#define _SFIFO_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include <errno.h> + +/* Defining SFIFO_STATIC and then including the sfifo.c will result in local code. */ +#ifdef SFIFO_STATIC +#define SFIFO_SCOPE static +#else +#define SFIFO_SCOPE +#endif + +/*------------------------------------------------ + "Private" stuff +------------------------------------------------*/ +/* + * Porting note: + * Reads and writes of a variable of this type in memory + * must be *atomic*! 'int' is *not* atomic on all platforms. + * A safe type should be used, and sfifo should limit the + * maximum buffer size accordingly. + */ +typedef int sfifo_atomic_t; +#ifdef __TURBOC__ +# define SFIFO_MAX_BUFFER_SIZE 0x7fff +#else /* Kludge: Assume 32 bit platform */ +# define SFIFO_MAX_BUFFER_SIZE 0x7fffffff +#endif + +typedef struct sfifo_t +{ + char *buffer; + int size; /* Number of bytes */ + sfifo_atomic_t readpos; /* Read position */ + sfifo_atomic_t writepos; /* Write position */ +} sfifo_t; + +#define SFIFO_SIZEMASK(x) ((x)->size - 1) + + +/*------------------------------------------------ + API +------------------------------------------------*/ +SFIFO_SCOPE int sfifo_init(sfifo_t *f, int size); +SFIFO_SCOPE void sfifo_close(sfifo_t *f); +SFIFO_SCOPE void sfifo_flush(sfifo_t *f); +SFIFO_SCOPE int sfifo_write(sfifo_t *f, const void *buf, int len); +SFIFO_SCOPE int sfifo_read(sfifo_t *f, void *buf, int len); +#define sfifo_used(x) (((x)->writepos - (x)->readpos) & SFIFO_SIZEMASK(x)) +#define sfifo_space(x) ((x)->size - 1 - sfifo_used(x)) +#define sfifo_size(x) ((x)->size - 1) + + +#ifdef __cplusplus +}; +#endif +#endif
View file
audiotools-2.19.tar.gz/src/parson.c
Added
@@ -0,0 +1,652 @@ +/* + Parson ( http://kgabis.github.com/parson/ ) + Copyright (c) 2012 Krzysztof Gabis + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#include "parson.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> + +#define ERROR 0 +#define SUCCESS 1 +#define STARTING_CAPACITY 15 +#define ARRAY_MAX_CAPACITY 122880 /* 15*(2^13) */ +#define OBJECT_MAX_CAPACITY 960 /* 15*(2^6) */ +#define MAX_NESTING 19 +#define sizeof_token(a) (sizeof(a) - 1) +#define skip_char(str) ((*str)++) +#define skip_whitespaces(str) while (isspace(**string)) { skip_char(string); } + +#define parson_malloc(a) malloc(a) +#define parson_free(a) free((void*)a) +#define parson_realloc(a, b) realloc(a, b) + +/* Type definitions */ +typedef union json_value_value { + const char *string; + double number; + JSON_Object *object; + JSON_Array *array; + int boolean; + int null; +} JSON_Value_Value; + +struct json_value_t { + JSON_Value_Type type; + JSON_Value_Value value; +}; + +struct json_object_t { + const char **names; + JSON_Value **values; + size_t count; + size_t capacity; +}; + +struct json_array_t { + JSON_Value **items; + size_t count; + size_t capacity; +}; + +/* Various */ +static int try_realloc(void **ptr, size_t new_size); +static char * parson_strndup(const char *string, size_t n); +static int is_utf(const char *string); +static int is_decimal(const char *string, size_t length); + +/* JSON Object */ +static JSON_Object * json_object_init(void); +static int json_object_add(JSON_Object *object, const char *name, JSON_Value *value); +static int json_object_resize(JSON_Object *object, size_t capacity); +static JSON_Value * json_object_nget_value(const JSON_Object *object, const char *name, size_t n); +static void json_object_free(JSON_Object *object); + +/* JSON Array */ +static JSON_Array * json_array_init(void); +static int json_array_add(JSON_Array *array, JSON_Value *value); +static int json_array_resize(JSON_Array *array, size_t capacity); +static void json_array_free(JSON_Array *array); + +/* JSON Value */ +static JSON_Value * json_value_init_object(void); +static JSON_Value * json_value_init_array(void); +static JSON_Value * json_value_init_string(const char *string); +static JSON_Value * json_value_init_number(double number); +static JSON_Value * json_value_init_boolean(int boolean); +static JSON_Value * json_value_init_null(void); + +/* Parser */ +static void skip_quotes(const char **string); +static const char * get_processed_string(const char **string); +static JSON_Value * parse_object_value(const char **string, size_t nesting); +static JSON_Value * parse_array_value(const char **string, size_t nesting); +static JSON_Value * parse_string_value(const char **string); +static JSON_Value * parse_boolean_value(const char **string); +static JSON_Value * parse_number_value(const char **string); +static JSON_Value * parse_null_value(const char **string); +static JSON_Value * parse_value(const char **string, size_t nesting); + +/* Various */ +static int try_realloc(void **ptr, size_t new_size) { + void *reallocated_ptr = parson_realloc(*ptr, new_size); + if (!reallocated_ptr) { return ERROR; } + *ptr = reallocated_ptr; + return SUCCESS; +} + +static char * parson_strndup(const char *string, size_t n) { + char *output_string = (char*)parson_malloc(n + 1); + if (!output_string) { return NULL; } + output_string[n] = '\0'; + strncpy(output_string, string, n); + return output_string; +} + +static int is_utf(const char *s) { + return isxdigit(s[0]) && isxdigit(s[1]) && isxdigit(s[2]) && isxdigit(s[3]); +} + +static int is_decimal(const char *string, size_t length) { + if (length > 1 && string[0] == '0' && string[1] != '.') { return 0; } + if (length > 2 && !strncmp(string, "-0", 2) && string[2] != '.') { return 0; } + while (length--) { if (strchr("xX", string[length])) { return 0; } } + return 1; +} + +/* JSON Object */ +static JSON_Object * json_object_init(void) { + JSON_Object *new_obj = (JSON_Object*)parson_malloc(sizeof(JSON_Object)); + if (!new_obj) { return NULL; } + new_obj->names = (const char**)parson_malloc(sizeof(char*) * STARTING_CAPACITY); + if (!new_obj->names) { parson_free(new_obj); return NULL; } + new_obj->values = (JSON_Value**)parson_malloc(sizeof(JSON_Value*) * STARTING_CAPACITY); + if (!new_obj->values) { parson_free(new_obj->names); parson_free(new_obj); return NULL; } + new_obj->capacity = STARTING_CAPACITY; + new_obj->count = 0; + return new_obj; +} + +static int json_object_add(JSON_Object *object, const char *name, JSON_Value *value) { + size_t index; + if (object->count >= object->capacity) { + size_t new_capacity = object->capacity * 2; + if (new_capacity > OBJECT_MAX_CAPACITY) { return ERROR; } + if (json_object_resize(object, new_capacity) == ERROR) { return ERROR; } + } + if (json_object_get_value(object, name) != NULL) { return ERROR; } + index = object->count; + object->names[index] = parson_strndup(name, strlen(name)); + if (!object->names[index]) { return ERROR; } + object->values[index] = value; + object->count++; + return SUCCESS; +} + +static int json_object_resize(JSON_Object *object, size_t capacity) { + if (try_realloc((void**)&object->names, capacity * sizeof(char*)) == ERROR) { return ERROR; } + if (try_realloc((void**)&object->values, capacity * sizeof(JSON_Value*)) == ERROR) { return ERROR; } + object->capacity = capacity; + return SUCCESS; +} + +static JSON_Value * json_object_nget_value(const JSON_Object *object, const char *name, size_t n) { + size_t i, name_length; + for (i = 0; i < json_object_get_count(object); i++) { + name_length = strlen(object->names[i]); + if (name_length != n) { continue; } + if (strncmp(object->names[i], name, n) == 0) { return object->values[i]; } + } + return NULL; +} + +static void json_object_free(JSON_Object *object) { + while(object->count--) { + parson_free(object->names[object->count]); + json_value_free(object->values[object->count]); + } + parson_free(object->names); + parson_free(object->values); + parson_free(object); +} + +/* JSON Array */ +static JSON_Array * json_array_init(void) { + JSON_Array *new_array = (JSON_Array*)parson_malloc(sizeof(JSON_Array)); + if (!new_array) { return NULL; } + new_array->items = (JSON_Value**)parson_malloc(STARTING_CAPACITY * sizeof(JSON_Value*)); + if (!new_array->items) { parson_free(new_array); return NULL; } + new_array->capacity = STARTING_CAPACITY; + new_array->count = 0; + return new_array; +} + +static int json_array_add(JSON_Array *array, JSON_Value *value) { + if (array->count >= array->capacity) { + size_t new_capacity = array->capacity * 2; + if (new_capacity > ARRAY_MAX_CAPACITY) { return ERROR; } + if (!json_array_resize(array, new_capacity)) { return ERROR; } + } + array->items[array->count] = value; + array->count++; + return SUCCESS; +} + +static int json_array_resize(JSON_Array *array, size_t capacity) { + if (try_realloc((void**)&array->items, capacity * sizeof(JSON_Value*)) == ERROR) { return ERROR; } + array->capacity = capacity; + return SUCCESS; +} + +static void json_array_free(JSON_Array *array) { + while (array->count--) { json_value_free(array->items[array->count]); } + parson_free(array->items); + parson_free(array); +} + +/* JSON Value */ +static JSON_Value * json_value_init_object(void) { + JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); + if (!new_value) { return NULL; } + new_value->type = JSONObject; + new_value->value.object = json_object_init(); + if (!new_value->value.object) { parson_free(new_value); return NULL; } + return new_value; +} + +static JSON_Value * json_value_init_array(void) { + JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); + if (!new_value) { return NULL; } + new_value->type = JSONArray; + new_value->value.array = json_array_init(); + if (!new_value->value.array) { parson_free(new_value); return NULL; } + return new_value; +} + +static JSON_Value * json_value_init_string(const char *string) { + JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); + if (!new_value) { return NULL; } + new_value->type = JSONString; + new_value->value.string = string; + return new_value; +} + +static JSON_Value * json_value_init_number(double number) { + JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); + if (!new_value) { return NULL; } + new_value->type = JSONNumber; + new_value->value.number = number; + return new_value; +} + +static JSON_Value * json_value_init_boolean(int boolean) { + JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); + if (!new_value) { return NULL; } + new_value->type = JSONBoolean; + new_value->value.boolean = boolean; + return new_value; +} + +static JSON_Value * json_value_init_null(void) { + JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); + if (!new_value) { return NULL; } + new_value->type = JSONNull; + return new_value; +} + +/* Parser */ +static void skip_quotes(const char **string) { + skip_char(string); + while (**string != '\"') { + if (**string == '\0') { return; } + if (**string == '\\') { skip_char(string); if (**string == '\0') { return; }} + skip_char(string); + } + skip_char(string); +} + +/* Returns contents of a string inside double quotes and parses escaped + characters inside. + Example: "\u006Corem ipsum" -> lorem ipsum */ +static const char * get_processed_string(const char **string) { + const char *string_start = *string; + char *output, *processed_ptr, *unprocessed_ptr, current_char; + unsigned int utf_val; + skip_quotes(string); + if (**string == '\0') { return NULL; } + output = parson_strndup(string_start + 1, *string - string_start - 2); + if (!output) { return NULL; } + processed_ptr = unprocessed_ptr = output; + while (*unprocessed_ptr) { + current_char = *unprocessed_ptr; + if (current_char == '\\') { + unprocessed_ptr++; + current_char = *unprocessed_ptr; + switch (current_char) { + case '\"': case '\\': case '/': break; + case 'b': current_char = '\b'; break; + case 'f': current_char = '\f'; break; + case 'n': current_char = '\n'; break; + case 'r': current_char = '\r'; break; + case 't': current_char = '\t'; break; + case 'u': + unprocessed_ptr++; + if (!is_utf(unprocessed_ptr) || + sscanf(unprocessed_ptr, "%4x", &utf_val) == EOF) { + parson_free(output); return NULL; + } + if (utf_val < 0x80) { + current_char = utf_val; + } else if (utf_val < 0x800) { + *processed_ptr++ = (utf_val >> 6) | 0xC0; + current_char = ((utf_val | 0x80) & 0xBF); + } else { + *processed_ptr++ = (utf_val >> 12) | 0xE0; + *processed_ptr++ = (((utf_val >> 6) | 0x80) & 0xBF); + current_char = ((utf_val | 0x80) & 0xBF); + } + unprocessed_ptr += 3; + break; + default: + parson_free(output); + return NULL; + break; + } + } else if (iscntrl(current_char)) { /* no control characters allowed */ + parson_free(output); + return NULL; + } + *processed_ptr = current_char; + processed_ptr++; + unprocessed_ptr++; + } + *processed_ptr = '\0'; + if (try_realloc((void**)&output, strlen(output) + 1) == ERROR) { return NULL; } + return output; +} + +static JSON_Value * parse_value(const char **string, size_t nesting) { + if (nesting > MAX_NESTING) { return NULL; } + skip_whitespaces(string); + switch (**string) { + case '{': + return parse_object_value(string, nesting + 1); + case '[': + return parse_array_value(string, nesting + 1); + case '\"': + return parse_string_value(string); + case 'f': case 't': + return parse_boolean_value(string); + case '-': + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + return parse_number_value(string); + case 'n': + return parse_null_value(string); + default: + return NULL; + } +} + +static JSON_Value * parse_object_value(const char **string, size_t nesting) { + JSON_Value *output_value = json_value_init_object(), *new_value = NULL; + JSON_Object *output_object = json_value_get_object(output_value); + const char *new_key = NULL; + if (!output_value) { return NULL; } + skip_char(string); + skip_whitespaces(string); + if (**string == '}') { skip_char(string); return output_value; } /* empty object */ + while (**string != '\0') { + new_key = get_processed_string(string); + skip_whitespaces(string); + if (!new_key || **string != ':') { + json_value_free(output_value); + return NULL; + } + skip_char(string); + new_value = parse_value(string, nesting); + if (!new_value) { + parson_free(new_key); + json_value_free(output_value); + return NULL; + } + if(!json_object_add(output_object, new_key, new_value)) { + parson_free(new_key); + parson_free(new_value); + json_value_free(output_value); + return NULL; + } + parson_free(new_key); + skip_whitespaces(string); + if (**string != ',') { break; } + skip_char(string); + skip_whitespaces(string); + } + skip_whitespaces(string); + if (**string != '}' || /* Trim object after parsing is over */ + json_object_resize(output_object, json_object_get_count(output_object)) == ERROR) { + json_value_free(output_value); + return NULL; + } + skip_char(string); + return output_value; +} + +static JSON_Value * parse_array_value(const char **string, size_t nesting) { + JSON_Value *output_value = json_value_init_array(), *new_array_value = NULL; + JSON_Array *output_array = json_value_get_array(output_value); + if (!output_value) { return NULL; } + skip_char(string); + skip_whitespaces(string); + if (**string == ']') { /* empty array */ + skip_char(string); + return output_value; + } + while (**string != '\0') { + new_array_value = parse_value(string, nesting); + if (!new_array_value) { + json_value_free(output_value); + return NULL; + } + if(json_array_add(output_array, new_array_value) == ERROR) { + parson_free(new_array_value); + json_value_free(output_value); + return NULL; + } + skip_whitespaces(string); + if (**string != ',') { break; } + skip_char(string); + skip_whitespaces(string); + } + skip_whitespaces(string); + if (**string != ']' || /* Trim array after parsing is over */ + json_array_resize(output_array, json_array_get_count(output_array)) == ERROR) { + json_value_free(output_value); + return NULL; + } + skip_char(string); + return output_value; +} + +static JSON_Value * parse_string_value(const char **string) { + const char *new_string = get_processed_string(string); + if (!new_string) { return NULL; } + return json_value_init_string(new_string); +} + +static JSON_Value * parse_boolean_value(const char **string) { + size_t true_token_size = sizeof_token("true"); + size_t false_token_size = sizeof_token("false"); + if (strncmp("true", *string, true_token_size) == 0) { + *string += true_token_size; + return json_value_init_boolean(1); + } else if (strncmp("false", *string, false_token_size) == 0) { + *string += false_token_size; + return json_value_init_boolean(0); + } + return NULL; +} + +static JSON_Value * parse_number_value(const char **string) { + char *end; + double number = strtod(*string, &end); + JSON_Value *output_value; + if (is_decimal(*string, end - *string)) { + *string = end; + output_value = json_value_init_number(number); + } else { + output_value = NULL; + } + return output_value; +} + +static JSON_Value * parse_null_value(const char **string) { + size_t token_size = sizeof_token("null"); + if (strncmp("null", *string, token_size) == 0) { + *string += token_size; + return json_value_init_null(); + } + return NULL; +} + +/* Parser API */ +JSON_Value * json_parse_file(const char *filename) { + FILE *fp = fopen(filename, "r"); + size_t file_size; + char *file_contents; + JSON_Value *output_value; + if (!fp) { return NULL; } + fseek(fp, 0L, SEEK_END); + file_size = ftell(fp); + rewind(fp); + file_contents = (char*)parson_malloc(sizeof(char) * (file_size + 1)); + if (!file_contents) { fclose(fp); return NULL; } + fread(file_contents, file_size, 1, fp); + fclose(fp); + file_contents[file_size] = '\0'; + output_value = json_parse_string(file_contents); + parson_free(file_contents); + return output_value; +} + +JSON_Value * json_parse_string(const char *string) { + if (string && (*string == '{' || *string == '[')) { + return parse_value((const char**)&string, 0); + } else { + return NULL; + } +} + +/* JSON Object API */ +JSON_Value * json_object_get_value(const JSON_Object *object, const char *name) { + return json_object_nget_value(object, name, strlen(name)); +} + +const char * json_object_get_string(const JSON_Object *object, const char *name) { + return json_value_get_string(json_object_get_value(object, name)); +} + +double json_object_get_number(const JSON_Object *object, const char *name) { + return json_value_get_number(json_object_get_value(object, name)); +} + +JSON_Object * json_object_get_object(const JSON_Object *object, const char *name) { + return json_value_get_object(json_object_get_value(object, name)); +} + +JSON_Array * json_object_get_array(const JSON_Object *object, const char *name) { + return json_value_get_array(json_object_get_value(object, name)); +} + +int json_object_get_boolean(const JSON_Object *object, const char *name) { + return json_value_get_boolean(json_object_get_value(object, name)); +} + +JSON_Value * json_object_dotget_value(const JSON_Object *object, const char *name) { + const char *dot_position = strchr(name, '.'); + if (!dot_position) { return json_object_get_value(object, name); } + object = json_value_get_object(json_object_nget_value(object, name, dot_position - name)); + return json_object_dotget_value(object, dot_position + 1); +} + +const char * json_object_dotget_string(const JSON_Object *object, const char *name) { + return json_value_get_string(json_object_dotget_value(object, name)); +} + +double json_object_dotget_number(const JSON_Object *object, const char *name) { + return json_value_get_number(json_object_dotget_value(object, name)); +} + +JSON_Object * json_object_dotget_object(const JSON_Object *object, const char *name) { + return json_value_get_object(json_object_dotget_value(object, name)); +} + +JSON_Array * json_object_dotget_array(const JSON_Object *object, const char *name) { + return json_value_get_array(json_object_dotget_value(object, name)); +} + +int json_object_dotget_boolean(const JSON_Object *object, const char *name) { + return json_value_get_boolean(json_object_dotget_value(object, name)); +} + +size_t json_object_get_count(const JSON_Object *object) { + return object ? object->count : 0; +} + +const char * json_object_get_name(const JSON_Object *object, size_t index) { + if (index >= json_object_get_count(object)) { return NULL; } + return object->names[index]; +} + +/* JSON Array API */ +JSON_Value * json_array_get_value(const JSON_Array *array, size_t index) { + if (index >= json_array_get_count(array)) { return NULL; } + return array->items[index]; +} + +const char * json_array_get_string(const JSON_Array *array, size_t index) { + return json_value_get_string(json_array_get_value(array, index)); +} + +double json_array_get_number(const JSON_Array *array, size_t index) { + return json_value_get_number(json_array_get_value(array, index)); +} + +JSON_Object * json_array_get_object(const JSON_Array *array, size_t index) { + return json_value_get_object(json_array_get_value(array, index)); +} + +JSON_Array * json_array_get_array(const JSON_Array *array, size_t index) { + return json_value_get_array(json_array_get_value(array, index)); +} + +int json_array_get_boolean(const JSON_Array *array, size_t index) { + return json_value_get_boolean(json_array_get_value(array, index)); +} + +size_t json_array_get_count(const JSON_Array *array) { + return array ? array->count : 0; +} + +/* JSON Value API */ +JSON_Value_Type json_value_get_type(const JSON_Value *value) { + return value ? value->type : JSONError; +} + +JSON_Object * json_value_get_object(const JSON_Value *value) { + return json_value_get_type(value) == JSONObject ? value->value.object : NULL; +} + +JSON_Array * json_value_get_array(const JSON_Value *value) { + return json_value_get_type(value) == JSONArray ? value->value.array : NULL; +} + +const char * json_value_get_string(const JSON_Value *value) { + return json_value_get_type(value) == JSONString ? value->value.string : NULL; +} + +double json_value_get_number(const JSON_Value *value) { + return json_value_get_type(value) == JSONNumber ? value->value.number : 0; +} + +int json_value_get_boolean(const JSON_Value *value) { + return json_value_get_type(value) == JSONBoolean ? value->value.boolean : -1; +} + +void json_value_free(JSON_Value *value) { + switch (json_value_get_type(value)) { + case JSONObject: + json_object_free(value->value.object); + break; + case JSONString: + if (value->value.string) { parson_free(value->value.string); } + break; + case JSONArray: + json_array_free(value->value.array); + break; + default: + break; + } + parson_free(value); +}
View file
audiotools-2.19.tar.gz/src/parson.h
Added
@@ -0,0 +1,100 @@ +/* + Parson ( http://kgabis.github.com/parson/ ) + Copyright (c) 2012 Krzysztof Gabis + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#ifndef parson_parson_h +#define parson_parson_h + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include <stddef.h> /* size_t */ + +/* Types and enums */ +typedef struct json_object_t JSON_Object; +typedef struct json_array_t JSON_Array; +typedef struct json_value_t JSON_Value; + +typedef enum json_value_type { + JSONError = 0, + JSONNull = 1, + JSONString = 2, + JSONNumber = 3, + JSONObject = 4, + JSONArray = 5, + JSONBoolean = 6 +} JSON_Value_Type; + +/* Parses first JSON value in a file, returns NULL in case of error */ +JSON_Value * json_parse_file(const char *filename); + +/* Parses first JSON value in a string, returns NULL in case of error */ +JSON_Value * json_parse_string(const char *string); + +/* JSON Object */ +JSON_Value * json_object_get_value (const JSON_Object *object, const char *name); +const char * json_object_get_string (const JSON_Object *object, const char *name); +JSON_Object * json_object_get_object (const JSON_Object *object, const char *name); +JSON_Array * json_object_get_array (const JSON_Object *object, const char *name); +double json_object_get_number (const JSON_Object *object, const char *name); +int json_object_get_boolean(const JSON_Object *object, const char *name); + +/* dotget functions enable addressing values with dot notation in nested objects, + just like in structs or c++/java/c# objects (e.g. objectA.objectB.value). + Because valid names in JSON can contain dots, some values may be inaccessible + this way. */ +JSON_Value * json_object_dotget_value (const JSON_Object *object, const char *name); +const char * json_object_dotget_string (const JSON_Object *object, const char *name); +JSON_Object * json_object_dotget_object (const JSON_Object *object, const char *name); +JSON_Array * json_object_dotget_array (const JSON_Object *object, const char *name); +double json_object_dotget_number (const JSON_Object *object, const char *name); +int json_object_dotget_boolean(const JSON_Object *object, const char *name); + +/* Functions to get available names */ +size_t json_object_get_count(const JSON_Object *object); +const char * json_object_get_name (const JSON_Object *object, size_t index); + +/* JSON Array */ +JSON_Value * json_array_get_value (const JSON_Array *array, size_t index); +const char * json_array_get_string (const JSON_Array *array, size_t index); +JSON_Object * json_array_get_object (const JSON_Array *array, size_t index); +JSON_Array * json_array_get_array (const JSON_Array *array, size_t index); +double json_array_get_number (const JSON_Array *array, size_t index); +int json_array_get_boolean(const JSON_Array *array, size_t index); +size_t json_array_get_count (const JSON_Array *array); + +/* JSON Value */ +JSON_Value_Type json_value_get_type (const JSON_Value *value); +JSON_Object * json_value_get_object (const JSON_Value *value); +JSON_Array * json_value_get_array (const JSON_Value *value); +const char * json_value_get_string (const JSON_Value *value); +double json_value_get_number (const JSON_Value *value); +int json_value_get_boolean(const JSON_Value *value); +void json_value_free (JSON_Value *value); + +#ifdef __cplusplus +} +#endif + +#endif
View file
audiotools-2.18.tar.gz/src/pcm.c -> audiotools-2.19.tar.gz/src/pcm.c
Changed
@@ -96,14 +96,14 @@ static PySequenceMethods pcm_FrameListType_as_sequence = { (lenfunc)FrameList_len, /* sq_length */ (binaryfunc)FrameList_concat, /* sq_concat */ - (ssizeargfunc)NULL, /* sq_repeat */ + (ssizeargfunc)FrameList_repeat, /* sq_repeat */ (ssizeargfunc)FrameList_GetItem, /* sq_item */ (ssizessizeargfunc)NULL, /* sq_slice */ (ssizeobjargproc)NULL, /* sq_ass_item */ (ssizessizeobjargproc)NULL, /* sq_ass_slice */ (objobjproc)NULL, /* sq_contains */ - (binaryfunc)NULL, /* sq_inplace_concat */ - (ssizeargfunc)NULL, /* sq_inplace_repeat */ + (binaryfunc)FrameList_inplace_concat, /* sq_inplace_concat */ + (ssizeargfunc)FrameList_inplace_repeat, /* sq_inplace_repeat */ }; PyTypeObject pcm_FrameListType = { @@ -529,17 +529,96 @@ } PyObject* +FrameList_repeat(pcm_FrameList *a, Py_ssize_t i) +{ + pcm_FrameList *repeat = FrameList_create(); + Py_ssize_t j; + + repeat->frames = (unsigned int)(a->frames * i); + repeat->channels = a->channels; + repeat->bits_per_sample = a->bits_per_sample; + repeat->samples_length = (unsigned int)(a->samples_length * i); + repeat->samples = malloc(sizeof(int) * repeat->samples_length); + + for (j = 0; j < i; j++) { + memcpy(repeat->samples + (j * a->samples_length), + a->samples, + a->samples_length * sizeof(int)); + } + + return (PyObject*)repeat; +} + +PyObject* +FrameList_inplace_concat(pcm_FrameList *a, PyObject *bb) +{ + pcm_FrameList *b; + const unsigned int old_samples_length = a->samples_length; + + if (!FrameList_CheckExact(bb)) { + PyErr_SetString(PyExc_TypeError, + "can only concatenate FrameList with other FrameLists" + ); + return NULL; + } else { + b = (pcm_FrameList*)bb; + } + + if (a->channels != b->channels) { + PyErr_SetString(PyExc_ValueError, + "both FrameLists must have the same number of channels" + ); + return NULL; + } + if (a->bits_per_sample != b->bits_per_sample) { + PyErr_SetString(PyExc_ValueError, + "both FrameLists must have the same number " + "of bits per sample"); + return NULL; + } + + a->frames += b->frames; + a->samples_length += b->samples_length; + a->samples = realloc(a->samples, a->samples_length * sizeof(int)); + memcpy(a->samples + old_samples_length, + b->samples, + b->samples_length * sizeof(int)); + + Py_INCREF(a); + return (PyObject*)a; +} + +PyObject* +FrameList_inplace_repeat(pcm_FrameList *a, Py_ssize_t i) +{ + const unsigned int original_length = a->samples_length; + Py_ssize_t j; + + a->frames = (unsigned int)(a->frames * i); + a->samples_length = (unsigned int)(a->samples_length * i); + a->samples = realloc(a->samples, a->samples_length * sizeof(int)); + + for (j = 1; j < i; j++) { + memcpy(a->samples + (j * original_length), + a->samples, + original_length * sizeof(int)); + } + + Py_INCREF(a); + return (PyObject*)a; +} + +PyObject* FrameList_to_float(pcm_FrameList *self, PyObject *args) { unsigned i; - int adjustment; + const int adjustment = 1 << (self->bits_per_sample - 1); pcm_FloatFrameList *framelist = FloatFrameList_create(); framelist->frames = self->frames; framelist->channels = self->channels; framelist->samples_length = self->samples_length; framelist->samples = malloc(sizeof(double) * framelist->samples_length); - adjustment = 1 << (self->bits_per_sample - 1); for (i = 0; i < self->samples_length; i++) { framelist->samples[i] = ((double)self->samples[i]) / adjustment; } @@ -824,7 +903,17 @@ return NULL; } - +int +FrameList_converter(PyObject* obj, void** framelist) +{ + if (PyObject_TypeCheck(obj, &pcm_FrameListType)) { + *framelist = obj; + return 1; + } else { + PyErr_SetString(PyExc_TypeError, "not a FrameList object"); + return 0; + } +} /*********************** FloatFrameList Object @@ -856,14 +945,14 @@ static PySequenceMethods pcm_FloatFrameListType_as_sequence = { (lenfunc)FloatFrameList_len, /* sq_length */ (binaryfunc)FloatFrameList_concat, /* sq_concat */ - (ssizeargfunc)NULL, /* sq_repeat */ + (ssizeargfunc)FloatFrameList_repeat, /* sq_repeat */ (ssizeargfunc)FloatFrameList_GetItem, /* sq_item */ (ssizessizeargfunc)NULL, /* sq_slice */ (ssizeobjargproc)NULL, /* sq_ass_item */ (ssizessizeobjargproc)NULL, /* sq_ass_slice */ (objobjproc)NULL, /* sq_contains */ - (binaryfunc)NULL, /* sq_inplace_concat */ - (ssizeargfunc)NULL, /* sq_inplace_repeat */ + (binaryfunc)FloatFrameList_inplace_concat, /* sq_inplace_concat */ + (ssizeargfunc)FloatFrameList_inplace_repeat, /* sq_inplace_repeat */ }; PyTypeObject pcm_FloatFrameListType = { @@ -1210,6 +1299,80 @@ return NULL; } + +PyObject* +FloatFrameList_repeat(pcm_FloatFrameList *a, Py_ssize_t i) +{ + pcm_FloatFrameList *repeat = FloatFrameList_create(); + Py_ssize_t j; + + repeat->frames = (unsigned int)(a->frames * i); + repeat->channels = a->channels; + repeat->samples_length = (unsigned int)(a->samples_length * i); + repeat->samples = malloc(sizeof(double) * repeat->samples_length); + + for (j = 0; j < i; j++) { + memcpy(repeat->samples + (j * a->samples_length), + a->samples, + a->samples_length * sizeof(double)); + } + + return (PyObject*)repeat; +} + +PyObject* +FloatFrameList_inplace_concat(pcm_FloatFrameList *a, PyObject *bb) +{ + pcm_FloatFrameList *b; + const unsigned int old_samples_length = a->samples_length; + + if (!FloatFrameList_CheckExact(bb)) { + PyErr_SetString(PyExc_TypeError, + "can only concatenate FloatFrameList " + "with other FloatFrameLists"); + return NULL; + } else { + b = (pcm_FloatFrameList*)bb; + } + + if (a->channels != b->channels) { + PyErr_SetString(PyExc_ValueError, + "both FloatFrameLists must have the same " + "number of channels"); + return NULL; + } + + a->frames += b->frames; + a->samples_length += b->samples_length; + a->samples = realloc(a->samples, a->samples_length * sizeof(double)); + memcpy(a->samples + old_samples_length, + b->samples, + b->samples_length * sizeof(double)); + + Py_INCREF(a); + return (PyObject*)a; +} + +PyObject* +FloatFrameList_inplace_repeat(pcm_FloatFrameList *a, Py_ssize_t i) +{ + const unsigned int original_length = a->samples_length; + Py_ssize_t j; + + a->frames = (unsigned int)(a->frames * i); + a->samples_length = (unsigned int)(a->samples_length * i); + a->samples = realloc(a->samples, a->samples_length * sizeof(double)); + + for (j = 1; j < i; j++) { + memcpy(a->samples + (j * original_length), + a->samples, + original_length * sizeof(double)); + } + + Py_INCREF(a); + return (PyObject*)a; +} + PyObject* FloatFrameList_from_frames(PyObject *dummy, PyObject *args) { @@ -1369,6 +1532,17 @@ return NULL; } +int +FloatFrameList_converter(PyObject* obj, void** floatframelist) +{ + if (PyObject_TypeCheck(obj, &pcm_FloatFrameListType)) { + *floatframelist = obj; + return 1; + } else { + PyErr_SetString(PyExc_TypeError, "not a FloatFrameList object"); + return 0; + } +} PyMODINIT_FUNC initpcm(void)
View file
audiotools-2.18.tar.gz/src/pcm.h -> audiotools-2.19.tar.gz/src/pcm.h
Changed
@@ -117,6 +117,15 @@ FrameList_concat(pcm_FrameList *a, PyObject *bb); PyObject* +FrameList_repeat(pcm_FrameList *a, Py_ssize_t i); + +PyObject* +FrameList_inplace_concat(pcm_FrameList *a, PyObject *bb); + +PyObject* +FrameList_inplace_repeat(pcm_FrameList *a, Py_ssize_t i); + +PyObject* FrameList_from_list(PyObject *dummy, PyObject *args); PyObject* @@ -125,6 +134,10 @@ PyObject* FrameList_from_channels(PyObject *dummy, PyObject *args); +/*for use with the PyArg_ParseTuple function*/ +int +FrameList_converter(PyObject* obj, void** framelist); + /*********************** FloatFrameList Object @@ -191,11 +204,24 @@ FloatFrameList_concat(pcm_FloatFrameList *a, PyObject *bb); PyObject* +FloatFrameList_repeat(pcm_FloatFrameList *a, Py_ssize_t i); + +PyObject* +FloatFrameList_inplace_concat(pcm_FloatFrameList *a, PyObject *bb); + +PyObject* +FloatFrameList_inplace_repeat(pcm_FloatFrameList *a, Py_ssize_t i); + +PyObject* FloatFrameList_from_frames(PyObject *dummy, PyObject *args); PyObject* FloatFrameList_from_channels(PyObject *dummy, PyObject *args); +/*for use with the PyArg_ParseTuple function*/ +int +FloatFrameList_converter(PyObject* obj, void** floatframelist); + #endif typedef int (*FrameList_char_to_int_converter)(unsigned char *s);
View file
audiotools-2.18.tar.gz/src/pcmconv.c -> audiotools-2.19.tar.gz/src/pcmconv.c
Changed
@@ -130,7 +130,8 @@ } } -struct pcmreader_s* open_pcmreader(PyObject* pcmreader_obj) +struct pcmreader_s* +open_pcmreader(PyObject* pcmreader_obj) { struct pcmreader_s* pcmreader = malloc(sizeof(struct pcmreader_s)); PyObject* attr; @@ -209,6 +210,18 @@ return NULL; } +int +pcmreader_converter(PyObject* obj, void** pcm_reader) +{ + pcmreader* pcmreader_s = open_pcmreader(obj); + if (pcmreader_s != NULL) { + *pcm_reader = pcmreader_s; + return 1; + } else { + return 0; + } +} + int pcmreader_read(struct pcmreader_s* reader, unsigned pcm_frames, array_ia* channels) @@ -224,13 +237,11 @@ unsigned char* string; Py_ssize_t string_length; - /*make a call to "pcmreader.read(bytes)" - where "bytes" is set to the proper PCM frame count*/ + /*make a call to "pcmreader.read(pcm_frames)" + where "pcm_frames" is set to the proper PCM frame count*/ if (((framelist_obj = PyObject_CallMethod(reader->pcmreader_obj, "read", "i", - pcm_frames * - reader->channels * - reader->bytes_per_sample))) == NULL) { + (int)pcm_frames))) == NULL) { /*ensure result isn't an exception*/ return 1; } @@ -249,8 +260,8 @@ channels->reset(channels); for (channel = 0; channel < framelist->channels; channel++) { channel_a = channels->append(channels); + channel_a->resize(channel_a, framelist->frames); for (frame = 0; frame < framelist->frames; frame++) { - channel_a->resize(channel_a, framelist->frames); a_append(channel_a, framelist->samples[(frame * framelist->channels) + channel]); @@ -293,7 +304,13 @@ void pcmreader_close(struct pcmreader_s* reader) { - /*FIXME*/ + PyObject* result = PyObject_CallMethod(reader->pcmreader_obj, + "close", NULL); + if (result != NULL) { + Py_DECREF(result); + } else { + PyErr_Clear(); + } } void pcmreader_del(struct pcmreader_s* reader) @@ -341,6 +358,8 @@ pcmreader->buffer_size = 1; pcmreader->buffer = malloc(pcmreader->buffer_size); + pcmreader->callback_buffer_size = 1; + pcmreader->callback_buffer = malloc(pcmreader->callback_buffer_size); pcmreader->buffer_converter = FrameList_get_char_to_int_converter(pcmreader->bits_per_sample, pcmreader->big_endian, @@ -375,7 +394,7 @@ struct pcmreader_callback *callback; FrameList_int_to_char_converter callback_converter; - uint8_t* callback_buffer; + if (reader->buffer_size < bytes_to_read) { reader->buffer_size = bytes_to_read; @@ -411,23 +430,25 @@ for (callback = reader->callbacks; callback != NULL; callback = callback->next) { + if (reader->callback_buffer_size < bytes_read) { + reader->callback_buffer_size = bytes_read; + reader->callback_buffer = realloc(reader->callback_buffer, + bytes_read); + } + callback_converter = FrameList_get_int_to_char_converter(reader->bits_per_sample, !callback->little_endian, callback->is_signed); - callback_buffer = malloc(bytes_read); - for (byte = 0; byte < bytes_read; byte += reader->bytes_per_sample) { callback_converter(reader->buffer_converter(reader->buffer + byte), - callback_buffer + byte); + reader->callback_buffer + byte); } callback->callback(callback->user_data, - (unsigned char*)callback_buffer, + (unsigned char*)reader->callback_buffer, (unsigned long)bytes_read); - - free(callback_buffer); } return 0; @@ -451,6 +472,7 @@ /*free temporary buffer*/ free(reader->buffer); + free(reader->callback_buffer); /*free pcmreader struct*/ free(reader);
View file
audiotools-2.18.tar.gz/src/pcmconv.h -> audiotools-2.19.tar.gz/src/pcmconv.h
Changed
@@ -76,8 +76,16 @@ or returns NULL with an exception set if an error occurs during the wrapping procedure - the object should be deallocated with reader->del(reader) when finished*/ -struct pcmreader_s* open_pcmreader(PyObject* pcmreader); + the object should be deallocated with reader->del(reader) when finished + + Python object is INCREFed by this function + and DECREFed by del() once no longer in use*/ +struct pcmreader_s* +open_pcmreader(PyObject* pcmreader); + +/*for use with the PyArg_ParseTuple function*/ +int +pcmreader_converter(PyObject* obj, void** pcmreader); typedef struct pcmreader_s { PyObject* pcmreader_obj; @@ -97,8 +105,8 @@ returns 0 on success, 1 if there's an exception during reading*/ int (*read)(struct pcmreader_s* reader, - unsigned pcm_frames, - array_ia* channels); + unsigned pcm_frames, + array_ia* channels); /*forwards a call to "close" to the wrapped PCMReader object*/ void (*close)(struct pcmreader_s* reader); @@ -151,6 +159,8 @@ unsigned buffer_size; uint8_t* buffer; + unsigned callback_buffer_size; + uint8_t* callback_buffer; FrameList_char_to_int_converter buffer_converter; struct pcmreader_callback* callbacks;
View file
audiotools-2.19.tar.gz/src/pcmconverter.c
Added
@@ -0,0 +1,775 @@ +#include <Python.h> +#include "pcmconv.h" +#include "array.h" +#include "bitstream.h" +#include "samplerate/samplerate.h" +#include "pcmconverter.h" +#include "dither.c" + +/******************************************************** + Audio Tools, a module and set of tools for manipulating audio data + Copyright (C) 2007-2012 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 +*******************************************************/ + +#ifndef MIN +#define MIN(x, y) ((x) < (y) ? (x) : (y)) +#endif +#ifndef MAX +#define MAX(x, y) ((x) > (y) ? (x) : (y)) +#endif + +/******************************************************* + Averager for reducing channel count from many to 1 +*******************************************************/ + +static PyObject* +Averager_sample_rate(pcmconverter_Averager *self, void *closure) +{ + return Py_BuildValue("I", self->pcmreader->sample_rate); +} + +static PyObject* +Averager_bits_per_sample(pcmconverter_Averager *self, void *closure) +{ + return Py_BuildValue("I", self->pcmreader->bits_per_sample); +} + +static PyObject* +Averager_channels(pcmconverter_Averager *self, void *closure) +{ + return Py_BuildValue("i", 1); +} + +static PyObject* +Averager_channel_mask(pcmconverter_Averager *self, void *closure) +{ + return Py_BuildValue("i", 0x4); +} + +static PyObject* +Averager_read(pcmconverter_Averager *self, PyObject *args) +{ + if (self->pcmreader->read(self->pcmreader, + 4096, + self->input_channels)) { + /*error occured during call to .read()*/ + return NULL; + } else { + unsigned c; + unsigned i; + array_ia* input = self->input_channels; + array_i* output = self->output_channel; + const unsigned frame_count = input->_[0]->len; + const unsigned channel_count = input->len; + PyThreadState *thread_state = PyEval_SaveThread(); + + output->reset(output); + output->resize(output, frame_count); + for (i = 0; i < frame_count; i++) { + int64_t accumulator = 0; + for (c = 0; c < channel_count; c++) { + accumulator += input->_[c]->_[i]; + } + a_append(output, (int)(accumulator / channel_count)); + } + + PyEval_RestoreThread(thread_state); + return array_i_to_FrameList(self->audiotools_pcm, + output, + 1, + self->pcmreader->bits_per_sample); + } +} + +static PyObject* +Averager_close(pcmconverter_Averager *self, PyObject *args) +{ + self->pcmreader->close(self->pcmreader); + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject* +Averager_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + pcmconverter_Averager *self; + + self = (pcmconverter_Averager *)type->tp_alloc(type, 0); + + return (PyObject *)self; +} + +void +Averager_dealloc(pcmconverter_Averager *self) +{ + if (self->pcmreader != NULL) + self->pcmreader->del(self->pcmreader); + self->input_channels->del(self->input_channels); + self->output_channel->del(self->output_channel); + Py_XDECREF(self->audiotools_pcm); + + self->ob_type->tp_free((PyObject*)self); +} + +int +Averager_init(pcmconverter_Averager *self, PyObject *args, PyObject *kwds) +{ + self->pcmreader = NULL; + self->input_channels = array_ia_new(); + self->output_channel = array_i_new(); + self->audiotools_pcm = NULL; + + if (!PyArg_ParseTuple(args, "O&", pcmreader_converter, + &(self->pcmreader))) + return -1; + + if ((self->audiotools_pcm = open_audiotools_pcm()) == NULL) + return -1; + + return 0; +} + + +/******************************************************* + Downmixer for reducing channel count from many to 2 +*******************************************************/ + +PyObject* +Downmixer_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + pcmconverter_Downmixer *self; + + self = (pcmconverter_Downmixer *)type->tp_alloc(type, 0); + + return (PyObject *)self; +} + +void +Downmixer_dealloc(pcmconverter_Downmixer *self) +{ + if (self->pcmreader != NULL) + self->pcmreader->del(self->pcmreader); + self->input_channels->del(self->input_channels); + self->empty_channel->del(self->empty_channel); + self->six_channels->del(self->six_channels); + self->output_channels->del(self->output_channels); + Py_XDECREF(self->audiotools_pcm); + + self->ob_type->tp_free((PyObject*)self); +} + +int +Downmixer_init(pcmconverter_Downmixer *self, PyObject *args, PyObject *kwds) +{ + self->pcmreader = NULL; + self->input_channels = array_ia_new(); + self->empty_channel = array_i_new(); + self->six_channels = array_lia_new(); + self->output_channels = array_ia_new(); + self->audiotools_pcm = NULL; + + if (!PyArg_ParseTuple(args, "O&", pcmreader_converter, + &(self->pcmreader))) + return -1; + + if ((self->audiotools_pcm = open_audiotools_pcm()) == NULL) + return -1; + + return 0; +} + +static PyObject* +Downmixer_sample_rate(pcmconverter_Downmixer *self, void *closure) +{ + return Py_BuildValue("I", self->pcmreader->sample_rate); +} + +static PyObject* +Downmixer_bits_per_sample(pcmconverter_Downmixer *self, void *closure) +{ + return Py_BuildValue("I", self->pcmreader->bits_per_sample); +} + +static PyObject* +Downmixer_channels(pcmconverter_Downmixer *self, void *closure) +{ + return Py_BuildValue("I", 2); +} + +static PyObject* +Downmixer_channel_mask(pcmconverter_Downmixer *self, void *closure) +{ + return Py_BuildValue("I", 0x3); +} + +static PyObject* +Downmixer_read(pcmconverter_Downmixer *self, PyObject *args) +{ + if (self->pcmreader->read(self->pcmreader, + 4096, + self->input_channels)) { + /*error occured during call to .read()*/ + return NULL; + } else { + unsigned i; + const unsigned frame_count = self->input_channels->_[0]->len; + unsigned input_mask; + unsigned mask; + unsigned channel = 0; + array_i* left; + array_i* right; + const double REAR_GAIN = 0.6; + const double CENTER_GAIN = 0.7; + const int SAMPLE_MIN = + -(1 << (self->pcmreader->bits_per_sample - 1)); + const int SAMPLE_MAX = + (1 << (self->pcmreader->bits_per_sample - 1)) - 1; + PyThreadState *thread_state = PyEval_SaveThread(); + + /*setup intermediate arrays*/ + if (self->empty_channel->len != frame_count) + self->empty_channel->mset(self->empty_channel, frame_count, 0); + self->six_channels->reset(self->six_channels); + + /*ensure PCMReader's channel mask is defined*/ + if (self->pcmreader->channel_mask != 0) { + input_mask = self->pcmreader->channel_mask; + } else { + /*invent channel mask for input based on channel count*/ + switch (self->pcmreader->channels) { + case 0: + input_mask = 0x0; + break; + case 1: + /*fC*/ + input_mask = 0x4; + break; + case 2: + /*fL, fR*/ + input_mask = 0x3; + break; + case 3: + /*fL, fR, fC*/ + input_mask = 0x7; + break; + case 4: + /*fL, fR, bL, bR*/ + input_mask = 0x33; + break; + case 5: + /*fL, fR, fC, bL, bR*/ + input_mask = 0x37; + break; + case 6: + /*fL, fR, fC, LFE, bL, bR*/ + input_mask = 0x3F; + break; + default: + /*more than 6 channels + fL, fR, fC, LFE, bL, bR, ...*/ + input_mask = 0x3F; + break; + } + } + + /*split pcm.FrameList into 6 channels*/ + for (mask = 1; mask <= 0x20; mask <<= 1) { + if (mask & input_mask) { + /*channel exists in PCMReader object*/ + self->input_channels->_[channel]->link( + self->input_channels->_[channel], + self->six_channels->append(self->six_channels)); + + channel++; + } else { + /*PCMReader object doesn't contain that channel + so pad with a channel of empty samples*/ + self->empty_channel->link( + self->empty_channel, + self->six_channels->append(self->six_channels)); + } + } + + /*reset output and perform downmixing across 6 channels*/ + self->output_channels->reset(self->output_channels); + left = self->output_channels->append(self->output_channels); + left->resize(left, frame_count); + right = self->output_channels->append(self->output_channels); + right->resize(right, frame_count); + + for (i = 0; i < frame_count; i++) { + /*bM (back mono) = 0.7 * (bL + bR)*/ + const double mono_rear = 0.7 * (self->six_channels->_[4]->_[i] + + self->six_channels->_[5]->_[i]); + + /*left = fL + rear_gain * bM + center_gain * fC*/ + const int left_i = (int)round( + self->six_channels->_[0]->_[i] + + REAR_GAIN * mono_rear + + CENTER_GAIN * self->six_channels->_[2]->_[i]); + + /*right = fR - rear_gain * bM + center_gain * fC*/ + const int right_i = (int)round( + self->six_channels->_[1]->_[i] - + REAR_GAIN * mono_rear + + CENTER_GAIN * self->six_channels->_[2]->_[i]); + + a_append(left, MAX(MIN(left_i, SAMPLE_MAX), SAMPLE_MIN)); + a_append(right, MAX(MIN(right_i, SAMPLE_MAX), SAMPLE_MIN)); + } + + PyEval_RestoreThread(thread_state); + /*convert output to pcm.FrameList object and return it*/ + return array_ia_to_FrameList(self->audiotools_pcm, + self->output_channels, + self->pcmreader->bits_per_sample); + } +} + +static PyObject* +Downmixer_close(pcmconverter_Downmixer *self, PyObject *args) +{ + self->pcmreader->close(self->pcmreader); + + Py_INCREF(Py_None); + return Py_None; +} + + +/******************************************************* + Resampler for changing a PCMReader's sample rate +*******************************************************/ + +static PyObject* +Resampler_sample_rate(pcmconverter_Resampler *self, void *closure) +{ + return Py_BuildValue("i", self->sample_rate); +} + +static PyObject* +Resampler_bits_per_sample(pcmconverter_Resampler *self, void *closure) +{ + return Py_BuildValue("I", self->pcmreader->bits_per_sample); +} + +static PyObject* +Resampler_channels(pcmconverter_Resampler *self, void *closure) +{ + return Py_BuildValue("I", self->pcmreader->channels); +} + +static PyObject* +Resampler_channel_mask(pcmconverter_Resampler *self, void *closure) +{ + return Py_BuildValue("I", self->pcmreader->channel_mask); +} + +#define OUTPUT_SAMPLES_LENGTH 0x100000 + +static PyObject* +Resampler_read(pcmconverter_Resampler *self, PyObject *args) +{ + /*read FrameList from PCMReader*/ + if (self->pcmreader->read(self->pcmreader, + 4096, + self->input_channels)) { + /*error reading from pcmreader*/ + return NULL; + } else { + const unsigned input_frame_count = self->input_channels->_[0]->len; + const unsigned channels = self->pcmreader->channels; + const unsigned quantization = + (1 << (self->pcmreader->bits_per_sample - 1)); + const int max_sample = + (1 << (self->pcmreader->bits_per_sample - 1)) - 1; + const int min_sample = + -(1 << (self->pcmreader->bits_per_sample - 1)); + SRC_DATA src_data; + int processing_error; + static float data_out[OUTPUT_SAMPLES_LENGTH]; + unsigned s = 0; + unsigned c; + unsigned i; + PyThreadState *thread_state = PyEval_SaveThread(); + + /*populate SRC_DATA from unprocessed samples and FrameList*/ + src_data.input_frames = (self->unprocessed_frame_count + + input_frame_count); + src_data.data_in = malloc(src_data.input_frames * + self->pcmreader->channels * + sizeof(float)); + src_data.output_frames = (OUTPUT_SAMPLES_LENGTH / + self->pcmreader->channels); + src_data.data_out = data_out; + src_data.end_of_input = (input_frame_count == 0); + src_data.src_ratio = self->ratio; + + /*first append the unprocessed samples*/ + for (i = 0; i < self->unprocessed_samples->len; i++) { + src_data.data_in[s++] = (float)(self->unprocessed_samples->_[i]); + } + /*then append the new input samples*/ + for (i = 0; i < input_frame_count; i++) { + for (c = 0; c < channels; c++) + src_data.data_in[s++] = + ((float)self->input_channels->_[c]->_[i] / + quantization); + } + + /*run src_process() on self->SRC_STATE and SRC_DATA*/ + if ((processing_error = src_process(self->src_state, &src_data)) != 0) { + /*some sort of processing error raises ValueError*/ + PyEval_RestoreThread(thread_state); + PyErr_SetString(PyExc_ValueError, + src_strerror(processing_error)); + free(src_data.data_in); + return NULL; + } else { + const unsigned processed_sample_count = + (unsigned)(src_data.output_frames_gen * + self->pcmreader->channels); + array_f* unprocessed_samples = self->unprocessed_samples; + array_i* processed_samples = self->processed_samples; + int i; + + /*save unprocessed samples for next run*/ + self->unprocessed_frame_count = (unsigned)( + src_data.input_frames - + src_data.input_frames_used); + unprocessed_samples->reset(unprocessed_samples); + unprocessed_samples->resize(unprocessed_samples, + self->unprocessed_frame_count * + self->pcmreader->channels);; + for (i = (unsigned)(src_data.input_frames_used * + self->pcmreader->channels); + i < (src_data.input_frames * self->pcmreader->channels); + i++) { + a_append(unprocessed_samples, src_data.data_in[i]); + } + + /*convert processed samples to integer array*/ + processed_samples->reset(processed_samples); + processed_samples->resize(processed_samples, + processed_sample_count); + for (i = 0; i < processed_sample_count; i++) { + const int sample = (int)(src_data.data_out[i] * quantization); + a_append(processed_samples, + MAX(MIN(sample, max_sample), min_sample)); + } + + /*return FrameList*/ + free(src_data.data_in); + PyEval_RestoreThread(thread_state); + return array_i_to_FrameList(self->audiotools_pcm, + processed_samples, + self->pcmreader->channels, + self->pcmreader->bits_per_sample); + } + } +} + +static PyObject* +Resampler_close(pcmconverter_Resampler *self, PyObject *args) +{ + self->pcmreader->close(self->pcmreader); + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject* +Resampler_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + pcmconverter_Resampler *self; + + self = (pcmconverter_Resampler *)type->tp_alloc(type, 0); + + return (PyObject *)self; +} + +void +Resampler_dealloc(pcmconverter_Resampler *self) +{ + if (self->pcmreader != NULL) + self->pcmreader->del(self->pcmreader); + self->input_channels->del(self->input_channels); + src_delete(self->src_state); + self->unprocessed_samples->del(self->unprocessed_samples); + self->processed_samples->del(self->processed_samples); + Py_XDECREF(self->audiotools_pcm); + + self->ob_type->tp_free((PyObject*)self); +} + +int +Resampler_init(pcmconverter_Resampler *self, PyObject *args, PyObject *kwds) +{ + int error; + + self->pcmreader = NULL; + self->input_channels = array_ia_new(); + self->unprocessed_samples = array_f_new(); + self->processed_samples = array_i_new(); + self->audiotools_pcm = NULL; + + if (!PyArg_ParseTuple(args, "O&i", pcmreader_converter, + &(self->pcmreader), + &(self->sample_rate))) + return -1; + + if (self->sample_rate <= 0) { + PyErr_SetString(PyExc_ValueError, + "new sample rate must be positive"); + return -1; + } + + self->src_state = src_new(0, self->pcmreader->channels, &error); + self->ratio = ((double)self->sample_rate / + (double)self->pcmreader->sample_rate); + + if ((self->audiotools_pcm = open_audiotools_pcm()) == NULL) + return -1; + + return 0; +} + +static int +read_os_random(PyObject* os_module, + struct bs_buffer* buffer); + +static void +close_os_random(PyObject* os_module); + +static void +free_os_random(PyObject* os_module); + +static PyObject* +BPSConverter_sample_rate(pcmconverter_BPSConverter *self, void *closure) +{ + return Py_BuildValue("i", self->pcmreader->sample_rate); +} + +static PyObject* +BPSConverter_bits_per_sample(pcmconverter_BPSConverter *self, void *closure) +{ + return Py_BuildValue("i", self->bits_per_sample); +} + +static PyObject* +BPSConverter_channels(pcmconverter_BPSConverter *self, void *closure) +{ + return Py_BuildValue("i", self->pcmreader->channels); +} + +static PyObject* +BPSConverter_channel_mask(pcmconverter_BPSConverter *self, void *closure) +{ + return Py_BuildValue("i", self->pcmreader->channel_mask); +} + +static PyObject* +BPSConverter_read(pcmconverter_BPSConverter *self, PyObject *args) +{ + /*read FrameList from PCMReader*/ + if (self->pcmreader->read(self->pcmreader, + 4096, + self->input_channels)) { + return NULL; + } else { + /*convert old bits-per-sample to new bits-per-sample using shifts*/ + if (self->bits_per_sample < self->pcmreader->bits_per_sample) { + BitstreamReader* white_noise = self->white_noise; + + /*decreasing bits-per-sample is a right shift*/ + if (!setjmp(*br_try(white_noise))) { + const unsigned shift = + self->pcmreader->bits_per_sample - self->bits_per_sample; + unsigned c; + + self->output_channels->reset(self->output_channels); + + for (c = 0; c < self->input_channels->len; c++) { + array_i* input_channel = + self->input_channels->_[c]; + array_i* output_channel = + self->output_channels->append(self->output_channels); + unsigned i; + + output_channel->resize(output_channel, input_channel->len); + for (i = 0; i < input_channel->len; i++) { + /*and add apply white noise dither + taken from os.random()*/ + a_append(output_channel, + (input_channel->_[i] >> shift) ^ + white_noise->read(white_noise, 1)); + } + } + + br_etry(white_noise); + return array_ia_to_FrameList(self->audiotools_pcm, + self->output_channels, + self->bits_per_sample); + } else { + /*I/O error reading white noise from os.random()*/ + br_etry(white_noise); + PyErr_SetString( + PyExc_IOError, + "I/O error reading dither data from os.urandom"); + return NULL; + } + } else if (self->bits_per_sample > self->pcmreader->bits_per_sample) { + /*increasing bits-per-sample is a simple left shift*/ + const unsigned shift = + self->bits_per_sample - self->pcmreader->bits_per_sample; + unsigned c; + + self->output_channels->reset(self->output_channels); + + for (c = 0; c < self->input_channels->len; c++) { + array_i* input_channel = + self->input_channels->_[c]; + array_i* output_channel = + self->output_channels->append(self->output_channels); + unsigned i; + + output_channel->resize(output_channel, input_channel->len); + for (i = 0; i < input_channel->len; i++) { + a_append(output_channel, input_channel->_[i] << shift); + } + } + + return array_ia_to_FrameList(self->audiotools_pcm, + self->output_channels, + self->bits_per_sample); + } else { + /*leaving bits-per-sample unchanged returns FrameList as-is*/ + return array_ia_to_FrameList(self->audiotools_pcm, + self->input_channels, + self->bits_per_sample); + } + } +} + +static PyObject* +BPSConverter_close(pcmconverter_BPSConverter *self, PyObject *args) +{ + self->pcmreader->close(self->pcmreader); + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject* +BPSConverter_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + pcmconverter_BPSConverter *self; + + self = (pcmconverter_BPSConverter *)type->tp_alloc(type, 0); + + return (PyObject *)self; +} + +void +BPSConverter_dealloc(pcmconverter_BPSConverter *self) +{ + if (self->pcmreader != NULL) + self->pcmreader->del(self->pcmreader); + self->input_channels->del(self->input_channels); + self->output_channels->del(self->output_channels); + Py_XDECREF(self->audiotools_pcm); + if (self->white_noise != NULL) + self->white_noise->close(self->white_noise); + + self->ob_type->tp_free((PyObject*)self); +} + +int +BPSConverter_init(pcmconverter_BPSConverter *self, + PyObject *args, PyObject *kwds) +{ + self->pcmreader = NULL; + self->input_channels = array_ia_new(); + self->output_channels = array_ia_new(); + self->audiotools_pcm = NULL; + self->white_noise = NULL; + + if (!PyArg_ParseTuple(args, "O&i", pcmreader_converter, + &(self->pcmreader), + &(self->bits_per_sample))) + return -1; + + /*ensure bits per sample is supported*/ + switch (self->bits_per_sample) { + case 8: + case 16: + case 24: + break; + default: + PyErr_SetString(PyExc_ValueError, + "new bits per sample must be 8, 16 or 24"); + return -1; + } + + if ((self->audiotools_pcm = open_audiotools_pcm()) == NULL) + return -1; + + if ((self->white_noise = open_dither()) == NULL) + return -1; + + return 0; +} + +PyMODINIT_FUNC +initpcmconverter(void) +{ + PyObject* m; + + pcmconverter_AveragerType.tp_new = PyType_GenericNew; + if (PyType_Ready(&pcmconverter_AveragerType) < 0) + return; + + pcmconverter_DownmixerType.tp_new = PyType_GenericNew; + if (PyType_Ready(&pcmconverter_DownmixerType) < 0) + return; + + pcmconverter_ResamplerType.tp_new = PyType_GenericNew; + if (PyType_Ready(&pcmconverter_ResamplerType) < 0) + return; + + pcmconverter_BPSConverterType.tp_new = PyType_GenericNew; + if (PyType_Ready(&pcmconverter_BPSConverterType) < 0) + return; + + m = Py_InitModule3("pcmconverter", module_methods, + "A PCM stream conversion module"); + + Py_INCREF(&pcmconverter_AveragerType); + PyModule_AddObject(m, "Averager", + (PyObject *)&pcmconverter_AveragerType); + + Py_INCREF(&pcmconverter_DownmixerType); + PyModule_AddObject(m, "Downmixer", + (PyObject *)&pcmconverter_DownmixerType); + + Py_INCREF(&pcmconverter_ResamplerType); + PyModule_AddObject(m, "Resampler", + (PyObject *)&pcmconverter_ResamplerType); + + Py_INCREF(&pcmconverter_BPSConverterType); + PyModule_AddObject(m, "BPSConverter", + (PyObject *)&pcmconverter_BPSConverterType); +} + +#include "samplerate/samplerate.c"
View file
audiotools-2.19.tar.gz/src/pcmconverter.h
Added
@@ -0,0 +1,405 @@ +/******************************************************** + Audio Tools, a module and set of tools for manipulating audio data + Copyright (C) 2007-2012 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 +*******************************************************/ + +PyMethodDef module_methods[] = { + {NULL} +}; + +typedef struct { + PyObject_HEAD + + struct pcmreader_s* pcmreader; + array_ia* input_channels; + array_i* output_channel; + PyObject* audiotools_pcm; +} pcmconverter_Averager; + +static PyObject* +Averager_sample_rate(pcmconverter_Averager *self, void *closure); + +static PyObject* +Averager_bits_per_sample(pcmconverter_Averager *self, void *closure); + +static PyObject* +Averager_channels(pcmconverter_Averager *self, void *closure); + +static PyObject* +Averager_channel_mask(pcmconverter_Averager *self, void *closure); + +static PyObject* +Averager_read(pcmconverter_Averager *self, PyObject *args); + +static PyObject* +Averager_close(pcmconverter_Averager *self, PyObject *args); + +static PyObject* +Averager_new(PyTypeObject *type, PyObject *args, PyObject *kwds); + +void +Averager_dealloc(pcmconverter_Averager *self); + +int +Averager_init(pcmconverter_Averager *self, PyObject *args, PyObject *kwds); + +PyGetSetDef Averager_getseters[] = { + {"sample_rate", (getter)Averager_sample_rate, NULL, "sample rate", NULL}, + {"bits_per_sample", (getter)Averager_bits_per_sample, NULL, "bits per sample", NULL}, + {"channels", (getter)Averager_channels, NULL, "channels", NULL}, + {"channel_mask", (getter)Averager_channel_mask, NULL, "channel_mask", NULL}, + {NULL} +}; + +PyMethodDef Averager_methods[] = { + {"read", (PyCFunction)Averager_read, METH_VARARGS, ""}, + {"close", (PyCFunction)Averager_close, METH_NOARGS, ""}, + {NULL} +}; + +PyTypeObject pcmconverter_AveragerType = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "pcmconverter.Averager", /*tp_name*/ + sizeof(pcmconverter_Averager),/*tp_basicsize*/ + 0, /*tp_itemsize*/ + (destructor)Averager_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash */ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + "Averager objects", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + Averager_methods, /* tp_methods */ + 0, /* tp_members */ + Averager_getseters, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)Averager_init, /* tp_init */ + 0, /* tp_alloc */ + Averager_new, /* tp_new */ +}; + + +typedef struct { + PyObject_HEAD + + struct pcmreader_s* pcmreader; + array_ia* input_channels; + array_i* empty_channel; + array_lia* six_channels; + array_ia* output_channels; + PyObject* audiotools_pcm; +} pcmconverter_Downmixer; + +static PyObject* +Downmixer_sample_rate(pcmconverter_Downmixer *self, void *closure); + +static PyObject* +Downmixer_bits_per_sample(pcmconverter_Downmixer *self, void *closure); + +static PyObject* +Downmixer_channels(pcmconverter_Downmixer *self, void *closure); + +static PyObject* +Downmixer_channel_mask(pcmconverter_Downmixer *self, void *closure); + +static PyObject* +Downmixer_read(pcmconverter_Downmixer *self, PyObject *args); + +static PyObject* +Downmixer_close(pcmconverter_Downmixer *self, PyObject *args); + +static PyObject* +Downmixer_new(PyTypeObject *type, PyObject *args, PyObject *kwds); + +void +Downmixer_dealloc(pcmconverter_Downmixer *self); + +int +Downmixer_init(pcmconverter_Downmixer *self, PyObject *args, PyObject *kwds); + +PyGetSetDef Downmixer_getseters[] = { + {"sample_rate", (getter)Downmixer_sample_rate, NULL, "sample rate", NULL}, + {"bits_per_sample", (getter)Downmixer_bits_per_sample, NULL, "bits per sample", NULL}, + {"channels", (getter)Downmixer_channels, NULL, "channels", NULL}, + {"channel_mask", (getter)Downmixer_channel_mask, NULL, "channel_mask", NULL}, + {NULL} +}; + +PyMethodDef Downmixer_methods[] = { + {"read", (PyCFunction)Downmixer_read, METH_VARARGS, ""}, + {"close", (PyCFunction)Downmixer_close, METH_NOARGS, ""}, + {NULL} +}; + +PyTypeObject pcmconverter_DownmixerType = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "pcmconverter.Downmixer", /*tp_name*/ + sizeof(pcmconverter_Downmixer),/*tp_basicsize*/ + 0, /*tp_itemsize*/ + (destructor)Downmixer_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash */ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + "Downmixer objects", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + Downmixer_methods, /* tp_methods */ + 0, /* tp_members */ + Downmixer_getseters, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)Downmixer_init, /* tp_init */ + 0, /* tp_alloc */ + Downmixer_new, /* tp_new */ +}; + +typedef struct { + PyObject_HEAD + + struct pcmreader_s* pcmreader; + array_ia* input_channels; + SRC_STATE *src_state; + double ratio; + unsigned unprocessed_frame_count; + array_f* unprocessed_samples; + array_i* processed_samples; + int sample_rate; + PyObject* audiotools_pcm; +} pcmconverter_Resampler; + +static PyObject* +Resampler_sample_rate(pcmconverter_Resampler *self, void *closure); + +static PyObject* +Resampler_bits_per_sample(pcmconverter_Resampler *self, void *closure); + +static PyObject* +Resampler_channels(pcmconverter_Resampler *self, void *closure); + +static PyObject* +Resampler_channel_mask(pcmconverter_Resampler *self, void *closure); + +static PyObject* +Resampler_read(pcmconverter_Resampler *self, PyObject *args); + +static PyObject* +Resampler_close(pcmconverter_Resampler *self, PyObject *args); + +static PyObject* +Resampler_new(PyTypeObject *type, PyObject *args, PyObject *kwds); + +void +Resampler_dealloc(pcmconverter_Resampler *self); + +int +Resampler_init(pcmconverter_Resampler *self, PyObject *args, PyObject *kwds); + +PyGetSetDef Resampler_getseters[] = { + {"sample_rate", (getter)Resampler_sample_rate, NULL, "sample rate", NULL}, + {"bits_per_sample", (getter)Resampler_bits_per_sample, NULL, "bits per sample", NULL}, + {"channels", (getter)Resampler_channels, NULL, "channels", NULL}, + {"channel_mask", (getter)Resampler_channel_mask, NULL, "channel_mask", NULL}, + {NULL} +}; + +PyMethodDef Resampler_methods[] = { + {"read", (PyCFunction)Resampler_read, METH_VARARGS, ""}, + {"close", (PyCFunction)Resampler_close, METH_NOARGS, ""}, + {NULL} +}; + +PyTypeObject pcmconverter_ResamplerType = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "pcmconverter.Resampler", /*tp_name*/ + sizeof(pcmconverter_Resampler),/*tp_basicsize*/ + 0, /*tp_itemsize*/ + (destructor)Resampler_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash */ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + "Resampler objects", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + Resampler_methods, /* tp_methods */ + 0, /* tp_members */ + Resampler_getseters, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)Resampler_init, /* tp_init */ + 0, /* tp_alloc */ + Resampler_new, /* tp_new */ +}; + +typedef struct { + PyObject_HEAD + + struct pcmreader_s* pcmreader; + int bits_per_sample; + array_ia* input_channels; + array_ia* output_channels; + BitstreamReader* white_noise; + PyObject* audiotools_pcm; +} pcmconverter_BPSConverter; + +static PyObject* +BPSConverter_sample_rate(pcmconverter_BPSConverter *self, void *closure); + +static PyObject* +BPSConverter_bits_per_sample(pcmconverter_BPSConverter *self, void *closure); + +static PyObject* +BPSConverter_channels(pcmconverter_BPSConverter *self, void *closure); + +static PyObject* +BPSConverter_channel_mask(pcmconverter_BPSConverter *self, void *closure); + +static PyObject* +BPSConverter_read(pcmconverter_BPSConverter *self, PyObject *args); + +static PyObject* +BPSConverter_close(pcmconverter_BPSConverter *self, PyObject *args); + +static PyObject* +BPSConverter_new(PyTypeObject *type, PyObject *args, PyObject *kwds); + +void +BPSConverter_dealloc(pcmconverter_BPSConverter *self); + +int +BPSConverter_init(pcmconverter_BPSConverter *self, + PyObject *args, PyObject *kwds); + +PyGetSetDef BPSConverter_getseters[] = { + {"sample_rate", (getter)BPSConverter_sample_rate, + NULL, "sample rate", NULL}, + {"bits_per_sample", (getter)BPSConverter_bits_per_sample, + NULL, "bits per sample", NULL}, + {"channels", (getter)BPSConverter_channels, + NULL, "channels", NULL}, + {"channel_mask", (getter)BPSConverter_channel_mask, + NULL, "channel_mask", NULL}, + {NULL} +}; + +PyMethodDef BPSConverter_methods[] = { + {"read", (PyCFunction)BPSConverter_read, METH_VARARGS, ""}, + {"close", (PyCFunction)BPSConverter_close, METH_NOARGS, ""}, + {NULL} +}; + +PyTypeObject pcmconverter_BPSConverterType = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "pcmconverter.BPSConverter", /*tp_name*/ + sizeof(pcmconverter_BPSConverter),/*tp_basicsize*/ + 0, /*tp_itemsize*/ + (destructor)BPSConverter_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash */ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + "BPSConverter objects", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + BPSConverter_methods, /* tp_methods */ + 0, /* tp_members */ + BPSConverter_getseters, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)BPSConverter_init, /* tp_init */ + 0, /* tp_alloc */ + BPSConverter_new, /* tp_new */ +};
View file
audiotools-2.18.tar.gz/src/replaygain.c -> audiotools-2.19.tar.gz/src/replaygain.c
Changed
@@ -1,6 +1,10 @@ #include <Python.h> -#include "replaygain.h" #include "pcm.h" +#include "pcmconv.h" +#include "bitstream.h" +#include "array.h" +#include "dither.c" +#include "replaygain.h" /* * ReplayGainAnalysis - analyzes input samples and give the recommended dB change @@ -42,12 +46,10 @@ }; PyMethodDef ReplayGain_methods[] = { - {"update",(PyCFunction)ReplayGain_update, - METH_VARARGS,"Updates the ReplayGain object with a FloatFrameList"}, {"title_gain",(PyCFunction)ReplayGain_title_gain, - METH_NOARGS,"Returns a (title gain,title peak) tuple and resets"}, + METH_VARARGS,"PCMReader -> (title gain, title peak) tuple"}, {"album_gain",(PyCFunction)ReplayGain_album_gain, - METH_NOARGS,"Returns an (album gain,album peak) tuple"}, + METH_NOARGS,"Returns an (album gain, album peak) tuple"}, {NULL} }; @@ -96,7 +98,6 @@ void ReplayGain_dealloc(replaygain_ReplayGain* self) { - Py_XDECREF(self->pcm_module); self->ob_type->tp_free((PyObject*)self); } @@ -106,7 +107,6 @@ replaygain_ReplayGain *self; self = (replaygain_ReplayGain *)type->tp_alloc(type, 0); - self->pcm_module = NULL; return (PyObject *)self; } @@ -117,12 +117,13 @@ long sample_rate; int i; - if (!PyArg_ParseTuple(args,"l",&sample_rate)) - return -1; + self->sample_rate = 0; - if ((self->pcm_module = PyImport_ImportModule("audiotools.pcm")) == NULL) + if (!PyArg_ParseTuple(args, "l", &sample_rate)) return -1; + self->sample_rate = (unsigned)sample_rate; + /* zero out initial values*/ for (i = 0; i < MAX_ORDER; i++ ) self->linprebuf[i] = @@ -175,168 +176,148 @@ memset (self->B, 0, sizeof(self->B)); - self->title_peak = self->album_peak = 0.0; + self->album_peak = 0.0; return 0; } + PyObject* -ReplayGain_update(replaygain_ReplayGain *self, PyObject *args) +ReplayGain_title_gain(replaygain_ReplayGain *self, PyObject *args) { - PyObject *framelist_obj = NULL; - PyObject *channels_obj = NULL; - PyObject *channel_l_obj = NULL; - PyObject *channel_r_obj = NULL; - PyObject *framelist_type_obj = NULL; - pcm_FrameList *channel_l; - pcm_FrameList *channel_r; - long channel_count; - double *channel_l_buffer = NULL; - double *channel_r_buffer = NULL; - unsigned sample; - double peak; - int32_t peak_shift; + double title_gain; + double title_peak = 0.0; + pcmreader* pcmreader = NULL; - /*receive a (presumably) FrameList from our arguments*/ - if (!PyArg_ParseTuple(args,"O",&framelist_obj)) + /*read PCMReader-compatible object from args*/ + if (!PyArg_ParseTuple(args, "O&", pcmreader_converter, &pcmreader)) { return NULL; + } else { + array_ia* channels = array_ia_new(); + array_fa* channels_f = array_fa_new(); + const int32_t peak_shift = 1 << (pcmreader->bits_per_sample - 1); - /*get framelist.channels attrib and convert it to an integer*/ - if ((channels_obj = PyObject_GetAttrString(framelist_obj,"channels")) == NULL) - goto error; - if (((channel_count = PyInt_AsLong(channels_obj)) == -1) && PyErr_Occurred()) - goto error; - - /*call framelist.channel(0) and framelist.channel(1)*/ - switch (channel_count) { - case 1: - if ((channel_l_obj = PyObject_CallMethod(framelist_obj, - "channel","(i)",0)) == NULL) - goto error; - if ((channel_r_obj = PyObject_CallMethod(framelist_obj, - "channel","(i)",0)) == NULL) - goto error; - break; - case 2: - if ((channel_l_obj = PyObject_CallMethod(framelist_obj, - "channel","(i)",0)) == NULL) - goto error; - if ((channel_r_obj = PyObject_CallMethod(framelist_obj, - "channel","(i)",1)) == NULL) - goto error; - break; - default: - PyErr_SetString(PyExc_ValueError,"channel count must be 1 or 2"); - goto error; - } - - /*ensure channel_l_obj and channel_r_obj are FrameLists*/ - if ((framelist_type_obj = PyObject_GetAttrString(self->pcm_module, - "FrameList")) == NULL) - goto error; - if (channel_l_obj->ob_type != (PyTypeObject*)framelist_type_obj) { - PyErr_SetString(PyExc_TypeError,"channel 0 must be a FrameList"); - goto error; - } - if (channel_r_obj->ob_type != (PyTypeObject*)framelist_type_obj) { - PyErr_SetString(PyExc_TypeError,"channel 1 must be a FrameList"); - goto error; - } + if (pcmreader->sample_rate != self->sample_rate) { + PyErr_SetString(PyExc_ValueError, + "pcmreader's sample rate doesn't match"); + pcmreader->del(pcmreader); + channels->del(channels); + channels_f->del(channels_f); + return NULL; + } - channel_l = (pcm_FrameList*)channel_l_obj; - channel_r = (pcm_FrameList*)channel_r_obj; + /*read a FrameList object from PCMReader*/ + if (pcmreader->read(pcmreader, 4096, channels)) { + pcmreader->del(pcmreader); + channels->del(channels); + channels_f->del(channels_f); + return NULL; + } - /*convert channel_l and channel_r to doubles, - but *not* doubles between -1.0 and 1.0*/ - channel_l_buffer = malloc(channel_l->frames * sizeof(double)); - channel_r_buffer = malloc(channel_r->frames * sizeof(double)); + /*while FrameList contains more samples*/ + while (channels->_[0]->len) { + unsigned c; + + /*ensure FrameList only contains 1 or 2 channels*/ + if ((channels->len != 1) && (channels->len != 2)) { + PyErr_SetString(PyExc_ValueError, + "FrameList must contain only 1 or 2 channels"); + pcmreader->del(pcmreader); + channels->del(channels); + channels_f->del(channels_f); + return NULL; + } - peak_shift = 1 << (channel_l->bits_per_sample - 1); + /*if only one channel, duplicate it to the other channel*/ + channels->_[0]->copy(channels->_[0], channels->append(channels)); + channels_f->reset(channels_f); + + /*convert left and right channels to doubles, + (but *not* doubles between -1.0 and 1.0) + and store peak values*/ + for (c = 0; c < 2; c++) { + array_i* channel_i = channels->_[c]; + array_f* channel_f = channels_f->append(channels_f); + int i; + double peak; + + channel_f->resize(channel_f, channel_i->len); + + switch (pcmreader->bits_per_sample) { + case 8: + for (i = 0; i < channel_i->len; i++) { + a_append(channel_f, (double)(channel_i->_[i] << 8)); + + peak = ((double)(abs(channel_i->_[i])) / peak_shift); + title_peak = MAX(title_peak, peak); + self->album_peak = MAX(self->album_peak, peak); + } + break; + case 16: + for (i = 0; i < channel_i->len; i++) { + a_append(channel_f, (double)(channel_i->_[i])); + + peak = ((double)(abs(channel_i->_[i])) / peak_shift); + title_peak = MAX(title_peak, peak); + self->album_peak = MAX(self->album_peak, peak); + } + break; + case 24: + for (i = 0; i < channel_i->len; i++) { + a_append(channel_f, (double)(channel_i->_[i] >> 8)); + + peak = ((double)(abs(channel_i->_[i])) / peak_shift); + title_peak = MAX(title_peak, peak); + self->album_peak = MAX(self->album_peak, peak); + } + break; + default: + PyErr_SetString(PyExc_ValueError, + "unsupported bits per sample"); + pcmreader->del(pcmreader); + channels->del(channels); + channels_f->del(channels_f); + return NULL; + } + } - switch (channel_l->bits_per_sample) { - case 8: - for (sample = 0; sample < channel_l->frames; sample++) { - channel_l_buffer[sample] = (double)(channel_l->samples[sample] << 8); - channel_r_buffer[sample] = (double)(channel_r->samples[sample] << 8); + /*perform actual gain analysis on channels*/ + if (ReplayGain_analyze_samples(self, + channels_f->_[0]->_, + channels_f->_[1]->_, + channels_f->_[0]->len, + 2) == GAIN_ANALYSIS_ERROR) { + PyErr_SetString(PyExc_ValueError, + "ReplayGain calculation error"); + pcmreader->del(pcmreader); + channels->del(channels); + channels_f->del(channels_f); + return NULL; + } - peak = (double)(MAX(abs(channel_l->samples[sample]), - abs(channel_r->samples[sample]))) / peak_shift; - self->title_peak = MAX(self->title_peak,peak); - self->album_peak = MAX(self->album_peak,peak); - } - break; - case 16: - for (sample = 0; sample < channel_l->frames; sample++) { - channel_l_buffer[sample] = (double)(channel_l->samples[sample]); - channel_r_buffer[sample] = (double)(channel_r->samples[sample]); - - peak = (double)(MAX(abs(channel_l->samples[sample]), - abs(channel_r->samples[sample]))) / peak_shift; - self->title_peak = MAX(self->title_peak,peak); - self->album_peak = MAX(self->album_peak,peak); - } - break; - case 24: - for (sample = 0; sample < channel_l->frames; sample++) { - channel_l_buffer[sample] = (double)(channel_l->samples[sample] >> 8); - channel_r_buffer[sample] = (double)(channel_r->samples[sample] >> 8); - - peak = (double)(MAX(abs(channel_l->samples[sample]), - abs(channel_r->samples[sample]))) / peak_shift; - self->title_peak = MAX(self->title_peak,peak); - self->album_peak = MAX(self->album_peak,peak); + /*read next FrameList object from PCMReader*/ + if (pcmreader->read(pcmreader, 4096, channels)) { + pcmreader->del(pcmreader); + channels->del(channels); + channels_f->del(channels_f); + return NULL; + } } - break; - default: - PyErr_SetString(PyExc_ValueError,"unsupported bits per sample"); - goto error; - } - - /*perform actual gain analysis on channels*/ - if (ReplayGain_analyze_samples(self, - channel_l_buffer, - channel_r_buffer, - channel_l->frames, - 2) == GAIN_ANALYSIS_ERROR) { - PyErr_SetString(PyExc_ValueError,"ReplayGain calculation error"); - goto error; - } - /*clean up Python objects and return None*/ - Py_XDECREF(channels_obj); - Py_XDECREF(channel_l_obj); - Py_XDECREF(channel_r_obj); - Py_XDECREF(framelist_type_obj); - if (channel_l_buffer != NULL) - free(channel_l_buffer); - if (channel_r_buffer != NULL) - free(channel_r_buffer); - Py_INCREF(Py_None); - return Py_None; - error: - Py_XDECREF(channels_obj); - Py_XDECREF(channel_l_obj); - Py_XDECREF(channel_r_obj); - Py_XDECREF(framelist_type_obj); - if (channel_l_buffer != NULL) - free(channel_l_buffer); - if (channel_r_buffer != NULL) - free(channel_r_buffer); - return NULL; -} - -PyObject* -ReplayGain_title_gain(replaygain_ReplayGain *self) -{ - double gain_value = ReplayGain_get_title_gain(self); - double peak_value = self->title_peak; - if (gain_value != GAIN_NOT_ENOUGH_SAMPLES) { - self->title_peak = 0.0; - return Py_BuildValue("(d,d)",gain_value,peak_value); - } else { - PyErr_SetString(PyExc_ValueError, - "Not enough samples to perform calculation"); - return NULL; + /*deallocate temporary variables*/ + pcmreader->del(pcmreader); + channels->del(channels); + channels_f->del(channels_f); + + /*return calculated title gain and title peak*/ + /*if enough samples have been read*/ + if ((title_gain = ReplayGain_get_title_gain(self)) != + GAIN_NOT_ENOUGH_SAMPLES) { + return Py_BuildValue("(d,d)", title_gain, title_peak); + } else { + title_gain = 0.0; + return Py_BuildValue("(d,d)", title_gain, title_peak); + } } } @@ -838,27 +819,25 @@ int ReplayGainReader_init(replaygain_ReplayGainReader *self, PyObject *args, PyObject *kwds) { - self->pcm_module = NULL; - self->os_module = NULL; self->pcmreader = NULL; + self->channels = array_ia_new(); + self->white_noise = NULL; + self->audiotools_pcm = NULL; + double replaygain; double peak; - if (!PyArg_ParseTuple(args, "Odd", - &(self->pcmreader), + if (!PyArg_ParseTuple(args, "O&dd", + pcmreader_converter, &(self->pcmreader), &(replaygain), &(peak))) return -1; - Py_INCREF(self->pcmreader); - - if ((self->pcm_module = PyImport_ImportModule("audiotools.pcm")) == NULL) { + if ((self->white_noise = open_dither()) == NULL) return -1; - } - if ((self->os_module = PyImport_ImportModule("os")) == NULL) { + if ((self->audiotools_pcm = open_audiotools_pcm()) == NULL) return -1; - } self->multiplier = powl(10.0l, replaygain / 20.0l); if (self->multiplier > 1.0l) @@ -869,149 +848,85 @@ void ReplayGainReader_dealloc(replaygain_ReplayGainReader* self) { - Py_XDECREF(self->pcmreader); - Py_XDECREF(self->pcm_module); - Py_XDECREF(self->os_module); + if (self->pcmreader != NULL) + self->pcmreader->del(self->pcmreader); + self->channels = array_ia_new(); + if (self->white_noise != NULL) + self->white_noise->close(self->white_noise); + Py_XDECREF(self->audiotools_pcm); + self->ob_type->tp_free((PyObject*)self); } static PyObject* ReplayGainReader_sample_rate(replaygain_ReplayGainReader *self, void *closure) { - return PyObject_GetAttrString(self->pcmreader, "sample_rate"); + return Py_BuildValue("i", self->pcmreader->sample_rate); } static PyObject* ReplayGainReader_bits_per_sample(replaygain_ReplayGainReader *self, void *closure) { - return PyObject_GetAttrString(self->pcmreader, "bits_per_sample"); + return Py_BuildValue("i", self->pcmreader->bits_per_sample); } static PyObject* ReplayGainReader_channels(replaygain_ReplayGainReader *self, void *closure) { - return PyObject_GetAttrString(self->pcmreader, "channels"); + return Py_BuildValue("i", self->pcmreader->channels); } static PyObject* ReplayGainReader_channel_mask(replaygain_ReplayGainReader *self, void *closure) { - return PyObject_GetAttrString(self->pcmreader, "channel_mask"); + return Py_BuildValue("i", self->pcmreader->channel_mask); } static PyObject* ReplayGainReader_read(replaygain_ReplayGainReader* self, PyObject *args) { - PyObject* bytes; + int pcm_frames = 0; + array_ia* channels = self->channels; + unsigned c; - PyObject* framelist_obj; - PyObject* framelist_type_obj; - pcm_FrameList *framelist; + const int max_value = (1 << (self->pcmreader->bits_per_sample - 1)) - 1; + const int min_value = -(1 << (self->pcmreader->bits_per_sample - 1)); + const double multiplier = self->multiplier; - PyObject* output_obj; - pcm_FrameList *output_framelist; - - PyObject* dither_obj; - uint8_t* dither; - Py_ssize_t dither_length; - - int max_value; - int min_value; - unsigned i; - double multiplier = self->multiplier; - - if (!PyArg_ParseTuple(args, "O", &bytes)) - return NULL; - - if ((framelist_obj = PyObject_CallMethod(self->pcmreader, - "read", - "O", bytes)) == NULL) + if (!PyArg_ParseTuple(args, "i", &pcm_frames)) return NULL; - /*ensure framelist_obj is a FrameList*/ - if ((framelist_type_obj = PyObject_GetAttrString(self->pcm_module, - "FrameList")) == NULL) { - Py_DECREF(framelist_obj); + if (pcm_frames <= 0) { + PyErr_SetString(PyExc_ValueError, "pcm_frames must be positive"); return NULL; } - if (framelist_obj->ob_type == (PyTypeObject*)framelist_type_obj) { - framelist = (pcm_FrameList*)framelist_obj; - - /*grab some white noise from os.urandom for dithering*/ - if ((dither_obj = PyObject_CallMethod( - self->os_module, - "urandom", - "i", - (framelist->samples_length / 8) + 1)) == NULL) { - Py_DECREF(framelist_obj); - return NULL; - } - - /*convert white noise to a buffer of bytes*/ - if (PyString_AsStringAndSize(dither_obj, - (char **)&dither, - &dither_length) == -1) { - Py_DECREF(dither_obj); - Py_DECREF(framelist_obj); - return NULL; - } - - /*ensure buffer is big enough to apply to our samples*/ - if ((dither_length * 8) < framelist->samples_length) { - PyErr_SetString(PyExc_ValueError, - "string returned by os.urandom is too short"); - Py_DECREF(dither_obj); - Py_DECREF(framelist_obj); - return NULL; - } - - /*build an output FrameList*/ - if ((output_obj = PyObject_CallMethod(self->pcm_module, - "__blank__", - NULL)) == NULL) { - Py_DECREF(dither_obj); - Py_DECREF(framelist_obj); - return NULL; - } + /*read FrameList object from internal PCMReader*/ + if (self->pcmreader->read(self->pcmreader, + (unsigned)pcm_frames, + channels)) + return NULL; - /*update output FrameList to match input FrameList*/ - output_framelist = (pcm_FrameList*)output_obj; - output_framelist->frames = framelist->frames; - output_framelist->channels = framelist->channels; - output_framelist->bits_per_sample = framelist->bits_per_sample; - output_framelist->samples_length = framelist->samples_length; - output_framelist->samples = realloc(output_framelist->samples, - sizeof(int) * - framelist->samples_length); - - /*apply our multiplier to framelist's integer samples - and apply dithering*/ - max_value = (1 << (framelist->bits_per_sample - 1)) - 1; - min_value = -(1 << (framelist->bits_per_sample - 1)); - - for (i = 0; i < framelist->samples_length; i++) { - output_framelist->samples[i] = - MIN(MAX((int)lround(framelist->samples[i] * - multiplier) ^ - (dither[i / 8] & (1 << (i % 8))) >> (i % 8), - min_value), - max_value); + /*apply our multiplier to framelist's integer samples + and apply dithering*/ + for (c = 0; c < channels->len; c++) { + array_i* channel = channels->_[c]; + unsigned i; + for (i = 0; i < channel->len; i++) { + channel->_[i] = (int)lround(channel->_[i] * multiplier); + channel->_[i] = (MIN(MAX(channel->_[i], min_value), max_value) ^ + self->white_noise->read(self->white_noise, 1)); } - - /*decref input FrameList and dither string*/ - Py_DECREF(dither_obj); - Py_DECREF(framelist_obj); - - return output_obj; - } else { - PyErr_SetString(PyExc_TypeError, - "results from pcmreader.read() must be FrameLists"); - Py_DECREF(framelist_obj); - return NULL; } + + /*return integer samples as a new FrameList object*/ + return array_ia_to_FrameList(self->audiotools_pcm, + channels, + self->pcmreader->bits_per_sample); } static PyObject* ReplayGainReader_close(replaygain_ReplayGainReader* self, PyObject *args) { - return PyObject_CallMethod(self->pcmreader, "close", NULL); + self->pcmreader->close(self->pcmreader); + Py_INCREF(Py_None); + return Py_None; }
View file
audiotools-2.18.tar.gz/src/replaygain.h -> audiotools-2.19.tar.gz/src/replaygain.h
Changed
@@ -72,10 +72,8 @@ uint32_t A [STEPS_per_dB_times_MAX_dB]; uint32_t B [STEPS_per_dB_times_MAX_dB]; - double title_peak; + unsigned sample_rate; double album_peak; - - PyObject *pcm_module; } replaygain_ReplayGain; void @@ -88,10 +86,7 @@ ReplayGain_init(replaygain_ReplayGain *self, PyObject *args, PyObject *kwds); PyObject* -ReplayGain_update(replaygain_ReplayGain *self, PyObject *args); - -PyObject* -ReplayGain_title_gain(replaygain_ReplayGain *self); +ReplayGain_title_gain(replaygain_ReplayGain *self, PyObject* args); PyObject* ReplayGain_album_gain(replaygain_ReplayGain *self); @@ -113,9 +108,10 @@ typedef struct { PyObject_HEAD; - PyObject* pcm_module; - PyObject* os_module; - PyObject* pcmreader; + pcmreader* pcmreader; + array_ia* channels; + BitstreamReader* white_noise; + PyObject* audiotools_pcm; double multiplier; } replaygain_ReplayGainReader;
View file
audiotools-2.18.tar.gz/test/apptest.sh -> audiotools-2.19.tar.gz/test/apptest.sh
Changed
@@ -38,7 +38,7 @@ rm -fv testdisc1.flac testdisc2.flac echo "Adding album cover" -tracktag --front-cover=testcover.png testdisc2/*01*.wv +covertag --front-cover=testcover.png testdisc2/*01*.wv echo "Checking album cover" mkdir -v "covers"
View file
audiotools-2.18.tar.gz/test/test.cfg -> audiotools-2.19.tar.gz/test/test.cfg
Changed
@@ -28,6 +28,7 @@ mp2 = on mp3 = on oggflac = on +opus = on shorten = on sines = on vorbis = on @@ -41,6 +42,7 @@ id3v1 = on id3v2 = on vorbis = on +opus = on m4a = on [Util] @@ -49,6 +51,7 @@ cdinfo = on cdplay = on coverdump = on +covertag = on coverview = on dvda2track = on dvdainfo = on
View file
audiotools-2.18.tar.gz/test/test.py -> audiotools-2.19.tar.gz/test/test.py
Changed
@@ -69,31 +69,31 @@ self.single_pcm_frame = audiotools.pcm.from_list( [1] * channels, channels, bits_per_sample, True) - def read(self, bytes): + def read(self, pcm_frames): if (self.total_frames > 0): frame = audiotools.pcm.from_frames( [self.single_pcm_frame] * - min(self.single_pcm_frame.frame_count(bytes) / self.channels, - self.total_frames)) + min(pcm_frames, self.total_frames)) self.total_frames -= frame.frames return frame else: return audiotools.pcm.FrameList( "", self.channels, self.bits_per_sample, True, True) + def read_error(self, pcm_frames): + raise ValueError("unable to read closed stream") + def close(self): - pass + self.read = self.read_error def reset(self): self.total_frames = self.original_frames class RANDOM_PCM_Reader(BLANK_PCM_Reader): - def read(self, bytes): + def read(self, pcm_frames): if (self.total_frames > 0): - frames_to_read = min( - self.single_pcm_frame.frame_count(bytes) / self.channels, - self.total_frames) + frames_to_read = min(pcm_frames, self.total_frames) frame = audiotools.pcm.FrameList( os.urandom(frames_to_read * (self.bits_per_sample / 8) * @@ -185,8 +185,8 @@ self.channels, self.bits_per_sample) - def read(self, bytes): - framelist = self.pcmreader.read(bytes) + def read(self, pcm_frames): + framelist = self.pcmreader.read(pcm_frames) self.md5.update(framelist.to_bytes(False, True)) return framelist @@ -211,7 +211,7 @@ self.range = range(self.channels * (self.bits_per_sample / 8), 4096) - def read(self, bytes): + def read(self, pcm_frames): return self.pcmreader.read(random.choice(self.range)) def close(self): @@ -235,9 +235,9 @@ self.bits_per_sample = pcm_readers[0].bits_per_sample self.readers = map(audiotools.BufferedPCMReader, pcm_readers) - def read(self, bytes): + def read(self, pcm_frames): return audiotools.pcm.from_channels( - [r.read(bytes) for r in self.readers]) + [r.read(pcm_frames) for r in self.readers]) def close(self): for r in self.readers: @@ -253,7 +253,7 @@ self.bits_per_sample = bits_per_sample self.pcm_frames = zip(*channel_data) - def read(self, bytes): + def read(self, pcm_frames): try: return audiotools.pcm.from_list(self.pcm_frames.pop(0), self.channels, @@ -304,6 +304,16 @@ yield [items[i]] + combos +def Possibilities(*lists): + if (len(lists) == 0): + yield () + else: + remainder = list(Possibilities(*lists[1:])) + for item in lists[0]: + for rem in remainder: + yield (item,) + rem + + from_channels = audiotools.ChannelMask.from_channels #these are combinations that tend to occur in nature
View file
audiotools-2.18.tar.gz/test/test_core.py -> audiotools-2.19.tar.gz/test/test_core.py
Changed
@@ -33,7 +33,7 @@ RANDOM_PCM_Reader, EXACT_BLANK_PCM_Reader, SHORT_PCM_COMBINATIONS, MD5_Reader, FrameCounter, - MiniFrameReader, Combinations, + MiniFrameReader, Combinations, Possibilities, TEST_COVER1, TEST_COVER2, TEST_COVER3, HUGE_BMP) @@ -53,14 +53,160 @@ option.upper())] = lambda function: do_nothing +class PCMReader(unittest.TestCase): + @LIB_PCM + def test_pcm(self): + from audiotools.pcm import from_list + + #try reading lots of bps/signed/endianness combinations + for bps in [8, 16, 24]: + for big_endian in [True, False]: + for signed in [True, False]: + reader = audiotools.PCMReader( + cStringIO.StringIO( + from_list(range(-5, 5), + 1, + bps, + True).to_bytes(big_endian, signed)), + sample_rate=44100, + channels=1, + channel_mask=0x4, + bits_per_sample=bps, + signed=signed, + big_endian=big_endian) + + self.assertEqual(reader.sample_rate, 44100) + self.assertEqual(reader.channels, 1) + self.assertEqual(reader.channel_mask, 0x4) + self.assertEqual(reader.bits_per_sample, bps) + + #ensure the FrameList is read correctly + f = reader.read((bps / 8) * 10) + self.assertEqual(len(f), 10) + self.assertEqual(list(f), range(-5, 5)) + + #ensure subsequent reads return empty FrameLists + for i in xrange(10): + f = reader.read((bps / 8) * 10) + self.assertEqual(len(f), 0) + + #ensure closing the stream raises ValueErrors + #on subsequent reads + reader.close() + + self.assertRaises(ValueError, reader.read, (bps / 8) * 10) + + +class PCMCat(unittest.TestCase): + @LIB_PCM + def test_pcm(self): + from audiotools.pcm import from_list + + #ensure mismatched streams raise ValueError at init time + audiotools.PCMCat([audiotools.PCMReader(cStringIO.StringIO(""), + sample_rate=44100, + channels=1, + channel_mask=0x4, + bits_per_sample=16)]) + + self.assertRaises(ValueError, + audiotools.PCMCat, + [audiotools.PCMReader(cStringIO.StringIO(""), + sample_rate=96000, + channels=1, + channel_mask=0x4, + bits_per_sample=16), + audiotools.PCMReader(cStringIO.StringIO(""), + sample_rate=44100, + channels=1, + channel_mask=0x4, + bits_per_sample=16)]) + + self.assertRaises(ValueError, + audiotools.PCMCat, + [audiotools.PCMReader(cStringIO.StringIO(""), + sample_rate=44100, + channels=2, + channel_mask=0x3, + bits_per_sample=16), + audiotools.PCMReader(cStringIO.StringIO(""), + sample_rate=44100, + channels=1, + channel_mask=0x4, + bits_per_sample=16)]) + + self.assertRaises(ValueError, + audiotools.PCMCat, + [audiotools.PCMReader(cStringIO.StringIO(""), + sample_rate=44100, + channels=1, + channel_mask=0x4, + bits_per_sample=24), + audiotools.PCMReader(cStringIO.StringIO(""), + sample_rate=44100, + channels=1, + channel_mask=0x4, + bits_per_sample=16)]) + + main_readers = [audiotools.PCMReader( + cStringIO.StringIO( + from_list(samples, 1, 16, True).to_bytes(True, + True)), + sample_rate=44100, + channels=1, + channel_mask=0x4, + bits_per_sample=16, + signed=True, + big_endian=True) + for samples in [range(-15, -5), + range(-5, 5), + range(5, 15)]] + + reader = audiotools.PCMCat(main_readers) + + #ensure PCMCat's stream attributes match first reader's + self.assertEqual(reader.sample_rate, 44100) + self.assertEqual(reader.channels, 1) + self.assertEqual(reader.channel_mask, 0x4) + self.assertEqual(reader.bits_per_sample, 16) + + #ensure all the substreams are read correctly + samples = [] + f = reader.read(2) + while (len(f) > 0): + samples.extend(list(f)) + f = reader.read(2) + + self.assertEqual(samples, range(-15, 15)) + + #ensure subsequent reads return empty FrameLists + for i in xrange(10): + self.assertEqual(len(reader.read(2)), 0) + + #main readers should not yet be closed + for r in main_readers: + for i in xrange(10): + self.assertEqual(len(r.read(2)), 0) + + #ensure closing the stream raises ValueErrors + #on subsequent reads + reader.close() + + self.assertRaises(ValueError, reader.read, 2) + + #sub readers should also be closed by PCMCat's close() + for r in main_readers: + self.assertRaises(ValueError, r.read, 2) + + class BufferedPCMReader(unittest.TestCase): - @LIB_CORE + @LIB_PCM def test_pcm(self): - def frame_lengths(reader, bytes): - frame = reader.read(bytes) + def frame_lengths(reader, pcm_frames): + frame = reader.read(pcm_frames) while (len(frame) > 0): yield frame.frames - frame = reader.read(bytes) + frame = reader.read(pcm_frames) else: reader.close() @@ -73,7 +219,7 @@ reader = audiotools.BufferedPCMReader( Variable_Reader(EXACT_BLANK_PCM_Reader(4096 * 100))) #(make sure to account for bps/channels in frame_lengths()) - self.assertEqual(set(frame_lengths(reader, 4096 * 4)), set([4096])) + self.assertEqual(set(frame_lengths(reader, 4096)), set([4096])) #check that sample_rate, bits_per_sample, channel_mask and channels #pass-through properly @@ -99,16 +245,219 @@ self.assertEqual(reader2.bits_per_sample, bits_per_sample) self.assertEqual(reader2.channel_mask, channel_mask) - #finally, ensure that random-sized reads also work okay + #ensure that random-sized reads also work okay total_frames = 4096 * 1000 reader = audiotools.BufferedPCMReader( Variable_Reader(EXACT_BLANK_PCM_Reader(total_frames))) while (total_frames > 0): frames = min(total_frames, random.choice(range(1, 1000))) - frame = reader.read(frames * 4) + frame = reader.read(frames) self.assertEqual(frame.frames, frames) total_frames -= frame.frames + #ensure reading after the stream has been exhausted + #results in empty FrameLists + reader = audiotools.BufferedPCMReader( + EXACT_BLANK_PCM_Reader(44100)) + f = reader.read(4096) + while (len(f) > 0): + f = reader.read(4096) + + self.assertEqual(len(f), 0) + + for i in xrange(10): + f = reader.read(4096) + self.assertEqual(len(f), 0) + + #and ensure reading after the stream is closed + #raises a ValueError + reader.close() + + self.assertRaises(ValueError, + reader.read, + 4096) + + +class LimitedPCMReader(unittest.TestCase): + @LIB_PCM + def test_pcm(self): + from audiotools.pcm import from_list + + main_reader = audiotools.PCMReader( + cStringIO.StringIO( + from_list(range(-50, 50), 1, 16, True).to_bytes(True, True)), + sample_rate=44100, + channels=1, + channel_mask=0x4, + bits_per_sample=16, + signed=True, + big_endian=True) + + total_samples = [] + for pcm_frames in [10, 20, 30, 40]: + reader_samples = [] + reader = audiotools.LimitedPCMReader(main_reader, pcm_frames) + self.assertEqual(reader.sample_rate, 44100) + self.assertEqual(reader.channels, 1) + self.assertEqual(reader.channel_mask, 0x4) + self.assertEqual(reader.bits_per_sample, 16) + + f = reader.read(2) + while (len(f) > 0): + reader_samples.extend(list(f)) + f = reader.read(2) + + self.assertEqual(len(reader_samples), pcm_frames) + + total_samples.extend(reader_samples) + + #ensure subsequent reads return empty FrameLists + for i in xrange(10): + self.assertEqual(len(reader.read(2)), 0) + + #ensure closing the substream raises ValueErrors + #on subsequent reads + #(note that this doesn't close the main reader) + reader.close() + + self.assertRaises(ValueError, reader.read, 2) + + self.assertEqual(total_samples, range(-50, 50)) + + #ensure subsequent reads of main reader return empty FrameLists + for i in xrange(10): + self.assertEqual(len(main_reader.read(2)), 0) + + #ensure closing the substream raises ValueErrors + #on subsequent reads + main_reader.close() + + self.assertRaises(ValueError, main_reader.read, 2) + + +class PCMReaderWindow(unittest.TestCase): + @LIB_PCM + def test_pcm(self): + from audiotools.pcm import from_list + + for initial_offset in range(-5, 5): + for pcm_frames in range(5, 15): + main_reader = audiotools.PCMReader( + cStringIO.StringIO( + from_list(range(1, 11), + 1, + 16, + True).to_bytes(True, True)), + sample_rate=44100, + channels=1, + channel_mask=0x4, + bits_per_sample=16, + signed=True, + big_endian=True) + + reader = audiotools.PCMReaderWindow(main_reader, + initial_offset, + pcm_frames) + + self.assertEqual(reader.sample_rate, + main_reader.sample_rate) + self.assertEqual(reader.channels, + main_reader.channels) + self.assertEqual(reader.channel_mask, + main_reader.channel_mask) + self.assertEqual(reader.bits_per_sample, + main_reader.bits_per_sample) + + #ensure reads generate the proper window of samples + samples = [] + f = reader.read(2) + while (len(f) > 0): + samples.extend(list(f)) + f = reader.read(2) + + self.assertEqual(len(samples), pcm_frames) + + target_samples = range(1, 11) + if (initial_offset < 0): + #negative offsets pad window with 0s + target_samples = (([0] * abs(initial_offset)) + + target_samples) + elif (initial_offset > 0): + #positive offsets remove samples from window + target_samples = target_samples[initial_offset:] + + if (len(target_samples) < pcm_frames): + #window longer than samples gets padded with 0s + target_samples += [0] * (pcm_frames - len(target_samples)) + elif (len(target_samples) > pcm_frames): + #window shorder than samples truncates samples + target_samples = target_samples[0:pcm_frames] + + self.assertEqual(samples, target_samples) + + #ensure subsequent reads return empty FrameLists + for i in xrange(10): + self.assertEqual(len(reader.read(2)), 0) + + #ensure closing the PCMReaderWindow + #generates ValueErrors on subsequent reads + reader.close() + + self.assertRaises(ValueError, reader.read, 2) + + #ensure closing the PCMReaderWindow + #closes the main PCMReader also + self.assertRaises(ValueError, main_reader.read, 2) + + +class Sines(unittest.TestCase): + @LIB_PCM + def test_pcm(self): + for stream in [ + test_streams.Generate01(44100), + test_streams.Generate02(44100), + test_streams.Generate03(44100), + test_streams.Generate04(44100), + + test_streams.Sine8_Mono(200000, 48000, + 441.0, 0.50, 441.0, 0.49), + test_streams.Sine8_Stereo(200000, 48000, + 441.0, 0.50, 441.0, 0.49, 1.0), + test_streams.Sine16_Mono(200000, 48000, + 441.0, 0.50, 441.0, 0.49), + test_streams.Sine16_Stereo(200000, 48000, + 441.0, 0.50, 441.0, 0.49, 1.0), + test_streams.Sine24_Mono(200000, 48000, + 441.0, 0.50, 441.0, 0.49), + test_streams.Sine24_Stereo(200000, 48000, + 441.0, 0.50, 441.0, 0.49, 1.0), + test_streams.Simple_Sine(200000, 44100, 0x3F, 16, + (6400, 10000), + (11520, 15000), + (16640, 20000), + (21760, 25000), + (26880, 30000), + (30720, 35000)), + + test_streams.fsd16([1, -1], 100), + + test_streams.WastedBPS16(1000)]: + + #read the base data from the stream + f = stream.read(4096) + while (len(f) > 0): + f = stream.read(4096) + + #ensure subsequent reads return empty FrameLists + for i in xrange(10): + self.assertEqual(len(stream.read(4096)), 0) + + #ensure subsequent reads on a closed stream + #raises ValueError + stream.close() + + self.assertRaises(ValueError, stream.read, 4096) + class CDDA(unittest.TestCase): @LIB_CORE @@ -118,13 +467,10 @@ self.cue = os.path.join(self.temp_dir, "Test.CUE") bin_file = open(self.bin, "wb") - # self.reader = MD5_Reader(EXACT_BLANK_PCM_Reader(69470436)) self.reader = test_streams.Sine16_Stereo(69470436, 44100, 441.0, 0.50, 4410.0, 0.49, 1.0) - audiotools.transfer_framelist_data( - - self.reader, bin_file.write) + audiotools.transfer_framelist_data(self.reader, bin_file.write) bin_file.close() f = open(self.cue, "w") @@ -164,13 +510,36 @@ cdda = audiotools.CDDA(self.cue) self.assertEqual(len(cdda), 4) checksum = md5() - audiotools.transfer_framelist_data( - audiotools.PCMCat(iter(cdda)), - checksum.update) + audiotools.transfer_framelist_data(audiotools.PCMCat(iter(cdda)), + checksum.update) self.assertEqual(self.reader.hexdigest(), checksum.hexdigest()) @LIB_CORE + def test_cdda_pcm(self): + cdda = audiotools.CDDA(self.cue) + + for track in cdda: + #ensure all track data reads correctly + track_frames = track.length() * 588 + total_frames = 0 + f = track.read(4096) + while (len(f) > 0): + total_frames += f.frames + f = track.read(4096) + self.assertEqual(total_frames, track_frames) + + #ensure further reads return empty FrameLists + for i in xrange(10): + self.assertEqual(len(track.read(4096)), 0) + + #ensure closing the reader raises ValueErrors + #on subsequent reads + track.close() + + self.assertRaises(ValueError, track.read, 4096) + + @LIB_CORE def test_cdda_positive_offset(self): #offset values don't apply to CD images #so this test doesn't do much @@ -242,6 +611,54 @@ self.assertEqual(mask, mask2) +class Filename(unittest.TestCase): + def setUp(self): + self.temp_dir = tempfile.mkdtemp() + self.temp_file1 = os.path.join(self.temp_dir, "file1") + self.temp_file2 = os.path.join(self.temp_dir, "file2") + f = open(self.temp_file1, "w") + f.write("hello world") + f.close() + os.link(self.temp_file1, self.temp_file2) + + def tearDown(self): + os.unlink(self.temp_file1) + os.unlink(self.temp_file2) + os.rmdir(self.temp_dir) + + @LIB_CORE + def test_filename(self): + file1 = audiotools.Filename(self.temp_file1) + file2 = audiotools.Filename(self.temp_file2) + file3 = audiotools.Filename(os.path.join(self.temp_dir, "file3")) + file4 = audiotools.Filename(os.path.join(self.temp_dir, "./file3")) + file5 = audiotools.Filename(os.path.join(self.temp_dir, "file4")) + + self.assert_(file1.disk_file()) + self.assert_(file2.disk_file()) + self.assertNotEqual(str(file1), str(file2)) + self.assertNotEqual(unicode(file1), unicode(file2)) + self.assertEqual(file1, file2) + self.assertEqual(hash(file1), hash(file2)) + + self.assert_(not file3.disk_file()) + self.assertNotEqual(str(file1), str(file3)) + self.assertNotEqual(unicode(file1), unicode(file3)) + self.assertNotEqual(file1, file3) + self.assertNotEqual(hash(file1), hash(file3)) + + self.assert_(not file4.disk_file()) + self.assertEqual(str(file3), str(file4)) + self.assertEqual(unicode(file3), unicode(file4)) + self.assertEqual(file3, file4) + self.assertEqual(hash(file3), hash(file4)) + + self.assert_(not file5.disk_file()) + self.assertNotEqual(str(file3), str(file5)) + self.assertNotEqual(unicode(file3), unicode(file5)) + self.assertNotEqual(file3, file5) + self.assertNotEqual(hash(file3), hash(file5)) + class ImageJPEG(unittest.TestCase): @LIB_CORE def setUp(self): @@ -419,25 +836,15 @@ class PCMConverter(unittest.TestCase): - @LIB_CORE + @LIB_PCM def setUp(self): self.tempwav = tempfile.NamedTemporaryFile(suffix=".wav") - @LIB_CORE + @LIB_PCM def tearDown(self): self.tempwav.close() - @LIB_CORE - def test_resampler_init(self): - from audiotools.resample import Resampler - - self.assertRaises(TypeError, Resampler) - self.assertRaises(ValueError, Resampler, -1, 1.0, 0) - self.assertRaises(ValueError, Resampler, 0, 1.0, 0) - self.assertRaises(ValueError, Resampler, 2, 1.0, -1) - self.assertRaises(ValueError, Resampler, 2, 1.0, 5) - - @LIB_CORE + @LIB_PCM def test_conversions(self): for ((i_sample_rate, i_channels, @@ -463,11 +870,13 @@ bits_per_sample=i_bits_per_sample, channel_mask=i_channel_mask) - converter = audiotools.PCMConverter(reader, - sample_rate=o_sample_rate, - channels=o_channels, - bits_per_sample=o_bits_per_sample, - channel_mask=o_channel_mask) + converter = audiotools.PCMConverter( + reader, + sample_rate=o_sample_rate, + channels=o_channels, + bits_per_sample=o_bits_per_sample, + channel_mask=o_channel_mask) + wave = audiotools.WaveAudio.from_pcm(self.tempwav.name, converter) converter.close() @@ -479,102 +888,57 @@ (decimal.Decimal(wave.cd_frames()) / 75).to_integral(), 5) + @LIB_PCM + def test_pcm(self): + for (in_sample_rate, + (in_channels, + in_channel_mask), + in_bits_per_sample) in Possibilities([44100, 96000], + [(1, 0x4), + (2, 0x3), + (4, 0x33)], + [16, 24]): + for (out_sample_rate, + (out_channels, + out_channel_mask), + out_bits_per_sample) in Possibilities([44100, 96000], + [(1, 0x4), + (2, 0x3), + (4, 0x33)], + [16, 24]): + + main_reader = BLANK_PCM_Reader( + length=1, + sample_rate=in_sample_rate, + channels=in_channels, + bits_per_sample=in_bits_per_sample, + channel_mask=in_channel_mask) + + reader = audiotools.PCMConverter( + pcmreader=main_reader, + sample_rate=out_sample_rate, + channels=out_channels, + channel_mask=out_channel_mask, + bits_per_sample=out_bits_per_sample) + + #read contents of converted stream + f = reader.read(4096) + while (len(f) > 0): + f = reader.read(4096) + + #ensure subsequent reads return empty FrameLists + for i in xrange(10): + self.assertEqual(len(reader.read(4096)), 0) -class LimitedPCMReader(unittest.TestCase): - @LIB_CORE - def test_read(self): - reader = audiotools.BufferedPCMReader(BLANK_PCM_Reader(1)) - counter1 = FrameCounter(2, 16, 44100) - counter2 = FrameCounter(2, 16, 44100) - audiotools.transfer_framelist_data( - audiotools.LimitedPCMReader(reader, 4100), counter1.update) - audiotools.transfer_framelist_data( - audiotools.LimitedPCMReader(reader, 40000), counter2.update) - self.assertEqual(counter1.value, 4100 * 4) - self.assertEqual(counter2.value, 40000 * 4) - - -class PCMCat(unittest.TestCase): - @LIB_CORE - def test_read(self): - reader1 = BLANK_PCM_Reader(1) - reader2 = BLANK_PCM_Reader(2) - reader3 = BLANK_PCM_Reader(3) - counter = FrameCounter(2, 16, 44100) - cat = audiotools.PCMCat(iter([reader1, reader2, reader3])) - self.assertEqual(cat.sample_rate, 44100) - self.assertEqual(cat.bits_per_sample, 16) - self.assertEqual(cat.channels, 2) - self.assertEqual(cat.channel_mask, 0x3) - audiotools.transfer_framelist_data(cat, counter.update) - self.assertEqual(int(counter), 6) - + #ensure closing stream raises ValueErrors + #on subsequent reads + reader.close() -class PCMReaderWindow(unittest.TestCase): - @LIB_CORE - def setUp(self): - self.channels = [range(0, 20), - range(20, 0, -1)] + self.assertRaises(ValueError, reader.read, 4096) - def __test_reader__(self, pcmreader, channels): - framelist = pcmreader.read(1024) - output_channels = [[] for i in xrange(len(channels))] - while (len(framelist) > 0): - for c in xrange(framelist.channels): - output_channels[c].extend(framelist.channel(c)) - framelist = pcmreader.read(1024) - self.assertEqual(channels, output_channels) - - @LIB_CORE - def test_basic(self): - self.__test_reader__(MiniFrameReader(self.channels, - 44100, 3, 16), - [range(0, 20), range(20, 0, -1)]) - - self.__test_reader__(audiotools.PCMReaderWindow( - MiniFrameReader(self.channels, 44100, 3, 16), 0, 20), - [range(0, 20), range(20, 0, -1)]) - - @LIB_CORE - def test_crop(self): - self.__test_reader__(audiotools.PCMReaderWindow( - MiniFrameReader(self.channels, 44100, 3, 16), 0, 15), - [range(0, 15), range(20, 5, -1)]) - - self.__test_reader__(audiotools.PCMReaderWindow( - MiniFrameReader(self.channels, 44100, 3, 16), 5, 15), - [range(5, 20), range(15, 0, -1)]) - - self.__test_reader__(audiotools.PCMReaderWindow( - MiniFrameReader(self.channels, 44100, 3, 16), 5, 10), - [range(5, 15), range(15, 5, -1)]) - - @LIB_CORE - def test_extend(self): - self.__test_reader__(audiotools.PCMReaderWindow( - MiniFrameReader(self.channels, 44100, 3, 16), -5, 25), - [[0] * 5 + range(0, 20), - [0] * 5 + range(20, 0, -1)]) - - self.__test_reader__(audiotools.PCMReaderWindow( - MiniFrameReader(self.channels, 44100, 3, 16), 0, 25), - [range(0, 20) + [0] * 5, - range(20, 0, -1) + [0] * 5]) - - self.__test_reader__(audiotools.PCMReaderWindow( - MiniFrameReader(self.channels, 44100, 3, 16), -5, 20), - [[0] * 5 + range(0, 15), - [0] * 5 + range(20, 5, -1)]) - - self.__test_reader__(audiotools.PCMReaderWindow( - MiniFrameReader(self.channels, 44100, 3, 16), -5, 15), - [[0] * 5 + range(0, 10), - [0] * 5 + range(20, 10, -1)]) - - self.__test_reader__(audiotools.PCMReaderWindow( - MiniFrameReader(self.channels, 44100, 3, 16), -5, 30), - [[0] * 5 + range(0, 20) + [0] * 5, - [0] * 5 + range(20, 0, -1) + [0] * 5]) + #ensure main reader is also closed + #when converter is closed + self.assertRaises(ValueError, main_reader.read, 4096) class Test_ReplayGain(unittest.TestCase): @@ -736,9 +1100,7 @@ finally: os.chmod(self.dummy1.name, 0600) - #ensure a file whose __init__ method triggers InvalidFile - #raises UnsupportedFile - self.assertRaises(audiotools.UnsupportedFile, + self.assertRaises(audiotools.InvalidFile, audiotools.open, self.dummy3.name) @@ -3316,30 +3678,28 @@ class TestReplayGain(unittest.TestCase): - @LIB_CORE + @LIB_REPLAYGAIN def test_basics(self): import audiotools.replaygain import audiotools.pcm + from cStringIO import StringIO #check for invalid sample rate self.assertRaises(ValueError, audiotools.replaygain.ReplayGain, 200000) - #check for invalid channel count + #check for a very small sample count rg = audiotools.replaygain.ReplayGain(44100) - self.assertRaises(ValueError, - rg.update, - audiotools.pcm.from_list(range(20), 4, 16, True)) - #check for not enough samples - rg.update(audiotools.pcm.from_list([1, 2], 2, 16, True)) - self.assertRaises(ValueError, rg.title_gain) + self.assertEqual( + rg.title_gain(audiotools.PCMReader(StringIO(""), + 44100, 2, 0x3, 16)), + (0.0, 0.0)) self.assertRaises(ValueError, rg.album_gain) #check for no tracks - gain = audiotools.calculate_replay_gain([]) - self.assertRaises(ValueError, list, gain) + assert(len(list(audiotools.calculate_replay_gain([]))) == 0) #check for lots of invalid combinations for calculate_replay_gain track_file1 = tempfile.NamedTemporaryFile(suffix=".wav") @@ -3350,46 +3710,11 @@ BLANK_PCM_Reader(2)) track2 = audiotools.WaveAudio.from_pcm(track_file2.name, BLANK_PCM_Reader(3)) - track3 = audiotools.WaveAudio.from_pcm( - track_file3.name, - BLANK_PCM_Reader(2, sample_rate=48000)) - - gain = audiotools.calculate_replay_gain([track1, track2, track3]) - self.assertRaises(ValueError, list, gain) - - track3 = audiotools.WaveAudio.from_pcm( - track_file3.name, - BLANK_PCM_Reader( - 2, - channels=4, - channel_mask=audiotools.ChannelMask.from_fields( - front_left=True, - front_right=True, - back_left=True, - back_right=True))) - - gain = audiotools.calculate_replay_gain([track1, track2, track3]) - self.assertRaises(ValueError, list, gain) - - track3 = audiotools.WaveAudio.from_pcm( - track_file3.name, - BLANK_PCM_Reader( - 2, - sample_rate=48000, - channels=3, - channel_mask=audiotools.ChannelMask.from_fields( - front_left=True, - front_right=True, - front_center=True))) - - gain = audiotools.calculate_replay_gain([track1, track2, track3]) - self.assertRaises(ValueError, list, gain) - - track3 = audiotools.WaveAudio.from_pcm( - track_file3.name, - BLANK_PCM_Reader(2)) + track3 = audiotools.WaveAudio.from_pcm(track_file3.name, + BLANK_PCM_Reader(2)) - gain = list(audiotools.calculate_replay_gain([track1, track2, track3])) + gain = list(audiotools.calculate_replay_gain( + [track1, track2, track3])) self.assertEqual(len(gain), 3) self.assert_(gain[0][0] is track1) self.assert_(gain[1][0] is track2) @@ -3399,7 +3724,7 @@ track_file2.close() track_file3.close() - @LIB_CORE + @LIB_REPLAYGAIN def test_valid_rates(self): import audiotools.replaygain @@ -3412,12 +3737,48 @@ 0x4, 16, (30000, sample_rate / 100)) - audiotools.transfer_data(reader.read, gain.update) - (gain, peak) = gain.title_gain() + (gain, peak) = gain.title_gain(reader) self.assert_(gain < -4.0) self.assert_(peak > .90) - @LIB_CORE + @LIB_REPLAYGAIN + def test_pcm(self): + import audiotools.replaygain + + gain = audiotools.replaygain.ReplayGain(44100) + (gain, peak) = gain.title_gain( + test_streams.Sine16_Stereo(44100, 44100, + 441.0, 0.50, + 4410.0, 0.49, 1.0)) + + main_reader = test_streams.Sine16_Stereo(44100, 44100, + 441.0, 0.50, + 4410.0, 0.49, 1.0) + + reader = audiotools.replaygain.ReplayGainReader(main_reader, + gain, + peak) + + #read FrameLists from ReplayGainReader + f = reader.read(4096) + while (len(f) > 0): + f = reader.read(4096) + + #ensure subsequent reads return empty FrameLists + for i in xrange(10): + self.assertEqual(len(reader.read(4096)), 0) + + #ensure closing the ReplayGainReader raises ValueError + #on subsequent reads + reader.close() + + self.assertRaises(ValueError, reader.read, 4096) + + #ensure wrapped reader is also closed + self.assertRaises(ValueError, main_reader.read, 4096) + + + @LIB_REPLAYGAIN def test_reader(self): import audiotools.replaygain @@ -3435,1851 +3796,24 @@ #calculate its ReplayGain gain = audiotools.replaygain.ReplayGain(track1.sample_rate()) - pcm = track1.to_pcm() - audiotools.transfer_data(pcm.read, gain.update) - (gain, peak) = gain.title_gain() + (gain, peak) = gain.title_gain(track1.to_pcm()) #apply gain to dummy file track2 = test_format.from_pcm( dummy2.name, - audiotools.ReplayGainReader(track1.to_pcm(), - gain, - peak)) + audiotools.replaygain.ReplayGainReader(track1.to_pcm(), + gain, + peak)) #ensure gain applied is quieter than without gain applied gain2 = audiotools.replaygain.ReplayGain(track1.sample_rate()) - pcm = track2.to_pcm() - audiotools.transfer_data(pcm.read, gain2.update) - (gain2, peak2) = gain2.title_gain() + (gain2, peak2) = gain2.title_gain(track2.to_pcm()) self.assert_(gain2 > gain) finally: dummy1.close() dummy2.close() - @LIB_CORE - def test_applicable(self): - #build a bunch of test tracks - test_format = audiotools.WaveAudio - - temp_files = [tempfile.NamedTemporaryFile( - suffix="." + test_format.SUFFIX) - for i in xrange(6)] - - try: - track1 = test_format.from_pcm(temp_files[0].name, - BLANK_PCM_Reader(1, 44100, 2, 16)) - - track2 = test_format.from_pcm(temp_files[1].name, - BLANK_PCM_Reader(2, 44100, 2, 16)) - - track3 = test_format.from_pcm(temp_files[2].name, - BLANK_PCM_Reader(3, 48000, 2, 16)) - - track4 = test_format.from_pcm(temp_files[3].name, - BLANK_PCM_Reader(4, 44100, 1, 16)) - - track5 = test_format.from_pcm(temp_files[4].name, - BLANK_PCM_Reader(5, 44100, 2, 24)) - - track6 = test_format.from_pcm(temp_files[5].name, - BLANK_PCM_Reader(6, 44100, 2, 16)) - - #inconsistent sample rates aren't applicable - self.assertEqual(audiotools.applicable_replay_gain([track1, - track2, - track3]), - False) - - #inconsistent channel counts aren't applicable - self.assertEqual(audiotools.applicable_replay_gain([track1, - track2, - track4]), - False) - - #inconsistent bit-per-sample *are* applicable - self.assertEqual(audiotools.applicable_replay_gain([track1, - track2, - track5]), - True) - - #consistent everything is applicable - self.assertEqual(audiotools.applicable_replay_gain([track1, - track2, - track6]), - True) - - finally: - for f in temp_files: - f.close() - - -#DEPRECATED - this test will be removed along with XMCD support -class TestXMCD(unittest.TestCase): - XMCD_FILES = [( -"""eJyFk0tv20YQgO8B8h+m8MHJReXyTQFEm0pyYcAvSELTHCmKigRLYiHSanUTSdt1agd9BGnsOo3R -uGmcNn60AYrakfNjsqVinfwXOpS0KwRtEQKL2Zmd/WZ2ZjgFXzTs8tUrU5CsYsuyl6HSshoOuJWK -5/heOrEnH1EEthWJIClMkUVFJVwxVFFiiiIagswU1dAFlSmGomg6BxNd0TmbSBoaJpquEW2Sgqqo -ItdUQyCcT3RNV3kAYojKJBFREGRDm2gKmaQvipqs83uiLKmGwTVVJTqPJxqSYHBNEiRR4xEkkWij -KiQrW/NsqDvN2341DbKk8IO80655NbeJ1kRdarm243lOGUqdNNjlcqkMbZJSUuLSnAAZ97NOq3a7 -6sM1+zoUfKftQMGuOq0KOD5Y9VSCKKyUGjXfR0S7ZqXhI7e5nGvaCUVIqaOw2dlCZjZrygoRKmWC -xmxxtjiXM2n0iIbHNDqk4elMfnGhOJvLw/vwlhkWafSygKuIS4L4YJsGezR49Xqne9l7ie9cJpe9 -c0Teyt3Im1hn7Fz249xCPmcW3JVm2U8G6uqV4jCigCE3aPSMhj/T8DGNXtDwJFGjHvMg5s2q5cN0 -yV3xodEBz7daH8CHM26r4TIf0UwuIyJ6zEwSgruMOgRHd2D4iOc0+gbfcXn+KP79fv/hbrz2PH74 -HQ1+o8Ev7LZs3nTqtosjX3RhvgMzVjNTXylNe7CQVP895qeY8clq/85mfPb09fZ6fHcjfrX19+mP -/Z0w6zanfSg5ULd8h7mr//UWdqiZwxdgovdpuE+jTRqt4wamNOahm7S7dfHnGuLfPDsb7B/HZw+G -9e+u0e5dyMzT8HxUQriWt5rLFnzitJLZus4Ihtnf3ht8f2+wv3vx0xYvsWC+eRrQ4Cg+79EAS/Tt -MJNDGkXYHe5FTBoc0uBe/8GTi4NtbsbiJ7li2L+wbbiBObfteNBxV6DjWFVeLCKZ8dGX8dFOvLYa -9/YuNk75iWwW5gvxydeDH77CNPqHW9gdGoRJSsl4HdPwYJjSr6Mh4feUSeNhMZVJ8QN1coCowYsn -iKLBHzQ44C6a2V/dxRGmAcbEd29g/2mwipNMgx0abHJH/V2jxD2Nt6JiqYY8DLyOvwha+LwK/9tr -+LzmV5PxaLu2Vff4DfKuKv/rYu7TYtaE5CdMw+gvREtRMEeSjKU4ltJYymOpjKU6ltpY6mNpMA4H -MiJhSMKYhEEJoxKGJYxLGJgwssjIYkJemrtxazGfzeVx/w8vFHIR""".decode('base64').decode('zlib'), - 4351, [150, 21035, 42561, 49623, 52904, 69806, 95578, - 118580, 137118, 138717, 156562, 169014, 187866, - 192523, 200497, 205135, 227486, 243699, 266182, - 293092, 303273, 321761], - [('EXTT0', u''), - ('EXTT1', u''), - ('EXTT2', u''), - ('EXTT3', u''), - ('EXTT4', u''), - ('EXTT5', u''), - ('EXTT6', u''), - ('EXTT7', u''), - ('EXTT8', u''), - ('EXTT9', u''), - ('DTITLE', u'\u30de\u30af\u30ed\u30b9FRONTIER / \u30de\u30af\u30ed\u30b9F O\u30fbS\u30fbT\u30fb3 \u5a18\u305f\u307e\u2640\uff3bDisk1\uff3d'), - ('EXTT19', u''), - ('DYEAR', u'2008'), - ('DISCID', u'4510fd16'), - ('TTITLE20', u'\u30a4\u30f3\u30d5\u30a3\u30cb\u30c6\u30a3 #7 without vocals'), - ('TTITLE21', u'\u30cb\u30f3\u30b8\u30fc\u30f3 Loves you yeah! without vocals'), - ('EXTT18', u''), - ('EXTD', u' YEAR: 2008'), - ('EXTT12', u''), - ('EXTT13', u''), - ('EXTT10', u''), - ('DGENRE', u'Soundtrack'), - ('EXTT16', u''), - ('EXTT17', u''), - ('EXTT14', u''), - ('EXTT15', u''), - ('EXTT20', u''), - ('TTITLE9', u'\u661f\u9593\u98db\u884c'), - ('TTITLE8', u'\u300c\u8d85\u6642\u7a7a\u98ef\u5e97 \u5a18\u3005\u300d CM\u30bd\u30f3\u30b0 (Ranka Version)'), - ('TTITLE5', u"\u5c04\u624b\u5ea7\u2606\u5348\u5f8c\u4e5d\u6642Don't be late"), - ('TTITLE4', u"Welcome To My FanClub's Night!"), - ('TTITLE7', u'\u30a4\u30f3\u30d5\u30a3\u30cb\u30c6\u30a3 #7'), - ('TTITLE6', u"What 'bout my star?"), - ('TTITLE1', u"What 'bout my star? @Formo"), - ('TTITLE0', u'\u30c8\u30e9\u30a4\u30a2\u30f3\u30b0\u30e9\u30fc'), - ('TTITLE3', u'\u30c0\u30a4\u30a2\u30e2\u30f3\u30c9 \u30af\u30ec\u30d0\u30b9\uff5e\u5c55\u671b\u516c\u5712\u306b\u3066'), - ('TTITLE2', u'\u30a2\u30a4\u30e2'), - ('TTITLE19', u'\u30a2\u30a4\u30e2\uff5e\u3053\u3044\u306e\u3046\u305f\uff5e'), - ('TTITLE18', u'\u30c0\u30a4\u30a2\u30e2\u30f3\u30c9 \u30af\u30ec\u30d0\u30b9'), - ('EXTT21', u''), - ('EXTT11', u''), - ('TTITLE11', u'\u306d\u3053\u65e5\u8a18'), - ('TTITLE10', u'\u79c1\u306e\u5f7c\u306f\u30d1\u30a4\u30ed\u30c3\u30c8'), - ('TTITLE13', u'\u5b87\u5b99\u5144\u5f1f\u8239'), - ('TTITLE12', u'\u30cb\u30f3\u30b8\u30fc\u30f3 Loves you yeah!'), - ('TTITLE15', u'\u30a2\u30a4\u30e2 O.C.'), - ('TTITLE14', u'SMS\u5c0f\u968a\u306e\u6b4c\uff5e\u3042\u306e\u5a18\u306f\u30a8\u30a4\u30ea\u30a2\u30f3'), - ('TTITLE17', u'\u611b\u30fb\u304a\u307c\u3048\u3066\u3044\u307e\u3059\u304b'), - ('TTITLE16', u'\u30a2\u30a4\u30e2\uff5e\u9ce5\u306e\u3072\u3068'), - ('PLAYORDER', u'')], - [12280380, 12657288, 4152456, 1929228, 9938376, 15153936, - 13525176, 10900344, 940212, 10492860, 7321776, 11084976, - 2738316, 4688712, 2727144, 13142388, 9533244, 13220004, - 15823080, 5986428, 10870944, 2687748]), - ( -"""eJxNU9uOo0gMfZ6W+h8szcuM1OqhuBOpHpImnYnUlyhh5/JYASeUGqhMQbKTv19XcclKSDb28bF9 -MJ/hb50X8JRCITqxFy3CQVZ4f/eZHsi0yD/goEWNoA6HFrt2RvFPLHCs8SOPGcdjLIqM48euHxgn -dJwkMU4Uu7F1Et/pMYw5SdjXu4Hj9DEv9qKwpw69yLde5Dpxn018P7RZl7HYtbWuG/mxbeX6bhTb -CjcMWRhbL46ixOI8h7k9s+fSTGzYLJVtDhU2x66cge8HAbSYq6Zoh/wWL7KVqpmBYYGNVjm2LRaw -v84gL4p9ARf2GDy6mxcHntTpquWx7OBL/hV2HV4QdnmJ+gDYgageDcXuvK9l1xHFRYoZKY5/gRj6 -gdL17mmdcpf2CwNGy6TZOntZ8vcG/zkR47mQqoVv8AsbdUShW3gx/Qj3eznfctqMpEhXy7ftkq/o -a93fZZbA4RuNtWpkR7uMQcZXWpSHB5q7+XNGrTR9XEiF/mhoxxHl8sw2olRX0j4dvTzAd4p1U3CD -6lRNzTz+LDTM/xVXo1ct2ynj89cr/JBVJY4I6xbezvUeNdB2IyLguxIvonuwvD9lU4Bs4UlUlWyO -IyjkO3qjZ+y/wqareviIiYhIkMzawAxmebTwVKOop+Vioyz8LBUshMYWnkVzbGHewUpNTAlfmIMw -xTsUIGikZ6mniZlDneTJpivEkwVsSWx925sxvtDqAxt4lZp0nuIu7+e5qavVbU/m8YyCi+qM5he8 -YIW3Up+/550y8r2iroWc5mWBrcqIuD1rs53MS5KwaVQHC9ND0cFP6JD/IHXxSjgk9P9lXyh9w0V0 -UJS0etojANlY9Ju9+N3HdYLGdoB5dSp7ud5rPIopm/B10ylY0rdpRNWLdn+3/JWlHMwVz6A/Y4pk -Du8tG6w7WG+w/mCDwYaDjQYbDzYZeSbCkZGNlGzkZCMpG1nZSMtGXjYSM8O8eZn/ft+myy35/wHM -D3PD""".decode('base64').decode('zlib'), - 4455, [150, 14731, 31177, 48245, 60099, 78289, 94077, - 110960, 125007, 138376, 156374, 172087, 194466, - 211820, 227485, 242784, 266168, 287790, 301276, - 320091], - [('EXTT0', u''), ('EXTT1', u''), ('EXTT2', u''), - ('EXTT3', u''), ('EXTT4', u''), ('EXTT5', u''), - ('EXTT6', u''), ('EXTT7', u''), ('EXTT8', u''), - ('EXTT9', u''), - ('DTITLE', u'OneUp Studios / Xenogears Light'), - ('EXTT19', u''), ('DYEAR', u'2005'), - ('DISCID', u'22116514'), ('EXTT18', u''), - ('EXTD', u' YEAR: 2005'), ('EXTT12', u''), - ('EXTT13', u''), ('EXTT10', u''), ('DGENRE', u'Game'), - ('EXTT16', u''), ('EXTT17', u''), ('EXTT14', u''), - ('EXTT15', u''), - ('TTITLE9', u'Bonds of Sea and Fire'), - ('TTITLE8', u'One Who Bares Fangs At God'), - ('TTITLE5', u'Shevat, the Wind is Calling'), - ('TTITLE4', u'My Village Is Number One'), - ('TTITLE7', u'Shattering the Egg of Dreams'), - ('TTITLE6', u'Singing of the Gentle Wind'), - ('TTITLE1', u'Grahf, Conqueror of Darkness'), - ('TTITLE0', u'Premonition'), - ('TTITLE3', u'Far Away Promise'), - ('TTITLE2', u'Tears of the Stars, Hearts of the People'), - ('TTITLE19', u'Into Eternal Sleep'), - ('TTITLE18', u'The Alpha and Omega'), - ('EXTT11', u''), - ('TTITLE11', u'Broken Mirror'), - ('TTITLE10', u'Ship of Sleep and Remorse'), - ('TTITLE13', u'The Blue Traveler'), - ('TTITLE12', u'Dreams of the Strong'), - ('TTITLE15', u'The Treasure Which Cannot Be Stolen'), - ('TTITLE14', u'October Mermaid'), - ('TTITLE17', u'Gathering Stars in the Night Sky'), - ('TTITLE16', u'Valley Where the Wind is Born'), - ('PLAYORDER', u'')], - [8573628, 9670248, 10035984, 6970152, 10695720, 9283344, - 9927204, 8259636, 7860972, 10582824, 9239244, 13158852, - 10204152, 9211020, 8995812, 13749792, 12713736, 7929768, - 11063220, 8289036]), - ( -"""eJxdUU1v00AQvVfqf5iqF5BoajuO7UTag5OY1lI+KtsN5OjYm8ZKYke2k5JLhW3EoYA4gjiAxNeh -iCKEQCAi8WMWqt76F1i3touwbL95s2/fzsxuwr2pZa+vbUL6Gb5pjWHom1MM3nAY4DCopfn0YStM -EVbLjJgTnpWqBRGYKlPOiSjxbEHoFqFaGDBVSbxmnMALUsF4XhAKQ1bgK9f2rChy/5YhsqKU1950 -Agsm2D0IRzXgJKlY0PDCCRzPpdmU7vmehYMA2zBY1sCy7YENC7ZUKXF7LQYa3mzpOwejEG5YN0EP -8QKDbo2wPwQcgjkppRb6fDB1wpBaLByzBnXPHSuulbowpezYpqo31CYyJWbAC4xFE4ZqtBTUM33H -mwcg+6EThAFsQ32CTWsExghDHQchNJpU3FdkDXEMI9B4R+loCpJdZ4rX14xLGwZ1Nbmzo8DVfxsu -VsdHJH5N4h8k/kWSk8vg01GuZ5HmYBjOqbLlDDE4AcUxBpPWboa5ikO73bYCbbmpwJ/Tb2fPnlI9 -ib+S5AuJP5LkHUlWF6uIvvmOMtrvKdqh509sKm1uhdhyvfSEXMAjkrxP9yfHqVf0k0QPSfTk7Pmr -XFFB+tjzZuC5oHtTPPDsJVWOzNlsOcPebFJYCWhX3dkF07WhTQOjD41uq6tR8e/v989XJyQ6PT/+ -nKtF1N9X03bV20qek5A+d3V6jfqhE4zSepKXJH5Lkhe0MTqxXFdFdUU2oKHt63QUmk6VRreTnnnr -PyzmyyASPaCNkTimdZDoMYkekTjteVfuyHW1ELIovaD4A0kikryh6+1uT+1sbKyvKXeNJtJ7dxpb -Is+xl9xg0BWyGXIZljPkM6xkKGQoZihlWM19CsPUca8l97sa7ZDGfwEBGThn""".decode('base64').decode('zlib'), - 2888, [150, 19307, 41897, 60903, 78413, 93069, 109879, - 126468, 144667, 164597, 177250, 197178], - [('EXTT0', u''), ('EXTT1', u''), ('EXTT2', u''), - ('EXTT3', u''), ('EXTT4', u''), ('EXTT5', u''), - ('EXTT6', u''), ('EXTT7', u''), ('EXTT8', u''), - ('EXTT9', u''), - ('DTITLE', u'Various Artists / Bleach The Best CD'), - ('DYEAR', u'2006'), ('DISCID', u'a80b460c'), - ('EXTD', u'SVWC-7421'), ('EXTT10', u''), - ('DGENRE', u'Anime'), - ('TTITLE9', u'BEAT CRUSADERS / TONIGHT,TONIGHT,TONIGHT'), - ('TTITLE8', u'SunSet Swish / \u30de\u30a4\u30da\u30fc\u30b9'), - ('TTITLE5', u'Skoop on Somebody / happypeople'), - ('TTITLE4', u'\u30e6\u30f3\u30ca / \u307b\u3046\u304d\u661f'), - ('TTITLE7', u'YUI / LIFE'), - ('TTITLE6', u'HIGH and MIGHTY COLOR / \u4e00\u8f2a\u306e\u82b1'), - ('TTITLE1', u'Rie fu / Life is Like a Boat'), - ('TTITLE0', u'ORANGE RANGE / \uff0a~\u30a2\u30b9\u30bf\u30ea\u30b9\u30af~'), - ('TTITLE3', u'UVERworld / D-tecnoLife'), - ('TTITLE2', u'HOME MADE \u5bb6\u65cf / \u30b5\u30f3\u30ad\u30e5\u30fc\uff01\uff01'), - ('EXTT11', u''), - ('TTITLE11', u'\u30bf\u30ab\u30c1\u30e3 / MOVIN!!'), - ('TTITLE10', u'\u3044\u304d\u3082\u306e\u304c\u304b\u308a / HANABI'), - ('PLAYORDER', u'')], - [11264316, 13282920, 11175528, 10295880, 8617728, 9884280, - 9754332, 10701012, 11718840, 7439964, 11717664, 11446596])] - - @LIB_CORE - def testroundtrip(self): - for (data, length, offsets, items, track_lengths) in self.XMCD_FILES: - f = tempfile.NamedTemporaryFile(suffix=".xmcd") - try: - f.write(data) - f.flush() - f.seek(0, 0) - - #check that reading in an XMCD file matches - #its expected values - xmcd = audiotools.XMCD.from_string(f.read()) - # self.assertEqual(length, xmcd.length) - # self.assertEqual(offsets, xmcd.offsets) - for (pair1, pair2) in zip(sorted(items), - sorted(xmcd.fields.items())): - self.assertEqual(pair1, pair2) - #self.assertEqual(dict(items),dict(xmcd.items())) - - #check that building an XMCD file from values - #and reading it back in results in the same values - f2 = tempfile.NamedTemporaryFile(suffix=".xmcd") - try: - f2.write(xmcd.to_string()) - f2.flush() - f2.seek(0, 0) - - xmcd2 = audiotools.XMCD.from_string(f2.read()) - # self.assertEqual(length, xmcd2.length) - # self.assertEqual(offsets, xmcd2.offsets) - for (pair1, pair2) in zip(sorted(items), - sorted(xmcd2.fields.items())): - self.assertEqual(pair1, pair2) - # self.assertEqual(xmcd.length, xmcd2.length) - # self.assertEqual(xmcd.offsets, xmcd2.offsets) - self.assertEqual(dict(xmcd.fields.items()), - dict(xmcd2.fields.items())) - finally: - f2.close() - finally: - f.close() - - @LIB_CORE - def testtracktagging(self): - for (data, length, offsets, items, track_lengths) in self.XMCD_FILES: - f = tempfile.NamedTemporaryFile(suffix=".xmcd") - try: - f.write(data) - f.flush() - f.seek(0, 0) - - xmcd = audiotools.XMCD.from_string(f.read()) - - #build a bunch of temporary FLAC files from the track_lengths - temp_files = [tempfile.NamedTemporaryFile(suffix=".flac") - for track_length in track_lengths] - try: - temp_tracks = [audiotools.FlacAudio.from_pcm( - temp_file.name, - EXACT_BLANK_PCM_Reader(track_length), - "1") - for (track_length, temp_file) in - zip(track_lengths, temp_files)] - - for i in xrange(len(track_lengths)): - temp_tracks[i].set_metadata( - audiotools.MetaData(track_number=i + 1)) - - #tag them with metadata from XMCD - for track in temp_tracks: - track.set_metadata(xmcd.track_metadata( - track.track_number())) - - #build a new XMCD file from track metadata - xmcd2 = audiotools.XMCD.from_tracks(temp_tracks) - - #check that the original XMCD values match the track ones - # self.assertEqual(xmcd.length, xmcd2.length) - # self.assertEqual(xmcd.offsets, xmcd2.offsets) - self.assertEqual(xmcd.fields['DISCID'].upper(), - xmcd2.fields['DISCID'].upper()) - if (len([pair for pair in xmcd.fields.items() - if (pair[0].startswith('TTITLE') and - (u" / " in pair[1]))]) > 0): - self.assertEqual( - xmcd.fields['DTITLE'].split(' / ', 1)[1], - xmcd2.fields['DTITLE'].split(' / ', 1)[1]) - else: - self.assertEqual(xmcd.fields['DTITLE'], - xmcd2.fields['DTITLE']) - self.assertEqual(xmcd.fields['DYEAR'], - xmcd2.fields['DYEAR']) - for (pair1, pair2) in zip( - sorted([pair for pair in xmcd.fields.items() - if (pair[0].startswith('TTITLE'))]), - sorted([pair for pair in xmcd2.fields.items() - if (pair[0].startswith('TTITLE'))])): - self.assertEqual(pair1, pair2) - finally: - for t in temp_files: - t.close() - finally: - f.close() - - @LIB_CORE - def test_formatting(self): - LENGTH = 1134 - OFFSETS = [150, 18740, 40778, 44676, 63267] - - #ensure that latin-1 and UTF-8 encodings are handled properly - for (encoding, data) in zip(["ISO-8859-1", "UTF-8", "UTF-8"], - [{"TTITLE0":u"track one", - "TTITLE1":u"track two", - "TTITLE2":u"track three", - "TTITLE4":u"track four", - "TTITLE5":u"track five"}, - {"TTITLE0":u"track \xf3ne", - "TTITLE1":u"track two", - "TTITLE2":u"track three", - "TTITLE4":u"track four", - "TTITLE5":u"track five"}, - {"TTITLE0":u'\u30de\u30af\u30ed\u30b9', - "TTITLE1":u"track tw\xf3", - "TTITLE2":u"track three", - "TTITLE4":u"track four", - "TTITLE5":u"track five"}]): - # xmcd = audiotools.XMCD(data, OFFSETS, LENGTH) - xmcd = audiotools.XMCD(data, [u"# xmcd"]) - xmcd2 = audiotools.XMCD.from_string(xmcd.to_string()) - self.assertEqual(dict(xmcd.fields.items()), - dict(xmcd2.fields.items())) - - xmcdfile = tempfile.NamedTemporaryFile(suffix='.xmcd') - try: - xmcdfile.write(xmcd.to_string()) - xmcdfile.flush() - xmcdfile.seek(0, 0) - xmcd2 = audiotools.XMCD.from_string(xmcdfile.read()) - self.assertEqual(dict(xmcd.fields.items()), - dict(xmcd2.fields.items())) - finally: - xmcdfile.close() - - #ensure that excessively long XMCD lines are wrapped properly - xmcd = audiotools.XMCD({"TTITLE0": u"l" + (u"o" * 512) + u"ng title", - "TTITLE1": u"track two", - "TTITLE2": u"track three", - "TTITLE4": u"track four", - "TTITLE5": u"track five"}, - [u"# xmcd"]) - xmcd2 = audiotools.XMCD.from_string(xmcd.to_string()) - self.assertEqual(dict(xmcd.fields.items()), - dict(xmcd2.fields.items())) - self.assert_(max(map(len, - cStringIO.StringIO(xmcd.to_string()).readlines())) < 80) - - #ensure that UTF-8 multi-byte characters aren't split - xmcd = audiotools.XMCD({"TTITLE0": u'\u30de\u30af\u30ed\u30b9' * 100, - "TTITLE1": u"a" + (u'\u30de\u30af\u30ed\u30b9' * 100), - "TTITLE2": u"ab" + (u'\u30de\u30af\u30ed\u30b9' * 100), - "TTITLE4": u"abc" + (u'\u30de\u30af\u30ed\u30b9' * 100), - "TTITLE5": u"track tw\xf3"}, - [u"# xmcd"]) - - xmcd2 = audiotools.XMCD.from_string(xmcd.to_string()) - self.assertEqual(dict(xmcd.fields.items()), - dict(xmcd2.fields.items())) - self.assert_(max(map(len, cStringIO.StringIO(xmcd.to_string()))) < 80) - - @LIB_CORE - def test_attrs(self): - for (xmcd_data, attrs) in zip( - self.XMCD_FILES, - [{"album_name": u"\u30de\u30af\u30ed\u30b9F O\u30fbS\u30fbT\u30fb3 \u5a18\u305f\u307e\u2640\uff3bDisk1\uff3d", - "artist_name": u"\u30de\u30af\u30ed\u30b9FRONTIER", - "year": u"2008", - "extra": u" YEAR: 2008"}, - {"album_name": u"Xenogears Light", - "artist_name": u"OneUp Studios", - "year": u"2005", - "extra": u" YEAR: 2005"}, - {"album_name": u"Bleach The Best CD", - "artist_name": u"Various Artists", - "year": u"2006", - "extra": u"SVWC-7421"}]): - xmcd_file = audiotools.XMCD.from_string(xmcd_data[0]) - - #first, check that attributes are retrieved properly - for key in attrs.keys(): - self.assertEqual(getattr(xmcd_file, key), - attrs[key]) - - #then, check that setting attributes round-trip properly - for xmcd_data in self.XMCD_FILES: - for (attr, new_value) in [ - ("album_name", u"New Album"), - ("artist_name", u"T\u00e9st N\u00e0me"), - ("year", u"2010"), - ("extra", u"Extra!" * 200)]: - xmcd_file = audiotools.XMCD.from_string(xmcd_data[0]) - setattr(xmcd_file, attr, new_value) - self.assertEqual(getattr(xmcd_file, attr), new_value) - - #finally, check that the file with set attributes - #round-trips properly - for xmcd_data in self.XMCD_FILES: - for (attr, new_value) in [ - ("album_name", u"New Album" * 8), - ("artist_name", u"T\u00e9st N\u00e0me" * 8), - ("year", u"2010"), - ("extra", u"Extra!" * 200)]: - xmcd_file = audiotools.XMCD.from_string(xmcd_data[0]) - setattr(xmcd_file, attr, new_value) - xmcd_file2 = audiotools.XMCD.from_string( - xmcd_file.to_string()) - self.assertEqual(getattr(xmcd_file2, attr), new_value) - self.assertEqual(getattr(xmcd_file, attr), - getattr(xmcd_file2, attr)) - - @LIB_CORE - def test_tracks(self): - for (xmcd_data, tracks) in zip( - self.XMCD_FILES, - [[(u'\u30c8\u30e9\u30a4\u30a2\u30f3\u30b0\u30e9\u30fc', - u'', u''), - (u"What 'bout my star? @Formo", - u'', u''), - (u'\u30a2\u30a4\u30e2', - u'', u''), - (u'\u30c0\u30a4\u30a2\u30e2\u30f3\u30c9 \u30af\u30ec\u30d0\u30b9\uff5e\u5c55\u671b\u516c\u5712\u306b\u3066', - u'', u''), - (u"Welcome To My FanClub's Night!", - u'', u''), - (u"\u5c04\u624b\u5ea7\u2606\u5348\u5f8c\u4e5d\u6642Don't be late", - u'', u''), - (u"What 'bout my star?", - u'', u''), - (u'\u30a4\u30f3\u30d5\u30a3\u30cb\u30c6\u30a3 #7', - u'', u''), - (u'\u300c\u8d85\u6642\u7a7a\u98ef\u5e97 \u5a18\u3005\u300d CM\u30bd\u30f3\u30b0 (Ranka Version)', - u'', u''), - (u'\u661f\u9593\u98db\u884c', - u'', u''), - (u'\u79c1\u306e\u5f7c\u306f\u30d1\u30a4\u30ed\u30c3\u30c8', - u'', u''), - (u'\u306d\u3053\u65e5\u8a18', - u'', u''), - (u'\u30cb\u30f3\u30b8\u30fc\u30f3 Loves you yeah!', - u'', u''), - (u'\u5b87\u5b99\u5144\u5f1f\u8239', - u'', u''), - (u'SMS\u5c0f\u968a\u306e\u6b4c\uff5e\u3042\u306e\u5a18\u306f\u30a8\u30a4\u30ea\u30a2\u30f3', - u'', u''), - (u'\u30a2\u30a4\u30e2 O.C.', - u'', u''), - (u'\u30a2\u30a4\u30e2\uff5e\u9ce5\u306e\u3072\u3068', - u'', u''), - (u'\u611b\u30fb\u304a\u307c\u3048\u3066\u3044\u307e\u3059\u304b', - u'', u''), - (u'\u30c0\u30a4\u30a2\u30e2\u30f3\u30c9 \u30af\u30ec\u30d0\u30b9', - u'', u''), - (u'\u30a2\u30a4\u30e2\uff5e\u3053\u3044\u306e\u3046\u305f\uff5e', - u'', u''), - (u'\u30a4\u30f3\u30d5\u30a3\u30cb\u30c6\u30a3 #7 without vocals', - u'', u''), - (u'\u30cb\u30f3\u30b8\u30fc\u30f3 Loves you yeah! without vocals', - u'', u'')], - [(u'Premonition', u'', u''), - (u'Grahf, Conqueror of Darkness', u'', u''), - (u'Tears of the Stars, Hearts of the People', - u'', u''), - (u'Far Away Promise', u'', u''), - (u'My Village Is Number One', u'', u''), - (u'Shevat, the Wind is Calling', u'', u''), - (u'Singing of the Gentle Wind', u'', u''), - (u'Shattering the Egg of Dreams', u'', u''), - (u'One Who Bares Fangs At God', u'', u''), - (u'Bonds of Sea and Fire', u'', u''), - (u'Ship of Sleep and Remorse', u'', u''), - (u'Broken Mirror', u'', u''), - (u'Dreams of the Strong', u'', u''), - (u'The Blue Traveler', u'', u''), - (u'October Mermaid', u'', u''), - (u'The Treasure Which Cannot Be Stolen', u'', u''), - (u'Valley Where the Wind is Born', u'', u''), - (u'Gathering Stars in the Night Sky', u'', u''), - (u'The Alpha and Omega', u'', u''), - (u'Into Eternal Sleep', u'', u'')], - [(u'\uff0a~\u30a2\u30b9\u30bf\u30ea\u30b9\u30af~', - u'ORANGE RANGE', u''), - (u'Life is Like a Boat', u'Rie fu', u''), - (u'\u30b5\u30f3\u30ad\u30e5\u30fc\uff01\uff01', - u'HOME MADE \u5bb6\u65cf', u''), - (u'D-tecnoLife', u'UVERworld', u''), - (u'\u307b\u3046\u304d\u661f', u'\u30e6\u30f3\u30ca', u''), - (u'happypeople', u'Skoop on Somebody', u''), - (u'\u4e00\u8f2a\u306e\u82b1', u'HIGH and MIGHTY COLOR', u''), - (u'LIFE', u'YUI', u''), - (u'\u30de\u30a4\u30da\u30fc\u30b9', u'SunSet Swish', u''), - (u'TONIGHT,TONIGHT,TONIGHT', u'BEAT CRUSADERS', u''), - (u'HANABI', u'\u3044\u304d\u3082\u306e\u304c\u304b\u308a', u''), - (u'MOVIN!!', u'\u30bf\u30ab\u30c1\u30e3', u'')]]): - xmcd_file = audiotools.XMCD.from_string(xmcd_data[0]) - - #first, check that tracks are read properly - for (i, data) in enumerate(tracks): - self.assertEqual(data, xmcd_file.get_track(i)) - - #then, check that setting tracks round-trip properly - for i in xrange(len(tracks)): - xmcd_file = audiotools.XMCD.from_string(xmcd_data[0]) - xmcd_file.set_track(i, - u"Track %d" % (i), - u"Art\u00ecst N\u00e4me" * 40, - u"Extr\u00e5" * 40) - self.assertEqual(xmcd_file.get_track(i), - (u"Track %d" % (i), - u"Art\u00ecst N\u00e4me" * 40, - u"Extr\u00e5" * 40)) - - #finally, check that a file with set tracks round-trips - for i in xrange(len(tracks)): - xmcd_file = audiotools.XMCD.from_string(xmcd_data[0]) - xmcd_file.set_track(i, - u"Track %d" % (i), - u"Art\u00ecst N\u00e4me" * 40, - u"Extr\u00e5" * 40) - xmcd_file2 = audiotools.XMCD.from_string( - xmcd_file.to_string()) - self.assertEqual(xmcd_file2.get_track(i), - (u"Track %d" % (i), - u"Art\u00ecst N\u00e4me" * 40, - u"Extr\u00e5" * 40)) - self.assertEqual(xmcd_file.get_track(i), - xmcd_file2.get_track(i)) - - @LIB_CORE - def test_from_tracks(self): - track_files = [tempfile.NamedTemporaryFile() for i in xrange(5)] - try: - tracks = [audiotools.FlacAudio.from_pcm( - track.name, - BLANK_PCM_Reader(1)) for track in track_files] - metadatas = [ - audiotools.MetaData(track_name=u"Track Name", - artist_name=u"Album Artist", - album_name=u"Test Album", - track_number=1, - track_total=5, - year=u"2010"), - audiotools.MetaData(track_name=u"Track Name 2", - artist_name=u"Album Artist", - album_name=u"Test Album", - track_number=2, - track_total=5, - year=u"2010"), - audiotools.MetaData(track_name=u"Track Name 4", - artist_name=u"Special Artist", - album_name=u"Test Album 2", - track_number=4, - track_total=5, - year=u"2009"), - audiotools.MetaData(track_name=u"Track N\u00e1me 3", - artist_name=u"Album Artist", - album_name=u"Test Album", - track_number=3, - track_total=5, - year=u"2010"), - audiotools.MetaData(track_name=u"Track Name 5" * 40, - artist_name=u"Album Artist", - album_name=u"Test Album", - track_number=5, - track_total=5, - year=u"2010")] - for (track, metadata) in zip(tracks, metadatas): - track.set_metadata(metadata) - self.assertEqual(track.get_metadata(), metadata) - xmcd = audiotools.XMCD.from_tracks(tracks) - self.assertEqual(len(xmcd), 5) - self.assertEqual(xmcd.album_name, u"Test Album") - self.assertEqual(xmcd.artist_name, u"Album Artist") - self.assertEqual(xmcd.year, u"2010") - self.assertEqual(xmcd.catalog, u"") - self.assertEqual(xmcd.extra, u"") - - #note that track 4 loses its intentionally malformed - #album name and year during the round-trip - for metadata in [ - audiotools.MetaData(track_name=u"Track Name", - artist_name=u"Album Artist", - album_name=u"Test Album", - track_number=1, - track_total=5, - year=u"2010"), - audiotools.MetaData(track_name=u"Track Name 2", - artist_name=u"Album Artist", - album_name=u"Test Album", - track_number=2, - track_total=5, - year=u"2010"), - audiotools.MetaData(track_name=u"Track Name 4", - artist_name=u"Special Artist", - album_name=u"Test Album", - track_number=4, - track_total=5, - year=u"2010"), - audiotools.MetaData(track_name=u"Track N\u00e1me 3", - artist_name=u"Album Artist", - album_name=u"Test Album", - track_number=3, - track_total=5, - year=u"2010"), - audiotools.MetaData(track_name=u"Track Name 5" * 40, - artist_name=u"Album Artist", - album_name=u"Test Album", - track_number=5, - track_total=5, - year=u"2010")]: - self.assertEqual(metadata, - xmcd.track_metadata(metadata.track_number)) - finally: - for track in track_files: - track.close() - - @LIB_CORE - def test_from_cuesheet(self): - CUESHEET = """REM DISCID 4A03DD06 -PERFORMER "Unknown Artist" -TITLE "Unknown Title" -FILE "cue.wav" WAVE - TRACK 01 AUDIO - TITLE "Track01" - INDEX 01 00:00:00 - TRACK 02 AUDIO - TITLE "Track02" - INDEX 00 03:00:21 - INDEX 01 03:02:21 - TRACK 03 AUDIO - TITLE "Track03" - INDEX 00 06:00:13 - INDEX 01 06:02:11 - TRACK 04 AUDIO - TITLE "Track04" - INDEX 00 08:23:32 - INDEX 01 08:25:32 - TRACK 05 AUDIO - TITLE "Track05" - INDEX 00 12:27:40 - INDEX 01 12:29:40 - TRACK 06 AUDIO - TITLE "Track06" - INDEX 00 14:32:05 - INDEX 01 14:34:05 -""" - cue_file = tempfile.NamedTemporaryFile(suffix=".cue") - try: - cue_file.write(CUESHEET) - cue_file.flush() - - #from_cuesheet wraps around from_tracks, - #so I don't need to hit this one so hard - xmcd = audiotools.XMCD.from_cuesheet( - cuesheet=audiotools.read_sheet(cue_file.name), - total_frames=43646652, - sample_rate=44100, - metadata=audiotools.MetaData(album_name=u"Test Album", - artist_name=u"Test Artist")) - - self.assertEqual(xmcd.album_name, u"Test Album") - self.assertEqual(xmcd.artist_name, u"Test Artist") - self.assertEqual(xmcd.year, u"") - self.assertEqual(xmcd.catalog, u"") - self.assertEqual(xmcd.extra, u"") - self.assertEqual(len(xmcd), 6) - for i in xrange(len(xmcd)): - self.assertEqual(xmcd.get_track(i), - (u"", u"", u"")) - finally: - cue_file.close() - - @LIB_CORE - def test_missing_fields(self): - xmcd_file_lines = ["# xmcd\r\n", - "DTITLE=Album Artist / Album Name\r\n", - "DYEAR=2010\r\n", - "TTITLE0=Track 1\r\n", - "TTITLE1=Track Artist / Track 2\r\n", - "TTITLE2=Track 3\r\n", - "EXTT0=Extra 1\r\n", - "EXTT1=Extra 2\r\n", - "EXTT2=Extra 3\r\n", - "EXTD=Disc Extra\r\n"] - - xmcd = audiotools.XMCD.from_string("".join(xmcd_file_lines)) - self.assertEqual(xmcd.album_name, u"Album Name") - self.assertEqual(xmcd.artist_name, u"Album Artist") - self.assertEqual(xmcd.year, u"2010") - self.assertEqual(xmcd.catalog, u"") - self.assertEqual(xmcd.extra, u"Disc Extra") - self.assertEqual(xmcd.get_track(0), - (u"Track 1", u"", u"Extra 1")) - self.assertEqual(xmcd.get_track(1), - (u"Track 2", u"Track Artist", u"Extra 2")) - self.assertEqual(xmcd.get_track(2), - (u"Track 3", u"", u"Extra 3")) - - lines = xmcd_file_lines[:] - del(lines[0]) - self.assertRaises(audiotools.XMCDException, - audiotools.XMCD.from_string, - "".join(lines)) - - lines = xmcd_file_lines[:] - del(lines[1]) - xmcd = audiotools.XMCD.from_string("".join(lines)) - self.assertEqual(xmcd.album_name, u"") - self.assertEqual(xmcd.artist_name, u"") - self.assertEqual(xmcd.year, u"2010") - self.assertEqual(xmcd.catalog, u"") - self.assertEqual(xmcd.extra, u"Disc Extra") - self.assertEqual(xmcd.get_track(0), - (u"Track 1", u"", u"Extra 1")) - self.assertEqual(xmcd.get_track(1), - (u"Track 2", u"Track Artist", u"Extra 2")) - self.assertEqual(xmcd.get_track(2), - (u"Track 3", u"", u"Extra 3")) - - lines = xmcd_file_lines[:] - del(lines[2]) - xmcd = audiotools.XMCD.from_string("".join(lines)) - self.assertEqual(xmcd.album_name, u"Album Name") - self.assertEqual(xmcd.artist_name, u"Album Artist") - self.assertEqual(xmcd.year, u"") - self.assertEqual(xmcd.catalog, u"") - self.assertEqual(xmcd.extra, u"Disc Extra") - self.assertEqual(xmcd.get_track(0), - (u"Track 1", u"", u"Extra 1")) - self.assertEqual(xmcd.get_track(1), - (u"Track 2", u"Track Artist", u"Extra 2")) - self.assertEqual(xmcd.get_track(2), - (u"Track 3", u"", u"Extra 3")) - - lines = xmcd_file_lines[:] - del(lines[3]) - xmcd = audiotools.XMCD.from_string("".join(lines)) - self.assertEqual(xmcd.album_name, u"Album Name") - self.assertEqual(xmcd.artist_name, u"Album Artist") - self.assertEqual(xmcd.year, u"2010") - self.assertEqual(xmcd.catalog, u"") - self.assertEqual(xmcd.extra, u"Disc Extra") - self.assertEqual(xmcd.get_track(0), - (u"", u"", u"")) - self.assertEqual(xmcd.get_track(1), - (u"Track 2", u"Track Artist", u"Extra 2")) - self.assertEqual(xmcd.get_track(2), - (u"Track 3", u"", u"Extra 3")) - - lines = xmcd_file_lines[:] - del(lines[4]) - xmcd = audiotools.XMCD.from_string("".join(lines)) - self.assertEqual(xmcd.album_name, u"Album Name") - self.assertEqual(xmcd.artist_name, u"Album Artist") - self.assertEqual(xmcd.year, u"2010") - self.assertEqual(xmcd.catalog, u"") - self.assertEqual(xmcd.extra, u"Disc Extra") - self.assertEqual(xmcd.get_track(0), - (u"Track 1", u"", u"Extra 1")) - self.assertEqual(xmcd.get_track(1), - (u"", u"", u"")) - self.assertEqual(xmcd.get_track(2), - (u"Track 3", u"", u"Extra 3")) - - lines = xmcd_file_lines[:] - del(lines[5]) - xmcd = audiotools.XMCD.from_string("".join(lines)) - self.assertEqual(xmcd.album_name, u"Album Name") - self.assertEqual(xmcd.artist_name, u"Album Artist") - self.assertEqual(xmcd.year, u"2010") - self.assertEqual(xmcd.catalog, u"") - self.assertEqual(xmcd.extra, u"Disc Extra") - self.assertEqual(xmcd.get_track(0), - (u"Track 1", u"", u"Extra 1")) - self.assertEqual(xmcd.get_track(1), - (u"Track 2", u"Track Artist", u"Extra 2")) - self.assertEqual(xmcd.get_track(2), - (u"", u"", u"")) - - lines = xmcd_file_lines[:] - del(lines[6]) - xmcd = audiotools.XMCD.from_string("".join(lines)) - self.assertEqual(xmcd.album_name, u"Album Name") - self.assertEqual(xmcd.artist_name, u"Album Artist") - self.assertEqual(xmcd.year, u"2010") - self.assertEqual(xmcd.catalog, u"") - self.assertEqual(xmcd.extra, u"Disc Extra") - self.assertEqual(xmcd.get_track(0), - (u"", u"", u"")) - self.assertEqual(xmcd.get_track(1), - (u"Track 2", u"Track Artist", u"Extra 2")) - self.assertEqual(xmcd.get_track(2), - (u"Track 3", u"", u"Extra 3")) - - lines = xmcd_file_lines[:] - del(lines[7]) - xmcd = audiotools.XMCD.from_string("".join(lines)) - self.assertEqual(xmcd.album_name, u"Album Name") - self.assertEqual(xmcd.artist_name, u"Album Artist") - self.assertEqual(xmcd.year, u"2010") - self.assertEqual(xmcd.catalog, u"") - self.assertEqual(xmcd.extra, u"Disc Extra") - self.assertEqual(xmcd.get_track(0), - (u"Track 1", u"", u"Extra 1")) - self.assertEqual(xmcd.get_track(1), - (u"", u"", u"")) - self.assertEqual(xmcd.get_track(2), - (u"Track 3", u"", u"Extra 3")) - - lines = xmcd_file_lines[:] - del(lines[8]) - xmcd = audiotools.XMCD.from_string("".join(lines)) - self.assertEqual(xmcd.album_name, u"Album Name") - self.assertEqual(xmcd.artist_name, u"Album Artist") - self.assertEqual(xmcd.year, u"2010") - self.assertEqual(xmcd.catalog, u"") - self.assertEqual(xmcd.extra, u"Disc Extra") - self.assertEqual(xmcd.get_track(0), - (u"Track 1", u"", u"Extra 1")) - self.assertEqual(xmcd.get_track(1), - (u"Track 2", u"Track Artist", u"Extra 2")) - self.assertEqual(xmcd.get_track(2), - (u"", u"", u"")) - - lines = xmcd_file_lines[:] - del(lines[9]) - xmcd = audiotools.XMCD.from_string("".join(lines)) - self.assertEqual(xmcd.album_name, u"Album Name") - self.assertEqual(xmcd.artist_name, u"Album Artist") - self.assertEqual(xmcd.year, u"2010") - self.assertEqual(xmcd.catalog, u"") - self.assertEqual(xmcd.extra, u"") - self.assertEqual(xmcd.get_track(0), - (u"Track 1", u"", u"Extra 1")) - self.assertEqual(xmcd.get_track(1), - (u"Track 2", u"Track Artist", u"Extra 2")) - self.assertEqual(xmcd.get_track(2), - (u"Track 3", u"", u"Extra 3")) - - @LIB_CORE - def test_metadata(self): - xmcd = audiotools.XMCD({"DTITLE": u"Album Artist / Album Name", - "DYEAR": u"2010", - "TTITLE0": u"Track 1", - "TTITLE1": u"Track 2", - "TTITLE2": u"Track 3"}, - [u"# xmcd"]) - self.assertEqual(xmcd.metadata(), - audiotools.MetaData(artist_name=u"Album Artist", - album_name=u"Album Name", - track_total=3, - year=u"2010")) - - -class TestMusicBrainzXML(unittest.TestCase): - XML_FILES = [( -"""QlpoOTFBWSZTWZHZsOEAAmLf+QAQeOf/9/1/3bA//99wf//K9x732X8f4FAGHp3dsABh2rtyIBUD -U0JiCaaZNJ5TBpqGjU2oeo09JoAZAMjQ0yBoGgaAaNGhp6E9Qip+EaZBoVKABpkaAAABoAAAAAAA -AAAAGU/AGiKNhR6npNAaAAANAAAAAAAAAAAAEGmJgAAAAAAAAAAAAAAjCMAAAACRQmggGhDTTVPJ -H6NU0DIejUNNlHqbUBmoDTQNAANA0AADN5RLP0eOEM/mcT4+2+SfHNdRRk+lrXyi2DVDhUA3BpKS -UhLoINkzquanlotv9PGs5xY5clNuvcvVLd0AwO1lx9K7n1nbrWefQoIZEg7p08WygJgBjCMXAuWL -aSakgglqhUchgiqtqpNQKCAakvsJANKGjEWUwzlgYZJCsEthxOMeGKG4K2pgHQCwJqXaV5Sxi4rm -SVxVEK+YOEm07ULFRFGF1B8CNoR02kIQORxurqm4bob4hbre+QrGJCwb+szLbl1rZe1NZhMojx4i -ocOccTgMKMyVrQQwiHQgQCiBKoCpbbbhSFUsM6ERGvOvhGLQbxapnFuBw81zDZAbgtevZuBXYlwe -62pJMU2K23PUgEwroQTY1Z613s2RZmuE1GARCzByvdOhW+szQjtriTiKXERJeKSM91nTZbkWGQrS -zp7YpVRXM3UcbnZMCoyJFwWiUCsRQdZXRqZnaARKTscCcS4iJBVcY2pBN0luuyIBu5C+gqIGUHMR -hTvi2pYmEqDiGhKDe8C4UIoyUKWplMbyLgHBRzGsZlBWbD1ihyHSC2tA9EtJ6CbVrpmcs4IVietG -zUfETxBIEXGZwGMA+s0RRvXcTzC51VQOhPgBZbyljbW5O4zVshxFNtZjMoeTqlCMTmwI4lixpDPt -ZrGGmBjeunrezi6XnWOHEDuq3q8g4q7CJA+sRdNyYQ0DDqRqU2nA0ksZtatMBm1BwYDgHlrCZVqw -kOe6WHTuRhErm7EUs2HUCaRRJSkpm4gwJF1285rvaDJZscjHe8XBFGumVMs50ENjJqn5/ydU0bmT -Wwg2x643BtuDg4OPZa04LcHP7UdWz0O10j5/F/S9LH+UPGn+ebt5EkLhYCW4WYrW4ptBHJGDLZAo -5+/4agFzqHVDwpALZdEqAE3qgOA0CmIi0KUqGIVwnz/AwNketGnqeb7MjkqgPerUKZcrhxQFWTn5 -bZjpNpabQQRBJHAIoqeZlZl+/es379a9RxHl31vLzXmrSHDqcYzuwG4n2TGjDTj7TeK23WnDgAcL -sFR4eHqQdxyJegRdEAZw0dDuaahUZc4T4MR+uNWqOi9rIdiAAMetaqFYbflOeeFNeaepNx5MdyJh -41y41X490KaUN5kE+SQBYCzyC5m4PTywUHZL7sw8A3UtWCGn1JE1AqWKNI3mEGc7kY4IktPEYZ9c -YTIbmjBHQYYwBlZFenCCXJFFAcUZSISAkRhT8bKeLLLIc7hIRlEKiqhznWW60y87uYzRvQ29hgLc -AXcGrmZs+fL4ahjvZJhs4as9FWfHTOOxGmycq47d+G3bcw6jDuAKoaGwQRPcg4M9WKCseZJ46Kjw -xR6igaSabIkIU1Tt6vDVxnTHXiyieoJ7EHWfhkDVuwClrYrLUrVpVHJDFuHStNdxGM2+6xsk2Vk2 -uhAkNOIDddEy1d95+BDseVVGVkgHfgU01jjLF800ohth9wGFo1ctUzReJxGALFKmLQ3qgIFKdxIF -hhjfNW7C+ZKxAmLd2UqJj9TgwX+dO9ZUFnd9hOpl8hoU6m1U9DAEyOCp2TuzmuvjKjAUhS7IWVl3 -R18lzwccNcvevCzP1oCBXeCjlOZGk0d1Mw7x6VpTw1Gxfeu85ClIFWQAFmk9Ojabb2uCgni6MTTe -ytRJl+K8QegMXQ00iotIG0sVttaComWXNeDsODekXSBejVllUlEoNpXYyKYK/cjFAKwwIFQgVIgX -MtObIBUNgKrAYjJmroiHYrAFpInfXsaslxwIhxXKlioaeIvH8L22A95Axja5zmMYBGtr7nuSPgzD -pJ2S4PmcbHewcGzhpNLMPDwegzwwZJv3YYmNDcmg7NePApT/5islCQ6AgfA5DIyGyBoEjCQUPU0A -hH8l+2x+drf3W9tm9uRe0f3AX6G7Yj2oRM3vtHvb04qlt26OazBgWgqZ98kSXP8lwRPWSuppyEWI -vCUDDrZiT4cevVmI9LRpPw/7DgctthGdx4P+LuSKcKEhI7Nhwg==""".decode('base64').decode('bz2'), - {1:audiotools.MetaData(track_name=u'Frontier 2059', track_number=1, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 2:audiotools.MetaData(track_name=u"Welcome To My FanClub's Night! (Sheryl On Stage)", track_number=2, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 3:audiotools.MetaData(track_name=u"What 'bout my star? (Sheryl On Stage)", track_number=3, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 4:audiotools.MetaData(track_name=u"\u5c04\u624b\u5ea7\u2606\u5348\u5f8c\u4e5d\u6642Don't be late (Sheryl On Stage)", track_number=4, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 5:audiotools.MetaData(track_name=u'Vital Force', track_number=5, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 6:audiotools.MetaData(track_name=u'\u30c8\u30e9\u30a4\u30a2\u30f3\u30b0\u30e9\u30fc', track_number=6, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 7:audiotools.MetaData(track_name=u'Zero Hour', track_number=7, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 8:audiotools.MetaData(track_name=u"What 'bout my star? @Formo", track_number=8, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 9:audiotools.MetaData(track_name=u'Innocent green', track_number=9, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 10:audiotools.MetaData(track_name=u'\u30a2\u30a4\u30e2', track_number=10, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 11:audiotools.MetaData(track_name=u'\u30d3\u30c3\u30b0\u30fb\u30dc\u30fc\u30a4\u30ba', track_number=11, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 12:audiotools.MetaData(track_name=u'Private Army', track_number=12, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 13:audiotools.MetaData(track_name=u'SMS\u5c0f\u968a\u306e\u6b4c\u301c\u3042\u306e\u5a18\u306f\u30a8\u30a4\u30ea\u30a2\u30f3', track_number=13, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 14:audiotools.MetaData(track_name=u'\u30cb\u30f3\u30b8\u30fc\u30f3 Loves you yeah!', track_number=14, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 15:audiotools.MetaData(track_name=u'\u8d85\u6642\u7a7a\u98ef\u5e97 \u5a18\u3005: CM\u30bd\u30f3\u30b0(Ranka Version)', track_number=15, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 16:audiotools.MetaData(track_name=u"Alto's Theme", track_number=16, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 17:audiotools.MetaData(track_name=u'Tally Ho!', track_number=17, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 18:audiotools.MetaData(track_name=u'The Target', track_number=18, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 19:audiotools.MetaData(track_name=u'Bajura', track_number=19, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 20:audiotools.MetaData(track_name=u'\u30ad\u30e9\u30ad\u30e9', track_number=20, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 21:audiotools.MetaData(track_name=u'\u30a2\u30a4\u30e2\u301c\u9ce5\u306e\u3072\u3068', track_number=21, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 22:audiotools.MetaData(track_name=u'Take Off', track_number=22, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 23:audiotools.MetaData(track_name=u'\u30a4\u30f3\u30d5\u30a3\u30cb\u30c6\u30a3', track_number=23, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u''), - 24:audiotools.MetaData(track_name=u'\u30c0\u30a4\u30a2\u30e2\u30f3\u30c9 \u30af\u30ec\u30d0\u30b9', track_number=24, track_total=24, album_name=u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', artist_name=u'\u83c5\u91ce\u3088\u3046\u5b50', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'VTCL-60060', copyright=u'', publisher=u'', year=u'2008', date=u'', album_number=0, album_total=0, comment=u'')}), - ( -"""QlpoOTFBWSZTWeDENZEAAz5fgAAQeef//7/f36A/799xYAcrsz7YABzkbI43Y2qaoDJCGTU2iZMm -NU0TMNJMjQepoGmgepggyEDQBJTUADyjTQABkAANNBzAEYJiAYBME0ZDQwCYIxMJCkamTJNqjaT0 -EbUPUGmgGRkGQAAcwBGCYgGATBNGQ0MAmCMTCKSYgIamlPRtRmhT2iDTITyhkG0jyhp6R7ft9O/i -1Z4/pPTR3rEndbINagfOT+z0r/acXK6koolQSF4RDaTfyoI9CdAf2Q+6+JfP58XljKSVU1jYzzsv -rxUEcNIiDTBtBYZYTFVzF0A1VJvW7m06MuQVuzR4vUQAFGcHeFFnWMEm8FVq6qJqbEzQY7rbK6Ht -qIIYMjFCBtu0Kgu9S2ICsWHCtVacniFimTBY7DoXQua5d7FuDdoQaI7j9Atk1vS7WB9OeZUNoZdb -Jh8ZzRmMZxD1rgPYXSVqTQ49QFKG8dGZ1mwhej0yDo6Bxd6YpMyuqauSI3gU1kOb5H5HKdqIViO0 -koeshQndohdYwJeDYy4GlnxbIpiIGW6ZW34jGcnGl7JHgzujXFHDKkYTtn1RTCY7hM3ZEghnCsZV -tN0FT6zXwo1rVuBEzmCxnRlcv8257p0KUrRqrHp+p1Tk6TrecakrEMGAbjiW+kEGOCynYNnhjLjU -jevIGSC2dXuxHShR0EbpUEoavBRa0bmbWx1npEYVQ3DzTJKGB0ctHoUzvdkzkoUqlr1siTbG1VK5 -EUfabNTHVcu/VJ8lvZnFVn01kCImuUqIkWNqLPlpPNUEtHTQmUIaCi1cNBDgXZsoN2XCIgMAi2IL -Q1XW7KKUETE3o9YbxRqxoCuw97jW8vIodbPNuHRsgUth5UDmVJadRqmJxZUSFMziL7k9ZCqz5vaW -FgcLRZ2ZoKwxeubSi44+ag8rykdMMX5mXIQbNdzTQG7C8Bmsq3MYQQQTNrIGgS4vIULY06UFebnd -6rBd8XDbpZpT4ajY5mC3MMX4WbArheSqtzyOIXaQOZhUKQeouZtZ2L3zkcGMRGBO7gWiDsyhFFRt -q6SllhMPhjYVWiJNLvuUMg6vuoEtGnSY21YtJbovyyy8Jv8UW8kF9u6SSSORscpsXGzYK/BWEtNm -GsujIUrKN2Ss56MbB3SRk4bxF+EKoUK3W2AsFkLICV23uqIVUKRGUCGExDlsoIcahKJAkKFQshGc -0ErO7j4BiaO0wgmJ2SNHzkTdJENkagwaYMaYe9wGJpMboKRMGNEZBjGM6GQQ3coRvVjxi3a5azbw -V2gVpxUXcpNNinvCQE4T8LeBx3Zmja767xrTY8PDsCQIxqNdWXop6PZ7MDd2/sAXTjstBQ/VrbRa -GgkePhNcp/8eC8fQodKhQP/gDae3RrBDxwWvs3cXPx/iZjzdEx1bXdbPxqYGMIsgT3qSaJY0YOkA -7y+c199/G44GbVbgQ3qdCYiJou5xOPJtgaqO+Bdpv1s4VPlrqYjss52VQc8gkJG62B5CWQ3pJ4ID -2nUsMdhqxaMSniimd0YCQNtlqY8II1ZfvgcTTKDmumwNSr9Jnc21qxPPKszuwbA6NCAkCuGukaY0 -ZzmgIVhczGuharoq2KBA1ggZ7z05EVtaCFpH2bMY3vGqjmChGcJqvF61SS9GPNCAki6WGNuBtHf/ -CE3h4ujYBYob9BCShBAMDChsTbaGk2x767N+WS14XpYH1zheB9tRfhwd5F91K66spY61AU7hX4dt -2Tf96Y49yUZ3E0YN7AZQ+cZNo+sOojVlHli5Ne5TOdW1EK3dfLr3NByIC3y737D62wz5VUFzVvzU -N4ACFBPjCrpRNQTVC8Z1ySDSAB2TeQC0C1dNofd58ZdJpx5BQ91DdLvXUhpDy65ZWYFhlAITweox -pdyuqQMaQMYCJyZkZAhjEGeFJufzPeuugmEDAoSCyyXMmxbLC8obFzimVLGy2SbICQZubln4UsTK -FSmMFEZk2yIQPFIKqFzJgGIIFgi2sd212zC6lNkrzMlOGps0Gek4N9LzrWfZecAwiHoL5395s37h -QwX9ABOznbOweXBfpbYNR22fwAX6WjNFiFCUcCQMgp4BBAF/8y2cvHEcvzLQ5OL03nDC5mZw6LaT -W9FZCg9QjtrHm+PNp5Zn2bw8qlCnfPkpA2E5xUKZkBwJAxJCxEpCpQIMbsZB7dYJ6sRQioEUZ11T -sVKGBzFcZXXrlOQBq14B8sRQoAmahcoY5ihS8xKCFYgQreW2rhgZYAcGy7y0RK484IQbjqDn69OU -Qav7keYLy1lhvaQNZW37i7kinChIcGIayIA=""".decode('base64').decode('bz2'), - {1:audiotools.MetaData(track_name=u'Scars Left by Time (feat. Dale North)', track_number=1, track_total=18, album_name=u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', artist_name=u'Ailsean', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'', copyright=u'', publisher=u'', year=u'', date=u'', album_number=0, album_total=0, comment=u''), - 2:audiotools.MetaData(track_name=u'Star Stealing Girl (feat. Miss Sara Broome)', track_number=2, track_total=18, album_name=u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', artist_name=u'The OneUps', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'', copyright=u'', publisher=u'', year=u'', date=u'', album_number=0, album_total=0, comment=u''), - 3:audiotools.MetaData(track_name=u"A Hero's Judgement (feat. Ailsean, Dale North & Roy McClanahan)", track_number=3, track_total=18, album_name=u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', artist_name=u'Matt Pollard', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'', copyright=u'', publisher=u'', year=u'', date=u'', album_number=0, album_total=0, comment=u''), - 4:audiotools.MetaData(track_name=u'Parallelism (The Frozen Flame)', track_number=4, track_total=18, album_name=u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', artist_name=u'Matt Pollard', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'', copyright=u'', publisher=u'', year=u'', date=u'', album_number=0, album_total=0, comment=u''), - 5:audiotools.MetaData(track_name=u'Guardian of Time (feat. Greg Kennedy)', track_number=5, track_total=18, album_name=u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', artist_name=u'Mustin', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'', copyright=u'', publisher=u'', year=u'', date=u'', album_number=0, album_total=0, comment=u''), - 6:audiotools.MetaData(track_name=u'The Boy Feared by Time', track_number=6, track_total=18, album_name=u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', artist_name=u'Ailsean', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'', copyright=u'', publisher=u'', year=u'', date=u'', album_number=0, album_total=0, comment=u''), - 7:audiotools.MetaData(track_name=u'The Girl Forgotten by Time', track_number=7, track_total=18, album_name=u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', artist_name=u'Mark Porter', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'', copyright=u'', publisher=u'', year=u'', date=u'', album_number=0, album_total=0, comment=u''), - 8:audiotools.MetaData(track_name=u'Wings of Time', track_number=8, track_total=18, album_name=u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', artist_name=u'Dale North', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'', copyright=u'', publisher=u'', year=u'', date=u'', album_number=0, album_total=0, comment=u''), - 9:audiotools.MetaData(track_name=u'Good to be Home', track_number=9, track_total=18, album_name=u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', artist_name=u'Dale North', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'', copyright=u'', publisher=u'', year=u'', date=u'', album_number=0, album_total=0, comment=u''), - 10:audiotools.MetaData(track_name=u'Dream of Another Time', track_number=10, track_total=18, album_name=u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', artist_name=u'Mustin', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'', copyright=u'', publisher=u'', year=u'', date=u'', album_number=0, album_total=0, comment=u''), - 11:audiotools.MetaData(track_name=u'Fields of Time', track_number=11, track_total=18, album_name=u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', artist_name=u'Mellogear vs. Mark Porter', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'', copyright=u'', publisher=u'', year=u'', date=u'', album_number=0, album_total=0, comment=u''), - 12:audiotools.MetaData(track_name=u'To Good Friends (feat. Tim Sheehy)', track_number=12, track_total=18, album_name=u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', artist_name=u'Dale North', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'', copyright=u'', publisher=u'', year=u'', date=u'', album_number=0, album_total=0, comment=u''), - 13:audiotools.MetaData(track_name=u'The Fighting Priest', track_number=13, track_total=18, album_name=u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', artist_name=u'Ailsean', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'', copyright=u'', publisher=u'', year=u'', date=u'', album_number=0, album_total=0, comment=u''), - 14:audiotools.MetaData(track_name=u'June Mermaid', track_number=14, track_total=18, album_name=u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', artist_name=u'Dale North', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'', copyright=u'', publisher=u'', year=u'', date=u'', album_number=0, album_total=0, comment=u''), - 15:audiotools.MetaData(track_name=u'Navigation is Key! (feat. Dale North)', track_number=15, track_total=18, album_name=u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', artist_name=u'Matt Pollard', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'', copyright=u'', publisher=u'', year=u'', date=u'', album_number=0, album_total=0, comment=u''), - 16:audiotools.MetaData(track_name=u'Gentle Wind', track_number=16, track_total=18, album_name=u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', artist_name=u'Dale North', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'', copyright=u'', publisher=u'', year=u'', date=u'', album_number=0, album_total=0, comment=u''), - 17:audiotools.MetaData(track_name=u'Star of Hope (feat. Mark Porter)', track_number=17, track_total=18, album_name=u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', artist_name=u'Dale North', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'', copyright=u'', publisher=u'', year=u'', date=u'', album_number=0, album_total=0, comment=u''), - 18:audiotools.MetaData(track_name=u'Shake the Heavens (feat. Matt Pollard & Dale North)', track_number=18, track_total=18, album_name=u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', artist_name=u'Mark Porter', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'', copyright=u'', publisher=u'', year=u'', date=u'', album_number=0, album_total=0, comment=u'')}), - ( -"""QlpoOTFBWSZTWborpk4AAb9f+QAQWIf/97//36A/79/xPeXqji45Q1ILwFAFBPIAg1bPdjQMGpkT -RGgmg9CbSZNGhpoaAMJ6EBoAAAaGgGj0hFPTRoyaBSjEaGmg0AaAAAAAAAAAAA4aAaAA0BoDQAAA -NNGmgDIAABo0yDDT1Up6mh6h6jJ6QABkDEABk0ABpoNMQAyAABJIEBMmjQ1PVTelPJk0T0n6o8oP -TKP1Jo9NT1PUNAAAAAA5jWBiUcwUqQeZFEVhfPM5ZoFkRkUyeSggCQZmRESkhAaLVnaTswolMTqp -VUriy58+eZUr/IogIFgJIqBg4DjeW5ErKKmgwAGWeGkB2zgzYEs+IZ+iyCFTtNww0FV4NO0wpGEW -ugQ4THUaiJTOpSo8eIBawqjIUtOtpyIbvia0AmlUWR15hznCDaz0WLrOQ3gOVAcbNyjkFAwkuXMx -ZVfdpfK/Tlhq0FLtPKEpqn0tOPcYtAm4CqUKmdXik1zmpOTxKUQSaxUBrQnVkSXgbroU1vFZT0Ty -CQSq1ye98wjZwQQMKj6RpjVDMJIOTgK8JA9xuqkMG4oYlPAZgxmzYmRSnLEHVrTC0GNInW4zogGs -hYDhh11gLMDqvR9bFBTuLxHI1Y3uECq4ARzgvBr2BRAwnJkgtYyQ3XC0b0tJoAyjZanQzhOQ1cJ1 -SLJZQsTILWnGkZuoYZrHI2KtBQZxioxjGZUoLMUluE3TqVDWKHeohCMQQXrUgiUFQovXAOwM3lOb -2QXvAxci0os2AUMHOor2uYOBHJHErAV2Y7SZoYjWjK5VyB63qRkIYoW2DXbDOyAJQG4uBxamr1+/ -qe3rNu0yGSPhRhR46vEP/Hd/X9/BlEBzsAiOnNwPchzkD3CtZzLsgk/N3ts4HGTsww0nOdsYDREI -sD4gMyOJA4ZwhkDpJRgkKHfxLvV5jMdR4SC20em+R5CDjm7fiK+oavx9BI6fZs5CuJaKxFkOsyFE -iS7o4JZXjh3r9W4WBhNNMTPJojE93sdZIy4jFZNG1rVZWNYUPmjEjBQJY3UKkBRiGLUCKFCbTsah -ChKCpMbUVUINMGyTz1CFXhHbyCCqMlG2PQxA6GpBBtFxdBgecg89BnF7d5ZJluGEATArSjyXG5Tb -iEyjQxJgMqrk0uxZU0UFbuKgYMKEwB0gjHlYtRlzYeMpgJVvp40gKwmJTDIA0roA6AJrV0xG7SRp -ua3UX3X54T3ZhBqnpM9NemyOq3s1oNYGE+N5jPkrtnByINkmdNlzyn0PG1V1xplVHHtbwvUZawu8 -sBwmBMIYAQMhAuBPwViZ+G/ckSCCG8DK8URNxh3QHCjAqBbU0cK9VDEcTccYsuJajyJGQs44kz0s -ty0ngwGHXTYo4OGaNEqSxqpQqm1ore2zhrM/FgdqNaHaLxtG6U0iDmOBHc291HesKgpYAtUJ3oGG -AZMzHGwgryCCc32uJqLdNAgiLfRuJQoKIk+yEAQTdvyJsGB1kAmsGQkk48yTrJQnABAgABPOihOU -yMhFVM08w8wtlqiXpkgh2GfuREHPEsRMeiWI0EyOfpQarp2kINnCPKADwG5uYMzNhvQMTPQ8qQ4H -CCAgoK8CsxTxre/rnFTMagyoLkFyKECvGKtIjW4nEDh1V74pJ5WTPHyamvNlgCKgRYgkgoEERwD2 -YCegjsMqDdN+VaW+AMYnBcp4HS4eFxdcgYpDyj+gF8PwDf3+jv5/z6YwvHbaV4nbSO9CzD7BoJQg -nbvdly53/Ea4Pe78RYPpd/V9AYf/k/36b75h+9/i7kinChIXRXTJwA==""".decode('base64').decode('bz2'), - {1:audiotools.MetaData(track_name=u'\u30e1\u30ea\u30c3\u30b5', track_number=1, track_total=8, album_name=u'FULLMETAL ALCHEMIST COMPLETE BEST', artist_name=u'\u30dd\u30eb\u30ce\u30b0\u30e9\u30d5\u30a3\u30c6\u30a3', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'SVWC-7218', copyright=u'', publisher=u'', year=u'2005', date=u'', album_number=0, album_total=0, comment=u''), - 2:audiotools.MetaData(track_name=u'\u6d88\u305b\u306a\u3044\u7f6a', track_number=2, track_total=8, album_name=u'FULLMETAL ALCHEMIST COMPLETE BEST', artist_name=u'\u5317\u51fa\u83dc\u5948', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'SVWC-7218', copyright=u'', publisher=u'', year=u'2005', date=u'', album_number=0, album_total=0, comment=u''), - 3:audiotools.MetaData(track_name=u'READY STEADY GO', track_number=3, track_total=8, album_name=u'FULLMETAL ALCHEMIST COMPLETE BEST', artist_name=u"L'Arc~en~Ciel", performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'SVWC-7218', copyright=u'', publisher=u'', year=u'2005', date=u'', album_number=0, album_total=0, comment=u''), - 4:audiotools.MetaData(track_name=u'\u6249\u306e\u5411\u3053\u3046\u3078', track_number=4, track_total=8, album_name=u'FULLMETAL ALCHEMIST COMPLETE BEST', artist_name=u'YeLLOW Generation', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'SVWC-7218', copyright=u'', publisher=u'', year=u'2005', date=u'', album_number=0, album_total=0, comment=u''), - 5:audiotools.MetaData(track_name=u'UNDO', track_number=5, track_total=8, album_name=u'FULLMETAL ALCHEMIST COMPLETE BEST', artist_name=u'COOL JOKE', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'SVWC-7218', copyright=u'', publisher=u'', year=u'2005', date=u'', album_number=0, album_total=0, comment=u''), - 6:audiotools.MetaData(track_name=u'Motherland', track_number=6, track_total=8, album_name=u'FULLMETAL ALCHEMIST COMPLETE BEST', artist_name=u'Crystal Kay', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'SVWC-7218', copyright=u'', publisher=u'', year=u'2005', date=u'', album_number=0, album_total=0, comment=u''), - 7:audiotools.MetaData(track_name=u'\u30ea\u30e9\u30a4\u30c8', track_number=7, track_total=8, album_name=u'FULLMETAL ALCHEMIST COMPLETE BEST', artist_name=u'ASIAN KUNG-FU GENERATION', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'SVWC-7218', copyright=u'', publisher=u'', year=u'2005', date=u'', album_number=0, album_total=0, comment=u''), - 8:audiotools.MetaData(track_name=u'I Will', track_number=8, track_total=8, album_name=u'FULLMETAL ALCHEMIST COMPLETE BEST', artist_name=u'Sowelu', performer_name=u'', composer_name=u'', conductor_name=u'', media=u'', ISRC=u'', catalog=u'SVWC-7218', copyright=u'', publisher=u'', year=u'2005', date=u'', album_number=0, album_total=0, comment=u'')})] - - @LIB_CORE - def testreading(self): - #check that reading in XML file data matches - #its expected values - for (xml, metadata) in self.XML_FILES: - mb_xml = audiotools.MusicBrainzReleaseXML.from_string(xml) - for i in xrange(len(mb_xml)): - self.assertEqual(mb_xml.track_metadata(i + 1), metadata[i + 1]) - - #check that reading in an XML file matches - #its expected values - for (xml, metadata) in self.XML_FILES: - f = tempfile.NamedTemporaryFile(suffix=".xml") - try: - f.write(xml) - f.flush() - f.seek(0, 0) - mb_xml = audiotools.MusicBrainzReleaseXML.from_string(f.read()) - for i in xrange(len(mb_xml)): - self.assertEqual(mb_xml.track_metadata(i + 1), - metadata[i + 1]) - finally: - f.close() - - @LIB_CORE - def testtracktagging(self): - for (xml, metadata) in self.XML_FILES: - #build a bunch of temporary FLAC files - temp_files = [tempfile.NamedTemporaryFile(suffix=".flac") - for i in metadata.keys()] - try: - temp_tracks = [audiotools.FlacAudio.from_pcm( - temp_file.name, - BLANK_PCM_Reader(5), - "1") for temp_file in temp_files] - for (i, track) in enumerate(temp_tracks): - track.set_metadata(audiotools.MetaData(track_number=i + 1)) - - #tag them with metadata from XML - xml_metadata = audiotools.MusicBrainzReleaseXML.from_string(xml) - for track in temp_tracks: - track.set_metadata( - xml_metadata.track_metadata(track.track_number())) - - #build a new XML file from track metadata - new_xml = audiotools.MusicBrainzReleaseXML.from_tracks( - temp_tracks) - - #check that the original XML values match the track ones - for i in xrange(len(new_xml)): - self.assertEqual(metadata[i + 1], - new_xml.track_metadata(i + 1)) - finally: - for t in temp_files: - t.close() - - @LIB_CORE - def testorder(self): - VALID_ORDER = \ -"""QlpoOTFBWSZTWQRNLWEAAMFfyQAQWGf/979fWCA/799wAIEJIQKgQAKLoc44CEqaSJ6bRMUzSejF -P1I9Q0GjyNTE0D1AD1CJiGgKeRRp6ZEAAGmgAAAAaEaiA0AAAAAABoAABIoQJ6ieo9JslGeVG9U0 -NPUBoaBkeUAJduZ5atyq+8Qc8hinE2gjl5at2wXrmqloSptHFn6YW86gJh7GnEnIAKMMoaQXMozq -1K7UmkegbTX00RcL0uyTdGC8Tme983GQhA7HG9bzzGQbhdre4hYMS3XjLNbnhrtDPc9Qcb8MMjmX -ym8V8hgpuGNwtUIIRolAixMpPW0GcINraYOOFjJLxWWC5sJUFqUIyF7q1JguFowcQRi8yXCyAkBu -eYnmBlYPxIJtedBnSs6IEbTkMosBGvk+dBhRIzc40cU11rKR+AX5sfbAAL7FSaN/OQrUpXKIAAQV -mzERCZ2ZzYgaEesQoAFlTdS40B41aoBnSQGgMgjhNVSK8Tlt/DI4GS69igp+lxwGDCsf3G13fFQY -2oJWjJpmpNDi0Guu4mihwtWdY5OHRZfoa1SkXbwjEY6Bn9CSuQTEIPassuTLFp8TAdTIK0oaMieM -MYonf4BIdUeufDDAKigH4ccczUCgOPYYyWxYZrEkXeRueqkwPhOIDY2ltvr9DR6VhvVkqY+ePzFM -pvxMOSfwvI7Oh23+Pb1dDyNL1nTn4oHKLMvOYiWCx8ETT2TNkmBq+tNcmhtiMxHStVhp00iONLHF -Koq1WRiFGPKcFBQsVENDV7AZOl11SKigtJKbdVJwWDV2Zr3mjgZWbYQQU9pnQdakbCPWXVuQiwjc -Bffsbb2bpGl6BmBPAJ+TGhKrqYuIiYnFbboQTuOeBUQIV8kaEokx0OycEFZNEkaBErSISbCrnLTK -dyoZiBkU31Oq3oLCLfCMIi75/brrrf67/F3JFOFCQBE0tYQ=""".decode('base64').decode('bz2') - - INVALID_ORDER = \ -"""QlpoOTFBWSZTWRPfcE8AAMHfyQAQWGf/979fWCA/799wAIEJIQKgQAKLoXcaghKmiRpkwk2TIZPU -j1MTRowTE0DQA9QiYhoCnkKaegQAA0aAA0AAIKJ+kmgyDIDCAaMgAaaNMQBhIkQnqelNqm0nspDe -pG1APUBoAZHlACc34Hlq4K1d4gzyH1MTeCMcWrhuF7MOmelNLaZuqHnbkUAxdy9mIogAowyhcF7K -M69Wu3NpHoG019VEXDGl+KbowXqdT3PB9yGz3uWK3nlOcNwupvcRQYluxGU5HXK60OW96g5H5MmJ -zMcpvFecwU3JhcLVCCEaJQIsTKT1tBnEDa2mDjhYyS8FlgubCVBalCMhe6tSYLhaMHEEYvMlwsgJ -AbnmJ5gZWD8CCbXnQZ0rOiBG05DKLARr5PnQYUSM3ONHFNdaykfiGPNh7oABfYqTPo4CJag5hUAC -wClmOyIhS4S+soJkaui++gXW7YPDMJTiYQ5QXwAiEjFMzEXhRkifr9SE+PIwG5vQgCAgNOKEqCnS -qkHa4skzTaMNRmYtBrrt1ooYclnWMoDLtsUtapSL5cw+j7oGP0JdUol0Qe13571+PR4lozvwakpa -PxvENwUUW7IkWu5sohigFRwFuKS5hagUhi3EhPtWGDakq3Ebj01FD16IabG0uXHR52j0LLeqTtl5 -pfMVS3HMy46/DEls6HHNlv5+hGkCSGSrpWSiqiYoGDwrFjYQb7ZecQCH1s2pttDEbRHgXFQvvtEd -hLPNK4u4qSkFmfZOChRRWRpaxYDLKmi6ZcWGBNVbutOCya17Tk3mngc9OWIQW9RtsOlTNhLpNehz -EUJawMcTYN7N0y96RmRXIK+LOxK7yMWokZmrDDSgrrOaC4gjRxysSkVHY6VhBoKomjSIngSCbYXc -xgc9dasZmA0qsKdVxQWEfGIfIyv5/a66+X9X/i7kinChICe+4J4=""".decode('base64').decode('bz2') - - self.assert_(VALID_ORDER != INVALID_ORDER) - - self.assertEqual(audiotools.MusicBrainzReleaseXML.from_string( - VALID_ORDER).metadata(), - audiotools.MusicBrainzReleaseXML.from_string( - INVALID_ORDER).metadata()) - - self.assertEqual(audiotools.MusicBrainzReleaseXML.from_string( - VALID_ORDER).to_string().replace('\n', ''), - VALID_ORDER.replace('\n', '')) - - self.assertEqual(audiotools.MusicBrainzReleaseXML.from_string( - INVALID_ORDER).to_string().replace('\n', ''), - VALID_ORDER.replace('\n', '')) - - @LIB_CORE - def test_attrs(self): - for (xml_data, attrs) in zip( - self.XML_FILES, - [{"album_name": u'\u30de\u30af\u30ed\u30b9\u30d5\u30ed\u30f3\u30c6\u30a3\u30a2: \u5a18\u30d5\u30ed', - "artist_name": u'\u83c5\u91ce\u3088\u3046\u5b50', - "year": u'2008', - "catalog": u'VTCL-60060', - "extra": u""}, - {"album_name": u'OneUp Studios presents Time & Space ~ A Tribute to Yasunori Mitsuda', - "artist_name": u'Various Artists', - "year": u'', - "catalog": u'', - "extra": u""}, - {"album_name": u'FULLMETAL ALCHEMIST COMPLETE BEST', - "artist_name": u'Various Artists', - "year": u'2005', - "catalog": u'SVWC-7218', - "extra": u""}]): - mb_xml = audiotools.MusicBrainzReleaseXML.from_string(xml_data[0]) - - #first, check that attributes are retrieved properly - for key in attrs.keys(): - self.assertEqual(getattr(mb_xml, key), - attrs[key]) - - #then, check that setting attributes round-trip properly - for (xml_data, xml_metadata) in self.XML_FILES: - for (attr, new_value) in [ - ("album_name", u"New Album"), - ("artist_name", u"T\u00e9st N\u00e0me"), - ("year", u"2010"), - ("catalog", u"Catalog #")]: - mb_xml = audiotools.MusicBrainzReleaseXML.from_string( - xml_data) - setattr(mb_xml, attr, new_value) - self.assertEqual(getattr(mb_xml, attr), new_value) - - #finally, check that the file with set attributes - #round-trips properly - for (xml_data, xml_metadata) in self.XML_FILES: - for (attr, new_value) in [ - ("album_name", u"New Album"), - ("artist_name", u"T\u00e9st N\u00e0me"), - ("year", u"2010"), - ("catalog", u"Catalog #")]: - mb_xml = audiotools.MusicBrainzReleaseXML.from_string( - xml_data) - setattr(mb_xml, attr, new_value) - mb_xml2 = audiotools.MusicBrainzReleaseXML.from_string( - mb_xml.to_string()) - self.assertEqual(getattr(mb_xml2, attr), new_value) - self.assertEqual(getattr(mb_xml, attr), - getattr(mb_xml2, attr)) - - @LIB_CORE - def test_tracks(self): - for ((xml_data, metadata), - track_metadata) in zip(self.XML_FILES, - [[(u"Frontier 2059", - u"", u""), - (u"Welcome To My FanClub's Night! (Sheryl On Stage)", - u"", u""), - (u"What 'bout my star? (Sheryl On Stage)", - u"", u""), - (u"\u5c04\u624b\u5ea7\u2606\u5348\u5f8c\u4e5d\u6642Don't be late (Sheryl On Stage)", - u"", u""), - (u"Vital Force", - u"", u""), - (u"\u30c8\u30e9\u30a4\u30a2\u30f3\u30b0\u30e9\u30fc", - u"", u""), - (u"Zero Hour", - u"", u""), - (u"What 'bout my star? @Formo", - u"", u""), - (u"Innocent green", - u"", u""), - (u"\u30a2\u30a4\u30e2", - u"", u""), - (u"\u30d3\u30c3\u30b0\u30fb\u30dc\u30fc\u30a4\u30ba", - u"", u""), - (u"Private Army", - u"", u""), - (u"SMS\u5c0f\u968a\u306e\u6b4c\u301c\u3042\u306e\u5a18\u306f\u30a8\u30a4\u30ea\u30a2\u30f3", - u"", u""), - (u"\u30cb\u30f3\u30b8\u30fc\u30f3 Loves you yeah!", - u"", u""), - (u"\u8d85\u6642\u7a7a\u98ef\u5e97 \u5a18\u3005: CM\u30bd\u30f3\u30b0(Ranka Version)", - u"", u""), - (u"Alto's Theme", - u"", u""), - (u"Tally Ho!", - u"", u""), - (u"The Target", - u"", u""), - (u"Bajura", - u"", u""), - (u"\u30ad\u30e9\u30ad\u30e9", - u"", u""), - (u"\u30a2\u30a4\u30e2\u301c\u9ce5\u306e\u3072\u3068", - u"", u""), - (u"Take Off", - u"", u""), - (u"\u30a4\u30f3\u30d5\u30a3\u30cb\u30c6\u30a3", - u"", u""), - (u"\u30c0\u30a4\u30a2\u30e2\u30f3\u30c9 \u30af\u30ec\u30d0\u30b9", - u"", u"")], - [(u"Scars Left by Time (feat. Dale North)", - u"Ailsean", u""), - (u"Star Stealing Girl (feat. Miss Sara Broome)", - u"The OneUps", u""), - (u"A Hero's Judgement (feat. Ailsean, Dale North & Roy McClanahan)", - u"Matt Pollard", u""), - (u"Parallelism (The Frozen Flame)", - u"Matt Pollard", u""), - (u"Guardian of Time (feat. Greg Kennedy)", - u"Mustin", u""), - (u"The Boy Feared by Time", - u"Ailsean", u""), - (u"The Girl Forgotten by Time", - u"Mark Porter", u""), - (u"Wings of Time", - u"Dale North", u""), - (u"Good to be Home", - u"Dale North", u""), - (u"Dream of Another Time", - u"Mustin", u""), - (u"Fields of Time", - u"Mellogear vs. Mark Porter", u""), - (u"To Good Friends (feat. Tim Sheehy)", - u"Dale North", u""), - (u"The Fighting Priest", - u"Ailsean", u""), - (u"June Mermaid", - u"Dale North", u""), - (u"Navigation is Key! (feat. Dale North)", - u"Matt Pollard", u""), - (u"Gentle Wind", - u"Dale North", u""), - (u"Star of Hope (feat. Mark Porter)", - u"Dale North", u""), - (u"Shake the Heavens (feat. Matt Pollard & Dale North)", - u"Mark Porter", u"")], - [(u"\u30e1\u30ea\u30c3\u30b5", - u"\u30dd\u30eb\u30ce\u30b0\u30e9\u30d5\u30a3\u30c6\u30a3", u""), - (u"\u6d88\u305b\u306a\u3044\u7f6a", - u"\u5317\u51fa\u83dc\u5948", u""), - (u"READY STEADY GO", - u"L'Arc~en~Ciel", u""), - (u"\u6249\u306e\u5411\u3053\u3046\u3078", - u"YeLLOW Generation", u""), - (u"UNDO", - u"COOL JOKE", u""), - (u"Motherland", - u"Crystal Kay", u""), - (u"\u30ea\u30e9\u30a4\u30c8", - u"ASIAN KUNG-FU GENERATION", u""), - (u"I Will", - u"Sowelu", u"")]]): - mb_xml = audiotools.MusicBrainzReleaseXML.from_string(xml_data) - - #first, check that tracks are read properly - for (i, data) in enumerate(metadata): - self.assertEqual(track_metadata[i], - mb_xml.get_track(i)) - - #then, check that setting tracks round-trip properly - for i in xrange(len(metadata)): - mb_xml = audiotools.MusicBrainzReleaseXML.from_string( - xml_data) - mb_xml.set_track(i, - u"Track %d" % (i), - u"Art\u00ecst N\u00e4me" * 40, - u"") - self.assertEqual(mb_xml.get_track(i), - (u"Track %d" % (i), - u"Art\u00ecst N\u00e4me" * 40, - u"")) - - #finally, check that a file with set tracks round-trips - for i in xrange(len(metadata)): - mb_xml = audiotools.MusicBrainzReleaseXML.from_string( - xml_data) - mb_xml.set_track(i, - u"Track %d" % (i), - u"Art\u00ecst N\u00e4me" * 40, - u"") - mb_xml2 = audiotools.MusicBrainzReleaseXML.from_string( - mb_xml.to_string()) - self.assertEqual(mb_xml2.get_track(i), - (u"Track %d" % (i), - u"Art\u00ecst N\u00e4me" * 40, - u"")) - self.assertEqual(mb_xml.get_track(i), - mb_xml2.get_track(i)) - - @LIB_CORE - def test_from_tracks(self): - track_files = [tempfile.NamedTemporaryFile() for i in xrange(5)] - try: - tracks = [audiotools.FlacAudio.from_pcm( - track.name, - BLANK_PCM_Reader(1)) for track in track_files] - metadatas = [ - audiotools.MetaData(track_name=u"Track Name", - artist_name=u"Album Artist", - album_name=u"Test Album", - track_number=1, - track_total=5, - year=u"2010"), - audiotools.MetaData(track_name=u"Track Name 2", - artist_name=u"Album Artist", - album_name=u"Test Album", - track_number=2, - track_total=5, - year=u"2010"), - audiotools.MetaData(track_name=u"Track Name 4", - artist_name=u"Special Artist", - album_name=u"Test Album 2", - track_number=4, - track_total=5, - year=u"2009"), - audiotools.MetaData(track_name=u"Track N\u00e1me 3", - artist_name=u"Album Artist", - album_name=u"Test Album", - track_number=3, - track_total=5, - year=u"2010"), - audiotools.MetaData(track_name=u"Track Name 5" * 40, - artist_name=u"Album Artist", - album_name=u"Test Album", - track_number=5, - track_total=5, - year=u"2010")] - for (track, metadata) in zip(tracks, metadatas): - track.set_metadata(metadata) - self.assertEqual(track.get_metadata(), metadata) - mb_xml = audiotools.MusicBrainzReleaseXML.from_tracks(tracks) - self.assertEqual(len(mb_xml), 5) - self.assertEqual(mb_xml.album_name, u"Test Album") - self.assertEqual(mb_xml.artist_name, u"Album Artist") - self.assertEqual(mb_xml.year, u"2010") - self.assertEqual(mb_xml.catalog, u"") - self.assertEqual(mb_xml.extra, u"") - - #note that track 4 loses its intentionally malformed - #album name and year during the round-trip - for metadata in [ - audiotools.MetaData(track_name=u"Track Name", - artist_name=u"Album Artist", - album_name=u"Test Album", - track_number=1, - track_total=5, - year=u"2010"), - audiotools.MetaData(track_name=u"Track Name 2", - artist_name=u"Album Artist", - album_name=u"Test Album", - track_number=2, - track_total=5, - year=u"2010"), - audiotools.MetaData(track_name=u"Track Name 4", - artist_name=u"Special Artist", - album_name=u"Test Album", - track_number=4, - track_total=5, - year=u"2010"), - audiotools.MetaData(track_name=u"Track N\u00e1me 3", - artist_name=u"Album Artist", - album_name=u"Test Album", - track_number=3, - track_total=5, - year=u"2010"), - audiotools.MetaData(track_name=u"Track Name 5" * 40, - artist_name=u"Album Artist", - album_name=u"Test Album", - track_number=5, - track_total=5, - year=u"2010")]: - self.assertEqual(metadata, - mb_xml.track_metadata(metadata.track_number)) - finally: - for track in track_files: - track.close() - - @LIB_CORE - def test_from_cuesheet(self): - CUESHEET = """REM DISCID 4A03DD06 -PERFORMER "Unknown Artist" -TITLE "Unknown Title" -FILE "cue.wav" WAVE - TRACK 01 AUDIO - TITLE "Track01" - INDEX 01 00:00:00 - TRACK 02 AUDIO - TITLE "Track02" - INDEX 00 03:00:21 - INDEX 01 03:02:21 - TRACK 03 AUDIO - TITLE "Track03" - INDEX 00 06:00:13 - INDEX 01 06:02:11 - TRACK 04 AUDIO - TITLE "Track04" - INDEX 00 08:23:32 - INDEX 01 08:25:32 - TRACK 05 AUDIO - TITLE "Track05" - INDEX 00 12:27:40 - INDEX 01 12:29:40 - TRACK 06 AUDIO - TITLE "Track06" - INDEX 00 14:32:05 - INDEX 01 14:34:05 -""" - cue_file = tempfile.NamedTemporaryFile(suffix=".cue") - try: - cue_file.write(CUESHEET) - cue_file.flush() - - #from_cuesheet wraps around from_tracks, - #so I don't need to hit this one so hard - mb_xml = audiotools.MusicBrainzReleaseXML.from_cuesheet( - cuesheet=audiotools.read_sheet(cue_file.name), - total_frames=43646652, - sample_rate=44100, - metadata=audiotools.MetaData(album_name=u"Test Album", - artist_name=u"Test Artist")) - - self.assertEqual(mb_xml.album_name, u"Test Album") - self.assertEqual(mb_xml.artist_name, u"Test Artist") - self.assertEqual(mb_xml.year, u"") - self.assertEqual(mb_xml.catalog, u"") - self.assertEqual(mb_xml.extra, u"") - self.assertEqual(len(mb_xml), 6) - for i in xrange(len(mb_xml)): - self.assertEqual(mb_xml.get_track(i), - (u"", u"", u"")) - finally: - cue_file.close() - - @LIB_CORE - def test_missing_fields(self): - def remove_node(parent, *to_remove): - toremove_parent = audiotools.walk_xml_tree(parent, - *to_remove[0:-1]) - if (len(to_remove) > 2): - self.assertEqual(toremove_parent.tagName, to_remove[-2]) - toremove = audiotools.walk_xml_tree(toremove_parent, - to_remove[-1]) - self.assertEqual(toremove.tagName, to_remove[-1]) - toremove_parent.removeChild(toremove) - - from xml.dom.minidom import parseString - - xml_data = """<?xml version="1.0" encoding="utf-8"?><metadata xmlns="http://musicbrainz.org/ns/mmd-1.0#" xmlns:ext="http://musicbrainz.org/ns/ext-1.0#"><release-list><release><title>Album Name</title><artist><name>Album Artist</name></artist><release-event-list><event date="2010" catalog-number="cat#"/></release-event-list><track-list><track><title>Track 1</title><duration>272000</duration></track><track><title>Track 2</title><artist><name>Track Artist</name></artist><duration>426333</duration></track><track><title>Track 3</title><duration>249560</duration></track></track-list></release></release-list></metadata>""" - - xml_dom = parseString(xml_data) - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"Album Name") - self.assertEqual(xml.artist_name, u"Album Artist") - self.assertEqual(xml.year, u"2010") - self.assertEqual(xml.catalog, u"cat#") - self.assertEqual(xml.extra, u"") - self.assertEqual(xml.get_track(0), - (u"Track 1", u"", u"")) - self.assertEqual(xml.get_track(1), - (u"Track 2", u"Track Artist", u"")) - self.assertEqual(xml.get_track(2), - (u"Track 3", u"", u"")) - - xml_dom = parseString(xml_data) - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"Album Name") - self.assertEqual(xml.artist_name, u"Album Artist") - self.assertEqual(xml.year, u"2010") - self.assertEqual(xml.catalog, u"cat#") - self.assertEqual(xml.extra, u"") - self.assertEqual(xml.get_track(0), - (u"Track 1", u"", u"")) - self.assertEqual(xml.get_track(1), - (u"Track 2", u"Track Artist", u"")) - self.assertEqual(xml.get_track(2), - (u"Track 3", u"", u"")) - - #removing <metadata> - xml_dom = parseString(xml_data) - xml_dom.removeChild(xml_dom.firstChild) - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"") - self.assertEqual(xml.artist_name, u"") - self.assertEqual(xml.year, u"") - self.assertEqual(xml.catalog, u"") - self.assertEqual(xml.extra, u"") - self.assertEqual(len(xml), 0) - - #removing <release-list> - xml_dom = parseString(xml_data) - remove_node(xml_dom, u'metadata', u'release-list') - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"") - self.assertEqual(xml.artist_name, u"") - self.assertEqual(xml.year, u"") - self.assertEqual(xml.catalog, u"") - self.assertEqual(xml.extra, u"") - self.assertEqual(len(xml), 0) - - #removing <release> - xml_dom = parseString(xml_data) - remove_node(xml_dom, u'metadata', u'release-list', u'release') - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"") - self.assertEqual(xml.artist_name, u"") - self.assertEqual(xml.year, u"") - self.assertEqual(xml.catalog, u"") - self.assertEqual(xml.extra, u"") - self.assertEqual(len(xml), 0) - - #removing <title> - xml_dom = parseString(xml_data) - remove_node(xml_dom, u'metadata', u'release-list', u'release', - u'title') - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"") - self.assertEqual(xml.artist_name, u"Album Artist") - self.assertEqual(xml.year, u"2010") - self.assertEqual(xml.catalog, u"cat#") - self.assertEqual(xml.extra, u"") - self.assertEqual(xml.get_track(0), - (u"Track 1", u"", u"")) - self.assertEqual(xml.get_track(1), - (u"Track 2", u"Track Artist", u"")) - self.assertEqual(xml.get_track(2), - (u"Track 3", u"", u"")) - - #removing <artist> - xml_dom = parseString(xml_data) - remove_node(xml_dom, u'metadata', u'release-list', u'release', - u'artist') - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"Album Name") - self.assertEqual(xml.artist_name, u"") - self.assertEqual(xml.year, u"2010") - self.assertEqual(xml.catalog, u"cat#") - self.assertEqual(xml.extra, u"") - self.assertEqual(xml.get_track(0), - (u"Track 1", u"", u"")) - self.assertEqual(xml.get_track(1), - (u"Track 2", u"Track Artist", u"")) - self.assertEqual(xml.get_track(2), - (u"Track 3", u"", u"")) - - #removing <artist> -> <name> - xml_dom = parseString(xml_data) - remove_node(xml_dom, u'metadata', u'release-list', u'release', - u'artist', u'name') - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"Album Name") - self.assertEqual(xml.artist_name, u"") - self.assertEqual(xml.year, u"2010") - self.assertEqual(xml.catalog, u"cat#") - self.assertEqual(xml.extra, u"") - self.assertEqual(xml.get_track(0), - (u"Track 1", u"", u"")) - self.assertEqual(xml.get_track(1), - (u"Track 2", u"Track Artist", u"")) - self.assertEqual(xml.get_track(2), - (u"Track 3", u"", u"")) - - #removing <release-event-list> - xml_dom = parseString(xml_data) - remove_node(xml_dom, u'metadata', u'release-list', u'release', - u'release-event-list') - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"Album Name") - self.assertEqual(xml.artist_name, u"Album Artist") - self.assertEqual(xml.year, u"") - self.assertEqual(xml.catalog, u"") - self.assertEqual(xml.extra, u"") - self.assertEqual(xml.get_track(0), - (u"Track 1", u"", u"")) - self.assertEqual(xml.get_track(1), - (u"Track 2", u"Track Artist", u"")) - self.assertEqual(xml.get_track(2), - (u"Track 3", u"", u"")) - - #removing <release-event-list> -> <event> - xml_dom = parseString(xml_data) - remove_node(xml_dom, u'metadata', u'release-list', u'release', - u'release-event-list', u'event') - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"Album Name") - self.assertEqual(xml.artist_name, u"Album Artist") - self.assertEqual(xml.year, u"") - self.assertEqual(xml.catalog, u"") - self.assertEqual(xml.extra, u"") - self.assertEqual(xml.get_track(0), - (u"Track 1", u"", u"")) - self.assertEqual(xml.get_track(1), - (u"Track 2", u"Track Artist", u"")) - self.assertEqual(xml.get_track(2), - (u"Track 3", u"", u"")) - - #removing <track-list> - xml_dom = parseString(xml_data) - remove_node(xml_dom, u'metadata', u'release-list', u'release', - u'track-list') - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"Album Name") - self.assertEqual(xml.artist_name, u"Album Artist") - self.assertEqual(xml.year, u"2010") - self.assertEqual(xml.catalog, u"cat#") - self.assertEqual(xml.extra, u"") - self.assertEqual(len(xml), 0) - - #removing <track> (1) - xml_dom = parseString(xml_data) - track_list = audiotools.walk_xml_tree(xml_dom, u'metadata', - u'release-list', u'release', - u'track-list') - self.assertEqual(track_list.tagName, u'track-list') - track_list.removeChild(track_list.childNodes[0]) - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"Album Name") - self.assertEqual(xml.artist_name, u"Album Artist") - self.assertEqual(xml.year, u"2010") - self.assertEqual(xml.catalog, u"cat#") - self.assertEqual(xml.extra, u"") - self.assertEqual(len(xml), 2) - self.assertEqual(xml.get_track(0), - (u"Track 2", u"Track Artist", u"")) - self.assertEqual(xml.get_track(1), - (u"Track 3", u"", u"")) - - #removing <track> (1) -> <title> - xml_dom = parseString(xml_data) - track_list = audiotools.walk_xml_tree(xml_dom, u'metadata', - u'release-list', u'release', - u'track-list') - self.assertEqual(track_list.tagName, u'track-list') - remove_node(track_list.childNodes[0], u'title') - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"Album Name") - self.assertEqual(xml.artist_name, u"Album Artist") - self.assertEqual(xml.year, u"2010") - self.assertEqual(xml.catalog, u"cat#") - self.assertEqual(xml.extra, u"") - self.assertEqual(len(xml), 3) - self.assertEqual(xml.get_track(0), - (u"", u"", u"")) - self.assertEqual(xml.get_track(1), - (u"Track 2", u"Track Artist", u"")) - self.assertEqual(xml.get_track(2), - (u"Track 3", u"", u"")) - - #removing <track> (2) - xml_dom = parseString(xml_data) - track_list = audiotools.walk_xml_tree(xml_dom, u'metadata', - u'release-list', u'release', - u'track-list') - self.assertEqual(track_list.tagName, u'track-list') - track_list.removeChild(track_list.childNodes[1]) - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"Album Name") - self.assertEqual(xml.artist_name, u"Album Artist") - self.assertEqual(xml.year, u"2010") - self.assertEqual(xml.catalog, u"cat#") - self.assertEqual(xml.extra, u"") - self.assertEqual(len(xml), 2) - self.assertEqual(xml.get_track(0), - (u"Track 1", u"", u"")) - self.assertEqual(xml.get_track(1), - (u"Track 3", u"", u"")) - - #removing <track> (2) -> <title> - xml_dom = parseString(xml_data) - track_list = audiotools.walk_xml_tree(xml_dom, u'metadata', - u'release-list', u'release', - u'track-list') - self.assertEqual(track_list.tagName, u'track-list') - remove_node(track_list.childNodes[1], u'title') - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"Album Name") - self.assertEqual(xml.artist_name, u"Album Artist") - self.assertEqual(xml.year, u"2010") - self.assertEqual(xml.catalog, u"cat#") - self.assertEqual(xml.extra, u"") - self.assertEqual(len(xml), 3) - self.assertEqual(xml.get_track(0), - (u"Track 1", u"", u"")) - self.assertEqual(xml.get_track(1), - (u"", u"Track Artist", u"")) - self.assertEqual(xml.get_track(2), - (u"Track 3", u"", u"")) - - #removing <track> (2) -> <artist> - xml_dom = parseString(xml_data) - track_list = audiotools.walk_xml_tree(xml_dom, u'metadata', - u'release-list', u'release', - u'track-list') - self.assertEqual(track_list.tagName, u'track-list') - remove_node(track_list.childNodes[1], u'artist') - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"Album Name") - self.assertEqual(xml.artist_name, u"Album Artist") - self.assertEqual(xml.year, u"2010") - self.assertEqual(xml.catalog, u"cat#") - self.assertEqual(xml.extra, u"") - self.assertEqual(len(xml), 3) - self.assertEqual(xml.get_track(0), - (u"Track 1", u"", u"")) - self.assertEqual(xml.get_track(1), - (u"Track 2", u"", u"")) - self.assertEqual(xml.get_track(2), - (u"Track 3", u"", u"")) - - #removing <track> (2) -> <artist> -> <name> - xml_dom = parseString(xml_data) - track_list = audiotools.walk_xml_tree(xml_dom, u'metadata', - u'release-list', u'release', - u'track-list') - self.assertEqual(track_list.tagName, u'track-list') - remove_node(track_list.childNodes[1], u'artist', u'name') - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"Album Name") - self.assertEqual(xml.artist_name, u"Album Artist") - self.assertEqual(xml.year, u"2010") - self.assertEqual(xml.catalog, u"cat#") - self.assertEqual(xml.extra, u"") - self.assertEqual(len(xml), 3) - self.assertEqual(xml.get_track(0), - (u"Track 1", u"", u"")) - self.assertEqual(xml.get_track(1), - (u"Track 2", u"", u"")) - self.assertEqual(xml.get_track(2), - (u"Track 3", u"", u"")) - - #removing <track> (3) - xml_dom = parseString(xml_data) - track_list = audiotools.walk_xml_tree(xml_dom, u'metadata', - u'release-list', u'release', - u'track-list') - self.assertEqual(track_list.tagName, u'track-list') - track_list.removeChild(track_list.childNodes[2]) - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"Album Name") - self.assertEqual(xml.artist_name, u"Album Artist") - self.assertEqual(xml.year, u"2010") - self.assertEqual(xml.catalog, u"cat#") - self.assertEqual(xml.extra, u"") - self.assertEqual(len(xml), 2) - self.assertEqual(xml.get_track(0), - (u"Track 1", u"", u"")) - self.assertEqual(xml.get_track(1), - (u"Track 2", u"Track Artist", u"")) - - #removing <track> (3) -> <title> - xml_dom = parseString(xml_data) - track_list = audiotools.walk_xml_tree(xml_dom, u'metadata', - u'release-list', u'release', - u'track-list') - self.assertEqual(track_list.tagName, u'track-list') - remove_node(track_list.childNodes[2], u'title') - xml = audiotools.MusicBrainzReleaseXML(xml_dom) - self.assertEqual(xml.album_name, u"Album Name") - self.assertEqual(xml.artist_name, u"Album Artist") - self.assertEqual(xml.year, u"2010") - self.assertEqual(xml.catalog, u"cat#") - self.assertEqual(xml.extra, u"") - self.assertEqual(len(xml), 3) - self.assertEqual(xml.get_track(0), - (u"Track 1", u"", u"")) - self.assertEqual(xml.get_track(1), - (u"Track 2", u"Track Artist", u"")) - self.assertEqual(xml.get_track(2), - (u"", u"", u"")) - - @LIB_CORE - def test_metadata(self): - xml = audiotools.MusicBrainzReleaseXML.from_string( - """<?xml version="1.0" encoding="utf-8"?><metadata xmlns="http://musicbrainz.org/ns/mmd-1.0#" xmlns:ext="http://musicbrainz.org/ns/ext-1.0#"><release-list><release><title>Album Name</title><artist><name>Album Artist</name></artist><release-event-list><event date="2010"/></release-event-list><track-list><track><title>Track 1</title><duration>272000</duration></track><track><title>Track 2</title><duration>426333</duration></track><track><title>Track 3</title><duration>249560</duration></track></track-list></release></release-list></metadata>""") - - self.assertEqual(xml.metadata(), - audiotools.MetaData(artist_name=u"Album Artist", - album_name=u"Album Name", - track_total=3, - year=u"2010")) - class testcuesheet(unittest.TestCase): @LIB_CORE @@ -5349,7 +3883,8 @@ (200347, ), (204985, ), (227336, ), (243382, 243549), (265893, 266032), (292606, 292942), (302893, 303123), (321611, )]) - self.assertEqual(list(sheet.pcm_lengths(191795016)), + self.assertEqual(list(sheet.pcm_lengths(191795016, + 44100)), [12280380, 12657288, 4152456, 1929228, 9938376, 15153936, 13525176, 10900344, 940212, 10492860, 7321776, 11084976, @@ -5375,8 +3910,10 @@ self.assertEqual(sheet.catalog(), cue_sheet.catalog()) self.assertEqual(list(sheet.indexes()), list(cue_sheet.indexes())) - self.assertEqual(list(sheet.pcm_lengths(191795016)), - list(cue_sheet.pcm_lengths(191795016))) + self.assertEqual(list(sheet.pcm_lengths(191795016, + 44100)), + list(cue_sheet.pcm_lengths(191795016, + 44100))) self.assertEqual(sorted(sheet.ISRCs().items()), sorted(cue_sheet.ISRCs().items())) finally: @@ -5394,8 +3931,10 @@ self.assertEqual(sheet.catalog(), toc_sheet.catalog()) self.assertEqual(list(sheet.indexes()), list(toc_sheet.indexes())) - self.assertEqual(list(sheet.pcm_lengths(191795016)), - list(toc_sheet.pcm_lengths(191795016))) + self.assertEqual(list(sheet.pcm_lengths(191795016, + 44100)), + list(toc_sheet.pcm_lengths(191795016, + 44100))) self.assertEqual(sorted(sheet.ISRCs().items()), sorted(toc_sheet.ISRCs().items())) finally: @@ -5418,8 +3957,10 @@ self.assertEqual(sheet.catalog(), f_sheet.catalog()) self.assertEqual(list(sheet.indexes()), list(f_sheet.indexes())) - self.assertEqual(list(sheet.pcm_lengths(191795016)), - list(f_sheet.pcm_lengths(191795016))) + self.assertEqual(list(sheet.pcm_lengths(191795016, + 44100)), + list(f_sheet.pcm_lengths(191795016, + 44100))) self.assertEqual(sorted(sheet.ISRCs().items()), sorted(f_sheet.ISRCs().items())) finally: @@ -5463,177 +4004,177 @@ class testflaccuesheet(testcuesheet): @LIB_CORE def setUp(self): - self.sheet_class = audiotools.Flac_CUESHEET + self.sheet_class = audiotools.flac.Flac_CUESHEET self.suffix = '.flac' self.test_sheets = [ - audiotools.Flac_CUESHEET( + audiotools.flac.Flac_CUESHEET( catalog_number='4580226563955\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', lead_in_samples=88200, is_cdda=1, - tracks=[audiotools.Flac_CUESHEET_track( + tracks=[audiotools.flac.Flac_CUESHEET_track( offset=0, number=1, ISRC='JPG750800183', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=12280380, number=2, ISRC='JPG750800212', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=24807132, number=3, ISRC='JPG750800214', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 0), - audiotools.Flac_CUESHEET_index(130536, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 0), + audiotools.flac.Flac_CUESHEET_index(130536, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=28954296, number=4, ISRC='JPG750800704', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 0), - audiotools.Flac_CUESHEET_index(135828, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 0), + audiotools.flac.Flac_CUESHEET_index(135828, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=31019352, number=5, ISRC='JPG750800705', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=40957728, number=6, ISRC='JPG750800706', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=56111664, number=7, ISRC='JPG750800707', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=69543348, number=8, ISRC='JPG750800708', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 0), - audiotools.Flac_CUESHEET_index(93492, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 0), + audiotools.flac.Flac_CUESHEET_index(93492, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=80537184, number=9, ISRC='JPG750800219', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=81398604, number=10, ISRC='JPG750800722', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 0), - audiotools.Flac_CUESHEET_index(78792, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 0), + audiotools.flac.Flac_CUESHEET_index(78792, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=91970256, number=11, ISRC='JPG750800709', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=99292032, number=12, ISRC='JPG750800290', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=110377008, number=13, ISRC='JPG750800218', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=113040060, number=14, ISRC='JPG750800710', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 0), - audiotools.Flac_CUESHEET_index(75264, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 0), + audiotools.flac.Flac_CUESHEET_index(75264, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=117804036, number=15, ISRC='JPG750800217', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=120531180, number=16, ISRC='JPG750800531', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=133673568, number=17, ISRC='JPG750800225', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=143108616, number=18, ISRC='JPG750800711', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 0), - audiotools.Flac_CUESHEET_index(98196, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 0), + audiotools.flac.Flac_CUESHEET_index(98196, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=156345084, number=19, ISRC='JPG750800180', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 0), - audiotools.Flac_CUESHEET_index(81732, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 0), + audiotools.flac.Flac_CUESHEET_index(81732, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=172052328, number=20, ISRC='JPG750800712', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 0), - audiotools.Flac_CUESHEET_index(197568, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 0), + audiotools.flac.Flac_CUESHEET_index(197568, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=178101084, number=21, ISRC='JPG750800713', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 0), - audiotools.Flac_CUESHEET_index(135240, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 0), + audiotools.flac.Flac_CUESHEET_index(135240, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=189107268, number=22, ISRC='JPG750800714', track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, 1)]), - audiotools.Flac_CUESHEET_track( + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=191795016, number=170, ISRC='\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', @@ -5651,15 +4192,15 @@ "1") metadata = tempflac.get_metadata() metadata.replace_blocks( - audiotools.Flac_CUESHEET.BLOCK_ID, - [audiotools.Flac_CUESHEET.converted( + audiotools.flac.Flac_CUESHEET.BLOCK_ID, + [audiotools.flac.Flac_CUESHEET.converted( test_sheet, 191795016)]) tempflac.update_metadata(metadata) sheet = audiotools.open( tempflacfile.name).get_metadata().get_block( - audiotools.Flac_CUESHEET.BLOCK_ID) + audiotools.flac.Flac_CUESHEET.BLOCK_ID) finally: tempflacfile.close() yield sheet @@ -5674,9 +4215,9 @@ self.channel_mask = channel_mask self.bits_per_sample = pcm_readers[0].bits_per_sample - def read(self, bytes): + def read(self, pcm_frames): return audiotools.pcm.from_channels( - [reader.read(bytes) for reader in self.buffers]) + [reader.read(pcm_frames) for reader in self.buffers]) def close(self): for reader in self.buffers: @@ -5693,11 +4234,12 @@ self.flac_channel_masks = [audiotools.FlacAudio, audiotools.OggFlacAudio] - if (audiotools.M4AAudio_nero.has_binaries(audiotools.BIN)): - self.flac_channel_masks.append(audiotools.M4AAudio_nero) + if (audiotools.m4a.M4AAudio_nero.has_binaries(audiotools.BIN)): + self.flac_channel_masks.append(audiotools.m4a.M4AAudio_nero) #these support a reordered subset of ChannelMasks up to 8 channels - self.vorbis_channel_masks = [audiotools.VorbisAudio] + self.vorbis_channel_masks = [audiotools.VorbisAudio, + audiotools.OpusAudio] def __test_mask_blank__(self, audio_class, channel_mask): temp_file = tempfile.NamedTemporaryFile(suffix="." + audio_class.SUFFIX) @@ -5833,20 +4375,22 @@ self.assertEqual(len(tone_tracks), len(channel_mask)) temp_file = tempfile.NamedTemporaryFile(suffix="." + audio_class.SUFFIX) - gain_calcs = [replaygain.ReplayGain(44100) for t in tone_tracks] try: temp_track = audio_class.from_pcm( temp_file.name, PCM_Reader_Multiplexer([t.to_pcm() for t in tone_tracks], channel_mask)) - pcm = temp_track.to_pcm() - frame = pcm.read(audiotools.BUFFER_SIZE) - while (len(frame) > 0): - for c in xrange(frame.channels): - gain_calcs[c].update(frame.channel(c)) - frame = pcm.read(audiotools.BUFFER_SIZE) - pcm.close() + gain_values = [ + replaygain.ReplayGain(temp_track.sample_rate()).title_gain( + audiotools.RemaskedPCMReader(temp_track.to_pcm(), + 1, + mask))[0] + for mask in + [int(audiotools.ChannelMask.from_fields( + **{channel_name:True})) + for channel_name in + channel_mask.channels()]] self.assertEqual(set([True]), set([prev.replay_gain().track_gain > @@ -5854,9 +4398,6 @@ for (prev, curr) in zip(tone_tracks, tone_tracks[1:])])) - gain_values = [gain_calc.title_gain()[0] - for gain_calc in gain_calcs] - self.assertEqual(set([True]), set([prev > curr for (prev, curr) in zip(gain_values, gain_values[1:])]), @@ -6034,7 +4575,7 @@ #This is likely due to the characteristics of #my input samples. if ((len(mask) == 6) and - ((audio_class is audiotools.M4AAudio_nero) or + ((audio_class is audiotools.m4a.M4AAudio_nero) or (audio_class is audiotools.VorbisAudio))): continue @@ -6065,15 +4606,15 @@ TONE_TRACKS[0:len(mask)], mask) - for mask in [from_fields(front_left=True, front_right=True), - from_fields(front_left=True, front_right=True, - back_left=True, back_right=True), - from_fields(front_left=True, side_left=True, - front_center=True, front_right=True, - side_right=True, back_center=True)]: - self.__test_assignment__(audiotools.AiffAudio, - TONE_TRACKS[0:len(mask)], - mask) + # for mask in [from_fields(front_left=True, front_right=True), + # from_fields(front_left=True, front_right=True, + # back_left=True, back_right=True), + # from_fields(front_left=True, side_left=True, + # front_center=True, front_right=True, + # side_right=True, back_center=True)]: + # self.__test_assignment__(audiotools.AiffAudio, + # TONE_TRACKS[0:len(mask)], + # mask) @LIB_CORE def test_unsupported_channel_mask_from_pcm(self): @@ -6081,11 +4622,6 @@ self.__test_undefined_mask_blank__(audiotools.WaveAudio, channels, False) - self.__test_error_channel_count__(audiotools.WaveAudio, - 19, audiotools.ChannelMask(0)) - self.__test_error_channel_count__(audiotools.WaveAudio, - 20, audiotools.ChannelMask(0)) - for channels in xrange(1, 3): self.__test_undefined_mask_blank__(audiotools.WavPackAudio, channels, @@ -6120,7 +4656,7 @@ for stereo_audio_class in [audiotools.MP3Audio, audiotools.MP2Audio, - audiotools.M4AAudio_faac]: + audiotools.m4a.M4AAudio_faac]: self.__test_undefined_mask_blank__(stereo_audio_class, 2, False) @@ -6174,9 +4710,9 @@ channels, True) - if (audiotools.M4AAudio_nero.has_binaries(audiotools.BIN)): + if (audiotools.m4a.M4AAudio_nero.has_binaries(audiotools.BIN)): for channels in xrange(1, 7): - self.__test_undefined_mask_blank__(audiotools.M4AAudio_nero, + self.__test_undefined_mask_blank__(audiotools.m4a.M4AAudio_nero, channels, False) @@ -6238,6 +4774,7 @@ player.play() time.sleep(6) self.assertEqual(callback.called, True) + player.close() class Test_CDPlayer(unittest.TestCase): @@ -6307,3 +4844,4 @@ player.play() time.sleep(6) self.assertEqual(callback.called, True) + player.close()
View file
audiotools-2.18.tar.gz/test/test_formats.py -> audiotools-2.19.tar.gz/test/test_formats.py
Changed
@@ -83,18 +83,17 @@ self.bits_per_sample, True) - def read(self, bytes): + def read(self, pcm_frames): if (self.minimum_successes > 0): self.minimum_successes -= 1 return audiotools.pcm.from_frames( - [self.frame for i in xrange(self.frame.frame_count(bytes))]) + [self.frame for i in xrange(pcm_frames)]) else: if (random.random() <= self.failure_chance): raise self.error else: return audiotools.pcm.from_frames( - [self.frame for i in - xrange(self.frame.frame_count(bytes))]) + [self.frame for i in xrange(pcm_frames)]) def close(self): pass @@ -162,18 +161,20 @@ valid = tempfile.NamedTemporaryFile(suffix=self.suffix) invalid = tempfile.NamedTemporaryFile(suffix=self.suffix) try: - #generate a valid file and check its is_type routine + #generate a valid file and check audiotools.file_type self.audio_class.from_pcm(valid.name, BLANK_PCM_Reader(1)) - f = open(valid.name, 'rb') - self.assertEqual(self.audio_class.is_type(f), True) - f.close() + self.assertEqual(audiotools.file_type(open(valid.name, "rb")), + self.audio_class) - #generate several invalid files and check its is_type routine + #several invalid files and ensure audiotools.file_type + #returns None + #(though it's *possible* os.urandom might generate a valid file + # by virtue of being random that's extremely unlikely in practice) for i in xrange(256): self.assertEqual(os.path.getsize(invalid.name), i) - f = open(invalid.name, 'rb') - self.assertEqual(self.audio_class.is_type(f), False) - f.close() + self.assertEqual( + audiotools.file_type(open(invalid.name, "rb")), + None) invalid.write(os.urandom(1)) invalid.flush() @@ -241,6 +242,7 @@ nonblank_metadata = audiotools.MetaData( track_name=u"Track Name", track_number=1, + track_total=2, album_name=u"Album Name") track.set_metadata(nonblank_metadata) self.assertEqual(track.get_metadata(), nonblank_metadata) @@ -249,9 +251,7 @@ if (metadata is not None): self.assertEqual( metadata, - audiotools.MetaData(track_name=u"", - track_number=0, - album_name=u"")) + audiotools.MetaData()) track.set_metadata(nonblank_metadata) self.assertEqual(track.get_metadata(), nonblank_metadata) @@ -290,9 +290,7 @@ for seconds in [1, 2, 3, 4, 5, 10, 20, 60, 120]: track = self.audio_class.from_pcm(temp.name, BLANK_PCM_Reader(seconds)) - self.assertEqual(track.total_frames(), seconds * 44100) - self.assertEqual(track.cd_frames(), seconds * 75) - self.assertEqual(track.seconds_length(), seconds) + self.assertEqual(int(track.seconds_length()), seconds) finally: temp.close() @@ -313,6 +311,10 @@ try: track = self.audio_class.from_pcm(temp.name, BLANK_PCM_Reader(10)) + if (track.lossless()): + self.assert_(audiotools.pcm_frame_cmp( + track.to_pcm(), + BLANK_PCM_Reader(10)) is None) for audio_class in audiotools.AVAILABLE_TYPES: outfile = tempfile.NamedTemporaryFile( suffix="." + audio_class.SUFFIX) @@ -331,7 +333,10 @@ if (track.lossless() and track2.lossless()): self.assert_(audiotools.pcm_frame_cmp( - track.to_pcm(), track2.to_pcm()) is None) + track.to_pcm(), track2.to_pcm()) is None, + "PCM mismatch converting %s to %s" % ( + self.audio_class.NAME, + audio_class.NAME)) finally: outfile.close() finally: @@ -347,22 +352,33 @@ track = self.audio_class.from_pcm( os.path.join(temp_dir, "abcde" + self.suffix), BLANK_PCM_Reader(1)) - self.assertEqual(track.track_number(), 0) - - track = self.audio_class.from_pcm( - os.path.join(temp_dir, "01 - abcde" + self.suffix), - BLANK_PCM_Reader(1)) - self.assertEqual(track.track_number(), 1) + if (track.get_metadata() is None): + self.assertEqual(track.track_number(), None) - track = self.audio_class.from_pcm( - os.path.join(temp_dir, "202 - abcde" + self.suffix), - BLANK_PCM_Reader(1)) - self.assertEqual(track.track_number(), 2) + track = self.audio_class.from_pcm( + os.path.join(temp_dir, "01 - abcde" + self.suffix), + BLANK_PCM_Reader(1)) + self.assertEqual(track.track_number(), 1) + + track = self.audio_class.from_pcm( + os.path.join(temp_dir, "202 - abcde" + self.suffix), + BLANK_PCM_Reader(1)) + self.assertEqual(track.track_number(), 2) - track = self.audio_class.from_pcm( - os.path.join(temp_dir, "303 45 - abcde" + self.suffix), - BLANK_PCM_Reader(1)) - self.assertEqual(track.track_number(), 3) + track = self.audio_class.from_pcm( + os.path.join(temp_dir, "303 45 - abcde" + self.suffix), + BLANK_PCM_Reader(1)) + self.assertEqual(track.track_number(), 3) + else: + self.audio_class.from_pcm( + os.path.join(temp_dir, "01 - abcde" + self.suffix), + BLANK_PCM_Reader(1)) + self.audio_class.from_pcm( + os.path.join(temp_dir, "202 - abcde" + self.suffix), + BLANK_PCM_Reader(1)) + self.audio_class.from_pcm( + os.path.join(temp_dir, "303 45 - abcde" + self.suffix), + BLANK_PCM_Reader(1)) track.set_metadata(audiotools.MetaData(track_number=2)) metadata = track.get_metadata() @@ -398,24 +414,35 @@ track = self.audio_class.from_pcm( os.path.join(temp_dir, "abcde" + self.suffix), BLANK_PCM_Reader(1)) - self.assertEqual(track.album_number(), 0) - - track = self.audio_class.from_pcm( - os.path.join(temp_dir, "01 - abcde" + self.suffix), - BLANK_PCM_Reader(1)) - self.assertEqual(track.album_number(), 0) - - track = self.audio_class.from_pcm( - os.path.join(temp_dir, "202 - abcde" + self.suffix), - BLANK_PCM_Reader(1)) if (track.get_metadata() is None): - self.assertEqual(track.album_number(), 2) + self.assertEqual(track.album_number(), None) - track = self.audio_class.from_pcm( - os.path.join(temp_dir, "303 45 - abcde" + self.suffix), - BLANK_PCM_Reader(1)) - if (track.get_metadata() is None): - self.assertEqual(track.album_number(), 3) + track = self.audio_class.from_pcm( + os.path.join(temp_dir, "01 - abcde" + self.suffix), + BLANK_PCM_Reader(1)) + self.assertEqual(track.album_number(), None) + + track = self.audio_class.from_pcm( + os.path.join(temp_dir, "202 - abcde" + self.suffix), + BLANK_PCM_Reader(1)) + if (track.get_metadata() is None): + self.assertEqual(track.album_number(), 2) + + track = self.audio_class.from_pcm( + os.path.join(temp_dir, "303 45 - abcde" + self.suffix), + BLANK_PCM_Reader(1)) + if (track.get_metadata() is None): + self.assertEqual(track.album_number(), 3) + else: + self.audio_class.from_pcm( + os.path.join(temp_dir, "01 - abcde" + self.suffix), + BLANK_PCM_Reader(1)) + self.audio_class.from_pcm( + os.path.join(temp_dir, "202 - abcde" + self.suffix), + BLANK_PCM_Reader(1)) + self.audio_class.from_pcm( + os.path.join(temp_dir, "303 45 - abcde" + self.suffix), + BLANK_PCM_Reader(1)) track.set_metadata(audiotools.MetaData(album_number=2)) metadata = track.get_metadata() @@ -632,7 +659,7 @@ (format_template % {u"basename": base}).encode('utf-8')) - #finally, ensure %(suffix)s is set properly + #ensure %(suffix)s is set properly format_template = u"Fo\u00f3 %(suffix)s" for path in ["track", "/foo/bar/track", @@ -648,9 +675,41 @@ self.audio_class.SUFFIX.decode( 'ascii')}).encode('utf-8')) + for metadata in [None, audiotools.MetaData()]: + #unsupported template fields raise UnsupportedTracknameField + self.assertRaises(audiotools.UnsupportedTracknameField, + self.audio_class.track_name, + "", metadata, + "%(foo)s") + + #broken template fields raise InvalidFilenameFormat + self.assertRaises(audiotools.InvalidFilenameFormat, + self.audio_class.track_name, + "", metadata, "%") + + self.assertRaises(audiotools.InvalidFilenameFormat, + self.audio_class.track_name, + "", metadata, "%{") + + self.assertRaises(audiotools.InvalidFilenameFormat, + self.audio_class.track_name, + "", metadata, "%[") + + self.assertRaises(audiotools.InvalidFilenameFormat, + self.audio_class.track_name, + "", metadata, "%(") + + self.assertRaises(audiotools.InvalidFilenameFormat, + self.audio_class.track_name, + "", metadata, "%(track_name") + + self.assertRaises(audiotools.InvalidFilenameFormat, + self.audio_class.track_name, + "", metadata, "%(track_name)") + @FORMAT_AUDIOFILE def test_replay_gain(self): - if (self.audio_class.can_add_replay_gain() and + if (self.audio_class.supports_replay_gain() and self.audio_class.lossless_replay_gain()): track_data1 = test_streams.Sine16_Stereo(44100, 44100, 441.0, 0.50, @@ -693,27 +752,24 @@ gains = audiotools.replaygain.ReplayGain(44100) track_data1.reset() - audiotools.transfer_data(track_data1.read, gains.update) track_gain1 = track1.replay_gain() - (track_gain, track_peak) = gains.title_gain() + (track_gain, track_peak) = gains.title_gain(track_data1) self.assertEqual(round(track_gain1.track_gain, 4), round(track_gain, 4)) self.assertEqual(round(track_gain1.track_peak, 4), round(track_peak, 4)) track_data2.reset() - audiotools.transfer_data(track_data2.read, gains.update) track_gain2 = track2.replay_gain() - (track_gain, track_peak) = gains.title_gain() + (track_gain, track_peak) = gains.title_gain(track_data2) self.assertEqual(round(track_gain2.track_gain, 4), round(track_gain, 4)) self.assertEqual(round(track_gain2.track_peak, 4), round(track_peak, 4)) track_data3.reset() - audiotools.transfer_data(track_data3.read, gains.update) track_gain3 = track3.replay_gain() - (track_gain, track_peak) = gains.title_gain() + (track_gain, track_peak) = gains.title_gain(track_data3) self.assertEqual(round(track_gain3.track_gain, 4), round(track_gain, 4)) self.assertEqual(round(track_gain3.track_peak, 4), @@ -745,6 +801,42 @@ track_file3.close() @FORMAT_AUDIOFILE + def test_read_after_eof(self): + if (self.audio_class is audiotools.AudioFile): + return None + + #build basic file + temp_file = tempfile.NamedTemporaryFile( + suffix="." + self.audio_class.SUFFIX) + try: + #build a generic file of silence + temp_track = self.audio_class.from_pcm( + temp_file.name, + EXACT_SILENCE_PCM_Reader(44100)) + + #read all the PCM frames from the file + pcmreader = temp_track.to_pcm() + f = pcmreader.read(4000) + while (len(f) > 0): + f = pcmreader.read(4000) + + self.assertEqual(len(f), 0) + + #then ensure subsequent reads return blank FrameList objects + #without triggering an error + for i in xrange(10): + f = pcmreader.read(4000) + self.assertEqual(len(f), 0) + + pcmreader.close() + + self.assertRaises(ValueError, + pcmreader.read, + 4000) + finally: + temp_file.close() + + @FORMAT_AUDIOFILE def test_invalid_from_pcm(self): if (self.audio_class is audiotools.AudioFile): return @@ -1152,21 +1244,6 @@ temp.close() @FORMAT_LOSSY - def test_sample_rate(self): - if (self.audio_class is audiotools.AudioFile): - return - - temp = tempfile.NamedTemporaryFile(suffix=self.suffix) - try: - track = self.audio_class.from_pcm(temp.name, BLANK_PCM_Reader( - 1, sample_rate=44100)) - self.assertEqual(track.sample_rate(), 44100) - track = audiotools.open(temp.name) - self.assertEqual(track.sample_rate(), 44100) - finally: - temp.close() - - @FORMAT_LOSSY def test_pcm(self): if (self.audio_class is audiotools.AudioFile): return @@ -1360,442 +1437,456 @@ class TestForeignWaveChunks: @FORMAT_LOSSLESS - def test_roundtrip_wave_chunks(self): + def test_convert_wave_chunks(self): import filecmp self.assert_(issubclass(self.audio_class, audiotools.WaveContainer)) - tempwav1 = tempfile.NamedTemporaryFile(suffix=".wav") - tempwav2 = tempfile.NamedTemporaryFile(suffix=".wav") - audio = tempfile.NamedTemporaryFile( - suffix='.' + self.audio_class.SUFFIX) - try: - #build a WAVE with some oddball chunks - audiotools.WaveAudio.wave_from_chunks( - tempwav1.name, - [audiotools.RIFF_Chunk( - 'fmt ', - 16, - '\x01\x00\x02\x00D\xac\x00\x00\x10\xb1\x02\x00\x04\x00\x10\x00'), - audiotools.RIFF_Chunk( - 'fooz', - 8, - 'testtext'), - audiotools.RIFF_Chunk( - 'barz', - 16, - 'somemoretesttext'), - audiotools.RIFF_Chunk( - 'bazz', - 1024, - chr(0) * 1024), - audiotools.RIFF_Chunk( - 'data', - 882000, - 'BZh91AY&SY\xdc\xd5\xc2\x8d\x06\xba\xa7\xc0\x00`\x00 \x000\x80MF\xa9$\x84\x9a\xa4\x92\x12qw$S\x85\t\r\xcd\\(\xd0'.decode('bz2')), - audiotools.RIFF_Chunk( - 'spam', - 12, - 'anotherchunk')]) - - wave = audiotools.open(tempwav1.name) - wave.verify() - - #convert it to our audio type using convert() - #(this used to be a to_wave()/from_wave() test, - # but I may deprecate that interface from direct use - # in favor of the more flexible convert() method) - track = wave.convert(audio.name, audiotools.WaveAudio) - - self.assertEqual(track.has_foreign_riff_chunks(), True) - - #convert it back to WAVE via convert() - track.convert(tempwav2.name, audiotools.WaveAudio) - - #check that the to WAVEs are byte-for-byte identical - self.assertEqual(filecmp.cmp(tempwav1.name, - tempwav2.name, - False), True) - - #finally, ensure that setting metadata doesn't erase the chunks - track.set_metadata(audiotools.MetaData(track_name=u"Foo")) - track = audiotools.open(track.filename) - self.assertEqual(track.has_foreign_riff_chunks(), True) - finally: - tempwav1.close() - tempwav2.close() - audio.close() + #several even-sized chunks + chunks1 = (("x\x9c\x0b\xf2ts\xdbQ\xc9\xcb\x10\xee\x18" + + "\xe6\x9a\x96[\xa2 \xc0\xc0\xc0\xc0\xc8\xc0" + + "\xc4\xe0\xb2\x86\x81A`#\x13\x03\x0b\x83" + + "\x00CZ~~\x15\x07P\xbc$\xb5\xb8\xa4$\xb5" + + "\xa2$)\xb1\xa8\n\xa4\xae8?757\xbf(\x15!^U" + + "\x05\xd40\nF\xc1(\x18\xc1 %\xb1$1\xa0\x94" + + "\x97\x01\x00`\xb0\x18\xf7").decode('zlib'), + (220500, 44100, 2, 16, 0x3), + "spam\x0c\x00\x00\x00anotherchunk") + + #several odd-sized chunks + chunks2 = (("x\x9c\x0b\xf2ts\xcbc``\x08w\x0csM\xcb\xcf\xaf" + + "\xe2b@\x06i\xb9%\n\x02@\x9a\x11\x08]\xd60" + + "\x801#\x03\x07CRbQ\x157H\x1c\x01\x18R\x12K\x12" + + "\xf9\x81b\x00\x19\xdd\x0ba").decode('zlib'), + (15, 44100, 1, 8, 0x4), + "\x00barz\x0b\x00\x00\x00\x01\x01\x01\x01" + + "\x01\x01\x01\x01\x01\x01\x01\x00") + + for (header, + (total_frames, + sample_rate, + channels, + bits_per_sample, + channel_mask), footer) in [chunks1, chunks2]: + temp1 = tempfile.NamedTemporaryFile( + suffix="." + self.audio_class.SUFFIX) + try: + #build our audio file from the from_pcm() interface + track = self.audio_class.from_pcm( + temp1.name, + EXACT_RANDOM_PCM_Reader( + pcm_frames=total_frames, + sample_rate=sample_rate, + channels=channels, + bits_per_sample=bits_per_sample, + channel_mask=channel_mask)) - @FORMAT_LOSSLESS - def test_convert_wave_chunks(self): - import filecmp + #check has_foreign_wave_chunks + self.assertEqual(track.has_foreign_wave_chunks(), False) + finally: + temp1.close() - #no "t" in this set - #which prevents a random generator from creating - #"fmt " or "data" chunk names - chunk_name_chars = "abcdefghijklmnopqrsuvwxyz " + for (header, + (total_frames, + sample_rate, + channels, + bits_per_sample, + channel_mask), footer) in [chunks1, chunks2]: + temp1 = tempfile.NamedTemporaryFile( + suffix="." + self.audio_class.SUFFIX) + try: + #build our audio file using the from_wave() interface + track = self.audio_class.from_wave( + temp1.name, + header, + EXACT_RANDOM_PCM_Reader( + pcm_frames=total_frames, + sample_rate=sample_rate, + channels=channels, + bits_per_sample=bits_per_sample, + channel_mask=channel_mask), + footer) + + #check has_foreign_wave_chunks + self.assertEqual(track.has_foreign_wave_chunks(), True) + + #ensure wave_header_footer returns same header and footer + (track_header, + track_footer) = track.wave_header_footer() + self.assertEqual(header, track_header) + self.assertEqual(footer, track_footer) + + #convert our file to every other WaveContainer format + #(including our own) + for new_class in audiotools.AVAILABLE_TYPES: + if (isinstance(new_class, audiotools.WaveContainer)): + temp2 = tempfile.NamedTemporaryFile( + suffix="." + wav_class.SUFFIX) + log = Log() + try: + track2 = track.convert(temp2, + new_class, + log.update) - input_wave = tempfile.NamedTemporaryFile(suffix=".wav") - track1_file = tempfile.NamedTemporaryFile( - suffix="." + self.audio_class.SUFFIX) - output_wave = tempfile.NamedTemporaryFile(suffix=".wav") - try: - #build a WAVE with some random oddball chunks - base_chunks = [ - audiotools.RIFF_Chunk( - 'fmt ', - 16, - '\x01\x00\x02\x00D\xac\x00\x00\x10\xb1\x02\x00\x04\x00\x10\x00'), - audiotools.RIFF_Chunk( - 'data', - 882000, - 'BZh91AY&SY\xdc\xd5\xc2\x8d\x06\xba\xa7\xc0\x00`\x00 \x000\x80MF\xa9$\x84\x9a\xa4\x92\x12qw$S\x85\t\r\xcd\\(\xd0'.decode('bz2'))] - - for i in xrange(random.choice(range(1, 10))): - chunk_size = random.choice(range(1, 1024)) * 2 - base_chunks.insert( - random.choice(range(0, len(base_chunks) + 1)), - audiotools.RIFF_Chunk( - "".join([random.choice(chunk_name_chars) - for i in xrange(4)]), - chunk_size, - os.urandom(chunk_size))) - - audiotools.WaveAudio.wave_from_chunks(input_wave.name, base_chunks) - wave = audiotools.open(input_wave.name) - wave.verify() - self.assert_(wave.has_foreign_riff_chunks()) - - #convert it to our audio type using convert() - track1 = wave.convert(track1_file.name, self.audio_class) - self.assert_(track1.has_foreign_riff_chunks()) - - #convert it to every other WAVE-containing format - for new_class in [t for t in audiotools.AVAILABLE_TYPES - if issubclass(t, audiotools.WaveContainer)]: - track2_file = tempfile.NamedTemporaryFile( - suffix="." + new_class.SUFFIX) - try: - track2 = track1.convert(track2_file.name, new_class) - self.assert_(track2.has_foreign_riff_chunks(), - "format %s lost RIFF chunks" % (new_class)) - - #then, convert it back to a WAVE - track2.convert(output_wave.name, audiotools.WaveAudio) - - #and ensure the result is byte-for-byte identical - self.assertEqual(filecmp.cmp(input_wave.name, - output_wave.name, - False), True, - "format %s lost RIFF chunks" % (new_class)) - finally: - track2_file.close() - finally: - input_wave.close() - track1_file.close() - output_wave.close() + #ensure the progress function + #gets called during conversion + self.assert_( + len(log.results) > 0, + "no logging converting %s to %s" % + (self.audio_class.NAME, + new_class.NAME)) - @FORMAT_LOSSLESS - def test_convert_progress_wave_chunks(self): - import filecmp + self.assert_( + len(set([r[1] for r in log.results])) == 1) + for x, y in zip(log.results[1:], log.results): + self.assert_((x[0] - y[0]) >= 0) - #no "t" in this set - #which prevents a random generator from creating - #"fmt " or "data" chunk names - chunk_name_chars = "abcdefghijklmnopqrsuvwxyz " + #ensure newly converted file + #matches has_foreign_wave_chunks + self.assertEqual( + track2.has_foreign_wave_chunks(), True) - input_wave = tempfile.NamedTemporaryFile(suffix=".wav") - track1_file = tempfile.NamedTemporaryFile( - suffix="." + self.audio_class.SUFFIX) - output_wave = tempfile.NamedTemporaryFile(suffix=".wav") + #ensure newly converted file + #has same header and footer + (track2_header, + track2_footer) = track2.wave_header_footer() + self.assertEqual(header, track2_header) + self.assertEqual(footer, track2_footer) - try: - #build a WAVE with some random oddball chunks - base_chunks = [ - audiotools.RIFF_Chunk( - 'fmt ', - 16, - '\x01\x00\x02\x00D\xac\x00\x00\x10\xb1\x02\x00\x04\x00\x10\x00'), - audiotools.RIFF_Chunk( - 'data', - 882000, - 'BZh91AY&SY\xdc\xd5\xc2\x8d\x06\xba\xa7\xc0\x00`\x00 \x000\x80MF\xa9$\x84\x9a\xa4\x92\x12qw$S\x85\t\r\xcd\\(\xd0'.decode('bz2'))] - - for i in xrange(random.choice(range(1, 10))): - chunk_size = random.choice(range(1, 1024)) * 2 - base_chunks.insert( - random.choice(range(0, len(base_chunks) + 1)), - audiotools.RIFF_Chunk( - "".join([random.choice(chunk_name_chars) - for i in xrange(4)]), - chunk_size, - os.urandom(chunk_size))) - - audiotools.WaveAudio.wave_from_chunks(input_wave.name, base_chunks) - wave = audiotools.open(input_wave.name) - wave.verify() - self.assert_(wave.has_foreign_riff_chunks()) - - #convert it to our audio type using convert - track1 = wave.convert(track1_file.name, self.audio_class) - self.assert_(track1.has_foreign_riff_chunks()) - - #convert our track to every other format - for new_class in audiotools.AVAILABLE_TYPES: - track2_file = tempfile.NamedTemporaryFile( - suffix="." + new_class.SUFFIX) - log = Log() - try: - track2 = track1.convert(track2_file.name, - new_class, - progress=log.update) - - self.assert_( - len(log.results) > 0, - "no logging converting %s to %s with RIFF chunks" % - (self.audio_class.NAME, - new_class.NAME)) - self.assert_(len(set([r[1] for r in log.results])) == 1) - for x, y in zip(log.results[1:], log.results): - self.assert_((x[0] - y[0]) >= 0) + #ensure newly converted file has same PCM data + self.assertEqual( + audiotools.pcm_frame_cmp( + track.to_pcm(), track2.to_pcm()), None) + finally: + temp2.close() + finally: + temp1.close() - #if the format is a WAVE container, convert it back - if (issubclass(new_class, audiotools.WaveContainer)): - track2.convert(output_wave.name, audiotools.WaveAudio) - - #and ensure the result is byte-for-byte identical - self.assertEqual(filecmp.cmp(input_wave.name, - output_wave.name, - False), True) - finally: - track2_file.close() - finally: - input_wave.close() - track1_file.close() - output_wave.close() + if (os.path.isfile("bad.wav")): + os.unlink("bad.wav") + for (header, footer) in [ + #wave header without "RIFF<size>WAVE raises an error + ("", ""), + ("FOOZ\x00\x00\x00\x00BARZ", ""), + + #invalid total size raises an error + ("RIFFZ\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" + + "\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00" + + "\x10\x00data2\x00\x00\x00", ""), + + #invalid data size raises an error + ("RIFFV\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" + + "\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00" + + "\x10\x00data6\x00\x00\x00", ""), + + #invalid chunk IDs in header raise an error + ("RIFFb\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" + + "\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00" + + "chn\x00\x04\x00\x00\x00\x01\x02\x03\x04" + + "data2\x00\x00\x00", ""), + + #mulitple fmt chunks raise an error + ("RIFFn\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" + + "\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00" + + "\x10\x00" + + "fmt \x10\x00\x00\x00\x01" + + "\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00" + + "\x10\x00" + + "data2\x00\x00\x00", ""), + + #data chunk before fmt chunk raises an error + ("RIFFJ\x00\x00\x00WAVE" + + "chnk\x04\x00\x00\x00\x01\x02\x03\x04" + + "data2\x00\x00\x00", ""), + + #bytes after data chunk raises an error + ("RIFFb\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" + + "\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00" + + "chnk\x04\x00\x00\x00\x01\x02\x03\x04" + + "data3\x00\x00\x00\x01", ""), + + #truncated chunks in header raise an error + ("RIFFb\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" + + "\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00" + + "chnk\x04\x00\x00\x00\x01\x02\x03", ""), + + #fmt chunk in footer raises an error + ("RIFFz\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" + + "\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00" + + "chnk\x04\x00\x00\x00\x01\x02\x03\x04" + + "data2\x00\x00\x00", + "fmt \x10\x00\x00\x00\x01" + + "\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00"), + + #data chunk in footer raises an error + ("RIFFn\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" + + "\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00" + + "chnk\x04\x00\x00\x00\x01\x02\x03\x04" + + "data2\x00\x00\x00", + "data\x04\x00\x00\x00\x01\x02\x03\x04"), + + #invalid chunk IDs in footer raise an error + ("RIFFn\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" + + "\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00" + + "chnk\x04\x00\x00\x00\x01\x02\x03\x04" + + "data2\x00\x00\x00", + "chn\x00\x04\x00\x00\x00\x01\x02\x03\x04"), + + #truncated chunks in footer raise an error + ("RIFFn\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" + + "\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00" + + "chnk\x04\x00\x00\x00\x01\x02\x03\x04" + + "data2\x00\x00\x00", + "chnk\x04\x00\x00\x00\x01\x02\x03"), + ]: + self.assertRaises(audiotools.EncodingError, + self.audio_class.from_wave, + "bad.wav", + header, + EXACT_BLANK_PCM_Reader(25, + 44100, + 1, + 16, + 0x4), + footer) + self.assertEqual(os.path.isfile("bad.wav"), False) class TestForeignAiffChunks: @FORMAT_LOSSLESS - def test_roundtrip_aiff_chunks(self): + def test_convert_aiff_chunks(self): import filecmp - tempaiff1 = tempfile.NamedTemporaryFile(suffix=".aiff") - tempaiff2 = tempfile.NamedTemporaryFile(suffix=".aiff") - audio = tempfile.NamedTemporaryFile( - suffix="." + self.audio_class.SUFFIX) - try: - #build an AIFF with some oddball chunks - audiotools.AiffAudio.aiff_from_chunks( - tempaiff1.name, - [audiotools.AIFF_Chunk( - 'COMM', - 18, - '\x00\x02\x00\x00\xacD\x00\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00'), - audiotools.AIFF_Chunk( - 'fooz', - 8, - 'testtext'), - audiotools.AIFF_Chunk( - 'barz', - 16, - 'somemoretesttext'), - audiotools.AIFF_Chunk( - 'bazz', - 1024, - chr(0) * 1024), - audiotools.AIFF_Chunk( - 'SSND', - 176408, - 'BZh91AY&SY&2\xd0\xeb\x00\x01Y\xc0\x04\xc0\x00\x00\x80\x00\x08 \x000\xcc\x05)\xa6\xa2\x93`\x94\x9e.\xe4\x8ap\xa1 Le\xa1\xd6'.decode('bz2')), - audiotools.AIFF_Chunk( - 'spam', - 12, - 'anotherchunk')]) - - aiff = audiotools.open(tempaiff1.name) - aiff.verify() - - #convert it to our audio type via convert() - track = aiff.convert(audio.name, self.audio_class) - if (hasattr(track, "has_foreign_aiff_chunks")): - self.assert_(track.has_foreign_aiff_chunks()) + self.assert_(issubclass(self.audio_class, + audiotools.AiffContainer)) - #convert it back to AIFF via convert() - self.assert_( - track.convert(tempaiff2.name, - audiotools.AiffAudio).has_foreign_aiff_chunks()) + #several even-sized chunks + chunks1 = (("x\x9cs\xf3\x0f\xf2e\xe0\xad<\xe4\xe8\xe9\xe6\xe6" + + "\xec\xef\xeb\xcb\xc0\xc0 \xc4\xc0\xc4\xc0\x1c\x1b" + + "\xc2 \xe0\xc0\xb7\xc6\x85\x01\x0c\xdc\xfc\xfd\xa3" + + "\x80\x14GIjqIIjE\x89\x93c\x10\x88/P\x9c\x9f\x9b" + + "\x9a\x9b_\x94\x8a\x10\x8f\x02\x8a\xb30\x8c" + + "\x82Q0\nF.\x08\x0e\xf6sa\xe0-\x8d\x80\xf1\x01" + + "\xcf\x8c\x17\x18").decode('zlib'), + (220500, 44100, 2, 16, 0x3), + "SPAM\x00\x00\x00\x0canotherchunk") + + #several odd-sized chunks + chunks2 = (("x\x9cs\xf3\x0f\xf2e``\xa8p\xf4tss\xf3\xf7\x8f" + + "\x02\xb2\xb9\x18\xe0\xc0\xd9\xdf\x17$+\xc4\xc0" + + "\x08$\xf9\x198\x1c\xf8\xd6\xb8@d\x9c\x1c\x83@j" + + "\xb9\x19\x11\x80!8\xd8\x0f$+\x0e\xd3\r" + + "\x00\x16\xa5\t3").decode('zlib'), + (15, 44100, 1, 8, 0x4), + "\x00BAZZ\x00\x00\x00\x0b\x02\x02\x02\x02" + + "\x02\x02\x02\x02\x02\x02\x02\x00") + + for (header, + (total_frames, + sample_rate, + channels, + bits_per_sample, + channel_mask), footer) in [chunks1, chunks2]: + temp1 = tempfile.NamedTemporaryFile( + suffix="." + self.audio_class.SUFFIX) + try: + #build our audio file from the from_pcm() interface + track = self.audio_class.from_pcm( + temp1.name, + EXACT_RANDOM_PCM_Reader( + pcm_frames=total_frames, + sample_rate=sample_rate, + channels=channels, + bits_per_sample=bits_per_sample, + channel_mask=channel_mask)) - #check that the two AIFFs are byte-for-byte identical - self.assertEqual(filecmp.cmp(tempaiff1.name, - tempaiff2.name, - False), True) - - #however, unlike WAVE, AIFF does support metadata - #so setting it will make the files no longer - #byte-for-byte identical, but the chunks in the new file - #should be a superset of the chunks in the old + #check has_foreign_aiff_chunks() + self.assertEqual(track.has_foreign_aiff_chunks(), False) + finally: + temp1.close() - track.set_metadata(audiotools.MetaData(track_name=u"Foo")) - track = audiotools.open(track.filename) - chunk_ids = set([chunk.id for chunk in - track.convert(tempaiff2.name, - audiotools.AiffAudio).chunks()]) - self.assert_(chunk_ids.issuperset(set(['COMM', - 'fooz', - 'barz', - 'bazz', - 'SSND', - 'spam']))) - finally: - tempaiff1.close() - tempaiff2.close() - audio.close() + for (header, + (total_frames, + sample_rate, + channels, + bits_per_sample, + channel_mask), footer) in [chunks1, chunks2]: + temp1 = tempfile.NamedTemporaryFile( + suffix="." + self.audio_class.SUFFIX) + try: + #build our audio file using from_aiff() interface + track = self.audio_class.from_aiff( + temp1.name, + header, + EXACT_RANDOM_PCM_Reader( + pcm_frames=total_frames, + sample_rate=sample_rate, + channels=channels, + bits_per_sample=bits_per_sample, + channel_mask=channel_mask), + footer) + + #check has_foreign_aiff_chunks() + self.assertEqual(track.has_foreign_aiff_chunks(), True) + + #ensure aiff_header_footer returns same header and footer + (track_header, + track_footer) = track.aiff_header_footer() + self.assertEqual(header, track_header) + self.assertEqual(footer, track_footer) + + #convert our file to every other AiffContainer format + #(including our own) + for new_class in audiotools.AVAILABLE_TYPES: + if (isinstance(new_class, audiotools.AiffContainer)): + temp2 = tempfile.NamedTemporaryFile( + suffix="." + wav_class.SUFFIX) + log = Log() + try: + track2 = track.convert(temp2, + new_class, + log.update) - @FORMAT_LOSSLESS - def test_convert_aiff_chunks(self): - import filecmp + #ensure the progress function + #gets called during conversion + self.assert_( + len(log.results) > 0, + "no logging converting %s to %s" % + (self.audio_class.NAME, + new_class.NAME)) - #no "M" or "N" in this set - #which prevents a random generator from creating - #"COMM" or "SSND" chunk names - chunk_name_chars = "ABCDEFGHIJKLOPQRSTUVWXYZ" + self.assert_( + len(set([r[1] for r in log.results])) == 1) + for x, y in zip(log.results[1:], log.results): + self.assert_((x[0] - y[0]) >= 0) - input_aiff = tempfile.NamedTemporaryFile(suffix=".aiff") - track1_file = tempfile.NamedTemporaryFile( - suffix="." + self.audio_class.SUFFIX) - output_aiff = tempfile.NamedTemporaryFile(suffix=".aiff") - try: - #build an AIFF with some random oddball chunks - base_chunks = [ - audiotools.AIFF_Chunk( - 'COMM', - 18, - '\x00\x02\x00\x00\xacD\x00\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00'), - audiotools.AIFF_Chunk( - 'SSND', - 176408, - 'BZh91AY&SY&2\xd0\xeb\x00\x01Y\xc0\x04\xc0\x00\x00\x80\x00\x08 \x000\xcc\x05)\xa6\xa2\x93`\x94\x9e.\xe4\x8ap\xa1 Le\xa1\xd6'.decode('bz2'))] - for i in xrange(random.choice(range(1, 10))): - block_size = random.choice(range(1, 1024)) * 2 - base_chunks.insert( - random.choice(range(0, len(base_chunks) + 1)), - audiotools.AIFF_Chunk( - "".join([random.choice(chunk_name_chars) - for i in xrange(4)]), - block_size, - os.urandom(block_size))) - - audiotools.AiffAudio.aiff_from_chunks(input_aiff.name, base_chunks) - aiff = audiotools.open(input_aiff.name) - aiff.verify() - self.assert_(aiff.has_foreign_aiff_chunks()) - - #convert it to our audio type using convert() - track1 = aiff.convert(track1_file.name, self.audio_class) - self.assert_(track1.has_foreign_aiff_chunks()) - - #convert it to every other AIFF-containing format - for new_class in [t for t in audiotools.AVAILABLE_TYPES - if issubclass(t, audiotools.AiffContainer)]: - track2_file = tempfile.NamedTemporaryFile( - suffix="." + new_class.SUFFIX) - try: - track2 = track1.convert(track2_file.name, new_class) - self.assert_(track2.has_foreign_aiff_chunks(), - "format %s lost AIFF chunks" % (new_class)) - - #then, convert it back to an AIFF - track2.convert(output_aiff.name, audiotools.AiffAudio) - - #and ensure the result is byte-for-byte identical - self.assertEqual(filecmp.cmp(input_aiff.name, - output_aiff.name, - False), True) - finally: - track2_file.close() - finally: - input_aiff.close() - track1_file.close() - output_aiff.close() + #ensure newly converted file + #matches has_foreign_wave_chunks + self.assertEqual( + track2.has_foreign_aiff_chunks(), True) - @FORMAT_LOSSLESS - def test_convert_progress_aiff_chunks(self): - import filecmp + #ensure newly converted file + #has same header and footer + (track2_header, + track2_footer) = track2.aiff_header_footer() + self.assertEqual(header, track2_header) + self.assertEqual(footer, track2_footer) - #no "M" or "N" in this set - #which prevents a random generator from creating - #"COMM" or "SSND" chunk names - chunk_name_chars = "ABCDEFGHIJKLOPQRSTUVWXYZ" + #ensure newly converted file has same PCM data + self.assertEqual( + audiotools.pcm_frame_cmp( + track.to_pcm(), track2.to_pcm()), None) + finally: + temp2.close() + finally: + temp1.close() - input_aiff = tempfile.NamedTemporaryFile(suffix=".aiff") - track1_file = tempfile.NamedTemporaryFile( - suffix="." + self.audio_class.SUFFIX) - output_aiff = tempfile.NamedTemporaryFile(suffix=".aiff") - try: - #build an AIFF with some random oddball chunks - base_chunks = [ - audiotools.AIFF_Chunk( - "COMM", - 18, - '\x00\x02\x00\x00\xacD\x00\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00'), - audiotools.AIFF_Chunk( - "SSND", - 176408, - 'BZh91AY&SY&2\xd0\xeb\x00\x01Y\xc0\x04\xc0\x00\x00\x80\x00\x08 \x000\xcc\x05)\xa6\xa2\x93`\x94\x9e.\xe4\x8ap\xa1 Le\xa1\xd6'.decode('bz2'))] - for i in xrange(random.choice(range(1, 10))): - chunk_size = random.choice(range(1, 1024)) * 2 - base_chunks.insert( - random.choice(range(0, len(base_chunks) + 1)), - audiotools.AIFF_Chunk( - "".join([random.choice(chunk_name_chars) - for i in xrange(4)]), - chunk_size, - os.urandom(chunk_size))) - - audiotools.AiffAudio.aiff_from_chunks(input_aiff.name, base_chunks) - aiff = audiotools.open(input_aiff.name) - aiff.verify() - self.assert_(aiff.has_foreign_aiff_chunks()) - - #convert it to our audio type using convert() - track1 = aiff.convert(track1_file.name, self.audio_class) - self.assert_(track1.has_foreign_aiff_chunks()) - - #convert it to every other format - for new_class in audiotools.AVAILABLE_TYPES: - track2_file = tempfile.NamedTemporaryFile( - suffix="." + new_class.SUFFIX) - log = Log() - try: - track2 = track1.convert(track2_file.name, - new_class, - progress=log.update) - - self.assert_( - len(log.results) > 0, - "no logging converting %s to %s with AIFF chunks" % - (self.audio_class.NAME, - new_class.NAME)) - self.assert_(len(set([r[1] for r in log.results])) == 1) - for x, y in zip(log.results[1:], log.results): - self.assert_((x[0] - y[0]) >= 0) + if (os.path.isfile("bad.aiff")): + os.unlink("bad.aiff") - #if the format is an AIFF container, convert it back - if (issubclass(new_class, audiotools.AiffContainer)): - track2.convert(output_aiff.name, audiotools.AiffAudio) - - #and ensure the result is byte-for-byte identical - self.assertEqual(filecmp.cmp(input_aiff.name, - output_aiff.name, - False), True) - finally: - track2_file.close() - finally: - input_aiff.close() - track1_file.close() - output_aiff.close() + for (header, footer) in [ + #aiff header without "FORM<size>AIFF raises an error + ("", ""), + ("FOOZ\x00\x00\x00\x00BARZ", ""), + + #invalid total size raises an error + ("FORM\x00\x00\x00tAIFF" + + "COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" + + "\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" + + "SSND\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x00\x00", + "ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"), + + #invalid SSND size raises an error + ("FORM\x00\x00\x00rAIFF" + + "COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" + + "\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" + + "SSND\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00", + "ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"), + + #invalid chunk IDs in header raise an error + ("FORM\x00\x00\x00~AIFF" + + "COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" + + "\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" + + "CHN\x00\x00\x00\x00\x04\x01\x02\x03\x04" + + "SSND\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x00\x00", + "ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"), + + #mulitple COMM chunks raise an error + ("FORM\x00\x00\x00\x8cAIFF" + + "COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" + + "\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" + + "COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" + + "\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" + + "SSND\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x00\x00", + "ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"), + + #SSND chunk before COMM chunk raises an error + ("FORM\x00\x00\x00XAIFF" + + "SSND\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x00\x00", + "ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"), + + #bytes missing from SSNK chunk raises an error + ("FORM\x00\x00\x00rAIFF" + + "COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" + + "\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" + + "SSND\x00\x00\x00<\x00\x00\x00\x00\x00\x00", + "ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"), + + #bytes after SSND chunk raises an error + ("FORM\x00\x00\x00rAIFF" + + "COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" + + "\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" + + "SSND\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + "ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"), + + #truncated chunks in header raise an error + ("FORM\x00\x00\x00rAIFF" + + "COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00", + "ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"), + + #COMM chunk in footer raises an error + ("FORM\x00\x00\x00\x8cAIFF" + + "COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" + + "\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" + + "SSND\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x00\x00", + "COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" + + "\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" + + "ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"), + + #SSND chunk in footer raises an error + ("FORM\x00\x00\x00rAIFF" + + "COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" + + "\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" + + "SSND\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x00\x00", + "ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00" + + "SSND\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x00\x00"), + + #invalid chunk IDs in footer raise an error + ("FORM\x00\x00\x00rAIFF" + + "COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" + + "\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" + + "SSND\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x00\x00", + "ID3\00\x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"), + + #truncated chunks in footer raise an error + ("FORM\x00\x00\x00rAIFF" + + "COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" + + "\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" + + "SSND\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x00\x00", + "ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00"), + ]: + self.assertRaises(audiotools.EncodingError, + self.audio_class.from_aiff, + "bad.aiff", + header, + EXACT_BLANK_PCM_Reader(25, + 44100, + 1, + 16, + 0x4), + footer) + self.assertEqual(os.path.isfile("bad.aiff"), False) class AiffFileTest(TestForeignAiffChunks, LosslessFileTest): @@ -1806,17 +1897,20 @@ @FORMAT_AIFF def test_ieee_extended(self): from audiotools.bitstream import BitstreamReader, BitstreamRecorder + import audiotools.aiff for i in xrange(0, 192000 + 1): w = BitstreamRecorder(0) - audiotools.build_ieee_extended(w, float(i)) + audiotools.aiff.build_ieee_extended(w, float(i)) s = cStringIO.StringIO(w.data()) self.assertEqual(w.data(), s.getvalue()) - self.assertEqual(i, audiotools.parse_ieee_extended( + self.assertEqual(i, audiotools.aiff.parse_ieee_extended( BitstreamReader(s, 0))) @FORMAT_AIFF def test_verify(self): + import audiotools.aiff + #test truncated file for aiff_file in ["aiff-8bit.aiff", "aiff-1ch.aiff", @@ -1843,7 +1937,7 @@ #then, check that a truncated ssnd chunk raises an exception #at read-time - for i in xrange(0x2F, len(aiff_data)): + for i in xrange(0x37, len(aiff_data)): temp.seek(0, 0) temp.write(aiff_data[0:i]) temp.flush() @@ -1892,11 +1986,11 @@ finally: temp.close() - COMM = audiotools.AIFF_Chunk( + COMM = audiotools.aiff.AIFF_Chunk( "COMM", 18, '\x00\x01\x00\x00\x00\r\x00\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00') - SSND = audiotools.AIFF_Chunk( + SSND = audiotools.aiff.AIFF_Chunk( "SSND", 34, '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x00\x03\x00\x02\x00\x01\x00\x00\xff\xff\xff\xfe\xff\xfd\xff\xfe\xff\xff\x00\x00') @@ -1922,11 +2016,13 @@ @FORMAT_AIFF def test_clean(self): - COMM = audiotools.AIFF_Chunk( + import audiotools.aiff + + COMM = audiotools.aiff.AIFF_Chunk( "COMM", 18, '\x00\x01\x00\x00\x00\r\x00\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00') - SSND = audiotools.AIFF_Chunk( + SSND = audiotools.aiff.AIFF_Chunk( "SSND", 34, '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x00\x03\x00\x02\x00\x01\x00\x00\xff\xff\xff\xfe\xff\xfd\xff\xfe\xff\xff\x00\x00') @@ -1970,7 +2066,7 @@ @FORMAT_ALAC def test_init(self): #check missing file - self.assertRaises(audiotools.InvalidALAC, + self.assertRaises(audiotools.m4a.InvalidALAC, audiotools.ALACAudio, "/dev/null/foo") @@ -1980,7 +2076,7 @@ for c in "invalidstringxxx": invalid_file.write(c) invalid_file.flush() - self.assertRaises(audiotools.InvalidALAC, + self.assertRaises(audiotools.m4a.InvalidALAC, audiotools.ALACAudio, invalid_file.name) finally: @@ -2159,10 +2255,10 @@ #has the same MD5 signature as pcmreader once decoded md5sum_decoder = md5() d = alac.to_pcm() - f = d.read(audiotools.BUFFER_SIZE) + f = d.read(audiotools.FRAMELIST_SIZE) while (len(f) > 0): md5sum_decoder.update(f.to_bytes(False, True)) - f = d.read(audiotools.BUFFER_SIZE) + f = d.read(audiotools.FRAMELIST_SIZE) d.close() self.assertEqual(md5sum_decoder.digest(), pcmreader.digest()) @@ -2198,10 +2294,10 @@ #has the same MD5 signature as pcmreader once decoded md5sum_decoder = md5() d = alac.to_pcm() - f = d.read(audiotools.BUFFER_SIZE) + f = d.read(audiotools.FRAMELIST_SIZE) while (len(f) > 0): md5sum_decoder.update(f.to_bytes(False, True)) - f = d.read(audiotools.BUFFER_SIZE) + f = d.read(audiotools.FRAMELIST_SIZE) d.close() self.assertEqual(md5sum_decoder.digest(), pcmreader.digest()) @@ -2329,19 +2425,19 @@ def test_streams(self): for g in self.__stream_variations__(): md5sum = md5() - f = g.read(audiotools.BUFFER_SIZE) + f = g.read(audiotools.FRAMELIST_SIZE) while (len(f) > 0): md5sum.update(f.to_bytes(False, True)) - f = g.read(audiotools.BUFFER_SIZE) + f = g.read(audiotools.FRAMELIST_SIZE) self.assertEqual(md5sum.digest(), g.digest()) g.close() for g in self.__multichannel_stream_variations__(): md5sum = md5() - f = g.read(audiotools.BUFFER_SIZE) + f = g.read(audiotools.FRAMELIST_SIZE) while (len(f) > 0): md5sum.update(f.to_bytes(False, True)) - f = g.read(audiotools.BUFFER_SIZE) + f = g.read(audiotools.FRAMELIST_SIZE) self.assertEqual(md5sum.digest(), g.digest()) g.close() @@ -2468,6 +2564,218 @@ 441.0, 0.61, 661.5, 0.37), block_size=1152) + @FORMAT_ALAC + def test_python_codec(self): + def test_python_reader(pcmreader, block_size=4096): + #ALAC doesn't really have encoding options worth mentioning + from audiotools.py_encoders import encode_mdat + + #encode file using Python-based encoder + temp_file = tempfile.NamedTemporaryFile(suffix=".m4a") + audiotools.ALACAudio.from_pcm( + temp_file.name, + pcmreader, + block_size=block_size, + encoding_function=encode_mdat) + + #verify contents of file decoded by + #Python-based decoder against contents decoded by + #C-based decoder + from audiotools.py_decoders import ALACDecoder as ALACDecoder1 + from audiotools.decoders import ALACDecoder as ALACDecoder2 + + self.assertEqual(audiotools.pcm_frame_cmp( + ALACDecoder1(temp_file.name), + ALACDecoder2(temp_file.name)), None) + + temp_file.close() + + #test small files + for g in [test_streams.Generate01, + test_streams.Generate02, + test_streams.Generate03, + test_streams.Generate04]: + test_python_reader(g(44100), block_size=1152) + + #test full scale deflection + for (bps, fsd) in [(16, test_streams.fsd16), + (24, test_streams.fsd24)]: + for pattern in [test_streams.PATTERN01, + test_streams.PATTERN02, + test_streams.PATTERN03, + test_streams.PATTERN04, + test_streams.PATTERN05, + test_streams.PATTERN06, + test_streams.PATTERN07]: + test_python_reader(fsd(pattern, 100), block_size=1152) + + #test sines + for g in [test_streams.Sine16_Mono(5000, 48000, + 441.0, 0.50, 441.0, 0.49), + test_streams.Sine16_Mono(5000, 96000, + 441.0, 0.61, 661.5, 0.37), + test_streams.Sine16_Stereo(5000, 48000, + 441.0, 0.50, 441.0, 0.49, 1.0), + test_streams.Sine16_Stereo(5000, 96000, + 441.0, 0.50, 882.0, 0.49, 1.0), + test_streams.Sine24_Mono(5000, 48000, + 441.0, 0.50, 441.0, 0.49), + test_streams.Sine24_Mono(5000, 96000, + 441.0, 0.61, 661.5, 0.37), + test_streams.Sine24_Stereo(5000, 48000, + 441.0, 0.50, 441.0, 0.49, 1.0), + test_streams.Sine24_Stereo(5000, 96000, + 441.0, 0.50, 882.0, 0.49, 1.0)]: + test_python_reader(g, block_size=1152) + + for g in [test_streams.Simple_Sine(5000, 44100, 0x0007, 16, + (6400, 10000), + (12800, 20000), + (30720, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x0107, 16, + (6400, 10000), + (12800, 20000), + (19200, 30000), + (16640, 40000)), + test_streams.Simple_Sine(5000, 44100, 0x0037, 16, + (6400, 10000), + (8960, 15000), + (11520, 20000), + (12800, 25000), + (14080, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x003F, 16, + (6400, 10000), + (11520, 15000), + (16640, 20000), + (21760, 25000), + (26880, 30000), + (30720, 35000)), + test_streams.Simple_Sine(5000, 44100, 0x013F, 16, + (6400, 10000), + (11520, 15000), + (16640, 20000), + (21760, 25000), + (26880, 30000), + (30720, 35000), + (29000, 40000)), + test_streams.Simple_Sine(5000, 44100, 0x00FF, 16, + (6400, 10000), + (11520, 15000), + (16640, 20000), + (21760, 25000), + (26880, 30000), + (30720, 35000), + (29000, 40000), + (28000, 45000)), + + test_streams.Simple_Sine(5000, 44100, 0x0007, 24, + (1638400, 10000), + (3276800, 20000), + (7864320, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x0107, 24, + (1638400, 10000), + (3276800, 20000), + (4915200, 30000), + (4259840, 40000)), + test_streams.Simple_Sine(5000, 44100, 0x0037, 24, + (1638400, 10000), + (2293760, 15000), + (2949120, 20000), + (3276800, 25000), + (3604480, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x003F, 24, + (1638400, 10000), + (2949120, 15000), + (4259840, 20000), + (5570560, 25000), + (6881280, 30000), + (7864320, 35000)), + test_streams.Simple_Sine(5000, 44100, 0x013F, 24, + (1638400, 10000), + (2949120, 15000), + (4259840, 20000), + (5570560, 25000), + (6881280, 30000), + (7864320, 35000), + (7000000, 40000)), + test_streams.Simple_Sine(5000, 44100, 0x00FF, 24, + (1638400, 10000), + (2949120, 15000), + (4259840, 20000), + (5570560, 25000), + (6881280, 30000), + (7864320, 35000), + (7000000, 40000), + (6000000, 45000))]: + test_python_reader(g, block_size=1152) + + #test wasted BPS + test_python_reader(test_streams.WastedBPS16(1000), + block_size=1152) + + #test block sizes + noise = struct.unpack(">32h", os.urandom(64)) + + for block_size in [16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33]: + test_python_reader(test_streams.MD5Reader( + test_streams.FrameListReader(noise, + 44100, 1, 16)), + block_size=block_size) + + #test noise + for (channels, mask) in [ + (1, audiotools.ChannelMask.from_channels(1)), + (2, audiotools.ChannelMask.from_channels(2))]: + for bps in [16, 24]: + #the reference decoder can't handle very large block sizes + for blocksize in [32, 4096, 8192]: + test_python_reader( + EXACT_RANDOM_PCM_Reader( + pcm_frames=4097, + sample_rate=44100, + channels=channels, + channel_mask=mask, + bits_per_sample=bps), + block_size=blocksize) + + #test fractional + for (block_size, + pcm_frames) in [(33, [31, 32, 33, 34, 35, 2046, + 2047, 2048, 2049, 2050]), + (256, [254, 255, 256, 257, 258, 510, 511, 512, + 513, 514, 1022, 1023, 1024, 1025, 1026, + 2046, 2047, 2048, 2049, 2050, 4094, 4095, + 4096, 4097, 4098])]: + for frame_count in pcm_frames: + test_python_reader(EXACT_RANDOM_PCM_Reader( + pcm_frames=frame_count, + sample_rate=44100, + channels=2, + bits_per_sample=16), + block_size=block_size) + + #test frame header variations + test_python_reader( + test_streams.Sine16_Mono(5000, 96000, + 441.0, 0.61, 661.5, 0.37), + block_size=16) + + test_python_reader( + test_streams.Sine16_Mono(5000, 9, + 441.0, 0.61, 661.5, 0.37), + block_size=1152) + + test_python_reader( + test_streams.Sine16_Mono(5000, 90, + 441.0, 0.61, 661.5, 0.37), + block_size=1152) + + test_python_reader( + test_streams.Sine16_Mono(5000, 90000, + 441.0, 0.61, 661.5, 0.37), + block_size=1152) + class AUFileTest(LosslessFileTest): def setUp(self): @@ -2629,7 +2937,7 @@ @FORMAT_FLAC def test_init(self): #check missing file - self.assertRaises(audiotools.InvalidFLAC, + self.assertRaises(audiotools.flac.InvalidFLAC, audiotools.FlacAudio, "/dev/null/foo") @@ -2639,7 +2947,7 @@ for c in "invalidstringxxx": invalid_file.write(c) invalid_file.flush() - self.assertRaises(audiotools.InvalidFLAC, + self.assertRaises(audiotools.flac.InvalidFLAC, audiotools.FlacAudio, invalid_file.name) finally: @@ -2747,17 +3055,17 @@ self.assert_(metadata is not None) self.assertEqual(metadata.track_name, u"Testing") self.assert_( - metadata.get_block(audiotools.Flac_VORBISCOMMENT.BLOCK_ID) + metadata.get_block(audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID) is not None) vorbis_comment = metadata.get_blocks( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID) + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID) proper_vendor_string = vorbis_comment[0].vendor_string vorbis_comment[0].vendor_string = u"Different String" - metadata.replace_blocks(audiotools.Flac_VORBISCOMMENT.BLOCK_ID, + metadata.replace_blocks(audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID, vorbis_comment) track.set_metadata(metadata) vendor_string = track.get_metadata().get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID).vendor_string + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID).vendor_string self.assertEqual(vendor_string, proper_vendor_string) #FIXME - ensure that channel mask isn't modified @@ -2776,7 +3084,7 @@ #attempt to adjust its metadata with bogus side data fields metadata = flac_file.get_metadata() - streaminfo = metadata.get_block(audiotools.Flac_STREAMINFO.BLOCK_ID) + streaminfo = metadata.get_block(audiotools.flac.Flac_STREAMINFO.BLOCK_ID) minimum_block_size = streaminfo.minimum_block_size maximum_block_size = streaminfo.maximum_block_size @@ -2798,13 +3106,13 @@ streaminfo.total_samples = 96000 streaminfo.md5sum = chr(1) * 16 - metadata.replace_blocks(audiotools.Flac_STREAMINFO.BLOCK_ID, + metadata.replace_blocks(audiotools.flac.Flac_STREAMINFO.BLOCK_ID, [streaminfo]) #ensure that set_metadata() restores fields to original values flac_file.set_metadata(metadata) metadata = flac_file.get_metadata() - streaminfo = metadata.get_block(audiotools.Flac_STREAMINFO.BLOCK_ID) + streaminfo = metadata.get_block(audiotools.flac.Flac_STREAMINFO.BLOCK_ID) self.assertEqual(minimum_block_size, streaminfo.minimum_block_size) @@ -2827,7 +3135,7 @@ #adjust its metadata with new bogus side data files metadata = flac_file.get_metadata() - streaminfo = metadata.get_block(audiotools.Flac_STREAMINFO.BLOCK_ID) + streaminfo = metadata.get_block(audiotools.flac.Flac_STREAMINFO.BLOCK_ID) streaminfo.minimum_block_size = 1 streaminfo.maximum_block_size = 10 streaminfo.minimum_frame_size = 2 @@ -2838,13 +3146,13 @@ streaminfo.total_samples = 96000 streaminfo.md5sum = chr(1) * 16 - metadata.replace_blocks(audiotools.Flac_STREAMINFO.BLOCK_ID, + metadata.replace_blocks(audiotools.flac.Flac_STREAMINFO.BLOCK_ID, [streaminfo]) #ensure that update_metadata() uses the bogus side data flac_file.update_metadata(metadata) metadata = flac_file.get_metadata() - streaminfo = metadata.get_block(audiotools.Flac_STREAMINFO.BLOCK_ID) + streaminfo = metadata.get_block(audiotools.flac.Flac_STREAMINFO.BLOCK_ID) self.assertEqual(streaminfo.minimum_block_size, 1) self.assertEqual(streaminfo.maximum_block_size, 10) self.assertEqual(streaminfo.minimum_frame_size, 2) @@ -2902,10 +3210,10 @@ temp.write(flac_data[0:i]) temp.flush() self.assertEqual(os.path.getsize(temp.name), i) - if (i < 8): - f = open(temp.name, 'rb') - self.assertEqual(audiotools.FlacAudio.is_type(f), False) - f.close() + if (i < 4): + self.assertEqual( + audiotools.file_type(open(temp.name, "rb")), + None) self.assertRaises(IOError, audiotools.decoders.FlacDecoder, temp.name, 1) @@ -3137,10 +3445,10 @@ def test_streams(self): for g in self.__stream_variations__(): md5sum = md5() - f = g.read(audiotools.BUFFER_SIZE) + f = g.read(audiotools.FRAMELIST_SIZE) while (len(f) > 0): md5sum.update(f.to_bytes(False, True)) - f = g.read(audiotools.BUFFER_SIZE) + f = g.read(audiotools.FRAMELIST_SIZE) self.assertEqual(md5sum.digest(), g.digest()) g.close() @@ -3168,10 +3476,10 @@ md5sum = md5() d = self.decoder(temp_file.name, pcmreader.channel_mask) - f = d.read(audiotools.BUFFER_SIZE) + f = d.read(audiotools.FRAMELIST_SIZE) while (len(f) > 0): md5sum.update(f.to_bytes(False, True)) - f = d.read(audiotools.BUFFER_SIZE) + f = d.read(audiotools.FRAMELIST_SIZE) d.close() self.assertEqual(md5sum.digest(), pcmreader.digest()) @@ -3451,6 +3759,13 @@ def test_clean(self): #metadata is tested separately + from audiotools.text import (CLEAN_FLAC_REMOVE_ID3V2, + CLEAN_FLAC_REMOVE_ID3V1, + CLEAN_FLAC_REORDERED_STREAMINFO, + CLEAN_FLAC_POPULATE_MD5, + CLEAN_FLAC_ADD_CHANNELMASK, + CLEAN_FLAC_FIX_SEEKTABLE) + #check FLAC files with ID3 tags f = open("flac-id3.flac", "rb") self.assertEqual(f.read(3), "ID3") @@ -3460,15 +3775,15 @@ fixes = [] self.assertEqual(track.clean(fixes), None) self.assertEqual(fixes, - [_(u"removed ID3v2 tag"), - _(u"removed ID3v1 tag")]) + [CLEAN_FLAC_REMOVE_ID3V2, + CLEAN_FLAC_REMOVE_ID3V1]) temp = tempfile.NamedTemporaryFile(suffix=".flac") try: fixes = [] self.assertNotEqual(track.clean(fixes, temp.name), None) self.assertEqual(fixes, - [_(u"removed ID3v2 tag"), - _(u"removed ID3v1 tag")]) + [CLEAN_FLAC_REMOVE_ID3V2, + CLEAN_FLAC_REMOVE_ID3V1]) f = open(temp.name, "rb") self.assertEqual(f.read(4), "fLaC") f.close() @@ -3488,13 +3803,13 @@ fixes = [] self.assertEqual(track.clean(fixes), None) self.assertEqual(fixes, - [_(u"moved STREAMINFO to first block")]) + [CLEAN_FLAC_REORDERED_STREAMINFO]) temp = tempfile.NamedTemporaryFile(suffix=".flac") try: fixes = [] self.assertNotEqual(track.clean(fixes, temp.name), None) self.assertEqual(fixes, - [_(u"moved STREAMINFO to first block")]) + [CLEAN_FLAC_REORDERED_STREAMINFO]) f = open(temp.name, "rb") self.assertEqual(f.read(5), "fLaC\x00") f.close() @@ -3509,17 +3824,17 @@ track = audiotools.open("flac-nonmd5.flac") fixes = [] self.assertEqual(track.get_metadata().get_block( - audiotools.Flac_STREAMINFO.BLOCK_ID).md5sum, chr(0) * 16) + audiotools.flac.Flac_STREAMINFO.BLOCK_ID).md5sum, chr(0) * 16) self.assertEqual(track.clean(fixes), None) - self.assertEqual(fixes, [_(u"populated empty MD5SUM")]) + self.assertEqual(fixes, [CLEAN_FLAC_POPULATE_MD5]) temp = tempfile.NamedTemporaryFile(suffix=".flac") try: fixes = [] self.assertNotEqual(track.clean(fixes, temp.name), None) - self.assertEqual(fixes, [_(u"populated empty MD5SUM")]) + self.assertEqual(fixes, [CLEAN_FLAC_POPULATE_MD5]) track2 = audiotools.open(temp.name) self.assertEqual(track2.get_metadata().get_block( - audiotools.Flac_STREAMINFO.BLOCK_ID).md5sum, + audiotools.flac.Flac_STREAMINFO.BLOCK_ID).md5sum, '\xd2\xb1 \x19\x90\x19\xb69' + '\xd5\xa7\xe2\xb3F>\x9c\x97') self.assertEqual(audiotools.pcm_frame_cmp( @@ -3532,43 +3847,57 @@ ("flac-nomask2.flac", 0x3F), ("flac-nomask3.flac", 0x3), ("flac-nomask4.flac", 0x3)]: - track = audiotools.open(path) - fixes = [] - self.assertEqual(track.clean(fixes), None) - self.assertEqual(fixes, - [_(u"added WAVEFORMATEXTENSIBLE_CHANNEL_MASK")]) - temp = tempfile.NamedTemporaryFile(suffix=".flac") + no_blocks_file = tempfile.NamedTemporaryFile(suffix=".flac") try: - fixes = [] - track.clean(fixes, temp.name) - self.assertEqual( - fixes, - [_(u"added WAVEFORMATEXTENSIBLE_CHANNEL_MASK")]) - new_track = audiotools.open(temp.name) - self.assertEqual(new_track.channel_mask(), - track.channel_mask()) - self.assertEqual(int(new_track.channel_mask()), mask) - metadata = new_track.get_metadata() + no_blocks_file.write(open(path, "rb").read()) + no_blocks_file.flush() + track = audiotools.open(no_blocks_file.name) + metadata = track.get_metadata() + for block_id in range(1, 7): + metadata.replace_blocks(block_id, []) + track.update_metadata(metadata) + + for track in [audiotools.open(path), + audiotools.open(no_blocks_file.name)]: + fixes = [] + self.assertEqual(track.clean(fixes), None) + self.assertEqual(fixes, [CLEAN_FLAC_ADD_CHANNELMASK]) - self.assertEqual( - metadata.get_block(audiotools.Flac_VORBISCOMMENT.BLOCK_ID)[ - u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"][0], - u"0x%.4X" % (mask)) + temp = tempfile.NamedTemporaryFile(suffix=".flac") + try: + fixes = [] + track.clean(fixes, temp.name) + self.assertEqual( + fixes, + [CLEAN_FLAC_ADD_CHANNELMASK]) + new_track = audiotools.open(temp.name) + self.assertEqual(new_track.channel_mask(), + track.channel_mask()) + self.assertEqual(int(new_track.channel_mask()), mask) + metadata = new_track.get_metadata() + + self.assertEqual( + metadata.get_block( + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[ + u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"][0], + u"0x%.4X" % (mask)) + finally: + temp.close() finally: - temp.close() + no_blocks_file.close() #check bad seekpoint destinations track = audiotools.open("flac-seektable.flac") fixes = [] self.assertEqual(track.clean(fixes), None) - self.assertEqual(fixes, [_(u"fixed invalid SEEKTABLE")]) + self.assertEqual(fixes, [CLEAN_FLAC_FIX_SEEKTABLE]) temp = tempfile.NamedTemporaryFile(suffix=".flac") try: fixes = [] track.clean(fixes, temp.name) self.assertEqual( fixes, - [_(u"fixed invalid SEEKTABLE")]) + [CLEAN_FLAC_FIX_SEEKTABLE]) new_track = audiotools.open(temp.name) fixes = [] new_track.clean(fixes, None) @@ -3594,6 +3923,185 @@ #verifies without errors self.assertEqual(flac.verify(), True) + @FORMAT_FLAC + def test_python_codec(self): + #Python decoder and encoder are far too slow + #to run anything resembling a complete set of tests + #so we'll cut them down to the very basics + + def test_python_reader(pcmreader, **encode_options): + from audiotools.py_encoders import encode_flac + + #encode file using Python-based encoder + temp_file = tempfile.NamedTemporaryFile(suffix=".flac") + encode_flac(temp_file.name, + audiotools.BufferedPCMReader(pcmreader), + **encode_options) + + #verify contents of file decoded by + #Python-based decoder against contents decoded by + #C-based decoder + from audiotools.py_decoders import FlacDecoder as FlacDecoder1 + from audiotools.decoders import FlacDecoder as FlacDecoder2 + + self.assertEqual(audiotools.pcm_frame_cmp( + FlacDecoder1(temp_file.name, 0), + FlacDecoder2(temp_file.name, 0)), None) + + temp_file.close() + + #test small files + for g in [test_streams.Generate01, + test_streams.Generate02, + test_streams.Generate03, + test_streams.Generate04]: + test_python_reader(g(44100), + block_size=1152, + max_lpc_order=16, + min_residual_partition_order=0, + max_residual_partition_order=3, + mid_side=True, + adaptive_mid_side=True, + exhaustive_model_search=True) + + #test full-scale deflection + for (bps, fsd) in [(8, test_streams.fsd8), + (16, test_streams.fsd16), + (24, test_streams.fsd24)]: + for pattern in [test_streams.PATTERN01, + test_streams.PATTERN02, + test_streams.PATTERN03, + test_streams.PATTERN04, + test_streams.PATTERN05, + test_streams.PATTERN06, + test_streams.PATTERN07]: + test_python_reader( + fsd(pattern, 100), + block_size=1152, + max_lpc_order=16, + min_residual_partition_order=0, + max_residual_partition_order=3, + mid_side=True, + adaptive_mid_side=True, + exhaustive_model_search=True) + + #test sines + for g in [test_streams.Sine8_Mono(5000, 48000, + 441.0, 0.50, 441.0, 0.49), + test_streams.Sine8_Stereo(5000, 48000, + 441.0, 0.50, 441.0, 0.49, 1.0), + test_streams.Sine16_Mono(5000, 48000, + 441.0, 0.50, 441.0, 0.49), + test_streams.Sine16_Stereo(5000, 48000, + 441.0, 0.50, 441.0, 0.49, 1.0), + test_streams.Sine24_Mono(5000, 48000, + 441.0, 0.50, 441.0, 0.49), + test_streams.Sine24_Stereo(5000, 48000, + 441.0, 0.50, 441.0, 0.49, 1.0), + test_streams.Simple_Sine(5000, 44100, 0x7, 8, + (25, 10000), + (50, 20000), + (120, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x33, 8, + (25, 10000), + (50, 20000), + (75, 30000), + (65, 40000)), + test_streams.Simple_Sine(5000, 44100, 0x37, 8, + (25, 10000), + (35, 15000), + (45, 20000), + (50, 25000), + (55, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x3F, 8, + (25, 10000), + (45, 15000), + (65, 20000), + (85, 25000), + (105, 30000), + (120, 35000)), + test_streams.Simple_Sine(5000, 44100, 0x7, 16, + (6400, 10000), + (12800, 20000), + (30720, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x33, 16, + (6400, 10000), + (12800, 20000), + (19200, 30000), + (16640, 40000)), + test_streams.Simple_Sine(5000, 44100, 0x37, 16, + (6400, 10000), + (8960, 15000), + (11520, 20000), + (12800, 25000), + (14080, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x3F, 16, + (6400, 10000), + (11520, 15000), + (16640, 20000), + (21760, 25000), + (26880, 30000), + (30720, 35000)), + test_streams.Simple_Sine(5000, 44100, 0x7, 24, + (1638400, 10000), + (3276800, 20000), + (7864320, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x33, 24, + (1638400, 10000), + (3276800, 20000), + (4915200, 30000), + (4259840, 40000)), + test_streams.Simple_Sine(5000, 44100, 0x37, 24, + (1638400, 10000), + (2293760, 15000), + (2949120, 20000), + (3276800, 25000), + (3604480, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x3F, 24, + (1638400, 10000), + (2949120, 15000), + (4259840, 20000), + (5570560, 25000), + (6881280, 30000), + (7864320, 35000))]: + test_python_reader(g, + block_size=1152, + max_lpc_order=16, + min_residual_partition_order=0, + max_residual_partition_order=3, + mid_side=True, + adaptive_mid_side=True, + exhaustive_model_search=True) + + #test wasted BPS + test_python_reader(test_streams.WastedBPS16(1000), + block_size=1152, + max_lpc_order=16, + min_residual_partition_order=0, + max_residual_partition_order=3, + mid_side=True, + adaptive_mid_side=True, + exhaustive_model_search=True) + + #test block sizes + noise = struct.unpack(">32h", os.urandom(64)) + + encoding_args = {"min_residual_partition_order": 0, + "max_residual_partition_order": 6, + "mid_side": True, + "adaptive_mid_side": True, + "exhaustive_model_search": True} + for block_size in [16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]: + for lpc_order in [0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 31, 32]: + args = encoding_args.copy() + args["block_size"] = block_size + args["max_lpc_order"] = lpc_order + test_python_reader( + test_streams.FrameListReader(noise, 44100, 1, 16), + **args) + + class M4AFileTest(LossyFileTest): def setUp(self): @@ -3618,7 +4126,7 @@ for channels in [1, 2, 3, 4, 5, 6]: track = self.audio_class.from_pcm(temp.name, BLANK_PCM_Reader( 1, channels=channels, channel_mask=0)) - if (self.audio_class is audiotools.M4AAudio_faac): + if (self.audio_class is audiotools.m4a.M4AAudio_faac): self.assertEqual(track.channels(), 2) track = audiotools.open(temp.name) self.assertEqual(track.channels(), 2) @@ -3888,6 +4396,7 @@ class OggVerify: @FORMAT_VORBIS + @FORMAT_OPUS @FORMAT_OGGFLAC def test_verify(self): good_file = tempfile.NamedTemporaryFile(suffix=self.suffix) @@ -3931,6 +4440,13 @@ good_file.close() bad_file.close() + if (self.audio_class is audiotools.OpusAudio): + #opusdec doesn't currently reject invalid + #streams like it should + #so the encoding test doesn't work right + #(this is a known bug) + return + temp = tempfile.NamedTemporaryFile(suffix=self.suffix) try: track = self.audio_class.from_pcm( @@ -3939,7 +4455,7 @@ self.assertEqual(track.verify(), True) good_data = open(temp.name, 'rb').read() f = open(temp.name, 'wb') - f.write(good_data[0:100]) + f.write(good_data[0:min(100, len(good_data) - 1)]) f.close() if (os.path.isfile("dummy.wav")): os.unlink("dummy.wav") @@ -3966,7 +4482,7 @@ @FORMAT_OGGFLAC def test_init(self): #check missing file - self.assertRaises(audiotools.InvalidFLAC, + self.assertRaises(audiotools.flac.InvalidFLAC, audiotools.OggFlacAudio, "/dev/null/foo") @@ -3976,7 +4492,7 @@ for c in "invalidstringxxx": invalid_file.write(c) invalid_file.flush() - self.assertRaises(audiotools.InvalidFLAC, + self.assertRaises(audiotools.flac.InvalidFLAC, audiotools.OggFlacAudio, invalid_file.name) finally: @@ -4009,7 +4525,7 @@ @FORMAT_SHORTEN def test_init(self): #check missing file - self.assertRaises(audiotools.InvalidShorten, + self.assertRaises(audiotools.shn.InvalidShorten, audiotools.ShortenAudio, "/dev/null/foo") @@ -4019,7 +4535,7 @@ for c in "invalidstringxxx": invalid_file.write(c) invalid_file.flush() - self.assertRaises(audiotools.InvalidShorten, + self.assertRaises(audiotools.shn.InvalidShorten, audiotools.ShortenAudio, invalid_file.name) finally: @@ -4210,10 +4726,10 @@ def test_streams(self): for g in self.__stream_variations__(): md5sum = md5() - f = g.read(audiotools.BUFFER_SIZE) + f = g.read(audiotools.FRAMELIST_SIZE) while (len(f) > 0): md5sum.update(f.to_bytes(False, True)) - f = g.read(audiotools.BUFFER_SIZE) + f = g.read(audiotools.FRAMELIST_SIZE) self.assertEqual(md5sum.digest(), g.digest()) g.close() @@ -4231,7 +4747,7 @@ temp_input_wave.verify() options = encode_options.copy() - (head, tail) = temp_input_wave.pcm_split() + (head, tail) = temp_input_wave.wave_header_footer() options["is_big_endian"] = False options["signed_samples"] = (pcmreader.bits_per_sample == 16) options["header_data"] = head @@ -4252,10 +4768,10 @@ #has the same MD5 signature as pcmreader once decoded md5sum = md5() d = self.decoder(temp_file.name) - f = d.read(audiotools.BUFFER_SIZE) + f = d.read(audiotools.FRAMELIST_SIZE) while (len(f) > 0): md5sum.update(f.to_bytes(False, True)) - f = d.read(audiotools.BUFFER_SIZE) + f = d.read(audiotools.FRAMELIST_SIZE) d.close() self.assertEqual(md5sum.digest(), pcmreader.digest()) @@ -4290,7 +4806,7 @@ options = encode_options.copy() options["is_big_endian"] = True options["signed_samples"] = True - (head, tail) = temp_input_aiff.pcm_split() + (head, tail) = temp_input_aiff.aiff_header_footer() options["header_data"] = head if (len(tail) > 0): options["footer_data"] = tail @@ -4401,6 +4917,133 @@ bits_per_sample=bps)), **encode_opts) + @FORMAT_SHORTEN + def test_python_codec(self): + def test_python_reader(pcmreader, block_size=256): + from audiotools.py_encoders import encode_shn + + temp_file = tempfile.NamedTemporaryFile(suffix=".shn") + audiotools.ShortenAudio.from_pcm( + temp_file.name, + pcmreader, + block_size=block_size, + encoding_function=encode_shn) + + from audiotools.decoders import SHNDecoder as SHNDecoder1 + from audiotools.py_decoders import SHNDecoder as SHNDecoder2 + + self.assertEqual(audiotools.pcm_frame_cmp( + SHNDecoder1(temp_file.name), + SHNDecoder2(temp_file.name)), None) + + temp_file.close() + + #test small files + for g in [test_streams.Generate01, + test_streams.Generate02, + test_streams.Generate03, + test_streams.Generate04]: + gen = g(44100) + test_python_reader(gen, block_size=256) + + #test full scale deflection + for (bps, fsd) in [(8, test_streams.fsd8), + (16, test_streams.fsd16)]: + for pattern in [test_streams.PATTERN01, + test_streams.PATTERN02, + test_streams.PATTERN03, + test_streams.PATTERN04, + test_streams.PATTERN05, + test_streams.PATTERN06, + test_streams.PATTERN07]: + stream = test_streams.MD5Reader(fsd(pattern, 100)) + test_python_reader(stream, block_size=256) + + #test sines + for g in [test_streams.Sine8_Mono(5000, 48000, + 441.0, 0.50, 441.0, 0.49), + test_streams.Sine8_Stereo(5000, 48000, + 441.0, 0.50, 441.0, 0.49, 1.0), + test_streams.Sine16_Mono(5000, 48000, + 441.0, 0.50, 441.0, 0.49), + test_streams.Sine16_Stereo(5000, 48000, + 441.0, 0.50, 441.0, 0.49, 1.0), + test_streams.Simple_Sine(5000, 44100, 0x7, 8, + (25, 10000), + (50, 20000), + (120, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x33, 8, + (25, 10000), + (50, 20000), + (75, 30000), + (65, 40000)), + test_streams.Simple_Sine(5000, 44100, 0x37, 8, + (25, 10000), + (35, 15000), + (45, 20000), + (50, 25000), + (55, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x3F, 8, + (25, 10000), + (45, 15000), + (65, 20000), + (85, 25000), + (105, 30000), + (120, 35000)), + test_streams.Simple_Sine(5000, 44100, 0x7, 16, + (6400, 10000), + (12800, 20000), + (30720, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x33, 16, + (6400, 10000), + (12800, 20000), + (19200, 30000), + (16640, 40000)), + test_streams.Simple_Sine(5000, 44100, 0x37, 16, + (6400, 10000), + (8960, 15000), + (11520, 20000), + (12800, 25000), + (14080, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x3F, 16, + (6400, 10000), + (11520, 15000), + (16640, 20000), + (21760, 25000), + (26880, 30000), + (30720, 35000))]: + test_python_reader(g, block_size=256) + + #test block sizes + noise = struct.unpack(">32h", os.urandom(64)) + + for block_size in [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 256, 1024]: + test_python_reader( + test_streams.FrameListReader(noise, 44100, 1, 16), + block_size=block_size) + + #test noise + for block_size in [4, 256, 1024]: + for (channels, mask) in [ + (1, audiotools.ChannelMask.from_channels(1)), + (2, audiotools.ChannelMask.from_channels(2)), + (4, audiotools.ChannelMask.from_fields( + front_left=True, + front_right=True, + back_left=True, + back_right=True)), + (8, audiotools.ChannelMask(0))]: + for bps in [8, 16]: + test_python_reader( + EXACT_RANDOM_PCM_Reader( + pcm_frames=5000, + sample_rate=44100, + channels=channels, + channel_mask=mask, + bits_per_sample=bps), + block_size=block_size) + class VorbisFileTest(OggVerify, LossyFileTest): def setUp(self): @@ -4467,6 +5110,47 @@ #which should eliminate the vorbisgain requirement. +class OpusFileTest(OggVerify, LossyFileTest): + def setUp(self): + self.audio_class = audiotools.OpusAudio + self.suffix = "." + self.audio_class.SUFFIX + + @FORMAT_OPUS + def test_channels(self): + #FIXME - test Opus channel assignment + pass + + @FORMAT_OPUS + def test_big_comment(self): + track_file = tempfile.NamedTemporaryFile( + suffix="." + self.audio_class.SUFFIX) + try: + track = self.audio_class.from_pcm(track_file.name, + BLANK_PCM_Reader(1)) + pcm = track.to_pcm() + original_pcm_sum = md5() + audiotools.transfer_framelist_data(pcm, original_pcm_sum.update) + pcm.close() + + comment = audiotools.MetaData( + track_name=u"Name", + track_number=1, + comment=u"abcdefghij" * 13005) + track.set_metadata(comment) + track = audiotools.open(track_file.name) + self.assertEqual(comment, track.get_metadata()) + + pcm = track.to_pcm() + new_pcm_sum = md5() + audiotools.transfer_framelist_data(pcm, new_pcm_sum.update) + pcm.close() + + self.assertEqual(original_pcm_sum.hexdigest(), + new_pcm_sum.hexdigest()) + finally: + track_file.close() + + class WaveFileTest(TestForeignWaveChunks, LosslessFileTest): def setUp(self): @@ -4547,7 +5231,7 @@ from struct import pack chunks = list(audiotools.open("wav-2ch.wav").chunks()) + \ - [audiotools.RIFF_Chunk("fooz", 10, chr(0) * 10)] + [audiotools.wav.RIFF_Chunk("fooz", 10, chr(0) * 10)] temp = tempfile.NamedTemporaryFile(suffix=".wav") try: audiotools.WaveAudio.wave_from_chunks(temp.name, @@ -4564,12 +5248,12 @@ finally: temp.close() - FMT = audiotools.RIFF_Chunk( + FMT = audiotools.wav.RIFF_Chunk( "fmt ", 16, '\x01\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00') - DATA = audiotools.RIFF_Chunk( + DATA = audiotools.wav.RIFF_Chunk( "data", 26, '\x00\x00\x01\x00\x02\x00\x03\x00\x02\x00\x01\x00\x00\x00\xff\xff\xfe\xff\xfd\xff\xfe\xff\xff\xff\x00\x00') @@ -4628,12 +5312,12 @@ @FORMAT_WAVE def test_clean(self): - FMT = audiotools.RIFF_Chunk( + FMT = audiotools.wav.RIFF_Chunk( "fmt ", 16, '\x01\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00') - DATA = audiotools.RIFF_Chunk( + DATA = audiotools.wav.RIFF_Chunk( "data", 26, '\x00\x00\x01\x00\x02\x00\x03\x00\x02\x00\x01\x00\x00\x00\xff\xff\xfe\xff\xfd\xff\xfe\xff\xff\xff\x00\x00') @@ -4681,42 +5365,42 @@ "false_stereo": True, "wasted_bits": True, "joint_stereo": False, - "decorrelation_passes": 0}, + "correlation_passes": 0}, {"block_size": 44100, "false_stereo": True, "wasted_bits": True, "joint_stereo": True, - "decorrelation_passes": 0}, + "correlation_passes": 0}, {"block_size": 44100, "false_stereo": True, "wasted_bits": True, "joint_stereo": True, - "decorrelation_passes": 1}, + "correlation_passes": 1}, {"block_size": 44100, "false_stereo": True, "wasted_bits": True, "joint_stereo": True, - "decorrelation_passes": 2}, + "correlation_passes": 2}, {"block_size": 44100, "false_stereo": True, "wasted_bits": True, "joint_stereo": True, - "decorrelation_passes": 5}, + "correlation_passes": 5}, {"block_size": 44100, "false_stereo": True, "wasted_bits": True, "joint_stereo": True, - "decorrelation_passes": 10}, + "correlation_passes": 10}, {"block_size": 44100, "false_stereo": True, "wasted_bits": True, "joint_stereo": True, - "decorrelation_passes": 16}] + "correlation_passes": 16}] @FORMAT_WAVPACK def test_init(self): #check missing file - self.assertRaises(audiotools.InvalidWavPack, + self.assertRaises(audiotools.wavpack.InvalidWavPack, audiotools.WavPackAudio, "/dev/null/foo") @@ -4726,7 +5410,7 @@ for c in "invalidstringxxx": invalid_file.write(c) invalid_file.flush() - self.assertRaises(audiotools.InvalidWavPack, + self.assertRaises(audiotools.wavpack.InvalidWavPack, audiotools.WavPackAudio, invalid_file.name) finally: @@ -4976,10 +5660,10 @@ self.assertEqual(wavpack.channel_mask, pcmreader.channel_mask) md5sum = md5() - f = wavpack.read(audiotools.BUFFER_SIZE) + f = wavpack.read(audiotools.FRAMELIST_SIZE) while (len(f) > 0): md5sum.update(f.to_bytes(False, True)) - f = wavpack.read(audiotools.BUFFER_SIZE) + f = wavpack.read(audiotools.FRAMELIST_SIZE) wavpack.close() self.assertEqual(md5sum.digest(), pcmreader.digest()) temp_file.close() @@ -5027,7 +5711,7 @@ for decorrelation_passes in [0, 1, 5]: opts_copy = opts.copy() opts_copy["block_size"] = block_size - opts_copy["decorrelation_passes"] = decorrelation_passes + opts_copy["correlation_passes"] = decorrelation_passes self.__test_reader__(test_streams.MD5Reader( test_streams.FrameListReader(noise, 44100, 1, 16)), @@ -5098,7 +5782,7 @@ channels=2, bits_per_sample=16)), block_size=block_size, - decorrelation_passes=5, + correlation_passes=5, false_stereo=False, wasted_bits=False, joint_stereo=False) @@ -5173,7 +5857,7 @@ block_size=44100, false_stereo=false_stereo, joint_stereo=joint_stereo, - decorrelation_passes=1, + correlation_passes=1, wasted_bits=False) @FORMAT_WAVPACK @@ -5201,7 +5885,211 @@ false_stereo=false_stereo, wasted_bits=wasted_bits, joint_stereo=joint_stereo, - decorrelation_passes=decorrelation_passes) + correlation_passes=decorrelation_passes) + + @FORMAT_WAVPACK + def test_python_codec(self): + def test_python_reader(pcmreader, **encode_options): + from audiotools.py_encoders import encode_wavpack + + #encode file using Python-based encoder + temp_file = tempfile.NamedTemporaryFile(suffix=".wv") + encode_wavpack(temp_file.name, + audiotools.BufferedPCMReader(pcmreader), + **encode_options) + + #verify contents of file decoded by + #Python-based decoder against contents decoded by + #C-based decoder + from audiotools.py_decoders import WavPackDecoder as WavPackDecoder1 + from audiotools.decoders import WavPackDecoder as WavPackDecoder2 + + self.assertEqual(audiotools.pcm_frame_cmp( + WavPackDecoder1(temp_file.name), + WavPackDecoder2(temp_file.name)), None) + + temp_file.close() + + #test small files + for opts in self.encode_opts: + for g in [test_streams.Generate01, + test_streams.Generate02, + test_streams.Generate03, + test_streams.Generate04]: + gen = g(44100) + test_python_reader(gen, **opts) + + #test full scale deflection + for opts in self.encode_opts: + for (bps, fsd) in [(8, test_streams.fsd8), + (16, test_streams.fsd16), + (24, test_streams.fsd24)]: + for pattern in [test_streams.PATTERN01, + test_streams.PATTERN02, + test_streams.PATTERN03, + test_streams.PATTERN04, + test_streams.PATTERN05, + test_streams.PATTERN06, + test_streams.PATTERN07]: + test_python_reader(fsd(pattern, 100), **opts) + + #test wasted BPS + for opts in self.encode_opts: + test_python_reader(test_streams.WastedBPS16(1000), **opts) + + #test block sizes + noise = struct.unpack(">32h", os.urandom(64)) + + opts = {"false_stereo": False, + "wasted_bits": False, + "joint_stereo": False} + for block_size in [16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]: + for decorrelation_passes in [0, 1, 5]: + opts_copy = opts.copy() + opts_copy["block_size"] = block_size + opts_copy["correlation_passes"] = decorrelation_passes + test_python_reader( + test_streams.FrameListReader(noise, + 44100, 1, 16), + **opts_copy) + + #test silence + for opts in self.encode_opts: + for (channels, mask) in [ + (1, audiotools.ChannelMask.from_channels(1)), + (2, audiotools.ChannelMask.from_channels(2))]: + opts_copy = opts.copy() + opts_copy['block_size'] = 4095 + test_python_reader( + EXACT_SILENCE_PCM_Reader( + pcm_frames=4096, + sample_rate=44100, + channels=channels, + channel_mask=mask, + bits_per_sample=16), + **opts_copy) + + #test noise + for opts in self.encode_opts: + for (channels, mask) in [ + (1, audiotools.ChannelMask.from_channels(1)), + (2, audiotools.ChannelMask.from_channels(2))]: + opts_copy = opts.copy() + opts_copy['block_size'] = 4095 + test_python_reader( + EXACT_RANDOM_PCM_Reader( + pcm_frames=4096, + sample_rate=44100, + channels=channels, + channel_mask=mask, + bits_per_sample=16), + **opts_copy) + + #test fractional + for (block_size, + pcm_frames_list) in [(33, [31, 32, 33, 34, 35, 2046, + 2047, 2048, 2049, 2050]), + (256, [254, 255, 256, 257, 258, 510, + 511, 512, 513, 514, 1022, 1023, + 1024, 1025, 1026, 2046, 2047, 2048, + 2049, 2050, 4094, 4095, 4096, 4097, + 4098])]: + for pcm_frames in pcm_frames_list: + test_python_reader(EXACT_RANDOM_PCM_Reader( + pcm_frames=pcm_frames, + sample_rate=44100, + channels=2, + bits_per_sample=16), + block_size=block_size, + correlation_passes=5, + false_stereo=False, + wasted_bits=False, + joint_stereo=False) + + #test sines + for opts in self.encode_opts: + for g in [test_streams.Sine8_Mono(5000, 48000, + 441.0, 0.50, 441.0, 0.49), + test_streams.Sine8_Stereo(5000, 48000, + 441.0, 0.50, 441.0, 0.49, 1.0), + test_streams.Sine16_Mono(5000, 48000, + 441.0, 0.50, 441.0, 0.49), + test_streams.Sine16_Stereo(5000, 48000, + 441.0, 0.50, 441.0, 0.49, 1.0), + test_streams.Sine24_Mono(5000, 48000, + 441.0, 0.50, 441.0, 0.49), + test_streams.Sine24_Stereo(5000, 48000, + 441.0, 0.50, 441.0, 0.49, 1.0), + test_streams.Simple_Sine(5000, 44100, 0x7, 8, + (25, 10000), + (50, 20000), + (120, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x33, 8, + (25, 10000), + (50, 20000), + (75, 30000), + (65, 40000)), + test_streams.Simple_Sine(5000, 44100, 0x37, 8, + (25, 10000), + (35, 15000), + (45, 20000), + (50, 25000), + (55, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x3F, 8, + (25, 10000), + (45, 15000), + (65, 20000), + (85, 25000), + (105, 30000), + (120, 35000)), + + test_streams.Simple_Sine(5000, 44100, 0x7, 16, + (6400, 10000), + (12800, 20000), + (30720, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x33, 16, + (6400, 10000), + (12800, 20000), + (19200, 30000), + (16640, 40000)), + test_streams.Simple_Sine(5000, 44100, 0x37, 16, + (6400, 10000), + (8960, 15000), + (11520, 20000), + (12800, 25000), + (14080, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x3F, 16, + (6400, 10000), + (11520, 15000), + (16640, 20000), + (21760, 25000), + (26880, 30000), + (30720, 35000)), + + test_streams.Simple_Sine(5000, 44100, 0x7, 24, + (1638400, 10000), + (3276800, 20000), + (7864320, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x33, 24, + (1638400, 10000), + (3276800, 20000), + (4915200, 30000), + (4259840, 40000)), + test_streams.Simple_Sine(5000, 44100, 0x37, 24, + (1638400, 10000), + (2293760, 15000), + (2949120, 20000), + (3276800, 25000), + (3604480, 30000)), + test_streams.Simple_Sine(5000, 44100, 0x3F, 24, + (1638400, 10000), + (2949120, 15000), + (4259840, 20000), + (5570560, 25000), + (6881280, 30000), + (7864320, 35000))]: + test_python_reader(g, **opts) class SineStreamTest(unittest.TestCase):
View file
audiotools-2.18.tar.gz/test/test_metadata.py -> audiotools-2.19.tar.gz/test/test_metadata.py
Changed
@@ -130,16 +130,17 @@ #check that blanking out the fields works for field in self.supported_fields: metadata = self.empty_metadata() + self.assertEqual(getattr(metadata, field), None) if (field not in audiotools.MetaData.INTEGER_FIELDS): setattr(metadata, field, u"") track.set_metadata(metadata) metadata = track.get_metadata() self.assertEqual(getattr(metadata, field), u"") else: - setattr(metadata, field, 0) + setattr(metadata, field, None) track.set_metadata(metadata) metadata = track.get_metadata() - self.assertEqual(getattr(metadata, field), 0) + self.assertEqual(getattr(metadata, field), None) #re-set the fields with random values for field in self.supported_fields: @@ -164,10 +165,11 @@ delattr(metadata, field) track.set_metadata(metadata) metadata = track.get_metadata() - if (field not in audiotools.MetaData.INTEGER_FIELDS): - self.assertEqual(getattr(metadata, field), u"") - else: - self.assertEqual(getattr(metadata, field), 0) + self.assertEqual(getattr(metadata, field), None, + "%s != %s for field %s" % ( + repr(getattr(metadata, field)), + None, + field)) finally: temp_file.close() @@ -222,10 +224,8 @@ if (field in self.supported_fields): self.assertEqual(getattr(metadata_orig, field), getattr(metadata_new, field)) - elif (field in audiotools.MetaData.INTEGER_FIELDS): - self.assertEqual(getattr(metadata_new, field), 0) else: - self.assertEqual(getattr(metadata_new, field), u"") + self.assertEqual(getattr(metadata_new, field), None) #ensure images match, if supported if (self.metadata_class.supports_images()): @@ -349,6 +349,585 @@ return self.metadata_class.converted(audiotools.MetaData()) @METADATA_WAVPACK + def test_getitem(self): + from audiotools.ape import ApeTag,ApeTagItem + + #getitem with no matches raises KeyError + self.assertRaises(KeyError, ApeTag([]).__getitem__, "Title") + + #getitem with one match returns that item + self.assertEqual(ApeTag([ApeTagItem(0, 0, "Foo", "Bar")])["Foo"], + ApeTagItem(0, 0, "Foo", "Bar")) + + #getitem with multiple matches returns the first match + #(this is not a valid ApeTag and should be cleaned) + self.assertEqual(ApeTag([ApeTagItem(0, 0, "Foo", "Bar"), + ApeTagItem(0, 0, "Foo", "Baz")])["Foo"], + ApeTagItem(0, 0, "Foo", "Bar")) + + #tag items *are* case-sensitive according to the specification + self.assertRaises(KeyError, + ApeTag([ApeTagItem(0, 0, "Foo", "Bar")]).__getitem__, + "foo") + + @METADATA_WAVPACK + def test_setitem(self): + from audiotools.ape import ApeTag,ApeTagItem + + #setitem adds new key if necessary + metadata = ApeTag([]) + metadata["Foo"] = ApeTagItem(0, 0, "Foo", "Bar") + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Foo", "Bar")]) + + #setitem replaces matching key with new value + metadata = ApeTag([ApeTagItem(0, 0, "Foo", "Bar")]) + metadata["Foo"] = ApeTagItem(0, 0, "Foo", "Baz") + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Foo", "Baz")]) + + #setitem leaves other items alone + #when adding or replacing tags + metadata = ApeTag([ApeTagItem(0, 0, "Kelp", "Spam")]) + metadata["Foo"] = ApeTagItem(0, 0, "Foo", "Bar") + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Kelp", "Spam"), + ApeTagItem(0, 0, "Foo", "Bar")]) + + metadata = ApeTag([ApeTagItem(0, 0, "Foo", "Bar"), + ApeTagItem(0, 0, "Kelp", "Spam")]) + metadata["Foo"] = ApeTagItem(0, 0, "Foo", "Baz") + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Foo", "Baz"), + ApeTagItem(0, 0, "Kelp", "Spam")]) + + #setitem is case-sensitive + metadata = ApeTag([ApeTagItem(0, 0, "foo", "Spam")]) + metadata["Foo"] = ApeTagItem(0, 0, "Foo", "Bar") + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "foo", "Spam"), + ApeTagItem(0, 0, "Foo", "Bar")]) + + @METADATA_WAVPACK + def test_getattr(self): + from audiotools.ape import ApeTag,ApeTagItem + + #track_number grabs the first available integer from "Track" + self.assertEqual(ApeTag([]).track_number, None) + + self.assertEqual( + ApeTag([ApeTagItem(0, 0, "Track", "2")]).track_number, + 2) + + self.assertEqual( + ApeTag([ApeTagItem(0, 0, "Track", "2/3")]).track_number, + 2) + + self.assertEqual( + ApeTag([ApeTagItem(0, 0, "Track", "foo 2 bar")]).track_number, + 2) + + #album_number grabs the first available from "Media" + self.assertEqual(ApeTag([]).album_number, None) + + self.assertEqual( + ApeTag([ApeTagItem(0, 0, "Media", "4")]).album_number, + 4) + + self.assertEqual( + ApeTag([ApeTagItem(0, 0, "Media", "4/5")]).album_number, + 4) + + self.assertEqual( + ApeTag([ApeTagItem(0, 0, "Media", "foo 4 bar")]).album_number, + 4) + + #track_total grabs the second number in a slashed field, if any + self.assertEqual(ApeTag([]).track_total, None) + + self.assertEqual( + ApeTag([ApeTagItem(0, 0, "Track", "2")]).track_total, + None) + + self.assertEqual( + ApeTag([ApeTagItem(0, 0, "Track", "2/3")]).track_total, + 3) + + self.assertEqual( + ApeTag([ApeTagItem(0, 0, + "Track", "foo 2 bar / baz 3 blah")]).track_total, + 3) + + #album_total grabs the second number in a slashed field, if any + self.assertEqual(ApeTag([]).album_total, None) + + self.assertEqual( + ApeTag([ApeTagItem(0, 0, "Media", "4")]).album_total, + None) + + self.assertEqual( + ApeTag([ApeTagItem(0, 0, "Media", "4/5")]).album_total, + 5) + + self.assertEqual( + ApeTag([ApeTagItem(0, 0, + "Media", "foo 4 bar / baz 5 blah")]).album_total, + 5) + + #other fields grab the first available item + #(though proper APEv2 tags should only contain one) + self.assertEqual(ApeTag([]).track_name, + None) + + self.assertEqual(ApeTag([ApeTagItem(0, 0, "Title", "foo")]).track_name, + u"foo") + + self.assertEqual(ApeTag([ApeTagItem(0, 0, "Title", "foo"), + ApeTagItem(0, 0, "Title", "bar")]).track_name, + u"foo") + + @METADATA_WAVPACK + def test_setattr(self): + from audiotools.ape import ApeTag,ApeTagItem + + #track_number adds new field if necessary + metadata = ApeTag([]) + metadata.track_number = 2 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Track", "2")]) + self.assertEqual(metadata.track_number, 2) + + metadata = ApeTag([ApeTagItem(0, 0, "Foo", "Bar")]) + metadata.track_number = 2 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Foo", "Bar"), + ApeTagItem(0, 0, "Track", "2")]) + self.assertEqual(metadata.track_number, 2) + + #track_number updates the first integer field + #and leaves other junk in that field alone + metadata = ApeTag([ApeTagItem(0, 0, "Track", "1")]) + metadata.track_number = 2 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Track", "2")]) + self.assertEqual(metadata.track_number, 2) + + metadata = ApeTag([ApeTagItem(0, 0, "Track", "1/3")]) + metadata.track_number = 2 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Track", "2/3")]) + self.assertEqual(metadata.track_number, 2) + + metadata = ApeTag([ApeTagItem(0, 0, "Track", "foo 1 bar")]) + metadata.track_number = 2 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Track", "foo 2 bar")]) + self.assertEqual(metadata.track_number, 2) + + #album_number adds new field if necessary + metadata = ApeTag([]) + metadata.album_number = 4 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Media", "4")]) + self.assertEqual(metadata.album_number, 4) + + metadata = ApeTag([ApeTagItem(0, 0, "Foo", "Bar")]) + metadata.album_number = 4 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Foo", "Bar"), + ApeTagItem(0, 0, "Media", "4")]) + self.assertEqual(metadata.album_number, 4) + + #album_number updates the first integer field + #and leaves other junk in that field alone + metadata = ApeTag([ApeTagItem(0, 0, "Media", "3")]) + metadata.album_number = 4 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Media", "4")]) + self.assertEqual(metadata.album_number, 4) + + metadata = ApeTag([ApeTagItem(0, 0, "Media", "3/5")]) + metadata.album_number = 4 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Media", "4/5")]) + self.assertEqual(metadata.album_number, 4) + + metadata = ApeTag([ApeTagItem(0, 0, "Media", "foo 3 bar")]) + metadata.album_number = 4 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Media", "foo 4 bar")]) + self.assertEqual(metadata.album_number, 4) + + #track_total adds a new field if necessary + metadata = ApeTag([]) + metadata.track_total = 3 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Track", "0/3")]) + self.assertEqual(metadata.track_total, 3) + + metadata = ApeTag([ApeTagItem(0, 0, "Foo", "Bar")]) + metadata.track_total = 3 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Foo", "Bar"), + ApeTagItem(0, 0, "Track", "0/3")]) + self.assertEqual(metadata.track_total, 3) + + #track_total adds a slashed side of the integer field + #and leaves other junk in that field alone + metadata = ApeTag([ApeTagItem(0, 0, "Track", "1")]) + metadata.track_total = 3 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Track", "1/3")]) + self.assertEqual(metadata.track_total, 3) + + metadata = ApeTag([ApeTagItem(0, 0, "Track", "1 ")]) + metadata.track_total = 3 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Track", "1 /3")]) + self.assertEqual(metadata.track_total, 3) + + metadata = ApeTag([ApeTagItem(0, 0, "Track", "1/2")]) + metadata.track_total = 3 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Track", "1/3")]) + self.assertEqual(metadata.track_total, 3) + + metadata = ApeTag([ApeTagItem(0, 0, "Track", "1 / baz 2 blah")]) + metadata.track_total = 3 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Track", "1 / baz 3 blah")]) + self.assertEqual(metadata.track_total, 3) + + metadata = ApeTag([ApeTagItem(0, 0, "Track", "foo 1 bar / baz 2 blah")]) + metadata.track_total = 3 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Track", "foo 1 bar / baz 3 blah")]) + self.assertEqual(metadata.track_total, 3) + + metadata = ApeTag([ + ApeTagItem(0, 0, "Track", "1 / 2 / 4")]) + metadata.track_total = 3 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Track", "1 / 3 / 4")]) + self.assertEqual(metadata.track_total, 3) + + metadata = ApeTag([ + ApeTagItem(0, 0, "Track", "foo / 2")]) + metadata.track_total = 3 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Track", "foo / 3")]) + self.assertEqual(metadata.track_total, 3) + + #album_total adds a new field if necessary + metadata = ApeTag([]) + metadata.album_total = 5 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Media", "0/5")]) + self.assertEqual(metadata.album_total, 5) + + metadata = ApeTag([ApeTagItem(0, 0, "Foo", "Bar")]) + metadata.album_total = 5 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Foo", "Bar"), + ApeTagItem(0, 0, "Media", "0/5")]) + self.assertEqual(metadata.album_total, 5) + + #album_total adds a slashed side of the integer field + #and leaves other junk in that field alone + metadata = ApeTag([ApeTagItem(0, 0, "Media", "3")]) + metadata.album_total = 5 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Media", "3/5")]) + self.assertEqual(metadata.album_total, 5) + + metadata = ApeTag([ApeTagItem(0, 0, "Media", "3 ")]) + metadata.album_total = 5 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Media", "3 /5")]) + self.assertEqual(metadata.album_total, 5) + + metadata = ApeTag([ApeTagItem(0, 0, "Media", "3/4")]) + metadata.album_total = 5 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Media", "3/5")]) + self.assertEqual(metadata.album_total, 5) + + metadata = ApeTag([ApeTagItem(0, 0, "Media", "1 / baz 2 blah")]) + metadata.album_total = 5 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Media", "1 / baz 5 blah")]) + self.assertEqual(metadata.album_total, 5) + + metadata = ApeTag([ApeTagItem(0, 0, "Media", "foo 1 bar / baz 2 blah")]) + metadata.album_total = 5 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Media", "foo 1 bar / baz 5 blah")]) + self.assertEqual(metadata.album_total, 5) + + metadata = ApeTag([ + ApeTagItem(0, 0, "Media", "3 / 4 / 6")]) + metadata.album_total = 5 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Media", "3 / 5 / 6")]) + self.assertEqual(metadata.album_total, 5) + + metadata = ApeTag([ + ApeTagItem(0, 0, "Media", "foo / 4")]) + metadata.album_total = 5 + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Media", "foo / 5")]) + self.assertEqual(metadata.album_total, 5) + + #other fields add a new item if necessary + #while leaving the rest alone + metadata = ApeTag([]) + metadata.track_name = u"Track Name" + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Title", "Track Name")]) + self.assertEqual(metadata.track_name, u"Track Name") + + metadata = ApeTag([ApeTagItem(0, 0, "Foo", "Bar")]) + metadata.track_name = u"Track Name" + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Foo", "Bar"), + ApeTagItem(0, 0, "Title", "Track Name")]) + self.assertEqual(metadata.track_name, u"Track Name") + + #other fields update the first match + #while leaving the rest alone + metadata = ApeTag([ApeTagItem(0, 0, "Title", "Blah")]) + metadata.track_name = u"Track Name" + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Title", "Track Name")]) + self.assertEqual(metadata.track_name, u"Track Name") + + metadata = ApeTag([ApeTagItem(0, 0, "Title", "Blah"), + ApeTagItem(0, 0, "Title", "Spam")]) + metadata.track_name = u"Track Name" + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Title", "Track Name"), + ApeTagItem(0, 0, "Title", "Spam")]) + self.assertEqual(metadata.track_name, u"Track Name") + + #setting field to an empty string is okay + metadata = ApeTag([]) + metadata.track_name = u"" + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Title", "")]) + self.assertEqual(metadata.track_name, u"") + + @METADATA_WAVPACK + def test_delattr(self): + from audiotools.ape import ApeTag,ApeTagItem + + #deleting nonexistent field is okay + for field in audiotools.MetaData.FIELDS: + metadata = ApeTag([]) + delattr(metadata, field) + self.assertEqual(getattr(metadata, field), None) + + #deleting field removes all instances of it + metadata = ApeTag([]) + del(metadata.track_name) + self.assertEqual(metadata.tags, []) + self.assertEqual(metadata.track_name, None) + + metadata = ApeTag([ApeTagItem(0, 0, "Title", "Track Name")]) + del(metadata.track_name) + self.assertEqual(metadata.tags, []) + self.assertEqual(metadata.track_name, None) + + metadata = ApeTag([ApeTagItem(0, 0, "Title", "Track Name"), + ApeTagItem(0, 0, "Title", "Track Name 2")]) + del(metadata.track_name) + self.assertEqual(metadata.tags, []) + self.assertEqual(metadata.track_name, None) + + #setting field to None is the same as deleting field + metadata = ApeTag([]) + metadata.track_name = None + self.assertEqual(metadata.tags, []) + self.assertEqual(metadata.track_name, None) + + metadata = ApeTag([ApeTagItem(0, 0, "Title", "Track Name")]) + metadata.track_name = None + self.assertEqual(metadata.tags, []) + self.assertEqual(metadata.track_name, None) + + metadata = ApeTag([ApeTagItem(0, 0, "Title", "Track Name"), + ApeTagItem(0, 0, "Title", "Track Name 2")]) + metadata.track_name = None + self.assertEqual(metadata.tags, []) + self.assertEqual(metadata.track_name, None) + + #deleting track_number without track_total removes "Track" field + metadata = ApeTag([ApeTagItem(0, 0, "Track", "1")]) + del(metadata.track_number) + self.assertEqual(metadata.tags, []) + self.assertEqual(metadata.track_number, None) + + metadata = ApeTag([ApeTagItem(0, 0, "Track", "1")]) + metadata.track_number = None + self.assertEqual(metadata.tags, []) + self.assertEqual(metadata.track_number, None) + + metadata = ApeTag([ApeTagItem(0, 0, "Track", "foo 1 bar")]) + metadata.track_number = None + self.assertEqual(metadata.tags, []) + self.assertEqual(metadata.track_number, None) + + #deleting track_number with track_total converts track_number to 0 + metadata = ApeTag([ApeTagItem(0, 0, "Track", "1/2")]) + del(metadata.track_number) + self.assertEqual(metadata.tags, [ApeTagItem(0, 0, "Track", "0/2")]) + self.assertEqual(metadata.track_number, None) + self.assertEqual(metadata.track_total, 2) + + metadata = ApeTag([ApeTagItem(0, 0, "Track", "1/2")]) + metadata.track_number = None + self.assertEqual(metadata.tags, [ApeTagItem(0, 0, "Track", "0/2")]) + self.assertEqual(metadata.track_number, None) + self.assertEqual(metadata.track_total, 2) + + metadata = ApeTag([ApeTagItem(0, 0, "Track", "foo 1 bar / baz 2 blah")]) + metadata.track_number = None + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Track", "foo 0 bar / baz 2 blah")]) + self.assertEqual(metadata.track_number, None) + self.assertEqual(metadata.track_total, 2) + + #deleting track_total without track_number removes "Track" field + metadata = ApeTag([ApeTagItem(0, 0, "Track", "0/2")]) + del(metadata.track_total) + self.assertEqual(metadata.tags, []) + self.assertEqual(metadata.track_number, None) + self.assertEqual(metadata.track_total, None) + + metadata = ApeTag([ApeTagItem(0, 0, "Track", "0/2")]) + metadata.track_total = None + self.assertEqual(metadata.tags, []) + self.assertEqual(metadata.track_number, None) + self.assertEqual(metadata.track_total, None) + + metadata = ApeTag([ApeTagItem(0, 0, "Track", "foo 0 bar / baz 2 blah")]) + metadata.track_total = None + self.assertEqual(metadata.tags, []) + self.assertEqual(metadata.track_number, None) + self.assertEqual(metadata.track_total, None) + + #deleting track_total with track_number removes slashed field + metadata = ApeTag([ApeTagItem(0, 0, "Track", "1/2")]) + del(metadata.track_total) + self.assertEqual(metadata.tags, [ApeTagItem(0, 0, "Track", "1")]) + self.assertEqual(metadata.track_number, 1) + self.assertEqual(metadata.track_total, None) + + metadata = ApeTag([ApeTagItem(0, 0, "Track", "1/2/3")]) + del(metadata.track_total) + self.assertEqual(metadata.tags, [ApeTagItem(0, 0, "Track", "1")]) + self.assertEqual(metadata.track_number, 1) + self.assertEqual(metadata.track_total, None) + + metadata = ApeTag([ApeTagItem(0, 0, "Track", "foo 1 bar / baz 2 blah")]) + del(metadata.track_total) + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Track", "foo 1 bar")]) + self.assertEqual(metadata.track_number, 1) + self.assertEqual(metadata.track_total, None) + + metadata = ApeTag([ApeTagItem(0, 0, "Track", "foo 1 bar / baz 2 blah")]) + metadata.track_total = None + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Track", "foo 1 bar")]) + self.assertEqual(metadata.track_number, 1) + self.assertEqual(metadata.track_total, None) + + #deleting album_number without album_total removes "Media" field + metadata = ApeTag([ApeTagItem(0, 0, "Media", "0/4")]) + del(metadata.album_total) + self.assertEqual(metadata.tags, []) + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.album_total, None) + + metadata = ApeTag([ApeTagItem(0, 0, "Media", "0/4")]) + metadata.album_total = None + self.assertEqual(metadata.tags, []) + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.album_total, None) + + metadata = ApeTag([ApeTagItem(0, 0, "Media", "foo 0 bar / baz 4 blah")]) + metadata.album_total = None + self.assertEqual(metadata.tags, []) + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.album_total, None) + + #deleting album_number with album_total converts album_number to 0 + metadata = ApeTag([ApeTagItem(0, 0, "Media", "3/4")]) + del(metadata.album_number) + self.assertEqual(metadata.tags, [ApeTagItem(0, 0, "Media", "0/4")]) + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.album_total, 4) + + metadata = ApeTag([ApeTagItem(0, 0, "Media", "3/4")]) + metadata.album_number = None + self.assertEqual(metadata.tags, [ApeTagItem(0, 0, "Media", "0/4")]) + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.album_total, 4) + + metadata = ApeTag([ApeTagItem(0, 0, "Media", "foo 3 bar / baz 4 blah")]) + metadata.album_number = None + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Media", "foo 0 bar / baz 4 blah")]) + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.album_total, 4) + + #deleting album_total without album_number removes "Media" field + metadata = ApeTag([ApeTagItem(0, 0, "Media", "0/4")]) + del(metadata.album_total) + self.assertEqual(metadata.tags, []) + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.album_total, None) + + metadata = ApeTag([ApeTagItem(0, 0, "Media", "0/4")]) + metadata.album_total = None + self.assertEqual(metadata.tags, []) + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.album_total, None) + + metadata = ApeTag([ApeTagItem(0, 0, "Media", "foo 0 bar / baz 4 blah")]) + metadata.album_total = None + self.assertEqual(metadata.tags, []) + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.album_total, None) + + #deleting album_total with album_number removes slashed field + metadata = ApeTag([ApeTagItem(0, 0, "Media", "1/2")]) + del(metadata.album_total) + self.assertEqual(metadata.tags, [ApeTagItem(0, 0, "Media", "1")]) + self.assertEqual(metadata.album_number, 1) + self.assertEqual(metadata.album_total, None) + + metadata = ApeTag([ApeTagItem(0, 0, "Media", "1/2/3")]) + del(metadata.album_total) + self.assertEqual(metadata.tags, [ApeTagItem(0, 0, "Media", "1")]) + self.assertEqual(metadata.album_number, 1) + self.assertEqual(metadata.album_total, None) + + metadata = ApeTag([ApeTagItem(0, 0, "Media", "foo 1 bar / baz 2 blah")]) + del(metadata.album_total) + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Media", "foo 1 bar")]) + self.assertEqual(metadata.album_number, 1) + self.assertEqual(metadata.album_total, None) + + metadata = ApeTag([ApeTagItem(0, 0, "Media", "foo 1 bar / baz 2 blah")]) + metadata.album_total = None + self.assertEqual(metadata.tags, + [ApeTagItem(0, 0, "Media", "foo 1 bar")]) + self.assertEqual(metadata.album_number, 1) + self.assertEqual(metadata.album_total, None) + + @METADATA_WAVPACK def test_update(self): import os @@ -391,7 +970,7 @@ track.get_metadata().__getitem__, "replaygain_track_gain") metadata["replaygain_track_gain"] = \ - audiotools.ApeTagItem.string( + audiotools.ape.ApeTagItem.string( "replaygain_track_gain", u"???") track.set_metadata(metadata) self.assertRaises(KeyError, @@ -399,13 +978,13 @@ "replaygain_track_gain") track.update_metadata(metadata) self.assertEqual(track.get_metadata()["replaygain_track_gain"], - audiotools.ApeTagItem.string( + audiotools.ape.ApeTagItem.string( "replaygain_track_gain", u"???")) #cuesheet not updated with set_metadata() #but can be updated with update_metadata() metadata["Cuesheet"] = \ - audiotools.ApeTagItem.string( + audiotools.ape.ApeTagItem.string( "Cuesheet", u"???") track.set_metadata(metadata) self.assertRaises(KeyError, @@ -413,7 +992,7 @@ "Cuesheet") track.update_metadata(metadata) self.assertEqual(track.get_metadata()["Cuesheet"], - audiotools.ApeTagItem.string( + audiotools.ape.ApeTagItem.string( "Cuesheet", u"???")) finally: @@ -423,11 +1002,11 @@ @METADATA_WAVPACK def test_foreign_field(self): metadata = audiotools.ApeTag( - [audiotools.ApeTagItem(0, False, "Title", 'Track Name'), - audiotools.ApeTagItem(0, False, "Album", 'Album Name'), - audiotools.ApeTagItem(0, False, "Track", "1/3"), - audiotools.ApeTagItem(0, False, "Media", "2/4"), - audiotools.ApeTagItem(0, False, "Foo", "Bar")]) + [audiotools.ape.ApeTagItem(0, False, "Title", 'Track Name'), + audiotools.ape.ApeTagItem(0, False, "Album", 'Album Name'), + audiotools.ape.ApeTagItem(0, False, "Track", "1/3"), + audiotools.ape.ApeTagItem(0, False, "Media", "2/4"), + audiotools.ape.ApeTagItem(0, False, "Foo", "Bar")]) for format in self.supported_formats: temp_file = tempfile.NamedTemporaryFile( suffix="." + format.SUFFIX) @@ -480,7 +1059,7 @@ for (field, key, value) in mapping: track.delete_metadata() metadata = self.empty_metadata() - metadata[key] = audiotools.ApeTagItem.string( + metadata[key] = audiotools.ape.ApeTagItem.string( key, unicode(value)) self.assertEqual(getattr(metadata, field), value) self.assertEqual(unicode(metadata[key]), unicode(value)) @@ -500,12 +1079,15 @@ metadata.track_total = 2 track.set_metadata(metadata) metadata = track.get_metadata() + if (unicode(metadata['Track']) != u'1/2'): + print repr(metadata) + self.assert_(False) self.assertEqual(unicode(metadata['Track']), u'1/2') - del(metadata.track_number) + del(metadata.track_total) track.set_metadata(metadata) metadata = track.get_metadata() - self.assertEqual(unicode(metadata['Track']), u'0/2') - del(metadata.track_total) + self.assertEqual(unicode(metadata['Track']), u'1') + del(metadata.track_number) track.set_metadata(metadata) metadata = track.get_metadata() self.assertRaises(KeyError, @@ -522,11 +1104,11 @@ track.set_metadata(metadata) metadata = track.get_metadata() self.assertEqual(unicode(metadata['Media']), u'3/4') - del(metadata.album_number) + del(metadata.album_total) track.set_metadata(metadata) metadata = track.get_metadata() - self.assertEqual(unicode(metadata['Media']), u'0/4') - del(metadata.album_total) + self.assertEqual(unicode(metadata['Media']), u'3') + del(metadata.album_number) track.set_metadata(metadata) metadata = track.get_metadata() self.assertRaises(KeyError, @@ -537,56 +1119,55 @@ #updates the numerical fields track.delete_metadata() metadata = self.empty_metadata() - metadata['Track'] = audiotools.ApeTagItem.string( + metadata['Track'] = audiotools.ape.ApeTagItem.string( 'Track', u"1") track.set_metadata(metadata) metadata = track.get_metadata() self.assertEqual(metadata.track_number, 1) - self.assertEqual(metadata.track_total, 0) - metadata['Track'] = audiotools.ApeTagItem.string( + self.assertEqual(metadata.track_total, None) + metadata['Track'] = audiotools.ape.ApeTagItem.string( 'Track', u"1/2") track.set_metadata(metadata) metadata = track.get_metadata() self.assertEqual(metadata.track_number, 1) self.assertEqual(metadata.track_total, 2) - metadata['Track'] = audiotools.ApeTagItem.string( + metadata['Track'] = audiotools.ape.ApeTagItem.string( 'Track', u"0/2") track.set_metadata(metadata) metadata = track.get_metadata() - self.assertEqual(metadata.track_number, 0) + self.assertEqual(metadata.track_number, None) self.assertEqual(metadata.track_total, 2) del(metadata['Track']) track.set_metadata(metadata) metadata = track.get_metadata() - self.assertEqual(metadata.track_number, 0) - self.assertEqual(metadata.track_total, 0) + self.assertEqual(metadata.track_number, None) + self.assertEqual(metadata.track_total, None) track.delete_metadata() metadata = self.empty_metadata() - metadata['Media'] = audiotools.ApeTagItem.string( + metadata['Media'] = audiotools.ape.ApeTagItem.string( 'Media', u"3") track.set_metadata(metadata) metadata = track.get_metadata() self.assertEqual(metadata.album_number, 3) - self.assertEqual(metadata.album_total, 0) - metadata['Media'] = audiotools.ApeTagItem.string( + self.assertEqual(metadata.album_total, None) + metadata['Media'] = audiotools.ape.ApeTagItem.string( 'Media', u"3/4") track.set_metadata(metadata) metadata = track.get_metadata() self.assertEqual(metadata.album_number, 3) self.assertEqual(metadata.album_total, 4) - metadata['Media'] = audiotools.ApeTagItem.string( + metadata['Media'] = audiotools.ape.ApeTagItem.string( 'Media', u"0/4") track.set_metadata(metadata) metadata = track.get_metadata() - self.assertEqual(metadata.album_number, 0) + self.assertEqual(metadata.album_number, None) self.assertEqual(metadata.album_total, 4) del(metadata['Media']) track.set_metadata(metadata) metadata = track.get_metadata() - self.assertEqual(metadata.album_number, 0) - self.assertEqual(metadata.album_total, 0) - + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.album_total, None) finally: temp_file.close() @@ -625,17 +1206,15 @@ if (field in self.supported_fields): self.assertEqual(getattr(metadata_orig, field), getattr(metadata_new, field)) - elif (field in audiotools.MetaData.INTEGER_FIELDS): - self.assertEqual(getattr(metadata_new, field), 0) else: - self.assertEqual(getattr(metadata_new, field), u"") + self.assertEqual(getattr(metadata_new, field), None) #ensure images match, if supported self.assertEqual(metadata_new.images(), [image1, image2]) #ensure non-MetaData fields are converted metadata_orig = self.empty_metadata() - metadata_orig['Foo'] = audiotools.ApeTagItem.string( + metadata_orig['Foo'] = audiotools.ape.ApeTagItem.string( 'Foo', u'Bar'.encode('utf-8')) metadata_new = self.metadata_class.converted(metadata_orig) self.assertEqual(metadata_orig['Foo'].data, @@ -700,104 +1279,209 @@ @METADATA_WAVPACK def test_clean(self): + from audiotools.ape import ApeTag,ApeTagItem + from audiotools.text import (CLEAN_REMOVE_TRAILING_WHITESPACE, + CLEAN_REMOVE_LEADING_WHITESPACE, + CLEAN_REMOVE_EMPTY_TAG, + CLEAN_REMOVE_DUPLICATE_TAG, + CLEAN_FIX_TAG_FORMATTING) + + #although the spec says APEv2 tags should be sorted + #ascending by size, I don't think anybody does this in practice + #check trailing whitespace - metadata = audiotools.ApeTag( - [audiotools.ApeTagItem.string('Title', u'Foo ')]) + metadata = ApeTag( + [ApeTagItem.string('Title', u'Foo ')]) self.assertEqual(metadata.track_name, u'Foo ') self.assertEqual(metadata['Title'].data, u'Foo '.encode('ascii')) fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed trailing whitespace from %(field)s") % + [CLEAN_REMOVE_TRAILING_WHITESPACE % {"field":'Title'.decode('ascii')}]) self.assertEqual(cleaned.track_name, u'Foo') self.assertEqual(cleaned['Title'].data, u'Foo'.encode('ascii')) #check leading whitespace - metadata = audiotools.ApeTag( - [audiotools.ApeTagItem.string('Title', u' Foo')]) + metadata = ApeTag( + [ApeTagItem.string('Title', u' Foo')]) self.assertEqual(metadata.track_name, u' Foo') self.assertEqual(metadata['Title'].data, u' Foo'.encode('ascii')) fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed leading whitespace from %(field)s") % + [CLEAN_REMOVE_LEADING_WHITESPACE % {"field":'Title'.decode('ascii')}]) self.assertEqual(cleaned.track_name, u'Foo') self.assertEqual(cleaned['Title'].data, u'Foo'.encode('ascii')) #check empty fields - metadata = audiotools.ApeTag( - [audiotools.ApeTagItem.string('Title', u'')]) + metadata = ApeTag( + [ApeTagItem.string('Title', u'')]) self.assertEqual(metadata.track_name, u'') self.assertEqual(metadata['Title'].data, u''.encode('ascii')) fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed empty field %(field)s") % + [CLEAN_REMOVE_EMPTY_TAG % {"field":'Title'.decode('ascii')}]) - self.assertEqual(cleaned.track_name, u'') + self.assertEqual(cleaned.track_name, None) self.assertRaises(KeyError, cleaned.__getitem__, 'Title') + #check duplicate fields + metadata = ApeTag( + [ApeTagItem.string('Title', u'Track Name 1'), + ApeTagItem.string('Title', u'Track Name 2')]) + fixes = [] + cleaned = metadata.clean(fixes) + self.assertEqual(fixes, + [CLEAN_REMOVE_DUPLICATE_TAG % + {"field":'Title'.decode('ascii')}]) + self.assertEqual(cleaned.tags, + [ApeTagItem.string('Title', u'Track Name 1')]) + + #check fields that differ only by case + metadata = ApeTag( + [ApeTagItem.string('title', u'Track Name 1'), + ApeTagItem.string('Title', u'Track Name 2')]) + fixes = [] + cleaned = metadata.clean(fixes) + self.assertEqual(fixes, + [CLEAN_REMOVE_DUPLICATE_TAG % + {"field":'Title'.decode('ascii')}]) + self.assertEqual(cleaned.tags, + [ApeTagItem.string('title', u'Track Name 1')]) + #check leading zeroes - metadata = audiotools.ApeTag( - [audiotools.ApeTagItem.string('Track', u'01')]) + metadata = ApeTag( + [ApeTagItem.string('Track', u'01')]) self.assertEqual(metadata.track_number, 1) - self.assertEqual(metadata.track_total, 0) + self.assertEqual(metadata.track_total, None) self.assertEqual(metadata['Track'].data, u'01'.encode('ascii')) fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed leading zeroes from %(field)s") % + [CLEAN_FIX_TAG_FORMATTING % {"field":'Track'.decode('ascii')}]) self.assertEqual(cleaned.track_number, 1) - self.assertEqual(cleaned.track_total, 0) + self.assertEqual(cleaned.track_total, None) self.assertEqual(cleaned['Track'].data, u'1'.encode('ascii')) - metadata = audiotools.ApeTag( - [audiotools.ApeTagItem.string('Track', u'01/2')]) + metadata = ApeTag( + [ApeTagItem.string('Track', u'01/2')]) self.assertEqual(metadata.track_number, 1) self.assertEqual(metadata.track_total, 2) self.assertEqual(metadata['Track'].data, u'01/2'.encode('ascii')) fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed leading zeroes from %(field)s") % + [CLEAN_FIX_TAG_FORMATTING % {"field":'Track'.decode('ascii')}]) self.assertEqual(cleaned.track_number, 1) self.assertEqual(cleaned.track_total, 2) self.assertEqual(cleaned['Track'].data, u'1/2'.encode('ascii')) - metadata = audiotools.ApeTag( - [audiotools.ApeTagItem.string('Track', u'1/02')]) + metadata = ApeTag( + [ApeTagItem.string('Track', u'1/02')]) self.assertEqual(metadata.track_number, 1) self.assertEqual(metadata.track_total, 2) self.assertEqual(metadata['Track'].data, u'1/02'.encode('ascii')) fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed leading zeroes from %(field)s") % + [CLEAN_FIX_TAG_FORMATTING % {"field":'Track'.decode('ascii')}]) self.assertEqual(cleaned.track_number, 1) self.assertEqual(cleaned.track_total, 2) self.assertEqual(cleaned['Track'].data, u'1/2'.encode('ascii')) - metadata = audiotools.ApeTag( - [audiotools.ApeTagItem.string('Track', u'01/02')]) + metadata = ApeTag( + [ApeTagItem.string('Track', u'01/02')]) self.assertEqual(metadata.track_number, 1) self.assertEqual(metadata.track_total, 2) self.assertEqual(metadata['Track'].data, u'01/02'.encode('ascii')) fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed leading zeroes from %(field)s") % + [CLEAN_FIX_TAG_FORMATTING % {"field":'Track'.decode('ascii')}]) self.assertEqual(cleaned.track_number, 1) self.assertEqual(cleaned.track_total, 2) self.assertEqual(cleaned['Track'].data, u'1/2'.encode('ascii')) + #check junk in slashed fields + metadata = ApeTag( + [ApeTagItem.string('Track', u'1/foo')]) + fixes = [] + cleaned = metadata.clean(fixes) + self.assertEqual(fixes, + [CLEAN_FIX_TAG_FORMATTING % + {"field":'Track'.decode('ascii')}]) + self.assertEqual(cleaned.tags, + [ApeTagItem.string('Track', u'1')]) + + metadata = ApeTag( + [ApeTagItem.string('Track', u'foo/2')]) + fixes = [] + cleaned = metadata.clean(fixes) + self.assertEqual(fixes, + [CLEAN_FIX_TAG_FORMATTING % + {"field":'Track'.decode('ascii')}]) + self.assertEqual(cleaned.tags, + [ApeTagItem.string('Track', u'0/2')]) + + metadata = ApeTag( + [ApeTagItem.string('Track', u'1/ baz 2 blah')]) + fixes = [] + cleaned = metadata.clean(fixes) + self.assertEqual(fixes, + [CLEAN_FIX_TAG_FORMATTING % + {"field":'Track'.decode('ascii')}]) + self.assertEqual(cleaned.tags, + [ApeTagItem.string('Track', u'1/2')]) + + metadata = ApeTag( + [ApeTagItem.string('Track', u'foo 1 bar /2')]) + fixes = [] + cleaned = metadata.clean(fixes) + self.assertEqual(fixes, + [CLEAN_FIX_TAG_FORMATTING % + {"field":'Track'.decode('ascii')}]) + self.assertEqual(cleaned.tags, + [ApeTagItem.string('Track', u'1/2')]) + + metadata = ApeTag( + [ApeTagItem.string('Track', u'foo 1 bar / baz 2 blah')]) + fixes = [] + cleaned = metadata.clean(fixes) + self.assertEqual(fixes, + [CLEAN_FIX_TAG_FORMATTING % + {"field":'Track'.decode('ascii')}]) + self.assertEqual(cleaned.tags, + [ApeTagItem.string('Track', u'1/2')]) + + metadata = ApeTag( + [ApeTagItem.string('Track', u'1/2/3')]) + fixes = [] + cleaned = metadata.clean(fixes) + self.assertEqual(fixes, + [CLEAN_FIX_TAG_FORMATTING % + {"field":'Track'.decode('ascii')}]) + self.assertEqual(cleaned.tags, + [ApeTagItem.string('Track', u'1/2')]) + + metadata = ApeTag( + [ApeTagItem.string('Track', u'1 / 2 / 3')]) + fixes = [] + cleaned = metadata.clean(fixes) + self.assertEqual(fixes, + [CLEAN_FIX_TAG_FORMATTING % + {"field":'Track'.decode('ascii')}]) + self.assertEqual(cleaned.tags, + [ApeTagItem.string('Track', u'1/2')]) + #images don't store metadata, #so no need to check their fields @@ -878,13 +1562,7 @@ audiotools.MP2Audio] def empty_metadata(self): - return self.metadata_class(track_name=u"", - artist_name=u"", - album_name=u"", - year=u"", - comment=u"", - track_number=0, - genre=0) + return self.metadata_class() @METADATA_ID3V1 def test_update(self): @@ -897,7 +1575,9 @@ temp_file_stat = os.stat(temp_file.name)[0] try: #update_metadata on file's internal metadata round-trips okay - track.set_metadata(audiotools.MetaData(track_name=u"Foo")) + metadata = self.empty_metadata() + metadata.track_name = u"Foo" + track.set_metadata(metadata) metadata = track.get_metadata() self.assertEqual(metadata.track_name, u"Foo") metadata.track_name = u"Bar" @@ -974,12 +1654,12 @@ setattr(metadata, field, u"") track.set_metadata(metadata) metadata = track.get_metadata() - self.assertEqual(getattr(metadata, field), u"") + self.assertEqual(getattr(metadata, field), None) else: setattr(metadata, field, 0) track.set_metadata(metadata) metadata = track.get_metadata() - self.assertEqual(getattr(metadata, field), 0) + self.assertEqual(getattr(metadata, field), None) #re-set the fields with random values for field in self.supported_fields: @@ -1006,10 +1686,7 @@ delattr(metadata, field) track.set_metadata(metadata) metadata = track.get_metadata() - if (field not in audiotools.MetaData.INTEGER_FIELDS): - self.assertEqual(getattr(metadata, field), u"") - else: - self.assertEqual(getattr(metadata, field), 0) + self.assertEqual(getattr(metadata, field), None) finally: temp_file.close() @@ -1046,53 +1723,34 @@ @METADATA_ID3V1 def test_clean(self): + from audiotools.text import (CLEAN_REMOVE_TRAILING_WHITESPACE, + CLEAN_REMOVE_LEADING_WHITESPACE) + #check trailing whitespace metadata = audiotools.ID3v1Comment( - track_name=u"Title ", - artist_name=u"", - album_name=u"", - year=u"", - comment=u"", - track_number=1, - genre=0) + track_name="Title " + chr(0) * 24) results = [] cleaned = metadata.clean(results) self.assertEqual(results, - [_(u"removed trailing whitespace from title")]) + [CLEAN_REMOVE_TRAILING_WHITESPACE % + {"field":u"title"}]) self.assertEqual( cleaned, audiotools.ID3v1Comment( - track_name=u"Title", - artist_name=u"", - album_name=u"", - year=u"", - comment=u"", - track_number=1, - genre=0)) + track_name="Title" + chr(0) * 25)) #check leading whitespace metadata = audiotools.ID3v1Comment( - track_name=u" Title", - artist_name=u"", - album_name=u"", - year=u"", - comment=u"", - track_number=1, - genre=0) + track_name=" Title" + chr(0) * 24) results = [] cleaned = metadata.clean(results) self.assertEqual(results, - [_(u"removed leading whitespace from title")]) + [CLEAN_REMOVE_LEADING_WHITESPACE % + {"field":u"title"}]) self.assertEqual( cleaned, audiotools.ID3v1Comment( - track_name=u"Title", - artist_name=u"", - album_name=u"", - year=u"", - comment=u"", - track_number=1, - genre=0)) + track_name="Title" + chr(0) * 25)) #ID3v1 has no empty fields, image data or leading zeroes #so those can be safely ignored @@ -1125,6 +1783,16 @@ def empty_metadata(self): return self.metadata_class([]) + def text_tag(self, attribute, unicode_text): + return self.metadata_class.TEXT_FRAME.converted( + self.metadata_class.ATTRIBUTE_MAP[attribute], + unicode_text) + + def unknown_tag(self, binary_string): + from audiotools.id3 import ID3v22_Frame + + return ID3v22_Frame("XXX", binary_string) + @METADATA_ID3V2 def test_update(self): import os @@ -1167,11 +1835,11 @@ @METADATA_ID3V2 def test_foreign_field(self): metadata = audiotools.ID3v22Comment( - [audiotools.ID3v22_T__Frame("TT2", 0, "Track Name"), - audiotools.ID3v22_T__Frame("TAL", 0, "Album Name"), - audiotools.ID3v22_T__Frame("TRK", 0, "1/3"), - audiotools.ID3v22_T__Frame("TPA", 0, "2/4"), - audiotools.ID3v22_T__Frame("TFO", 0, "Bar")]) + [audiotools.id3.ID3v22_T__Frame("TT2", 0, "Track Name"), + audiotools.id3.ID3v22_T__Frame("TAL", 0, "Album Name"), + audiotools.id3.ID3v22_T__Frame("TRK", 0, "1/3"), + audiotools.id3.ID3v22_T__Frame("TPA", 0, "2/4"), + audiotools.id3.ID3v22_T__Frame("TFO", 0, "Bar")]) for format in self.supported_formats: temp_file = tempfile.NamedTemporaryFile( suffix="." + format.SUFFIX) @@ -1245,7 +1913,7 @@ #>>> id3.track_number = 2 #>>> id3['TRK'][0] == u"2" id3.track_number = 3 - id3.track_total = 0 + id3.track_total = None self.assertEqual( unicode(id3[id3_class.TEXT_FRAME.NUMERICAL_IDS[0]][0]), u"3") @@ -1256,7 +1924,7 @@ u"3/8") id3.album_number = 2 - id3.album_total = 0 + id3.album_total = None self.assertEqual( unicode(id3[id3_class.TEXT_FRAME.NUMERICAL_IDS[1]][0]), u"2") @@ -1322,7 +1990,7 @@ id3_class.TEXT_FRAME.NUMERICAL_IDS[0], u"4")] self.assertEqual(id3.track_number, 4) - self.assertEqual(id3.track_total, 0) + self.assertEqual(id3.track_total, None) id3[id3_class.TEXT_FRAME.NUMERICAL_IDS[0]] = [ id3_class.TEXT_FRAME.converted( @@ -1336,7 +2004,7 @@ id3_class.TEXT_FRAME.NUMERICAL_IDS[1], u"3")] self.assertEqual(id3.album_number, 3) - self.assertEqual(id3.album_total, 0) + self.assertEqual(id3.album_total, None) id3[id3_class.TEXT_FRAME.NUMERICAL_IDS[1]] = [ id3_class.TEXT_FRAME.converted( @@ -1346,10 +2014,645 @@ self.assertEqual(id3.album_total, 7) @METADATA_ID3V2 + def test_getitem(self): + field = self.metadata_class.ATTRIBUTE_MAP["track_name"] + + #getitem with no matches raises KeyError + metadata = self.metadata_class([]) + self.assertRaises(KeyError, + metadata.__getitem__, + field) + + metadata = self.metadata_class([self.unknown_tag("FOO")]) + self.assertRaises(KeyError, + metadata.__getitem__, + field) + + #getitem with one match returns that item + metadata = self.metadata_class([self.text_tag("track_name", + u"Track Name")]) + self.assertEqual(metadata[field], + [self.text_tag("track_name", + u"Track Name")]) + + metadata = self.metadata_class([self.text_tag("track_name", + u"Track Name"), + self.unknown_tag("FOO")]) + self.assertEqual(metadata[field], + [self.text_tag("track_name", + u"Track Name")]) + + #getitem with multiple matches returns all items, in order + metadata = self.metadata_class([self.text_tag("track_name", u"1"), + self.text_tag("track_name", u"2"), + self.text_tag("track_name", u"3")]) + self.assertEqual(metadata[field], + [self.text_tag("track_name", u"1"), + self.text_tag("track_name", u"2"), + self.text_tag("track_name", u"3"),]) + + metadata = self.metadata_class([self.text_tag("track_name", u"1"), + self.unknown_tag("FOO"), + self.text_tag("track_name", u"2"), + self.unknown_tag("BAR"), + self.text_tag("track_name", u"3")]) + self.assertEqual(metadata[field], + [self.text_tag("track_name", u"1"), + self.text_tag("track_name", u"2"), + self.text_tag("track_name", u"3"),]) + + @METADATA_ID3V2 + def test_setitem(self): + field = self.metadata_class.ATTRIBUTE_MAP["track_name"] + + #setitem replaces all keys with new values + # - zero new values + metadata = self.metadata_class([]) + metadata[field] = [] + self.assertEqual(metadata.track_name, None) + self.assertEqual(metadata.frames, []) + + metadata = self.metadata_class([self.text_tag("track_name", u"X")]) + metadata[field] = [] + self.assertEqual(metadata.track_name, None) + self.assertEqual(metadata.frames, []) + + metadata = self.metadata_class([self.text_tag("track_name", u"X"), + self.text_tag("track_name", u"Y")]) + metadata[field] = [] + self.assertEqual(metadata.track_name, None) + self.assertEqual(metadata.frames, []) + + # - one new value + metadata = self.metadata_class([]) + metadata[field] = [self.text_tag("track_name", u"A")] + self.assertEqual(metadata.track_name, u"A") + self.assertEqual(metadata.frames, + [self.text_tag("track_name", u"A")]) + + metadata = self.metadata_class([self.text_tag("track_name", u"X")]) + metadata[field] = [self.text_tag("track_name", u"A")] + self.assertEqual(metadata.track_name, u"A") + self.assertEqual(metadata.frames, + [self.text_tag("track_name", u"A")]) + + metadata = self.metadata_class([self.text_tag("track_name", u"X"), + self.text_tag("track_name", u"Y")]) + metadata[field] = [self.text_tag("track_name", u"A")] + self.assertEqual(metadata.track_name, u"A") + self.assertEqual(metadata.frames, + [self.text_tag("track_name", u"A")]) + + # - two new values + metadata = self.metadata_class([]) + metadata[field] = [self.text_tag("track_name", u"A"), + self.text_tag("track_name", u"B"),] + self.assertEqual(metadata.track_name, u"A") + self.assertEqual(metadata.frames, + [self.text_tag("track_name", u"A"), + self.text_tag("track_name", u"B")]) + + metadata = self.metadata_class([self.text_tag("track_name", u"X")]) + metadata[field] = [self.text_tag("track_name", u"A"), + self.text_tag("track_name", u"B"),] + self.assertEqual(metadata.track_name, u"A") + self.assertEqual(metadata.frames, + [self.text_tag("track_name", u"A"), + self.text_tag("track_name", u"B")]) + + metadata = self.metadata_class([self.text_tag("track_name", u"X"), + self.text_tag("track_name", u"Y")]) + metadata[field] = [self.text_tag("track_name", u"A"), + self.text_tag("track_name", u"B"),] + self.assertEqual(metadata.track_name, u"A") + self.assertEqual(metadata.frames, + [self.text_tag("track_name", u"A"), + self.text_tag("track_name", u"B")]) + + #setitem leaves other items alone + metadata = self.metadata_class([self.unknown_tag("FOO")]) + metadata[field] = [] + self.assertEqual(metadata.track_name, None) + self.assertEqual(metadata.frames, [self.unknown_tag("FOO")]) + + metadata = self.metadata_class([self.unknown_tag("FOO"), + self.text_tag("track_name", u"X")]) + metadata[field] = [self.text_tag("track_name", u"A")] + self.assertEqual(metadata.track_name, u"A") + self.assertEqual(metadata.frames, + [self.unknown_tag("FOO"), + self.text_tag("track_name", u"A")]) + + + metadata = self.metadata_class([self.text_tag("track_name", u"X"), + self.unknown_tag("FOO"), + self.text_tag("track_name", u"Y")]) + metadata[field] = [self.text_tag("track_name", u"A"), + self.text_tag("track_name", u"B"),] + self.assertEqual(metadata.track_name, u"A") + self.assertEqual(metadata.frames, + [self.text_tag("track_name", u"A"), + self.unknown_tag("FOO"), + self.text_tag("track_name", u"B")]) + + @METADATA_ID3V2 + def test_getattr(self): + #track_number grabs the first available integer, if any + metadata = self.metadata_class([]) + self.assertEqual(metadata.track_number, None) + + metadata = self.metadata_class([ + self.text_tag("track_number", u"1")]) + self.assertEqual(metadata.track_number, 1) + + metadata = self.metadata_class([ + self.text_tag("track_number", u"foo")]) + self.assertEqual(metadata.track_number, None) + + metadata = self.metadata_class([ + self.text_tag("track_number", u"1/2")]) + self.assertEqual(metadata.track_number, 1) + + metadata = self.metadata_class([ + self.text_tag("track_number", u"foo 1 bar")]) + self.assertEqual(metadata.track_number, 1) + + #album_number grabs the first available integer, if any + metadata = self.metadata_class([]) + self.assertEqual(metadata.album_number, None) + + metadata = self.metadata_class([ + self.text_tag("album_number", u"2")]) + self.assertEqual(metadata.album_number, 2) + + metadata = self.metadata_class([ + self.text_tag("album_number", u"foo")]) + self.assertEqual(metadata.album_number, None) + + metadata = self.metadata_class([ + self.text_tag("album_number", u"2/4")]) + self.assertEqual(metadata.album_number, 2) + + metadata = self.metadata_class([ + self.text_tag("album_number", u"foo 2 bar")]) + self.assertEqual(metadata.album_number, 2) + + #track_total grabs the first slashed field integer, if any + metadata = self.metadata_class([]) + self.assertEqual(metadata.track_total, None) + + metadata = self.metadata_class([ + self.text_tag("track_number", u"1")]) + self.assertEqual(metadata.track_total, None) + + metadata = self.metadata_class([ + self.text_tag("track_number", u"foo")]) + self.assertEqual(metadata.track_total, None) + + metadata = self.metadata_class([ + self.text_tag("track_number", u"1/2")]) + self.assertEqual(metadata.track_total, 2) + + metadata = self.metadata_class([ + self.text_tag("track_number", u"foo 1 bar / baz 2 blah")]) + self.assertEqual(metadata.track_total, 2) + + #album_total grabs the first slashed field integer, if any + metadata = self.metadata_class([]) + self.assertEqual(metadata.album_total, None) + + metadata = self.metadata_class([ + self.text_tag("album_number", u"2")]) + self.assertEqual(metadata.album_total, None) + + metadata = self.metadata_class([ + self.text_tag("album_number", u"foo")]) + self.assertEqual(metadata.album_total, None) + + metadata = self.metadata_class([ + self.text_tag("album_number", u"2/4")]) + self.assertEqual(metadata.album_total, 4) + + metadata = self.metadata_class([ + self.text_tag("album_number", u"foo 2 bar / baz 4 blah")]) + self.assertEqual(metadata.album_total, 4) + + #other fields grab the first available item, if any + metadata = self.metadata_class([]) + self.assertEqual(metadata.track_name, None) + + metadata = self.metadata_class([self.text_tag("track_name", u"1")]) + self.assertEqual(metadata.track_name, u"1") + + metadata = self.metadata_class([self.text_tag("track_name", u"1"), + self.text_tag("track_name", u"2")]) + self.assertEqual(metadata.track_name, u"1") + + @METADATA_ID3V2 + def test_setattr(self): + #track_number adds new field if necessary + metadata = self.metadata_class([]) + metadata.track_number = 1 + self.assertEqual(metadata.track_number, 1) + self.assertEqual(metadata.frames, + [self.text_tag("track_number", u"1")]) + + #track_number updates the first integer field + #and leaves other junk in that field alone + metadata = self.metadata_class([ + self.text_tag("track_number", u"6")]) + metadata.track_number = 1 + self.assertEqual(metadata.track_number, 1) + self.assertEqual(metadata.frames, + [self.text_tag("track_number", u"1")]) + + metadata = self.metadata_class([ + self.text_tag("track_number", u"6"), + self.text_tag("track_number", u"10")]) + metadata.track_number = 1 + self.assertEqual(metadata.track_number, 1) + self.assertEqual(metadata.frames, + [self.text_tag("track_number", u"1"), + self.text_tag("track_number", u"10")]) + + metadata = self.metadata_class([ + self.text_tag("track_number", u"6/2")]) + metadata.track_number = 1 + self.assertEqual(metadata.track_number, 1) + self.assertEqual(metadata.frames, + [self.text_tag("track_number", u"1/2")]) + + metadata = self.metadata_class([ + self.text_tag("track_number", u"foo 6 bar")]) + metadata.track_number = 1 + self.assertEqual(metadata.track_number, 1) + self.assertEqual(metadata.frames, + [self.text_tag("track_number", u"foo 1 bar")]) + + metadata = self.metadata_class([ + self.text_tag("track_number", u"foo 6 bar / blah 7 baz")]) + metadata.track_number = 1 + self.assertEqual(metadata.track_number, 1) + self.assertEqual(metadata.frames, + [self.text_tag("track_number", + u"foo 1 bar / blah 7 baz")]) + + #album_number adds new field if necessary + metadata = self.metadata_class([]) + metadata.album_number = 3 + self.assertEqual(metadata.album_number, 3) + self.assertEqual(metadata.frames, + [self.text_tag("album_number", u"3")]) + + #album_number updates the first integer field + #and leaves other junk in that field alone + metadata = self.metadata_class([ + self.text_tag("album_number", u"7")]) + metadata.album_number = 3 + self.assertEqual(metadata.album_number, 3) + self.assertEqual(metadata.frames, + [self.text_tag("album_number", u"3")]) + + metadata = self.metadata_class([ + self.text_tag("album_number", u"7"), + self.text_tag("album_number", u"10")]) + metadata.album_number = 3 + self.assertEqual(metadata.album_number, 3) + self.assertEqual(metadata.frames, + [self.text_tag("album_number", u"3"), + self.text_tag("album_number", u"10")]) + + metadata = self.metadata_class([ + self.text_tag("album_number", u"7/4")]) + metadata.album_number = 3 + self.assertEqual(metadata.album_number, 3) + self.assertEqual(metadata.frames, + [self.text_tag("album_number", u"3/4")]) + + metadata = self.metadata_class([ + self.text_tag("album_number", u"foo 7 bar")]) + metadata.album_number = 3 + self.assertEqual(metadata.album_number, 3) + self.assertEqual(metadata.frames, + [self.text_tag("album_number", u"foo 3 bar")]) + + metadata = self.metadata_class([ + self.text_tag("album_number", u"foo 7 bar / blah 8 baz")]) + metadata.album_number = 3 + self.assertEqual(metadata.album_number, 3) + self.assertEqual(metadata.frames, + [self.text_tag("album_number", + u"foo 3 bar / blah 8 baz")]) + + #track_total adds new field if necessary + metadata = self.metadata_class([]) + metadata.track_total = 2 + self.assertEqual(metadata.track_total, 2) + self.assertEqual(metadata.frames, + [self.text_tag("track_number", u"0/2")]) + + #track_total updates the second integer field + #and leaves other junk in that field alone + metadata = self.metadata_class([ + self.text_tag("track_number", u"6")]) + metadata.track_total = 2 + self.assertEqual(metadata.track_total, 2) + self.assertEqual(metadata.frames, + [self.text_tag("track_number", u"6/2")]) + + metadata = self.metadata_class([ + self.text_tag("track_number", u"6"), + self.text_tag("track_number", u"10")]) + metadata.track_total = 2 + self.assertEqual(metadata.track_total, 2) + self.assertEqual(metadata.frames, + [self.text_tag("track_number", u"6/2"), + self.text_tag("track_number", u"10")]) + + metadata = self.metadata_class([ + self.text_tag("track_number", u"6/7")]) + metadata.track_total = 2 + self.assertEqual(metadata.track_total, 2) + self.assertEqual(metadata.frames, + [self.text_tag("track_number", u"6/2")]) + + metadata = self.metadata_class([ + self.text_tag("track_number", u"foo 6 bar / blah 7 baz")]) + metadata.track_total = 2 + self.assertEqual(metadata.track_total, 2) + self.assertEqual(metadata.frames, + [self.text_tag("track_number", + u"foo 6 bar / blah 2 baz")]) + + #album_total adds new field if necessary + metadata = self.metadata_class([]) + metadata.album_total = 4 + self.assertEqual(metadata.album_total, 4) + self.assertEqual(metadata.frames, + [self.text_tag("album_number", u"0/4")]) + + #album_total updates the second integer field + #and leaves other junk in that field alone + metadata = self.metadata_class([ + self.text_tag("album_number", u"9")]) + metadata.album_total = 4 + self.assertEqual(metadata.album_total, 4) + self.assertEqual(metadata.frames, + [self.text_tag("album_total", u"9/4")]) + + metadata = self.metadata_class([ + self.text_tag("album_number", u"9"), + self.text_tag("album_number", u"10")]) + metadata.album_total = 4 + self.assertEqual(metadata.album_total, 4) + self.assertEqual(metadata.frames, + [self.text_tag("album_number", u"9/4"), + self.text_tag("album_number", u"10")]) + + metadata = self.metadata_class([ + self.text_tag("album_number", u"9/10")]) + metadata.album_total = 4 + self.assertEqual(metadata.album_total, 4) + self.assertEqual(metadata.frames, + [self.text_tag("album_number", u"9/4")]) + + metadata = self.metadata_class([ + self.text_tag("album_total", u"foo 9 bar / blah 10 baz")]) + metadata.album_total = 4 + self.assertEqual(metadata.album_total, 4) + self.assertEqual(metadata.frames, + [self.text_tag("album_number", + u"foo 9 bar / blah 4 baz")]) + + #other fields update the first match + #while leaving the rest alone + metadata = self.metadata_class([]) + metadata.track_name = u"A" + self.assertEqual(metadata.track_name, u"A") + self.assertEqual(metadata.frames, + [self.text_tag("track_name", u"A")]) + + metadata = self.metadata_class([self.text_tag("track_name", u"X")]) + metadata.track_name = u"A" + self.assertEqual(metadata.track_name, u"A") + self.assertEqual(metadata.frames, + [self.text_tag("track_name", u"A")]) + + metadata = self.metadata_class([self.text_tag("track_name", u"X"), + self.text_tag("track_name", u"Y")]) + metadata.track_name = u"A" + self.assertEqual(metadata.track_name, u"A") + self.assertEqual(metadata.frames, + [self.text_tag("track_name", u"A"), + self.text_tag("track_name", u"Y")]) + + #setting field to an empty string is okay + metadata = self.metadata_class([]) + metadata.track_name = u"" + self.assertEqual(metadata.track_name, u"") + self.assertEqual(metadata.frames, + [self.text_tag("track_name", u"")]) + + @METADATA_ID3V2 + def test_delattr(self): + #deleting nonexistent field is okay + for field in audiotools.MetaData.FIELDS: + metadata = self.metadata_class([]) + delattr(metadata, field) + self.assertEqual(getattr(metadata, field), None) + + #deleting field removes all instances of it + metadata = self.metadata_class([self.text_tag("track_name", u"A")]) + del(metadata.track_name) + self.assertEqual(metadata.track_name, None) + self.assertEqual(metadata.frames, []) + + metadata = self.metadata_class([self.text_tag("track_name", u"A"), + self.text_tag("track_name", u"B")]) + del(metadata.track_name) + self.assertEqual(metadata.track_name, None) + self.assertEqual(metadata.frames, []) + + #setting field to None is the same as deleting field + for field in audiotools.MetaData.FIELDS: + metadata = self.metadata_class([]) + setattr(metadata, field, None) + self.assertEqual(getattr(metadata, field), None) + + metadata = self.metadata_class([self.text_tag("track_name", u"A")]) + metadata.track_name = None + self.assertEqual(metadata.track_name, None) + self.assertEqual(metadata.frames, []) + + metadata = self.metadata_class([self.text_tag("track_name", u"A"), + self.text_tag("track_name", u"B")]) + metadata.track_name = None + self.assertEqual(metadata.track_name, None) + self.assertEqual(metadata.frames, []) + + #deleting track_number without track_total removes field + metadata = self.metadata_class([self.text_tag("track_number", u"1")]) + del(metadata.track_number) + self.assertEqual(metadata.track_number, None) + self.assertEqual(metadata.frames, []) + + metadata = self.metadata_class([self.text_tag("track_number", u"1"), + self.text_tag("track_number", u"2")]) + del(metadata.track_number) + self.assertEqual(metadata.track_number, None) + self.assertEqual(metadata.frames, []) + + metadata = self.metadata_class([self.text_tag("track_number", + u"foo 1 bar")]) + del(metadata.track_number) + self.assertEqual(metadata.track_number, None) + self.assertEqual(metadata.frames, []) + + #deleting track_number with track_total converts track_number to None + metadata = self.metadata_class([self.text_tag("track_number", u"1/2")]) + del(metadata.track_number) + self.assertEqual(metadata.track_number, None) + self.assertEqual(metadata.track_total, 2) + self.assertEqual(metadata.frames, + [self.text_tag("track_number", u"0/2")]) + + metadata = self.metadata_class([self.text_tag( + "track_number", u"foo 1 bar / blah 2 baz")]) + del(metadata.track_number) + self.assertEqual(metadata.track_number, None) + self.assertEqual(metadata.track_total, 2) + self.assertEqual(metadata.frames, + [self.text_tag("track_number", + u"foo 0 bar / blah 2 baz")]) + + #deleting track_total without track_number removes field + metadata = self.metadata_class([self.text_tag( + "track_number", u"0/1")]) + del(metadata.track_total) + self.assertEqual(metadata.track_total, None) + self.assertEqual(metadata.frames, []) + + metadata = self.metadata_class([self.text_tag( + "track_number", u"foo 0 bar / 1")]) + del(metadata.track_total) + self.assertEqual(metadata.track_total, None) + self.assertEqual(metadata.frames, []) + + metadata = self.metadata_class([self.text_tag( + "track_number", u"foo / 1")]) + del(metadata.track_total) + self.assertEqual(metadata.track_total, None) + self.assertEqual(metadata.frames, []) + + #deleting track_total with track_number removes slashed field + metadata = self.metadata_class([self.text_tag( + "track_number", u"1/2")]) + del(metadata.track_total) + self.assertEqual(metadata.track_number, 1) + self.assertEqual(metadata.track_total, None) + self.assertEqual(metadata.frames, + [self.text_tag("track_number", u"1")]) + + metadata = self.metadata_class([self.text_tag( + "track_number", u"1 / 2")]) + del(metadata.track_total) + self.assertEqual(metadata.track_number, 1) + self.assertEqual(metadata.track_total, None) + self.assertEqual(metadata.frames, + [self.text_tag("track_number", u"1")]) + + metadata = self.metadata_class([self.text_tag( + "track_number", u"foo 1 bar / baz 2 blah")]) + del(metadata.track_total) + self.assertEqual(metadata.track_number, 1) + self.assertEqual(metadata.track_total, None) + self.assertEqual(metadata.frames, + [self.text_tag("track_number", u"foo 1 bar")]) + + #deleting album_number without album_total removes field + metadata = self.metadata_class([self.text_tag("album_number", u"3")]) + del(metadata.album_number) + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.frames, []) + + metadata = self.metadata_class([self.text_tag("album_number", u"3"), + self.text_tag("album_number", u"4")]) + del(metadata.album_number) + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.frames, []) + + metadata = self.metadata_class([self.text_tag("album_number", + u"foo 3 bar")]) + del(metadata.album_number) + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.frames, []) + + #deleting album_number with album_total converts album_number to None + metadata = self.metadata_class([self.text_tag("album_number", u"3/4")]) + del(metadata.album_number) + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.album_total, 4) + self.assertEqual(metadata.frames, + [self.text_tag("album_number", u"0/4")]) + + metadata = self.metadata_class([self.text_tag( + "album_number", u"foo 3 bar / blah 4 baz")]) + del(metadata.album_number) + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.album_total, 4) + self.assertEqual(metadata.frames, + [self.text_tag("album_number", + u"foo 0 bar / blah 4 baz")]) + + #deleting album_total without album_number removes field + metadata = self.metadata_class([self.text_tag( + "album_number", u"0/1")]) + del(metadata.album_total) + self.assertEqual(metadata.album_total, None) + self.assertEqual(metadata.frames, []) + + metadata = self.metadata_class([self.text_tag( + "album_number", u"foo 0 bar / 1")]) + del(metadata.album_total) + self.assertEqual(metadata.album_total, None) + self.assertEqual(metadata.frames, []) + + metadata = self.metadata_class([self.text_tag( + "album_number", u"foo / 1")]) + del(metadata.album_total) + self.assertEqual(metadata.album_total, None) + self.assertEqual(metadata.frames, []) + + #deleting album_total with album_number removes slashed field + metadata = self.metadata_class([self.text_tag( + "album_number", u"3/4")]) + del(metadata.album_total) + self.assertEqual(metadata.album_number, 3) + self.assertEqual(metadata.album_total, None) + self.assertEqual(metadata.frames, + [self.text_tag("album_number", u"3")]) + + metadata = self.metadata_class([self.text_tag( + "album_number", u"3 / 4")]) + del(metadata.album_total) + self.assertEqual(metadata.album_number, 3) + self.assertEqual(metadata.album_total, None) + self.assertEqual(metadata.frames, + [self.text_tag("album_number", u"3")]) + + metadata = self.metadata_class([self.text_tag( + "album_number", u"foo 3 bar / baz 4 blah")]) + del(metadata.album_total) + self.assertEqual(metadata.album_number, 3) + self.assertEqual(metadata.album_total, None) + self.assertEqual(metadata.frames, + [self.text_tag("album_number", u"foo 3 bar")]) + + @METADATA_ID3V2 def test_clean(self): #check trailing whitespace metadata = audiotools.ID3v22Comment( - [audiotools.ID3v22_T__Frame.converted("TT2", u"Title ")]) + [audiotools.id3.ID3v22_T__Frame.converted("TT2", u"Title ")]) self.assertEqual(metadata.track_name, u"Title ") fixes = [] cleaned = metadata.clean(fixes) @@ -1360,7 +2663,7 @@ #check leading whitespace metadata = audiotools.ID3v22Comment( - [audiotools.ID3v22_T__Frame.converted("TT2", u" Title")]) + [audiotools.id3.ID3v22_T__Frame.converted("TT2", u" Title")]) self.assertEqual(metadata.track_name, u" Title") fixes = [] cleaned = metadata.clean(fixes) @@ -1371,7 +2674,7 @@ #check empty fields metadata = audiotools.ID3v22Comment( - [audiotools.ID3v22_T__Frame.converted("TT2", u"")]) + [audiotools.id3.ID3v22_T__Frame.converted("TT2", u"")]) self.assertEqual(metadata["TT2"][0].data, "") fixes = [] cleaned = metadata.clean(fixes) @@ -1393,9 +2696,9 @@ True) metadata = audiotools.ID3v22Comment( - [audiotools.ID3v22_T__Frame.converted("TRK", u"1")]) + [audiotools.id3.ID3v22_T__Frame.converted("TRK", u"1")]) self.assertEqual(metadata.track_number, 1) - self.assertEqual(metadata.track_total, 0) + self.assertEqual(metadata.track_total, None) self.assertEqual(metadata["TRK"][0].data, "1") fixes = [] cleaned = metadata.clean(fixes) @@ -1403,11 +2706,11 @@ ["added leading zeroes to %(field)s" % {"field":u"TRK"}]) self.assertEqual(cleaned.track_number, 1) - self.assertEqual(cleaned.track_total, 0) + self.assertEqual(cleaned.track_total, None) self.assertEqual(cleaned["TRK"][0].data, "01") metadata = audiotools.ID3v22Comment( - [audiotools.ID3v22_T__Frame.converted("TRK", u"1/2")]) + [audiotools.id3.ID3v22_T__Frame.converted("TRK", u"1/2")]) self.assertEqual(metadata.track_number, 1) self.assertEqual(metadata.track_total, 2) self.assertEqual(metadata["TRK"][0].data, "1/2") @@ -1426,9 +2729,9 @@ False) metadata = audiotools.ID3v22Comment( - [audiotools.ID3v22_T__Frame.converted("TRK", u"01")]) + [audiotools.id3.ID3v22_T__Frame.converted("TRK", u"01")]) self.assertEqual(metadata.track_number, 1) - self.assertEqual(metadata.track_total, 0) + self.assertEqual(metadata.track_total, None) self.assertEqual(metadata["TRK"][0].data, "01") fixes = [] cleaned = metadata.clean(fixes) @@ -1436,11 +2739,11 @@ ["removed leading zeroes from %(field)s" % {"field":u"TRK"}]) self.assertEqual(cleaned.track_number, 1) - self.assertEqual(cleaned.track_total, 0) + self.assertEqual(cleaned.track_total, None) self.assertEqual(cleaned["TRK"][0].data, "1") metadata = audiotools.ID3v22Comment( - [audiotools.ID3v22_T__Frame.converted("TRK", u"01/2")]) + [audiotools.id3.ID3v22_T__Frame.converted("TRK", u"01/2")]) self.assertEqual(metadata.track_number, 1) self.assertEqual(metadata.track_total, 2) self.assertEqual(metadata["TRK"][0].data, "01/2") @@ -1454,7 +2757,7 @@ self.assertEqual(cleaned["TRK"][0].data, "1/2") metadata = audiotools.ID3v22Comment( - [audiotools.ID3v22_T__Frame.converted("TRK", u"1/02")]) + [audiotools.id3.ID3v22_T__Frame.converted("TRK", u"1/02")]) self.assertEqual(metadata.track_number, 1) self.assertEqual(metadata.track_total, 2) self.assertEqual(metadata["TRK"][0].data, "1/02") @@ -1468,7 +2771,7 @@ self.assertEqual(cleaned["TRK"][0].data, "1/2") metadata = audiotools.ID3v22Comment( - [audiotools.ID3v22_T__Frame.converted("TRK", u"01/02")]) + [audiotools.id3.ID3v22_T__Frame.converted("TRK", u"01/02")]) self.assertEqual(metadata.track_number, 1) self.assertEqual(metadata.track_total, 2) self.assertEqual(metadata["TRK"][0].data, "01/02") @@ -1507,14 +2810,19 @@ self.supported_formats = [audiotools.MP3Audio, audiotools.MP2Audio] + def unknown_tag(self, binary_string): + from audiotools.id3 import ID3v23_Frame + + return ID3v23_Frame("XXXX", binary_string) + @METADATA_ID3V2 def test_foreign_field(self): metadata = self.metadata_class( - [audiotools.ID3v23_T___Frame("TIT2", 0, "Track Name"), - audiotools.ID3v23_T___Frame("TALB", 0, "Album Name"), - audiotools.ID3v23_T___Frame("TRCK", 0, "1/3"), - audiotools.ID3v23_T___Frame("TPOS", 0, "2/4"), - audiotools.ID3v23_T___Frame("TFOO", 0, "Bar")]) + [audiotools.id3.ID3v23_T___Frame("TIT2", 0, "Track Name"), + audiotools.id3.ID3v23_T___Frame("TALB", 0, "Album Name"), + audiotools.id3.ID3v23_T___Frame("TRCK", 0, "1/3"), + audiotools.id3.ID3v23_T___Frame("TPOS", 0, "2/4"), + audiotools.id3.ID3v23_T___Frame("TFOO", 0, "Bar")]) for format in self.supported_formats: temp_file = tempfile.NamedTemporaryFile( suffix="." + format.SUFFIX) @@ -1534,36 +2842,42 @@ @METADATA_ID3V2 def test_clean(self): + from audiotools.text import (CLEAN_REMOVE_LEADING_WHITESPACE, + CLEAN_REMOVE_TRAILING_WHITESPACE, + CLEAN_REMOVE_EMPTY_TAG, + CLEAN_REMOVE_LEADING_ZEROES, + CLEAN_ADD_LEADING_ZEROES) + #check trailing whitespace metadata = audiotools.ID3v23Comment( - [audiotools.ID3v23_T___Frame.converted("TIT2", u"Title ")]) + [audiotools.id3.ID3v23_T___Frame.converted("TIT2", u"Title ")]) self.assertEqual(metadata.track_name, u"Title ") fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed trailing whitespace from %(field)s") % + [CLEAN_REMOVE_TRAILING_WHITESPACE % {"field":u"TIT2"}]) self.assertEqual(cleaned.track_name, u"Title") #check leading whitespace metadata = audiotools.ID3v23Comment( - [audiotools.ID3v23_T___Frame.converted("TIT2", u" Title")]) + [audiotools.id3.ID3v23_T___Frame.converted("TIT2", u" Title")]) self.assertEqual(metadata.track_name, u" Title") fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed leading whitespace from %(field)s") % + [CLEAN_REMOVE_LEADING_WHITESPACE % {"field":u"TIT2"}]) self.assertEqual(cleaned.track_name, u"Title") #check empty fields metadata = audiotools.ID3v23Comment( - [audiotools.ID3v23_T___Frame.converted("TIT2", u"")]) + [audiotools.id3.ID3v23_T___Frame.converted("TIT2", u"")]) self.assertEqual(metadata["TIT2"][0].data, "") fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed empty field %(field)s") % + [CLEAN_REMOVE_EMPTY_TAG % {"field":u"TIT2"}]) self.assertRaises(KeyError, cleaned.__getitem__, @@ -1580,28 +2894,28 @@ True) metadata = audiotools.ID3v23Comment( - [audiotools.ID3v23_T___Frame.converted("TRCK", u"1")]) + [audiotools.id3.ID3v23_T___Frame.converted("TRCK", u"1")]) self.assertEqual(metadata.track_number, 1) - self.assertEqual(metadata.track_total, 0) + self.assertEqual(metadata.track_total, None) self.assertEqual(metadata["TRCK"][0].data, "1") fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"added leading zeroes to %(field)s") % + [CLEAN_ADD_LEADING_ZEROES % {"field":u"TRCK"}]) self.assertEqual(cleaned.track_number, 1) - self.assertEqual(cleaned.track_total, 0) + self.assertEqual(cleaned.track_total, None) self.assertEqual(cleaned["TRCK"][0].data, "01") metadata = audiotools.ID3v23Comment( - [audiotools.ID3v23_T___Frame.converted("TRCK", u"1/2")]) + [audiotools.id3.ID3v23_T___Frame.converted("TRCK", u"1/2")]) self.assertEqual(metadata.track_number, 1) self.assertEqual(metadata.track_total, 2) self.assertEqual(metadata["TRCK"][0].data, "1/2") fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"added leading zeroes to %(field)s") % + [CLEAN_ADD_LEADING_ZEROES % {"field":u"TRCK"}]) self.assertEqual(cleaned.track_number, 1) self.assertEqual(cleaned.track_total, 2) @@ -1613,56 +2927,56 @@ False) metadata = audiotools.ID3v23Comment( - [audiotools.ID3v23_T___Frame.converted("TRCK", u"01")]) + [audiotools.id3.ID3v23_T___Frame.converted("TRCK", u"01")]) self.assertEqual(metadata.track_number, 1) - self.assertEqual(metadata.track_total, 0) + self.assertEqual(metadata.track_total, None) self.assertEqual(metadata["TRCK"][0].data, "01") fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed leading zeroes from %(field)s") % + [CLEAN_REMOVE_LEADING_ZEROES % {"field":u"TRCK"}]) self.assertEqual(cleaned.track_number, 1) - self.assertEqual(cleaned.track_total, 0) + self.assertEqual(cleaned.track_total, None) self.assertEqual(cleaned["TRCK"][0].data, "1") metadata = audiotools.ID3v23Comment( - [audiotools.ID3v23_T___Frame.converted("TRCK", u"01/2")]) + [audiotools.id3.ID3v23_T___Frame.converted("TRCK", u"01/2")]) self.assertEqual(metadata.track_number, 1) self.assertEqual(metadata.track_total, 2) self.assertEqual(metadata["TRCK"][0].data, "01/2") fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed leading zeroes from %(field)s") % + [CLEAN_REMOVE_LEADING_ZEROES % {"field":u"TRCK"}]) self.assertEqual(cleaned.track_number, 1) self.assertEqual(cleaned.track_total, 2) self.assertEqual(cleaned["TRCK"][0].data, "1/2") metadata = audiotools.ID3v23Comment( - [audiotools.ID3v23_T___Frame.converted("TRCK", u"1/02")]) + [audiotools.id3.ID3v23_T___Frame.converted("TRCK", u"1/02")]) self.assertEqual(metadata.track_number, 1) self.assertEqual(metadata.track_total, 2) self.assertEqual(metadata["TRCK"][0].data, "1/02") fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed leading zeroes from %(field)s") % + [CLEAN_REMOVE_LEADING_ZEROES % {"field":u"TRCK"}]) self.assertEqual(cleaned.track_number, 1) self.assertEqual(cleaned.track_total, 2) self.assertEqual(cleaned["TRCK"][0].data, "1/2") metadata = audiotools.ID3v23Comment( - [audiotools.ID3v23_T___Frame.converted("TRCK", u"01/02")]) + [audiotools.id3.ID3v23_T___Frame.converted("TRCK", u"01/02")]) self.assertEqual(metadata.track_number, 1) self.assertEqual(metadata.track_total, 2) self.assertEqual(metadata["TRCK"][0].data, "01/02") fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed leading zeroes from %(field)s") % + [CLEAN_REMOVE_LEADING_ZEROES % {"field":u"TRCK"}]) self.assertEqual(cleaned.track_number, 1) self.assertEqual(cleaned.track_total, 2) @@ -1673,7 +2987,7 @@ class ID3v24MetaData(ID3v22MetaData): def setUp(self): - self.metadata_class = audiotools.ID3v23Comment + self.metadata_class = audiotools.ID3v24Comment self.supported_fields = ["track_name", "track_number", "track_total", @@ -1694,41 +3008,52 @@ self.supported_formats = [audiotools.MP3Audio, audiotools.MP2Audio] + def unknown_tag(self, binary_string): + from audiotools.id3 import ID3v24_Frame + + return ID3v24_Frame("XXXX", binary_string) + def empty_metadata(self): return self.metadata_class([]) @METADATA_ID3V2 def test_clean(self): - #check trailing whitespace + from audiotools.text import (CLEAN_REMOVE_TRAILING_WHITESPACE, + CLEAN_REMOVE_LEADING_WHITESPACE, + CLEAN_REMOVE_EMPTY_TAG, + CLEAN_ADD_LEADING_ZEROES, + CLEAN_REMOVE_LEADING_ZEROES) + + #check trailing whitespace metadata = audiotools.ID3v24Comment( - [audiotools.ID3v24_T___Frame.converted("TIT2", u"Title ")]) + [audiotools.id3.ID3v24_T___Frame.converted("TIT2", u"Title ")]) self.assertEqual(metadata.track_name, u"Title ") fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed trailing whitespace from %(field)s") % + [CLEAN_REMOVE_TRAILING_WHITESPACE % {"field":u"TIT2"}]) self.assertEqual(cleaned.track_name, u"Title") #check leading whitespace metadata = audiotools.ID3v24Comment( - [audiotools.ID3v24_T___Frame.converted("TIT2", u" Title")]) + [audiotools.id3.ID3v24_T___Frame.converted("TIT2", u" Title")]) self.assertEqual(metadata.track_name, u" Title") fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed leading whitespace from %(field)s") % + [CLEAN_REMOVE_LEADING_WHITESPACE % {"field":u"TIT2"}]) self.assertEqual(cleaned.track_name, u"Title") #check empty fields metadata = audiotools.ID3v24Comment( - [audiotools.ID3v24_T___Frame.converted("TIT2", u"")]) + [audiotools.id3.ID3v24_T___Frame.converted("TIT2", u"")]) self.assertEqual(metadata["TIT2"][0].data, "") fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed empty field %(field)s") % + [CLEAN_REMOVE_EMPTY_TAG % {"field":u"TIT2"}]) self.assertRaises(KeyError, cleaned.__getitem__, @@ -1745,28 +3070,28 @@ True) metadata = audiotools.ID3v24Comment( - [audiotools.ID3v24_T___Frame.converted("TRCK", u"1")]) + [audiotools.id3.ID3v24_T___Frame.converted("TRCK", u"1")]) self.assertEqual(metadata.track_number, 1) - self.assertEqual(metadata.track_total, 0) + self.assertEqual(metadata.track_total, None) self.assertEqual(metadata["TRCK"][0].data, "1") fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"added leading zeroes to %(field)s") % + [CLEAN_ADD_LEADING_ZEROES % {"field":u"TRCK"}]) self.assertEqual(cleaned.track_number, 1) - self.assertEqual(cleaned.track_total, 0) + self.assertEqual(cleaned.track_total, None) self.assertEqual(cleaned["TRCK"][0].data, "01") metadata = audiotools.ID3v24Comment( - [audiotools.ID3v24_T___Frame.converted("TRCK", u"1/2")]) + [audiotools.id3.ID3v24_T___Frame.converted("TRCK", u"1/2")]) self.assertEqual(metadata.track_number, 1) self.assertEqual(metadata.track_total, 2) self.assertEqual(metadata["TRCK"][0].data, "1/2") fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"added leading zeroes to %(field)s") % + [CLEAN_ADD_LEADING_ZEROES % {"field":u"TRCK"}]) self.assertEqual(cleaned.track_number, 1) self.assertEqual(cleaned.track_total, 2) @@ -1778,56 +3103,56 @@ False) metadata = audiotools.ID3v24Comment( - [audiotools.ID3v24_T___Frame.converted("TRCK", u"01")]) + [audiotools.id3.ID3v24_T___Frame.converted("TRCK", u"01")]) self.assertEqual(metadata.track_number, 1) - self.assertEqual(metadata.track_total, 0) + self.assertEqual(metadata.track_total, None) self.assertEqual(metadata["TRCK"][0].data, "01") fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed leading zeroes from %(field)s") % + [CLEAN_REMOVE_LEADING_ZEROES % {"field":u"TRCK"}]) self.assertEqual(cleaned.track_number, 1) - self.assertEqual(cleaned.track_total, 0) + self.assertEqual(cleaned.track_total, None) self.assertEqual(cleaned["TRCK"][0].data, "1") metadata = audiotools.ID3v24Comment( - [audiotools.ID3v24_T___Frame.converted("TRCK", u"01/2")]) + [audiotools.id3.ID3v24_T___Frame.converted("TRCK", u"01/2")]) self.assertEqual(metadata.track_number, 1) self.assertEqual(metadata.track_total, 2) self.assertEqual(metadata["TRCK"][0].data, "01/2") fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed leading zeroes from %(field)s") % + [CLEAN_REMOVE_LEADING_ZEROES % {"field":u"TRCK"}]) self.assertEqual(cleaned.track_number, 1) self.assertEqual(cleaned.track_total, 2) self.assertEqual(cleaned["TRCK"][0].data, "1/2") metadata = audiotools.ID3v24Comment( - [audiotools.ID3v24_T___Frame.converted("TRCK", u"1/02")]) + [audiotools.id3.ID3v24_T___Frame.converted("TRCK", u"1/02")]) self.assertEqual(metadata.track_number, 1) self.assertEqual(metadata.track_total, 2) self.assertEqual(metadata["TRCK"][0].data, "1/02") fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed leading zeroes from %(field)s") % + [CLEAN_REMOVE_LEADING_ZEROES % {"field":u"TRCK"}]) self.assertEqual(cleaned.track_number, 1) self.assertEqual(cleaned.track_total, 2) self.assertEqual(cleaned["TRCK"][0].data, "1/2") metadata = audiotools.ID3v24Comment( - [audiotools.ID3v24_T___Frame.converted("TRCK", u"01/02")]) + [audiotools.id3.ID3v24_T___Frame.converted("TRCK", u"01/02")]) self.assertEqual(metadata.track_number, 1) self.assertEqual(metadata.track_total, 2) self.assertEqual(metadata["TRCK"][0].data, "01/02") fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_(u"removed leading zeroes from %(field)s") % + [CLEAN_REMOVE_LEADING_ZEROES % {"field":u"TRCK"}]) self.assertEqual(cleaned.track_number, 1) self.assertEqual(cleaned.track_total, 2) @@ -1933,8 +3258,8 @@ #streaminfo not updated with set_metadata() #but can be updated with update_metadata() old_streaminfo = metadata.get_block( - audiotools.Flac_STREAMINFO.BLOCK_ID) - new_streaminfo = audiotools.Flac_STREAMINFO( + audiotools.flac.Flac_STREAMINFO.BLOCK_ID) + new_streaminfo = audiotools.flac.Flac_STREAMINFO( minimum_block_size=old_streaminfo.minimum_block_size, maximum_block_size=old_streaminfo.maximum_block_size, minimum_frame_size=0, @@ -1945,41 +3270,41 @@ total_samples=old_streaminfo.total_samples, md5sum=old_streaminfo.md5sum) metadata.replace_blocks( - audiotools.Flac_STREAMINFO.BLOCK_ID, [new_streaminfo]) + audiotools.flac.Flac_STREAMINFO.BLOCK_ID, [new_streaminfo]) track.set_metadata(metadata) self.assertEqual(track.get_metadata().get_block( - audiotools.Flac_STREAMINFO.BLOCK_ID), + audiotools.flac.Flac_STREAMINFO.BLOCK_ID), old_streaminfo) track.update_metadata(metadata) self.assertEqual(track.get_metadata().get_block( - audiotools.Flac_STREAMINFO.BLOCK_ID), + audiotools.flac.Flac_STREAMINFO.BLOCK_ID), new_streaminfo) #vendor_string not updated with set_metadata() #but can be updated with update_metadata() old_vorbiscomment = metadata.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID) - new_vorbiscomment = audiotools.Flac_VORBISCOMMENT( + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID) + new_vorbiscomment = audiotools.flac.Flac_VORBISCOMMENT( comment_strings=old_vorbiscomment.comment_strings[:], vendor_string=u"Vendor String") metadata.replace_blocks( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID, [new_vorbiscomment]) + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID, [new_vorbiscomment]) track.set_metadata(metadata) self.assertEqual(track.get_metadata().get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID ).vendor_string, old_vorbiscomment.vendor_string) track.update_metadata(metadata) self.assertEqual(track.get_metadata().get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID ).vendor_string, new_vorbiscomment.vendor_string) #REPLAYGAIN_* tags not updated with set_metadata() #but can be updated with update_metadata() old_vorbiscomment = metadata.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID) - new_vorbiscomment = audiotools.Flac_VORBISCOMMENT( + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID) + new_vorbiscomment = audiotools.flac.Flac_VORBISCOMMENT( comment_strings=old_vorbiscomment.comment_strings + [u"REPLAYGAIN_REFERENCE_LOUDNESS=89.0 dB"], vendor_string=old_vorbiscomment.vendor_string) @@ -1987,18 +3312,18 @@ new_vorbiscomment[u"REPLAYGAIN_REFERENCE_LOUDNESS"], [u"89.0 dB"]) metadata.replace_blocks( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID, [new_vorbiscomment]) + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID, [new_vorbiscomment]) track.set_metadata(metadata) self.assertRaises( KeyError, track.get_metadata().get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID ).__getitem__, u"REPLAYGAIN_REFERENCE_LOUDNESS") track.update_metadata(metadata) self.assertEqual( track.get_metadata().get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID )[u"REPLAYGAIN_REFERENCE_LOUDNESS"], [u"89.0 dB"]) @@ -2006,8 +3331,8 @@ #not updated with set_metadata() #but can be updated with update_metadata() old_vorbiscomment = metadata.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID) - new_vorbiscomment = audiotools.Flac_VORBISCOMMENT( + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID) + new_vorbiscomment = audiotools.flac.Flac_VORBISCOMMENT( comment_strings=old_vorbiscomment.comment_strings + [u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK=0x0003"], vendor_string=old_vorbiscomment.vendor_string) @@ -2015,48 +3340,48 @@ new_vorbiscomment[u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"], [u"0x0003"]) metadata.replace_blocks( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID, [new_vorbiscomment]) + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID, [new_vorbiscomment]) track.set_metadata(metadata) self.assertRaises( KeyError, track.get_metadata().get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID ).__getitem__, u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK") track.update_metadata(metadata) self.assertEqual( track.get_metadata().get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID )[u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"], [u"0x0003"]) #cuesheet not updated with set_metadata() #but can be updated with update_metadata() - new_cuesheet = audiotools.Flac_CUESHEET( + new_cuesheet = audiotools.flac.Flac_CUESHEET( catalog_number=chr(0) * 128, lead_in_samples=0, is_cdda=1, - tracks=[audiotools.Flac_CUESHEET_track( + tracks=[audiotools.flac.Flac_CUESHEET_track( offset=0, number=0, ISRC=" " * 12, track_type=0, pre_emphasis=0, - index_points=[audiotools.Flac_CUESHEET_index(0, + index_points=[audiotools.flac.Flac_CUESHEET_index(0, 0)])]) metadata = track.get_metadata() self.assertRaises(IndexError, metadata.get_block, - audiotools.Flac_CUESHEET.BLOCK_ID) + audiotools.flac.Flac_CUESHEET.BLOCK_ID) metadata.add_block(new_cuesheet) track.set_metadata(metadata) self.assertRaises(IndexError, track.get_metadata().get_block, - audiotools.Flac_CUESHEET.BLOCK_ID) + audiotools.flac.Flac_CUESHEET.BLOCK_ID) track.update_metadata(metadata) self.assertEqual( track.get_metadata().get_block( - audiotools.Flac_CUESHEET.BLOCK_ID), + audiotools.flac.Flac_CUESHEET.BLOCK_ID), new_cuesheet) if (audio_class is not audiotools.OggFlacAudio): @@ -2068,42 +3393,42 @@ metadata = track.get_metadata() old_seektable = metadata.get_block( - audiotools.Flac_SEEKTABLE.BLOCK_ID) + audiotools.flac.Flac_SEEKTABLE.BLOCK_ID) - new_seektable = audiotools.Flac_SEEKTABLE( + new_seektable = audiotools.flac.Flac_SEEKTABLE( seekpoints=[(1, 1, 4096)] + old_seektable.seekpoints[1:]) metadata.replace_blocks( - audiotools.Flac_SEEKTABLE.BLOCK_ID, + audiotools.flac.Flac_SEEKTABLE.BLOCK_ID, [new_seektable]) track.set_metadata(metadata) self.assertEqual( track.get_metadata().get_block( - audiotools.Flac_SEEKTABLE.BLOCK_ID), + audiotools.flac.Flac_SEEKTABLE.BLOCK_ID), old_seektable) track.update_metadata(metadata) self.assertEqual( track.get_metadata().get_block( - audiotools.Flac_SEEKTABLE.BLOCK_ID), + audiotools.flac.Flac_SEEKTABLE.BLOCK_ID), new_seektable) #application blocks not updated with set_metadata() #but can be updated with update_metadata() - application = audiotools.Flac_APPLICATION( + application = audiotools.flac.Flac_APPLICATION( application_id="fooz", data="kelp") metadata = track.get_metadata() self.assertRaises(IndexError, metadata.get_block, - audiotools.Flac_APPLICATION.BLOCK_ID) + audiotools.flac.Flac_APPLICATION.BLOCK_ID) metadata.add_block(application) track.set_metadata(metadata) self.assertRaises(IndexError, track.get_metadata().get_block, - audiotools.Flac_APPLICATION.BLOCK_ID) + audiotools.flac.Flac_APPLICATION.BLOCK_ID) track.update_metadata(metadata) self.assertEqual(track.get_metadata().get_block( - audiotools.Flac_APPLICATION.BLOCK_ID), + audiotools.flac.Flac_APPLICATION.BLOCK_ID), application) finally: temp_file.close() @@ -2111,7 +3436,7 @@ @METADATA_FLAC def test_foreign_field(self): metadata = audiotools.FlacMetaData([ - audiotools.Flac_VORBISCOMMENT( + audiotools.flac.Flac_VORBISCOMMENT( [u"TITLE=Track Name", u"ALBUM=Album Name", u"TRACKNUMBER=1", @@ -2130,7 +3455,7 @@ self.assertEqual(metadata, metadata2) self.assert_(isinstance(metadata, audiotools.FlacMetaData)) self.assertEqual(track.get_metadata().get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID)["FOO"], + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)["FOO"], [u"Bar"]) finally: temp_file.close() @@ -2168,14 +3493,14 @@ self.assertEqual(getattr(metadata, field), value) self.assertEqual( metadata.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID)[key][0], + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[key][0], unicode(value)) track.set_metadata(metadata) metadata2 = track.get_metadata() self.assertEqual(getattr(metadata2, field), value) self.assertEqual( metadata2.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID)[key][0], + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[key][0], unicode(value)) #ensure that updating the low-level implementation @@ -2184,19 +3509,19 @@ track.delete_metadata() metadata = self.empty_metadata() metadata.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID)[key] = \ + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[key] = \ [unicode(value)] self.assertEqual(getattr(metadata, field), value) self.assertEqual( metadata.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID)[key][0], + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[key][0], unicode(value)) track.set_metadata(metadata) metadata2 = track.get_metadata() self.assertEqual(getattr(metadata2, field), value) self.assertEqual( metadata2.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID)[key][0], + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[key][0], unicode(value)) finally: # temp_file.close() @@ -2208,17 +3533,17 @@ metadata_orig = self.empty_metadata() metadata_orig.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID)[u'FOO'] = [u'bar'] + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[u'FOO'] = [u'bar'] self.assertEqual(metadata_orig.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID)[u'FOO'], [u'bar']) + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[u'FOO'], [u'bar']) metadata_new = self.metadata_class.converted(metadata_orig) self.assertEqual(metadata_orig.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID)[u'FOO'], + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[u'FOO'], metadata_new.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID)[u'FOO']) + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[u'FOO']) #ensure that convert() builds a whole new object metadata_new.track_name = u"Foo" @@ -2261,86 +3586,100 @@ def test_totals(self): metadata = self.empty_metadata() metadata.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID)["TRACKNUMBER"] = [u"2/4"] + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)["TRACKNUMBER"] = [u"2/4"] self.assertEqual(metadata.track_number, 2) self.assertEqual(metadata.track_total, 4) metadata = self.empty_metadata() metadata.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID)["DISCNUMBER"] = [u"1/3"] + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)["DISCNUMBER"] = [u"1/3"] self.assertEqual(metadata.album_number, 1) self.assertEqual(metadata.album_total, 3) @METADATA_FLAC def test_clean(self): + from audiotools.text import (CLEAN_REMOVE_TRAILING_WHITESPACE, + CLEAN_REMOVE_LEADING_WHITESPACE, + CLEAN_REMOVE_LEADING_ZEROES, + CLEAN_REMOVE_EMPTY_TAG, + CLEAN_FIX_IMAGE_FIELDS, + CLEAN_FLAC_REMOVE_SEEKPOINTS, + CLEAN_FLAC_REORDER_SEEKPOINTS) + #check no blocks + metadata = audiotools.FlacMetaData([]) + results = [] + cleaned = metadata.clean(results) + self.assertEqual(metadata, cleaned) + self.assertEqual(results, []) + #check trailing whitespace metadata = audiotools.FlacMetaData([ - audiotools.Flac_VORBISCOMMENT( + audiotools.flac.Flac_VORBISCOMMENT( [u"TITLE=Foo "], u"")]) self.assertEqual(metadata.track_name, u'Foo ') results = [] cleaned = metadata.clean(results) self.assertEqual(cleaned.track_name, u'Foo') self.assertEqual(results, - [_(u"removed trailing whitespace from %(field)s") % + [CLEAN_REMOVE_TRAILING_WHITESPACE % {"field":u"TITLE"}]) #check leading whitespace metadata = audiotools.FlacMetaData([ - audiotools.Flac_VORBISCOMMENT( + audiotools.flac.Flac_VORBISCOMMENT( [u"TITLE= Foo"], u"")]) self.assertEqual(metadata.track_name, u' Foo') results = [] cleaned = metadata.clean(results) self.assertEqual(cleaned.track_name, u'Foo') self.assertEqual(results, - [_(u"removed leading whitespace from %(field)s") % + [CLEAN_REMOVE_LEADING_WHITESPACE % {"field":u"TITLE"}]) #check leading zeroes metadata = audiotools.FlacMetaData([ - audiotools.Flac_VORBISCOMMENT( + audiotools.flac.Flac_VORBISCOMMENT( [u"TRACKNUMBER=01"], u"")]) self.assertEqual( metadata.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID)["TRACKNUMBER"], + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)["TRACKNUMBER"], [u"01"]) results = [] cleaned = metadata.clean(results) self.assertEqual( cleaned.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID)["TRACKNUMBER"], + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)["TRACKNUMBER"], [u"1"]) self.assertEqual(results, - [_(u"removed leading zeroes from %(field)s") % + [CLEAN_REMOVE_LEADING_ZEROES % {"field": u"TRACKNUMBER"}]) #check empty fields metadata = audiotools.FlacMetaData([ - audiotools.Flac_VORBISCOMMENT( + audiotools.flac.Flac_VORBISCOMMENT( ["TITLE= "], u"")]) self.assertEqual( metadata.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID)["TITLE"], [u' ']) + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)["TITLE"], [u' ']) results = [] cleaned = metadata.clean(results) self.assertEqual(cleaned, audiotools.FlacMetaData([ - audiotools.Flac_VORBISCOMMENT([], u"")])) + audiotools.flac.Flac_VORBISCOMMENT([], u"")])) self.assertEqual(results, - [_(u"removed empty field %(field)s") % + [CLEAN_REMOVE_EMPTY_TAG % {"field": u"TITLE"}]) #check mis-tagged images metadata = audiotools.FlacMetaData( - [audiotools.Flac_PICTURE( + [audiotools.flac.Flac_PICTURE( 0, "image/jpeg", u"", 20, 20, 24, 10, """iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKAQMAAAC3/F3+AAAAAXNSR0IArs4c6QAAAANQTFRF//// p8QbyAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sEBBMWM3PnvjMAAAALSURBVAjXY2DA BwAAHgABboVHMgAAAABJRU5ErkJggg==""".decode('base64'))]) self.assertEqual( - len(metadata.get_blocks(audiotools.Flac_PICTURE.BLOCK_ID)), 1) + len(metadata.get_blocks(audiotools.flac.Flac_PICTURE.BLOCK_ID)), 1) image = metadata.images()[0] self.assertEqual(image.mime_type, "image/jpeg") self.assertEqual(image.width, 20) @@ -2351,9 +3690,9 @@ results = [] cleaned = metadata.clean(results) self.assertEqual(results, - [_(u"fixed embedded image metadata fields")]) + [CLEAN_FIX_IMAGE_FIELDS]) self.assertEqual( - len(cleaned.get_blocks(audiotools.Flac_PICTURE.BLOCK_ID)), 1) + len(cleaned.get_blocks(audiotools.flac.Flac_PICTURE.BLOCK_ID)), 1) image = cleaned.images()[0] self.assertEqual(image.mime_type, "image/png") self.assertEqual(image.width, 10) @@ -2363,7 +3702,7 @@ #check seektable with empty seekpoints metadata = audiotools.FlacMetaData( - [audiotools.Flac_SEEKTABLE([(0, 10, 10), + [audiotools.flac.Flac_SEEKTABLE([(0, 10, 10), (10, 20, 0), (10, 20, 0), (10, 20, 0), @@ -2371,15 +3710,15 @@ results = [] cleaned = metadata.clean(results) self.assertEqual(results, - [_(u"removed empty seekpoints from seektable")]) + [CLEAN_FLAC_REMOVE_SEEKPOINTS]) self.assertEqual( - cleaned.get_block(audiotools.Flac_SEEKTABLE.BLOCK_ID), - audiotools.Flac_SEEKTABLE([(0, 10, 10), + cleaned.get_block(audiotools.flac.Flac_SEEKTABLE.BLOCK_ID), + audiotools.flac.Flac_SEEKTABLE([(0, 10, 10), (10, 20, 20)])) #check seektable with duplicate seekpoints metadata = audiotools.FlacMetaData( - [audiotools.Flac_SEEKTABLE([(0, 0, 10), + [audiotools.flac.Flac_SEEKTABLE([(0, 0, 10), (2, 20, 10), (2, 20, 10), (2, 20, 10), @@ -2387,16 +3726,16 @@ results = [] cleaned = metadata.clean(results) self.assertEqual(results, - [_(u"reordered seektable to be in ascending order")]) + [CLEAN_FLAC_REORDER_SEEKPOINTS]) self.assertEqual( - cleaned.get_block(audiotools.Flac_SEEKTABLE.BLOCK_ID), - audiotools.Flac_SEEKTABLE([(0, 0, 10), + cleaned.get_block(audiotools.flac.Flac_SEEKTABLE.BLOCK_ID), + audiotools.flac.Flac_SEEKTABLE([(0, 0, 10), (2, 20, 10), (4, 40, 10)])) #check seektable with mis-ordered seekpoints metadata = audiotools.FlacMetaData( - [audiotools.Flac_SEEKTABLE([(0, 0, 10), + [audiotools.flac.Flac_SEEKTABLE([(0, 0, 10), (6, 60, 10), (4, 40, 10), (2, 20, 10), @@ -2404,10 +3743,10 @@ results = [] cleaned = metadata.clean(results) self.assertEqual(results, - [_(u"reordered seektable to be in ascending order")]) + [CLEAN_FLAC_REORDER_SEEKPOINTS]) self.assertEqual( - cleaned.get_block(audiotools.Flac_SEEKTABLE.BLOCK_ID), - audiotools.Flac_SEEKTABLE([(0, 0, 10), + cleaned.get_block(audiotools.flac.Flac_SEEKTABLE.BLOCK_ID), + audiotools.flac.Flac_SEEKTABLE([(0, 0, 10), (2, 20, 10), (4, 40, 10), (6, 60, 10), @@ -2416,7 +3755,7 @@ #check that cleanup doesn't disturb other metadata blocks #FIXME metadata = audiotools.FlacMetaData([ - audiotools.Flac_STREAMINFO( + audiotools.flac.Flac_STREAMINFO( minimum_block_size=4096, maximum_block_size=4096, minimum_frame_size=14, @@ -2427,10 +3766,10 @@ total_samples=149606016L, md5sum='\xae\x87\x1c\x8e\xe1\xfc\x16\xde' + '\x86\x81&\x8e\xc8\xd52\xff'), - audiotools.Flac_APPLICATION( + audiotools.flac.Flac_APPLICATION( application_id="FOOZ", data="KELP"), - audiotools.Flac_SEEKTABLE([ + audiotools.flac.Flac_SEEKTABLE([ (0L, 0L, 4096), (8335360L, 30397L, 4096), (8445952L, 30816L, 4096), @@ -2456,162 +3795,162 @@ (125526016L, 488160L, 4096), (138788864L, 539968L, 4096), (138903552L, 540416L, 4096)]), - audiotools.Flac_VORBISCOMMENT(["TITLE=Foo "], u""), - audiotools.Flac_CUESHEET( + audiotools.flac.Flac_VORBISCOMMENT(["TITLE=Foo "], u""), + audiotools.flac.Flac_CUESHEET( catalog_number='4560248013904\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', lead_in_samples=88200L, is_cdda=1, tracks=[ - audiotools.Flac_CUESHEET_track( + audiotools.flac.Flac_CUESHEET_track( offset=0L, number=1, ISRC='JPK631002201', track_type=0, pre_emphasis=0, index_points=[ - audiotools.Flac_CUESHEET_index(0L, 1)]), - audiotools.Flac_CUESHEET_track( + audiotools.flac.Flac_CUESHEET_index(0L, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=8336076L, number=2, ISRC='JPK631002202', track_type=0, pre_emphasis=0, index_points=[ - audiotools.Flac_CUESHEET_index(0L, 0), - audiotools.Flac_CUESHEET_index(113484L, 1)]), - audiotools.Flac_CUESHEET_track( + audiotools.flac.Flac_CUESHEET_index(0L, 0), + audiotools.flac.Flac_CUESHEET_index(113484L, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=17379516L, number=3, ISRC='JPK631002203', track_type=0, pre_emphasis=0, index_points=[ - audiotools.Flac_CUESHEET_index(0L, 0), - audiotools.Flac_CUESHEET_index(113484L, 1)]), - audiotools.Flac_CUESHEET_track( + audiotools.flac.Flac_CUESHEET_index(0L, 0), + audiotools.flac.Flac_CUESHEET_index(113484L, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=28042308L, number=4, ISRC='JPK631002204', track_type=0, pre_emphasis=0, index_points=[ - audiotools.Flac_CUESHEET_index(0L, 0), - audiotools.Flac_CUESHEET_index(113484L, 1)]), - audiotools.Flac_CUESHEET_track( + audiotools.flac.Flac_CUESHEET_index(0L, 0), + audiotools.flac.Flac_CUESHEET_index(113484L, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=41672736L, number=5, ISRC='JPK631002205', track_type=0, pre_emphasis=0, index_points=[ - audiotools.Flac_CUESHEET_index(0L, 0), - audiotools.Flac_CUESHEET_index(113484L, 1)]), - audiotools.Flac_CUESHEET_track( + audiotools.flac.Flac_CUESHEET_index(0L, 0), + audiotools.flac.Flac_CUESHEET_index(113484L, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=54447624L, number=6, ISRC='JPK631002206', track_type=0, pre_emphasis=0, index_points=[ - audiotools.Flac_CUESHEET_index(0L, 0), - audiotools.Flac_CUESHEET_index(113484L, 1)]), - audiotools.Flac_CUESHEET_track( + audiotools.flac.Flac_CUESHEET_index(0L, 0), + audiotools.flac.Flac_CUESHEET_index(113484L, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=65689596L, number=7, ISRC='JPK631002207', track_type=0, pre_emphasis=0, index_points=[ - audiotools.Flac_CUESHEET_index(0L, 0), - audiotools.Flac_CUESHEET_index(113484L, 1)]), - audiotools.Flac_CUESHEET_track( + audiotools.flac.Flac_CUESHEET_index(0L, 0), + audiotools.flac.Flac_CUESHEET_index(113484L, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=76267716L, number=8, ISRC='JPK631002208', track_type=0, pre_emphasis=0, index_points=[ - audiotools.Flac_CUESHEET_index(0L, 0), - audiotools.Flac_CUESHEET_index(113484L, 1)]), - audiotools.Flac_CUESHEET_track( + audiotools.flac.Flac_CUESHEET_index(0L, 0), + audiotools.flac.Flac_CUESHEET_index(113484L, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=89627076L, number=9, ISRC='JPK631002209', track_type=0, pre_emphasis=0, index_points=[ - audiotools.Flac_CUESHEET_index(0L, 0), - audiotools.Flac_CUESHEET_index(113484L, 1)]), - audiotools.Flac_CUESHEET_track( + audiotools.flac.Flac_CUESHEET_index(0L, 0), + audiotools.flac.Flac_CUESHEET_index(113484L, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=99691872L, number=10, ISRC='JPK631002210', track_type=0, pre_emphasis=0, index_points=[ - audiotools.Flac_CUESHEET_index(0L, 0), - audiotools.Flac_CUESHEET_index(113484L, 1)]), - audiotools.Flac_CUESHEET_track( + audiotools.flac.Flac_CUESHEET_index(0L, 0), + audiotools.flac.Flac_CUESHEET_index(113484L, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=114176076L, number=11, ISRC='JPK631002211', track_type=0, pre_emphasis=0, index_points=[ - audiotools.Flac_CUESHEET_index(0L, 0), - audiotools.Flac_CUESHEET_index(113484L, 1)]), - audiotools.Flac_CUESHEET_track( + audiotools.flac.Flac_CUESHEET_index(0L, 0), + audiotools.flac.Flac_CUESHEET_index(113484L, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=125415696L, number=12, ISRC='JPK631002212', track_type=0, pre_emphasis=0, index_points=[ - audiotools.Flac_CUESHEET_index(0L, 0), - audiotools.Flac_CUESHEET_index(114072L, 1)]), - audiotools.Flac_CUESHEET_track( + audiotools.flac.Flac_CUESHEET_index(0L, 0), + audiotools.flac.Flac_CUESHEET_index(114072L, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=138791520L, number=13, ISRC='JPK631002213', track_type=0, pre_emphasis=0, index_points=[ - audiotools.Flac_CUESHEET_index(0L, 0), - audiotools.Flac_CUESHEET_index(114072L, 1)]), - audiotools.Flac_CUESHEET_track( + audiotools.flac.Flac_CUESHEET_index(0L, 0), + audiotools.flac.Flac_CUESHEET_index(114072L, 1)]), + audiotools.flac.Flac_CUESHEET_track( offset=149606016L, number=170, ISRC='\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', track_type=0, pre_emphasis=0, index_points=[])]), - audiotools.Flac_PICTURE(0, "image/jpeg", u"", + audiotools.flac.Flac_PICTURE(0, "image/jpeg", u"", 500, 500, 24, 0, TEST_COVER1)]) self.assertEqual([b.BLOCK_ID for b in metadata.block_list], - [audiotools.Flac_STREAMINFO.BLOCK_ID, - audiotools.Flac_APPLICATION.BLOCK_ID, - audiotools.Flac_SEEKTABLE.BLOCK_ID, - audiotools.Flac_VORBISCOMMENT.BLOCK_ID, - audiotools.Flac_CUESHEET.BLOCK_ID, - audiotools.Flac_PICTURE.BLOCK_ID]) + [audiotools.flac.Flac_STREAMINFO.BLOCK_ID, + audiotools.flac.Flac_APPLICATION.BLOCK_ID, + audiotools.flac.Flac_SEEKTABLE.BLOCK_ID, + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID, + audiotools.flac.Flac_CUESHEET.BLOCK_ID, + audiotools.flac.Flac_PICTURE.BLOCK_ID]) results = [] cleaned = metadata.clean(results) self.assertEqual(results, - [_(u"removed trailing whitespace from %(field)s") % + [CLEAN_REMOVE_TRAILING_WHITESPACE % {"field":u"TITLE"}]) - for block_id in [audiotools.Flac_STREAMINFO.BLOCK_ID, - audiotools.Flac_APPLICATION.BLOCK_ID, - audiotools.Flac_SEEKTABLE.BLOCK_ID, - audiotools.Flac_CUESHEET.BLOCK_ID, - audiotools.Flac_PICTURE.BLOCK_ID]: + for block_id in [audiotools.flac.Flac_STREAMINFO.BLOCK_ID, + audiotools.flac.Flac_APPLICATION.BLOCK_ID, + audiotools.flac.Flac_SEEKTABLE.BLOCK_ID, + audiotools.flac.Flac_CUESHEET.BLOCK_ID, + audiotools.flac.Flac_PICTURE.BLOCK_ID]: self.assertEqual(metadata.get_blocks(block_id), cleaned.get_blocks(block_id)) self.assertNotEqual( - metadata.get_blocks(audiotools.Flac_VORBISCOMMENT.BLOCK_ID), - cleaned.get_blocks(audiotools.Flac_VORBISCOMMENT.BLOCK_ID)) + metadata.get_blocks(audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID), + cleaned.get_blocks(audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)) @METADATA_FLAC def test_replay_gain(self): @@ -2642,6 +3981,22 @@ temp2 = tempfile.NamedTemporaryFile( suffix="." + input_class.SUFFIX) try: + #ensure file with no metadata blocks + #has metadata set correctly + track2 = output_class.from_pcm( + temp2.name, + test_streams.Sine16_Stereo(66150, 44100, + 8820.0, 0.70, + 4410.0, 0.29, 1.0)) + metadata = track2.get_metadata() + for block_id in range(1, 7): + metadata.replace_blocks(block_id, []) + track2.update_metadata(metadata) + self.assert_(track2.replay_gain() is None) + + output_class.add_replay_gain([track2.filename]) + self.assert_(track2.replay_gain() is not None) + track2 = output_class.from_pcm( temp2.name, test_streams.Sine16_Stereo(66150, 44100, @@ -2677,6 +4032,717 @@ finally: temp1.close() + @METADATA_FLAC + def test_getattr(self): + #track_number grabs the first available integer, if any + self.assertEqual( + audiotools.FlacMetaData([]).track_number, None) + + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER=10"], + u"vendor")]).track_number, + 10) + + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER=10", + u"TRACKNUMBER=5"], + u"vendor")]).track_number, + 10) + + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER=foo 10 bar"], + u"vendor")]).track_number, + 10) + + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER=foo", + u"TRACKNUMBER=10"], + u"vendor")]).track_number, + 10) + + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER=foo", + u"TRACKNUMBER=foo 10 bar"], + u"vendor")]).track_number, + 10) + + #track_number is case-insensitive + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"tRaCkNuMbEr=10"], + u"vendor")]).track_number, + 10) + + #album_number grabs the first available integer, if any + self.assertEqual( + audiotools.FlacMetaData([]).album_number, None) + + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER=20"], + u"vendor")]).album_number, + 20) + + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER=20", + u"DISCNUMBER=5"], + u"vendor")]).album_number, + 20) + + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER=foo 20 bar"], + u"vendor")]).album_number, + 20) + + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER=foo", + u"DISCNUMBER=20"], + u"vendor")]).album_number, + 20) + + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER=foo", + u"DISCNUMBER=foo 20 bar"], + u"vendor")]).album_number, + 20) + + #album_number is case-insensitive + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"dIsCnUmBeR=20"], + u"vendor")]).album_number, + 20) + + #track_total grabs the first available TRACKTOTAL integer + #before falling back on slashed fields, if any + self.assertEqual( + audiotools.FlacMetaData([]).track_total, None) + + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKTOTAL=15"], + u"vendor")]).track_total, + 15) + + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER=5/10"], + u"vendor")]).track_total, + 10) + + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER=5/10", + u"TRACKTOTAL=15"], + u"vendor")]).track_total, + 15) + + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKTOTAL=15", + u"TRACKNUMBER=5/10"], + u"vendor")]).track_total, + 15) + + #track_total is case-insensitive + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"tracktotal=15"], + u"vendor")]).track_total, + 15) + + #track_total supports aliases + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TOTALTRACKS=15"], + u"vendor")]).track_total, + 15) + + #album_total grabs the first available DISCTOTAL integer + #before falling back on slashed fields, if any + self.assertEqual( + audiotools.FlacMetaData([]).album_total, None) + + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCTOTAL=25"], + u"vendor")]).album_total, + 25) + + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER=10/30"], + u"vendor")]).album_total, + 30) + + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER=10/30", + u"DISCTOTAL=25"], + u"vendor")]).album_total, + 25) + + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCTOTAL=25", + u"DISCNUMBER=10/30"], + u"vendor")]).album_total, + 25) + + #album_total is case-insensitive + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"disctotal=25"], + u"vendor")]).album_total, + 25) + + #album_total supports aliases + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TOTALDISCS=25"], + u"vendor")]).album_total, + 25) + + #other fields grab the first available item + self.assertEqual( + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TITLE=first", + u"TITLE=last"], + u"vendor")]).track_name, + u"first") + + @METADATA_FLAC + def test_setattr(self): + #track_number adds new field if necessary + for metadata in [ + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT([], u"vendor")]), + audiotools.FlacMetaData([])]: + + self.assertEqual(metadata.track_number, None) + metadata.track_number = 11 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TRACKNUMBER=11"]) + self.assertEqual(metadata.track_number, 11) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER=blah"], + u"vendor")]) + self.assertEqual(metadata.track_number, None) + metadata.track_number = 11 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TRACKNUMBER=blah", + u"TRACKNUMBER=11"]) + self.assertEqual(metadata.track_number, 11) + + #track_number updates the first integer field + #and leaves other junk in that field alone + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER=10/12"], u"vendor")]) + self.assertEqual(metadata.track_number, 10) + metadata.track_number = 11 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TRACKNUMBER=11/12"]) + self.assertEqual(metadata.track_number, 11) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER=foo 10 bar"], + u"vendor")]) + self.assertEqual(metadata.track_number, 10) + metadata.track_number = 11 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TRACKNUMBER=foo 11 bar"]) + self.assertEqual(metadata.track_number, 11) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER=foo 10 bar", + u"TRACKNUMBER=blah"], + u"vendor")]) + self.assertEqual(metadata.track_number, 10) + metadata.track_number = 11 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TRACKNUMBER=foo 11 bar", + u"TRACKNUMBER=blah"]) + self.assertEqual(metadata.track_number, 11) + + #album_number adds new field if necessary + for metadata in [ + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT([], u"vendor")]), + audiotools.FlacMetaData([])]: + + self.assertEqual(metadata.album_number, None) + metadata.album_number = 3 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"DISCNUMBER=3"]) + self.assertEqual(metadata.album_number, 3) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER=blah"], + u"vendor")]) + self.assertEqual(metadata.album_number, None) + metadata.album_number = 3 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"DISCNUMBER=blah", + u"DISCNUMBER=3"]) + self.assertEqual(metadata.album_number, 3) + + #album_number updates the first integer field + #and leaves other junk in that field alone + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER=2/4"], u"vendor")]) + self.assertEqual(metadata.album_number, 2) + metadata.album_number = 3 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"DISCNUMBER=3/4"]) + self.assertEqual(metadata.album_number, 3) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER=foo 2 bar"], + u"vendor")]) + self.assertEqual(metadata.album_number, 2) + metadata.album_number = 3 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"DISCNUMBER=foo 3 bar"]) + self.assertEqual(metadata.album_number, 3) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER=foo 2 bar", + u"DISCNUMBER=blah"], + u"vendor")]) + self.assertEqual(metadata.album_number, 2) + metadata.album_number = 3 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"DISCNUMBER=foo 3 bar", + u"DISCNUMBER=blah"]) + self.assertEqual(metadata.album_number, 3) + + #track_total adds new TRACKTOTAL field if necessary + for metadata in [ + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT([], u"vendor")]), + audiotools.FlacMetaData([])]: + + self.assertEqual(metadata.track_total, None) + metadata.track_total = 12 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TRACKTOTAL=12"]) + self.assertEqual(metadata.track_total, 12) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKTOTAL=blah"], + u"vendor")]) + self.assertEqual(metadata.track_total, None) + metadata.track_total = 12 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TRACKTOTAL=blah", + u"TRACKTOTAL=12"]) + self.assertEqual(metadata.track_total, 12) + + #track_total updates first integer TRACKTOTAL field first if possible + #including aliases + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKTOTAL=blah", + u"TRACKTOTAL=2"], u"vendor")]) + self.assertEqual(metadata.track_total, 2) + metadata.track_total = 3 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TRACKTOTAL=blah", + u"TRACKTOTAL=3"]) + self.assertEqual(metadata.track_total, 3) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TOTALTRACKS=blah", + u"TOTALTRACKS=2"], u"vendor")]) + self.assertEqual(metadata.track_total, 2) + metadata.track_total = 3 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TOTALTRACKS=blah", + u"TOTALTRACKS=3"]) + self.assertEqual(metadata.track_total, 3) + + #track_total updates slashed TRACKNUMBER field if necessary + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER=1/4", + u"TRACKTOTAL=2"], u"vendor")]) + self.assertEqual(metadata.track_total, 2) + metadata.track_total = 3 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TRACKNUMBER=1/4", + u"TRACKTOTAL=3"]) + self.assertEqual(metadata.track_total, 3) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER=1/4"], u"vendor")]) + self.assertEqual(metadata.track_total, 4) + metadata.track_total = 3 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TRACKNUMBER=1/3"]) + self.assertEqual(metadata.track_total, 3) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER= foo / 4 bar"], + u"vendor")]) + self.assertEqual(metadata.track_total, 4) + metadata.track_total = 3 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TRACKNUMBER= foo / 3 bar"]) + self.assertEqual(metadata.track_total, 3) + + #album_total adds new DISCTOTAL field if necessary + for metadata in [ + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT([], u"vendor")]), + audiotools.FlacMetaData([])]: + + self.assertEqual(metadata.album_total, None) + metadata.album_total = 4 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"DISCTOTAL=4"]) + self.assertEqual(metadata.album_total, 4) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCTOTAL=blah"], + u"vendor")]) + self.assertEqual(metadata.album_total, None) + metadata.album_total = 4 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"DISCTOTAL=blah", + u"DISCTOTAL=4"]) + self.assertEqual(metadata.album_total, 4) + + #album_total updates DISCTOTAL field first if possible + #including aliases + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCTOTAL=blah", + u"DISCTOTAL=3"], u"vendor")]) + self.assertEqual(metadata.album_total, 3) + metadata.album_total = 4 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"DISCTOTAL=blah", + u"DISCTOTAL=4"]) + self.assertEqual(metadata.album_total, 4) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TOTALDISCS=blah", + u"TOTALDISCS=3"], u"vendor")]) + self.assertEqual(metadata.album_total, 3) + metadata.album_total = 4 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TOTALDISCS=blah", + u"TOTALDISCS=4"]) + self.assertEqual(metadata.album_total, 4) + + #album_total updates slashed DISCNUMBER field if necessary + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER=2/3", + u"DISCTOTAL=5"], u"vendor")]) + self.assertEqual(metadata.album_total, 5) + metadata.album_total = 6 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"DISCNUMBER=2/3", + u"DISCTOTAL=6"]) + self.assertEqual(metadata.album_total, 6) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER=2/3"], u"vendor")]) + self.assertEqual(metadata.album_total, 3) + metadata.album_total = 6 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"DISCNUMBER=2/6"]) + self.assertEqual(metadata.album_total, 6) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER= foo / 3 bar"], + u"vendor")]) + self.assertEqual(metadata.album_total, 3) + metadata.album_total = 6 + self.assertEqual(metadata.get_block(4).comment_strings, + [u"DISCNUMBER= foo / 6 bar"]) + self.assertEqual(metadata.album_total, 6) + + #other fields update the first match + #while leaving the rest alone + metadata = audiotools.FlacMetaData([]) + metadata.track_name = u"blah" + self.assertEqual(metadata.track_name, u"blah") + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TITLE=blah"]) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TITLE=foo", + u"TITLE=bar", + u"FOO=baz"], + u"vendor")]) + metadata.track_name = u"blah" + self.assertEqual(metadata.track_name, u"blah") + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TITLE=blah", + u"TITLE=bar", + u"FOO=baz"]) + + #setting field to an empty string is okay + for metadata in [ + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT([], u"vendor")]), + audiotools.FlacMetaData([])]: + metadata.track_name = u"" + self.assertEqual(metadata.track_name, u"") + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TITLE="]) + + @METADATA_FLAC + def test_delattr(self): + #deleting nonexistent field is okay + for field in audiotools.MetaData.FIELDS: + for metadata in [ + audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT([], u"vendor")]), + audiotools.FlacMetaData([])]: + + delattr(metadata, field) + self.assertEqual(getattr(metadata, field), None) + + #deleting field removes all instances of it + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TITLE=track name"], + u"vendor")]) + del(metadata.track_name) + self.assertEqual(metadata.get_block(4).comment_strings, + []) + self.assertEqual(metadata.track_name, None) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TITLE=track name", + u"ALBUM=album name"], + u"vendor")]) + del(metadata.track_name) + self.assertEqual(metadata.get_block(4).comment_strings, + [u"ALBUM=album name"]) + self.assertEqual(metadata.track_name, None) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TITLE=track name", + u"TITLE=track name 2", + u"ALBUM=album name", + u"TITLE=track name 3"], + u"vendor")]) + del(metadata.track_name) + self.assertEqual(metadata.get_block(4).comment_strings, + [u"ALBUM=album name"]) + self.assertEqual(metadata.track_name, None) + + #setting field to None is the same as deleting field + metadata = audiotools.FlacMetaData([]) + metadata.track_name = None + self.assertEqual(metadata.track_name, None) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TITLE=track name"], + u"vendor")]) + metadata.track_name = None + self.assertEqual(metadata.get_block(4).comment_strings, + []) + self.assertEqual(metadata.track_name, None) + + #deleting track_number removes TRACKNUMBER field + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER=1"], + u"vendor")]) + del(metadata.track_number) + self.assertEqual(metadata.get_block(4).comment_strings, + []) + self.assertEqual(metadata.track_name, None) + + #deleting slashed TRACKNUMBER converts it to fresh TRACKTOTAL field + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER=1/3"], + u"vendor")]) + del(metadata.track_number) + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TRACKTOTAL=3"]) + self.assertEqual(metadata.track_number, None) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER=1/3", + u"TRACKTOTAL=4"], + u"vendor")]) + self.assertEqual(metadata.track_total, 4) + del(metadata.track_number) + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TRACKTOTAL=4", + u"TRACKTOTAL=3"]) + self.assertEqual(metadata.track_total, 4) + self.assertEqual(metadata.track_number, None) + + #deleting track_total removes TRACKTOTAL/TOTALTRACKS fields + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKTOTAL=3", + u"TOTALTRACKS=4"], + u"vendor")]) + del(metadata.track_total) + self.assertEqual(metadata.get_block(4).comment_strings, + []) + self.assertEqual(metadata.track_total, None) + + #deleting track_total also removes slashed side of TRACKNUMBER fields + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER=1/3"], + u"vendor")]) + del(metadata.track_total) + self.assertEqual(metadata.track_total, None) + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TRACKNUMBER=1"]) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER=1 / foo 3 baz"], + u"vendor")]) + del(metadata.track_total) + self.assertEqual(metadata.track_total, None) + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TRACKNUMBER=1"]) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"TRACKNUMBER= foo 1 bar / blah 4 baz"], u"vendor")]) + del(metadata.track_total) + self.assertEqual(metadata.track_total, None) + self.assertEqual(metadata.get_block(4).comment_strings, + [u"TRACKNUMBER= foo 1 bar"]) + + #deleting album_number removes DISCNUMBER field + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER=2"], + u"vendor")]) + del(metadata.album_number) + self.assertEqual(metadata.get_block(4).comment_strings, + []) + + #deleting slashed DISCNUMBER converts it to fresh DISCTOTAL field + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER=2/4"], + u"vendor")]) + del(metadata.album_number) + self.assertEqual(metadata.get_block(4).comment_strings, + [u"DISCTOTAL=4"]) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER=2/4", + u"DISCTOTAL=5"], + u"vendor")]) + self.assertEqual(metadata.album_total, 5) + del(metadata.album_number) + self.assertEqual(metadata.get_block(4).comment_strings, + [u"DISCTOTAL=5", + u"DISCTOTAL=4"]) + self.assertEqual(metadata.album_total, 5) + + #deleting album_total removes DISCTOTAL/TOTALDISCS fields + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCTOTAL=4", + u"TOTALDISCS=5"], + u"vendor")]) + del(metadata.album_total) + self.assertEqual(metadata.get_block(4).comment_strings, + []) + self.assertEqual(metadata.album_total, None) + + #deleting album_total also removes slashed side of DISCNUMBER fields + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER=2/4"], + u"vendor")]) + del(metadata.album_total) + self.assertEqual(metadata.album_total, None) + self.assertEqual(metadata.get_block(4).comment_strings, + [u"DISCNUMBER=2"]) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER=2 / foo 4 baz"], + u"vendor")]) + del(metadata.album_total) + self.assertEqual(metadata.album_total, None) + self.assertEqual(metadata.get_block(4).comment_strings, + [u"DISCNUMBER=2"]) + + metadata = audiotools.FlacMetaData([ + audiotools.flac.Flac_VORBISCOMMENT( + [u"DISCNUMBER= foo 2 bar / blah 4 baz"], u"vendor")]) + del(metadata.album_total) + self.assertEqual(metadata.album_total, None) + self.assertEqual(metadata.get_block(4).comment_strings, + [u"DISCNUMBER= foo 2 bar"]) + class M4AMetaDataTest(MetaDataTest): def setUp(self): @@ -2738,9 +4804,9 @@ #set_metadata can't alter the '\xa9too' field metadata = track.get_metadata() old_ilst = metadata.ilst_atom()["\xa9too"] - new_ilst = audiotools.M4A_ILST_Leaf_Atom( + new_ilst = audiotools.m4a_atoms.M4A_ILST_Leaf_Atom( '\xa9too', - [audiotools.M4A_ILST_Unicode_Data_Atom( + [audiotools.m4a_atoms.M4A_ILST_Unicode_Data_Atom( 0, 1, "Fooz")]) metadata.ilst_atom().replace_child(new_ilst) self.assertEqual(metadata.ilst_atom()["\xa9too"], @@ -2752,9 +4818,9 @@ #update_metadata can alter the '\xa9too' field metadata = track.get_metadata() old_ilst = metadata.ilst_atom()["\xa9too"] - new_ilst = audiotools.M4A_ILST_Leaf_Atom( + new_ilst = audiotools.m4a_atoms.M4A_ILST_Leaf_Atom( '\xa9too', - [audiotools.M4A_ILST_Unicode_Data_Atom( + [audiotools.m4a_atoms.M4A_ILST_Unicode_Data_Atom( 0, 1, "Fooz")]) metadata.ilst_atom().replace_child(new_ilst) self.assertEqual(metadata.ilst_atom()["\xa9too"], @@ -2767,14 +4833,14 @@ @METADATA_M4A def test_foreign_field(self): - from audiotools import M4A_META_Atom - from audiotools import M4A_HDLR_Atom - from audiotools import M4A_Tree_Atom - from audiotools import M4A_ILST_Leaf_Atom - from audiotools import M4A_ILST_Unicode_Data_Atom - from audiotools import M4A_ILST_TRKN_Data_Atom - from audiotools import M4A_ILST_DISK_Data_Atom - from audiotools import M4A_FREE_Atom + from audiotools.m4a_atoms import M4A_META_Atom + from audiotools.m4a_atoms import M4A_HDLR_Atom + from audiotools.m4a_atoms import M4A_Tree_Atom + from audiotools.m4a_atoms import M4A_ILST_Leaf_Atom + from audiotools.m4a_atoms import M4A_ILST_Unicode_Data_Atom + from audiotools.m4a_atoms import M4A_ILST_TRKN_Data_Atom + from audiotools.m4a_atoms import M4A_ILST_DISK_Data_Atom + from audiotools.m4a_atoms import M4A_FREE_Atom metadata = M4A_META_Atom( 0, 0, @@ -2851,9 +4917,9 @@ track.delete_metadata() metadata = self.empty_metadata() metadata['ilst'].add_child( - audiotools.M4A_ILST_Leaf_Atom( + audiotools.m4a_atoms.M4A_ILST_Leaf_Atom( key, - [audiotools.M4A_ILST_Unicode_Data_Atom( + [audiotools.m4a_atoms.M4A_ILST_Unicode_Data_Atom( 0, 1, unicode(value).encode('utf-8'))])) self.assertEqual(getattr(metadata, field), value) self.assertEqual( @@ -2932,6 +4998,591 @@ temp_file.close() @METADATA_M4A + def test_getattr(self): + from audiotools.m4a_atoms import M4A_META_Atom + from audiotools.m4a_atoms import M4A_Tree_Atom + from audiotools.m4a_atoms import M4A_ILST_Leaf_Atom + from audiotools.m4a_atoms import M4A_ILST_Unicode_Data_Atom + from audiotools.m4a_atoms import M4A_ILST_TRKN_Data_Atom + from audiotools.m4a_atoms import M4A_ILST_DISK_Data_Atom + + #no ilst atom is okay + for attr in audiotools.MetaData.FIELDS: + metadata = M4A_META_Atom(0, 0, []) + self.assertEqual(getattr(metadata, attr), None) + + #empty ilst atom is okay + for attr in audiotools.MetaData.FIELDS: + metadata = M4A_META_Atom(0, 0, [M4A_Tree_Atom('ilst', [])]) + self.assertEqual(getattr(metadata, attr), None) + + #fields grab the first available atom from ilst atom, if any + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', + [M4A_ILST_Leaf_Atom('\xa9nam', [])])]) + self.assertEqual(metadata.track_name, None) + + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom(0, 1, + "Track Name")])])]) + self.assertEqual(metadata.track_name, u"Track Name") + + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom(0, 1, + "Track Name")]), + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom(0, 1, + "Another Name")])])]) + self.assertEqual(metadata.track_name, u"Track Name") + + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom(0, 1, + "Track Name"), + M4A_ILST_Unicode_Data_Atom(0, 1, + "Another Name")])])]) + self.assertEqual(metadata.track_name, u"Track Name") + + #ensure track_number/_total/album_number/_total fields work + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', + [M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(1, 2)]), + M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_DISK_Data_Atom(3, 4)])])]) + self.assertEqual(metadata.track_number, 1) + self.assertEqual(metadata.track_total, 2) + self.assertEqual(metadata.album_number, 3) + self.assertEqual(metadata.album_total, 4) + + @METADATA_M4A + def test_setattr(self): + from audiotools.m4a_atoms import M4A_META_Atom + from audiotools.m4a_atoms import M4A_Tree_Atom + from audiotools.m4a_atoms import M4A_ILST_Leaf_Atom + from audiotools.m4a_atoms import M4A_ILST_Unicode_Data_Atom + from audiotools.m4a_atoms import M4A_ILST_TRKN_Data_Atom + from audiotools.m4a_atoms import M4A_ILST_DISK_Data_Atom + + #fields add a new ilst atom, if necessary + metadata = M4A_META_Atom(0, 0, []) + metadata.track_name = u"Track Name" + self.assertEqual(metadata.track_name, u"Track Name") + self.assertEqual( + metadata, + M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom(0, 1, + "Track Name")])])])) + + #fields add a new entry to ilst atom, if necessary + metadata = M4A_META_Atom(0, 0, [M4A_Tree_Atom('ilst', [])]) + metadata.track_name = u"Track Name" + self.assertEqual(metadata.track_name, u"Track Name") + self.assertEqual( + metadata, + M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom(0, 1, + "Track Name")])])])) + + #fields overwrite first child of ilst atom and leave rest alone + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom(0, 1, + "Old Track Name")])])]) + metadata.track_name = u"Track Name" + self.assertEqual(metadata.track_name, u"Track Name") + self.assertEqual( + metadata, + M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom(0, 1, + "Track Name")])])])) + + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom(0, 1, + "Old Track Name")]), + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom(0, 1, + "Old Track Name 2") + ])])]) + metadata.track_name = u"Track Name" + self.assertEqual(metadata.track_name, u"Track Name") + self.assertEqual( + metadata, + M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom(0, 1, + "Track Name")]), + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom(0, 1, + "Old Track Name 2") + ])])])) + + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom( + 0, 1, "Old Track Name"), + M4A_ILST_Unicode_Data_Atom( + 0, 1, "Track Name 2")])])]) + metadata.track_name = u"Track Name" + self.assertEqual(metadata.track_name, u"Track Name") + self.assertEqual( + metadata, + M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom( + 0, 1, "Track Name"), + M4A_ILST_Unicode_Data_Atom( + 0, 1, "Track Name 2")])])])) + + #setting track_number/_total/album_number/_total + #adds a new field if necessary + metadata = M4A_META_Atom(0, 0, []) + metadata.track_number = 1 + self.assertEqual(metadata.track_number, 1) + self.assertEqual( + metadata, + M4A_META_Atom(0, 0, + [M4A_Tree_Atom('ilst', + [M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(1, 0)])])])) + + metadata = M4A_META_Atom(0, 0, [M4A_Tree_Atom('ilst', [])]) + metadata.track_number = 1 + self.assertEqual(metadata.track_number, 1) + self.assertEqual( + metadata, + M4A_META_Atom(0, 0, + [M4A_Tree_Atom('ilst', + [M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(1, 0)])])])) + + metadata = M4A_META_Atom(0, 0, []) + metadata.track_total = 2 + self.assertEqual(metadata.track_total, 2) + self.assertEqual( + metadata, + M4A_META_Atom(0, 0, + [M4A_Tree_Atom('ilst', + [M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(0, 2)])])])) + + metadata = M4A_META_Atom(0, 0, [M4A_Tree_Atom('ilst', [])]) + metadata.track_total = 2 + self.assertEqual(metadata.track_total, 2) + self.assertEqual( + metadata, + M4A_META_Atom(0, 0, + [M4A_Tree_Atom('ilst', + [M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(0, 2)])])])) + + metadata = M4A_META_Atom(0, 0, []) + metadata.album_number = 3 + self.assertEqual(metadata.album_number, 3) + self.assertEqual( + metadata, + M4A_META_Atom(0, 0, + [M4A_Tree_Atom('ilst', + [M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_DISK_Data_Atom(3, 0)])])])) + + metadata = M4A_META_Atom(0, 0, [M4A_Tree_Atom('ilst', [])]) + metadata.album_number = 3 + self.assertEqual(metadata.album_number, 3) + self.assertEqual( + metadata, + M4A_META_Atom(0, 0, + [M4A_Tree_Atom('ilst', + [M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_DISK_Data_Atom(3, 0)])])])) + + metadata = M4A_META_Atom(0, 0, []) + metadata.album_total = 4 + self.assertEqual(metadata.album_total, 4) + self.assertEqual( + metadata, + M4A_META_Atom(0, 0, + [M4A_Tree_Atom('ilst', + [M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_TRKN_Data_Atom(0, 4)])])])) + + metadata = M4A_META_Atom(0, 0, [M4A_Tree_Atom('ilst', [])]) + metadata.album_total = 4 + self.assertEqual(metadata.album_total, 4) + self.assertEqual( + metadata, + M4A_META_Atom(0, 0, + [M4A_Tree_Atom('ilst', + [M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_TRKN_Data_Atom(0, 4)])])])) + + + #setting track_number/_total/album_number/_total + #overwrites existing field if necessary + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', + [M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(1, 2)]), + M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_DISK_Data_Atom(3, 4)])])]) + metadata.track_number = 6 + self.assertEqual(metadata.track_number, 6) + self.assertEqual(metadata, + M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', + [M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(6, 2)]), + M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_DISK_Data_Atom(3, 4)])])])) + + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', + [M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(1, 2)]), + M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_DISK_Data_Atom(3, 4)])])]) + metadata.track_total = 7 + self.assertEqual(metadata.track_total, 7) + self.assertEqual(metadata, + M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', + [M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(1, 7)]), + M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_DISK_Data_Atom(3, 4)])])])) + + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', + [M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(1, 2)]), + M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_DISK_Data_Atom(3, 4)])])]) + metadata.album_number = 8 + self.assertEqual(metadata.album_number, 8) + self.assertEqual(metadata, + M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', + [M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(1, 2)]), + M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_DISK_Data_Atom(8, 4)])])])) + + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', + [M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(1, 2)]), + M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_DISK_Data_Atom(3, 4)])])]) + metadata.album_total = 9 + self.assertEqual(metadata.album_total, 9) + self.assertEqual(metadata, + M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', + [M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(1, 2)]), + M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_DISK_Data_Atom(3, 9)])])])) + + @METADATA_M4A + def test_delattr(self): + from audiotools.m4a_atoms import M4A_META_Atom + from audiotools.m4a_atoms import M4A_Tree_Atom + from audiotools.m4a_atoms import M4A_ILST_Leaf_Atom + from audiotools.m4a_atoms import M4A_ILST_Unicode_Data_Atom + from audiotools.m4a_atoms import M4A_ILST_TRKN_Data_Atom + from audiotools.m4a_atoms import M4A_ILST_DISK_Data_Atom + + #fields remove all matching children from ilst atom + # - no ilst atom + metadata = M4A_META_Atom(0, 0, []) + del(metadata.track_name) + self.assertEqual(metadata.track_name, None) + self.assertEqual(metadata, M4A_META_Atom(0, 0, [])) + + # - empty ilst atom + metadata = M4A_META_Atom(0, 0, [M4A_Tree_Atom('ilst', [])]) + del(metadata.track_name) + self.assertEqual(metadata.track_name, None) + self.assertEqual(metadata, + M4A_META_Atom(0, 0, [M4A_Tree_Atom('ilst', [])])) + + # - 1 matching item in ilst atom + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom(0, 1, + "Track Name")])])]) + del(metadata.track_name) + self.assertEqual(metadata.track_name, None) + self.assertEqual(metadata, + M4A_META_Atom(0, 0, [M4A_Tree_Atom('ilst', [])])) + + # - 2 maching items in ilst atom + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom(0, 1, + "Track Name")]), + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom(0, 1, + "Track Name 2")])])]) + del(metadata.track_name) + self.assertEqual(metadata.track_name, None) + self.assertEqual(metadata, + M4A_META_Atom(0, 0, [M4A_Tree_Atom('ilst', [])])) + + # - 2 matching data atoms in ilst child + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom(0, 1, + "Track Name"), + M4A_ILST_Unicode_Data_Atom(0, 1, + "Track Name 2")])])]) + del(metadata.track_name) + self.assertEqual(metadata.track_name, None) + self.assertEqual(metadata, + M4A_META_Atom(0, 0, [M4A_Tree_Atom('ilst', [])])) + + #setting item to None is the same as deleting it + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + '\xa9nam', + [M4A_ILST_Unicode_Data_Atom(0, 1, + "Track Name")])])]) + metadata.track_name = None + self.assertEqual(metadata.track_name, None) + self.assertEqual(metadata, + M4A_META_Atom(0, 0, [M4A_Tree_Atom('ilst', [])])) + + #removing track number removes atom if track total is 0 + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(1, 0)])])]) + self.assertEqual(metadata.track_number, 1) + self.assertEqual(metadata.track_total, None) + del(metadata.track_number) + self.assertEqual(metadata.track_number, None) + self.assertEqual(metadata.track_total, None) + self.assertEqual(metadata, + M4A_META_Atom(0, 0, [M4A_Tree_Atom('ilst', [])])) + + #removing track number sets value to None if track total is > 0 + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(1, 2)])])]) + self.assertEqual(metadata.track_number, 1) + self.assertEqual(metadata.track_total, 2) + del(metadata.track_number) + self.assertEqual(metadata.track_number, None) + self.assertEqual(metadata.track_total, 2) + self.assertEqual(metadata, + M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(0, 2)])])])) + + #removing track total removes atom if track number is 0 + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(0, 2)])])]) + self.assertEqual(metadata.track_number, None) + self.assertEqual(metadata.track_total, 2) + del(metadata.track_total) + self.assertEqual(metadata.track_number, None) + self.assertEqual(metadata.track_total, None) + self.assertEqual(metadata, + M4A_META_Atom(0, 0, [M4A_Tree_Atom('ilst', [])])) + + #removing track total sets value to None if track number is > 0 + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(1, 2)])])]) + self.assertEqual(metadata.track_number, 1) + self.assertEqual(metadata.track_total, 2) + del(metadata.track_total) + self.assertEqual(metadata.track_number, 1) + self.assertEqual(metadata.track_total, None) + self.assertEqual(metadata, + M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + 'trkn', + [M4A_ILST_TRKN_Data_Atom(1, 0)])])])) + + #removing album number removes atom if album total is 0 + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_DISK_Data_Atom(3, 0)])])]) + self.assertEqual(metadata.album_number, 3) + self.assertEqual(metadata.album_total, None) + del(metadata.album_number) + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.album_total, None) + self.assertEqual(metadata, + M4A_META_Atom(0, 0, [M4A_Tree_Atom('ilst', [])])) + + #removing album number sets value to None if album total is > 0 + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_DISK_Data_Atom(3, 4)])])]) + self.assertEqual(metadata.album_number, 3) + self.assertEqual(metadata.album_total, 4) + del(metadata.album_number) + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.album_total, 4) + self.assertEqual(metadata, + M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_DISK_Data_Atom(0, 4)])])])) + + #removing album total removes atom if album number if 0 + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_DISK_Data_Atom(0, 4)])])]) + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.album_total, 4) + del(metadata.album_total) + self.assertEqual(metadata.album_number, None) + self.assertEqual(metadata.album_total, None) + self.assertEqual(metadata, + M4A_META_Atom(0, 0, [M4A_Tree_Atom('ilst', [])])) + + #removing album total sets value to None if album number is > 0 + metadata = M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_DISK_Data_Atom(3, 4)])])]) + self.assertEqual(metadata.album_number, 3) + self.assertEqual(metadata.album_total, 4) + del(metadata.album_total) + self.assertEqual(metadata.album_number, 3) + self.assertEqual(metadata.album_total, None) + self.assertEqual(metadata, + M4A_META_Atom( + 0, 0, + [M4A_Tree_Atom('ilst', [ + M4A_ILST_Leaf_Atom( + 'disk', + [M4A_ILST_DISK_Data_Atom(3, 0)])])])) + + @METADATA_M4A def test_images(self): for audio_class in self.supported_formats: temp_file = tempfile.NamedTemporaryFile( @@ -2997,10 +5648,8 @@ if (field in self.supported_fields): self.assertEqual(getattr(metadata_orig, field), getattr(metadata_new, field)) - elif (field in audiotools.MetaData.INTEGER_FIELDS): - self.assertEqual(getattr(metadata_new, field), 0) else: - self.assertEqual(getattr(metadata_new, field), u"") + self.assertEqual(getattr(metadata_new, field), None) #ensure images match, if supported if (self.metadata_class.supports_images()): @@ -3009,9 +5658,9 @@ #check non-MetaData fields metadata_orig = self.empty_metadata() metadata_orig['ilst'].add_child( - audiotools.M4A_ILST_Leaf_Atom( + audiotools.m4a_atoms.M4A_ILST_Leaf_Atom( 'test', - [audiotools.M4A_Leaf_Atom("data", "foobar")])) + [audiotools.m4a_atoms.M4A_Leaf_Atom("data", "foobar")])) self.assertEqual(metadata_orig['ilst']['test']['data'].data, "foobar") metadata_new = self.metadata_class.converted(metadata_orig) self.assertEqual(metadata_orig['ilst']['test']['data'].data, "foobar") @@ -3027,58 +5676,62 @@ @METADATA_M4A def test_clean(self): + from audiotools.text import (CLEAN_REMOVE_TRAILING_WHITESPACE, + CLEAN_REMOVE_LEADING_WHITESPACE, + CLEAN_REMOVE_EMPTY_TAG) + #check trailing whitespace - metadata = audiotools.M4A_META_Atom( - 0, 0, [audiotools.M4A_Tree_Atom('ilst', [])]) + metadata = audiotools.m4a_atoms.M4A_META_Atom( + 0, 0, [audiotools.m4a_atoms.M4A_Tree_Atom('ilst', [])]) metadata['ilst'].add_child( - audiotools.M4A_ILST_Leaf_Atom( + audiotools.m4a_atoms.M4A_ILST_Leaf_Atom( "\xa9nam", - [audiotools.M4A_ILST_Unicode_Data_Atom(0, 1, "Foo ")])) + [audiotools.m4a_atoms.M4A_ILST_Unicode_Data_Atom(0, 1, "Foo ")])) self.assertEqual(metadata['ilst']["\xa9nam"]['data'].data, "Foo ") self.assertEqual(metadata.track_name, u'Foo ') fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_("removed trailing whitespace from %(field)s") % + [CLEAN_REMOVE_TRAILING_WHITESPACE % {"field":"nam"}]) self.assertEqual(cleaned['ilst']['\xa9nam']['data'].data, "Foo") self.assertEqual(cleaned.track_name, u'Foo') #check leading whitespace - metadata = audiotools.M4A_META_Atom( - 0, 0, [audiotools.M4A_Tree_Atom('ilst', [])]) + metadata = audiotools.m4a_atoms.M4A_META_Atom( + 0, 0, [audiotools.m4a_atoms.M4A_Tree_Atom('ilst', [])]) metadata['ilst'].add_child( - audiotools.M4A_ILST_Leaf_Atom( + audiotools.m4a_atoms.M4A_ILST_Leaf_Atom( "\xa9nam", - [audiotools.M4A_ILST_Unicode_Data_Atom(0, 1, " Foo")])) + [audiotools.m4a_atoms.M4A_ILST_Unicode_Data_Atom(0, 1, " Foo")])) self.assertEqual(metadata['ilst']["\xa9nam"]['data'].data, " Foo") self.assertEqual(metadata.track_name, u' Foo') fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_("removed leading whitespace from %(field)s") % + [CLEAN_REMOVE_LEADING_WHITESPACE % {"field":"nam"}]) self.assertEqual(cleaned['ilst']['\xa9nam']['data'].data, "Foo") self.assertEqual(cleaned.track_name, u'Foo') #check empty fields - metadata = audiotools.M4A_META_Atom( - 0, 0, [audiotools.M4A_Tree_Atom('ilst', [])]) + metadata = audiotools.m4a_atoms.M4A_META_Atom( + 0, 0, [audiotools.m4a_atoms.M4A_Tree_Atom('ilst', [])]) metadata['ilst'].add_child( - audiotools.M4A_ILST_Leaf_Atom( + audiotools.m4a_atoms.M4A_ILST_Leaf_Atom( "\xa9nam", - [audiotools.M4A_ILST_Unicode_Data_Atom(0, 1, "")])) + [audiotools.m4a_atoms.M4A_ILST_Unicode_Data_Atom(0, 1, "")])) self.assertEqual(metadata['ilst']["\xa9nam"]['data'].data, "") self.assertEqual(metadata.track_name, u'') fixes = [] cleaned = metadata.clean(fixes) self.assertEqual(fixes, - [_("removed empty field %(field)s") % + [CLEAN_REMOVE_EMPTY_TAG % {"field":"nam"}]) self.assertRaises(KeyError, cleaned['ilst'].__getitem__, '\xa9nam') - self.assertEqual(cleaned.track_name, u'') + self.assertEqual(cleaned.track_name, None) #numerical fields can't have whitespace #and images aren't stored with metadata @@ -3264,6 +5917,717 @@ temp_file.close() @METADATA_VORBIS + def test_getitem(self): + #getitem with no matches raises KeyError + self.assertRaises(KeyError, + audiotools.VorbisComment([u"FOO=kelp"], + u"vendor").__getitem__, + u"BAR") + + #getitem with 1 match returns list of length 1 + self.assertEqual( + audiotools.VorbisComment([u"FOO=kelp", + u"BAR=spam"], u"vendor")[u"FOO"], + [u"kelp"]) + + #getitem with multiple matches returns multiple items, in order + self.assertEqual( + audiotools.VorbisComment([u"FOO=1", + u"BAR=spam", + u"FOO=2", + u"FOO=3"], u"vendor")[u"FOO"], + [u"1", u"2", u"3"]) + + #getitem with aliases returns all matching items, in order + self.assertEqual( + audiotools.VorbisComment([u"TRACKTOTAL=1", + u"TOTALTRACKS=2", + u"TRACKTOTAL=3"], + u"vendor")[u"TRACKTOTAL"], + [u"1", u"2", u"3"]) + + self.assertEqual( + audiotools.VorbisComment([u"TRACKTOTAL=1", + u"TOTALTRACKS=2", + u"TRACKTOTAL=3"], + u"vendor")[u"TOTALTRACKS"], + [u"1", u"2", u"3"]) + + #getitem is case-insensitive + self.assertEqual( + audiotools.VorbisComment([u"FOO=kelp"], u"vendor")[u"FOO"], + [u"kelp"]) + + self.assertEqual( + audiotools.VorbisComment([u"FOO=kelp"], u"vendor")[u"foo"], + [u"kelp"]) + + self.assertEqual( + audiotools.VorbisComment([u"foo=kelp"], u"vendor")[u"FOO"], + [u"kelp"]) + + self.assertEqual( + audiotools.VorbisComment([u"foo=kelp"], u"vendor")[u"foo"], + [u"kelp"]) + + @METADATA_VORBIS + def test_setitem(self): + #setitem replaces all keys with new values + metadata = audiotools.VorbisComment([], u"vendor") + metadata[u"FOO"] = [u"bar"] + self.assertEqual(metadata[u"FOO"], [u"bar"]) + + metadata = audiotools.VorbisComment([u"FOO=1"], u"vendor") + metadata[u"FOO"] = [u"bar"] + self.assertEqual(metadata[u"FOO"], [u"bar"]) + + metadata = audiotools.VorbisComment([u"FOO=1", + u"FOO=2"], u"vendor") + metadata[u"FOO"] = [u"bar"] + self.assertEqual(metadata[u"FOO"], [u"bar"]) + + metadata = audiotools.VorbisComment([], u"vendor") + metadata[u"FOO"] = [u"bar", u"baz"] + self.assertEqual(metadata[u"FOO"], [u"bar", u"baz"]) + + metadata = audiotools.VorbisComment([u"FOO=1"], u"vendor") + metadata[u"FOO"] = [u"bar", u"baz"] + self.assertEqual(metadata[u"FOO"], [u"bar", u"baz"]) + + metadata = audiotools.VorbisComment([u"FOO=1", + u"FOO=2"], u"vendor") + metadata[u"FOO"] = [u"bar", u"baz"] + self.assertEqual(metadata[u"FOO"], [u"bar", u"baz"]) + + #setitem leaves other items alone + metadata = audiotools.VorbisComment([u"BAR=bar"], + u"vendor") + metadata[u"FOO"] = [u"foo"] + self.assertEqual(metadata.comment_strings, + [u"BAR=bar", u"FOO=foo"]) + + metadata = audiotools.VorbisComment([u"FOO=ack", + u"BAR=bar"], + u"vendor") + metadata[u"FOO"] = [u"foo"] + self.assertEqual(metadata.comment_strings, + [u"FOO=foo", u"BAR=bar"]) + + metadata = audiotools.VorbisComment([u"FOO=ack", + u"BAR=bar"], + u"vendor") + metadata[u"FOO"] = [u"foo", u"fud"] + self.assertEqual(metadata.comment_strings, + [u"FOO=foo", u"BAR=bar", u"FOO=fud"]) + + #setitem handles aliases automatically + metadata = audiotools.VorbisComment([u"TRACKTOTAL=1", + u"TOTALTRACKS=2", + u"TRACKTOTAL=3"], + u"vendor") + metadata[u"TRACKTOTAL"] = [u"4", u"5", u"6"] + self.assertEqual(metadata.comment_strings, + [u"TRACKTOTAL=4", + u"TOTALTRACKS=5", + u"TRACKTOTAL=6"]) + + metadata = audiotools.VorbisComment([u"TRACKTOTAL=1", + u"TOTALTRACKS=2", + u"TRACKTOTAL=3"], + u"vendor") + metadata[u"TOTALTRACKS"] = [u"4", u"5", u"6"] + self.assertEqual(metadata.comment_strings, + [u"TRACKTOTAL=4", + u"TOTALTRACKS=5", + u"TRACKTOTAL=6"]) + + #setitem is case-preserving + metadata = audiotools.VorbisComment([u"FOO=1"], u"vendor") + metadata[u"FOO"] = [u"bar"] + self.assertEqual(metadata.comment_strings, + [u"FOO=bar"]) + + metadata = audiotools.VorbisComment([u"FOO=1"], u"vendor") + metadata[u"foo"] = [u"bar"] + self.assertEqual(metadata.comment_strings, + [u"FOO=bar"]) + + metadata = audiotools.VorbisComment([u"foo=1"], u"vendor") + metadata[u"FOO"] = [u"bar"] + self.assertEqual(metadata.comment_strings, + [u"foo=bar"]) + + metadata = audiotools.VorbisComment([u"foo=1"], u"vendor") + metadata[u"foo"] = [u"bar"] + self.assertEqual(metadata.comment_strings, + [u"foo=bar"]) + + @METADATA_VORBIS + def test_getattr(self): + #track_number grabs the first available integer + self.assertEqual( + audiotools.VorbisComment([u"TRACKNUMBER=10"], + u"vendor").track_number, + 10) + + self.assertEqual( + audiotools.VorbisComment([u"TRACKNUMBER=10", + u"TRACKNUMBER=5"], + u"vendor").track_number, + 10) + + self.assertEqual( + audiotools.VorbisComment([u"TRACKNUMBER=foo 10 bar"], + u"vendor").track_number, + 10) + + self.assertEqual( + audiotools.VorbisComment([u"TRACKNUMBER=foo", + u"TRACKNUMBER=10"], + u"vendor").track_number, + 10) + + self.assertEqual( + audiotools.VorbisComment([u"TRACKNUMBER=foo", + u"TRACKNUMBER=foo 10 bar"], + u"vendor").track_number, + 10) + + #track_number is case-insensitive + self.assertEqual( + audiotools.VorbisComment([u"tRaCkNuMbEr=10"], + u"vendor").track_number, + 10) + + #album_number grabs the first available integer + self.assertEqual( + audiotools.VorbisComment([u"DISCNUMBER=20"], + u"vendor").album_number, + 20) + + self.assertEqual( + audiotools.VorbisComment([u"DISCNUMBER=20", + u"DISCNUMBER=5"], + u"vendor").album_number, + 20) + + self.assertEqual( + audiotools.VorbisComment([u"DISCNUMBER=foo 20 bar"], + u"vendor").album_number, + 20) + + self.assertEqual( + audiotools.VorbisComment([u"DISCNUMBER=foo", + u"DISCNUMBER=20"], + u"vendor").album_number, + 20) + + self.assertEqual( + audiotools.VorbisComment([u"DISCNUMBER=foo", + u"DISCNUMBER=foo 20 bar"], + u"vendor").album_number, + 20) + + #album_number is case-insensitive + self.assertEqual( + audiotools.VorbisComment([u"dIsCnUmBeR=20"], + u"vendor").album_number, + 20) + + #track_total grabs the first available TRACKTOTAL integer + #before falling back on slashed fields + self.assertEqual( + audiotools.VorbisComment([u"TRACKTOTAL=15"], + u"vendor").track_total, + 15) + + self.assertEqual( + audiotools.VorbisComment([u"TRACKNUMBER=5/10"], + u"vendor").track_total, + 10) + + self.assertEqual( + audiotools.VorbisComment([u"TRACKTOTAL=foo/10"], + u"vendor").track_total, + 10) + + self.assertEqual( + audiotools.VorbisComment([u"TRACKNUMBER=5/10", + u"TRACKTOTAL=15"], + u"vendor").track_total, + 15) + + self.assertEqual( + audiotools.VorbisComment([u"TRACKTOTAL=15", + u"TRACKNUMBER=5/10"], + u"vendor").track_total, + 15) + + #track_total is case-insensitive + self.assertEqual( + audiotools.VorbisComment([u"tracktotal=15"], + u"vendor").track_total, + 15) + + #track_total supports aliases + self.assertEqual( + audiotools.VorbisComment([u"TOTALTRACKS=15"], + u"vendor").track_total, + 15) + + #album_total grabs the first available DISCTOTAL integer + #before falling back on slashed fields + self.assertEqual( + audiotools.VorbisComment([u"DISCTOTAL=25"], + u"vendor").album_total, + 25) + + self.assertEqual( + audiotools.VorbisComment([u"DISCNUMBER=10/30"], + u"vendor").album_total, + 30) + + self.assertEqual( + audiotools.VorbisComment([u"DISCNUMBER=foo/30"], + u"vendor").album_total, + 30) + + self.assertEqual( + audiotools.VorbisComment([u"DISCNUMBER=10/30", + u"DISCTOTAL=25"], + u"vendor").album_total, + 25) + + self.assertEqual( + audiotools.VorbisComment([u"DISCTOTAL=25", + u"DISCNUMBER=10/30"], + u"vendor").album_total, + 25) + + #album_total is case-insensitive + self.assertEqual( + audiotools.VorbisComment([u"disctotal=25"], + u"vendor").album_total, + 25) + + #album_total supports aliases + self.assertEqual( + audiotools.VorbisComment([u"TOTALDISCS=25"], + u"vendor").album_total, + 25) + + #other fields grab the first available item + self.assertEqual( + audiotools.VorbisComment([u"TITLE=first", + u"TITLE=last"], + u"vendor").track_name, + u"first") + + @METADATA_VORBIS + def test_setattr(self): + #track_number adds new field if necessary + metadata = audiotools.VorbisComment([], u"vendor") + self.assertEqual(metadata.track_number, None) + metadata.track_number = 11 + self.assertEqual(metadata.comment_strings, + [u"TRACKNUMBER=11"]) + self.assertEqual(metadata.track_number, 11) + + metadata = audiotools.VorbisComment([u"TRACKNUMBER=blah"], + u"vendor") + self.assertEqual(metadata.track_number, None) + metadata.track_number = 11 + self.assertEqual(metadata.comment_strings, + [u"TRACKNUMBER=blah", + u"TRACKNUMBER=11"]) + self.assertEqual(metadata.track_number, 11) + + #track_number updates the first integer field + #and leaves other junk in that field alone + metadata = audiotools.VorbisComment([u"TRACKNUMBER=10/12"], u"vendor") + self.assertEqual(metadata.track_number, 10) + metadata.track_number = 11 + self.assertEqual(metadata.comment_strings, + [u"TRACKNUMBER=11/12"]) + self.assertEqual(metadata.track_number, 11) + + metadata = audiotools.VorbisComment([u"TRACKNUMBER=foo 10 bar"], + u"vendor") + self.assertEqual(metadata.track_number, 10) + metadata.track_number = 11 + self.assertEqual(metadata.comment_strings, + [u"TRACKNUMBER=foo 11 bar"]) + self.assertEqual(metadata.track_number, 11) + + metadata = audiotools.VorbisComment([u"TRACKNUMBER=foo 10 bar", + u"TRACKNUMBER=blah"], + u"vendor") + self.assertEqual(metadata.track_number, 10) + metadata.track_number = 11 + self.assertEqual(metadata.comment_strings, + [u"TRACKNUMBER=foo 11 bar", + u"TRACKNUMBER=blah"]) + self.assertEqual(metadata.track_number, 11) + + #album_number adds new field if necessary + metadata = audiotools.VorbisComment([], u"vendor") + self.assertEqual(metadata.album_number, None) + metadata.album_number = 3 + self.assertEqual(metadata.comment_strings, + [u"DISCNUMBER=3"]) + self.assertEqual(metadata.album_number, 3) + + metadata = audiotools.VorbisComment([u"DISCNUMBER=blah"], + u"vendor") + self.assertEqual(metadata.album_number, None) + metadata.album_number = 3 + self.assertEqual(metadata.comment_strings, + [u"DISCNUMBER=blah", + u"DISCNUMBER=3"]) + self.assertEqual(metadata.album_number, 3) + + #album_number updates the first integer field + #and leaves other junk in that field alone + metadata = audiotools.VorbisComment([u"DISCNUMBER=2/4"], u"vendor") + self.assertEqual(metadata.album_number, 2) + metadata.album_number = 3 + self.assertEqual(metadata.comment_strings, + [u"DISCNUMBER=3/4"]) + self.assertEqual(metadata.album_number, 3) + + metadata = audiotools.VorbisComment([u"DISCNUMBER=foo 2 bar"], + u"vendor") + self.assertEqual(metadata.album_number, 2) + metadata.album_number = 3 + self.assertEqual(metadata.comment_strings, + [u"DISCNUMBER=foo 3 bar"]) + self.assertEqual(metadata.album_number, 3) + + metadata = audiotools.VorbisComment([u"DISCNUMBER=foo 2 bar", + u"DISCNUMBER=blah"], + u"vendor") + self.assertEqual(metadata.album_number, 2) + metadata.album_number = 3 + self.assertEqual(metadata.comment_strings, + [u"DISCNUMBER=foo 3 bar", + u"DISCNUMBER=blah"]) + self.assertEqual(metadata.album_number, 3) + + #track_total adds new TRACKTOTAL field if necessary + metadata = audiotools.VorbisComment([], u"vendor") + self.assertEqual(metadata.track_total, None) + metadata.track_total = 12 + self.assertEqual(metadata.comment_strings, + [u"TRACKTOTAL=12"]) + self.assertEqual(metadata.track_total, 12) + + metadata = audiotools.VorbisComment([u"TRACKTOTAL=blah"], + u"vendor") + self.assertEqual(metadata.track_total, None) + metadata.track_total = 12 + self.assertEqual(metadata.comment_strings, + [u"TRACKTOTAL=blah", + u"TRACKTOTAL=12"]) + self.assertEqual(metadata.track_total, 12) + + #track_total updates first integer TRACKTOTAL field first if possible + #including aliases + metadata = audiotools.VorbisComment([u"TRACKTOTAL=blah", + u"TRACKTOTAL=2"], u"vendor") + self.assertEqual(metadata.track_total, 2) + metadata.track_total = 3 + self.assertEqual(metadata.comment_strings, + [u"TRACKTOTAL=blah", + u"TRACKTOTAL=3"]) + self.assertEqual(metadata.track_total, 3) + + metadata = audiotools.VorbisComment([u"TOTALTRACKS=blah", + u"TOTALTRACKS=2"], u"vendor") + self.assertEqual(metadata.track_total, 2) + metadata.track_total = 3 + self.assertEqual(metadata.comment_strings, + [u"TOTALTRACKS=blah", + u"TOTALTRACKS=3"]) + self.assertEqual(metadata.track_total, 3) + + #track_total updates slashed TRACKNUMBER field if necessary + metadata = audiotools.VorbisComment([u"TRACKNUMBER=1/4", + u"TRACKTOTAL=2"], u"vendor") + self.assertEqual(metadata.track_total, 2) + metadata.track_total = 3 + self.assertEqual(metadata.comment_strings, + [u"TRACKNUMBER=1/4", + u"TRACKTOTAL=3"]) + self.assertEqual(metadata.track_total, 3) + + metadata = audiotools.VorbisComment([u"TRACKNUMBER=1/4"], u"vendor") + self.assertEqual(metadata.track_total, 4) + metadata.track_total = 3 + self.assertEqual(metadata.comment_strings, + [u"TRACKNUMBER=1/3"]) + self.assertEqual(metadata.track_total, 3) + + metadata = audiotools.VorbisComment([u"TRACKNUMBER= foo / 4 bar"], + u"vendor") + self.assertEqual(metadata.track_total, 4) + metadata.track_total = 3 + self.assertEqual(metadata.comment_strings, + [u"TRACKNUMBER= foo / 3 bar"]) + self.assertEqual(metadata.track_total, 3) + + #album_total adds new DISCTOTAL field if necessary + metadata = audiotools.VorbisComment([], u"vendor") + self.assertEqual(metadata.album_total, None) + metadata.album_total = 4 + self.assertEqual(metadata.comment_strings, + [u"DISCTOTAL=4"]) + self.assertEqual(metadata.album_total, 4) + + metadata = audiotools.VorbisComment([u"DISCTOTAL=blah"], + u"vendor") + self.assertEqual(metadata.album_total, None) + metadata.album_total = 4 + self.assertEqual(metadata.comment_strings, + [u"DISCTOTAL=blah", + u"DISCTOTAL=4"]) + self.assertEqual(metadata.album_total, 4) + + #album_total updates DISCTOTAL field first if possible + #including aliases + metadata = audiotools.VorbisComment([u"DISCTOTAL=blah", + u"DISCTOTAL=3"], u"vendor") + self.assertEqual(metadata.album_total, 3) + metadata.album_total = 4 + self.assertEqual(metadata.comment_strings, + [u"DISCTOTAL=blah", + u"DISCTOTAL=4"]) + self.assertEqual(metadata.album_total, 4) + + metadata = audiotools.VorbisComment([u"TOTALDISCS=blah", + u"TOTALDISCS=3"], u"vendor") + self.assertEqual(metadata.album_total, 3) + metadata.album_total = 4 + self.assertEqual(metadata.comment_strings, + [u"TOTALDISCS=blah", + u"TOTALDISCS=4"]) + self.assertEqual(metadata.album_total, 4) + + #album_total updates slashed DISCNUMBER field if necessary + metadata = audiotools.VorbisComment([u"DISCNUMBER=2/3", + u"DISCTOTAL=5"], u"vendor") + self.assertEqual(metadata.album_total, 5) + metadata.album_total = 6 + self.assertEqual(metadata.comment_strings, + [u"DISCNUMBER=2/3", + u"DISCTOTAL=6"]) + self.assertEqual(metadata.album_total, 6) + + metadata = audiotools.VorbisComment([u"DISCNUMBER=2/3"], u"vendor") + self.assertEqual(metadata.album_total, 3) + metadata.album_total = 6 + self.assertEqual(metadata.comment_strings, + [u"DISCNUMBER=2/6"]) + self.assertEqual(metadata.album_total, 6) + + metadata = audiotools.VorbisComment([u"DISCNUMBER= foo / 3 bar"], + u"vendor") + self.assertEqual(metadata.album_total, 3) + metadata.album_total = 6 + self.assertEqual(metadata.comment_strings, + [u"DISCNUMBER= foo / 6 bar"]) + self.assertEqual(metadata.album_total, 6) + + #other fields update the first match + #while leaving the rest alone + metadata = audiotools.VorbisComment([u"TITLE=foo", + u"TITLE=bar", + u"FOO=baz"], + u"vendor") + metadata.track_name = u"blah" + self.assertEqual(metadata.track_name, u"blah") + self.assertEqual(metadata.comment_strings, + [u"TITLE=blah", + u"TITLE=bar", + u"FOO=baz"]) + + #setting field to an empty string is okay + metadata = audiotools.VorbisComment([], u"vendor") + metadata.track_name = u"" + self.assertEqual(metadata.track_name, u"") + self.assertEqual(metadata.comment_strings, + [u"TITLE="]) + + @METADATA_VORBIS + def test_delattr(self): + #deleting nonexistent field is okay + for field in audiotools.MetaData.FIELDS: + metadata = audiotools.VorbisComment([], + u"vendor") + delattr(metadata, field) + self.assertEqual(getattr(metadata, field), None) + + #deleting field removes all instances of it + metadata = audiotools.VorbisComment([], + u"vendor") + del(metadata.track_name) + self.assertEqual(metadata.comment_strings, + []) + self.assertEqual(metadata.track_name, None) + + metadata = audiotools.VorbisComment([u"TITLE=track name"], + u"vendor") + del(metadata.track_name) + self.assertEqual(metadata.comment_strings, + []) + self.assertEqual(metadata.track_name, None) + + metadata = audiotools.VorbisComment([u"TITLE=track name", + u"ALBUM=album name"], + u"vendor") + del(metadata.track_name) + self.assertEqual(metadata.comment_strings, + [u"ALBUM=album name"]) + self.assertEqual(metadata.track_name, None) + + metadata = audiotools.VorbisComment([u"TITLE=track name", + u"TITLE=track name 2", + u"ALBUM=album name", + u"TITLE=track name 3"], + u"vendor") + del(metadata.track_name) + self.assertEqual(metadata.comment_strings, + [u"ALBUM=album name"]) + self.assertEqual(metadata.track_name, None) + + #setting field to None is the same as deleting field + metadata = audiotools.VorbisComment([u"TITLE=track name"], + u"vendor") + metadata.track_name = None + self.assertEqual(metadata.comment_strings, + []) + self.assertEqual(metadata.track_name, None) + + metadata = audiotools.VorbisComment([u"TITLE=track name"], + u"vendor") + metadata.track_name = None + self.assertEqual(metadata.comment_strings, + []) + self.assertEqual(metadata.track_name, None) + + #deleting track_number removes TRACKNUMBER field + metadata = audiotools.VorbisComment([u"TRACKNUMBER=1"], + u"vendor") + del(metadata.track_number) + self.assertEqual(metadata.comment_strings, + []) + self.assertEqual(metadata.track_number, None) + + #deleting slashed TRACKNUMBER converts it to fresh TRACKTOTAL field + metadata = audiotools.VorbisComment([u"TRACKNUMBER=1/3"], + u"vendor") + del(metadata.track_number) + self.assertEqual(metadata.comment_strings, + [u"TRACKTOTAL=3"]) + self.assertEqual(metadata.track_number, None) + + metadata = audiotools.VorbisComment([u"TRACKNUMBER=1/3", + u"TRACKTOTAL=4"], + u"vendor") + self.assertEqual(metadata.track_total, 4) + del(metadata.track_number) + self.assertEqual(metadata.comment_strings, + [u"TRACKTOTAL=4", + u"TRACKTOTAL=3"]) + self.assertEqual(metadata.track_total, 4) + self.assertEqual(metadata.track_number, None) + + #deleting track_total removes TRACKTOTAL/TOTALTRACKS fields + metadata = audiotools.VorbisComment([u"TRACKTOTAL=3", + u"TOTALTRACKS=4"], + u"vendor") + del(metadata.track_total) + self.assertEqual(metadata.comment_strings, + []) + self.assertEqual(metadata.track_total, None) + + #deleting track_total also removes slashed side of TRACKNUMBER fields + metadata = audiotools.VorbisComment([u"TRACKNUMBER=1/3"], + u"vendor") + del(metadata.track_total) + self.assertEqual(metadata.track_total, None) + self.assertEqual(metadata.comment_strings, + [u"TRACKNUMBER=1"]) + + metadata = audiotools.VorbisComment([u"TRACKNUMBER=1 / foo 3 baz"], + u"vendor") + del(metadata.track_total) + self.assertEqual(metadata.track_total, None) + self.assertEqual(metadata.comment_strings, + [u"TRACKNUMBER=1"]) + + metadata = audiotools.VorbisComment( + [u"TRACKNUMBER= foo 1 bar / blah 4 baz"], u"vendor") + del(metadata.track_total) + self.assertEqual(metadata.track_total, None) + self.assertEqual(metadata.comment_strings, + [u"TRACKNUMBER= foo 1 bar"]) + + #deleting album_number removes DISCNUMBER field + metadata = audiotools.VorbisComment([u"DISCNUMBER=2"], + u"vendor") + del(metadata.album_number) + self.assertEqual(metadata.comment_strings, + []) + + #deleting slashed DISCNUMBER converts it to fresh DISCTOTAL field + metadata = audiotools.VorbisComment([u"DISCNUMBER=2/4"], + u"vendor") + del(metadata.album_number) + self.assertEqual(metadata.comment_strings, + [u"DISCTOTAL=4"]) + + metadata = audiotools.VorbisComment([u"DISCNUMBER=2/4", + u"DISCTOTAL=5"], + u"vendor") + self.assertEqual(metadata.album_total, 5) + del(metadata.album_number) + self.assertEqual(metadata.comment_strings, + [u"DISCTOTAL=5", + u"DISCTOTAL=4"]) + self.assertEqual(metadata.album_total, 5) + + #deleting album_total removes DISCTOTAL/TOTALDISCS fields + metadata = audiotools.VorbisComment([u"DISCTOTAL=4", + u"TOTALDISCS=5"], + u"vendor") + del(metadata.album_total) + self.assertEqual(metadata.comment_strings, + []) + self.assertEqual(metadata.album_total, None) + + #deleting album_total also removes slashed side of DISCNUMBER fields + metadata = audiotools.VorbisComment([u"DISCNUMBER=2/4"], + u"vendor") + del(metadata.album_total) + self.assertEqual(metadata.album_total, None) + self.assertEqual(metadata.comment_strings, + [u"DISCNUMBER=2"]) + + metadata = audiotools.VorbisComment([u"DISCNUMBER=2 / foo 4 baz"], + u"vendor") + del(metadata.album_total) + self.assertEqual(metadata.album_total, None) + self.assertEqual(metadata.comment_strings, + [u"DISCNUMBER=2"]) + + metadata = audiotools.VorbisComment( + [u"DISCNUMBER= foo 2 bar / blah 4 baz"], u"vendor") + del(metadata.album_total) + self.assertEqual(metadata.album_total, None) + self.assertEqual(metadata.comment_strings, + [u"DISCNUMBER= foo 2 bar"]) + + @METADATA_VORBIS def test_supports_images(self): self.assertEqual(self.metadata_class.supports_images(), False) @@ -3373,6 +6737,21 @@ self.assertEqual(metadata.track_total, 4) metadata = self.empty_metadata() + metadata[u"TRACKNUMBER"] = [u"foo 2 bar /4"] + self.assertEqual(metadata.track_number, 2) + self.assertEqual(metadata.track_total, 4) + + metadata = self.empty_metadata() + metadata[u"TRACKNUMBER"] = [u"2/ foo 4 bar"] + self.assertEqual(metadata.track_number, 2) + self.assertEqual(metadata.track_total, 4) + + metadata = self.empty_metadata() + metadata[u"TRACKNUMBER"] = [u"foo 2 bar / kelp 4 spam"] + self.assertEqual(metadata.track_number, 2) + self.assertEqual(metadata.track_total, 4) + + metadata = self.empty_metadata() metadata[u"DISCNUMBER"] = [u"1/3"] self.assertEqual(metadata.album_number, 1) self.assertEqual(metadata.album_total, 3) @@ -3392,8 +6771,30 @@ self.assertEqual(metadata.album_number, 1) self.assertEqual(metadata.album_total, 3) + metadata = self.empty_metadata() + metadata[u"DISCNUMBER"] = [u"foo 1 bar /3"] + self.assertEqual(metadata.album_number, 1) + self.assertEqual(metadata.album_total, 3) + + metadata = self.empty_metadata() + metadata[u"DISCNUMBER"] = [u"1/ foo 3 bar"] + self.assertEqual(metadata.album_number, 1) + self.assertEqual(metadata.album_total, 3) + + metadata = self.empty_metadata() + metadata[u"DISCNUMBER"] = [u"foo 1 bar / kelp 3 spam"] + self.assertEqual(metadata.album_number, 1) + self.assertEqual(metadata.album_total, 3) + + @METADATA_VORBIS def test_clean(self): + from audiotools.text import (CLEAN_REMOVE_TRAILING_WHITESPACE, + CLEAN_REMOVE_LEADING_WHITESPACE, + CLEAN_REMOVE_LEADING_ZEROES, + CLEAN_REMOVE_LEADING_WHITESPACE_ZEROES, + CLEAN_REMOVE_EMPTY_TAG) + #check trailing whitespace metadata = audiotools.VorbisComment([u"TITLE=Foo "], u"vendor") results = [] @@ -3401,7 +6802,7 @@ self.assertEqual(cleaned, audiotools.VorbisComment(["TITLE=Foo"], u"vendor")) self.assertEqual(results, - [_(u"removed trailing whitespace from %(field)s") % + [CLEAN_REMOVE_TRAILING_WHITESPACE % {"field":u"TITLE"}]) #check leading whitespace @@ -3411,7 +6812,7 @@ self.assertEqual(cleaned, audiotools.VorbisComment([u"TITLE=Foo"], u"vendor")) self.assertEqual(results, - [_(u"removed leading whitespace from %(field)s") % + [CLEAN_REMOVE_LEADING_WHITESPACE % {"field":u"TITLE"}]) #check leading zeroes @@ -3422,9 +6823,25 @@ audiotools.VorbisComment([u"TRACKNUMBER=1"], u"vendor")) self.assertEqual(results, - [_(u"removed leading zeroes from %(field)s") % + [CLEAN_REMOVE_LEADING_ZEROES % {"field":u"TRACKNUMBER"}]) + #check leading space/zeroes in slashed field + for field in [u"TRACKNUMBER=01/2", + u"TRACKNUMBER=1/02", + u"TRACKNUMBER=01/02", + u"TRACKNUMBER=1/ 2", + u"TRACKNUMBER=1/ 02"]: + metadata = audiotools.VorbisComment([field], u"vendor") + results = [] + cleaned = metadata.clean(results) + self.assertEqual(cleaned, + audiotools.VorbisComment([u"TRACKNUMBER=1/2"], + u"vendor")) + self.assertEqual(results, + [CLEAN_REMOVE_LEADING_WHITESPACE_ZEROES % + {"field":u"TRACKNUMBER"}]) + #check empty fields metadata = audiotools.VorbisComment([u"TITLE="], u"vendor") results = [] @@ -3432,7 +6849,7 @@ self.assertEqual(cleaned, audiotools.VorbisComment([], u"vendor")) self.assertEqual(results, - [_(u"removed empty field %(field)s") % + [CLEAN_REMOVE_EMPTY_TAG % {"field":u"TITLE"}]) metadata = audiotools.VorbisComment([u"TITLE= "], u"vendor") @@ -3441,7 +6858,7 @@ self.assertEqual(cleaned, audiotools.VorbisComment([], u"vendor")) self.assertEqual(results, - [_(u"removed empty field %(field)s") % + [CLEAN_REMOVE_EMPTY_TAG % {"field":u"TITLE"}]) @METADATA_VORBIS @@ -3542,3 +6959,186 @@ temp2.close() finally: temp1.close() + + +class OpusTagsTest(MetaDataTest): + def setUp(self): + self.metadata_class = audiotools.OpusTags + self.supported_fields = ["track_name", + "track_number", + "track_total", + "album_name", + "artist_name", + "performer_name", + "composer_name", + "conductor_name", + "media", + "ISRC", + "catalog", + "copyright", + "publisher", + "year", + "album_number", + "album_total", + "comment"] + self.supported_formats = [audiotools.OpusAudio] + + def empty_metadata(self): + return self.metadata_class.converted(audiotools.MetaData()) + + @METADATA_OPUS + def test_update(self): + import os + + for audio_class in self.supported_formats: + temp_file = tempfile.NamedTemporaryFile( + suffix="." + audio_class.SUFFIX) + track = audio_class.from_pcm(temp_file.name, BLANK_PCM_Reader(10)) + temp_file_stat = os.stat(temp_file.name)[0] + try: + #update_metadata on file's internal metadata round-trips okay + track.set_metadata(audiotools.MetaData(track_name=u"Foo")) + metadata = track.get_metadata() + self.assertEqual(metadata.track_name, u"Foo") + metadata.track_name = u"Bar" + track.update_metadata(metadata) + metadata = track.get_metadata() + self.assertEqual(metadata.track_name, u"Bar") + + #update_metadata on unwritable file generates IOError + metadata = track.get_metadata() + os.chmod(temp_file.name, 0) + self.assertRaises(IOError, + track.update_metadata, + metadata) + os.chmod(temp_file.name, temp_file_stat) + + #update_metadata with foreign MetaData generates ValueError + self.assertRaises(ValueError, + track.update_metadata, + audiotools.MetaData(track_name=u"Foo")) + + #update_metadata with None makes no changes + track.update_metadata(None) + metadata = track.get_metadata() + self.assertEqual(metadata.track_name, u"Bar") + + #vendor_string not updated with set_metadata() + #but can be updated with update_metadata() + old_metadata = track.get_metadata() + new_metadata = audiotools.OpusTags( + comment_strings=old_metadata.comment_strings[:], + vendor_string=u"Vendor String") + track.set_metadata(new_metadata) + self.assertEqual(track.get_metadata().vendor_string, + old_metadata.vendor_string) + track.update_metadata(new_metadata) + self.assertEqual(track.get_metadata().vendor_string, + new_metadata.vendor_string) + + #REPLAYGAIN_* tags not updated with set_metadata() + #but can be updated with update_metadata() + old_metadata = track.get_metadata() + new_metadata = audiotools.OpusTags( + comment_strings=old_metadata.comment_strings + + [u"REPLAYGAIN_REFERENCE_LOUDNESS=89.0 dB"], + vendor_string=old_metadata.vendor_string) + track.set_metadata(new_metadata) + self.assertRaises( + KeyError, + track.get_metadata().__getitem__, + u"REPLAYGAIN_REFERENCE_LOUDNESS") + track.update_metadata(new_metadata) + self.assertEqual( + track.get_metadata()[u"REPLAYGAIN_REFERENCE_LOUDNESS"], + [u"89.0 dB"]) + finally: + temp_file.close() + + @METADATA_OPUS + def test_foreign_field(self): + metadata = audiotools.OpusTags([u"TITLE=Track Name", + u"ALBUM=Album Name", + u"TRACKNUMBER=1", + u"TRACKTOTAL=3", + u"DISCNUMBER=2", + u"DISCTOTAL=4", + u"FOO=Bar"], u"") + for format in self.supported_formats: + temp_file = tempfile.NamedTemporaryFile( + suffix="." + format.SUFFIX) + try: + track = format.from_pcm(temp_file.name, + BLANK_PCM_Reader(1)) + track.set_metadata(metadata) + metadata2 = track.get_metadata() + self.assert_(set(metadata.comment_strings).issubset( + set(metadata2.comment_strings))) + self.assertEqual(metadata.__class__, metadata2.__class__) + self.assertEqual(metadata2[u"FOO"], [u"Bar"]) + finally: + temp_file.close() + + @METADATA_OPUS + def test_field_mapping(self): + mapping = [('track_name', 'TITLE', u'a'), + ('track_number', 'TRACKNUMBER', 1), + ('track_total', 'TRACKTOTAL', 2), + ('album_name', 'ALBUM', u'b'), + ('artist_name', 'ARTIST', u'c'), + ('performer_name', 'PERFORMER', u'd'), + ('composer_name', 'COMPOSER', u'e'), + ('conductor_name', 'CONDUCTOR', u'f'), + ('media', 'SOURCE MEDIUM', u'g'), + ('ISRC', 'ISRC', u'h'), + ('catalog', 'CATALOG', u'i'), + ('copyright', 'COPYRIGHT', u'j'), + ('year', 'DATE', u'k'), + ('album_number', 'DISCNUMBER', 3), + ('album_total', 'DISCTOTAL', 4), + ('comment', 'COMMENT', u'l')] + + for format in self.supported_formats: + temp_file = tempfile.NamedTemporaryFile(suffix="." + format.SUFFIX) + try: + track = format.from_pcm(temp_file.name, BLANK_PCM_Reader(1)) + + #ensure that setting a class field + #updates its corresponding low-level implementation + for (field, key, value) in mapping: + track.delete_metadata() + metadata = self.empty_metadata() + setattr(metadata, field, value) + self.assertEqual(getattr(metadata, field), value) + self.assertEqual( + metadata[key][0], + unicode(value)) + track.set_metadata(metadata) + metadata2 = track.get_metadata() + self.assertEqual(getattr(metadata2, field), value) + self.assertEqual( + metadata2[key][0], + unicode(value)) + + #ensure that updating the low-level implementation + #is reflected in the class field + for (field, key, value) in mapping: + track.delete_metadata() + metadata = self.empty_metadata() + metadata[key] = [unicode(value)] + self.assertEqual(getattr(metadata, field), value) + self.assertEqual( + metadata[key][0], + unicode(value)) + track.set_metadata(metadata) + metadata2 = track.get_metadata() + self.assertEqual(getattr(metadata2, field), value) + self.assertEqual( + metadata2[key][0], + unicode(value)) + finally: + temp_file.close() + + @METADATA_OPUS + def test_supports_images(self): + self.assertEqual(self.metadata_class.supports_images(), False)
View file
audiotools-2.18.tar.gz/test/test_streams.py -> audiotools-2.19.tar.gz/test/test_streams.py
Changed
@@ -29,13 +29,15 @@ self.channel_mask = channel_mask self.bits_per_sample = bits_per_sample - def read(self, bytes): - (framelist, self.framelist) = self.framelist.split( - self.framelist.frame_count(bytes)) + def read(self, pcm_frames): + (framelist, self.framelist) = self.framelist.split(pcm_frames) return framelist + def read_closed(self, pcm_frames): + raise ValueError() + def close(self): - pass + self.read = self.read_closed class MD5Reader: @@ -57,8 +59,8 @@ self.channels, self.bits_per_sample) - def read(self, bytes): - framelist = self.pcmreader.read(bytes) + def read(self, pcm_frames): + framelist = self.pcmreader.read(pcm_frames) self.md5.update(framelist.to_bytes(False, True)) return framelist @@ -121,8 +123,8 @@ self.a2 = a2 self.md5 = md5() - def read(self, bytes): - framelist = Sine_Mono.read(self, bytes) + def read(self, pcm_frames): + framelist = Sine_Mono.read(self, pcm_frames) self.md5.update(framelist.to_bytes(False, True)) return framelist @@ -159,8 +161,8 @@ self.fmult = fmult self.md5 = md5() - def read(self, bytes): - framelist = Sine_Stereo.read(self, bytes) + def read(self, pcm_frames): + framelist = Sine_Stereo.read(self, pcm_frames) self.md5.update(framelist.to_bytes(False, True)) return framelist @@ -300,9 +302,9 @@ self.channel_counts)] self.md5 = md5() - def read(self, bytes): + def read(self, pcm_frames): framelist = audiotools.pcm.from_channels( - [stream.read(bytes / self.channels) for stream in self.streams]) + [stream.read(pcm_frames) for stream in self.streams]) self.md5.update(framelist.to_bytes(False, True)) return framelist @@ -345,10 +347,9 @@ self.sample_frame = audiotools.pcm.FrameList("", 2, 16, False, False) self.md5 = md5() - def read(self, bytes): + def read(self, pcm_frames): wave = [] - for i in xrange(min(self.sample_frame.frame_count(bytes), - self.pcm_frames)): + for i in xrange(min(pcm_frames, self.pcm_frames)): wave.append((self.i % 2000) << 2) wave.append((self.i % 1000) << 3) self.i += 1 @@ -361,6 +362,9 @@ self.md5.update(framelist.to_bytes(False, True)) return framelist + def read_closed(self, pcm_frames): + raise ValueError() + def reset(self): self.i = 0 self.pcm_frames = self.total_frames @@ -373,7 +377,7 @@ return self.md5.hexdigest() def close(self): - self.pcm_frames = 0 + self.read = self.read_closed def __repr__(self): return "WastedBPS(%s)" % (repr(self.pcm_frames))
View file
audiotools-2.18.tar.gz/test/test_utils.py -> audiotools-2.19.tar.gz/test/test_utils.py
Changed
@@ -30,7 +30,7 @@ import test_streams from hashlib import md5 -from test import (parser, BLANK_PCM_Reader, Combinations, +from test import (parser, BLANK_PCM_Reader, Combinations, Possibilities, EXACT_BLANK_PCM_Reader, RANDOM_PCM_Reader, TEST_COVER1, TEST_COVER2, TEST_COVER3, TEST_COVER4, HUGE_BMP) @@ -82,14 +82,17 @@ def __run_checks__(self): for (stream, expected_output) in self.line_checks: - self.assertEqual( - unicodedata.normalize( - 'NFC', - getattr(self, - stream).readline().decode(audiotools.IO_ENCODING)), - unicodedata.normalize( - 'NFC', - expected_output) + unicode(os.linesep)) + stream_line = unicodedata.normalize( + 'NFC', + getattr(self, + stream).readline().decode(audiotools.IO_ENCODING)) + expected_line = unicodedata.normalize( + 'NFC', + expected_output) + unicode(os.linesep) + self.assertEqual(stream_line, expected_line, + "%s != %s" % ( + repr(stream_line), + repr(expected_line))) self.line_checks = [] def __clear_checks__(self): @@ -217,7 +220,8 @@ @UTIL_CD2TRACK def test_options(self): - messenger = audiotools.VerboseMessenger("cd2track") + from audiotools.text import (ERR_UNSUPPORTED_COMPRESSION_MODE, + LAB_CD2TRACK_PROGRESS) all_options = ["-t", "-q", "-d", "--format", "--album-number", "--album-total"] @@ -238,8 +242,7 @@ "-c", self.cue_file] + options), 1) self.__check_error__( - _(u"\"%(quality)s\" is not a supported " + - u"compression mode for type \"%(type)s\"") % + ERR_UNSUPPORTED_COMPRESSION_MODE % {"quality": "1", "type": output_type.NAME}) continue @@ -277,10 +280,12 @@ #check that the output is being generated correctly for (i, path) in enumerate(output_filenames): self.__check_info__( - _(u"track %(track_number)2.2d -> %(filename)s") % \ + audiotools.output_progress( + LAB_CD2TRACK_PROGRESS % {"track_number": i + 1, - "filename": messenger.filename( - os.path.join(output_dir, path))}) + "filename": audiotools.Filename( + os.path.join(output_dir, path))}, + i + 1, len(output_filenames))) #rip log is generated afterward as a table #FIXME - check table of rip log? @@ -293,17 +298,16 @@ self.stream.reset() self.assert_( audiotools.pcm_frame_cmp( - audiotools.PCMCat(iter([t.to_pcm() - for t in output_tracks])), + audiotools.PCMCat([t.to_pcm() for t in output_tracks]), self.stream) is None) #make sure metadata fits our expectations for i in xrange(len(output_tracks)): metadata = output_tracks[i].get_metadata() if (metadata is not None): - self.assertEqual(metadata.track_name, u"") - self.assertEqual(metadata.album_name, u"") - self.assertEqual(metadata.artist_name, u"") + self.assertEqual(metadata.track_name, None) + self.assertEqual(metadata.album_name, None) + self.assertEqual(metadata.artist_name, None) self.assertEqual(metadata.track_number, i + 1) self.assertEqual(metadata.track_total, 3) @@ -311,12 +315,45 @@ if ("--album-number" in options): self.assertEqual(metadata.album_number, 8) else: - self.assertEqual(metadata.album_number, 0) + self.assertEqual(metadata.album_number, None) if ("--album-total" in options): self.assertEqual(metadata.album_total, 9) else: - self.assertEqual(metadata.album_total, 0) + self.assertEqual(metadata.album_total, None) + + @UTIL_CD2TRACK + def test_unicode(self): + from shutil import rmtree + + for (output_directory, + format_string) in Possibilities( + ["testdir", + u'abc\xe0\xe7\xe8\u3041\u3044\u3046'.encode('utf-8')], + ["%(track_number)d.%(suffix)s", + u'%(track_number)d - abc\xe0\xe7\xe8\u3041\u3044\u3046.%(suffix)s'.encode('utf-8')]): + if (os.path.isdir(output_directory)): + rmtree(output_directory) + + self.assertEqual( + self.__run_app__( + ["cd2track", "-c", self.cue_file, + "--type", "flac", + "--format", format_string, + "--dir", output_directory]), 0) + + tracks = [audiotools.open( + os.path.join(output_directory, + format_string % {"track_number":i, + "suffix":"flac"})) + for i in range(1, 4)] + + self.assertEqual(sum([t.total_frames() for t in tracks]), + 793800) + + if (os.path.isdir(output_directory)): + rmtree(output_directory) + def populate_bad_options(self, options): populated = ["--no-musicbrainz", "--no-freedb"] @@ -347,7 +384,18 @@ @UTIL_CD2TRACK def test_errors(self): - filename = audiotools.Messenger("cd2track", None).filename + from audiotools.text import (ERR_DUPLICATE_OUTPUT_FILE, + ERR_UNSUPPORTED_COMPRESSION_MODE, + ERR_UNKNOWN_FIELD, + LAB_SUPPORTED_FIELDS, + ERR_ENCODING_ERROR, + ) + + self.assertEqual( + self.__run_app__(["cd2track", "-c", self.cue_file, + "--format=foo"]), 1) + self.__check_error__(ERR_DUPLICATE_OUTPUT_FILE % + (audiotools.Filename("foo"),)) all_options = ["-t", "-q", "-d", "--format", "--album-number", "--album-total"] @@ -378,35 +426,24 @@ options), 1) - if (("-o" in options) and - ("-d" in options)): - self.__check_error__( - _(u"-o and -d options are not compatible")) - self.__check_info__( - _(u"Please specify either -o or -d but not both")) - continue - - if (("--format" in options) and ("-o" in options)): - self.__queue_warning__( - _(u"--format has no effect when used with -o")) - if ("-q" in options): self.__check_error__( - _(u"\"%(quality)s\" is not a supported compression mode for type \"%(type)s\"") % + ERR_UNSUPPORTED_COMPRESSION_MODE % {"quality": "bar", "type": audiotools.DEFAULT_TYPE}) continue if ("--format" in options): self.__check_error__( - _(u"Unknown field \"%s\" in file format") % ("foo")) - self.__check_info__(_(u"Supported fields are:")) + ERR_UNKNOWN_FIELD % ("foo")) + self.__check_info__(LAB_SUPPORTED_FIELDS) for field in sorted(audiotools.MetaData.FIELDS + \ ("album_track_number", "suffix")): if (field == 'track_number'): self.__check_info__(u"%(track_number)2.2d") else: self.__check_info__(u"%%(%s)s" % (field)) + self.__check_info__(u"%(basename)s") continue if ("-d" in options): @@ -417,8 +454,8 @@ audiotools.MetaData(track_number=1, track_total=3))) self.__check_error__( - _(u"Unable to write \"%s\"") % \ - (output_path)) + ERR_ENCODING_ERROR % + (audiotools.Filename(output_path),)) continue @@ -508,7 +545,8 @@ @UTIL_COVERDUMP def test_options(self): - msg = audiotools.VerboseMessenger("coverdump") + from audiotools.text import (LAB_ENCODE, + ) all_options = ["-d", "-p"] for count in xrange(len(all_options) + 1): @@ -548,9 +586,9 @@ output_path = os.path.join(".", output_filename) self.__check_info__( - _(u"%(source)s -> %(destination)s") % - {"source": msg.filename(self.track1.filename), - "destination": msg.filename(output_path)}) + LAB_ENCODE % + {"source": audiotools.Filename(self.track1.filename), + "destination": audiotools.Filename(output_path)}) output_image = audiotools.Image.new( open(output_path, "rb").read(), u"", @@ -589,9 +627,9 @@ output_path = os.path.join(".", output_filename) self.__check_info__( - _(u"%(source)s -> %(destination)s") % - {"source": msg.filename(self.track2.filename), - "destination": msg.filename(output_path)}) + LAB_ENCODE % + {"source": audiotools.Filename(self.track2.filename), + "destination": audiotools.Filename(output_path)}) output_image = audiotools.Image.new( open(output_path, "rb").read(), u"", @@ -599,23 +637,69 @@ self.assertEqual(output_image, image) @UTIL_COVERDUMP + def test_unicode(self): + from shutil import rmtree + + for (output_directory, + file_path, + prefix) in Possibilities( + ["testdir", #check --dir + u'abc\xe0\xe7\xe8\u3041\u3044\u3046'.encode('utf-8')], + ["test.flac", #check filename arguments + u'abc\xe0\xe7\xe8\u3041\u3044\u3046.flac'.encode('utf-8')], + [None, #check --prefix + "prefix_", + u'abc\xe0\xe7\xe8\u3041\u3044\u3046_'.encode('utf-8')]): + if (os.path.isdir(output_directory)): + rmtree(output_directory) + if (os.path.isfile(file_path)): + os.unlink(file_path) + + track = audiotools.FlacAudio.from_pcm( + file_path, + BLANK_PCM_Reader(1)) + metadata = track.get_metadata() + metadata.add_image(audiotools.Image.new(TEST_COVER1, + u"", + 0)) + track.update_metadata(metadata) + + self.assertEqual( + self.__run_app__( + ["coverdump", + "--dir", output_directory] + + (["--prefix", prefix] if prefix is not None else []) + + [file_path]), 0) + + self.assertEqual( + os.path.isfile( + os.path.join(output_directory, + (prefix if prefix is not None else "") + + "front_cover.jpg")), True) + + if (os.path.isdir(output_directory)): + rmtree(output_directory) + if (os.path.isfile(file_path)): + os.unlink(file_path) + + @UTIL_COVERDUMP def test_errors(self): - msg = audiotools.VerboseMessenger("coverdump") + from audiotools.text import (ERR_1_FILE_REQUIRED, + ERR_ENCODING_ERROR, + ERR_OUTPUT_IS_INPUT) #check no input files self.assertEqual(self.__run_app__( ["coverdump", "-V", "normal"]), 1) - self.__check_error__( - _(u"You must specify exactly 1 supported audio file")) + self.__check_error__(ERR_1_FILE_REQUIRED) #check multiple input files self.assertEqual(self.__run_app__( ["coverdump", "-V", "normal", self.track1.filename, self.track2.filename]), 1) - self.__check_error__( - _(u"You must specify exactly 1 supported audio file")) + self.__check_error__(ERR_1_FILE_REQUIRED) #check unwritable output dir old_mode = os.stat(self.output_dir).st_mode @@ -625,9 +709,9 @@ ["coverdump", "-V", "normal", "-d", self.output_dir, self.track1.filename]), 1) self.__check_error__( - _(u"Unable to write \"%s\"") % (msg.filename( - os.path.join(self.output_dir, - "front_cover01.png")))) + ERR_ENCODING_ERROR % + (audiotools.Filename(os.path.join(self.output_dir, + "front_cover01.png")),)) finally: os.chmod(self.output_dir, old_mode) @@ -639,11 +723,27 @@ ["coverdump", "-V", "normal", self.track1.filename]), 1) self.__check_error__( - _(u"[Errno 13] Permission denied: '.'")) + ERR_ENCODING_ERROR % + (audiotools.Filename("front_cover01.png"),)) finally: os.chmod(self.cwd_dir, old_mode) + #check input file same as output file + track = audiotools.FlacAudio.from_pcm( + os.path.join(self.output_dir, "front_cover.jpg"), + BLANK_PCM_Reader(1)) + metadata = track.get_metadata() + metadata.add_image(audiotools.Image.new(TEST_COVER1, u"", 0)) + track.update_metadata(metadata) + + self.assertEqual(self.__run_app__( + ["coverdump", "-V", "normal", + "-d", self.output_dir, track.filename]), 1) + self.__check_error__( + ERR_OUTPUT_IS_INPUT % + (audiotools.Filename(track.filename),)) + class dvdainfo(UtilTest): @UTIL_DVDAINFO @@ -662,20 +762,23 @@ @UTIL_DVDAINFO def test_errors(self): + from audiotools.text import (ERR_NO_AUDIO_TS, + ERR_DVDA_IOERROR_AUDIO_TS, + ERR_DVDA_INVALID_AUDIO_TS) + #test with no -A option self.assertEqual(self.__run_app__(["dvdainfo"]), 1) - self.__check_error__( - _(u"You must specify the DVD-Audio's AUDIO_TS directory with -A")) + self.__check_error__(ERR_NO_AUDIO_TS) #test with an invalid AUDIO_TS dir self.assertEqual(self.__run_app__(["dvdainfo", "-A", self.invalid_dir1]), 1) - self.__check_error__(_(u"unable to open AUDIO_TS.IFO")) + self.__check_error__(ERR_DVDA_IOERROR_AUDIO_TS) #test with an invalid AUDIO_TS/AUDIO_TS.IFO file self.assertEqual(self.__run_app__(["dvdainfo", "-A", self.invalid_dir2]), 1) - self.__check_error__(_(u"invalid AUDIO_TS.IFO")) + self.__check_error__(ERR_DVDA_INVALID_AUDIO_TS) class dvda2track(UtilTest): @@ -695,20 +798,23 @@ @UTIL_DVDA2TRACK def test_errors(self): + from audiotools.text import (ERR_NO_AUDIO_TS, + ERR_DVDA_IOERROR_AUDIO_TS, + ERR_DVDA_INVALID_AUDIO_TS) + #test with no -A option self.assertEqual(self.__run_app__(["dvda2track"]), 1) - self.__check_error__( - _(u"You must specify the DVD-Audio's AUDIO_TS directory with -A")) + self.__check_error__(ERR_NO_AUDIO_TS) #test with an invalid AUDIO_TS dir self.assertEqual(self.__run_app__(["dvda2track", "-A", self.invalid_dir1]), 1) - self.__check_error__(_(u"unable to open AUDIO_TS.IFO")) + self.__check_error__(ERR_DVDA_IOERROR_AUDIO_TS) #test with an invalid AUDIO_TS/AUDIO_TS.IFO file self.assertEqual(self.__run_app__(["dvda2track", "-A", self.invalid_dir2]), 1) - self.__check_error__(_(u"invalid AUDIO_TS.IFO")) + self.__check_error__(ERR_DVDA_INVALID_AUDIO_TS) #FIXME #It's difficult to test an invalid --title or invalid --xmcd @@ -864,9 +970,17 @@ @UTIL_TRACK2TRACK def test_options(self): + from audiotools.text import (ERR_TRACK2TRACK_O_AND_D, + ERR_TRACK2TRACK_O_AND_D_SUGGESTION, + ERR_TRACK2TRACK_O_AND_FORMAT, + ERR_UNSUPPORTED_COMPRESSION_MODE, + LAB_ENCODE, + RG_REPLAYGAIN_ADDED, + RG_REPLAYGAIN_APPLIED) + messenger = audiotools.Messenger("track2track", None) - all_options = ["-t", "-q", "-d", "--format", "-o", "-T", + all_options = ["-t", "-q", "-d", "--format", "-o", "--replay-gain", "--no-replay-gain"] for count in xrange(1, len(all_options) + 1): @@ -882,15 +996,12 @@ self.assertEqual( self.__run_app__(["track2track"] + options), 1) - self.__check_error__( - _(u"-o and -d options are not compatible")) - self.__check_info__( - _(u"Please specify either -o or -d but not both")) + self.__check_error__(ERR_TRACK2TRACK_O_AND_D) + self.__check_info__(ERR_TRACK2TRACK_O_AND_D_SUGGESTION) continue if (("--format" in options) and ("-o" in options)): - self.__queue_warning__( - _(u"--format has no effect when used with -o")) + self.__queue_warning__(ERR_TRACK2TRACK_O_AND_FORMAT) if ('-t' in options): output_class = audiotools.TYPE_MAP[ @@ -907,10 +1018,9 @@ self.assertEqual( self.__run_app__(["track2track"] + options), 1) self.__check_error__( - _(u"\"%(quality)s\" is not a supported " + - u"compression mode for type \"%(type)s\"") % - {"quality": options[options.index("-q") + 1], - "type": output_class.NAME}) + ERR_UNSUPPORTED_COMPRESSION_MODE % + {"quality": options[options.index("-q") + 1], + "type": output_class.NAME}) continue if ('--format' in options): @@ -937,17 +1047,17 @@ if ("-o" not in options): self.__check_info__( - _(u"%(source)s -> %(destination)s") % + LAB_ENCODE % {"source": - messenger.filename(self.track1.filename), + audiotools.Filename(self.track1.filename), "destination": - messenger.filename(output_path)}) + audiotools.Filename(output_path)}) track2 = audiotools.open(output_path) self.assertEqual(track2.NAME, output_class.NAME) if (self.track1.lossless() and track2.lossless() and not - (output_class.can_add_replay_gain() and + (output_class.supports_replay_gain() and "--replay-gain" in options)): self.assert_( audiotools.pcm_frame_cmp(self.track1.to_pcm(), @@ -956,34 +1066,112 @@ self.assertEqual(track2.get_metadata(), metadata) image = track2.get_metadata().images()[0] - if ('-T' in options): - self.assertEqual(max(image.width, - image.height), - audiotools.THUMBNAIL_SIZE) - else: - self.assertEqual(image.width, self.cover.width) - self.assertEqual(image.height, self.cover.height) + self.assertEqual(image.width, self.cover.width) + self.assertEqual(image.height, self.cover.height) - if (output_class.can_add_replay_gain()): + if (output_class.supports_replay_gain()): if (output_class.lossless_replay_gain()): if (("-o" not in options) and audiotools.ADD_REPLAYGAIN and ("--no-replay-gain" not in options)): - self.__check_info__( - _(u"ReplayGain added")) + self.__check_info__(RG_REPLAYGAIN_ADDED) self.assert_(track2.replay_gain() is not None) else: if ("--replay-gain" in options): - self.__check_info__( - _(u"ReplayGain applied")) + self.__check_info__(RG_REPLAYGAIN_APPLIED) + + @UTIL_TRACK2TRACK + def test_unicode(self): + from shutil import rmtree + + for (output_directory, + format_string, + file_path) in Possibilities( + ["testdir", #check --dir + u'abc\xe0\xe7\xe8\u3041\u3044\u3046'.encode('utf-8')], + ["new_file.flac", #check --format] + u'abc\xe0\xe7\xe8\u3041\u3044\u3046-2.flac'.encode('utf-8')], + ["file.flac", #check filename arguments + u'abc\xe0\xe7\xe8\u3041\u3044\u3046.flac'.encode('utf-8')]): + if (os.path.isdir(output_directory)): + rmtree(output_directory) + if (os.path.isfile(file_path)): + os.unlink(file_path) + + track = audiotools.FlacAudio.from_pcm( + file_path, + BLANK_PCM_Reader(1)) + + self.assertEqual( + self.__run_app__( + ["track2track", + "--dir", output_directory, + "--format", format_string, + file_path]), 0) + + self.assertEqual( + audiotools.pcm_frame_cmp( + track.to_pcm(), + audiotools.open(os.path.join(output_directory, + format_string)).to_pcm()), + None) + + if (os.path.isdir(output_directory)): + rmtree(output_directory) + if (os.path.isfile(file_path)): + os.unlink(file_path) + + for (file_path, + output_path) in Possibilities( + ["file.flac", #check filename arguments + u'abc\xe0\xe7\xe8\u3041\u3044\u3046.flac'.encode('utf-8')], + ["output_file.flac", #check --output argument + u'abc\xe0\xe7\xe8\u3041\u3044\u3046-2.flac'.encode('utf-8')]): + if (os.path.isfile(output_path)): + os.unlink(output_path) + if (os.path.isfile(file_path)): + os.unlink(file_path) + + track = audiotools.FlacAudio.from_pcm( + file_path, + BLANK_PCM_Reader(1)) + + self.assertEqual( + self.__run_app__( + ["track2track", "-o", output_path, file_path]), 0) + + self.assertEqual( + audiotools.pcm_frame_cmp( + track.to_pcm(), + audiotools.open(output_path).to_pcm()), + None) + + if (os.path.isfile(output_path)): + os.unlink(output_path) + if (os.path.isfile(file_path)): + os.unlink(file_path) @UTIL_TRACK2TRACK def test_errors(self): - messenger = audiotools.Messenger("track2track", None) - filename = messenger.filename + from audiotools.text import (ERR_TRACK2TRACK_O_AND_D, + ERR_TRACK2TRACK_O_AND_D_SUGGESTION, + ERR_TRACK2TRACK_O_AND_FORMAT, + ERR_UNSUPPORTED_COMPRESSION_MODE, + ERR_INVALID_JOINT, + ERR_UNKNOWN_FIELD, + LAB_SUPPORTED_FIELDS, + ERR_FILES_REQUIRED, + ERR_TRACK2TRACK_O_AND_MULTIPLE, + ERR_DUPLICATE_FILE, + ERR_OUTPUT_IS_INPUT, + ERR_DUPLICATE_OUTPUT_FILE, + ERR_UNSUPPORTED_CHANNEL_COUNT, + ERR_UNSUPPORTED_CHANNEL_MASK, + ERR_UNSUPPORTED_BITS_PER_SAMPLE, + ) all_options = ["-t", "-q", "-d", "--format", "-o", "-j", - "-T", "--replay-gain", "--no-replay-gain"] + "--replay-gain", "--no-replay-gain"] for count in xrange(0, len(all_options) + 1): for options in Combinations(all_options, count): self.clean_output_dirs() @@ -1009,44 +1197,42 @@ if (("-o" in options) and ("-d" in options)): - self.__check_error__( - _(u"-o and -d options are not compatible")) - self.__check_info__( - _(u"Please specify either -o or -d but not both")) + self.__check_error__(ERR_TRACK2TRACK_O_AND_D) + self.__check_info__(ERR_TRACK2TRACK_O_AND_D_SUGGESTION) continue if (("--format" in options) and ("-o" in options)): - self.__queue_warning__( - _(u"--format has no effect when used with -o")) + self.__queue_warning__(ERR_TRACK2TRACK_O_AND_FORMAT) if ("-q" in options): self.__check_error__( - _(u"\"%(quality)s\" is not a supported compression mode for type \"%(type)s\"") % + ERR_UNSUPPORTED_COMPRESSION_MODE % {"quality": "bar", "type": output_class.NAME}) continue if ("-j" in options): self.__check_error__( - _(u"You must run at least 1 process at a time")) + ERR_INVALID_JOINT) continue if ("-o" in options): self.__check_error__( - _(u"[Errno 20] Not a directory: '%s'") % + u"[Errno 20] Not a directory: '%s'" % (self.unwritable_file)) continue if ("--format" in options): self.__check_error__( - _(u"Unknown field \"%s\" in file format") % ("foo")) - self.__check_info__(_(u"Supported fields are:")) + ERR_UNKNOWN_FIELD % ("foo")) + self.__check_info__(LAB_SUPPORTED_FIELDS) for field in sorted(audiotools.MetaData.FIELDS + \ ("album_track_number", "suffix")): if (field == 'track_number'): self.__check_info__(u"%(track_number)2.2d") else: self.__check_info__(u"%%(%s)s" % (field)) + self.__check_info__(u"%(basename)s") continue if ("-d" in options): @@ -1057,8 +1243,8 @@ self.track1.get_metadata(), None)) self.__check_error__( - _(u"[Errno 13] Permission denied: '%s'") % \ - (output_path)) + u"[Errno 13] Permission denied: '%s'" % + (output_path)) continue #the error triggered by a broken file is variable @@ -1067,8 +1253,7 @@ #check no input files self.assertEqual(self.__run_app__(["track2track"]), 1) - self.__check_error__( - _(u"You must specify at least 1 supported audio file")) + self.__check_error__(ERR_FILES_REQUIRED) self.track2 = self.input_format.from_pcm( os.path.join(self.input_dir, "02.%s" % (self.input_format.SUFFIX)), @@ -1079,8 +1264,45 @@ "-o", self.output_file.name, self.track1.filename, self.track2.filename]), 1) + self.__check_error__(ERR_TRACK2TRACK_O_AND_MULTIPLE) + + #check duplicate input file + self.assertEqual(self.__run_app__(["track2track", + self.track1.filename, + self.track1.filename, + self.track2.filename]), 1) + self.__check_error__( + ERR_DUPLICATE_FILE % + (audiotools.Filename(self.track1.filename),)) + + #check identical input and output file + self.assertEqual( + self.__run_app__(["track2track", + self.track1.filename, + "-t", self.input_format.NAME, + "-d", self.input_dir, + "--format=%(track_number)2.2d.%(suffix)s"]), 1) + self.__check_error__( + ERR_OUTPUT_IS_INPUT % + (audiotools.Filename(self.track1.filename),)) + + #check identical input and output file with -o + self.assertEqual(self.__run_app__(["track2track", + "-t", self.input_format.NAME, + "-o", self.track1.filename, + self.track1.filename]), 1) + self.__check_error__( + ERR_OUTPUT_IS_INPUT % + (audiotools.Filename(self.track1.filename),)) + + #check duplicate output files + self.assertEqual(self.__run_app__(["track2track", + "--format", "foo", + self.track1.filename, + self.track2.filename]), 1) self.__check_error__( - _(u"You may specify only 1 input file for use with -o")) + ERR_DUPLICATE_OUTPUT_FILE % ( + audiotools.Filename(os.path.join(".", "foo")),)) #check conversion from supported to unsupported channel count unsupported_count_file = tempfile.NamedTemporaryFile( @@ -1096,10 +1318,9 @@ self.output_dir, supported_track.filename]), 1) self.__check_error__( - _(u"Unable to write \"%(target_filename)s\"" + - u" with %(channels)d channel input") % - {"target_filename": filename(os.path.join(self.output_dir, - "00 - .flac")), + ERR_UNSUPPORTED_CHANNEL_COUNT % + {"target_filename": audiotools.Filename( + os.path.join(self.output_dir, "00 - .flac")), "channels": 10}) self.assertEqual(self.__run_app__(["track2track", @@ -1107,9 +1328,9 @@ unsupported_count_file.name, supported_track.filename]), 1) self.__check_error__( - _(u"Unable to write \"%(target_filename)s\"" + - u" with %(channels)d channel input") % - {"target_filename": filename(unsupported_count_file.name), + ERR_UNSUPPORTED_CHANNEL_COUNT % + {"target_filename": audiotools.Filename( + unsupported_count_file.name), "channels": 10}) finally: unsupported_count_file.close() @@ -1128,10 +1349,9 @@ self.output_dir, supported_track.filename]), 1) self.__check_error__( - _(u"Unable to write \"%(target_filename)s\"" + - u" with channel assignment \"%(assignment)s\"") % - {"target_filename": filename(os.path.join(self.output_dir, - "00 - .flac")), + ERR_UNSUPPORTED_CHANNEL_MASK % + {"target_filename": audiotools.Filename( + os.path.join(self.output_dir, "00 - .flac")), "assignment": audiotools.ChannelMask(0x3F000)}) self.assertEqual(self.__run_app__(["track2track", @@ -1139,9 +1359,9 @@ unsupported_mask_file.name, supported_track.filename]), 1) self.__check_error__( - _(u"Unable to write \"%(target_filename)s\"" + - u" with channel assignment \"%(assignment)s\"") % - {"target_filename": filename(unsupported_mask_file.name), + ERR_UNSUPPORTED_CHANNEL_MASK % + {"target_filename": audiotools.Filename( + unsupported_mask_file.name), "assignment": audiotools.ChannelMask(0x3F000)}) finally: unsupported_mask_file.close() @@ -1160,10 +1380,9 @@ self.output_dir, supported_track.filename]), 1) self.__check_error__( - _(u"Unable to write \"%(target_filename)s\"" + - u" with %(bps)d bits per sample") % - {"target_filename": filename(os.path.join(self.output_dir, - "00 - .shn")), + ERR_UNSUPPORTED_BITS_PER_SAMPLE % + {"target_filename": audiotools.Filename( + os.path.join(self.output_dir, "00 - .shn")), "bps": 24}) self.assertEqual(self.__run_app__(["track2track", @@ -1171,9 +1390,9 @@ unsupported_bps_file.name, supported_track.filename]), 1) self.__check_error__( - _(u"Unable to write \"%(target_filename)s\"" + - u" with %(bps)d bits per sample") % - {"target_filename": filename(unsupported_bps_file.name), + ERR_UNSUPPORTED_BITS_PER_SAMPLE % + {"target_filename": audiotools.Filename( + unsupported_bps_file.name), "bps": 24}) finally: unsupported_bps_file.close() @@ -1425,13 +1644,23 @@ @UTIL_TRACKCAT def test_options(self): - messenger = audiotools.Messenger("trackcat", None) + from audiotools.text import (ERR_FILES_REQUIRED, + ERR_BPS_MISMATCH, + ERR_CHANNEL_COUNT_MISMATCH, + ERR_SAMPLE_RATE_MISMATCH, + ERR_CUE_IOERROR, + ERR_CUE_MISSING_TAG, + ERR_DUPLICATE_FILE, + ERR_OUTPUT_IS_INPUT, + ERR_NO_OUTPUT_FILE, + ERR_UNSUPPORTED_AUDIO_TYPE, + ERR_UNSUPPORTED_COMPRESSION_MODE, + ERR_ENCODING_ERROR) #first, check the error conditions self.assertEqual( self.__run_app__(["trackcat", "-o", "fail.flac"]), 1) - self.__check_error__( - _(u"You must specify at least 1 supported audio file")) + self.__check_error__(ERR_FILES_REQUIRED) self.assertEqual( self.__run_app__(["trackcat", "-o", "fail.flac", @@ -1439,8 +1668,7 @@ self.track2.filename, self.track3.filename, self.track4.filename]), 1) - self.__check_error__( - _(u"All audio files must have the same bits per sample")) + self.__check_error__(ERR_BPS_MISMATCH) self.assertEqual( self.__run_app__(["trackcat", "-o", "fail.flac", @@ -1448,8 +1676,7 @@ self.track2.filename, self.track3.filename, self.track5.filename]), 1) - self.__check_error__( - _(u"All audio files must have the same channel count")) + self.__check_error__(ERR_CHANNEL_COUNT_MISMATCH) self.assertEqual( self.__run_app__(["trackcat", "-o", "fail.flac", @@ -1457,8 +1684,7 @@ self.track2.filename, self.track3.filename, self.track6.filename]), 1) - self.__check_error__( - _(u"All audio files must have the same sample rate")) + self.__check_error__(ERR_SAMPLE_RATE_MISMATCH) self.assertEqual( self.__run_app__(["trackcat", "--cue", "/dev/null/foo.cue", @@ -1466,7 +1692,7 @@ self.track1.filename, self.track2.filename, self.track3.filename]), 1) - self.__check_error__(_(u"Unable to read cuesheet")) + self.__check_error__(ERR_CUE_IOERROR) self.assertEqual( self.__run_app__(["trackcat", "--cue", self.invalid_cuesheet.name, @@ -1474,7 +1700,26 @@ self.track1.filename, self.track2.filename, self.track3.filename]), 1) - self.__check_error__(_(u"Missing tag at line 1")) + self.__check_error__(ERR_CUE_MISSING_TAG % (1)) + + self.assertEqual( + self.__run_app__(["trackcat", + "-o", self.suffix_outfile.name, + self.track1.filename, + self.track1.filename]), 0) + self.__check_warning__( + ERR_DUPLICATE_FILE % + (audiotools.Filename(self.track1.filename),)) + + self.assertEqual( + self.__run_app__(["trackcat", + "-o", self.track1.filename, + self.track1.filename, + self.track2.filename, + self.track3.filename]), 1) + self.__check_error__( + ERR_OUTPUT_IS_INPUT % + (audiotools.Filename(self.track1.filename),)) #then, check the option combinations #along with a few different output files and types @@ -1484,92 +1729,148 @@ outfile, count, options) in self.output_combinations(all_options): - if (os.path.isfile(outfile)): - f = open(outfile, "wb") - f.close() - - options = self.populate_options(options, - type, - quality, - outfile) + \ - [self.track1.filename, - self.track2.filename, - self.track3.filename] - - #check a few common errors - if ("-o" not in options): - self.assertEqual(self.__run_app__(["trackcat"] + options), - 1) + if (os.path.isfile(outfile)): + f = open(outfile, "wb") + f.close() - self.__check_error__(_(u"You must specify an output file")) - continue + options = self.populate_options(options, + type, + quality, + outfile) + \ + [self.track1.filename, + self.track2.filename, + self.track3.filename] + + #check a few common errors + if ("-o" not in options): + self.assertEqual(self.__run_app__(["trackcat"] + options), + 1) - if ("-t" in options): - output_format = audiotools.TYPE_MAP[type] - else: - try: - output_format = audiotools.filename_to_type(outfile) - except audiotools.UnknownAudioType: - self.assertEqual(self.__run_app__(["trackcat"] + - options), 1) + self.__check_error__(ERR_NO_OUTPUT_FILE) + continue - self.__check_error__(_(u"Unsupported audio type \"\"")) - continue + if ("-t" in options): + output_format = audiotools.TYPE_MAP[type] + else: + try: + output_format = audiotools.filename_to_type(outfile) + except audiotools.UnknownAudioType: + self.assertEqual(self.__run_app__(["trackcat"] + + options), 1) - if (("-q" in options) and - (quality not in output_format.COMPRESSION_MODES)): - self.assertEqual(self.__run_app__(["trackcat"] + options), - 1) self.__check_error__( - _(u"\"%(quality)s\" is not a supported " + - u"compression mode for type \"%(type)s\"") % - {"quality": quality, - "type": output_format.NAME.decode('ascii')}) + ERR_UNSUPPORTED_AUDIO_TYPE % (u"",)) continue - if (outfile.startswith("/dev/")): - self.assertEqual(self.__run_app__(["trackcat"] + options), - 1) - self.__check_error__( - _(u"%(filename)s: [Errno 20] Not a directory: '%(filename)s'") % - {"filename": messenger.filename(outfile)}) - continue + if (("-q" in options) and + (quality not in output_format.COMPRESSION_MODES)): + self.assertEqual(self.__run_app__(["trackcat"] + options), + 1) + self.__check_error__( + ERR_UNSUPPORTED_COMPRESSION_MODE % + {"quality": quality, + "type": output_format.NAME.decode('ascii')}) + continue + + if (outfile.startswith("/dev/")): + self.assertEqual(self.__run_app__(["trackcat"] + options), + 1) + self.__check_error__( + ERR_ENCODING_ERROR % (audiotools.Filename(outfile),)) + continue - #check that no PCM data is lost - self.assertEqual( - self.__run_app__(["trackcat"] + options), 0) - new_track = audiotools.open(outfile) - self.assertEqual(new_track.NAME, output_format.NAME) - self.assertEqual(new_track.total_frames(), 793800) - self.assert_(audiotools.pcm_frame_cmp( - new_track.to_pcm(), - audiotools.PCMCat(iter([track.to_pcm() for track in - [self.track1, - self.track2, - self.track3]]))) is None) - - #check that metadata is merged properly - metadata = new_track.get_metadata() - if (metadata is not None): - self.assertEqual(metadata.track_name, u"") - self.assertEqual(metadata.album_name, u"Album") - self.assertEqual(metadata.artist_name, u"Artist") - self.assertEqual(metadata.track_number, 0) - self.assertEqual(metadata.track_total, 3) - - #check that the cuesheet is embedded properly - if (("--cue" in options) and - (output_format is audiotools.FlacAudio)): - cuesheet = new_track.get_cuesheet() - self.assert_(cuesheet is not None) - self.assertEqual(cuesheet.ISRCs(), - {1: 'JPPI00652340', - 2: 'JPPI00652349', - 3: 'JPPI00652341'}) - self.assertEqual(list(cuesheet.indexes()), - [(0,), (225, 375), (675, 825)]) - self.assertEqual(cuesheet.pcm_lengths(793800), - [220500, 264600, 308700]) + #check that no PCM data is lost + self.assertEqual( + self.__run_app__(["trackcat"] + options), 0) + new_track = audiotools.open(outfile) + self.assertEqual(new_track.NAME, output_format.NAME) + self.assertEqual(new_track.total_frames(), 793800) + self.assert_(audiotools.pcm_frame_cmp( + new_track.to_pcm(), + audiotools.PCMCat([track.to_pcm() for track in + [self.track1, + self.track2, + self.track3]])) is None) + + #check that metadata is merged properly + metadata = new_track.get_metadata() + if (metadata is not None): + self.assertEqual(metadata.track_name, None) + self.assertEqual(metadata.album_name, u"Album") + self.assertEqual(metadata.artist_name, u"Artist") + self.assertEqual(metadata.track_number, None) + self.assertEqual(metadata.track_total, 3) + + #check that the cuesheet is embedded properly + if (("--cue" in options) and + (output_format is audiotools.FlacAudio)): + cuesheet = new_track.get_cuesheet() + self.assert_(cuesheet is not None) + self.assertEqual(cuesheet.ISRCs(), + {1: 'JPPI00652340', + 2: 'JPPI00652349', + 3: 'JPPI00652341'}) + self.assertEqual(list(cuesheet.indexes()), + [(0,), (225, 375), (675, 825)]) + self.assertEqual(cuesheet.pcm_lengths(793800, 44100), + [220500, 264600, 308700]) + + @UTIL_TRACKCAT + def test_unicode(self): + for (input_filenames, + output_path, + cuesheet_file) in Possibilities( + #check filename arguments + [["track%d.flac" % (i) for i in range(3)], + [(u'abc\xe0\xe7\xe8\u3041\u3044\u3046-%d.flac' % + (i)).encode('utf-8') for i in range(3)]], + #check output filename argument + ["output.flac", + u'abc\xe0\xe7\xe8\u3041\u3044\u3046-out.flac'.encode('utf-8')], + #check --cue argument + [None, + "cuesheet.cue", + u'abc\xe0\xe7\xe8\u3041\u3044\u3046.cue'.encode('utf-8')]): + + for input_filename in input_filenames: + if (os.path.isfile(input_filename)): + os.unlink(input_filename) + if (os.path.isfile(output_path)): + os.unlink(output_path) + if ((cuesheet_file is not None) and + os.path.isfile(cuesheet_file)): + os.unlink(cuesheet_file) + + tracks = [audiotools.FlacAudio.from_pcm( + input_filename, + EXACT_BLANK_PCM_Reader(pcm_frames)) + for (input_filename, pcm_frames) in + zip(input_filenames, [220500, 264600, 308700])] + + if (cuesheet_file is not None): + f = open(cuesheet_file, "wb") + f.write('FILE "CDImage.wav" WAVE\r\n TRACK 01 AUDIO\r\n ISRC JPPI00652340\r\n INDEX 01 00:00:00\r\n TRACK 02 AUDIO\r\n ISRC JPPI00652349\r\n INDEX 00 00:03:00\r\n INDEX 01 00:05:00\r\n TRACK 03 AUDIO\r\n ISRC JPPI00652341\r\n INDEX 00 00:9:00\r\n INDEX 01 00:11:00\r\n') + f.close() + + self.assertEqual( + self.__run_app__( + ["trackcat"] + input_filenames + + ([cuesheet_file] if cuesheet_file is not None else []) + + ["--output", output_path]), 0) + + self.assertEqual( + audiotools.pcm_frame_cmp( + audiotools.PCMCat([t.to_pcm() for t in tracks]), + audiotools.open(output_path).to_pcm()), None) + + for input_filename in input_filenames: + if (os.path.isfile(input_filename)): + os.unlink(input_filename) + if (os.path.isfile(output_path)): + os.unlink(output_path) + if ((cuesheet_file is not None) and + os.path.isfile(cuesheet_file)): + os.unlink(cuesheet_file) class trackcmp(UtilTest): @@ -1651,7 +1952,11 @@ @UTIL_TRACKCMP def test_combinations(self): - msg = audiotools.VerboseMessenger("trackcmp") + from audiotools.text import (LAB_TRACKCMP_CMP, + LAB_TRACKCMP_MISMATCH, + LAB_TRACKCMP_TYPE_MISMATCH, + LAB_TRACKCMP_OK, + LAB_TRACKCMP_MISSING) #check matching file against maching file self.assertEqual( @@ -1659,17 +1964,25 @@ self.match_file1.name, self.match_file2.name]), 0) + #check matching file against itself + self.assertEqual( + self.__run_app__(["trackcmp", "-V", "normal", + self.match_file1.name, self.match_file1.name]), + 0) + #check matching file against mismatching file self.assertEqual( self.__run_app__(["trackcmp", "-V", "normal", self.match_file1.name, self.mismatch_file.name]), 1) self.__check_info__( - _(u"%(path1)s <> %(path2)s : %(result)s") % { - "path1": msg.filename(self.match_file1.name), - "path2": msg.filename(self.mismatch_file.name), - "result": _(u"differ at PCM frame %(frame_number)d") % - {"frame_number": 1}}) + (LAB_TRACKCMP_CMP % + {"file1":audiotools.Filename(self.match_file1.name), + "file2":audiotools.Filename(self.mismatch_file.name)}) + + u" : " + + (LAB_TRACKCMP_MISMATCH % + {"frame_number": 1})) + #(ANSI output won't be generated because stdout isn't a TTY) #check matching file against missing file @@ -1678,17 +1991,17 @@ self.match_file1.name, "/dev/null/foo"]), 1) self.__check_error__( - _(u"%(path1)s <> %(path2)s : %(result)s") % { - "path1": msg.filename(self.match_file1.name), - "path2": msg.filename("/dev/null/foo"), - "result": _(u"must be either files or directories")}) + (LAB_TRACKCMP_CMP % + {"file1":audiotools.Filename(self.match_file1.name), + "file2":audiotools.Filename("/dev/null/foo")}) + + u" : " + LAB_TRACKCMP_TYPE_MISMATCH) #check matching file against broken file self.assertEqual( self.__run_app__(["trackcmp", "-V", "normal", self.match_file1.name, self.broken_file.name]), 1) - self.__check_error__(_(u"EOF reading frame")) + self.__check_error__(u"EOF reading frame") #check file against directory self.assertEqual( @@ -1696,10 +2009,10 @@ self.match_file1.name, self.match_dir1]), 1) self.__check_error__( - _(u"%(path1)s <> %(path2)s : %(result)s") % { - "path1": msg.filename(self.match_file1.name), - "path2": msg.filename(self.match_dir1), - "result": _(u"must be either files or directories")}) + (LAB_TRACKCMP_CMP % + {"file1":audiotools.Filename(self.match_file1.name), + "file2":audiotools.Filename(self.match_dir1)}) + + u" : " + LAB_TRACKCMP_TYPE_MISMATCH) #check directory against file self.assertEqual( @@ -1707,10 +2020,10 @@ self.match_dir1, self.match_file1.name]), 1) self.__check_error__( - _(u"%(path1)s <> %(path2)s : %(result)s") % { - "path1": msg.filename(self.match_dir1), - "path2": msg.filename(self.match_file1.name), - "result": _(u"must be either files or directories")}) + (LAB_TRACKCMP_CMP % + {"file1":audiotools.Filename(self.match_dir1), + "file2":audiotools.Filename(self.match_file1.name)}) + + u" : " + LAB_TRACKCMP_TYPE_MISMATCH) #check matching directory against matching directory self.assertEqual( @@ -1719,14 +2032,40 @@ 0) for i in xrange(1, 4): self.__check_info__( - _(u"%(path1)s <> %(path2)s : %(result)s") % { - "path1": msg.filename( - os.path.join(self.match_dir1, - "%2.2d.%s" % (i, self.type.SUFFIX))), - "path2": msg.filename( - os.path.join(self.match_dir2, - "%2.2d.%s" % (i, self.type.SUFFIX))), - "result": _(u"OK")}) + audiotools.output_progress( + (LAB_TRACKCMP_CMP % + {"file1":audiotools.Filename( + os.path.join(self.match_dir1, + "%2.2d.%s" % + (i, self.type.SUFFIX))), + "file2":audiotools.Filename( + os.path.join(self.match_dir2, + "%2.2d.%s" % + (i, self.type.SUFFIX)))}) + + u" : " + + LAB_TRACKCMP_OK, + i, 3)) + + #check matching directory against itself + self.assertEqual( + self.__run_app__(["trackcmp", "-V", "normal", "-j", "1", + self.match_dir1, self.match_dir1]), + 0) + for i in xrange(1, 4): + self.__check_info__( + audiotools.output_progress( + (LAB_TRACKCMP_CMP % + {"file1":audiotools.Filename( + os.path.join(self.match_dir1, + "%2.2d.%s" % + (i, self.type.SUFFIX))), + "file2":audiotools.Filename( + os.path.join(self.match_dir1, + "%2.2d.%s" % + (i, self.type.SUFFIX)))}) + + u" : " + + LAB_TRACKCMP_OK, + i, 3)) #check matching directory against mismatching directory self.assertEqual( @@ -1735,15 +2074,20 @@ 1) for i in xrange(1, 4): self.__check_info__( - _(u"%(path1)s <> %(path2)s : %(result)s") % { - "path1": msg.filename( - os.path.join(self.match_dir1, - "%2.2d.%s" % (i, self.type.SUFFIX))), - "path2": msg.filename( - os.path.join(self.mismatch_dir1, - "%2.2d.%s" % (i, self.type.SUFFIX))), - "result": _(u"differ at PCM frame %(frame_number)d" % - {"frame_number": 1})}) + audiotools.output_progress( + (LAB_TRACKCMP_CMP % + {"file1":audiotools.Filename( + os.path.join(self.match_dir1, + "%2.2d.%s" % + (i, self.type.SUFFIX))), + "file2":audiotools.Filename( + os.path.join(self.mismatch_dir1, + "%2.2d.%s" % + (i, self.type.SUFFIX)))}) + + u" : " + + (LAB_TRACKCMP_MISMATCH % + {"frame_number": 1}), + i, 3)) #check matching directory against directory missing file self.assertEqual( @@ -1751,20 +2095,26 @@ self.match_dir1, self.mismatch_dir2]), 1) self.__check_info__( - _(u"%(path)s : %(result)s") % { - "path": os.path.join(self.mismatch_dir2, - "track %2.2d" % (3)), - "result": _(u"missing")}) - for i in xrange(1, 2): + os.path.join(self.mismatch_dir2, + "track %2.2d" % (3)) + + u" : " + + LAB_TRACKCMP_MISSING) + + for i in xrange(1, 3): self.__check_info__( - _(u"%(path1)s <> %(path2)s : %(result)s") % { - "path1": msg.filename( - os.path.join(self.match_dir1, - "%2.2d.%s" % (i, self.type.SUFFIX))), - "path2": msg.filename( - os.path.join(self.mismatch_dir2, - "%2.2d.%s" % (i, self.type.SUFFIX))), - "result": _(u"OK")}) + audiotools.output_progress( + (LAB_TRACKCMP_CMP % + {"file1":audiotools.Filename( + os.path.join(self.match_dir1, + "%2.2d.%s" % + (i, self.type.SUFFIX))), + "file2":audiotools.Filename( + os.path.join(self.mismatch_dir2, + "%2.2d.%s" % + (i, self.type.SUFFIX)))}) + + u" : " + + LAB_TRACKCMP_OK, + i, 2)) #check matching directory against directory with extra file self.assertEqual( @@ -1772,20 +2122,59 @@ self.match_dir1, self.mismatch_dir3]), 1) self.__check_info__( - _(u"%(path)s : %(result)s") % { - "path": os.path.join(self.match_dir1, - "track %2.2d" % (4)), - "result": _(u"missing")}) - for i in xrange(1, 3): + os.path.join(self.match_dir1, + "track %2.2d" % (4)) + + u" : " + + LAB_TRACKCMP_MISSING) + + for i in xrange(1, 4): self.__check_info__( - _(u"%(path1)s <> %(path2)s : %(result)s") % { - "path1": msg.filename( - os.path.join(self.match_dir1, - "%2.2d.%s" % (i, self.type.SUFFIX))), - "path2": msg.filename( - os.path.join(self.mismatch_dir3, - "%2.2d.%s" % (i, self.type.SUFFIX))), - "result": _(u"OK")}) + audiotools.output_progress( + (LAB_TRACKCMP_CMP % + {"file1":audiotools.Filename( + os.path.join(self.match_dir1, + "%2.2d.%s" % + (i, self.type.SUFFIX))), + "file2":audiotools.Filename( + os.path.join(self.mismatch_dir3, + "%2.2d.%s" % + (i, self.type.SUFFIX)))}) + + u" : " + + LAB_TRACKCMP_OK, + i, 3)) + + @UTIL_TRACKCMP + def test_unicode(self): + for (file1, file2) in Possibilities( + ["file1.flac", + u'abc\xe0\xe7\xe8\u3041\u3044\u3046-1.flac'.encode('utf-8')], + ["file2.flac", + u'abc\xe0\xe7\xe8\u3041\u3044\u3046-2.flac'.encode('utf-8')]): + if (os.path.isfile(file1)): + os.unlink(file1) + if (os.path.isfile(file2)): + os.unlink(file2) + + track1 = audiotools.FlacAudio.from_pcm( + file1, + BLANK_PCM_Reader(1)) + track2 = audiotools.FlacAudio.from_pcm( + file2, + BLANK_PCM_Reader(1)) + + self.assertEqual( + self.__run_app__( + ["trackcmp", file1, file2]), 0) + + self.assertEqual( + audiotools.pcm_frame_cmp( + track1.to_pcm(), + track2.to_pcm()), None) + + if (os.path.isfile(file1)): + os.unlink(file1) + if (os.path.isfile(file2)): + os.unlink(file2) class trackinfo(UtilTest): @@ -1829,16 +2218,16 @@ def test_trackinfo(self): import re import StringIO + from audiotools.text import (LAB_TRACKINFO_CHANNELS, + LAB_TRACKINFO_CHANNEL, + MASK_FRONT_LEFT, + MASK_FRONT_RIGHT) all_options = ["-n", "-L", "-b", "-%", "-C"] for track in self.metadata_tracks: for count in xrange(1, len(all_options) + 1): for options in Combinations(all_options, count): - # print track.filename,repr(options) - # if (self.__run_app__(["trackinfo"] + options + [track.filename]) != 0): - # print self.stderr.getvalue() - # self.assert_(False) self.assertEqual( self.__run_app__( ["trackinfo"] + options + [track.filename]), 0) @@ -1855,17 +2244,15 @@ (track.filename), line) is not None) else: self.assert_( - re.match(r'\d+:\d+ 2ch 44100Hz 16-bit: %s\n' % + re.match(r'\d+:\d+ 2ch 44.1kHz 16-bit: %s\n' % (track.filename), line) is not None) #check metadata/low-level metadata if -n not present if ("-n" not in options): if ("-L" not in options): - self.__check_output__(u" Title : a") - self.__check_output__(u" Artist : c") - self.__check_output__(u" Album : b") - self.__check_output__(u"Track # : 1/2") - self.__check_output__(u"Comment : d") + for line in StringIO.StringIO( + unicode(track.get_metadata())): + self.__check_output__(line.rstrip('\r\n')) else: for line in StringIO.StringIO( track.get_metadata().raw_info()): @@ -1878,9 +2265,33 @@ #check channel assignment if -C present if ("-C" in options): - self.__check_output__(_(u"Assigned Channels:")) - self.__check_output__(_(u"channel 1 - Front Left")) - self.__check_output__(_(u"channel 2 - Front Right")) + self.__check_output__(LAB_TRACKINFO_CHANNELS) + self.__check_output__( + LAB_TRACKINFO_CHANNEL % + {"channel_number":1, + "channel_name":MASK_FRONT_LEFT}) + self.__check_output__( + LAB_TRACKINFO_CHANNEL % + {"channel_number":2, + "channel_name":MASK_FRONT_RIGHT}) + + @UTIL_TRACKINFO + def test_unicode(self): + for filename in [ + "track.flac", + u'abc\xe0\xe7\xe8\u3041\u3044\u3046.flac'.encode('utf-8')]: + if (os.path.isfile(filename)): + os.unlink(filename) + + track = audiotools.FlacAudio.from_pcm( + filename, + BLANK_PCM_Reader(1)) + + self.assertEqual( + self.__run_app__(["trackinfo", filename]), 0) + + if (os.path.isfile(filename)): + os.unlink(filename) class tracklength(UtilTest): @@ -1895,6 +2306,11 @@ @UTIL_TRACKLENGTH def test_tracklength(self): import shutil + from audiotools.text import (LAB_TRACKLENGTH_FILE_FORMAT, + LAB_TRACKLENGTH_FILE_COUNT, + LAB_TRACKLENGTH_FILE_LENGTH, + LAB_TRACKLENGTH_FILE_SIZE, + LAB_TRACKLENGTH) track1 = audiotools.open("1s.flac") track2 = audiotools.open("1m.flac") @@ -1903,23 +2319,124 @@ self.assertEqual(track2.seconds_length(), 60) self.assertEqual(track3.seconds_length(), 60 * 60) self.assertEqual(self.__run_app__(["tracklength", "1s.flac"]), 0) - self.__check_output__(u"0:00:01") + self.__check_output__(u"%6s %5s %7s %4s" % + (LAB_TRACKLENGTH_FILE_FORMAT, + LAB_TRACKLENGTH_FILE_COUNT, + LAB_TRACKLENGTH_FILE_LENGTH, + LAB_TRACKLENGTH_FILE_SIZE)) + self.__check_output__(u"%s %s %s %s" % + (u"-" * 6, + u"-" * 5, + u"-" * 7, + u"-" * 4)) + self.__check_output__(u"%6s %5s %7s %4s" % + (u"flac", + 1, + LAB_TRACKLENGTH % {"hours":0, + "minutes":0, + "seconds":1}, + 380)) + self.assertEqual(self.__run_app__(["tracklength", "1s.flac", "1s.flac"]), 0) - self.__check_output__(u"0:00:02") + self.__check_output__(u"%6s %5s %7s %4s" % + (LAB_TRACKLENGTH_FILE_FORMAT, + LAB_TRACKLENGTH_FILE_COUNT, + LAB_TRACKLENGTH_FILE_LENGTH, + LAB_TRACKLENGTH_FILE_SIZE)) + self.__check_output__(u"%s %s %s %s" % + (u"-" * 6, + u"-" * 5, + u"-" * 7, + u"-" * 4)) + self.__check_output__(u"%6s %5s %7s %4s" % + (u"flac", + 2, + LAB_TRACKLENGTH % {"hours":0, + "minutes":0, + "seconds":2}, + 760)) + self.assertEqual(self.__run_app__(["tracklength", "1s.flac", "1m.flac"]), 0) - self.__check_output__(u"0:01:01") + self.__check_output__(u"%6s %5s %7s %4s" % + (LAB_TRACKLENGTH_FILE_FORMAT, + LAB_TRACKLENGTH_FILE_COUNT, + LAB_TRACKLENGTH_FILE_LENGTH, + LAB_TRACKLENGTH_FILE_SIZE)) + self.__check_output__(u"%s %s %s %s" % + (u"-" * 6, + u"-" * 5, + u"-" * 7, + u"-" * 4)) + self.__check_output__(u"%6s %5s %7s %4s" % + (u"flac", + 2, + LAB_TRACKLENGTH % {"hours":0, + "minutes":1, + "seconds":1}, + u"9.8K")) + self.assertEqual(self.__run_app__(["tracklength", "1s.flac", "1m.flac", "1m.flac"]), 0) - self.__check_output__(u"0:02:01") + self.__check_output__(u"%6s %5s %7s %5s" % + (LAB_TRACKLENGTH_FILE_FORMAT, + LAB_TRACKLENGTH_FILE_COUNT, + LAB_TRACKLENGTH_FILE_LENGTH, + LAB_TRACKLENGTH_FILE_SIZE)) + self.__check_output__(u"%s %s %s %s" % + (u"-" * 6, + u"-" * 5, + u"-" * 7, + u"-" * 5)) + self.__check_output__(u"%6s %5s %7s %5s" % + (u"flac", + 3, + LAB_TRACKLENGTH % {"hours":0, + "minutes":2, + "seconds":1}, + u"19.1K")) + self.assertEqual(self.__run_app__(["tracklength", "1s.flac", "1m.flac", "1h.flac"]), 0) - self.__check_output__(u"1:01:01") + self.__check_output__(u"%6s %5s %7s %5s" % + (LAB_TRACKLENGTH_FILE_FORMAT, + LAB_TRACKLENGTH_FILE_COUNT, + LAB_TRACKLENGTH_FILE_LENGTH, + LAB_TRACKLENGTH_FILE_SIZE)) + self.__check_output__(u"%s %s %s %s" % + (u"-" * 6, + u"-" * 5, + u"-" * 7, + u"-" * 5)) + self.__check_output__(u"%6s %5s %7s %5s" % + (u"flac", + 3, + LAB_TRACKLENGTH % {"hours":1, + "minutes":1, + "seconds":1}, + u"22.5K")) + self.assertEqual(self.__run_app__(["tracklength", "1s.flac", "1m.flac", "1h.flac", "1h.flac"]), 0) - self.__check_output__(u"2:01:01") + self.__check_output__(u"%6s %5s %7s %5s" % + (LAB_TRACKLENGTH_FILE_FORMAT, + LAB_TRACKLENGTH_FILE_COUNT, + LAB_TRACKLENGTH_FILE_LENGTH, + LAB_TRACKLENGTH_FILE_SIZE)) + self.__check_output__(u"%s %s %s %s" % + (u"-" * 6, + u"-" * 5, + u"-" * 7, + u"-" * 5)) + self.__check_output__(u"%6s %5s %7s %5s" % + (u"flac", + 4, + LAB_TRACKLENGTH % {"hours":2, + "minutes":1, + "seconds":1}, + u"35.3K")) tempdir = tempfile.mkdtemp() try: @@ -1927,12 +2444,46 @@ shutil.copy(track2.filename, tempdir) shutil.copy(track3.filename, tempdir) self.assertEqual(self.__run_app__(["tracklength", tempdir]), 0) - self.__check_output__(u"1:01:01") + self.__check_output__(u"%6s %5s %7s %5s" % + (LAB_TRACKLENGTH_FILE_FORMAT, + LAB_TRACKLENGTH_FILE_COUNT, + LAB_TRACKLENGTH_FILE_LENGTH, + LAB_TRACKLENGTH_FILE_SIZE)) + self.__check_output__(u"%s %s %s %s" % + (u"-" * 6, + u"-" * 5, + u"-" * 7, + u"-" * 5)) + self.__check_output__(u"%6s %5s %7s %5s" % + (u"flac", + 3, + LAB_TRACKLENGTH % {"hours":1, + "minutes":1, + "seconds":1}, + u"22.5K")) finally: for f in os.listdir(tempdir): os.unlink(os.path.join(tempdir, f)) os.rmdir(tempdir) + @UTIL_TRACKLENGTH + def test_unicode(self): + for filename in [ + "track.flac", + u'abc\xe0\xe7\xe8\u3041\u3044\u3046.flac'.encode('utf-8')]: + if (os.path.isfile(filename)): + os.unlink(filename) + + track = audiotools.FlacAudio.from_pcm( + filename, + BLANK_PCM_Reader(1)) + + self.assertEqual( + self.__run_app__(["tracklength", filename]), 0) + + if (os.path.isfile(filename)): + os.unlink(filename) + class tracklint(UtilTest): @UTIL_TRACKLINT @@ -1976,7 +2527,7 @@ metadata = track.get_metadata() if (isinstance(metadata, audiotools.FlacMetaData)): metadata = metadata.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID) + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID) self.assertEqual(metadata, bad_vorbiscomment) for (key, value) in metadata.items(): self.assertEqual(value, bad_vorbiscomment[key]) @@ -2003,7 +2554,7 @@ metadata = track.get_metadata() if (isinstance(metadata, audiotools.FlacMetaData)): metadata = metadata.get_block( - audiotools.Flac_VORBISCOMMENT.BLOCK_ID) + audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID) self.assertEqual(metadata, bad_vorbiscomment) self.assertNotEqual(metadata, fixed) for (key, value) in metadata.items(): @@ -2163,12 +2714,12 @@ def test_apev2(self): for audio_class in [audiotools.WavPackAudio]: bad_apev2 = audiotools.ApeTag( - [audiotools.ApeTagItem(0, False, "Title", "Track Name "), - audiotools.ApeTagItem(0, False, "Track", "02"), - audiotools.ApeTagItem(0, False, "Artist", " Some Artist"), - audiotools.ApeTagItem(0, False, "Catalog", ""), - audiotools.ApeTagItem(0, False, "Year", " "), - audiotools.ApeTagItem(0, False, "Comment", " Some Comment ")]) + [audiotools.ape.ApeTagItem(0, False, "Title", "Track Name "), + audiotools.ape.ApeTagItem(0, False, "Track", "02"), + audiotools.ape.ApeTagItem(0, False, "Artist", " Some Artist"), + audiotools.ape.ApeTagItem(0, False, "Catalog", ""), + audiotools.ape.ApeTagItem(0, False, "Year", " "), + audiotools.ape.ApeTagItem(0, False, "Comment", " Some Comment ")]) fixed = audiotools.MetaData( track_name=u"Track Name", @@ -2312,19 +2863,19 @@ def test_id3v22(self): self.__id3_text__( audiotools.ID3v22Comment( - [audiotools.ID3v22_T__Frame.converted( + [audiotools.id3.ID3v22_T__Frame.converted( "TT2", u"Track Name "), - audiotools.ID3v22_T__Frame.converted( + audiotools.id3.ID3v22_T__Frame.converted( "TRK", u"02"), - audiotools.ID3v22_T__Frame.converted( + audiotools.id3.ID3v22_T__Frame.converted( "TPA", u"003"), - audiotools.ID3v22_T__Frame.converted( + audiotools.id3.ID3v22_T__Frame.converted( "TP1", u" Some Artist\u0000"), - audiotools.ID3v22_T__Frame.converted( + audiotools.id3.ID3v22_T__Frame.converted( "TRC", u""), - audiotools.ID3v22_T__Frame.converted( + audiotools.id3.ID3v22_T__Frame.converted( "TYE", u""), - audiotools.ID3v22_COM_Frame.converted( + audiotools.id3.ID3v22_COM_Frame.converted( "COM", u" Some Comment ")])) #ID3v2.2 doesn't store most image fields internally @@ -2334,19 +2885,19 @@ def test_id3v23(self): self.__id3_text__( audiotools.ID3v23Comment( - [audiotools.ID3v23_T___Frame.converted( + [audiotools.id3.ID3v23_T___Frame.converted( "TIT2", u"Track Name "), - audiotools.ID3v23_T___Frame.converted( + audiotools.id3.ID3v23_T___Frame.converted( "TRCK", u"02"), - audiotools.ID3v23_T___Frame.converted( + audiotools.id3.ID3v23_T___Frame.converted( "TPOS", u"003"), - audiotools.ID3v23_T___Frame.converted( + audiotools.id3.ID3v23_T___Frame.converted( "TPE1", u" Some Artist\u0000"), - audiotools.ID3v23_T___Frame.converted( + audiotools.id3.ID3v23_T___Frame.converted( "TYER", u""), - audiotools.ID3v23_T___Frame.converted( + audiotools.id3.ID3v23_T___Frame.converted( "TCOP", u""), - audiotools.ID3v23_COMM_Frame.converted( + audiotools.id3.ID3v23_COMM_Frame.converted( "COMM", u" Some Comment ")])) good_image = audiotools.Image.new(TEST_COVER1, u"Description", 0) @@ -2368,19 +2919,19 @@ def test_id3v24(self): self.__id3_text__( audiotools.ID3v24Comment( - [audiotools.ID3v24_T___Frame.converted( + [audiotools.id3.ID3v24_T___Frame.converted( "TIT2", u"Track Name "), - audiotools.ID3v24_T___Frame.converted( + audiotools.id3.ID3v24_T___Frame.converted( "TRCK", u"02"), - audiotools.ID3v24_T___Frame.converted( + audiotools.id3.ID3v24_T___Frame.converted( "TPOS", u"003"), - audiotools.ID3v24_T___Frame.converted( + audiotools.id3.ID3v24_T___Frame.converted( "TPE1", u" Some Artist\u0000"), - audiotools.ID3v24_T___Frame.converted( + audiotools.id3.ID3v24_T___Frame.converted( "TYER", u""), - audiotools.ID3v24_T___Frame.converted( + audiotools.id3.ID3v24_T___Frame.converted( "TCOP", u""), - audiotools.ID3v24_COMM_Frame.converted( + audiotools.id3.ID3v24_COMM_Frame.converted( "COMM", u" Some Comment ")])) good_image = audiotools.Image.new(TEST_COVER1, u"Description", 0) @@ -2400,6 +2951,8 @@ @UTIL_TRACKLINT def test_mp3(self): + from audiotools.text import (ERR_ENCODING_ERROR) + track_file = tempfile.NamedTemporaryFile( suffix="." + audiotools.MP3Audio.SUFFIX) track_file_stat = os.stat(track_file.name)[0] @@ -2422,15 +2975,15 @@ ["tracklint", "--fix", "--db", undo_db, track.filename]), 1) - self.__check_error__(_(u"Unable to write \"%s\"") % \ - (self.filename(track.filename))) + self.__check_error__(ERR_ENCODING_ERROR % + (audiotools.Filename(track.filename),)) #no undo DB, unwritable file self.assertEqual(self.__run_app__( ["tracklint", "--fix", track.filename]), 1) - self.__check_error__(_(u"Unable to write \"%s\"") % \ - (self.filename(track.filename))) + self.__check_error__(ERR_ENCODING_ERROR % + (audiotools.Filename(track.filename),)) finally: os.chmod(track_file.name, track_file_stat) track_file.close() @@ -2441,14 +2994,14 @@ @UTIL_TRACKLINT def test_m4a(self): - from audiotools import M4A_Tree_Atom - from audiotools import M4A_META_Atom - from audiotools import M4A_HDLR_Atom - from audiotools import M4A_ILST_Leaf_Atom - from audiotools import M4A_ILST_Unicode_Data_Atom - from audiotools import M4A_ILST_TRKN_Data_Atom - from audiotools import M4A_ILST_DISK_Data_Atom - from audiotools import M4A_FREE_Atom + from audiotools.m4a import M4A_Tree_Atom + from audiotools.m4a import M4A_META_Atom + from audiotools.m4a import M4A_HDLR_Atom + from audiotools.m4a import M4A_ILST_Leaf_Atom + from audiotools.m4a import M4A_ILST_Unicode_Data_Atom + from audiotools.m4a import M4A_ILST_TRKN_Data_Atom + from audiotools.m4a import M4A_ILST_DISK_Data_Atom + from audiotools.m4a import M4A_FREE_Atom bad_m4a = M4A_META_Atom( 0, 0, @@ -2492,7 +3045,9 @@ fixed = audiotools.MetaData( track_name=u"Track Name", track_number=2, + track_total=None, album_number=3, + album_total=None, artist_name=u"Some Artist", comment=u"Some Comment") @@ -2555,7 +3110,8 @@ BLANK_PCM_Reader(5)) metadata = audiotools.MetaData( track_name="Track Name", - track_number=1) + track_number=1, + track_total=2) track.set_metadata(metadata) if (track.get_metadata() is not None): orig_stat = os.stat(track.filename) @@ -2597,7 +3153,8 @@ BLANK_PCM_Reader(5)) metadata = audiotools.MetaData( track_name="Track Name", - track_number=1) + track_number=1, + track_total=2) track.set_metadata(metadata) if (track.get_metadata() is not None): orig_stat = os.stat(track.filename) @@ -2628,7 +3185,61 @@ track_file.close() @UTIL_TRACKLINT + def test_unicode(self): + for (input_filename, + backup_database) in Possibilities( + ["track.flac", + u'abc\xe0\xe7\xe8\u3041\u3044\u3046.flac'.encode('utf-8')], + ["undo.db", + u'abc\xe0\xe7\xe8\u3041\u3044\u3046.db'.encode('utf-8')]): + if (os.path.isfile(input_filename)): + os.unlink(input_filename) + if (os.path.isfile(backup_database)): + os.unlink(backup_database) + + track = audiotools.FlacAudio.from_pcm( + input_filename, + BLANK_PCM_Reader(1)) + + metadata = track.get_metadata() + metadata.track_name = u"Track Name " + track.update_metadata(metadata) + + self.assertEqual( + audiotools.open(input_filename).get_metadata().track_name, + u"Track Name ") + + self.assertEqual( + self.__run_app__(["tracklint", + "--fix", + "--db", backup_database, + input_filename]), 0) + + self.assertEqual( + audiotools.open(input_filename).get_metadata().track_name, + u"Track Name") + + self.assertEqual( + self.__run_app__(["tracklint", + "--undo", + "--db", backup_database, + input_filename]), 0) + + self.assertEqual( + audiotools.open(input_filename).get_metadata().track_name, + u"Track Name ") + + if (os.path.isfile(input_filename)): + os.unlink(input_filename) + if (os.path.isfile(backup_database)): + os.unlink(backup_database) + + @UTIL_TRACKLINT def test_errors1(self): + from audiotools.text import (ERR_NO_UNDO_DB, + ERR_OPEN_IOERROR, + ERR_ENCODING_ERROR) + for audio_class in audiotools.AVAILABLE_TYPES: track_file = tempfile.NamedTemporaryFile( suffix="." + audio_class.SUFFIX) @@ -2642,39 +3253,44 @@ BLANK_PCM_Reader(5)) track.set_metadata(audiotools.MetaData( track_name=u"Track Name ", - track_number=1)) + track_number=1, + track_total=2)) #general-purpose errors self.assertEqual(self.__run_app__( ["tracklint", "--undo", track.filename]), 1) - self.__check_error__(_(u"Cannot perform undo without undo db")) + self.__check_error__(ERR_NO_UNDO_DB) self.assertEqual(self.__run_app__( ["tracklint", "--fix", "--db", "/dev/null/foo.db", track.filename]), 1) - self.__check_error__(_(u"Unable to open \"%s\"") % \ - (self.filename("/dev/null/foo.db"))) + self.__check_error__( + ERR_OPEN_IOERROR % + (audiotools.Filename("/dev/null/foo.db"),)) self.assertEqual(self.__run_app__( ["tracklint", "--undo", "--db", "/dev/null/foo.db", track.filename]), 1) - self.__check_error__(_(u"Unable to open \"%s\"") % \ - (self.filename("/dev/null/foo.db"))) + self.__check_error__( + ERR_OPEN_IOERROR % + (audiotools.Filename("/dev/null/foo.db"),)) if (track.get_metadata() is not None): #unwritable undo DB, writable file self.assertEqual(self.__run_app__( ["tracklint", "--fix", "--db", "/dev/null/undo.db", track.filename]), 1) - self.__check_error__(_(u"Unable to open \"%s\"") % - (self.filename("/dev/null/undo.db"))) + self.__check_error__( + ERR_OPEN_IOERROR % + (audiotools.Filename("/dev/null/undo.db"),)) self.assertEqual(self.__run_app__( ["tracklint", "--undo", "--db", "/dev/null/undo.db", track.filename]), 1) - self.__check_error__(_(u"Unable to open \"%s\"") % - (self.filename("/dev/null/undo.db"))) + self.__check_error__( + ERR_OPEN_IOERROR % + (audiotools.Filename("/dev/null/undo.db"),)) #unwritable undo DB, unwritable file os.chmod(track.filename, track_file_stat & 0x7555) @@ -2682,15 +3298,17 @@ self.assertEqual(self.__run_app__( ["tracklint", "--fix", "--db", "/dev/null/undo.db", track.filename]), 1) - self.__check_error__(_(u"Unable to open \"%s\"") % - (self.filename("/dev/null/undo.db"))) + self.__check_error__( + ERR_OPEN_IOERROR % + (audiotools.Filename("/dev/null/undo.db"),)) self.assertEqual(self.__run_app__( ["tracklint", "--undo", "--db", "/dev/null/undo.db", track.filename]), 1) - self.__check_error__(_(u"Unable to open \"%s\"") % - (self.filename("/dev/null/undo.db"))) + self.__check_error__( + ERR_OPEN_IOERROR % + (audiotools.Filename("/dev/null/undo.db"),)) #restore from DB to unwritable file os.chmod(track.filename, track_file_stat) @@ -2701,8 +3319,9 @@ self.assertEqual(self.__run_app__( ["tracklint", "--undo", "--db", undo_db, track.filename]), 1) - self.__check_error__(_(u"Unable to write \"%s\"") % - (self.filename(track.filename))) + self.__check_error__( + ERR_ENCODING_ERROR % + (audiotools.Filename(track.filename),)) finally: os.chmod(track_file.name, track_file_stat) @@ -2714,6 +3333,8 @@ @UTIL_TRACKLINT def test_errors2(self): + from audiotools.text import (ERR_ENCODING_ERROR) + for audio_class in audiotools.AVAILABLE_TYPES: track_file = tempfile.NamedTemporaryFile( suffix="." + audio_class.SUFFIX) @@ -2737,10 +3358,9 @@ ["tracklint", "--fix", "--db", undo_db, track.filename]), 1) - self.__check_error__(_(u"Unable to write \"%s\"") % - (self.filename(track.filename))) - - + self.__check_error__( + ERR_ENCODING_ERROR % + (audiotools.Filename(track.filename),)) finally: os.chmod(track_file.name, track_file_stat) track_file.close() @@ -2777,53 +3397,20 @@ self.track_file = tempfile.NamedTemporaryFile() - self.cuesheet = tempfile.NamedTemporaryFile(suffix=".cue") - self.cuesheet.write('FILE "CDImage.wav" WAVE\r\n TRACK 01 AUDIO\r\n ISRC JPPI00652340\r\n INDEX 01 00:00:00\r\n TRACK 02 AUDIO\r\n ISRC JPPI00652349\r\n INDEX 00 03:40:72\r\n INDEX 01 03:42:27\r\n TRACK 03 AUDIO\r\n ISRC JPPI00652341\r\n INDEX 00 07:22:45\r\n INDEX 01 07:24:37\r\n') - self.cuesheet.flush() - self.comment_file = tempfile.NamedTemporaryFile(suffix=".txt") self.comment_file.write("Comment File") self.comment_file.flush() - self.front_cover = tempfile.NamedTemporaryFile(suffix=".png") - self.front_cover.write(TEST_COVER4) - self.front_cover.flush() - - self.back_cover = tempfile.NamedTemporaryFile(suffix=".png") - self.back_cover.write(TEST_COVER2) - self.back_cover.flush() - - self.front_cover_image = audiotools.Image.new( - TEST_COVER4, u"", 0) - self.back_cover_image = audiotools.Image.new( - TEST_COVER2, u"", 1) - - self.thumbnailed_front_cover_image = self.front_cover_image.thumbnail( - audiotools.THUMBNAIL_SIZE, - audiotools.THUMBNAIL_SIZE, - audiotools.THUMBNAIL_FORMAT) - - self.thumbnailed_back_cover_image = self.back_cover_image.thumbnail( - audiotools.THUMBNAIL_SIZE, - audiotools.THUMBNAIL_SIZE, - audiotools.THUMBNAIL_FORMAT) - @UTIL_TRACKTAG def tearDown(self): self.track_file.close() - self.cuesheet.close() self.comment_file.close() - self.front_cover.close() - self.back_cover.close() def populate_options(self, options): populated = [] for option in sorted(options): - if (option == '--cue'): - populated.append(option) - populated.append(self.cuesheet.name) - elif (option == '--name'): + if (option == '--name'): populated.append(option) populated.append("Name 3") elif (option == '--artist'): @@ -2850,12 +3437,6 @@ elif (option == '--comment-file'): populated.append(option) populated.append(self.comment_file.name) - elif (option == '--front-cover'): - populated.append(option) - populated.append(self.front_cover.name) - elif (option == '--back-cover'): - populated.append(option) - populated.append(self.back_cover.name) else: populated.append(option) @@ -2863,6 +3444,8 @@ @UTIL_TRACKTAG def test_options(self): + from audiotools.text import (ERR_DUPLICATE_FILE,) + #start out with a bit of sanity checking f = open(self.track_file.name, 'wb') f.write(self.track_data) @@ -2881,11 +3464,15 @@ #Since most of those options are straight text, #we'll restrict the tests to the more interesting ones #which is still over 8000 different option combinations. - most_options = ['-r', '--cue', - '--name', '--number', '--track-total', - '--album-number', '--comment', '--comment-file', - '--remove-images', '--front-cover', '--back-cover', - '-T'] + most_options = ['-r', '--name', '--number', '--track-total', + '--album-number', '--comment', '--comment-file'] + + #ensure tagging the same file twice triggers an error + self.assertEqual(self.__run_app__( + ["tracktag", "--name=Test", + self.track_file.name, self.track_file.name]), 1) + self.__check_error__(ERR_DUPLICATE_FILE % + (audiotools.Filename(self.track_file.name),)) for count in xrange(1, len(most_options) + 1): for options in Combinations(most_options, count): @@ -2894,6 +3481,7 @@ f.close() options = self.populate_options(options) + self.assertEqual( self.__run_app__(["tracktag"] + options + @@ -2906,49 +3494,49 @@ if ("--name" in options): self.assertEqual(metadata.track_name, u"Name 3") elif ("-r" in options): - self.assertEqual(metadata.track_name, u"") + self.assertEqual(metadata.track_name, None) else: self.assertEqual(metadata.track_name, u"Name 1") if ("--artist" in options): self.assertEqual(metadata.artist_name, u"Artist 3") elif ("-r" in options): - self.assertEqual(metadata.artist_name, u"") + self.assertEqual(metadata.artist_name, None) else: self.assertEqual(metadata.artist_name, u"Artist 1") if ("--album" in options): self.assertEqual(metadata.album_name, u"Album 3") elif ("-r" in options): - self.assertEqual(metadata.album_name, u"") + self.assertEqual(metadata.album_name, None) else: self.assertEqual(metadata.album_name, u"Album 1") if ("--number" in options): self.assertEqual(metadata.track_number, 5) elif ("-r" in options): - self.assertEqual(metadata.track_number, 0) + self.assertEqual(metadata.track_number, None) else: self.assertEqual(metadata.track_number, 1) if ("--track-total" in options): self.assertEqual(metadata.track_total, 6) elif ("-r" in options): - self.assertEqual(metadata.track_total, 0) + self.assertEqual(metadata.track_total, None) else: self.assertEqual(metadata.track_total, 2) if ("--album-number" in options): self.assertEqual(metadata.album_number, 7) elif ("-r" in options): - self.assertEqual(metadata.album_number, 0) + self.assertEqual(metadata.album_number, None) else: self.assertEqual(metadata.album_number, 3) if ("--album-total" in options): self.assertEqual(metadata.album_total, 8) elif ("-r" in options): - self.assertEqual(metadata.album_total, 0) + self.assertEqual(metadata.album_total, None) else: self.assertEqual(metadata.album_total, 4) @@ -2957,118 +3545,25 @@ elif ("--comment" in options): self.assertEqual(metadata.comment, u"Comment 3") elif ("-r" in options): - self.assertEqual(metadata.comment, u"") + self.assertEqual(metadata.comment, None) else: self.assertEqual(metadata.comment, u"Comment 1") - if (("--cue" in options) and ("--number" not in options)): - self.assertEqual(metadata.ISRC, u"JPPI00652340") - elif ("-r" in options): - self.assertEqual(metadata.ISRC, u"") + if ("-r" in options): + self.assertEqual(metadata.ISRC, None) else: self.assertEqual(metadata.ISRC, u"ABCD00000000") - if (("--front-cover" in options) and - ("--back-cover" in options)): - #adding front and back cover - - if (("-r" in options) or - ("--remove-images" in options)): - if ("-T" in options): - self.assertEqual( - metadata.front_covers(), - [self.thumbnailed_front_cover_image]) - self.assertEqual( - metadata.back_covers(), - [self.thumbnailed_back_cover_image]) - else: - self.assertEqual(metadata.front_covers(), - [self.front_cover_image]) - self.assertEqual(metadata.back_covers(), - [self.back_cover_image]) - self.assertEqual(len(metadata.images()), 2) - else: - if ("-T" in options): - self.assertEqual( - metadata.front_covers(), - [self.image, - self.thumbnailed_front_cover_image]) - self.assertEqual( - metadata.back_covers(), - [self.thumbnailed_back_cover_image]) - else: - self.assertEqual(metadata.front_covers(), - [self.image, - self.front_cover_image]) - self.assertEqual(metadata.back_covers(), - [self.back_cover_image]) - self.assertEqual(len(metadata.images()), 3) - elif ("--front-cover" in options): - #adding front-cover - - if (("-r" in options) or - ("--remove-images" in options)): - if ("-T" in options): - self.assertEqual( - metadata.images(), - [self.thumbnailed_front_cover_image]) - else: - self.assertEqual(metadata.images(), - [self.front_cover_image]) - self.assertEqual(len(metadata.images()), 1) - else: - if ("-T" in options): - self.assertEqual( - metadata.images(), - [self.image, - self.thumbnailed_front_cover_image]) - else: - self.assertEqual(metadata.images(), - [self.image, - self.front_cover_image]) - self.assertEqual(len(metadata.images()), 2) - elif ("--back-cover" in options): - #adding back cover - - if (("-r" in options) or - ("--remove-images" in options)): - if ("-T" in options): - self.assertEqual( - metadata.images(), - [self.thumbnailed_back_cover_image]) - else: - self.assertEqual(metadata.images(), - [self.back_cover_image]) - self.assertEqual(len(metadata.images()), 1) - else: - self.assertEqual(metadata.front_covers(), - [self.image]) - if ("-T" in options): - self.assertEqual( - metadata.back_covers(), - [self.thumbnailed_back_cover_image]) - else: - self.assertEqual(metadata.back_covers(), - [self.back_cover_image]) - self.assertEqual(len(metadata.images()), 2) - else: - #no new images added - - if (("-r" in options) or - ("--remove-images" in options)): - self.assertEqual(len(metadata.images()), 0) - else: - self.assertEqual(metadata.images(), - [self.image]) - self.assertEqual(len(metadata.images()), 1) - if ("--replay-gain" in options): self.assert_(track.replay_gain() is not None) @UTIL_TRACKTAG def test_replaygain(self): + from audiotools.text import (RG_REPLAYGAIN_ADDED, + RG_REPLAYGAIN_APPLIED) + for audio_class in audiotools.AVAILABLE_TYPES: - if (audio_class.can_add_replay_gain()): + if (audio_class.supports_replay_gain()): track_file = tempfile.NamedTemporaryFile( suffix="." + audio_class.SUFFIX) try: @@ -3079,20 +3574,101 @@ self.__run_app__(["tracktag", "--replay-gain", track.filename]), 0) if (audio_class.lossless_replay_gain()): - self.__check_info__( - _(u"ReplayGain added")) + self.__check_info__(RG_REPLAYGAIN_ADDED) track2 = audiotools.open(track_file.name) self.assert_(track2.replay_gain() is not None) else: - self.__check_info__( - _(u"ReplayGain applied")) + self.__check_info__(RG_REPLAYGAIN_APPLIED) finally: track_file.close() + @UTIL_TRACKTAG + def test_unicode(self): + for (input_filename, + (argument, attribute), + unicode_value) in Possibilities( + ["track.flac", #check filename arguments + u'abc\xe0\xe7\xe8\u3041\u3044\u3046.flac'.encode('utf-8')], + [("--name", "track_name"), #check text arguments + ("--artist", "artist_name"), + ("--album", "album_name"), + ("--performer", "performer_name"), + ("--composer", "composer_name"), + ("--conductor", "conductor_name"), + ("--catalog", "catalog"), + ("--ISRC", "ISRC"), + ("--publisher", "publisher"), + ("--media-type", "media"), + ("--year", "year"), + ("--date", "date"), + ("--copyright", "copyright"), + ("--comment", "comment")], + [u"text", + u'value abc\xe0\xe7\xe8\u3041\u3044\u3046']): + self.assert_(isinstance(unicode_value, unicode)) + + if (os.path.isfile(input_filename)): + os.unlink(input_filename) + + track = audiotools.FlacAudio.from_pcm( + input_filename, + BLANK_PCM_Reader(1)) + + self.assertEqual( + self.__run_app__(["tracktag", + argument, + unicode_value.encode('utf-8'), + input_filename]), 0) + + set_value = getattr(audiotools.open(input_filename).get_metadata(), + attribute) + if (set_value is not None): + self.assertEqual(set_value, unicode_value) + + if (os.path.isfile(input_filename)): + os.unlink(input_filename) + + for (input_filename, + comment_filename) in Possibilities( + ["track.flac", #check input filename arguments + u'abc\xe0\xe7\xe8\u3041\u3044\u3046.flac'.encode('utf-8')], + ["comment.txt", #check comment filename arguments + u'abc\xe0\xe7\xe8\u3041\u3044\u3046.txt'.encode('utf-8')]): + if (os.path.isfile(input_filename)): + os.unlink(input_filename) + if (os.path.isfile(comment_filename)): + os.unlink(comment_filename) + + track = audiotools.FlacAudio.from_pcm( + input_filename, + BLANK_PCM_Reader(1)) + + f = open(comment_filename, "wb") + f.write("Test Text") + f.close() + + self.assertEqual( + self.__run_app__(["tracktag", + "--comment-file", comment_filename, + input_filename]), 0) + + self.assertEqual( + audiotools.open(input_filename).get_metadata().comment, + u"Test Text") + + if (os.path.isfile(input_filename)): + os.unlink(input_filename) + if (os.path.isfile(comment_filename)): + os.unlink(comment_filename) class tracktag_errors(UtilTest): @UTIL_TRACKTAG def test_bad_options(self): + from audiotools.text import (ERR_OPEN_IOERROR, + ERR_ENCODING_ERROR, + ERR_TRACKTAG_COMMENT_IOERROR, + ERR_TRACKTAG_COMMENT_NOT_UTF8) + temp_comment = tempfile.NamedTemporaryFile(suffix=".txt") temp_track_file = tempfile.NamedTemporaryFile(suffix=".flac") temp_track_stat = os.stat(temp_track_file.name)[0] @@ -3104,18 +3680,10 @@ temp_track.set_metadata(audiotools.MetaData(track_name=u"Foo")) self.assertEqual(self.__run_app__( - ["tracktag", "--front-cover=/dev/null/foo.jpg", - temp_track.filename]), 1) - self.__check_error__( - _(u"%(filename)s: %(message)s") % \ - {"filename": self.filename(temp_track.filename), - "message": _(u"Unable to open file")}) - - self.assertEqual(self.__run_app__( ["tracktag", "--comment-file=/dev/null/foo.txt", - self.filename(temp_track.filename)]), 1) - self.__check_error__(_(u"Unable to open comment file \"%s\"") % \ - (self.filename("/dev/null/foo.txt"))) + temp_track.filename]), 1) + self.__check_error__(ERR_TRACKTAG_COMMENT_IOERROR % + (audiotools.Filename("/dev/null/foo.txt"),)) temp_comment.write( os.urandom(1024) + ((u"\uFFFD".encode('utf-8')) * 103)) @@ -3124,16 +3692,15 @@ self.assertEqual(self.__run_app__( ["tracktag", "--comment-file=%s" % (temp_comment.name), temp_track.filename]), 1) - self.__check_error__( - _(u"Comment file \"%s\" does not appear to be UTF-8 text") % \ - (temp_comment.name)) + self.__check_error__(ERR_TRACKTAG_COMMENT_NOT_UTF8 % + (audiotools.Filename(temp_comment.name),)) os.chmod(temp_track_file.name, temp_track_stat & 07555) self.assertEqual(self.__run_app__( - ["tracktag", "--name=Foo", - self.filename(temp_track.filename)]), 1) - self.__check_error__(_(u"Unable to modify \"%s\"") % \ - (self.filename(temp_track.filename))) + ["tracktag", "--name=Bar", + temp_track.filename]), 1) + self.__check_error__(ERR_ENCODING_ERROR % + (audiotools.Filename(temp_track.filename),)) finally: os.chmod(temp_track_file.name, temp_track_stat) temp_track_file.close() @@ -3147,7 +3714,6 @@ suffix="." + audio_class.SUFFIX) tempwv = tempfile.NamedTemporaryFile( suffix="." + audiotools.WavPackAudio.SUFFIX) - big_bmp = tempfile.NamedTemporaryFile(suffix=".bmp") big_text = tempfile.NamedTemporaryFile(suffix=".txt") try: flac = audio_class.from_pcm( @@ -3156,9 +3722,6 @@ flac.set_metadata(audiotools.MetaData(track_name=u"Foo")) - big_bmp.write(HUGE_BMP.decode('bz2')) - big_bmp.flush() - big_text.write("QlpoOTFBWSZTWYmtEk8AgICBAKAAAAggADCAKRoBANIBAOLuSKcKEhE1okng".decode('base64').decode('bz2')) big_text.flush() @@ -3167,18 +3730,6 @@ audiotools.transfer_framelist_data(pcm, orig_md5.update) pcm.close() - #ensure that setting a big image via tracktag - #doesn't break the file - subprocess.call(["tracktag", "-V", "quiet", - "--front-cover=%s" % (big_bmp.name), - flac.filename]) - new_md5 = md5() - pcm = flac.to_pcm() - audiotools.transfer_framelist_data(pcm, new_md5.update) - pcm.close() - self.assertEqual(orig_md5.hexdigest(), - new_md5.hexdigest()) - #ensure that setting big text via tracktag #doesn't break the file subprocess.call(["tracktag", "-V", "quiet", @@ -3201,14 +3752,13 @@ self.assertEqual(subprocess.call( ["tracktag", "-V", "quiet", - "--front-cover=%s" % (big_bmp.name), "--comment-file=%s" % (big_text.name), wv.filename]), 0) - self.assertEqual(len(wv.get_metadata().images()), 1) self.assert_(len(wv.get_metadata().comment) > 0) - subprocess.call(["track2track", "-t", audio_class.NAME, "-o", + subprocess.call(["track2track", "-V", "quiet", + "-t", audio_class.NAME, "-o", flac.filename, wv.filename]) flac = audiotools.open(tempflac.name) @@ -3216,7 +3766,6 @@ finally: tempflac.close() tempwv.close() - big_bmp.close() big_text.close() @@ -3227,6 +3776,38 @@ class tracktag_misc(UtilTest): @UTIL_TRACKTAG def test_text_options(self): + def number_fields_values(fields, metadata_class): + values = set([]) + for field in audiotools.MetaData.INTEGER_FIELDS: + if (field in fields): + values.add( + (field, + audiotools.MetaData.INTEGER_FIELDS.index( + field) + 1)) + else: + values.add((field, None)) + + return values + + def deleted_number_fields_values(fields, metadata_class): + values = set([]) + for field in audiotools.MetaData.INTEGER_FIELDS: + if (field not in fields): + values.add( + (field, + audiotools.MetaData.INTEGER_FIELDS.index( + field) + 1)) + else: + values.add((field, None)) + + return values + + def metadata_fields_values(metadata): + values = set([]) + for field in audiotools.MetaData.INTEGER_FIELDS: + values.add((field, getattr(metadata, field))) + return values + for audio_type in audiotools.AVAILABLE_TYPES: temp_file = tempfile.NamedTemporaryFile( suffix="." + audio_type.SUFFIX) @@ -3285,7 +3866,7 @@ metadata = new_track.get_metadata() if (metadata is None): break - elif (len(getattr(metadata, field_name)) > 0): + elif (getattr(metadata, field_name) is not None): self.assertEqual(getattr(metadata, field_name), u'foo') @@ -3298,40 +3879,10 @@ self.assertEqual( getattr(metadata, field_name), - u'', + None, "remove option failed for %s field %s" % (audio_type.NAME, remove_field)) - def number_fields_values(fields): - values = set([]) - for field in audiotools.MetaData.INTEGER_FIELDS: - if (field in fields): - values.add( - (field, - audiotools.MetaData.INTEGER_FIELDS.index( - field) + 1)) - else: - values.add((field, 0)) - return values - - def deleted_number_fields_values(fields): - values = set([]) - for field in audiotools.MetaData.INTEGER_FIELDS: - if (field not in fields): - values.add( - (field, - audiotools.MetaData.INTEGER_FIELDS.index( - field) + 1)) - else: - values.add((field, 0)) - return values - - def metadata_fields_values(metadata): - values = set([]) - for field in audiotools.MetaData.INTEGER_FIELDS: - values.add((field, getattr(metadata, field))) - return values - number_fields = ['track_number', 'track_total', 'album_number', @@ -3352,7 +3903,13 @@ self.assert_( metadata_fields_values(metadata).issubset( - number_fields_values(fields))) + number_fields_values( + fields, metadata.__class__)), + "%s not subset of %s for fields %s" % ( + metadata_fields_values(metadata), + number_fields_values( + fields, metadata.__class__), + repr(fields))) #make sure the number fields get removed properly, also number_metadata = audiotools.MetaData(track_number=1, @@ -3372,11 +3929,15 @@ track.filename).get_metadata() self.assert_( metadata_fields_values(metadata).issubset( - deleted_number_fields_values(fields)), - "%s not subset of %s for options %s type %s" % + deleted_number_fields_values( + fields, metadata.__class__)), + "%s not subset of %s for options %s, fields %s, type %s" % (metadata_fields_values(metadata), - deleted_number_fields_values(fields), - self.populate_delete_number_fields(fields), + deleted_number_fields_values( + fields, metadata.__class__), + self.populate_delete_number_fields( + fields), + fields, audio_type.NAME)) except NoMetaData: @@ -3415,126 +3976,296 @@ options.append('--remove-album-total') return options - @UTIL_TRACKTAG - def test_cuesheet1(self): - for audio_class in [audiotools.FlacAudio, - audiotools.WavPackAudio]: - #create single track and cuesheet - temp_track = tempfile.NamedTemporaryFile( - suffix="." + audio_class.SUFFIX) - temp_sheet = tempfile.NamedTemporaryFile( - suffix=".cue") - try: - temp_sheet.write( -"""eJydkF1LwzAUQN8L/Q+X/oBxk6YfyVtoM4mu68iy6WudQ8qkHbNu+u9NneCc1IdCnk649xyuUQXk -epnpHGiOMU2Q+Z5xMCuLQs0tBOq92nTy7alus3b/AUeccL5/ZIHvZdLKWXkDjKcpIg2RszjxvYUy -09IUykCwanZNe2pAHrr6tXMjVtuZ+uG27l62Dk91T03VPG8np+oYwL1cK98DsEZmd4AE5CrXZU8c -O++wh2qzQxKc4X/S/l8vTQa3i7V2kWEap/iN57l66Pcjiq93IaWDUjpOyn9LETAVyASh1y0OR4Il -Fy3hYEs4qiXB6wOQULBQkOhCygalbISUUvrnACQVERfIr1scI4K5lk9od5+/""".decode('base64').decode('zlib')) - temp_sheet.flush() - album = audio_class.from_pcm( - temp_track.name, - EXACT_BLANK_PCM_Reader(69470436)) - sheet = audiotools.read_sheet(temp_sheet.name) - - #add metadata - self.assertEqual(subprocess.call(["tracktag", - "--album", "Album Name", - "--artist", "Artist Name", - "--album-number", "2", - "--album-total", "3", - temp_track.name]), 0) - metadata = audiotools.MetaData( - album_name=u"Album Name", - artist_name=u"Artist Name", - album_number=2, - album_total=3) +class covertag(UtilTest): + @UTIL_COVERTAG + def setUp(self): + track_file_base = tempfile.NamedTemporaryFile() + self.initial_metadata = audiotools.MetaData( + track_name=u"Name 1", + track_number=1, + track_total=2, + album_name=u"Album 1", + artist_name=u"Artist 1", + album_number=3, + album_total=4, + ISRC=u'ABCD00000000', + comment=u"Comment 1") + + self.image = audiotools.Image.new(TEST_COVER1, u"", 0) + self.initial_metadata.add_image(self.image) + + track_base = audiotools.FlacAudio.from_pcm( + track_file_base.name, + BLANK_PCM_Reader(1)) + track_base.set_metadata(self.initial_metadata) + self.track_data = open(track_base.filename, 'rb').read() + track_file_base.close() + + self.track_file = tempfile.NamedTemporaryFile() + + self.front_cover1 = tempfile.NamedTemporaryFile(suffix=".png") + self.front_cover1.write(TEST_COVER4) + self.front_cover1.flush() + + self.front_cover2 = tempfile.NamedTemporaryFile(suffix=".jpg") + self.front_cover2.write(TEST_COVER3) + self.front_cover2.flush() + + self.back_cover = tempfile.NamedTemporaryFile(suffix=".png") + self.back_cover.write(TEST_COVER2) + self.back_cover.flush() + + self.leaflet = tempfile.NamedTemporaryFile(suffix=".jpg") + self.leaflet.write(TEST_COVER1) + self.leaflet.flush() + + self.media = tempfile.NamedTemporaryFile(suffix=".png") + self.media.write(TEST_COVER2) + self.media.flush() + + self.other = tempfile.NamedTemporaryFile(suffix=".png") + self.other.write(TEST_COVER4) + self.other.flush() + + self.front_cover1_image = audiotools.Image.new( + TEST_COVER4, u"", 0) + self.front_cover2_image = audiotools.Image.new( + TEST_COVER3, u"", 0) + self.back_cover_image = audiotools.Image.new( + TEST_COVER2, u"", 1) + self.leaflet_image = audiotools.Image.new( + TEST_COVER1, u"", 2) + self.media_image = audiotools.Image.new( + TEST_COVER2, u"", 3) + self.other_image = audiotools.Image.new( + TEST_COVER4, u"", 4) + + @UTIL_COVERTAG + def tearDown(self): + self.track_file.close() + self.front_cover1.close() + self.front_cover2.close() + self.back_cover.close() + self.leaflet.close() + self.media.close() + self.other.close() + + def populate_options(self, options): + populated = [] + front_covers = [self.front_cover1.name, self.front_cover2.name] + + for option in sorted(options): + if (option == '--front-cover'): + populated.append(option) + populated.append(front_covers.pop(0)) + elif (option == '--back-cover'): + populated.append(option) + populated.append(self.back_cover.name) + elif (option == '--leaflet'): + populated.append(option) + populated.append(self.leaflet.name) + elif (option == '--media'): + populated.append(option) + populated.append(self.media.name) + elif (option == '--other-image'): + populated.append(option) + populated.append(self.other.name) + else: + populated.append(option) + + return populated + + @UTIL_COVERTAG + def test_options(self): + from audiotools.text import (ERR_DUPLICATE_FILE,) + + #start out with a bit of sanity checking + f = open(self.track_file.name, 'wb') + f.write(self.track_data) + f.close() - #add cuesheet + track = audiotools.open(self.track_file.name) + track.verify() + metadata = track.get_metadata() + self.assertEqual(metadata.images(), + [self.image]) + + covertag_options = ['-r', '--front-cover', '--front-cover', + '--back-cover', '--leaflet', '--media', + '--other-image'] + + #ensure tagging the same file twice triggers an error + self.assertEqual(self.__run_app__( + ["covertag", "--front-cover", self.front_cover1.name, + self.track_file.name, self.track_file.name]), 1) + self.__check_error__(ERR_DUPLICATE_FILE % + (audiotools.Filename(self.track_file.name),)) + + for count in xrange(1, len(covertag_options) + 1): + for options in Combinations(covertag_options, count): + f = open(self.track_file.name, 'wb') + f.write(self.track_data) + f.close() + + options = self.populate_options(options) self.assertEqual( - subprocess.call(["tracktag", "--cue", temp_sheet.name, - temp_track.name]), 0) + self.__run_app__(["covertag"] + + options + + [self.track_file.name]), 0) + + track = audiotools.open(self.track_file.name) + track.verify() + metadata = track.get_metadata() + + if ('-r' in options): + if (options.count('--front-cover') == 0): + self.assertEqual(metadata.front_covers(), + []) + elif (options.count('--front-cover') == 1): + self.assertEqual(metadata.front_covers(), + [self.front_cover1_image]) + elif (options.count('--front-cover') == 2): + self.assertEqual(metadata.front_covers(), + [self.front_cover1_image, + self.front_cover2_image]) + else: + if (options.count('--front-cover') == 0): + self.assertEqual(metadata.front_covers(), + [self.image]) + elif (options.count('--front-cover') == 1): + self.assertEqual(metadata.front_covers(), + [self.image, + self.front_cover1_image]) + elif (options.count('--front-cover') == 2): + self.assertEqual(metadata.front_covers(), + [self.image, + self.front_cover1_image, + self.front_cover2_image]) + if ('--back-cover' in options): + self.assertEqual(metadata.back_covers(), + [self.back_cover_image]) + else: + self.assertEqual(metadata.back_covers(), + []) + if ('--leaflet' in options): + self.assertEqual(metadata.leaflet_pages(), + [self.leaflet_image]) + else: + self.assertEqual(metadata.leaflet_pages(), + []) + if ('--media' in options): + self.assertEqual(metadata.media_images(), + [self.media_image]) + else: + self.assertEqual(metadata.media_images(), + []) + if ('--other-image' in options): + self.assertEqual(metadata.other_images(), + [self.other_image]) + else: + self.assertEqual(metadata.other_images(), + []) - #ensure metadata matches - self.assertEqual(album.get_metadata(), metadata) + @UTIL_COVERTAG + def test_unicode(self): + from shutil import rmtree + + for (file_path, + option, + image_path) in Possibilities( + ["test.flac", #check filename arguments + u'abc\xe0\xe7\xe8\u3041\u3044\u3046.flac'.encode('utf-8')], + ["--front-cover", + "--back-cover", + "--leaflet", + "--media", + "--other-image"], + ["image.jpg", #check image path arguments + u'abc\xe0\xe7\xe8\u3041\u3044\u3046.jpg'.encode('utf-8')]): + if (os.path.isfile(file_path)): + os.unlink(file_path) + if (os.path.isfile(image_path)): + os.unlink(image_path) + + track = audiotools.FlacAudio.from_pcm( + file_path, + BLANK_PCM_Reader(1)) - #ensure cuesheet matches - sheet2 = album.get_cuesheet() + f = open(image_path, "wb") + f.write(TEST_COVER1) + f.close() - self.assertNotEqual(sheet2, None) - self.assertEqual(sheet.catalog(), - sheet2.catalog()) - self.assertEqual(sorted(sheet.ISRCs().items()), - sorted(sheet2.ISRCs().items())) - self.assertEqual(list(sheet.indexes()), - list(sheet2.indexes())) - self.assertEqual(list(sheet.pcm_lengths(69470436)), - list(sheet2.pcm_lengths(69470436))) - finally: - temp_track.close() - temp_sheet.close() + self.assertEqual( + self.__run_app__( + ["covertag", option, image_path, file_path]), 0) - @UTIL_TRACKTAG - def test_cuesheet2(self): + self.assertEqual( + audiotools.open(file_path).get_metadata().images()[0].data, + TEST_COVER1) + + if (os.path.isfile(file_path)): + os.unlink(file_path) + if (os.path.isfile(image_path)): + os.unlink(image_path) + +class covertag_errors(UtilTest): + @UTIL_COVERTAG + def test_bad_options(self): + from audiotools.text import (ERR_OPEN_IOERROR,) + + temp_track_file = tempfile.NamedTemporaryFile(suffix=".flac") + temp_track_stat = os.stat(temp_track_file.name)[0] + try: + temp_track = audiotools.FlacAudio.from_pcm( + temp_track_file.name, + BLANK_PCM_Reader(5)) + + self.assertEqual(self.__run_app__( + ["covertag", "--front-cover=/dev/null/foo.jpg", + temp_track.filename]), 1) + self.__check_error__( + ERR_OPEN_IOERROR % (audiotools.Filename(u"/dev/null/foo.jpg"),)) + finally: + os.chmod(temp_track_file.name, temp_track_stat) + temp_track_file.close() + + @UTIL_COVERTAG + def test_oversized_metadata(self): for audio_class in [audiotools.FlacAudio, - audiotools.WavPackAudio]: - #create single track and cuesheet - temp_track = tempfile.NamedTemporaryFile( + audiotools.OggFlacAudio]: + tempflac = tempfile.NamedTemporaryFile( suffix="." + audio_class.SUFFIX) - temp_sheet = tempfile.NamedTemporaryFile( - suffix=".cue") + big_bmp = tempfile.NamedTemporaryFile(suffix=".bmp") try: - temp_sheet.write( -"""eJydkF1LwzAUQN8L/Q+X/oBxk6YfyVtoM4mu68iy6WudQ8qkHbNu+u9NneCc1IdCnk649xyuUQXk -epnpHGiOMU2Q+Z5xMCuLQs0tBOq92nTy7alus3b/AUeccL5/ZIHvZdLKWXkDjKcpIg2RszjxvYUy -09IUykCwanZNe2pAHrr6tXMjVtuZ+uG27l62Dk91T03VPG8np+oYwL1cK98DsEZmd4AE5CrXZU8c -O++wh2qzQxKc4X/S/l8vTQa3i7V2kWEap/iN57l66Pcjiq93IaWDUjpOyn9LETAVyASh1y0OR4Il -Fy3hYEs4qiXB6wOQULBQkOhCygalbISUUvrnACQVERfIr1scI4K5lk9od5+/""".decode('base64').decode('zlib')) - temp_sheet.flush() - album = audio_class.from_pcm( - temp_track.name, - EXACT_BLANK_PCM_Reader(69470436)) - sheet = audiotools.read_sheet(temp_sheet.name) + flac = audio_class.from_pcm( + tempflac.name, + BLANK_PCM_Reader(5)) - #add cuesheet - self.assertEqual( - subprocess.call(["tracktag", "--cue", temp_sheet.name, - temp_track.name]), 0) + flac.set_metadata(audiotools.MetaData(track_name=u"Foo")) - #add metadata - self.assertEqual(subprocess.call(["tracktag", - "--album", "Album Name", - "--artist", "Artist Name", - "--album-number", "2", - "--album-total", "3", - temp_track.name]), 0) + big_bmp.write(HUGE_BMP.decode('bz2')) + big_bmp.flush() - metadata = audiotools.MetaData( - album_name=u"Album Name", - artist_name=u"Artist Name", - album_number=2, - album_total=3) - - #ensure metadata matches - self.assertEqual(album.get_metadata(), metadata) - - #ensure cuesheet matches - sheet2 = album.get_cuesheet() - - self.assertNotEqual(sheet2, None) - self.assertEqual(sheet.catalog(), - sheet2.catalog()) - self.assertEqual(sorted(sheet.ISRCs().items()), - sorted(sheet2.ISRCs().items())) - self.assertEqual(list(sheet.indexes()), - list(sheet2.indexes())) - self.assertEqual(list(sheet.pcm_lengths(69470436)), - list(sheet2.pcm_lengths(69470436))) - finally: - temp_track.close() - temp_sheet.close() + orig_md5 = md5() + pcm = flac.to_pcm() + audiotools.transfer_framelist_data(pcm, orig_md5.update) + pcm.close() + #ensure that setting a big image via covertag + #doesn't break the file + subprocess.call(["covertag", "-V", "quiet", + "--front-cover=%s" % (big_bmp.name), + flac.filename]) + new_md5 = md5() + pcm = flac.to_pcm() + audiotools.transfer_framelist_data(pcm, new_md5.update) + pcm.close() + self.assertEqual(orig_md5.hexdigest(), + new_md5.hexdigest()) + finally: + tempflac.close() + big_bmp.close() class trackrename(UtilTest): @UTIL_TRACKRENAME @@ -3582,7 +4313,7 @@ @UTIL_TRACKRENAME def test_options(self): - messenger = audiotools.Messenger("trackrename", None) + from audiotools.text import (LAB_ENCODE) all_options = ["--format"] for count in xrange(0, len(all_options) + 1): @@ -3630,18 +4361,62 @@ format=output_format)) self.__check_info__( - _(u"%(source)s -> %(destination)s") % + LAB_ENCODE % {"source": - messenger.filename(track.filename), + audiotools.Filename(track.filename), "destination": - messenger.filename(destination_filename)}) + audiotools.Filename(destination_filename)}) #check that the file is identical self.assertEqual(track_data, open(destination_filename, 'rb').read()) @UTIL_TRACKRENAME + def test_duplicate(self): + from audiotools.text import (ERR_DUPLICATE_FILE, + ERR_DUPLICATE_OUTPUT_FILE, + ) + + name1 = "01 - name." + self.type.SUFFIX + name2 = "02 - name." + self.type.SUFFIX + + track1 = self.type.from_pcm( + os.path.join(self.input_dir, name1), + BLANK_PCM_Reader(1)) + track1.set_metadata(audiotools.MetaData(track_number=1)) + + track2 = self.type.from_pcm( + os.path.join(self.input_dir, name2), + BLANK_PCM_Reader(1)) + track2.set_metadata(audiotools.MetaData(track_number=2)) + + self.assertEqual( + self.__run_app__(["trackrename", "-V", "normal", + "--format", self.format, + track1.filename, track1.filename]), 1) + + self.__check_error__( + ERR_DUPLICATE_FILE % + (audiotools.Filename(track1.filename),)) + + self.assertEqual( + self.__run_app__(["trackrename", "-V", "normal", + "--format", "foo", + track1.filename, track2.filename]), 1) + + self.__check_error__( + ERR_DUPLICATE_OUTPUT_FILE % + (audiotools.Filename( + os.path.join( + os.path.dirname(track1.filename), "foo")),)) + + @UTIL_TRACKRENAME def test_errors(self): + from audiotools.text import (ERR_FILES_REQUIRED, + ERR_UNKNOWN_FIELD, + LAB_SUPPORTED_FIELDS, + ) + tempdir = tempfile.mkdtemp() tempdir_stat = os.stat(tempdir)[0] track = self.type.from_pcm( @@ -3652,21 +4427,20 @@ album_name=u"Album")) try: self.assertEqual(self.__run_app__(["trackrename"]), 1) - self.__check_error__(_(u"You must specify at least 1 supported audio file")) - + self.__check_error__(ERR_FILES_REQUIRED) self.assertEqual(self.__run_app__( ["trackrename", "--format=%(foo)s", track.filename]), 1) - self.__check_error__(_(u"Unknown field \"%s\" in file format") % \ - ("foo")) - self.__check_info__(_(u"Supported fields are:")) + self.__check_error__(ERR_UNKNOWN_FIELD % ("foo")) + self.__check_info__(LAB_SUPPORTED_FIELDS) for field in sorted(audiotools.MetaData.FIELDS + \ ("album_track_number", "suffix")): if (field == 'track_number'): self.__check_info__(u"%(track_number)2.2d") else: self.__check_info__(u"%%(%s)s" % (field)) + self.__check_info__(u"%(basename)s") if (track.get_metadata() is not None): os.chmod(tempdir, tempdir_stat & 0x7555) @@ -3676,14 +4450,11 @@ '--format=%(album_name)s/%(track_number)2.2d - %(track_name)s.%(suffix)s', track.filename]), 1) - self.__check_error__(_(u"Unable to write \"%s\"") % \ - self.filename( - os.path.join( - "Album", - "%(track_number)2.2d - %(track_name)s.%(suffix)s" % \ - {"track_number": 1, - "track_name": "Name", - "suffix": self.type.SUFFIX}))) + self.__check_error__( + u"[Errno 13] Permission denied: \'%s\'" % \ + (audiotools.Filename( + os.path.join( + os.path.dirname(track.filename), "Album")),)) self.assertEqual(self.__run_app__( ["trackrename", @@ -3695,6 +4466,37 @@ os.unlink(track.filename) os.rmdir(tempdir) + @UTIL_TRACKRENAME + def test_unicode(self): + for (file_path, + format_string) in Possibilities( + ["file.flac", + u'abc\xe0\xe7\xe8\u3041\u3044\u3046.flac'.encode('utf-8')], + ["new_file.flac", + u'abc\xe0\xe7\xe8\u3041\u3044\u3046-2.flac'.encode('utf-8')]): + if (os.path.isfile(file_path)): + os.unlink(file_path) + if (os.path.isfile(format_string)): + os.unlink(format_string) + + track = audiotools.FlacAudio.from_pcm( + file_path, + BLANK_PCM_Reader(1)) + + self.assertEqual(os.path.isfile(file_path), True) + self.assertEqual(os.path.isfile(format_string), False) + + self.assertEqual( + self.__run_app__( + ["trackrename", "--format", format_string, file_path]), 0) + + self.assertEqual(os.path.isfile(file_path), False) + self.assertEqual(os.path.isfile(format_string), True) + + if (os.path.isfile(file_path)): + os.unlink(file_path) + if (os.path.isfile(format_string)): + os.unlink(format_string) class tracksplit(UtilTest): @UTIL_TRACKSPLIT @@ -3723,6 +4525,10 @@ self.cuesheet2.write('FILE "CDImage.wav" WAVE\r\n TRACK 01 AUDIO\r\n ISRC ABCD00000001\r\n INDEX 01 00:00:00\r\n TRACK 02 AUDIO\r\n ISRC ABCD00000002\r\n INDEX 00 00:03:00\r\n INDEX 01 00:05:00\r\n TRACK 03 AUDIO\r\n ISRC ABCD00000003\r\n INDEX 00 00:9:00\r\n INDEX 01 00:11:00\r\n') self.cuesheet2.flush() + self.cuesheet3 = tempfile.NamedTemporaryFile(suffix=".cue") + self.cuesheet3.write('FILE "CDImage.wav" WAVE\r\n TRACK 01 AUDIO\r\n ISRC JPPI00652340\r\n INDEX 01 00:00:00\r\n') + self.cuesheet3.flush() + self.unsplit_file2 = tempfile.NamedTemporaryFile(suffix=".flac") self.stream = test_streams.Sine16_Stereo(793800, 44100, @@ -3744,6 +4550,7 @@ self.unsplit_file2.close() self.cuesheet.close() self.cuesheet2.close() + self.cuesheet3.close() for f in os.listdir(self.output_dir): os.unlink(os.path.join(self.output_dir, f)) @@ -3788,7 +4595,8 @@ @UTIL_TRACKSPLIT def test_options_no_embedded_cue(self): - messenger = audiotools.Messenger("trackcat", None) + from audiotools.text import (ERR_UNSUPPORTED_COMPRESSION_MODE, + ERR_TRACKSPLIT_NO_CUESHEET) all_options = ["--cue", "-t", "-q", "-d", "--format"] @@ -3809,25 +4617,26 @@ if (("-q" in options) and ("1" not in output_type.COMPRESSION_MODES)): self.assertEqual( - self.__run_app__(["tracksplit", "-V", "normal"] + + self.__run_app__(["tracksplit", "-V", "normal", + "--no-freedb", "--no-musicbrainz"] + options + [track.filename]), 1) self.__check_error__( - _(u"\"%(quality)s\" is not a supported " + - u"compression mode for type \"%(type)s\"") % + ERR_UNSUPPORTED_COMPRESSION_MODE % {"quality": "1", "type": output_type.NAME}) continue if ("--cue" not in options): self.assertEqual( - self.__run_app__(["tracksplit", "-V", "normal"] + + self.__run_app__(["tracksplit", "-V", "normal", + "--no-freedb", "--no-musicbrainz"] + options + [track.filename]), 1) - self.__check_error__( - _(u"You must specify a cuesheet to split audio file")) + self.__check_error__(ERR_TRACKSPLIT_NO_CUESHEET) continue self.assertEqual( - self.__run_app__(["tracksplit", "-V", "normal"] + + self.__run_app__(["tracksplit", "-V", "normal", + "--no-freedb", "--no-musicbrainz"] + options + [track.filename]), 0) if ("--format" in options): output_format = self.format @@ -3858,14 +4667,16 @@ format=output_format)) #check that the output is being generated correctly - for path in output_filenames: + for (i, path) in enumerate(output_filenames): self.__check_info__( - _(u"%(source)s -> %(destination)s") % \ - {"source": - messenger.filename(track.filename), - "destination": - messenger.filename( - os.path.join(output_dir, path))}) + audiotools.output_progress( + u"%(source)s -> %(destination)s" % + {"source": + audiotools.Filename(track.filename), + "destination": + audiotools.Filename( + os.path.join(output_dir, path))}, + i + 1, len(output_filenames))) #make sure no track data has been lost output_tracks = [ @@ -3874,15 +4685,14 @@ self.stream.reset() self.assert_( audiotools.pcm_frame_cmp( - audiotools.PCMCat(iter([t.to_pcm() - for t in output_tracks])), + audiotools.PCMCat([t.to_pcm() for t in output_tracks]), self.stream) is None) #make sure metadata fits our expectations for i in xrange(len(output_tracks)): metadata = output_tracks[i].get_metadata() if (metadata is not None): - self.assertEqual(metadata.track_name, u"") + self.assertEqual(metadata.track_name, None) self.assertEqual(metadata.album_name, u"Album 1") self.assertEqual(metadata.artist_name, u"Artist 1") @@ -3903,7 +4713,9 @@ @UTIL_TRACKSPLIT def test_options_embedded_cue(self): - messenger = audiotools.Messenger("trackcat", None) + from audiotools.text import (ERR_UNSUPPORTED_COMPRESSION_MODE, + LAB_ENCODE, + ) all_options = ["--cue", "-t", "-q", "-d", "--format"] @@ -3926,17 +4738,18 @@ if (("-q" in options) and ("1" not in output_type.COMPRESSION_MODES)): self.assertEqual( - self.__run_app__(["tracksplit", "-V", "normal"] + + self.__run_app__(["tracksplit", "-V", "normal", + "--no-freedb", "--no-musicbrainz"] + options + [track.filename]), 1) self.__check_error__( - _(u"\"%(quality)s\" is not a supported " + - u"compression mode for type \"%(type)s\"") % + ERR_UNSUPPORTED_COMPRESSION_MODE % {"quality": "1", "type": output_type.NAME}) continue self.assertEqual( - self.__run_app__(["tracksplit", "-V", "normal"] + + self.__run_app__(["tracksplit", "-V", "normal", + "--no-freedb", "--no-musicbrainz"] + options + [track.filename]), 0) if ("--format" in options): output_format = self.format @@ -3966,14 +4779,16 @@ output_format)) #check that the output is being generated correctly - for path in output_filenames: + for (i, path) in enumerate(output_filenames): self.__check_info__( - _(u"%(source)s -> %(destination)s") % \ - {"source": - messenger.filename(track.filename), - "destination": - messenger.filename( - os.path.join(output_dir, path))}) + audiotools.output_progress( + LAB_ENCODE % + {"source": + audiotools.Filename(track.filename), + "destination": + audiotools.Filename( + os.path.join(output_dir, path))}, + i + 1, len(output_filenames))) #make sure no track data has been lost output_tracks = [ @@ -3982,15 +4797,14 @@ self.stream.reset() self.assert_( audiotools.pcm_frame_cmp( - audiotools.PCMCat(iter([t.to_pcm() - for t in output_tracks])), + audiotools.PCMCat([t.to_pcm() for t in output_tracks]), self.stream) is None) #make sure metadata fits our expectations for i in xrange(len(output_tracks)): metadata = output_tracks[i].get_metadata() if (metadata is not None): - self.assertEqual(metadata.track_name, u"") + self.assertEqual(metadata.track_name, None) self.assertEqual(metadata.album_name, u"Album 1") self.assertEqual(metadata.artist_name, u"Artist 1") @@ -4017,6 +4831,69 @@ if (metadata is not None): self.assertEqual(metadata.ISRC, ISRC) + @UTIL_TRACKSPLIT + def test_unicode(self): + import shutil + + for (input_filename, + cuesheet_file, + output_directory, + output_format) in Possibilities( + ["track.flac", #check filename arguments + u'abc\xe0\xe7\xe8\u3041\u3044\u3046.flac'.encode('utf-8')], + ["cuesheet.cue", #check --cue argument + u'abc\xe0\xe7\xe8\u3041\u3044\u3046.cue'.encode('utf-8')], + ["testdir", #check --dir argument + u'abc\xe0\xe7\xe8\u3041\u3044\u3046-dir'.encode('utf-8')], + ["%(track_number)d.%(suffix)s", #check --format argument + u'%(track_number)d - abc\xe0\xe7\xe8\u3041\u3044\u3046.%(suffix)s'.encode('utf-8')]): + if (os.path.isfile(input_filename)): + os.unlink(input_filename) + if (os.path.isfile(cuesheet_file)): + os.unlink(cuesheet_file) + if (os.path.isdir(output_directory)): + shutil.rmtree(output_directory) + + track = audiotools.FlacAudio.from_pcm( + input_filename, + EXACT_BLANK_PCM_Reader(sum([220500, 264600, 308700]))) + + f = open(cuesheet_file, "wb") + f.write('FILE "CDImage.wav" WAVE\r\n TRACK 01 AUDIO\r\n ISRC JPPI00652340\r\n INDEX 01 00:00:00\r\n TRACK 02 AUDIO\r\n ISRC JPPI00652349\r\n INDEX 00 00:03:00\r\n INDEX 01 00:05:00\r\n TRACK 03 AUDIO\r\n ISRC JPPI00652341\r\n INDEX 00 00:9:00\r\n INDEX 01 00:11:00\r\n') + f.close() + + self.assertEqual( + self.__run_app__( + ["tracksplit", + "--type", "flac", + "--cue", cuesheet_file, + "--dir", output_directory, + "--format", output_format, + input_filename]), 0) + + output_filenames = [output_format % {"track_number":i, + "suffix":"flac"} + for i in range(1, 4)] + for f in output_filenames: + self.assertEqual( + os.path.isfile(os.path.join(output_directory, f)), True) + + tracks = [audiotools.open(os.path.join(output_directory, f)) + for f in output_filenames] + + self.assertEqual( + audiotools.pcm_frame_cmp( + track.to_pcm(), + audiotools.PCMCat([t.to_pcm() for t in tracks])), + None) + + if (os.path.isfile(input_filename)): + os.unlink(input_filename) + if (os.path.isfile(cuesheet_file)): + os.unlink(cuesheet_file) + if (os.path.isdir(output_directory)): + shutil.rmtree(output_directory) + def populate_bad_options(self, options): populated = ["--no-musicbrainz", "--no-freedb"] @@ -4040,7 +4917,43 @@ @UTIL_TRACKSPLIT def test_errors(self): - filename = audiotools.Messenger("tracksplit", None).filename + from audiotools.text import (ERR_OUTPUT_IS_INPUT, + ERR_DUPLICATE_OUTPUT_FILE, + ERR_UNSUPPORTED_COMPRESSION_MODE, + ERR_UNKNOWN_FIELD, + LAB_SUPPORTED_FIELDS, + ERR_1_FILE_REQUIRED, + ERR_TRACKSPLIT_NO_CUESHEET, + ERR_TRACKSPLIT_OVERLONG_CUESHEET, + +) + + #ensure that unsplitting file to itself generates an error + track = self.type.from_pcm(self.unsplit_file.name, + BLANK_PCM_Reader(18)) + self.assertEqual( + self.__run_app__( + ["tracksplit", self.unsplit_file.name, + "--no-freedb", "--no-musicbrainz", + "--cue", self.cuesheet3.name, + "-d", os.path.dirname(self.unsplit_file.name), + "--format", os.path.basename(self.unsplit_file.name)]), 1) + self.__check_error__(ERR_OUTPUT_IS_INPUT % + (audiotools.Filename(self.unsplit_file.name),)) + + #ensure that unsplitting file to identical names generates an error + self.assertEqual( + self.__run_app__( + ["tracksplit", self.unsplit_file.name, + "--no-freedb", "--no-musicbrainz", + "--cue", self.cuesheet.name, + "-d", os.path.dirname(self.unsplit_file.name), + "--format", "foo"]), 1) + self.__check_error__( + ERR_DUPLICATE_OUTPUT_FILE % + (audiotools.Filename( + os.path.join(os.path.dirname(self.unsplit_file.name), + "foo")),)) track1 = self.type.from_pcm(self.unsplit_file.name, BLANK_PCM_Reader(18)) @@ -4072,21 +4985,22 @@ if ("-q" in options): self.__check_error__( - _(u"\"%(quality)s\" is not a supported compression mode for type \"%(type)s\"") % + ERR_UNSUPPORTED_COMPRESSION_MODE % {"quality": "bar", "type": audiotools.DEFAULT_TYPE}) continue if ("--format" in options): self.__check_error__( - _(u"Unknown field \"%s\" in file format") % ("foo")) - self.__check_info__(_(u"Supported fields are:")) + ERR_UNKNOWN_FIELD % ("foo")) + self.__check_info__(LAB_SUPPORTED_FIELDS) for field in sorted(audiotools.MetaData.FIELDS + \ ("album_track_number", "suffix")): if (field == 'track_number'): self.__check_info__(u"%(track_number)2.2d") else: self.__check_info__(u"%%(%s)s" % (field)) + self.__check_info__(u"%(basename)s") continue if ("-d" in options): @@ -4097,31 +5011,31 @@ audiotools.MetaData(track_number=1, track_total=3))) self.__check_error__( - _(u"[Errno 13] Permission denied: \'%s\'") % \ - (output_path)) + u"[Errno 13] Permission denied: \'%s\'" % + (output_path)) continue self.assertEqual(self.__run_app__( ["tracksplit", "-t", "flac", "-d", self.output_dir]), 1) - self.__check_error__(_(u"You must specify exactly 1 supported audio file")) + self.__check_error__(ERR_1_FILE_REQUIRED) self.assertEqual(self.__run_app__( ["tracksplit", "-t", "flac", "-d", self.output_dir, self.unsplit_file.name, self.unsplit_file2.name]), 1) - self.__check_error__(_(u"You must specify exactly 1 supported audio file")) + self.__check_error__(ERR_1_FILE_REQUIRED) self.assertEqual(self.__run_app__( ["tracksplit", "-t", "flac", "-d", self.output_dir, self.unsplit_file.name]), 1) - self.__check_error__(_(u"You must specify a cuesheet to split audio file")) + self.__check_error__(ERR_TRACKSPLIT_NO_CUESHEET) self.assertEqual(self.__run_app__( ["tracksplit", "-t", "flac", "-d", self.output_dir, "--cue", self.cuesheet.name, track2.filename]), 1) - self.__check_error__(_(u"Cuesheet too long for track being split")) + self.__check_error__(ERR_TRACKSPLIT_OVERLONG_CUESHEET) #FIXME? - check for broken cue sheet output?
View file
audiotools-2.18.tar.gz/track2cd -> audiotools-2.19.tar.gz/track2cd
Changed
@@ -21,22 +21,20 @@ import os import os.path import sys -import audiotools -import audiotools.toc import tempfile import subprocess -import gettext - -gettext.install("audiotools", unicode=True) +import audiotools +import audiotools.toc +import audiotools.text as _ MAX_CPUS = audiotools.MAX_JOBS def convert_to_wave(progress, audiofile, wave_filename): try: - if ((audiofile.sample_rate() == 44100) and - (audiofile.channels() == 2) and - (audiofile.bits_per_sample() == 16)): # already CD quality + if (((audiofile.sample_rate() == 44100) and + (audiofile.channels() == 2) and + (audiofile.bits_per_sample() == 16))): # already CD quality audiofile.convert(target_path=wave_filename, target_class=audiotools.WaveAudio, progress=progress) @@ -61,19 +59,18 @@ if (__name__ == '__main__'): parser = audiotools.OptionParser( - usage=_(u"%prog [options] <track 1> [track 2] ..."), + usage=_.USAGE_TRACK2CD, version="Python Audio Tools %s" % (audiotools.VERSION)) parser.add_option( "-c", "--cdrom", dest="dev", - default=audiotools.DEFAULT_CDROM, - help=_(u"cdrom device to use")) + default=audiotools.DEFAULT_CDROM) parser.add_option( "-s", "--speed", dest="speed", default=20, type="int", - help=_(u"the speed to burn the CD at")) + help=_.OPT_SPEED) parser.add_option( '--cue', @@ -81,7 +78,7 @@ type='string', dest='cuesheet', metavar='FILENAME', - help=_(u'the cuesheet to use for writing tracks')) + help=_.OPT_CUESHEET_TRACK2CD) parser.add_option( '-j', '--joint', @@ -89,7 +86,7 @@ type='int', default=MAX_CPUS, dest='max_processes', - help=_(u'the maximum number of processes to run at a time')) + help=_.OPT_JOINT) parser.add_option( '-V', '--verbose', @@ -97,31 +94,34 @@ dest='verbosity', choices=audiotools.VERBOSITY_LEVELS, default=audiotools.DEFAULT_VERBOSITY, - help=_(u'the verbosity level to execute at')) + help=_.OPT_VERBOSE) (options, args) = parser.parse_args() msg = audiotools.Messenger("track2cd", options) if (options.max_processes < 1): - msg.error(_(u'You must run at least 1 process at a time')) + msg.error(_.ERR_INVALID_JOINT) sys.exit(1) else: max_processes = options.max_processes - audiofiles = audiotools.open_files(args, sorted=False, messenger=msg) + audiofiles = audiotools.open_files(args, + sorted=False, + messenger=msg, + warn_duplicates=True) if (len(audiofiles) < 1): - msg.error(_(u"You must specify at least 1 supported audio file")) + msg.error(_.ERR_FILES_REQUIRED) sys.exit(1) - if ((len(audiofiles) == 1) and - (audiofiles[0].get_cuesheet() is not None)): + if (((len(audiofiles) == 1) and + (audiofiles[0].get_cuesheet() is not None))): #writing a single file with an embedded cuesheet #so extract its contents to wave/cue and call cdrdao if (not audiotools.BIN.can_execute(audiotools.BIN['cdrdao'])): - msg.error(_(u'Unable to find "cdrdao" executable')) - msg.info(_(u'Please install "cdrdao" to burn CDs')) + msg.error(_.ERR_NO_CDRDAO) + msg.info(_.ERR_GET_CDRDAO) sys.exit(1) cuesheet = audiofiles[0].get_cuesheet() @@ -129,16 +129,17 @@ temptoc = tempfile.NamedTemporaryFile(suffix='.toc') tempwav = tempfile.NamedTemporaryFile(suffix='.wav') - temptoc.write(audiotools.toc.TOCFile.file( + temptoc.write( + audiotools.toc.TOCFile.file( cuesheet, os.path.basename(tempwav.name))) temptoc.flush() progress = audiotools.SingleProgressDisplay( - msg, _(u"Converting audio file")) + msg, _.LAB_CONVERTING_FILE) - if ((audiofiles[0].sample_rate() == 44100) and - (audiofiles[0].channels() == 2) and - (audiofiles[0].bits_per_sample() == 16)): + if (((audiofiles[0].sample_rate() == 44100) and + (audiofiles[0].channels() == 2) and + (audiofiles[0].bits_per_sample() == 16))): #already CD quality, so no additional conversion necessary temptrack = audiotools.WaveAudio.from_pcm( tempwav.name, @@ -193,20 +194,20 @@ #so combine them into a single wave/cue and call cdrdao if (not audiotools.BIN.can_execute(audiotools.BIN['cdrdao'])): - msg.error(_(u'Unable to find "cdrdao" executable')) - msg.info(_(u'Please install "cdrdao" to burn CDs')) + msg.error(_.ERR_NO_CDRDAO) + msg.info(_.ERR_GET_CDRDAO) sys.exit(1) if (len(set([f.sample_rate() for f in audiofiles])) != 1): - msg.error(_(u"All audio files must have the same sample rate")) + msg.error(_.ERR_SAMPLE_RATE_MISMATCH) sys.exit(1) if (len(set([f.channels() for f in audiofiles])) != 1): - msg.error(_(u"All audio files must have the same channel count")) + msg.error(_.ERR_CHANNEL_COUNT_MISMATCH) sys.exit(1) if (len(set([f.bits_per_sample() for f in audiofiles])) != 1): - msg.error(_(u"All audio files must have the same bits per sample")) + msg.error(_.ERR_BPS_MISMATCH) sys.exit(1) try: @@ -218,17 +219,16 @@ temptoc = tempfile.NamedTemporaryFile(suffix='.toc') tempwav = tempfile.NamedTemporaryFile(suffix='.wav') - temptoc.write(audiotools.toc.TOCFile.file( - toc, os.path.basename(tempwav.name))) + temptoc.write( + audiotools.toc.TOCFile.file(toc, os.path.basename(tempwav.name))) temptoc.flush() input_frames = sum([af.total_frames() for af in audiofiles]) - if ((audiofiles[0].sample_rate() == 44100) and - (audiofiles[0].channels() == 2) and - (audiofiles[0].bits_per_sample() == 16)): - pcmreader = \ - audiotools.PCMCat(iter([af.to_pcm() for af in audiofiles])) + if (((audiofiles[0].sample_rate() == 44100) and + (audiofiles[0].channels() == 2) and + (audiofiles[0].bits_per_sample() == 16))): + pcmreader = audiotools.PCMCat([af.to_pcm() for af in audiofiles]) output_frames = input_frames else: #this presumes a cuesheet and non-CD audio @@ -236,7 +236,7 @@ #envision a case in which this happens pcmreader = audiotools.PCMConverter( pcmreader=audiotools.PCMCat( - iter([af.to_pcm() for af in audiofiles])), + [af.to_pcm() for af in audiofiles]), sample_rate=44100, channels=2, channel_mask=0x3, @@ -245,10 +245,11 @@ audiofiles[0].sample_rate()) progress = audiotools.SingleProgressDisplay( - msg, _(u"Converting audio files")) + msg, _.LAB_CONVERTING_FILE) try: - write_offset = int(audiotools.config.get_default( + write_offset = int( + audiotools.config.get_default( "System", "cdrom_write_offset", "0")) except ValueError: write_offset = 0 @@ -257,18 +258,18 @@ temptrack = audiotools.WaveAudio.from_pcm( tempwav.name, audiotools.PCMReaderProgress( - pcmreader=pcmreader, - total_frames=output_frames, - progress=progress.update)) + pcmreader=pcmreader, + total_frames=output_frames, + progress=progress.update)) else: temptrack = audiotools.WaveAudio.from_pcm( tempwav.name, audiotools.PCMReaderProgress( - pcmreader=audiotools.PCMReaderWindow(pcmreader, - write_offset, - input_frames), - total_frames=output_frames, - progress=progress_update)) + pcmreader=audiotools.PCMReaderWindow(pcmreader, + write_offset, + input_frames), + total_frames=output_frames, + progress=progress_update)) progress.clear() @@ -300,8 +301,8 @@ #so extract them to waves and call cdrecord if (not audiotools.BIN.can_execute(audiotools.BIN['cdrecord'])): - msg.error(_(u'Unable to find "cdrecord" executable')) - msg.info(_(u'Please install "cdrecord" to burn CDs')) + msg.error(_.ERR_NO_CDRECORD) + msg.info(_.ERR_GET_CDRECORD) sys.exit(1) exec_args = [audiotools.BIN['cdrecord']] @@ -313,7 +314,6 @@ exec_args.append(options.dev) exec_args.append("-dao") - exec_args.append("-pad") exec_args.append("-audio") temp_pool = [] @@ -328,10 +328,11 @@ f = tempfile.mkstemp(suffix='.wav') temp_pool.append(f) wave_files.append(f[1]) - queue.execute(function=convert_to_wave, - progress_text=msg.filename(audiofile.filename), - audiofile=audiofile, - wave_filename=f[1]) + queue.execute( + function=convert_to_wave, + progress_text=audiotools.Filename(audiofile.filename), + audiofile=audiofile, + wave_filename=f[1]) queue.run(max_processes) @@ -341,7 +342,7 @@ except (audiotools.UnsupportedFile, audiotools.InvalidFile, IOError): - msg.error(_(u"Not all files are valid. Unable to write CD")) + msg.error(_.ERR_TRACK2CD_INVALIDFILE) sys.exit(1) exec_args += wave_files
View file
audiotools-2.18.tar.gz/track2track -> audiotools-2.19.tar.gz/track2track
Changed
@@ -18,22 +18,23 @@ #Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import audiotools import sys import os import os.path import select -import gettext import cPickle +import audiotools +import audiotools.ui +import audiotools.text as _ +import termios -gettext.install("audiotools", unicode=True) MAX_CPUS = audiotools.MAX_JOBS def convert(progress, source_audiofile, destination_filename, destination_class, compression, - metadata, thumbnail_images): + metadata): destination_audiofile = source_audiofile.convert( destination_filename, destination_class, @@ -41,19 +42,16 @@ progress) if (metadata is not None): - if (thumbnail_images): - for img in metadata.images(): - metadata.delete_image(img) - metadata.add_image(img.thumbnail( - audiotools.THUMBNAIL_SIZE, - audiotools.THUMBNAIL_SIZE, - audiotools.THUMBNAIL_FORMAT)) - destination_audiofile.set_metadata(metadata) - else: - destination_audiofile.set_metadata(audiotools.MetaData( + elif ((source_audiofile.track_number() is not None) or + (source_audiofile.album_number() is not None)): + destination_audiofile.set_metadata( + audiotools.MetaData( track_number=source_audiofile.track_number(), album_number=source_audiofile.album_number())) + else: + #don't set any metadata + pass existing_cuesheet = source_audiofile.get_cuesheet() if (existing_cuesheet is not None): @@ -64,24 +62,39 @@ if (__name__ == '__main__'): parser = audiotools.OptionParser( - usage=_(u'%prog [options] <track 1> [track 2] ...'), + usage=_.USAGE_TRACK2TRACK, version="Python Audio Tools %s" % (audiotools.VERSION)) - conversion = audiotools.OptionGroup(parser, _(u"Conversion Options")) + parser.add_option( + '-I', '--interactive', + action='store_true', + default=False, + dest='interactive', + help=_.OPT_INTERACTIVE_OPTIONS) + + parser.add_option( + '-V', '--verbose', + action='store', + dest='verbosity', + choices=audiotools.VERBOSITY_LEVELS, + default=audiotools.DEFAULT_VERBOSITY, + help=_.OPT_VERBOSE) + + conversion = audiotools.OptionGroup(parser, _.OPT_CAT_CONVERSION) conversion.add_option( '-t', '--type', action='store', dest='type', - choices=audiotools.TYPE_MAP.keys(), - help=_(u'the type of audio track to convert to')) + choices=sorted(audiotools.TYPE_MAP.keys() + ['help']), + help=_.OPT_TYPE) conversion.add_option( '-q', '--quality', action='store', type='string', dest='quality', - help=_(u'the quality to store audio tracks at')) + help=_.OPT_QUALITY) conversion.add_option( '-d', '--dir', @@ -89,7 +102,7 @@ type='string', dest='dir', default='.', - help=_(u'the directory to store converted audio tracks')) + help=_.OPT_DIR) conversion.add_option( '--format', @@ -97,13 +110,13 @@ type='string', default=None, dest='format', - help=_(u'the format string for new filenames')) + help=_.OPT_FORMAT) conversion.add_option( '-o', '--output', action='store', dest='output', - help=_(u'output filename to use, overriding default and -d')) + help=_.OPT_OUTPUT_TRACK2TRACK) conversion.add_option( '-j', '--joint', @@ -111,63 +124,96 @@ type='int', default=MAX_CPUS, dest='max_processes', - help=_(u'the maximum number of processes to run at a time')) + help=_.OPT_JOINT) parser.add_option_group(conversion) - metadata = audiotools.OptionGroup(parser, _(u"Metadata Options")) + lookup = audiotools.OptionGroup(parser, _.OPT_CAT_CD_LOOKUP) + + lookup.add_option( + '-M', '--metadata-lookup', action='store_true', + default=False, dest='metadata_lookup', + help=_.OPT_METADATA_LOOKUP) + + lookup.add_option( + '--musicbrainz-server', action='store', + type='string', dest='musicbrainz_server', + default=audiotools.MUSICBRAINZ_SERVER, + metavar='HOSTNAME') + lookup.add_option( + '--musicbrainz-port', action='store', + type='int', dest='musicbrainz_port', + default=audiotools.MUSICBRAINZ_PORT, + metavar='PORT') + lookup.add_option( + '--no-musicbrainz', action='store_false', + dest='use_musicbrainz', + default=audiotools.MUSICBRAINZ_SERVICE, + help=_.OPT_NO_MUSICBRAINZ) + + lookup.add_option( + '--freedb-server', action='store', + type='string', dest='freedb_server', + default=audiotools.FREEDB_SERVER, + metavar='HOSTNAME') + lookup.add_option( + '--freedb-port', action='store', + type='int', dest='freedb_port', + default=audiotools.FREEDB_PORT, + metavar='PORT') + lookup.add_option( + '--no-freedb', action='store_false', + dest='use_freedb', + default=audiotools.FREEDB_SERVICE, + help=_.OPT_NO_FREEDB) + + lookup.add_option( + '-D', '--default', + dest='use_default', action='store_true', default=False, + help=_.OPT_DEFAULT) + + parser.add_option_group(lookup) + + metadata = audiotools.OptionGroup(parser, _.OPT_CAT_METADATA) metadata.add_option( - '-T', '--thumbnail', - action='store_true', - default=False, - dest='thumbnail', - help=_(u'convert embedded images to smaller thumbnails ' + - u'during conversion')) - - #if adding ReplayGain is a lossless process - #(i.e. added as tags rather than modifying track data) - #add_replay_gain should default to True - #if not, add_replay_gain should default to False - #which is which depends on the track type - metadata.add_option( '--replay-gain', action='store_true', dest='add_replay_gain', - help=_(u'add ReplayGain metadata to newly created tracks')) + help=_.OPT_REPLAY_GAIN) metadata.add_option( '--no-replay-gain', action='store_false', dest='add_replay_gain', - help=_(u'do not add ReplayGain metadata to newly created tracks')) + help=_.OPT_NO_REPLAY_GAIN) parser.add_option_group(metadata) - parser.add_option( - '-V', '--verbose', - action='store', - dest='verbosity', - choices=audiotools.VERBOSITY_LEVELS, - default=audiotools.DEFAULT_VERBOSITY, - help=_(u'the verbosity level to execute at')) - (options, args) = parser.parse_args() msg = audiotools.Messenger("track2track", options) + #ensure interactive mode is available, if selected + if (options.interactive and (not audiotools.ui.AVAILABLE)): + audiotools.ui.not_available_message(msg) + sys.exit(1) + #if one specifies incompatible output options, #complain about it right away if (options.output is not None): if (options.dir != "."): - msg.error(_(u"-o and -d options are not compatible")) - msg.info(_(u"Please specify either -o or -d but not both")) + msg.error(_.ERR_TRACK2TRACK_O_AND_D) + msg.info(_.ERR_TRACK2TRACK_O_AND_D_SUGGESTION) sys.exit(1) if (options.format is not None): - msg.warning(_(u"--format has no effect when used with -o")) + msg.warning(_.ERR_TRACK2TRACK_O_AND_FORMAT) #get the AudioFile class we are converted to - if (options.output is None): + if (options.type == 'help'): + audiotools.ui.show_available_formats(msg) + sys.exit(0) + elif (options.output is None): if (options.type is not None): AudioType = audiotools.TYPE_MAP[options.type] else: @@ -184,195 +230,319 @@ #ensure the selected compression is compatible with that class if (options.quality == 'help'): - if (len(AudioType.COMPRESSION_MODES) > 1): - msg.info(_(u"Available compression types for %s:") % \ - (AudioType.NAME)) - for mode in AudioType.COMPRESSION_MODES: - msg.new_row() - if (mode == audiotools.__default_quality__(AudioType.NAME)): - msg.output_column(msg.ansi(mode.decode('ascii'), - [msg.BOLD, - msg.UNDERLINE]), True) - else: - msg.output_column(mode.decode('ascii'), True) - if (mode in AudioType.COMPRESSION_DESCRIPTIONS): - msg.output_column(u" : ") - else: - msg.output_column(u" ") - msg.output_column( - AudioType.COMPRESSION_DESCRIPTIONS.get(mode, u"")) - msg.info_rows() - else: - msg.error(_(u"Audio type %s has no compression modes") % \ - (AudioType.NAME)) + audiotools.ui.show_available_qualities(msg, AudioType) sys.exit(0) elif (options.quality is None): options.quality = audiotools.__default_quality__(AudioType.NAME) elif (options.quality not in AudioType.COMPRESSION_MODES): - msg.error( - _(u"\"%(quality)s\" is not a supported " + - u"compression mode for type \"%(type)s\"") % - {"quality": options.quality, - "type": AudioType.NAME}) + msg.error(_.ERR_UNSUPPORTED_COMPRESSION_MODE % + {"quality": options.quality, + "type": AudioType.NAME}) sys.exit(1) #grab the list of AudioFile objects we are converting from - audiofiles = audiotools.open_files(args, messenger=msg) + input_filenames = set([]) + try: + audiofiles = audiotools.open_files(args, + messenger=msg, + no_duplicates=True, + opened_files=input_filenames) + except audiotools.DuplicateFile, err: + msg.error(_.ERR_DUPLICATE_FILE % (err.filename,)) + sys.exit(1) + if (len(audiofiles) < 1): - msg.error(_(u"You must specify at least 1 supported audio file")) + msg.error(_.ERR_FILES_REQUIRED) sys.exit(1) if (options.max_processes < 1): - msg.error(_(u'You must run at least 1 process at a time')) + msg.error(_.ERR_INVALID_JOINT) sys.exit(1) - if ((options.output is not None) and - (len(audiofiles) != 1)): - msg.error(_(u'You may specify only 1 input file for use with -o')) + if ((options.output is not None) and (len(audiofiles) != 1)): + msg.error(_.ERR_TRACK2TRACK_O_AND_MULTIPLE) sys.exit(1) - #determine whether to add ReplayGain by default - if (options.add_replay_gain is None): - options.add_replay_gain = ( - audiotools.ADD_REPLAYGAIN and - AudioType.lossless_replay_gain() and - audiotools.applicable_replay_gain(audiofiles)) - - if (options.thumbnail): - if (not audiotools.can_thumbnail()): - msg.error(_(u"Unable to generate thumbnails")) - msg.info( - _(u"Please install the Python Imaging Library")) - msg.info( - _(u"available at http://www.pythonware.com/products/pil/")) - msg.info(_(u"to enable image resizing")) - sys.exit(1) - - if (audiotools.THUMBNAIL_FORMAT.upper() not in - audiotools.thumbnail_formats()): - msg.error(_(u"Unsupported thumbnail format \"%s\"") % - (audiotools.THUMBNAIL_FORMAT)) - msg.info(_(u"Available formats are: %s") % - (", ".join(audiotools.thumbnail_formats()))) - sys.exit(1) - - quality = options.quality - max_processes = options.max_processes - if (options.output is None): #the default encoding method, without an output file - base_directory = options.dir - + previous_output_widget = None queue = audiotools.ExecProgressQueue(audiotools.ProgressDisplay(msg)) - for audiofile in audiofiles: - track_metadata = audiofile.get_metadata() - - #use old track's metadata for new track, if any - track_metadata = audiofile.get_metadata() - - try: - filename = os.path.join( - base_directory, - AudioType.track_name(file_path=audiofile.filename, - track_metadata=track_metadata, - format=options.format)) - except audiotools.UnsupportedTracknameField, err: - err.error_msg(msg) - sys.exit(1) - - #try to create subdirectories in advance - #so as to bail out as early as possible - try: - audiotools.make_dirs(filename) - except OSError: - msg.error(_(u"Unable to write \"%s\"") % \ - (filename)) - sys.exit(1) - - #queue up conversion job - queue.execute(function=convert, - progress_text=msg.filename(filename), - completion_output=u"%s -> %s" % \ - (msg.filename(audiofile.filename), - msg.filename(filename)), - source_audiofile=audiofile, - destination_filename=filename, - destination_class=AudioType, - compression=quality, - metadata=track_metadata, - thumbnail_images=options.thumbnail) - - #perform all queued conversion jobs + #split tracks by album only if metadata lookups are required + if (options.metadata_lookup): + albums_iter = audiotools.iter_last( + audiotools.group_tracks(audiofiles)) + else: + albums_iter = iter([(True, audiofiles)]) + + for (last_album, album_tracks) in albums_iter: + format = (audiotools.FILENAME_FORMAT if + options.format is None else + options.format) + + track_metadatas = [f.get_metadata() for f in album_tracks] + + if (not options.metadata_lookup): + #pull metadata from existing files, if any + metadata_choices = [[f.get_metadata() for f in album_tracks]] + else: + #perform CD lookup for existing files + metadata_choices = audiotools.track_metadata_lookup( + audiofiles=album_tracks, + musicbrainz_server=options.musicbrainz_server, + musicbrainz_port=options.musicbrainz_port, + freedb_server=options.freedb_server, + freedb_port=options.freedb_port, + use_musicbrainz=options.use_musicbrainz, + use_freedb=options.use_freedb) + + #and prepend metadata from existing files as an option, if any + if (track_metadatas != [None] * len(track_metadatas)): + metadata_choices.insert( + 0, + [(m if m is not None else audiotools.MetaData()) + for m in track_metadatas]) + + if (options.interactive): + #pick options using interactive widget + + #if previous widget has been used, + #pull default options from it + if (previous_output_widget is None): + output_directory = options.dir + format_string = format + output_class = AudioType + quality = options.quality + else: + output_directory = \ + previous_output_widget.output_directory() + format_string = \ + previous_output_widget.format_string() + output_class = \ + previous_output_widget.output_class() + quality = \ + previous_output_widget.quality() + + #execute output widget per album's worth of tracks + output_widget = audiotools.ui.OutputFiller( + track_labels=[ + unicode(audiotools.Filename(f.filename).basename()) + for f in album_tracks], + metadata_choices=metadata_choices, + input_filenames=[audiotools.Filename(f.filename) + for f in album_tracks], + output_directory=output_directory, + format_string=format_string, + output_class=output_class, + quality=quality, + completion_label=((_.LAB_TRACK2TRACK_APPLY if + (len(album_tracks) != 1) else + _.LAB_TRACK2TRACK_APPLY_1) + if last_album + else _.LAB_TRACK2TRACK_NEXT)) + + loop = audiotools.ui.urwid.MainLoop( + output_widget, + audiotools.ui.style(), + unhandled_input=output_widget.handle_text, + pop_ups=True) + try: + loop.run() + msg.ansi_clearscreen() + except (termios.error, IOError): + msg.error(_.ERR_TERMIOS_ERROR) + msg.info(_.ERR_TERMIOS_SUGGESTION) + msg.info(audiotools.ui.xargs_suggestion(sys.argv)) + sys.exit(1) + + previous_output_widget = output_widget + + #any cancellation quits the entire transcode + if (not output_widget.cancelled()): + from itertools import izip + + #merge input track metadata + #with new metadata returned from widget + output_tracks = [] + for (current_metadata, + (output_class, + output_filename, + output_quality, + output_metadata) + ) in izip(iter(track_metadatas), + output_widget.output_tracks()): + if (current_metadata is not None): + for attr in audiotools.MetaData.FIELDS: + original_value = getattr(current_metadata, + attr) + updated_value = getattr(output_metadata, + attr) + if (original_value != updated_value): + setattr(current_metadata, + attr, + updated_value) + else: + output_tracks.append((output_class, + output_filename, + output_quality, + current_metadata)) + else: + output_tracks.append((output_class, + output_filename, + output_quality, + output_metadata)) + else: + sys.exit(0) + else: + #pick options without using GUI + try: + output_tracks = list( + audiotools.ui.process_output_options( + metadata_choices=metadata_choices, + input_filenames=[audiotools.Filename(f.filename) + for f in album_tracks], + output_directory=options.dir, + format_string=format, + output_class=AudioType, + quality=options.quality, + msg=msg, + use_default=options.use_default)) + except audiotools.UnsupportedTracknameField, err: + err.error_msg(msg) + sys.exit(1) + except (audiotools.InvalidFilenameFormat, + audiotools.OutputFileIsInput, + audiotools.DuplicateOutputFile), err: + msg.error(unicode(err)) + sys.exit(1) + + #queue jobs to be executed + for (audiofile, (output_class, + output_filename, + output_quality, + output_metadata)) in zip(album_tracks, + output_tracks): + #try to create subdirectories in advance + #so as to bail out as early as possible + try: + audiotools.make_dirs(str(output_filename)) + except OSError: + msg.error(_.ERR_ENCODING_ERROR % (output_filename,)) + sys.exit(1) + + queue.execute( + function=convert, + progress_text=unicode(output_filename), + completion_output= + (_.LAB_ENCODE % + {"source": audiotools.Filename(audiofile.filename), + "destination": output_filename}), + source_audiofile=audiofile, + destination_filename=str(output_filename), + destination_class=output_class, + compression=output_quality, + metadata=output_metadata) + + #perform actual track conversion try: - queue.run(max_processes) + queue.run(options.max_processes) except audiotools.EncodingError, err: msg.error(unicode(err)) sys.exit(1) + output_files = audiotools.open_files(queue.results.values()) + #add ReplayGain to converted files, if necessary - if (options.add_replay_gain and AudioType.can_add_replay_gain()): + if ((audiotools.ADD_REPLAYGAIN and + (options.add_replay_gain if (options.add_replay_gain is not None) + else output_class.lossless_replay_gain()) and + output_class.can_add_replay_gain(output_files))): + #separate encoded files by album_name and album_number + for album in audiotools.group_tracks(output_files): + #add ReplayGain to groups of files + #belonging to the same album + + album_number = set([a.album_number() for a in album]).pop() + + #FIXME - should pull ReplayGain text from elsewhere + queue.execute( + output_class.add_replay_gain, + (u"%s ReplayGain%s" % + ((u"Adding" if output_class.lossless_replay_gain() else + u"Applying"), + (u"" if album_number is None else + (u" to album %d" % (album_number))))), + (u"ReplayGain %s%s" % + ((u"added" if output_class.lossless_replay_gain() else + u"applied"), + (u"" if album_number is None else + (u" to album %d" % (album_number))))), + [a.filename for a in album]) + try: - #separate encoded files by album_name and album_number - for album in audiotools.group_tracks( - audiotools.open_files(queue.results.values())): - #add ReplayGain to groups of files - #belonging to the same album - - album_number = set([a.album_number() for a in album]).pop() - - if (album_number == 0): - if (AudioType.lossless_replay_gain()): - progress_text = _(u"Adding ReplayGain") - completion_output = _(u"ReplayGain added") - else: - progress_text = _(u"Applying ReplayGain") - completion_output = _(u"ReplayGain applied") - else: - if (AudioType.lossless_replay_gain()): - progress_text = ( - _(u"Adding ReplayGain to album %d") % - (album_number)) - completion_output = ( - _(u"ReplayGain added to album %d") % - (album_number)) - else: - progress_text = ( - _(u"Applying ReplayGain to album %d") % - (album_number)) - completion_output = ( - _(u"ReplayGain applied to album %d") % - (album_number)) - - queue.execute(AudioType.add_replay_gain, - progress_text, - completion_output, - [a.filename for a in album]) - - queue.run(max_processes) + queue.run(options.max_processes) except ValueError, err: msg.error(unicode(err)) sys.exit(1) else: #encoding only a single file audiofile = audiofiles[0] - track_metadata = audiofile.get_metadata() + input_filename = audiotools.Filename(audiofile.filename) + + if (options.interactive): + track_metadata = audiofile.get_metadata() - queue = audiotools.ExecProgressQueue( - audiotools.ProgressDisplay( - audiotools.SilentMessenger("track2track"))) + output_widget = audiotools.ui.SingleOutputFiller( + track_label=unicode(input_filename), + metadata_choices=[track_metadata if track_metadata is not None + else audiotools.MetaData()], + input_filenames=[input_filename], + output_file=options.output, + output_class=AudioType, + quality=options.quality, + completion_label=_.LAB_TRACK2TRACK_APPLY_1) + loop = audiotools.ui.urwid.MainLoop( + output_widget, + audiotools.ui.style(), + unhandled_input=output_widget.handle_text, + pop_ups=True) + loop.run() + msg.ansi_clearscreen() + + if (not output_widget.cancelled()): + (destination_class, + output_filename, + compression, + track_metadata) = output_widget.output_track() + else: + sys.exit(0) + else: + output_filename = audiotools.Filename(options.output) + destination_class = AudioType + compression = options.quality + track_metadata = audiofile.get_metadata() - queue.execute(function=convert, - source_audiofile=audiofile, - destination_filename=options.output, - destination_class=AudioType, - compression=quality, - metadata=track_metadata, - thumbnail_images=options.thumbnail) + if (input_filename == output_filename): + msg.error(_.ERR_OUTPUT_IS_INPUT % + (output_filename,)) + sys.exit(1) + progress = audiotools.SingleProgressDisplay( + messenger=msg, + progress_text=unicode(output_filename)) try: - queue.run(1) + convert(progress=progress.update, + source_audiofile=audiofile, + destination_filename=str(output_filename), + destination_class=destination_class, + compression=compression, + metadata=track_metadata) + progress.clear() + + msg.output(_.LAB_ENCODE % {"source": input_filename, + "destination": output_filename}) except audiotools.EncodingError, err: + progress.clear() msg.error(unicode(err)) sys.exit(1)
View file
audiotools-2.18.tar.gz/trackcat -> audiotools-2.19.tar.gz/trackcat
Changed
@@ -18,190 +18,322 @@ #Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import audiotools import sys import os import os.path -import subprocess -import gettext +import audiotools +import audiotools.ui +import audiotools.text as _ +import termios -gettext.install("audiotools", unicode=True) -if (__name__ == '__main__'): - parser = audiotools.OptionParser( - usage=_(u'%prog [options] [-o output] <track 1> [track 2] ...'), - version="Python Audio Tools %s" % (audiotools.VERSION)) +def merge_metadatas(metadatas): + """given a list of MetaData objects, or Nones, + returns a single MetaData object + containing only fields that are the same across all objects + or returns None if all MetaData objects are None""" - conversion = audiotools.OptionGroup(parser, _(u"Encoding Options")) + track_metadatas = [m for m in metadatas if m is not None] - conversion.add_option('-t', '--type', - action='store', - dest='type', - choices=audiotools.TYPE_MAP.keys(), - help=_(u'the type of audio value to convert to')) + if (len(track_metadatas) == 0): + return None + elif (len(track_metadatas) == 1): + return track_metadatas[0] + else: + track_fields = dict([(field, + set([getattr(m, field) + for m in track_metadatas])) + for field in audiotools.MetaData.FIELDS]) - conversion.add_option('-q', '--quality', - action='store', - type='string', - dest='quality', - help=_(u'the quality to store audio values at')) + metadata = audiotools.MetaData(**dict([(field, values.pop()) + for (field, values) in + track_fields.items() + if (len(values) == 1)])) - conversion.add_option('-o', '--output', dest='filename', - metavar='FILE', - help=_(u'the output file')) + #port over non-duplicate images + images = [] + for m in track_metadatas: + for i in m.images(): + if (i not in images): + images.append(i) + for i in images: + metadata.add_image(i) - parser.add_option_group(conversion) + return metadata - parser.add_option('--cue', - action='store', - type='string', - dest='cuesheet', - metavar='FILENAME', - help=_(u'a cuesheet to embed in the output file')) - parser.add_option('-V', '--verbose', - action='store', - dest='verbosity', - choices=audiotools.VERBOSITY_LEVELS, - default=audiotools.DEFAULT_VERBOSITY, - help=_(u'the verbosity level to execute at')) +if (__name__ == '__main__'): + parser = audiotools.OptionParser( + usage=_.USAGE_TRACKCAT, + version="Python Audio Tools %s" % (audiotools.VERSION)) - (options, args) = parser.parse_args() - msg = audiotools.Messenger("trackcat", options) + parser.add_option( + '-I', '--interactive', + action='store_true', + default=False, + dest='interactive', + help=_.OPT_INTERACTIVE_OPTIONS) + + parser.add_option( + '--cue', + action='store', + type='string', + dest='cuesheet', + metavar='FILENAME', + help=_.OPT_CUESHEET_TRACKCAT) + + parser.add_option( + '-V', '--verbose', + action='store', + dest='verbosity', + choices=audiotools.VERBOSITY_LEVELS, + default=audiotools.DEFAULT_VERBOSITY, + help=_.OPT_VERBOSE) + + conversion = audiotools.OptionGroup(parser, _.OPT_CAT_ENCODING) + + conversion.add_option( + '-o', '--output', + dest='filename', + metavar='FILE', + help=_.OPT_OUTPUT_TRACKCAT) + + conversion.add_option( + '-t', '--type', + action='store', + dest='type', + choices=sorted(audiotools.TYPE_MAP.keys() + ['help']), + help=_.OPT_TYPE) + + conversion.add_option( + '-q', '--quality', + action='store', + type='string', + dest='quality', + help=_.OPT_QUALITY) - if (options.filename is None): - msg.error(_(u'You must specify an output file')) - sys.exit(1) + parser.add_option_group(conversion) - #get the AudioFile class we are converted to - if (options.type is not None): - AudioType = audiotools.TYPE_MAP[options.type] - else: - try: - AudioType = audiotools.filename_to_type(options.filename) - except audiotools.UnknownAudioType, exp: - exp.error_msg(msg) - sys.exit(1) + lookup = audiotools.OptionGroup(parser, _.OPT_CAT_CD_LOOKUP) + + lookup.add_option( + '-M', '--metadata-lookup', action='store_true', + default=False, dest='metadata_lookup', + help=_.OPT_METADATA_LOOKUP) + + lookup.add_option( + '--musicbrainz-server', action='store', + type='string', dest='musicbrainz_server', + default=audiotools.MUSICBRAINZ_SERVER, + metavar='HOSTNAME') + lookup.add_option( + '--musicbrainz-port', action='store', + type='int', dest='musicbrainz_port', + default=audiotools.MUSICBRAINZ_PORT, + metavar='PORT') + lookup.add_option( + '--no-musicbrainz', action='store_false', + dest='use_musicbrainz', + default=audiotools.MUSICBRAINZ_SERVICE, + help=_.OPT_NO_MUSICBRAINZ) + + lookup.add_option( + '--freedb-server', action='store', + type='string', dest='freedb_server', + default=audiotools.FREEDB_SERVER, + metavar='HOSTNAME') + lookup.add_option( + '--freedb-port', action='store', + type='int', dest='freedb_port', + default=audiotools.FREEDB_PORT, + metavar='PORT') + lookup.add_option( + '--no-freedb', action='store_false', + dest='use_freedb', + default=audiotools.FREEDB_SERVICE, + help=_.OPT_NO_FREEDB) + + lookup.add_option( + '-D', '--default', + dest='use_default', action='store_true', default=False, + help=_.OPT_DEFAULT) + + parser.add_option_group(lookup) - #ensure the selected compression is compatible with that class - if (options.quality == 'help'): - if (len(AudioType.COMPRESSION_MODES) > 1): - msg.info(_(u"Available compression types for %s:") % \ - (AudioType.NAME)) - for mode in AudioType.COMPRESSION_MODES: - msg.new_row() - if (mode == audiotools.__default_quality__(AudioType.NAME)): - msg.output_column(msg.ansi(mode.decode('ascii'), - [msg.BOLD, - msg.UNDERLINE]), True) - else: - msg.output_column(mode.decode('ascii'), True) - if (mode in AudioType.COMPRESSION_DESCRIPTIONS): - msg.output_column(u" : ") - else: - msg.output_column(u" ") - msg.output_column( - AudioType.COMPRESSION_DESCRIPTIONS.get(mode, u"")) - msg.info_rows() - else: - msg.error(_(u"Audio type %s has no compression modes") % \ - (AudioType.NAME)) - sys.exit(0) - elif (options.quality is None): - options.quality = audiotools.__default_quality__(AudioType.NAME) - elif (options.quality not in AudioType.COMPRESSION_MODES): - msg.error(_(u"\"%(quality)s\" is not a supported " + - u"compression mode for type \"%(type)s\"") % - {"quality": options.quality, - "type": AudioType.NAME}) + (options, args) = parser.parse_args() + msg = audiotools.Messenger("trackcat", options) + + #ensure interactive mode is available, if selected + if (options.interactive and (not audiotools.ui.AVAILABLE)): + audiotools.ui.not_available_message(msg) sys.exit(1) #grab the list of AudioFile objects we are converting from - audiofiles = audiotools.open_files(args, messenger=msg) + opened_files = set([]) + audiofiles = audiotools.open_files(args, + messenger=msg, + warn_duplicates=True, + opened_files=opened_files) + + #perform some option sanity checking if (len(audiofiles) < 1): - msg.error(_(u"You must specify at least 1 supported audio file")) + msg.error(_.ERR_FILES_REQUIRED) sys.exit(1) if (len(set([f.sample_rate() for f in audiofiles])) != 1): - msg.error(_(u"All audio files must have the same sample rate")) + msg.error(_.ERR_SAMPLE_RATE_MISMATCH) sys.exit(1) if (len(set([f.channels() for f in audiofiles])) != 1): - msg.error(_(u"All audio files must have the same channel count")) + msg.error(_.ERR_CHANNEL_COUNT_MISMATCH) sys.exit(1) if (len(set([int(f.channel_mask()) for f in audiofiles])) != 1): - msg.error(_(u"All audio files must have the same channel assignment")) + msg.error(_.ERR_CHANNEL_MASK_MISMATCH) sys.exit(1) if (len(set([f.bits_per_sample() for f in audiofiles])) != 1): - msg.error(_(u"All audio files must have the same bits per sample")) + msg.error(_.ERR_BPS_MISMATCH) sys.exit(1) #if embedding a cuesheet, try to read it before doing any work if (options.cuesheet is not None): try: cuesheet = audiotools.read_sheet(options.cuesheet) + #FIXME - ensure cuesheet is long enough to be embedded + + opened_files.add(audiotools.Filename(options.cuesheet)) except audiotools.SheetException, err: msg.error(unicode(err)) sys.exit(1) else: cuesheet = None - #constuct a MetaData object from our audiofiles - track_metadatas = [f.get_metadata() for f in - audiofiles if (f.get_metadata() is not None)] + if (options.filename is None): + msg.error(_.ERR_NO_OUTPUT_FILE) + sys.exit(1) + else: + output_filename = audiotools.Filename(options.filename) + if (output_filename in opened_files): + msg.error(_.ERR_OUTPUT_IS_INPUT % (output_filename,)) + sys.exit(1) - if (len(track_metadatas) > 0): - track_fields = dict([(field, - set([getattr(m, field) - for m in track_metadatas])) - for field in audiotools.MetaData.FIELDS]) + #get the AudioFile class we are converted to + if (options.type == 'help'): + import audiotools.ui + audiotools.ui.show_available_formats(msg) + sys.exit(0) + elif (options.type is not None): + AudioType = audiotools.TYPE_MAP[options.type] + else: + if (options.filename is not None): + try: + AudioType = audiotools.filename_to_type(options.filename) + except audiotools.UnknownAudioType, exp: + exp.error_msg(msg) + sys.exit(1) + else: + AudioType = audiotools.TYPE_MAP[audiotools.DEFAULT_TYPE] - metadata = audiotools.MetaData(**dict([(field, list(values)[0]) - for (field, values) in - track_fields.items() - if (len(values) == 1)])) + #ensure the selected compression is compatible with that class + if (options.quality == 'help'): + import audiotools.ui + audiotools.ui.show_available_qualities(msg, AudioType) + sys.exit(0) + elif (options.quality is None): + options.quality = audiotools.__default_quality__(AudioType.NAME) + elif (options.quality not in AudioType.COMPRESSION_MODES): + msg.error(_.ERR_UNSUPPORTED_COMPRESSION_MODE % + {"quality": options.quality, + "type": AudioType.NAME}) + sys.exit(1) - #port over non-duplicate images - images = [] - for m in track_metadatas: - for i in m.images(): - if (i not in images): - images.append(i) - for i in images: - metadata.add_image(i) + #constuct a MetaData object from our audiofiles + #which may be None if there is no metadata + metadata = merge_metadatas([t.get_metadata() for t in audiofiles]) + + if (not options.metadata_lookup): + metadata_choices = [metadata] else: - metadata = None + #perform CD lookup for existing files + metadata_choices = [ + merge_metadatas(choice) for choice in + audiotools.track_metadata_lookup( + audiofiles=audiofiles, + musicbrainz_server=options.musicbrainz_server, + musicbrainz_port=options.musicbrainz_port, + freedb_server=options.freedb_server, + freedb_port=options.freedb_port, + use_musicbrainz=options.use_musicbrainz, + use_freedb=options.use_freedb)] + + #and prepend metadata from existing files as an option, if any + if (metadata is not None): + metadata_choices.insert(0, metadata) + + if (options.interactive): + #pick options using interactive widget + output_widget = audiotools.ui.SingleOutputFiller( + track_label=_.LAB_TRACKCAT_INPUT % (len(audiofiles)), + metadata_choices=metadata_choices, + input_filenames=opened_files, + output_file=str(output_filename), + output_class=AudioType, + quality=options.quality, + completion_label=_.LAB_TRACKCAT_APPLY) + loop = audiotools.ui.urwid.MainLoop( + output_widget, + audiotools.ui.style(), + unhandled_input=output_widget.handle_text, + pop_ups=True) + try: + loop.run() + msg.ansi_clearscreen() + except (termios.error, IOError): + msg.error(_.ERR_TERMIOS_ERROR) + msg.info(_.ERR_TERMIOS_SUGGESTION) + msg.info(audiotools.ui.xargs_suggestion(sys.argv)) + sys.exit(1) + if (not output_widget.cancelled()): + (output_class, + output_filename, + output_quality, + metadata) = output_widget.output_track() + else: + sys.exit(0) + else: + #pick options without using GUI + output_class = AudioType + output_quality = options.quality + metadata = audiotools.ui.select_metadata( + [[m] for m in metadata_choices], + msg, + options.use_default)[0] + + # perform track concatenation using options progress = audiotools.SingleProgressDisplay( - msg, msg.filename(options.filename)) + msg, unicode(output_filename)) try: - encoded = AudioType.from_pcm( - options.filename, + encoded = output_class.from_pcm( + str(output_filename), audiotools.PCMReaderProgress( - audiotools.PCMCat(iter([af.to_pcm() for af in audiofiles])), + audiotools.PCMCat([af.to_pcm() for af in audiofiles]), sum([af.total_frames() for af in audiofiles]), progress.update), - options.quality) + output_quality) encoded.set_metadata(metadata) progress.clear() if (cuesheet is not None): - #set_metadata() will sometimes transfer a cuesheet automatically - #in that case, don't transfer it again via set_cuesheet() - existing_cuesheet = encoded.get_cuesheet() - if (existing_cuesheet is None): - encoded.set_cuesheet(cuesheet) + encoded.set_cuesheet(cuesheet) except audiotools.EncodingError, err: - msg.error(_(u"%(filename)s: %(error)s") % - {"filename": msg.filename(options.filename), - "error": err}) + msg.error(_.ERR_ENCODING_ERROR % (output_filename,)) sys.exit(1) - except audiotools.InvalidFormat, err: + except audiotools.InvalidFilenameFormat, err: msg.error(unicode(err)) sys.exit(1)
View file
audiotools-2.18.tar.gz/trackcmp -> audiotools-2.19.tar.gz/trackcmp
Changed
@@ -18,24 +18,12 @@ #Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import audiotools import sys import os import os.path import operator -import gettext - -gettext.install("audiotools", unicode=True) - - -def near_equal(audiofile1, audiofile2): - p1 = audiofile1.to_pcm() - p2 = audiofile2.to_pcm() - try: - return audiotools.stripped_pcm_cmp(p1, p2) - finally: - p1.close() - p2.close() +import audiotools +import audiotools.text as _ def cmp_files(progress, audiofile1, audiofile2): @@ -46,11 +34,17 @@ a negative value if some error occurs.""" try: - return (audiofile1.filename, - audiofile2.filename, - audiotools.pcm_frame_cmp(audiotools.to_pcm_progress(audiofile1, - progress), - audiofile2.to_pcm())) + if (os.path.samefile(audiofile1.filename, audiofile2.filename)): + return (audiofile1.filename, + audiofile2.filename, + None) + else: + return (audiofile1.filename, + audiofile2.filename, + audiotools.pcm_frame_cmp( + audiotools.to_pcm_progress(audiofile1, + progress), + audiofile2.to_pcm())) except (IOError, ValueError, audiotools.DecodingError): return (audiofile1.filename, audiofile2.filename, @@ -65,18 +59,21 @@ def missing(self, directory, track_number, album_number): self.failures += 1 - if (album_number == 0): - self.msg.info(_(u"%(path)s : %(result)s") % { - "path": os.path.join(directory, - "track %2.2d" % (track_number)), - "result": self.msg.ansi(_(u"missing"), [self.msg.FG_RED])}) - else: - self.msg.info(_(u"%(path)s : %(result)s") % { - "path": os.path.join( - directory, - u"album %d track %2.2d" % (album_number, - track_number)), - "result": self.msg.ansi(_(u"missing"), [self.msg.FG_RED])}) + track_name = (("track %2.2d" % + (track_number + if track_number is not None else 0)) + if album_number is None else + ("album %d track %2.2d" % (album_number, + track_number + if track_number is not None + else 0))) + self.msg.info( + unicode( + audiotools.Filename( + os.path.join(directory, track_name))) + + u" : " + + self.msg.ansi(_.LAB_TRACKCMP_MISSING, + [self.msg.FG_RED])) sys.stdout.flush() def cmp_result(self, result): @@ -85,34 +82,36 @@ if (mismatch is None): self.successes += 1 - return _(u"%(path1)s <> %(path2)s : %(result)s") % { - "path1": self.msg.filename(path1), - "path2": self.msg.filename(path2), - "result": self.msg.ansi( - _(u"OK"), - [self.msg.FG_GREEN])} + return ((_.LAB_TRACKCMP_CMP % + {"file1": audiotools.Filename(path1), + "file2": audiotools.Filename(path2)}) + + u" : " + + self.msg.ansi(_.LAB_TRACKCMP_OK, + [self.msg.FG_GREEN])) elif (mismatch >= 0): self.failures += 1 - return _(u"%(path1)s <> %(path2)s : %(result)s") % { - "path1": self.msg.filename(path1), - "path2": self.msg.filename(path2), - "result": self.msg.ansi( - _(u"differ at PCM frame %d") % (mismatch + 1), - [self.msg.FG_RED])} + return ((_.LAB_TRACKCMP_CMP % + {"file1": audiotools.Filename(path1), + "file2": audiotools.Filename(path2)}) + + u" : " + + self.msg.ansi(_.LAB_TRACKCMP_MISMATCH % + {"frame_number": mismatch + 1}, + [self.msg.FG_RED])) else: self.failures += 1 - return _(u"%(path1)s <> %(path2)s : %(result)s") % { - "path1": self.msg.filename(path1), - "path2": self.msg.filename(path2), - "result": self.msg.ansi( - _(u"error"), - [self.msg.FG_RED])} + return ((_.LAB_TRACKCMP_CMP % + {"file1": audiotools.Filename(path1), + "file2": audiotools.Filename(path2)}) + + u" : " + + self.msg.ansi(_.LAB_TRACKCMP_ERROR, + [self.msg.FG_RED])) + if (__name__ == '__main__'): parser = audiotools.OptionParser( - usage=_(u'%prog <file 1> <file 2>'), + usage=_.USAGE_TRACKCMP, version="Python Audio Tools %s" % (audiotools.VERSION)) parser.add_option( @@ -121,57 +120,73 @@ type='int', default=audiotools.MAX_JOBS, dest='max_processes', - help=_(u'the maximum number of processes to run at a time')) + help=_.OPT_JOINT) parser.add_option('-R', '--no-summary', action='store_true', dest='no_summary', - help=_(u'suppress summary output')) + help=_.OPT_NO_SUMMARY) parser.add_option('-V', '--verbose', action='store', dest='verbosity', choices=audiotools.VERBOSITY_LEVELS, default=audiotools.DEFAULT_VERBOSITY, - help=_(u'the verbosity level to execute at')) + help=_.OPT_VERBOSE) (options, args) = parser.parse_args() msg = audiotools.Messenger("trackcmp", options) if (options.max_processes < 1): - msg.error(_(u'You must run at least 1 process at a time')) + msg.error(_.ERR_INVALID_JOINT) sys.exit(1) check_function = audiotools.pcm_frame_cmp if (len(args) == 2): if (os.path.isfile(args[0]) and os.path.isfile(args[1])): + #comparing two files + audiofiles = audiotools.open_files(args, messenger=msg, sorted=False) if (len(audiofiles) != 2): - msg.error(_(u"Both files to be compared must be audio files")) + msg.error(_.ERR_TRACKCMP_TYPE_MISMATCH) sys.exit(1) try: - frame_mismatch = check_function(audiofiles[0].to_pcm(), - audiofiles[1].to_pcm()) - if (frame_mismatch is None): + (audiofile_1, + audiofile_2) = audiofiles + filename_1 = audiotools.Filename(args[0]) + filename_2 = audiotools.Filename(args[1]) + + if (filename_1 == filename_2): + #both files are the same so they must be identical pass else: - msg.partial_info( - _(u"%(file1)s <> %(file2)s : ") % \ - {"file1": msg.filename(audiofiles[0].filename), - "file2": msg.filename(audiofiles[1].filename)}) - msg.info(msg.ansi( - _(u"differ at PCM frame %(frame_number)d") % - {"frame_number": frame_mismatch + 1}, - [msg.FG_RED])) - sys.exit(1) + frame_mismatch = check_function(audiofile_1.to_pcm(), + audiofile_2.to_pcm()) + if (frame_mismatch is None): + #all PCM frames the same + #so files are identical + pass + else: + msg.partial_info( + (_.LAB_TRACKCMP_CMP % + {"file1": filename_1, + "file2": filename_2}) + + u" : ") + msg.info( + msg.ansi( + _.LAB_TRACKCMP_MISMATCH % + {"frame_number": frame_mismatch + 1}, + [msg.FG_RED])) + sys.exit(1) except (IOError, ValueError, audiotools.DecodingError), err: msg.error(unicode(err)) sys.exit(1) elif (os.path.isdir(args[0]) and os.path.isdir(args[1])): + #comparing two directories results = Results(msg) files1 = audiotools.open_files( @@ -194,23 +209,32 @@ queue = audiotools.ExecProgressQueue( audiotools.ProgressDisplay(msg)) - for (album_number, track_number) in sorted( - list(files1_tracknumbers - files2_tracknumbers)): + for (album_number, + track_number) in sorted(list(files1_tracknumbers - + files2_tracknumbers)): results.missing(args[1], track_number, album_number) - for (album_number, track_number) in sorted( - list(files2_tracknumbers - files1_tracknumbers)): + for (album_number, + track_number) in sorted(list(files2_tracknumbers - + files1_tracknumbers)): results.missing(args[0], track_number, album_number) - for (album_number, track_number) in sorted( - list(files1_tracknumbers & files2_tracknumbers)): + for (album_number, + track_number) in sorted(list(files1_tracknumbers & + files2_tracknumbers)): + progress_text = ( + _.LAB_TRACKCMP_CMP % + {"file1": + audiotools.Filename( + files1_map[(album_number, + track_number)].filename), + "file2": + audiotools.Filename( + files2_map[(album_number, + track_number)].filename)}) queue.execute( function=cmp_files, - progress_text=u"%s <> %s" % \ - (msg.filename(files1_map[(album_number, - track_number)].filename), - msg.filename(files2_map[(album_number, - track_number)].filename)), + progress_text=progress_text, completion_output=results.cmp_result, audiofile1=files1_map[(album_number, track_number)], audiofile2=files2_map[(album_number, track_number)]) @@ -218,14 +242,14 @@ queue.run(options.max_processes) if (not options.no_summary): - msg.output(_(u"Results:")) + msg.output(_.LAB_TRACKCMP_RESULTS) msg.output(u"") msg.new_row() - msg.output_column(_(u"success"), True) + msg.output_column(_.LAB_TRACKCMP_HEADER_SUCCESS, True) msg.output_column(u" ") - msg.output_column(_(u"failure"), True) + msg.output_column(_.LAB_TRACKCMP_HEADER_FAILURE, True) msg.output_column(u" ") - msg.output_column(_(u"total"), True) + msg.output_column(_.LAB_TRACKCMP_HEADER_TOTAL, True) msg.divider_row([u"-", u" ", u"-", u" ", u"-"]) msg.new_row() msg.output_column(unicode(results.successes), True) @@ -239,27 +263,27 @@ if (results.failures > 0): sys.exit(1) else: - msg.error(_(u"%(file1)s <> %(file2)s : %(result)s") % - {"file1": msg.filename(args[0]), - "file2": msg.filename(args[1]), - "result": msg.ansi( - _(u"must be either files or directories"), - [msg.FG_RED])}) + #comparison mismatch + msg.error((_.LAB_TRACKCMP_CMP % + {"file1": audiotools.Filename(args[0]), + "file2": audiotools.Filename(args[1])}) + + u" : " + + msg.ansi(_.LAB_TRACKCMP_TYPE_MISMATCH, + [msg.FG_RED])) sys.exit(1) elif (len(args) > 2): + #possibly comparing disk image against tracks progress = audiotools.SingleProgressDisplay(msg, u"") progress.delete_row(0) audiofiles = audiotools.open_files(args, messenger=msg, sorted=False) - #try to compare the smaller files against the largest file - audiofiles.sort(lambda t1, t2: cmp(t1.total_frames(), t2.total_frames())) - if (sum([t.total_frames() for t in audiofiles[0:-1]]) != - audiofiles[-1].total_frames()): - msg.usage(u"<CD image> <track 1> <track 2> ...") + if ((sum([t.total_frames() for t in audiofiles[0:-1]]) != + audiofiles[-1].total_frames())): + msg.usage(_.USAGE_TRACKCMP_CDIMAGE) sys.exit(1) cd_image = audiofiles[-1] @@ -270,30 +294,30 @@ t2.track_number())) cd_data = audiotools.BufferedPCMReader(cd_image.to_pcm()) - for track in tracks: - progress.add_row(0, u"%(file1)s <> %(file2)s" % - {"file1": msg.filename(cd_image.filename), - "file2": msg.filename(track.filename)}) + for (i, track) in enumerate(tracks): + progress.add_row(0, + _.LAB_TRACKCMP_CMP % + {"file1": audiotools.Filename(cd_image.filename), + "file2": audiotools.Filename(track.filename)}) mismatch = audiotools.pcm_frame_cmp( audiotools.to_pcm_progress(track, progress.update), audiotools.LimitedPCMReader(cd_data, track.total_frames())) progress.delete_row(0) progress.clear() - if (mismatch is None): - msg.output(_(u"%(path1)s <> %(path2)s : %(result)s") % { - "path1": msg.filename(cd_image.filename), - "path2": msg.filename(track.filename), - "result": msg.ansi( - _(u"OK"), - [msg.FG_GREEN])}) - else: - msg.output(_(u"%(path1)s <> %(path2)s : %(result)s") % { - "path1": msg.filename(cd_image.filename), - "path2": msg.filename(track.filename), - "result": msg.ansi( - _(u"differ at PCM frame %d") % (mismatch + 1), - [msg.FG_RED])}) + msg.output( + audiotools.output_progress( + (_.LAB_TRACKCMP_CMP % + {"file1": audiotools.Filename(cd_image.filename), + "file2": audiotools.Filename(track.filename)}) + + u" : " + + (msg.ansi(_.LAB_TRACKCMP_OK, [msg.FG_GREEN]) + if mismatch is None else + msg.ansi(_.LAB_TRACKCMP_MISMATCH % + {"frame_number": mismatch + 1}, + [msg.FG_RED])), + i + 1, len(tracks))) + if (mismatch is not None): sys.exit(1) else: - msg.usage(u"<track 1> <track 2>") - msg.exit(1) + msg.usage(_.USAGE_TRACKCMP_FILES) + sys.exit(1)
View file
audiotools-2.18.tar.gz/trackinfo -> audiotools-2.19.tar.gz/trackinfo
Changed
@@ -20,13 +20,11 @@ import os.path import audiotools -import gettext - -gettext.install("audiotools", unicode=True) +import audiotools.text as _ if (__name__ == '__main__'): parser = audiotools.OptionParser( - usage=_(u"%prog [options] <track 1> [track 2] ..."), + usage=_.USAGE_TRACKINFO, version="Python Audio Tools %s" % (audiotools.VERSION)) parser.add_option("-n", "--no-metadata", action="store_true", dest="no_metadata", @@ -53,39 +51,37 @@ if (options.show_bitrate): try: msg.output( - _(u"%(bitrate)4.4s kbps: %(filename)s") % + _.LAB_TRACKINFO_BITRATE % {'bitrate': ((os.path.getsize(file.filename) * 8) / 2 ** 10) / length, - 'filename': msg.filename(file.filename)}) + 'filename': audiotools.Filename(file.filename)}) except ZeroDivisionError: msg.output( - _(u"%(bitrate)4.4s kbps: %(filename)s") % + _.LAB_TRACKINFO_BITRATE % {'bitrate': 0, - 'filename': msg.filename(file.filename)}) + 'filename': audiotools.Filename(file.filename)}) elif (options.show_percentage): try: + percentage = (float(os.path.getsize(file.filename) * 100) / + (file.total_frames() * file.channels() * + (file.bits_per_sample() / 8))) msg.output( - _(u"%(percentage)3.3s%%: %(filename)s") % - {'percentage': - int(round(float( - os.path.getsize(file.filename) * 100) / - (file.total_frames() * file.channels() * - (file.bits_per_sample() / 8)))), - 'filename': msg.filename(file.filename)}) + _.LAB_TRACKINFO_PERCENTAGE % + {'percentage': int(round(percentage)), + 'filename': audiotools.Filename(file.filename)}) except ZeroDivisionError: - msg.output(_(u"%(percentage)3.3s%%: %(filename)s") % \ - {'percentage': "0", - 'filenam': msg.filename(file.filename)}) + msg.output(_.LAB_TRACKINFO_PERCENTAGE % + {'percentage': "0", + 'filenam': audiotools.Filename(file.filename)}) else: msg.output( - _(u"%(minutes)2.2d:%(seconds)2.2d " + - u"%(channels)dch %(rate)dHz %(bits)d-bit: %(filename)s") % + _.LAB_TRACKINFO_ATTRIBS % {"minutes": length / 60, "seconds": length % 60, "channels": file.channels(), - "rate": file.sample_rate(), + "rate": audiotools.khz(file.sample_rate()), "bits": file.bits_per_sample(), - "filename": msg.filename(file.filename)}) + "filename": audiotools.Filename(file.filename)}) if (not options.no_metadata): metadata = file.get_metadata() if (not options.low_level): @@ -94,44 +90,45 @@ msg.output(u"") replay_gain = file.replay_gain() if (replay_gain is not None): - msg.output(_(u"ReplayGain:")) + msg.output(_.LAB_TRACKINFO_REPLAYGAIN) msg.new_row() - msg.output_column(_(u"Track Gain : "), True) + msg.output_column(_.LAB_TRACKINFO_TRACK_GAIN, True) msg.output_column(unicode(replay_gain.track_gain) + u" dB") msg.new_row() - msg.output_column(_(u"Track Peak : "), True) + msg.output_column(_.LAB_TRACKINFO_TRACK_PEAK, True) msg.output_column(u"%f" % (replay_gain.track_peak)) msg.new_row() - msg.output_column(_(u"Album Gain : "), True) + msg.output_column(_.LAB_TRACKINFO_ALBUM_GAIN, True) msg.output_column(unicode(replay_gain.album_gain) + u" dB") msg.new_row() - msg.output_column(_(u"Album Peak : "), True) + msg.output_column(_.LAB_TRACKINFO_ALBUM_PEAK, True) msg.output_column(u"%f" % (replay_gain.album_peak)) msg.output_rows() msg.output(u"") cuesheet = file.get_cuesheet() if (cuesheet is not None): ISRCs = cuesheet.ISRCs() - msg.output(_(u"Cuesheet:")) + msg.output(_.LAB_TRACKINFO_CUESHEET) msg.new_row() msg.output_column(u" ") - msg.output_column(_(u"track"), True) + msg.output_column(_.LAB_TRACKINFO_CUESHEET_TRACK, True) msg.output_column(u" ") - msg.output_column(_(u"length"), True) + msg.output_column(_.LAB_TRACKINFO_CUESHEET_LENGTH, True) msg.output_column(u" ") - msg.output_column(_(u"ISRC")) + msg.output_column(_.LAB_TRACKINFO_CUESHEET_ISRC) for (i, pcm_length) in enumerate( - cuesheet.pcm_lengths(file.total_frames())): + cuesheet.pcm_lengths(file.total_frames(), + file.sample_rate())): ISRC = ISRCs.get(i + 1, None) msg.new_row() msg.output_column(u"") msg.output_column(unicode(i + 1), True) msg.output_column(u"") msg.output_column( - u"%d:%2.2d" % (pcm_length / - file.sample_rate() / 60, - pcm_length / - file.sample_rate() % 60), + _.LAB_TRACK_LENGTH % (pcm_length / + file.sample_rate() / 60, + pcm_length / + file.sample_rate() % 60), True) msg.output_column(u"") if (ISRC is not None): @@ -145,34 +142,20 @@ msg.output(metadata.raw_info()) msg.output(u"") if (options.channel_assignment): - msg.output(_("Assigned Channels:")) - channel_names = { - "front_left": _(u"Front Left"), - "front_right": _(u"Front Right"), - "front_center": _(u"Front Center"), - "low_frequency": _(u"Low Frequency"), - "back_left": _(u"Back Left"), - "back_right": _(u"Back Right"), - "front_left_of_center": _(u"Front Left of Center"), - "front_right_of_center": _(u"Front Right of Center"), - "back_center": _(u"Back Center"), - "side_left": _(u"Side Left"), - "side_right": _(u"Side Right"), - "top_center": _(u"Top Center"), - "top_front_left": _(u"Top Front Left"), - "top_front_center": _(u"Top Front Center"), - "top_front_right": _(u"Top Front Right"), - "top_back_left": _(u"Top Back Left"), - "top_back_center": _(u"Top Back Center"), - "top_back_right": _(u"Top Back Right")} + msg.output(_.LAB_TRACKINFO_CHANNELS) + + channel_names = dict( + [(attr, audiotools.ChannelMask.MASK_TO_NAME[mask]) + for (attr, mask) in + audiotools.ChannelMask.SPEAKER_TO_MASK.items()]) if (file.channel_mask().defined()): for (i, channel) in enumerate(file.channel_mask().channels()): - msg.output( - _(u"channel %(channel_number)d - %(channel_name)s") % - {"channel_number": i + 1, - "channel_name": channel_names[channel]}) + msg.output(_.LAB_TRACKINFO_CHANNEL % + {"channel_number": i + 1, + "channel_name": channel_names[channel]}) else: for i in xrange(file.channels()): - msg.output(_(u"channel %(channel_number)d - undefined") % - {"channel_number": i + 1}) + msg.output(_.LAB_TRACKINFO_CHANNEL % + {"channel_number": i + 1, + "channel_name": _.LAB_TRACKINFO_UNDEFINED})
View file
audiotools-2.18.tar.gz/tracklength -> audiotools-2.19.tar.gz/tracklength
Changed
@@ -19,38 +19,106 @@ import os.path -import audiotools -import gettext from decimal import Decimal +import audiotools +import audiotools.text as _ + + +def audio_files(msg, args): + for audio_file in audiotools.open_files(filter(os.path.isfile, args), + messenger=msg, + warn_duplicates=True): + yield audio_file + + for parent_dir in filter(os.path.isdir, args): + for audio_file in audiotools.open_directory(parent_dir, + sorted=False, + messenger=msg): + yield audio_file -gettext.install("audiotools", unicode=True) -OUTPUT = _(u"%(hours)d:%(minutes)2.2d:%(seconds)2.2d") +class FormatSummary: + def __init__(self): + self.total_length = Decimal(0) + self.file_count = 0 + self.total_size = 0 + + def add(self, audiofile): + self.total_length += audiofile.seconds_length() + self.file_count += 1 + self.total_size += os.path.getsize(audiofile.filename) + + def to_row(self, name, msg): + msg.new_row() + msg.output_column(name, True) + msg.output_column(u" ") + msg.output_column(unicode(self.file_count), True) + msg.output_column(u" ") + format_length = int(self.total_length) + msg.output_column(_.LAB_TRACKLENGTH % + {"hours": format_length / (60 * 60), + "minutes": (format_length / 60) % 60, + "seconds": format_length % 60}, + True) + msg.output_column(u" ") + if (self.total_size > (2 ** 40)): + #terabytes + total_size = u"%sT" % \ + ((self.total_size / + Decimal(2 ** 40)).quantize(Decimal("1.0"))) + elif (self.total_size > (2 ** 30)): + #gigabytes + total_size = u"%sG" % \ + ((self.total_size / + Decimal(2 ** 30)).quantize(Decimal("1.0"))) + elif (self.total_size > (2 ** 20)): + #megabytes + total_size = u"%sM" % \ + ((self.total_size / + Decimal(2 ** 20)).quantize(Decimal("1.0"))) + elif (self.total_size > (2 ** 10)): + #kilobytes + total_size = u"%sK" % \ + ((self.total_size / + Decimal(2 ** 10)).quantize(Decimal("1.0"))) + else: + #bytes + total_size = unicode(self.total_size) + msg.output_column(total_size, True) + if (__name__ == '__main__'): parser = audiotools.OptionParser( - usage=_(u"%prog <track 1> [track 2] ..."), + usage=_.USAGE_TRACKLENGTH, version="Python Audio Tools %s" % (audiotools.VERSION)) (options, args) = parser.parse_args() msg = audiotools.Messenger("tracklength", options) - total_length = Decimal(0) + format_summaries = {} + total_summary = FormatSummary() - audio_files = audiotools.open_files(filter(os.path.isfile, args), - messenger=msg) - total_length += sum([file.seconds_length() for file in audio_files]) + for audio_file in audio_files(msg, args): + if (audio_file.NAME not in format_summaries.keys()): + format_summaries[audio_file.NAME] = FormatSummary() + format_summaries[audio_file.NAME].add(audio_file) + total_summary.add(audio_file) - for parent_dir in filter(os.path.isdir, args): - for f in audiotools.open_directory(parent_dir, sorted=False, - messenger=msg): - total_length += f.seconds_length() - - total_length = int(total_length) - - if (total_length > 0): - msg.output(OUTPUT % {"hours": total_length / (60 * 60), - "minutes": (total_length / 60) % 60, - "seconds": total_length % 60}) - else: - msg.output(OUTPUT % {"hours": 0, "minutes": 0, "seconds": 0}) + if (total_summary.file_count > 0): + msg.new_row() + msg.output_column(_.LAB_TRACKLENGTH_FILE_FORMAT, True) + msg.output_column(u" ") + msg.output_column(_.LAB_TRACKLENGTH_FILE_COUNT, True) + msg.output_column(u" ") + msg.output_column(_.LAB_TRACKLENGTH_FILE_LENGTH, True) + msg.output_column(u" ") + msg.output_column(_.LAB_TRACKLENGTH_FILE_SIZE, True) + msg.divider_row([u"-", u" ", u"-", u" ", u"-", u" ", u"-"]) + for name in sorted(format_summaries.keys()): + format_summaries[name].to_row(name.decode('ascii'), msg) + + if (len(format_summaries.keys()) > 1): + msg.divider_row([u"-", u" ", u"-", u" ", u"-", u" ", u"-"]) + total_summary.to_row(_.LAB_TRACKLENGTH_FILE_TOTAL, msg) + + msg.output_rows()
View file
audiotools-2.18.tar.gz/tracklint -> audiotools-2.19.tar.gz/tracklint
Changed
@@ -17,19 +17,13 @@ #along with this program; if not, write to the Free Software #Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import audiotools -import audiotools.delta import sys import os import os.path -import re import tempfile -import subprocess -import cStringIO -import anydbm -import gettext - -gettext.install("audiotools", unicode=True) +import audiotools +import audiotools.delta +import audiotools.text as _ try: from hashlib import sha1 @@ -49,20 +43,34 @@ def display_messages(messenger, track, messages): for message in messages: - messenger.info(_(u"* %(filename)s: %(message)s") % \ - {"filename": messenger.filename(track.filename), - "message": message}) + messenger.info(_.LAB_TRACKLINT_MESSAGE % + {"filename": audiotools.Filename(track.filename), + "message": message}) -def audiofiles(paths, messenger): +def audiofiles(paths, processed_filenames, messenger): directories = [p for p in paths if os.path.isdir(p)] files = [p for p in paths if os.path.isfile(p)] - for f in audiotools.open_files(files, messenger=messenger): - yield f - for d in directories: - for f in audiotools.open_directory(d, messenger=messenger): - yield f + for p in paths: + if (os.path.isfile(p)): + filename = audiotools.Filename(p) + try: + if (filename not in processed_filenames): + processed_filenames.add(filename) + yield audiotools.open(str(filename)) + except audiotools.UnsupportedFile: + pass + except IOError, err: + messenger.warning(_.ERR_OPEN_IOERROR % (filename,)) + except audiotools.InvalidFile, err: + messenger.error(unicode(err)) + elif (os.path.isdir(p)): + for (d, ds, fs) in os.walk(p): + for t in audiofiles([os.path.join(d, f) for f in fs], + processed_filenames, + messenger): + yield t def update_without_backup(track, messenger): @@ -115,46 +123,46 @@ def undo_from_backup(track, undo_db, messenger): if (undo_db.undo(track.filename)): - messenger.info(_(u"Restored: %s") % \ - (messenger.filename(track.filename))) + messenger.info(_.LAB_TRACKLINT_RESTORED % + (audiotools.Filename(track.filename),)) if (__name__ == '__main__'): parser = audiotools.OptionParser( - usage=_(u'%prog [options] [--fix] [--undo] [--db file] ' + - u'<track 1> [track 2] ...'), + usage=_.USAGE_TRACKLINT, version="Python Audio Tools %s" % (audiotools.VERSION)) parser.add_option('--fix', action='store_true', default=False, dest='fix', - help=_(u'perform suggest fixes')) + help=_.OPT_TRACKLINT_FIX) parser.add_option('--db', action='store', type='string', dest='db', - help=_(u'undo database file')) + help=_.OPT_TRACKLINT_DB) parser.add_option('--undo', action='store_true', default=False, dest='undo', - help=_(u'undo performed fixes')) + help=_.OPT_TRACKLINT_UNDO) parser.add_option('-V', '--verbose', action='store', dest='verbosity', choices=audiotools.VERBOSITY_LEVELS, default=audiotools.DEFAULT_VERBOSITY, - help=_(u'the verbosity level to execute at')) + help=_.OPT_VERBOSE) (options, args) = parser.parse_args() + processed_filenames = set([]) msg = audiotools.Messenger("tracklint", options) if (options.undo and (options.db is None)): - msg.error(_(u"Cannot perform undo without undo db")) + msg.error(_.ERR_NO_UNDO_DB) sys.exit(1) if (options.fix): @@ -164,16 +172,18 @@ try: undo_db = audiotools.delta.open_db(options.db) except: - msg.error(_(u"Unable to open \"%s\"") % - (msg.filename(options.db))) + msg.error(_.ERR_OPEN_IOERROR % + (audiotools.Filename(options.db),)) sys.exit(1) try: - for track in audiofiles(args, messenger=msg): + for track in audiofiles(args, + processed_filenames, + messenger=msg): try: update_and_backup(track, undo_db, msg) except IOError: - msg.error(_(u"Unable to write \"%s\"") % \ - (msg.filename(track.filename))) + msg.error(_.ERR_ENCODING_ERROR % + (audiotools.Filename(track.filename),)) sys.exit(1) except ValueError, err: msg.error(unicode(err)) @@ -184,12 +194,12 @@ #if we're fixing tracks and have no undo DB, #simply overwrite the track and track metadata directly #if changes have been made - for track in audiofiles(args, messenger=msg): + for track in audiofiles(args, processed_filenames, messenger=msg): try: update_without_backup(track, msg) except IOError: - msg.error(_(u"Unable to write \"%s\"") % \ - (msg.filename(track.filename))) + msg.error(_.ERR_ENCODING_ERROR % + (audiotools.Filename(track.filename),)) sys.exit(1) except ValueError, err: msg.error(unicode(err)) @@ -198,20 +208,21 @@ try: undo_db = audiotools.delta.open_db(options.db) except: - msg.error(_(u"Unable to open \"%s\"") % (msg.filename(options.db))) + msg.error(_.ERR_OPEN_IOERROR % + (audiotools.Filename(options.db),)) sys.exit(1) try: - for track in audiofiles(args, messenger=msg): + for track in audiofiles(args, processed_filenames, messenger=msg): try: undo_from_backup(track, undo_db, msg) except IOError: - msg.error(_(u"Unable to write \"%s\"") % \ - (msg.filename(track.filename))) + msg.error(_.ERR_ENCODING_ERROR % + (audiotools.Filename(track.filename),)) sys.exit(1) finally: undo_db.close() else: # a dry-run of the fixing procedure, with no changes made - for track in audiofiles(args, messenger=msg): + for track in audiofiles(args, processed_filenames, messenger=msg): fixes = [] track.clean(fixes) display_messages(msg, track, fixes)
View file
audiotools-2.18.tar.gz/trackplay -> audiotools-2.19.tar.gz/trackplay
Changed
@@ -17,452 +17,177 @@ #along with this program; if not, write to the Free Software #Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import audiotools -import audiotools.player import sys -import gettext import time -import select -import os -import tty import termios +import os +import audiotools +import audiotools.ui +import audiotools.player +import audiotools.text as _ -gettext.install("audiotools", unicode=True) - -try: - import urwid - - class MappedButton(urwid.Button): - def __init__(self, label, on_press=None, user_data=None, - key_map={}): - urwid.Button.__init__(self, label=label, - on_press=on_press, - user_data=user_data) - self.__key_map__ = key_map - - def keypress(self, size, key): - return urwid.Button.keypress(self, - size, - self.__key_map__.get(key, key)) - - class MappedRadioButton(urwid.RadioButton): - def __init__(self, group, label, state='first True', - on_state_change=None, user_data=None, - key_map={}): - urwid.RadioButton.__init__(self, group=group, - label=label, - state=state, - on_state_change=on_state_change, - user_data=user_data) - self.__key_map__ = key_map - - def keypress(self, size, key): - return urwid.RadioButton.keypress(self, - size, - self.__key_map__.get(key, key)) - - class AudioProgressBar(urwid.ProgressBar): - def __init__(self, normal, complete, sample_rate, current=0, done=100, - satt=None): - urwid.ProgressBar.__init__(self, - normal=normal, - complete=complete, - current=current, - done=done, - satt=satt) - self.sample_rate = sample_rate - - def render(self, size, focus=False): - """ - Render the progress bar. - """ - - #leeched from the orignal implementation - #only the txt formatting differs - - (maxcol,) = size - - try: - txt = urwid.Text("%d:%2.2d" % \ - ((self.current / self.sample_rate) / 60, - (self.current / self.sample_rate) % 60), - 'center', 'clip') - except ZeroDivisionError: - txt = urwid.Text("0:00", 'center', 'clip') - c = txt.render((maxcol,)) - - cf = float(self.current) * maxcol / self.done - ccol = int(cf) - cs = 0 - if self.satt is not None: - cs = int((cf - ccol) * 8) - if ccol < 0 or (ccol == 0 and cs == 0): - c._attr = [[(self.normal, maxcol)]] - elif ccol >= maxcol: - c._attr = [[(self.complete, maxcol)]] - elif cs and c._text[0][ccol] == " ": - t = c._text[0] - cenc = self.eighths[cs].encode("utf-8") - c._text[0] = t[:ccol] + cenc + t[ccol + 1:] - a = [] - if ccol > 0: - a.append((self.complete, ccol)) - a.append((self.satt, len(cenc))) - if maxcol - ccol - 1 > 0: - a.append((self.normal, maxcol - ccol - 1)) - c._attr = [a] - c._cs = [[(None, len(c._text[0]))]] - else: - c._attr = [[(self.complete, ccol), - (self.normal, maxcol - ccol)]] - return c - - class TrackplayGUI(urwid.Frame): +if (audiotools.ui.AVAILABLE): + urwid = audiotools.ui.urwid + + class TrackplayGUI(audiotools.ui.PlayerGUI): def __init__(self, track_list, audio_output, replay_gain=audiotools.player.RG_NO_REPLAYGAIN): def track_name(track): metadata = track.get_metadata() - if (metadata is not None): + if (((metadata is not None) and + (metadata.track_name is not None))): return metadata.track_name else: - return track.filename - - self.track_list = track_list - self.audio_output = audio_output - self.player = audiotools.player.Player( - audio_output=audio_output, - replay_gain=replay_gain, - next_track_callback=self.next_track) - - self.track_name = urwid.Text("") - self.album_name = urwid.Text("") - self.artist_name = urwid.Text("") - self.tracknum = urwid.Text("") - self.length = urwid.Text("", align='right') - self.channels = urwid.Text("", align='right') - self.sample_rate = urwid.Text("", align='right') - self.bits_per_sample = urwid.Text("", align='right') - self.play_pause_button = MappedButton("Play", - on_press=self.play_pause, - key_map={'tab': 'right'}) - self.progress = AudioProgressBar(normal='pg normal', - complete='pg complete', - sample_rate=0, - current=0, - done=100) - - self.track_group = [] - - track_len = sum([track.seconds_length() for track in track_list]) - - if (len(self.track_list) == 1): - self.status = urwid.Text( - "1 track, %(min)d:%(sec)2.2d minutes" % { - "count": len(self.track_list), - "min": int(track_len) / 60, - "sec": int(track_len) % 60}, - align='center') - else: - self.status = urwid.Text( - "%(count)d tracks, %(min)d:%(sec)2.2d minutes" % { - "count": len(self.track_list), - "min": int(track_len) / 60, - "sec": int(track_len) % 60}, - align='center') - - header = urwid.Pile([ - urwid.Columns([ - ('fixed', 9, urwid.Text(('header', u"Name : "), - align='right')), - ('weight', 1, self.track_name)]), - urwid.Columns([ - ('fixed', 9, urwid.Text(('header', u"Album : "), - align='right')), - ('weight', 1, self.album_name)]), - urwid.Columns([ - ('fixed', 9, urwid.Text(('header', u"Artist : "), - align='right')), - ('weight', 1, self.artist_name)]), - urwid.Columns([ - ('fixed', 9, urwid.Text(('header', u"Track : "), - align='right')), - ('fixed', 7, self.tracknum), - ('fixed', 7, self.length), - ('fixed', 5, self.channels), - ('fixed', 10, self.sample_rate), - ('fixed', 7, self.bits_per_sample)]), - self.progress]) - - controls = urwid.GridFlow([ - MappedButton("Prev", on_press=self.previous_track, - key_map={'tab': 'right'}), - self.play_pause_button, - MappedButton("Next", on_press=self.next_track, - key_map={'tab': 'down'})], - 9, 4, 1, 'center') - controls.set_focus(1) - - track_list = urwid.ListBox([ - MappedRadioButton(group=self.track_group, - label=track_name(track), - user_data=(track, track.get_metadata()), - on_state_change=self.select_track, - key_map={'tab': 'down'}) - for track in self.track_list]) - body = urwid.Pile([('fixed', 2, urwid.Filler(urwid.Pile([ - controls, urwid.Divider(div_char='-')]))), - ('weight', 1, track_list)]) - footer = urwid.Pile([urwid.Divider(div_char='-'), - self.status]) - - urwid.Frame.__init__(self, body=body, header=header, footer=footer) - - def next_track(self, user_data=None): - track_index = [g.state for g in self.track_group].index(True) - try: - self.track_group[track_index + 1].set_state(True) - except IndexError: - pass - - def previous_track(self, user_data=None): - track_index = [g.state for g in self.track_group].index(True) - try: - if (track_index == 0): - raise IndexError() - else: - self.track_group[track_index - 1].set_state(True) - except IndexError: - pass + return unicode(audiotools.Filename(track.filename)) + + audiotools.ui.PlayerGUI.__init__( + self, + audiotools.player.Player( + audio_output=audio_output, + replay_gain=replay_gain, + next_track_callback=self.next_track), + [(track_name(track), + int(track.seconds_length()), + (track, track.get_metadata())) for track in track_list], + sum([track.seconds_length() for track in track_list])) def select_track(self, radio_button, new_state, user_data, auto_play=True): - if (new_state == True): + if (new_state): (track, metadata) = user_data if (metadata is not None): - self.track_name.set_text(metadata.track_name) - self.album_name.set_text(metadata.album_name) - self.artist_name.set_text(metadata.artist_name) - track_number = track.track_number() - track_total = metadata.track_total - if (track_number > 0): - if (track_total > 0): - self.tracknum.set_text(u"%d / %d" % \ - (track_number, - track_total)) - else: - self.tracknum.set_text(unicode(track_number)) - else: - self.tracknum.set_text(u"") + self.update_metadata( + track_name=metadata.track_name, + album_name=metadata.album_name, + artist_name=metadata.artist_name, + track_number=metadata.track_number, + track_total=metadata.track_total, + pcm_frames=track.total_frames(), + channels=track.channels(), + sample_rate=track.sample_rate(), + bits_per_sample=track.bits_per_sample()) + else: - self.track_name.set_text(u"") - self.album_name.set_text(u"") - self.artist_name.set_text(u"") - track_number = track.track_number() - if (track_number > 0): - self.tracknum.set_text(unicode(track_number)) - else: - self.tracknum.set_text(u"") - - track_length = track.seconds_length() - self.length.set_text("%d:%2.2d" % (int(track_length) / 60, - int(track_length) % 60)) - self.channels.set_text("%dch" % (track.channels())) - self.sample_rate.set_text("%dHz" % (track.sample_rate())) - self.bits_per_sample.set_text( - "%dbps" % (track.bits_per_sample())) - - self.progress.current = 0 - self.progress.done = track.total_frames() - self.progress.sample_rate = track.sample_rate() + self.update_metadata( + pcm_frames=track.total_frames(), + channels=track.channels(), + sample_rate=track.sample_rate(), + bits_per_sample=track.bits_per_sample()) self.player.open(track) if (auto_play): + from audiotools.text import LAB_PAUSE_BUTTON + self.player.play() - self.play_pause_button.set_label("Pause") - - def update_status(self): - self.progress.set_completion(self.player.progress()[0]) - - def play_pause(self, user_data): - self.player.toggle_play_pause() - if (self.play_pause_button.get_label() == "Play"): - self.play_pause_button.set_label("Pause") - elif (self.play_pause_button.get_label() == "Pause"): - self.play_pause_button.set_label("Play") - - def stop(self): - self.player.stop() - self.play_pause_button.set_label("Play") - - def handle_text(self, i): - if ((i == 'esc') or (i == 'q') or (i == 'Q')): - self.perform_exit() - elif ((i == 'n') or (i == 'N')): - self.next_track() - elif ((i == 'p') or (i == 'P')): - self.previous_track() - elif ((i == 's') or (i == 'S')): - self.stop() - - def perform_exit(self, *args): - self.player.close() - raise urwid.ExitMainLoop() - - def timer(main_loop, trackplay): - trackplay.update_status() - loop.set_alarm_at(tm=time.time() + 1, - callback=timer, - user_data=trackplay) + self.play_pause_button.set_label(LAB_PAUSE_BUTTON) interactive_available = True -except ImportError: +else: interactive_available = False -class TrackplayTTY: - OUTPUT_FORMAT = (u"%(track_number)d/%(track_total)d " + - u"[%(sent_minutes)d:%(sent_seconds)2.2d / " + - u"%(total_minutes)d:%(total_seconds)2.2d] " + - u"%(channels)dch %(sample_rate)dHz " + - u"%(bits_per_sample)d-bit") - +class TrackplayTTY(audiotools.ui.PlayerTTY): def __init__(self, track_list, audio_output, replay_gain=audiotools.player.RG_NO_REPLAYGAIN): - self.track_list = track_list - self.player = audiotools.player.Player( - audio_output=audio_output, - replay_gain=replay_gain, - next_track_callback=self.next_track) self.track_index = -1 - self.current_track = None - self.seconds_total = 0 - self.channels = 0 - self.sample_rate = 0 - self.bits_per_sample = 0 - self.track_number = 0 - self.track_total = len(track_list) - - def quit(self): - if (self.current_track is not None): - self.current_track = None - self.player.close() - - def toggle_play_pause(self): - if (self.current_track is not None): - self.player.toggle_play_pause() - - def play(self): - self.next_track() - - def stop(self): - self.player.stop() + self.track_list = track_list + audiotools.ui.PlayerTTY.__init__( + self, + audiotools.player.Player( + audio_output=audio_output, + replay_gain=replay_gain, + next_track_callback=self.next_track)) def previous_track(self): if (self.track_index > 0): self.track_index -= 1 - self.current_track = self.track_list[self.track_index] - self.channels = self.current_track.channels() - self.sample_rate = self.current_track.sample_rate() - self.bits_per_sample = self.current_track.bits_per_sample() - self.track_number = self.track_index + 1 - self.player.open(self.current_track) + current_track = self.track_list[self.track_index] + self.set_metadata( + track_number=self.track_index + 1, + track_total=len(self.track_list), + channels=current_track.channels(), + sample_rate=current_track.sample_rate(), + bits_per_sample=current_track.bits_per_sample()) + self.player.open(current_track) self.player.play() def next_track(self): try: self.track_index += 1 - self.current_track = self.track_list[self.track_index] - self.channels = self.current_track.channels() - self.sample_rate = self.current_track.sample_rate() - self.bits_per_sample = self.current_track.bits_per_sample() - self.track_number = self.track_index + 1 - self.player.open(self.current_track) + current_track = self.track_list[self.track_index] + self.set_metadata( + track_number=self.track_index + 1, + track_total=len(self.track_list), + channels=current_track.channels(), + sample_rate=current_track.sample_rate(), + bits_per_sample=current_track.bits_per_sample()) + self.player.open(current_track) self.player.play() except IndexError: - self.current_track = None - self.player.close() - - def progress(self): - return self.player.progress() - - def progress_line(self): - return (self.OUTPUT_FORMAT % - {"track_number": self.track_number, - "track_total": self.track_total, - "sent_minutes": - (frames_sent / self.sample_rate) / 60, - "sent_seconds": - (frames_sent / self.sample_rate) % 60, - "total_minutes": - (frames_total / self.sample_rate) / 60, - "total_seconds": - (frames_total / self.sample_rate) % 60, - "channels": self.channels, - "sample_rate": self.sample_rate, - "bits_per_sample": self.bits_per_sample}) + self.playing_finished = True if (__name__ == '__main__'): parser = audiotools.OptionParser( - usage=_(u"%prog <track 1> [track 2] ..."), + usage=_.USAGE_TRACKPLAY, version="Python Audio Tools %s" % (audiotools.VERSION)) parser.add_option( - '-V', '--verbose', - action='store', - dest='verbosity', - choices=audiotools.VERBOSITY_LEVELS, - default=audiotools.DEFAULT_VERBOSITY, - help=_(u'the verbosity level to execute at')) - - parser.add_option( '-I', '--interactive', action='store_true', default=False, dest='interactive', - help=_(u'run in interactive mode')) + help=_.OPT_INTERACTIVE_PLAY) + + players_map = dict([(player.NAME, player) + for player in audiotools.player.AUDIO_OUTPUT]) + + parser.add_option( + '-o', '--output', + action='store', + dest='output', + choices=[player.NAME for player in audiotools.player.AUDIO_OUTPUT + if player.available()], + default=[player.NAME for player in audiotools.player.AUDIO_OUTPUT + if player.available()][0], + help=(_.OPT_OUTPUT_PLAY % + u", ".join([u"\"%s\"" % (player.NAME.decode('ascii')) + for player in audiotools.player.AUDIO_OUTPUT + if player.available()]))) parser.add_option( '-T', '--track-replaygain', action='store_true', default=False, dest='track_replaygain', - help=_(u'apply track ReplayGain during playback, if present')) + help=_.OPT_PLAYBACK_TRACK_GAIN) parser.add_option( '-A', '--album-replaygain', action='store_true', default=False, dest='album_replaygain', - help=_(u'apply album ReplayGain during playback, if present')) - - players_map = dict([(player.NAME, player) - for player in audiotools.player.AUDIO_OUTPUT]) + help=_.OPT_PLAYBACK_ALBUM_GAIN) parser.add_option( - '-o', '--output', - action='store', - dest='output', - choices=players_map.keys(), - default=[player.NAME for player in audiotools.player.AUDIO_OUTPUT - if player.available()][0], - help=_(u"the method to play audio (choose from: %s)") % \ - u", ".join([u"\"%s\"" % (player.NAME.decode('ascii')) - for player in audiotools.player.AUDIO_OUTPUT - if player.available()])) + '--shuffle', action='store_true', dest='shuffle', default=False, + help=_.OPT_SHUFFLE) parser.add_option( - '--shuffle', action='store_true', dest='shuffle', default=False, - help='shuffle tracks') + '-V', '--verbose', + action='store', + dest='verbosity', + choices=audiotools.VERBOSITY_LEVELS, + default=audiotools.DEFAULT_VERBOSITY, + help=_.OPT_VERBOSE) (options, args) = parser.parse_args() msg = audiotools.Messenger("trackplay", options) if (options.interactive and (not interactive_available)): - msg.error(_(u"urwid is required for interactive mode")) - msg.output(_(u"Please download and install " + - u"urwid from http://excess.org/urwid/")) - msg.output(_(u"or your system's package manager.")) + msg.error(_.ERR_URWID_REQUIRED) + msg.output(_.ERR_GET_URWID1) + msg.output(_.ERR_GET_URWID2) sys.exit(1) audiofiles = audiotools.open_files(args, sorted=False, messenger=msg) @@ -489,74 +214,23 @@ audiofiles[0].get_metadata()), False) loop = urwid.MainLoop(trackplay, - [('header', 'default,bold', 'default', ''), - ('pg normal', 'white', 'black', 'standout'), - ('pg complete', 'white', 'dark blue'), - ('pg smooth', 'dark blue', 'black')], + audiotools.ui.style(), unhandled_input=trackplay.handle_text) loop.set_alarm_at(tm=time.time() + 1, - callback=timer, + callback=audiotools.ui.timer, user_data=trackplay) try: loop.run() - except termios.error: - msg.error(_(u"Unable to get tty settings")) - msg.info(_(u"If piping arguments via xargs(1), " + - u"try using its -o option")) + msg.ansi_clearscreen() + except (termios.error, IOError): + msg.error(_.ERR_TERMIOS_ERROR) + msg.info(_.ERR_TERMIOS_SUGGESTION) + msg.info(audiotools.ui.xargs_suggestion(sys.argv)) sys.exit(1) else: - try: - original_terminal_settings = termios.tcgetattr(0) - except termios.error: - msg.error(_(u"Unable to get tty settings")) - msg.info(_(u"If piping arguments via xargs(1), " + - u"try using its -o option")) - sys.exit(1) - trackplay = TrackplayTTY(track_list=audiofiles, audio_output=players_map[options.output](), replay_gain=replay_gain) - trackplay.play() - output_line_len = 0 - - try: - tty.setcbreak(sys.stdin.fileno()) - while (trackplay.current_track is not None): - (frames_sent, frames_total) = trackplay.progress() - output_line = trackplay.progress_line() - msg.ansi_clearline() - if (len(output_line) > output_line_len): - output_line_len = len(output_line) - msg.partial_output(output_line) - else: - msg.partial_output(output_line + - (u" " * (output_line_len - - len(output_line)))) - - (r_list, w_list, x_list) = select.select([sys.stdin.fileno()], - [], [], 1) - if (len(r_list) > 0): - char = os.read(sys.stdin.fileno(), 1) - if ((char == 'q') or - (char == 'Q') or - (char == '\x1B')): - trackplay.quit() - elif (char == ' '): - trackplay.toggle_play_pause() - elif ((char == 'n') or - (char == 'N')): - trackplay.next_track() - elif ((char == 'p') or - (char == 'P')): - trackplay.previous_track() - elif ((char == 's') or - (char == 'S')): - trackplay.stop() - else: - pass - - msg.ansi_clearline() - finally: - termios.tcsetattr(0, termios.TCSADRAIN, original_terminal_settings) + sys.exit(trackplay.run(msg, sys.stdin))
View file
audiotools-2.18.tar.gz/trackrename -> audiotools-2.19.tar.gz/trackrename
Changed
@@ -20,67 +20,273 @@ import sys import audiotools +import audiotools.ui import os.path -import subprocess -import gettext +import shutil +import termios +import audiotools.text as _ + + +if (audiotools.ui.AVAILABLE): + urwid = audiotools.ui.urwid + + class Trackrename(urwid.Pile): + def __init__(self, audio_class, format_string, + input_filenames, metadatas): + """audio_class is an AudioFile class + format_string is a UTF-8 encoded plain string + input_filenames is a list of Filename objects + metadata is a list of MetaData objects, or Nones""" + + assert(len(input_filenames) == len(metadatas)) + self.__cancelled__ = True + + self.input_filenames = input_filenames + + #setup a previous/finish button set + metadata_buttons = urwid.Filler( + urwid.Columns( + widget_list=[('weight', 1, + urwid.Button(_.LAB_CANCEL_BUTTON, + on_press=self.exit)), + ('weight', 2, + urwid.Button(_.LAB_TRACKRENAME_RENAME, + on_press=self.apply))], + dividechars=3, + focus_column=1)) + + #setup a widget with an output preview + self.track_previews = [urwid.Text(u"") for m in metadatas] + self.output_tracks_list = urwid.ListBox( + self.track_previews) + self.output_tracks_frame = urwid.Frame( + body=self.output_tracks_list) + self.invalid_output_format = urwid.Filler( + urwid.Text(_.ERR_INVALID_FILENAME_FORMAT, + align="center")) + + #setup a widget with the rename template + output_format = urwid.Edit( + edit_text=format_string.decode('utf-8'), + wrap='clip') + urwid.connect_signal(output_format, + 'change', + self.format_changed, + (audio_class, + input_filenames, + metadatas)) + + browse_fields = audiotools.ui.BrowseFields(output_format) + template_row = urwid.Columns( + [('fixed', 10, + urwid.Text(('label', + u"%s : " % + (_.LAB_OPTIONS_FILENAME_FORMAT)), + align="right")), + ('weight', 1, output_format), + ('fixed', 10, browse_fields)]) + + #perform initial data population + self.format_changed(output_format, + format_string, + (audio_class, + input_filenames, + metadatas)) + + urwid.Pile.__init__( + self, + [('fixed', 3, urwid.LineBox(urwid.Filler(template_row))), + ('weight', 1, + urwid.LineBox(self.output_tracks_frame, + title=_.LAB_OPTIONS_OUTPUT_FILES)), + ('fixed', 1, metadata_buttons)]) + + def apply(self, button): + if (not self.has_errors): + self.__cancelled__ = False + raise urwid.ExitMainLoop() + + def exit(self, button): + self.__cancelled__ = True + raise urwid.ExitMainLoop() + + def cancelled(self): + return self.__cancelled__ + + def handle_text(self, i): + if (i == 'esc'): + self.exit(None) + + def format_changed(self, widget, new_value, user_data): + (output_class, + input_filenames, + metadatas) = user_data + + try: + #generate list of Filename objects + #from paths, metadatas and format + self.output_filenames = [ + audiotools.Filename( + output_class.track_name( + file_path=str(filename), + track_metadata=metadata, + format=new_value.encode('utf-8'))) + for (filename, + metadata) in zip(input_filenames, metadatas)] + + #and populate output files list + for (path, preview) in zip(self.output_filenames, + self.track_previews): + preview.set_text(unicode(path)) + + if ((self.output_tracks_frame.get_body() is not + self.output_tracks_list)): + self.output_tracks_frame.set_body( + self.output_tracks_list) + self.has_errors = False + except (audiotools.UnsupportedTracknameField, + audiotools.InvalidFilenameFormat): + #invalid filename string + if ((self.output_tracks_frame.get_body() is not + self.invalid_output_format)): + self.output_tracks_frame.set_body( + self.invalid_output_format) + self.has_errors = True + + def to_rename(self): + """yields (old_name, new_name) tuples + where old_name and new_name differ + where the names are Filename objects""" + + if (not self.has_errors): + for (old_name, new_name) in zip(self.input_filenames, + self.output_filenames): + if (old_name != new_name): + yield (old_name, new_name) -gettext.install("audiotools", unicode=True) if (__name__ == '__main__'): parser = audiotools.OptionParser( - _(u"%prog [options] <track 1> [track 2] ..."), + _.USAGE_TRACKRENAME, version="Python Audio Tools %s" % (audiotools.VERSION)) + parser.add_option( + '-I', '--interactive', + action='store_true', + default=False, + dest='interactive', + help=_.OPT_INTERACTIVE_METADATA) + parser.add_option('--format', action='store', type='string', - default=None, + default=audiotools.FILENAME_FORMAT, dest='format', - help=_(u'the format string for new filenames')) + help=_.OPT_FORMAT) parser.add_option('-V', '--verbose', action='store', dest='verbosity', choices=audiotools.VERBOSITY_LEVELS, default=audiotools.DEFAULT_VERBOSITY, - help=_(u'the verbosity level to execute at')) + help=_.OPT_VERBOSE) (options, args) = parser.parse_args() msg = audiotools.Messenger("trackrename", options) - audiofiles = audiotools.open_files(args, messenger=msg) - - if (len(audiofiles) < 1): - msg.error(_(u"You must specify at least 1 supported audio file")) + #ensure interactive mode is available, if selected + if (options.interactive and (not audiotools.ui.AVAILABLE)): + audiotools.ui.not_available_message(msg) sys.exit(1) try: - for track in audiofiles: - track_metadata = track.get_metadata() - - new_name = track.track_name(file_path=track.filename, - track_metadata=track_metadata, - format=options.format) - - (path, filename) = os.path.split(track.filename) - if (filename != new_name): - try: - audiotools.make_dirs(os.path.join(path, new_name)) - except OSError: - msg.error(_(u"Unable to write \"%s\"") % \ - (new_name)) - sys.exit(1) - - if (subprocess.call([audiotools.BIN['mv'], '-i', - track.filename, - os.path.join(path, new_name)]) == 0): - if (options.verbosity != 'quiet'): - msg.info( - u"%s -> %s" % - (msg.filename(track.filename), - msg.filename(os.path.join(path, new_name)))) - else: - sys.exit(1) - except audiotools.UnsupportedTracknameField, err: - err.error_msg(msg) + audiofiles = audiotools.open_files(args, + messenger=msg, + no_duplicates=True) + except audiotools.DuplicateFile, err: + msg.error(_.ERR_DUPLICATE_FILE % (err.filename,)) + sys.exit(1) + + if (len(audiofiles) < 1): + msg.error(_.ERR_FILES_REQUIRED) sys.exit(1) + + #get a set of files to be renamed + #and generate an error if a duplicate occurs + renamed_filenames = set([audiotools.Filename(t.filename) for t in + audiofiles]) + + if (options.interactive): + widget = Trackrename( + audio_class=audiofiles[0].__class__, + format_string=options.format, + input_filenames=[audiotools.Filename(t.filename) for t in + audiofiles], + metadatas=[t.get_metadata() for t in audiofiles]) + loop = audiotools.ui.urwid.MainLoop( + widget, + audiotools.ui.style(), + unhandled_input=widget.handle_text, + pop_ups=True) + + try: + loop.run() + msg.ansi_clearscreen() + except (termios.error, IOError): + msg.error(_.ERR_TERMIOS_ERROR) + msg.info(_.ERR_TERMIOS_SUGGESTION) + msg.info(audiotools.ui.xargs_suggestion(sys.argv)) + sys.exit(1) + + if (not widget.cancelled()): + to_rename = list(widget.to_rename()) + else: + sys.exit(0) + else: + to_rename = [] # a (old_name, new_name) tuple + try: + for track in audiofiles: + original_filename = audiotools.Filename(track.filename) + new_filename = audiotools.Filename( + os.path.join( + os.path.dirname(track.filename), + track.track_name(file_path=track.filename, + track_metadata=track.get_metadata(), + format=options.format))) + if (new_filename != original_filename): + if (new_filename not in renamed_filenames): + renamed_filenames.add(new_filename) + to_rename.append((original_filename, new_filename)) + else: + msg.error(_.ERR_DUPLICATE_OUTPUT_FILE % + (new_filename,)) + sys.exit(1) + except audiotools.UnsupportedTracknameField, err: + err.error_msg(msg) + sys.exit(1) + except audiotools.InvalidFilenameFormat, err: + msg.error(unicode(err)) + sys.exit(1) + + #create subdirectories for renamed files if necessary + for (original_filename, new_filename) in to_rename: + try: + audiotools.make_dirs(str(new_filename)) + except OSError, err: + msg.os_error(err) + sys.exit(1) + + #perform the actual renaming itself + for (i, (original_filename, new_filename)) in enumerate(to_rename): + try: + shutil.move(str(original_filename), str(new_filename)) + msg.info( + audiotools.output_progress( + _.LAB_ENCODE % {"source": original_filename, + "destination": new_filename}, + i + 1, len(to_rename))) + except IOError, err: + msg.error(_.ERR_RENAME % + {"source": original_filename, + "target": new_filename}) + sys.exit(1)
View file
audiotools-2.18.tar.gz/tracksplit -> audiotools-2.19.tar.gz/tracksplit
Changed
@@ -17,16 +17,14 @@ #along with this program; if not, write to the Free Software #Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -import audiotools -import audiotools.cue -import audiotools.ui import sys import os import os.path -import gettext - -gettext.install("audiotools", unicode=True) +import audiotools +import audiotools.cue +import audiotools.ui +import audiotools.text as _ +import termios def has_embedded_cuesheet(audiofile): @@ -34,16 +32,15 @@ if (__name__ == '__main__'): parser = audiotools.OptionParser( - _(u'%prog [options] [-d directory] <track>'), + usage=_.USAGE_TRACKSPLIT, version="Python Audio Tools %s" % (audiotools.VERSION)) parser.add_option( - '-V', '--verbose', - action='store', - dest='verbosity', - choices=audiotools.VERBOSITY_LEVELS, - default=audiotools.DEFAULT_VERBOSITY, - help=_(u'the verbosity level to execute at')) + '-I', '--interactive', + action='store_true', + default=False, + dest='interactive', + help=_.OPT_INTERACTIVE_OPTIONS) parser.add_option( '--cue', @@ -51,23 +48,31 @@ type='string', dest='cuesheet', metavar='FILENAME', - help=_(u'the cuesheet to use for splitting track')) + help=_.OPT_CUESHEET_TRACKSPLIT) + + parser.add_option( + '-V', '--verbose', + action='store', + dest='verbosity', + choices=audiotools.VERBOSITY_LEVELS, + default=audiotools.DEFAULT_VERBOSITY, + help=_.OPT_VERBOSE) - conversion = audiotools.OptionGroup(parser, _(u"Encoding Options")) + conversion = audiotools.OptionGroup(parser, _.OPT_CAT_ENCODING) conversion.add_option( '-t', '--type', action='store', dest='type', - choices=audiotools.TYPE_MAP.keys(), - help=_(u'the type of audio track to convert to')) + choices=sorted(audiotools.TYPE_MAP.keys() + ['help']), + help=_.OPT_TYPE) conversion.add_option( '-q', '--quality', action='store', type='string', dest='quality', - help=_(u'the quality to store audio tracks at')) + help=_.OPT_QUALITY) conversion.add_option( '-d', '--dir', @@ -75,19 +80,19 @@ type='string', dest='dir', default='.', - help=_(u'the directory to store converted audio tracks')) + help=_.OPT_DIR) conversion.add_option( '--format', action='store', type='string', - default=None, + default=audiotools.FILENAME_FORMAT, dest='format', - help=_(u'the format string for new filenames')) + help=_.OPT_FORMAT) parser.add_option_group(conversion) - lookup = audiotools.OptionGroup(parser, _(u"CD Lookup Options")) + lookup = audiotools.OptionGroup(parser, _.OPT_CAT_CD_LOOKUP) lookup.add_option( '--musicbrainz-server', action='store', @@ -103,7 +108,7 @@ '--no-musicbrainz', action='store_false', dest='use_musicbrainz', default=audiotools.MUSICBRAINZ_SERVICE, - help='do not query MusicBrainz for metadata') + help=_.OPT_NO_MUSICBRAINZ) lookup.add_option( '--freedb-server', action='store', @@ -119,24 +124,16 @@ '--no-freedb', action='store_false', dest='use_freedb', default=audiotools.FREEDB_SERVICE, - help='do not query FreeDB for metadata') - - lookup.add_option( - '-I', '--interactive', - action='store_true', - default=False, - dest='interactive', - help=_(u'edit metadata in interactive mode')) + help=_.OPT_NO_FREEDB) lookup.add_option( '-D', '--default', dest='use_default', action='store_true', default=False, - help=_(u'when multiple choices are available, ' + - u'select the first one automatically')) + help=_.OPT_DEFAULT) parser.add_option_group(lookup) - metadata = audiotools.OptionGroup(parser, _(u"Metadata Options")) + metadata = audiotools.OptionGroup(parser, _.OPT_CAT_METADATA) metadata.add_option( '--album-number', @@ -144,8 +141,7 @@ action='store', type='int', default=0, - help=_(u'the album number of this CD, ' + - u'if it is one of a series of albums')) + help=_.OPT_ALBUM_NUMBER) metadata.add_option( '--album-total', @@ -153,25 +149,19 @@ action='store', type='int', default=0, - help=_(u'the total albums of this CD\'s set, ' + - u'if it is one of a series of albums')) - - #if adding ReplayGain is a lossless process - #(i.e. added as tags rather than modifying track data) - #add_replay_gain should default to True - #if not, add_replay_gain should default to False - #which is which depends on the track type + help=_.OPT_ALBUM_TOTAL) + metadata.add_option( '--replay-gain', action='store_true', dest='add_replay_gain', - help=_(u'add ReplayGain metadata to newly created tracks')) + help=_.OPT_REPLAY_GAIN) metadata.add_option( '--no-replay-gain', action='store_false', dest='add_replay_gain', - help=_(u'do not add ReplayGain metadata to newly extracted tracks')) + help=_.OPT_NO_REPLAY_GAIN) parser.add_option_group(metadata) @@ -184,6 +174,9 @@ sys.exit(1) #get the AudioFile class we are converted to + if (options.type == 'help'): + audiotools.ui.show_available_formats(msg) + sys.exit(0) if (options.type is not None): AudioType = audiotools.TYPE_MAP[options.type] else: @@ -191,58 +184,32 @@ #ensure the selected compression is compatible with that class if (options.quality == 'help'): - if (len(AudioType.COMPRESSION_MODES) > 1): - msg.info(_(u"Available compression types for %s:") % \ - (AudioType.NAME)) - for mode in AudioType.COMPRESSION_MODES: - msg.new_row() - if (mode == audiotools.__default_quality__(AudioType.NAME)): - msg.output_column(msg.ansi(mode.decode('ascii'), - [msg.BOLD, - msg.UNDERLINE]), True) - else: - msg.output_column(mode.decode('ascii'), True) - if (mode in AudioType.COMPRESSION_DESCRIPTIONS): - msg.output_column(u" : ") - else: - msg.output_column(u" ") - msg.output_column( - AudioType.COMPRESSION_DESCRIPTIONS.get(mode, u"")) - msg.info_rows() - else: - msg.error(_(u"Audio type %s has no compression modes") % \ - (AudioType.NAME)) + audiotools.ui.show_available_qualities(msg, AudioType) sys.exit(0) elif (options.quality is None): options.quality = audiotools.__default_quality__(AudioType.NAME) elif (options.quality not in AudioType.COMPRESSION_MODES): - msg.error(_(u"\"%(quality)s\" is not a supported " + - u"compression mode for type \"%(type)s\"") % + msg.error(_.ERR_UNSUPPORTED_COMPRESSION_MODE % {"quality": options.quality, "type": AudioType.NAME}) sys.exit(1) if (len(args) != 1): - msg.error(_(u"You must specify exactly 1 supported audio file")) + msg.error(_.ERR_1_FILE_REQUIRED) sys.exit(1) + else: + input_filename = audiotools.Filename(args[0]) + input_filenames = set([input_filename]) try: - audiofile = audiotools.open(args[0]) + audiofile = audiotools.open(str(input_filename)) except audiotools.UnsupportedFile: - msg.error(_(u"You must specify exactly 1 supported audio file")) + msg.error(_.ERR_1_FILE_REQUIRED) sys.exit(1) + except audiotools.InvalidFile: + msg.error(_.ERR_INVALID_FILE % (input_filename,)) except IOError: - msg.error(_(u"Unable to open \"%s\"") % (msg.filename(args[0]))) - sys.exit(1) - - if (options.add_replay_gain is None): - options.add_replay_gain = ( - audiotools.ADD_REPLAYGAIN and - AudioType.lossless_replay_gain() and - audiotools.applicable_replay_gain([audiofile])) - - if ((options.cuesheet is None) and (not has_embedded_cuesheet(audiofile))): - msg.error(_(u"You must specify a cuesheet to split audio file")) + msg.error(_.ERR_OPEN_IOERROR % (input_filename,)) sys.exit(1) base_directory = options.dir @@ -253,20 +220,28 @@ #(this overrides an embedded cuesheet) try: cuesheet = audiotools.read_sheet(options.cuesheet) + input_filenames.add(audiotools.Filename(options.cuesheet)) except audiotools.SheetException, err: msg.error(unicode(err)) sys.exit(1) else: - cuesheet = audiofile.get_cuesheet() + if (has_embedded_cuesheet(audiofile)): + cuesheet = audiofile.get_cuesheet() + else: + msg.error(_.ERR_TRACKSPLIT_NO_CUESHEET) + sys.exit(1) - if (list(cuesheet.pcm_lengths(audiofile.total_frames()))[-1] <= 0): - msg.error(_(u"Cuesheet too long for track being split")) + if (list(cuesheet.pcm_lengths(audiofile.total_frames(), + audiofile.sample_rate()))[-1] <= 0): + msg.error(_.ERR_TRACKSPLIT_OVERLONG_CUESHEET) sys.exit(1) + output_track_count = len(list(cuesheet.indexes())) + #use cuesheet to query metadata services for metadata choices metadata_choices = audiotools.metadata_lookup( first_track_number=1, - last_track_number=len(list(cuesheet.indexes())), + last_track_number=output_track_count, offsets=[i[-1] + 150 for i in cuesheet.indexes()], lead_out_offset=audiofile.cd_frames() + 150, total_length=audiofile.cd_frames(), @@ -315,95 +290,127 @@ for m in c: m.album_total = options.album_total - #decide which metadata to use to tag split tracks + #pull ISRC metadata from the cuesheet, if any + cuesheet_ISRCs = cuesheet.ISRCs() + for track_metadatas in metadata_choices: + for metadata in track_metadatas: + if ((metadata.ISRC is None) and (metadata.track_number in + cuesheet_ISRCs.keys())): + metadata.ISRC = cuesheet_ISRCs[metadata.track_number].decode( + 'ascii', 'replace') + + #decide which metadata and output options to use when splitting tracks if (options.interactive): #pick choice using interactive widget - metadata_widget = audiotools.ui.MetaDataFiller(metadata_choices) + output_widget = audiotools.ui.OutputFiller( + track_labels=[_.LAB_TRACK_X_OF_Y % (i + 1, output_track_count) + for i in xrange(output_track_count)], + metadata_choices=metadata_choices, + input_filenames=[input_filename for i in + xrange(output_track_count)], + output_directory=options.dir, + format_string=options.format, + output_class=AudioType, + quality=options.quality, + completion_label=_.LAB_TRACKSPLIT_APPLY) + loop = audiotools.ui.urwid.MainLoop( - metadata_widget, - [('key', 'white', 'dark blue')], - unhandled_input=metadata_widget.handle_text) - loop.run() + output_widget, + audiotools.ui.style(), + unhandled_input=output_widget.handle_text, + pop_ups=True) + try: + loop.run() + msg.ansi_clearscreen() + except (termios.error, IOError): + msg.error(_.ERR_TERMIOS_ERROR) + msg.info(_.ERR_TERMIOS_SUGGESTION) + msg.info(audiotools.ui.xargs_suggestion(sys.argv)) + sys.exit(1) - track_metadatas = dict([(m.track_number, m) for m in - metadata_widget.populated_metadata()]) - else: - if ((len(metadata_choices) == 1) or options.use_default): - #use default choice - track_metadatas = dict([(m.track_number, m) for m in - metadata_choices[0]]) + if (not output_widget.cancelled()): + output_tracks = list(output_widget.output_tracks()) else: - #pick choice using raw stdin/stdout - track_metadatas = \ - dict([(m.track_number, m) for m in - audiotools.ui.select_metadata(metadata_choices, msg)]) - - #pull ISRC metadata from the cuesheet, if any - cuesheet_ISRCs = cuesheet.ISRCs() - for metadata in track_metadatas.values(): - if ((len(metadata.ISRC) == 0) and - (metadata.track_number in cuesheet_ISRCs.keys())): - metadata.ISRC = \ - cuesheet_ISRCs[metadata.track_number].decode('ascii', - 'replace') + sys.exit(0) + else: + #pick choice without using GUI + try: + output_tracks = list( + audiotools.ui.process_output_options( + metadata_choices=metadata_choices, + input_filenames=[input_filename for i in + xrange(output_track_count)], + output_directory=options.dir, + format_string=options.format, + output_class=AudioType, + quality=options.quality, + msg=msg, + use_default=options.use_default)) + except audiotools.UnsupportedTracknameField, err: + err.error_msg(msg) + sys.exit(1) + except (audiotools.InvalidFilenameFormat, + audiotools.OutputFileIsInput, + audiotools.DuplicateOutputFile), err: + msg.error(unicode(err)) + sys.exit(1) #perform actual track splitting and tagging - encoded_files = [] + encoded_tracks = [] total_pcm = audiotools.BufferedPCMReader(audiofile.to_pcm()) try: - for (i, pcm_frames) in enumerate( - cuesheet.pcm_lengths(audiofile.total_frames())): - track_number = i + 1 - track_metadata = track_metadatas.get(track_number, None) - - filename = os.path.join( - base_directory, - AudioType.track_name( - file_path=audiofile.filename, - track_metadata=track_metadata, - format=options.format)) - - audiotools.make_dirs(filename) + for (i, (pcm_frames, + (output_class, + output_filename, + output_quality, + output_metadata)) + ) in enumerate(zip(cuesheet.pcm_lengths(audiofile.total_frames(), + audiofile.sample_rate()), + output_tracks)): + try: + audiotools.make_dirs(str(output_filename)) + except OSError, err: + msg.os_error(err) + sys.exit(1) progress = audiotools.SingleProgressDisplay( - msg, msg.filename(filename)) + msg, unicode(output_filename)) - #FIXME - catch from_pcm errors here - encoded_files.append( - AudioType.from_pcm( - filename, + encoded_tracks.append( + output_class.from_pcm( + str(output_filename), audiotools.PCMReaderProgress( audiotools.LimitedPCMReader(total_pcm, pcm_frames), pcm_frames, progress.update), - options.quality)) - encoded_files[-1].set_metadata(track_metadata) + output_quality)) + encoded_tracks[-1].set_metadata(output_metadata) progress.clear() - msg.info(u"%s -> %s" % - (msg.filename(audiofile.filename), - msg.filename(filename))) - - except audiotools.UnsupportedTracknameField, err: - err.error_msg(msg) - sys.exit(1) + msg.info( + audiotools.output_progress( + u"%s -> %s" % (input_filename, output_filename), + i + 1, len(output_tracks))) except audiotools.EncodingError, err: msg.error(unicode(err)) sys.exit(1) #apply ReplayGain to split tracks, if requested - if (options.add_replay_gain and AudioType.can_add_replay_gain()): + if ((audiotools.ADD_REPLAYGAIN and + (options.add_replay_gain if (options.add_replay_gain is not None) + else output_class.lossless_replay_gain()) and + output_class.can_add_replay_gain(encoded_tracks))): rg_progress = audiotools.ReplayGainProgressDisplay( - msg, AudioType.lossless_replay_gain()) + msg, output_class.lossless_replay_gain()) rg_progress.initial_message() try: #separate encoded files by album_name and album_number - for album in audiotools.group_tracks(encoded_files): + for album in audiotools.group_tracks(encoded_tracks): #add ReplayGain to groups of files #belonging to the same album - AudioType.add_replay_gain([a.filename for a in album], - rg_progress.update) + output_class.add_replay_gain([a.filename for a in album], + rg_progress.update) except ValueError, err: rg_progress.clear() msg.error(unicode(err))
View file
audiotools-2.18.tar.gz/tracktag -> audiotools-2.19.tar.gz/tracktag
Changed
@@ -19,391 +19,156 @@ import sys +import os.path import audiotools import audiotools.ui -import os.path -import gettext +import audiotools.text as _ +import termios -gettext.install("audiotools", unicode=True) if (audiotools.ui.AVAILABLE): urwid_present = True urwid = audiotools.ui.urwid - from audiotools.ui import get_focus - - class PathAlbum(audiotools.ui.Album): - def update_tracks(self): - """updates metadata of Album's tracks - returns total number of tracks updated""" - - total = 0 - - for (track, metadata) in zip(self.tracks, self.get_metadata()): - track_metadata = track.track.get_metadata() - updated = 0 - if (track_metadata is not None): - #transfer metadata to track's native format - for (attr, field) in metadata.fields(): - if (getattr(track_metadata, attr) != field): - setattr(track_metadata, attr, field) - updated = 1 - if (updated == 1): - #but only perform update if any changes are made - track.track.update_metadata(track_metadata) - else: - track.track.set_metadata(metadata) - updated = 1 - - total += updated - - return total - - class PathTrack(audiotools.ui.Track): - FIELDS = [(u"Path", "path"), - (u"Track Name", "track_name"), - (u"Track Number", "track_number"), - (u"Album", "album_name"), - (u"Artist", "artist_name"), - (u"Performer", "performer_name"), - (u"Composer", "composer_name"), - (u"Conductor", "conductor_name"), - (u"ISRC", "ISRC"), - (u"Copyright", "copyright"), - (u"Recording Date", "date"), - (u"Comment", "comment")] - - def __init__(self, track, metadata): - """takes an AudioFile""" - - audiotools.ui.Track.__init__(self, metadata) - self.track = track - - self.path = urwid.Text( - audiotools.VerboseMessenger("").filename(self.track.filename)) - - class Tracktag(audiotools.ui.FocusFrame): - (EDITING_LIST_ONLY, - EDITING_LIST_W_FIELDS, - EDITING_FIELDS, - APPLY_LIST, - APPLY_LIST_W_FIELDS, - UNKNOWN_STATE) = range(6) - - #the Tracktag UI states are: - #| | fields closed | fields open | - #|------------------+-------------------+-----------------------| - #| cursor on list | EDITING_LIST_ONLY | EDITING_LIST_W_FIELDS | - #| cursor on fields | N/A | EDITING_FIELDS | - #| cursor on apply | APPLY_LIST | APPLY_LIST_W_FIELDS | - #|------------------+-------------------+-----------------------| - - def __init__(self, tracks): - def by_album(a1, a2): - c = cmp(a1[0][1], a2[0][1]) - if (c == 0): - return cmp(a1[0][2], a2[0][2]) - else: - return c - #group tracks by album - #(track_total, album_name, album_number, album_total) -> - # [(track, metadata), ...] - grouped_tracks = {} - for track in tracks: - try: - metadata = track.get_metadata() - if (metadata is None): - metadata = audiotools.MetaData() - except IOError: - continue - - grouped_tracks.setdefault((metadata.track_total, - metadata.album_name, - track.album_number(), - metadata.album_total), - []).append((track, metadata)) - - #build Albums from grouped tracks - #which are used to build AlbumList - albums = [PathAlbum([PathTrack(*t) for t in tracks]) - for (album, tracks) in - sorted(grouped_tracks.items(), by_album)] - - #attach on-change callback to Albums and Tracks - #to activate the "<Apply>" button's styling - for album in albums: - for field in ["album_name", - "artist_name", - "performer_name", - "composer_name", - "conductor_name", - "media", - "catalog", - "copyright", - "publisher", - "year", - "date", - "comment"]: - urwid.connect_signal(getattr(album, field), - 'change', - self.modified) - for track in album.tracks: - for field in ["track_name", - "artist_name", - "performer_name", - "composer_name", - "conductor_name", - "ISRC", - "copyright", - "date", - "comment"]: - urwid.connect_signal(getattr(track, field), - 'change', - self.modified) - - self.cancel = urwid.Button("Quit", on_press=self.exit) - self.status = urwid.Text(u"", align='left') - self.collapsed = urwid.Divider(div_char=u'\u2500') - self.__modified__ = False - - self.track_selector = audiotools.ui.AlbumList( - albums, self.select_item) - if (len(albums) > 0): - self.save = urwid.Button("Apply", on_press=self.save) - controls = urwid.BoxAdapter( - urwid.ListBox([ - urwid.GridFlow([self.save, self.cancel], - 10, 5, 1, 'center'), - self.status]), - 2) - self.work_area = audiotools.ui.FocusFrame( - body=urwid.Filler(self.track_selector), - footer=self.collapsed) - self.work_area.set_focus_callback(self.update_focus) - self.has_selector = True - else: - controls = urwid.BoxAdapter( - urwid.ListBox([ - urwid.GridFlow([self.cancel], - 10, 5, 1, 'center'), - self.status]), - 2) - - self.work_area = urwid.Frame(body=urwid.Filler(urwid.Text( - u"please select 1 or more tracks " + - u"from the command line", align='center'))) - self.has_selector = False - - audiotools.ui.FocusFrame.__init__(self, - body=self.work_area, - footer=controls) - - if (len(albums) > 0): - self.set_focus_callback(self.update_focus) - else: - self.set_focus('footer') - - def get_state(self): - if (self.work_area.get_footer() is not self.collapsed): - #fields open - if ((get_focus(self) == 'body') and - (get_focus(self.work_area) == 'body')): - #cursor on list - return self.EDITING_LIST_W_FIELDS - elif ((get_focus(self) == 'body') and - (get_focus(self.work_area) == 'footer')): - #cursor on fields - return self.EDITING_FIELDS - elif (get_focus(self) == 'footer'): - #cursor on apply/close - return self.APPLY_LIST_W_FIELDS - else: - #unknown cursor position - return self.UNKNOWN_STATE - else: - #fields closed - if ((get_focus(self) == 'body') and - (get_focus(self.work_area) == 'body')): - #cursor on list - return self.EDITING_LIST_ONLY - elif ((get_focus(self) == 'body') and - (get_focus(self.work_area) == 'footer')): - #cursor on fields - return self.UNKNOWN_STATE - elif (get_focus(self) == 'footer'): - #cursor on apply/close - return self.APPLY_LIST - else: - #unknown cursor position - return self.UNKNOWN_STATE - - def set_state_message(self, state): - if (state != self.UNKNOWN_STATE): - self.set_keys([ - #EDITING_LIST_ONLY - [(u"esc", u"go to Apply / Quit buttons")], - - #EDITING_LIST_W_FIELDS - [(u"esc", u"close fields"), - (u"tab", u"return to fields")], + class Tracktag(urwid.Frame): + def __init__(self, tracks, metadata_choices, final_album=True): + self.__cancelled__ = True + status = urwid.Text(u"") + + buttons = urwid.Filler( + urwid.Columns( + widget_list=[('weight', 1, + urwid.Button(_.LAB_CANCEL_BUTTON, + on_press=self.exit)), + ('weight', 2, + urwid.Button(_.LAB_APPLY_BUTTON + if final_album + else _.LAB_TRACK2TRACK_NEXT, + on_press=self.apply))], + dividechars=3, + focus_column=1)) + + self.filler = audiotools.ui.MetaDataFiller( + [unicode(audiotools.Filename(t.filename).basename()) + for t in tracks], + metadata_choices, + status) + + urwid.Frame.__init__( + self, + body=urwid.Pile([("weight", 1, self.filler), + ("fixed", 1, buttons)]), + footer=status) + + def apply(self, button): + self.__cancelled__ = False + raise urwid.ExitMainLoop() - #EDITING_FIELDS - [(u"esc", u"close fields"), - (u"tab", u"return to list")], + def exit(self, button): + self.__cancelled__ = True + raise urwid.ExitMainLoop() - #APPLY_LIST - [(u"esc", u"return to list")], - - #APPLY_LIST_W_FIELDS - [(u"esc", u"return to list")] - ][state]) - else: - self.status.set_text(u"") - - def select_item(self, checkbox, state_change, user_data=None): - if (state_change == True): - #select item - self.work_area.set_footer( - urwid.BoxAdapter(user_data, user_data.field_count())) - self.work_area.set_focus('footer') - elif (state_change == False): - #unselect item - self.work_area.set_footer(self.collapsed) - self.work_area.set_focus('body') + def cancelled(self): + return self.__cancelled__ def handle_text(self, i): - state = self.get_state() - if (state == self.EDITING_LIST_ONLY): - if (i == 'esc'): - self.set_focus('footer') - elif (state == self.EDITING_LIST_W_FIELDS): - if (i == 'tab'): - self.work_area.set_focus('footer') - self.set_focus('body') - elif (i == 'esc'): - for checkbox in self.track_selector.radios: - checkbox.set_state(False, do_callback=False) - self.work_area.set_footer(self.collapsed) - self.work_area.set_focus('body') - self.set_focus('body') - elif (state == self.EDITING_FIELDS): - if (i == 'tab'): - self.work_area.set_focus('body') - self.set_focus('body') - elif (i == 'esc'): - for checkbox in self.track_selector.radios: - checkbox.set_state(False, do_callback=False) - self.work_area.set_footer(self.collapsed) - self.work_area.set_focus('body') - self.set_focus('body') - elif (state == self.APPLY_LIST): - if (i == 'esc'): - self.set_focus('body') - self.work_area.set_focus('body') - elif (state == self.APPLY_LIST_W_FIELDS): - if (i == 'esc'): - self.set_focus('body') - self.work_area.set_focus('body') - - def update_focus(self, widget, focus_part): - """widget is a urwid.Frame subclass, focus_part is a string""" - - self.set_state_message(self.get_state()) - - def set_keys(self, keys): - """keys is a [(key, action), ...] list - where 'key' and 'action' are both strings""" - - text = [] - for (last, (key, action)) in audiotools.iter_last(iter(keys)): - text.append(('key', key)) - text.append(u" - " + action) - if (not last): - text.append(u" ") - - self.status.set_text(text) - - def modified(self, widget, new_value): - if (not self.__modified__): - self.save.set_label(('modified', u'Apply')) - self.__modified__ = True - - def save(self, button, arg=None): - if (self.__modified__): - self.status.set_text(u"updating tracks") - self.draw_screen() - total = 0 - for album in self.track_selector.albums: - total += album.update_tracks() - - if (total != 1): - self.status.set_text(u"%d tracks updated" % (total)) - else: - self.status.set_text(u"%d track updated" % (total)) - self.save.set_label(u"Apply") - self.__modified__ = False - else: - self.set_keys([(u"esc", u"return to list")]) + if (i == 'esc'): + self.exit(None) + elif (i == 'f1'): + self.filler.selected_match.select_previous_item() + elif (i == 'f2'): + self.filler.selected_match.select_next_item() - def exit(self, button, arg=None): - raise urwid.ExitMainLoop() + def populated_metadata(self): + """yields a new, populated MetaData object per track + to be called once Urwid's main loop has completed""" + + for (track_id, metadata) in self.filler.selected_match.metadata(): + yield metadata else: urwid_present = False -#tries to return a populated Image object of the appropriate type -#raises InvalidImage if something goes wrong during opening or parsing -def get_raw_image(filename, type): - try: - f = open(filename, 'rb') - data = f.read() - f.close() - - return audiotools.Image.new(data, u'', type) - except IOError: - raise audiotools.InvalidImage(_(u"Unable to open file")) - - -def get_thumbnailed_image(filename, type): - image = get_raw_image(filename, type) - if ((image.width > audiotools.THUMBNAIL_SIZE) or - (image.height > audiotools.THUMBNAIL_SIZE)): - return image.thumbnail(audiotools.THUMBNAIL_SIZE, - audiotools.THUMBNAIL_SIZE, - audiotools.THUMBNAIL_FORMAT) - else: - return image - - -#given a comment filename -#returns the comment as a unicode string -#or exits with an error if the file cannot be read -#or is not UTF-8 text -def read_comment(filename, messenger): - try: - f = open(filename, 'rb') - data = f.read().decode('utf-8', 'replace') - f.close() - - if (((data.count(u"\uFFFD") * 100) / len(data)) >= 10): - messenger.error( - _(u"Comment file \"%s\" does not appear to be UTF-8 text") % - (messenger.filename(filename))) - sys.exit(1) - else: - return data - except IOError: - messenger.error(_(u"Unable to open comment file \"%s\"") % \ - (messenger.filename(filename))) - sys.exit(1) - +UPDATE_OPTIONS = {"track_name": ("--name", + _.LAB_TRACKTAG_UPDATE_TRACK_NAME), + "artist_name": ("--artist", + _.LAB_TRACKTAG_UPDATE_ARTIST_NAME), + "performer_name": ("--performer", + _.LAB_TRACKTAG_UPDATE_PERFORMER_NAME), + "composer_name": ("--composer", + _.LAB_TRACKTAG_UPDATE_COMPOSER_NAME), + "conductor_name": ("--conductor", + _.LAB_TRACKTAG_UPDATE_CONDUCTOR_NAME), + "album_name": ("--album", + _.LAB_TRACKTAG_UPDATE_ALBUM_NAME), + "catalog": ("--catalog", + _.LAB_TRACKTAG_UPDATE_CATALOG), + "track_number": ("--number", + _.LAB_TRACKTAG_UPDATE_TRACK_NUMBER), + "track_total": ("--track-total", + _.LAB_TRACKTAG_UPDATE_TRACK_TOTAL), + "album_number": ("--album-number", + _.LAB_TRACKTAG_UPDATE_ALBUM_NUMBER), + "album_total": ("--album-total", + _.LAB_TRACKTAG_UPDATE_ALBUM_TOTAL), + "ISRC": ("--ISRC", + _.LAB_TRACKTAG_UPDATE_ISRC), + "publisher": ("--publisher", + _.LAB_TRACKTAG_UPDATE_PUBLISHER), + "media": ("--media-type", + _.LAB_TRACKTAG_UPDATE_MEDIA), + "year": ("--year", + _.LAB_TRACKTAG_UPDATE_YEAR), + "date": ("--date", + _.LAB_TRACKTAG_UPDATE_DATE), + "copyright": ("--copyright", + _.LAB_TRACKTAG_UPDATE_COPYRIGHT), + "comment": ("--comment", + _.LAB_TRACKTAG_UPDATE_COMMENT)} + +REMOVE_OPTIONS = {"track_name": ("--remove-name", + _.LAB_TRACKTAG_REMOVE_TRACK_NAME), + "artist_name": ("--remove-artist", + _.LAB_TRACKTAG_REMOVE_ARTIST_NAME), + "performer_name": ("--remove-performer", + _.LAB_TRACKTAG_REMOVE_PERFORMER_NAME), + "composer_name": ("--remove-composer", + _.LAB_TRACKTAG_REMOVE_COMPOSER_NAME), + "conductor_name": ("--remove-conductor", + _.LAB_TRACKTAG_REMOVE_CONDUCTOR_NAME), + "album_name": ("--remove-album", + _.LAB_TRACKTAG_REMOVE_ALBUM_NAME), + "catalog": ("--remove-catalog", + _.LAB_TRACKTAG_REMOVE_CATALOG), + "track_number": ("--remove-number", + _.LAB_TRACKTAG_REMOVE_TRACK_NUMBER), + "track_total": ("--remove-track-total", + _.LAB_TRACKTAG_REMOVE_TRACK_TOTAL), + "album_number": ("--remove-album-number", + _.LAB_TRACKTAG_REMOVE_ALBUM_NUMBER), + "album_total": ("--remove-album-total", + _.LAB_TRACKTAG_REMOVE_ALBUM_TOTAL), + "ISRC": ("--remove-ISRC", + _.LAB_TRACKTAG_REMOVE_ISRC), + "publisher": ("--remove-publisher", + _.LAB_TRACKTAG_REMOVE_PUBLISHER), + "media": ("--remove-media-type", + _.LAB_TRACKTAG_REMOVE_MEDIA), + "year": ("--remove-year", + _.LAB_TRACKTAG_REMOVE_YEAR), + "date": ("--remove-date", + _.LAB_TRACKTAG_REMOVE_DATE), + "copyright": ("--remove-copyright", + _.LAB_TRACKTAG_REMOVE_COPYRIGHT), + "comment": ("--remove-comment", + _.LAB_TRACKTAG_REMOVE_COMMENT)} if (__name__ == '__main__'): #add an enormous number of options to the parser #neatly categorized for convenience parser = audiotools.OptionParser( - usage=_(u"%prog [options] <track 1> [track 2] ..."), + usage=_.USAGE_TRACKTAG, version="Python Audio Tools %s" % (audiotools.VERSION)) parser.add_option( @@ -411,46 +176,23 @@ action='store_true', default=False, dest='interactive', - help=_(u'run in interactive mode')) - - text_group = audiotools.OptionGroup(parser, _(u"Text Options")) - - for (option, destination, helptext) in [ - ('--name', 'track_name', _(u"the name of the track")), - ('--artist', 'artist_name', _(u'the name of the artist')), - ('--performer', 'performer_name', _(u'the name of the performer')), - ('--composer', 'composer_name', _(u'the name of the composer')), - ('--conductor', 'conductor_name', _(u'the name of the conductor')), - ('--album', 'album_name', _(u'the name of the album')), - ('--catalog', 'catalog', _(u'the catalog number of the album')), - ('--number', 'track_number', - _(u"the number of the track in the album")), - ('--track-total', 'track_total', - _(u"the total number of tracks in the album")), - ('--album-number', 'album_number', - _(u'the number of the album in a set of albums')), - ('--album-total', 'album_total', - _(u"the total number of albums in a set of albums")), - ('--ISRC', 'ISRC', _(u'the ISRC of the track')), - ('--publisher', 'publisher', _(u'the publisher of the album')), - ('--media-type', 'media_type', - _(u'the media type of the album, such as "CD"')), - ('--year', 'year', _(u'the year of release')), - ('--date', 'date', _(u'the date of recording')), - ('--copyright', 'copyright', _(u'copyright information')), - ('--comment', 'comment', _(u'a text comment'))]: - if (destination not in audiotools.MetaData.INTEGER_FIELDS): - text_group.add_option(option, - action='store', - type='string', - dest=destination, - help=helptext) - else: - text_group.add_option(option, - action='store', - type='int', - dest=destination, - help=helptext) + help=_.OPT_INTERACTIVE_METADATA) + + text_group = audiotools.OptionGroup(parser, _.OPT_CAT_TEXT) + + for field in audiotools.MetaData.FIELD_ORDER: + if (field in UPDATE_OPTIONS): + variable = "update_%s" % (field) + (option, help_text) = UPDATE_OPTIONS[field] + text_group.add_option( + option, + action='store', + type='string' if field not in + audiotools.MetaData.INTEGER_FIELDS else 'int', + dest=variable, + metavar='STRING' if field not in + audiotools.MetaData.INTEGER_FIELDS else 'INT', + help=help_text) text_group.add_option( '--comment-file', @@ -458,7 +200,7 @@ type='string', dest='comment_file', metavar='FILENAME', - help=_(u'a file containing comment text')) + help=_.OPT_TRACKTAG_COMMENT_FILE) parser.add_option_group(text_group) @@ -467,97 +209,75 @@ action='store_true', default=False, dest='replace', - help=_(u'completely replace all metadata')) + help=_.OPT_TRACKTAG_REPLACE) - parser.add_option( - '--cue', - action='store', - type='string', - dest='cue', - metavar='FILENAME', - help=_(u'a cuesheet to import or get audio metadata from')) + remove_group = audiotools.OptionGroup(parser, _.OPT_CAT_REMOVAL) - img_group = audiotools.OptionGroup(parser, _(u"Image Options")) - - img_group.add_option( - '--remove-images', - action='store_true', - default=False, - dest='remove_images', - help=_(u'remove existing images prior to adding new ones')) - - for (option, destination, helptext) in [ - ('--front-cover', 'front_cover', - _(u'an image file of the front cover')), - ('--back-cover', 'back_cover', - _(u'an image file of the back cover')), - ('--leaflet', 'leaflet', _(u'an image file of a leaflet page')), - ('--media', 'media', _(u'an image file of the media')), - ('--other-image', 'other_image', - _(u'an image file related to the track'))]: - img_group.add_option( - option, - action='append', - type='string', - dest=destination, - metavar='FILENAME', - help=helptext) - - img_group.add_option( - '-T', '--thumbnail', - action='store_true', - default=False, - dest='thumbnail', - help=_(u'convert given images to smaller thumbnails ' + - u'before adding')) - - parser.add_option_group(img_group) - - remove_group = audiotools.OptionGroup(parser, _(u"Removal Options")) - - for (option, destination, helptext) in [ - ('--remove-name', 'remove_track_name', _(u'remove track name')), - ('--remove-artist', 'remove_artist_name', _(u'remove track artist')), - ('--remove-performer', 'remove_performer_name', - _(u'remove track performer')), - ('--remove-composer', 'remove_composer_name', - _(u'remove track composer')), - ('--remove-conductor', 'remove_conductor_name', - _(u'remove track conductor')), - ('--remove-album', 'remove_album_name', _(u'remove album name')), - ('--remove-catalog', 'remove_catalog', _(u'remove catalog number')), - ('--remove-number', 'remove_track_number', - _(u'remove track number')), - ('--remove-track-total', 'remove_track_total', - _(u'remove total number of tracks')), - ('--remove-album-number', 'remove_album_number', - _(u'remove album number')), - ('--remove-album-total', 'remove_album_total', - _(u'remove total number of albums')), - ('--remove-ISRC', 'remove_ISRC', _(u'remove ISRC')), - ('--remove-publisher', 'remove_publisher', _(u'remove publisher')), - ('--remove-media-type', 'remove_media_type', - _(u'remove album\'s media type')), - ('--remove-year', 'remove_year', _(u'remove release year')), - ('--remove-date', 'remove_date', _(u'remove recording date')), - ('--remove-copyright', 'remove_copyright', - _(u'remove copyright information')), - ('--remove-comment', 'remove_comment', _(u'remove text comment'))]: - remove_group.add_option( - option, - action='store_true', - default=False, - dest=destination, - help=helptext) + for field in audiotools.MetaData.FIELD_ORDER: + if (field in REMOVE_OPTIONS): + variable = "remove_%s" % (field) + (option, help_text) = REMOVE_OPTIONS[field] + remove_group.add_option( + option, + action='store_true', + default=False, + dest=variable, + help=help_text) parser.add_option_group(remove_group) + lookup = audiotools.OptionGroup(parser, _.OPT_CAT_CD_LOOKUP) + + lookup.add_option( + '-M', '--metadata-lookup', action='store_true', + default=False, dest='metadata_lookup', + help=_.OPT_METADATA_LOOKUP) + + lookup.add_option( + '--musicbrainz-server', action='store', + type='string', dest='musicbrainz_server', + default=audiotools.MUSICBRAINZ_SERVER, + metavar='HOSTNAME') + lookup.add_option( + '--musicbrainz-port', action='store', + type='int', dest='musicbrainz_port', + default=audiotools.MUSICBRAINZ_PORT, + metavar='PORT') + lookup.add_option( + '--no-musicbrainz', action='store_false', + dest='use_musicbrainz', + default=audiotools.MUSICBRAINZ_SERVICE, + help=_.OPT_NO_MUSICBRAINZ) + + lookup.add_option( + '--freedb-server', action='store', + type='string', dest='freedb_server', + default=audiotools.FREEDB_SERVER, + metavar='HOSTNAME') + lookup.add_option( + '--freedb-port', action='store', + type='int', dest='freedb_port', + default=audiotools.FREEDB_PORT, + metavar='PORT') + lookup.add_option( + '--no-freedb', action='store_false', + dest='use_freedb', + default=audiotools.FREEDB_SERVICE, + help=_.OPT_NO_FREEDB) + + lookup.add_option( + '-D', '--default', + dest='use_default', action='store_true', default=False, + help=_.OPT_DEFAULT) + + parser.add_option_group(lookup) + parser.add_option( '--replay-gain', action='store_true', default=False, dest='add_replay_gain', - help=_(u'add ReplayGain metadata to track(s)')) + help=_.OPT_REPLAY_GAIN_TRACKTAG) parser.add_option( '-j', '--joint', @@ -565,7 +285,7 @@ type='int', default=audiotools.MAX_JOBS, dest='max_processes', - help=_(u'the maximum number of processes to run at a time')) + help=_.OPT_JOINT) parser.add_option( '-V', '--verbose', @@ -573,260 +293,236 @@ dest='verbosity', choices=audiotools.VERBOSITY_LEVELS, default=audiotools.DEFAULT_VERBOSITY, - help=_(u'the verbosity level to execute at')) + help=_.OPT_VERBOSE) (options, args) = parser.parse_args() msg = audiotools.Messenger("tracktag", options) - #open our set of input files for tagging - audiofiles = audiotools.open_files(args, messenger=msg) + #ensure interactive mode is available, if selected + if (options.interactive and (not audiotools.ui.AVAILABLE)): + audiotools.ui.not_available_message(msg) + sys.exit(1) - #and the --cue file - isrcs = {} - import_cuesheet = None - if (options.cue is not None): + #open a --comment-file as UTF-8 + if (options.comment_file is not None): try: - cuesheet = audiotools.read_sheet(options.cue) - - #if there's a single audio file - #and the cuesheet is sized to fit that file - #attempt to embed the cuesheet in the file - if ((len(audiofiles) == 1) and - (list(cuesheet.pcm_lengths( - audiofiles[0].total_frames()))[-1] > 0)): - import_cuesheet = cuesheet - else: - #otherwise, treat the cuesheet is a source of ISRC data - isrcs = dict([(k, v.decode('ascii', 'replace')) - for (k, v) in - cuesheet.ISRCs().items()]) - except audiotools.SheetException, err: - msg.error(unicode(err)) - sys.exit(1) - - if (options.thumbnail): - if (not audiotools.can_thumbnail()): - msg.error(_(u"Unable to generate thumbnails")) - msg.info( - _(u"Please install the Python Imaging Library")) - msg.info( - _(u"available at http://www.pythonware.com/products/pil/")) - msg.info(_(u"to enable image resizing")) + comment_file = open(options.comment_file, + "rb").read().decode('utf-8', 'replace') + except IOError: + msg.error(_.ERR_TRACKTAG_COMMENT_IOERROR % + (audiotools.Filename(options.comment_file),)) sys.exit(1) - if (audiotools.THUMBNAIL_FORMAT.upper() not in - audiotools.thumbnail_formats()): - msg.error(_(u"Unsupported thumbnail format \"%s\"") % - (audiotools.THUMBNAIL_FORMAT)) - msg.info(_(u"Available formats are: %s") % - (", ".join(audiotools.thumbnail_formats()))) + if (((comment_file.count(u"\uFFFD") * 100) / + len(comment_file)) >= 10): + msg.error(_.ERR_TRACKTAG_COMMENT_NOT_UTF8 % + (audiotools.Filename(options.comment_file),)) sys.exit(1) - - get_image = get_thumbnailed_image else: - get_image = get_raw_image + comment_file = None - for file in audiofiles: - track_modified = False - - #determine which MetaData to use as our base - #depending on whether we're performing a full replacement - if (not options.replace): - metadata = file.get_metadata() - if (metadata is not None): - update_method = "update_metadata" - else: - metadata = audiotools.MetaData() - update_method = "set_metadata" - else: - metadata = audiotools.MetaData() - update_method = "set_metadata" - - #apply tagging options to that metadata in reverse order of precedence + #open our set of input files for tagging + try: + tracks = audiotools.open_files(args, + messenger=msg, + no_duplicates=True) + except audiotools.DuplicateFile, err: + msg.error(_.ERR_DUPLICATE_FILE % (err.filename,)) + sys.exit(1) - #perform the image tagging - try: - if (metadata.supports_images()): - if (options.remove_images): - for i in metadata.images(): - metadata.delete_image(i) - track_modified = True - - if (options.front_cover is not None): - for path in options.front_cover: - metadata.add_image(get_image(path, 0)) - track_modified = True - - if (options.leaflet is not None): - for path in options.leaflet: - metadata.add_image(get_image(path, 2)) - track_modified = True - - if (options.back_cover is not None): - for path in options.back_cover: - metadata.add_image(get_image(path, 1)) - track_modified = True - - if (options.media is not None): - for path in options.media: - metadata.add_image(get_image(path, 3)) - track_modified = True - - if (options.other_image is not None): - for path in options.other_image: - metadata.add_image(get_image(path, 4)) - track_modified = True - except audiotools.InvalidImage, err: - msg.error(_(u"%(filename)s: %(message)s") % \ - {"filename": msg.filename(file.filename), - "message": unicode(err)}) - sys.exit(1) + if (len(tracks) == 0): + msg.error(_.ERR_1_FILE_REQUIRED) + sys.exit(1) - #apply text field removal - for field in ('track_name', - 'artist_name', - 'performer_name', - 'composer_name', - 'conductor_name', - 'album_name', - 'catalog', - 'track_number', - 'track_total', - 'album_number', - 'album_total', - 'ISRC', - 'publisher', - 'media_type', - 'year', - 'date', - 'copyright', - 'comment'): - if (getattr(options, 'remove_' + field)): - delattr(metadata, field) - track_modified = True - - if (options.track_number is not None): - track_number = options.track_number - else: - track_number = file.track_number() - - #handle cuesheet ISRC data - if (track_number in isrcs): - metadata.ISRC = isrcs[track_number] - track_modified = True - - #update fields from the command line - for field in ('track_name', - 'artist_name', - 'performer_name', - 'composer_name', - 'conductor_name', - 'album_name', - 'catalog', - 'track_number', - 'track_total', - 'album_number', - 'album_total', - 'ISRC', - 'publisher', - 'media_type', - 'year', - 'date', - 'copyright', - 'comment'): - if (getattr(options, field) is not None): - attr = getattr(options, field) - if (isinstance(attr, str)): - attr = attr.decode(audiotools.IO_ENCODING, 'replace') - setattr(metadata, field, attr) - track_modified = True - - #add comment-file - if (options.comment_file is not None): - metadata.comment = read_comment(options.comment_file, msg) - track_modified = True - - #check if there's been any modifications made - if (track_modified or - (import_cuesheet is not None) or - options.replace): + if (not options.metadata_lookup): + #if not performing metadata lookup, + #build fresh MetaData objects + #and tag them all simultaneously + input_albums = [(tracks, + [t.get_metadata() for t in tracks], + [[audiotools.MetaData() for t in tracks]])] + else: + #if performining metadata lookup, + #get initial metadata from CD lookup service + #for each album in set of tracks + #and tag them album-by-album + input_albums = [] + for album_tracks in audiotools.group_tracks(tracks): + track_metadatas = [t.get_metadata() for t in album_tracks] + + metadata_choices = audiotools.track_metadata_lookup( + audiofiles=album_tracks, + musicbrainz_server=options.musicbrainz_server, + musicbrainz_port=options.musicbrainz_port, + freedb_server=options.freedb_server, + freedb_port=options.freedb_port, + use_musicbrainz=options.use_musicbrainz, + use_freedb=options.use_freedb) + + if (not options.interactive): + #if interactive mode not indicated, + #have user pick choice right away + metadata_choices = [ + audiotools.ui.select_metadata( + metadata_choices, msg, options.use_default)] + else: + #otherwise, add tracks' original metadatas + #to list of possibilities + metadata_choices.insert(0, track_metadatas) + + input_albums.append((album_tracks, + track_metadatas, + metadata_choices)) + + #a list of (tracks, old_metadatas, new_metadatas) tuples + #to apply once all interactive widgets have finished + to_tag = [] + + for (last_album, + (album_tracks, + album_track_metadatas, + album_metadata_choices)) in audiotools.iter_last(iter(input_albums)): + #if not replacing all metadata + #merge metadata with that from original files, if any + #where new values take precedence over old ones + if (not options.replace): + for choice in album_metadata_choices: + for (new_metadata, + old_metadata) in zip(choice, album_track_metadatas): + if (old_metadata is not None): + for (attr, value) in new_metadata.empty_fields(): + setattr(new_metadata, attr, + getattr(old_metadata, attr)) + + for choice in album_metadata_choices: + for metadata in choice: + #apply field removal options across all metadata choices + for attr in audiotools.MetaData.FIELD_ORDER: + if (getattr(options, "remove_%s" % (attr))): + delattr(metadata, attr) + + #apply field addition options across all metadata choices + for attr in audiotools.MetaData.FIELD_ORDER: + if (getattr(options, "update_%s" % (attr)) is not None): + value = getattr(options, "update_%s" % (attr)) + setattr(metadata, + attr, + value.decode(audiotools.FS_ENCODING) + if (attr not in + audiotools.MetaData.INTEGER_FIELDS) + else value) + + #apply comment file across all metadata choices + if (comment_file is not None): + metadata.comment = comment_file + + if (options.interactive): + #if interactive mode indicated, edit metadata choices in widget + widget = Tracktag(album_tracks, + album_metadata_choices, + last_album) + loop = audiotools.ui.urwid.MainLoop( + widget, + audiotools.ui.style(), + unhandled_input=widget.handle_text) try: - #either set or update metadata - #depending on whether we're performing a full replacement - getattr(file, update_method)(metadata) - - #insert embedded cuesheet file - if (import_cuesheet is not None): - file.set_cuesheet(import_cuesheet) - except IOError: - msg.error(_(u"Unable to modify \"%s\"") % \ - msg.filename(file.filename)) + loop.run() + msg.ansi_clearscreen() + except (termios.error, IOError): + msg.error(_.ERR_TERMIOS_ERROR) + msg.info(_.ERR_TERMIOS_SUGGESTION) + msg.info(audiotools.ui.xargs_suggestion(sys.argv)) sys.exit(1) - #add/apply replay_gain to tracks if indicated - if (options.add_replay_gain and - (len(audiofiles) > 0) and - (audiofiles[0].can_add_replay_gain())): - #separate encoded files by album_name and album_number - try: - queue = audiotools.ExecProgressQueue( - audiotools.ProgressDisplay(msg)) - - for album in audiotools.group_tracks(audiofiles): - #add ReplayGain to groups of files - #belonging to the same album - - album_number = set([a.album_number() for a in album]).pop() - audio_type = album[0].__class__ - - if (album_number == 0): - if (audio_type.lossless_replay_gain()): - progress_text = _(u"Adding ReplayGain") - completion_output = _(u"ReplayGain added") - else: - progress_text = _(u"Applying ReplayGain") - completion_output = _(u"ReplayGain applied") + if (not widget.cancelled()): + to_tag.append((album_tracks, + album_track_metadatas, + list(widget.populated_metadata()))) + else: + sys.exit(0) + else: + #if interactive mode not indicated, use first choice + #since all other choices have been culled already + to_tag.append((album_tracks, + album_track_metadatas, + album_metadata_choices[0])) + + #once all final output metadata is set, + #perform actual tagging + for (album_tracks, old_metadatas, new_metadatas) in to_tag: + if (not options.replace): + #apply final metadata to tracks using update_metadata + #if no replacement + for (old_metadata, + (track, + new_metadata)) in zip(old_metadatas, + zip(album_tracks, new_metadatas)): + if (old_metadata is not None): + #merge new fields with old fields + field_updated = False + for (attr, value) in new_metadata.fields(): + if (getattr(old_metadata, + attr) != getattr(new_metadata, + attr)): + setattr(old_metadata, attr, value) + field_updated = True + + if (field_updated): + #update track if at least one field has changed + try: + track.update_metadata(old_metadata) + except IOError, err: + msg.error( + _.ERR_ENCODING_ERROR % + (audiotools.Filename(track.filename),)) + sys.exit(1) else: - if (audio_type.lossless_replay_gain()): - progress_text = ( - _(u"Adding ReplayGain to album %d") % - (album_number)) - completion_output = ( - _(u"ReplayGain added to album %d") % - (album_number)) - else: - progress_text = ( - _(u"Applying ReplayGain to album %d") % - (album_number)) - completion_output = ( - _(u"ReplayGain applied to album %d") % - (album_number)) - - queue.execute(audio_type.add_replay_gain, - progress_text, - completion_output, - [a.filename for a in album]) - - queue.run(options.max_processes) - except ValueError, err: - msg.error(unicode(err)) - sys.exit(1) - - #finally, after all the command-line arguments are processed - #run interactive mode if indicated - if (options.interactive): - if (not urwid_present): - msg.error(_(u"urwid is required for interactive mode")) - msg.output(_(u"Please download and install urwid " + - u"from http://excess.org/urwid/")) - msg.output(_(u"or your system's package manager.")) - sys.exit(1) - - tracktag = Tracktag(audiofiles) - loop = urwid.MainLoop(tracktag, - [('key', 'white', 'dark blue'), - ('albumname', 'default,underline', 'default'), - ('modified', 'default,bold', 'default', '')], - unhandled_input=tracktag.handle_text) - tracktag.draw_screen = loop.draw_screen - loop.run() + try: + track.set_metadata(new_metadata) + except IOError, err: + msg.error(_.ERR_ENCODING_ERROR % + (audiotools.Filename(track.filename),)) + sys.exit(1) + else: + #apply final metadata to tracks + #using set_metadata() if replacement + for (track, metadata) in zip(album_tracks, new_metadatas): + try: + track.set_metadata(metadata) + except IOError, err: + msg.error(_.ERR_ENCODING_ERROR % + (audiotools.Filename(track.filename),)) + sys.exit(1) + + #add ReplayGain to tracks, if indicated + queue = audiotools.ExecProgressQueue( + audiotools.ProgressDisplay(msg)) + + for album_tracks in audiotools.group_tracks(tracks): + if (options.add_replay_gain and (len(album_tracks) > 0)): + album_number = set([t.album_number() for t in album_tracks]).pop() + audio_type = album_tracks[0].__class__ + + if (audio_type.can_add_replay_gain(album_tracks)): + #FIXME - should pull ReplayGain text + #from elsewhere + queue.execute( + audio_type.add_replay_gain, + (u"%s ReplayGain%s" % + ((u"Adding" if audio_type.lossless_replay_gain() else + u"Applying"), + (u"" if album_number is None else + (u" to album %d" % (album_number))))), + (u"ReplayGain %s%s" % + ((u"added" if audio_type.lossless_replay_gain() else + u"applied"), + (u"" if album_number is None else + (u" to album %d" % (album_number))))), + [a.filename for a in album_tracks]) + + #execute ReplayGain addition once all tracks have been tagged + try: + queue.run(options.max_processes) + except ValueError, err: + msg.error(unicode(err)) + sys.exit(1)
View file
audiotools-2.18.tar.gz/trackverify -> audiotools-2.19.tar.gz/trackverify
Changed
@@ -20,9 +20,7 @@ import sys import os.path import audiotools -import gettext - -gettext.install("audiotools", unicode=True) +import audiotools.text as _ MAX_CPUS = audiotools.MAX_JOBS @@ -39,9 +37,9 @@ class FailedAudioFile: - def __init__(self, class_name, path, err): + def __init__(self, class_name, filename, err): self.NAME = class_name - self.filename = path + self.filename = filename self.err = err def verify(self): @@ -49,53 +47,54 @@ def open_file(filename): + #this is much like audiotools.open + #except that file init errors fail differently + f = open(filename, "rb") try: - for audioclass in audiotools.TYPE_MAP.values(): - f.seek(0, 0) - if (audioclass.is_type(f)): - class_name = audioclass.NAME - try: - return audioclass(filename) - except audiotools.InvalidFile, err: - return FailedAudioFile(class_name, filename, err) + audio_class = audiotools.file_type(f) + if (((audio_class is not None) and + (audio_class.has_binaries(audiotools.BIN)))): + class_name = audio_class.NAME + try: + return audio_class(filename) + except audiotools.InvalidFile, err: + return FailedAudioFile(class_name, filename, err) else: raise audiotools.UnsupportedFile(filename) finally: f.close() -def get_tracks(args, accept_list): +def get_tracks(args, queued_files, accept_list=None): if (accept_list is not None): accept_list = set(accept_list) for path in args: if (os.path.isfile(path)): try: - track = open_file(path) - if not ((accept_list is not None) and - (track.NAME not in accept_list)): - yield track - except (audiotools.UnsupportedFile, IOError): + filename = audiotools.Filename(path) + if (filename not in queued_files): + queued_files.add(filename) + track = open_file(str(filename)) + if ((accept_list is None) or (track.NAME in accept_list)): + yield track + except (audiotools.UnsupportedFile, IOError, OSError): continue elif (os.path.isdir(path)): for (d, ds, fs) in os.walk(path): - for f in fs: - try: - track = open_file(os.path.join(d, f)) - if not ((accept_list is not None) and - (track.NAME not in accept_list)): - yield track - except (audiotools.UnsupportedFile, IOError): - continue + for track in get_tracks([os.path.join(d, f) for f in fs], + queued_files, + accept_list=accept_list): + yield track def verify(progress, track): try: track.verify(progress) - return (track.filename, track.NAME, None) + return (audiotools.Filename(track.filename), track.NAME, None) except audiotools.InvalidFile, err: - return (track.filename, track.NAME, unicode(err)) + return (audiotools.Filename(track.filename), track.NAME, unicode(err)) class Results: @@ -105,24 +104,24 @@ self.summary_failure = {} def display(self, result): - (path, track_type, error) = result + (filename, track_type, error) = result if (error is None): self.summary_success.setdefault(track_type, Counter()).increment() - return _(u"%(path)s : %(result)s") % { - "path": self.msg.filename(path), - "result": self.msg.ansi(_(u"OK"), [self.msg.FG_GREEN])} + return _.LAB_TRACKVERIFY_RESULT % { + "path": filename, + "result": self.msg.ansi(_.LAB_TRACKVERIFY_OK, + [self.msg.FG_GREEN])} else: self.summary_failure.setdefault(track_type, Counter()).increment() - return _(u"%(path)s : %(result)s") % { - "path": self.msg.filename(path), - "result": self.msg.ansi(error, [self.msg.FG_RED])} - + return _.LAB_TRACKVERIFY_RESULT % { + "path": filename, + "result": self.msg.ansi(error, + [self.msg.FG_RED])} -gettext.install("audiotools", unicode=True) if (__name__ == '__main__'): parser = audiotools.OptionParser( - usage=_(u"%prog <track 1> [track 2] ..."), + usage=_.USAGE_TRACKVERIFY, version="Python Audio Tools %s" % (audiotools.VERSION)) parser.add_option('-t', '--type', @@ -130,40 +129,37 @@ dest='accept_list', metavar='type', choices=audiotools.TYPE_MAP.keys(), - help=_(u'a type of audio to accept')) + help=_.OPT_TYPE_TRACKVERIFY) parser.add_option('-R', '--no-summary', action='store_true', dest='no_summary', - help=_(u'suppress summary output')) + help=_.OPT_NO_SUMMARY) parser.add_option('-j', '--joint', - action='store', - type='int', - default=MAX_CPUS, - dest='max_processes', - help=_(u'the maximum number of processes to run at a time')) + action='store', + type='int', + default=MAX_CPUS, + dest='max_processes', + help=_.OPT_JOINT) parser.add_option('-V', '--verbose', action='store', dest='verbosity', choices=audiotools.VERBOSITY_LEVELS, default=audiotools.DEFAULT_VERBOSITY, - help=_(u'the verbosity level to execute at')) + help=_.OPT_VERBOSE) (options, args) = parser.parse_args() msg = audiotools.Messenger("trackverify", options) + queued_files = set([]) # a set of Filename objects already encountered results = Results(msg) queue = audiotools.ExecProgressQueue(audiotools.ProgressDisplay(msg)) - for track in get_tracks(args, options.accept_list): - # if (i == 0): - # pass - # elif ((i % 100) == 0): - # msg.ansi_clearline() - # msg.partial_output(_(u"Finding tracks (%d)") % (i)) + for track in get_tracks(args, queued_files, options.accept_list): queue.execute(function=verify, - progress_text=msg.filename(track.filename), + progress_text= + unicode(audiotools.Filename(track.filename)), completion_output=results.display, track=track) msg.ansi_clearline() @@ -178,16 +174,16 @@ failure_total = sum(map(int, summary_failure.values())) if ((len(formats) > 0) and (not options.no_summary)): - msg.output(_(u"Results:")) + msg.output(_.LAB_TRACKVERIFY_RESULTS) msg.output(u"") msg.new_row() - msg.output_column(_(u"format"), True) + msg.output_column(_.LAB_TRACKVERIFY_RESULT_FORMAT, True) msg.output_column(u" ") - msg.output_column(_(u"success"), True) + msg.output_column(_.LAB_TRACKVERIFY_RESULT_SUCCESS, True) msg.output_column(u" ") - msg.output_column(_(u"failure"), True) + msg.output_column(_.LAB_TRACKVERIFY_RESULT_FAILURE, True) msg.output_column(u" ") - msg.output_column(_(u"total"), True) + msg.output_column(_.LAB_TRACKVERIFY_RESULT_TOTAL, True) msg.divider_row([u"-", u" ", u"-", u" ", u"-", u" ", u"-"]) for format in formats:
Locations
Projects
Search
Status Monitor
Help
Open Build Service
OBS Manuals
API Documentation
OBS Portal
Reporting a Bug
Contact
Mailing List
Forums
Chat (IRC)
Twitter
Open Build Service (OBS)
is an
openSUSE project
.