Melodia/Melodia/resources/audiotools/toc.py

247 lines
7.6 KiB
Python

#!/usr/bin/python
#Audio Tools, a module and set of tools for manipulating audio data
#Copyright (C) 2008-2011 Brian Langenberger
#This program is free software; you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation; either version 2 of the License, or
#(at your option) any later version.
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#GNU General Public License for more details.
#You should have received a copy of the GNU General Public License
#along with this program; if not, write to the Free Software
#Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
"""The TOC file handling module."""
import re
from audiotools import SheetException, parse_timestamp, build_timestamp
import gettext
gettext.install("audiotools", unicode=True)
###################
#TOC Parsing
###################
class TOCException(SheetException):
"""Raised by TOC file parsing errors."""
pass
def parse(lines):
"""Returns a TOCFile object from an iterator of lines.
Raises TOCException if some problem occurs parsing the file."""
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"))
lines = iter(lines)
toc = TOCFile()
track = None
track_number = 0
line_number = 0
try:
while (True):
line_number += 1
line = lines.next().strip()
if (len(line) == 0):
pass
elif (TRACKLINE.match(line)):
if (track is not None):
toc.tracks[track.number] = track
track_number += 1
track = Track(track_number)
else:
if (track is not None):
track.lines.append(line)
if (line.startswith('FILE') or
line.startswith('AUDIOFILE')):
if ('"' in line):
track.indexes = map(
parse_timestamp,
re.findall(r'\d+:\d+:\d+|\d+',
line[line.rindex('"') + 1:]))
else:
track.indexes = map(
parse_timestamp,
re.findall(r'\d+:\d+:\d+|\d+',
line))
elif (line.startswith('START')):
track.start = parse_timestamp(line[len('START '):])
else:
toc.lines.append(line)
except StopIteration:
if (track is not None):
toc.tracks[track.number] = track
return toc
class TOCFile:
"""An object representing a TOC file."""
def __init__(self):
self.lines = []
self.tracks = {}
def __repr__(self):
return "TOCFile(lines=%s,tracks=%s)" % (repr(self.lines),
repr(self.tracks))
def catalog(self):
"""Returns the cuesheet's CATALOG number as a plain string, or None.
If present, this value is typically a CD's UPC code."""
for line in self.lines:
if (line.startswith('CATALOG')):
result = re.search(r'"(.+)"', line)
if (result is not None):
return result.group(1)
else:
continue
else:
return None
def indexes(self):
"""Yields a set of index lists, one for each track in the file."""
for track in sorted(self.tracks.values()):
if (track.start != 0):
yield (track.indexes[0], track.indexes[0] + track.start)
else:
yield (track.indexes[0],)
def pcm_lengths(self, total_length):
"""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."""
previous = None
for current in self.indexes():
if (previous is None):
previous = current
else:
track_length = (max(current) - max(previous)) * (44100 / 75)
total_length -= track_length
yield track_length
previous = current
yield total_length
def ISRCs(self):
"""Returns a track_number->ISRC dict of all non-empty tracks."""
return dict([(track.number, track.ISRC()) for track in
self.tracks.values() if track.ISRC() is not None])
@classmethod
def file(cls, sheet, filename):
"""Constructs a new TOC file string from a compatible object.
sheet must have catalog(), indexes() and ISRCs() methods.
filename is a string to the filename the TOC file is created for.
Although we don't care whether the filename points to a real file,
other tools sometimes do.
"""
import cStringIO
catalog = sheet.catalog() # a catalog string, or None
indexes = list(sheet.indexes()) # a list of index tuples
ISRCs = sheet.ISRCs() # a track_number->ISRC dict
data = cStringIO.StringIO()
data.write("CD_DA\n\n")
if ((catalog is not None) and (len(catalog) > 0)):
data.write("CATALOG \"%s\"\n\n" % (catalog))
for (i, (current, next)) in enumerate(zip(indexes,
indexes[1:] + [None])):
tracknum = i + 1
data.write("TRACK AUDIO\n")
if (tracknum in ISRCs.keys()):
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])))
else:
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])))
if (next is not None):
data.write("\n")
return data.getvalue()
class Track:
"""A track inside a TOCFile object."""
def __init__(self, number):
self.number = number
self.lines = []
self.indexes = []
self.start = 0
def __cmp__(self, t):
return cmp(self.number, t.number)
def __repr__(self):
return "Track(%s,lines=%s,indexes=%s,start=%s)" % \
(repr(self.number), repr(self.lines),
repr(self.indexes), repr(self.start))
def ISRC(self):
"""Returns the track's ISRC value, or None."""
for line in self.lines:
if (line.startswith('ISRC')):
match = re.search(r'"(.+)"', line)
if (match is not None):
return match.group(1)
else:
return None
def read_tocfile(filename):
"""Returns a TOCFile from a TOC filename on disk.
Raises TOCException if some error occurs reading or parsing the file.
"""
try:
f = open(filename, 'r')
except IOError, msg:
raise TOCException(str(msg))
try:
return parse(iter(f.readlines()))
finally:
f.close()