#!/usr/bin/env python """ Usage: tocsplitter.py tocfile prefix (N|N-(M)|dry) Individual WAV files will be generated for each track described in tocfile. tocfile should be a file in CD TOC format. Each file will be named prefix_NN.wav where NN is the track number. You can include a directory path as part of the prefix. The optional last argument controls the range of tracks: a single integer means extract only that track, N-M means extract tracks N through M inclusive, N- means all tracks starting with N, and any non-numeric value means do a dry run (do not write anything). Default is to extract all. """ import sys import re import os, os.path ## XXX for future work w/ querying cddb database, # see /usr/share/doc/cddb-py* #import CDDB #import DiscID # for handling audio files import pyeca, ecacontrol titleregex = re.compile(r'\bTITLE\s+"(.*?)"') fileregex = re.compile(r'^FILE\s+"(.+?)"\s+(\S+)\s+(\S+)', re.MULTILINE) def frames_to_time(n): secs = int(n) / 44100.0 mm, secs = divmod(secs, 60) ss, r = divmod(secs, 1) ff = r * 75 return '%02d:%02d:%02d' % (int(mm), int(ss), int(ff)) def time_to_frames(s): mm, ss, ff = s.split(':') secs = int(ss) + int(mm) * 60 #blocks = int(ff) + secs * 75 secs += int(ff) / 75.0 frames = secs * 44100 return int(frames) #return blocks def parse_toc(fileob): """ read the file object as a cd .TOC file and return a list of dicts representing them.""" text = fileob.read() rawtracks = text.split('// Track ') rawtracks = [r for r in rawtracks if r.count('TRACK AUDIO')] items = [] for r in rawtracks: found_title = titleregex.search(r) found_file = fileregex.search(r) if not found_file: #and found_file: continue filename = found_file.group(1) title = found_title and found_title.group(1) or '' # get the times set up. # They might be in samples, or mm:ss:ff. start = found_file.group(2) length = found_file.group(3) if start.count(':'): start = time_to_frames(start) else: start = int(start) if length.count(':'): length = time_to_frames(length) else: length = int(length) d = {'title': title, 'file': filename, 'start': start, 'length': length} items.append(d) return items def _parse_trackrange(trackrange=None): mintrack = 0 maxtrack = sys.maxint dry = 0 if trackrange is not None: try: trackrange = trackrange.split('-', 1) mintrack=int(trackrange[0]) -1 # count from zero maxtrack=int(trackrange[-1] or maxtrack) dry = 0 except ValueError: # arg not a valid range. dry = 1 return mintrack, maxtrack, dry class EcaError(Exception): pass class EcaEditor: def __init__(self): self.eci = ecacontrol.ECA_CONTROL_INTERFACE() # or ecacontrol.ECA_... self._debug_out = open('/tmp/eci_out.txt', 'w') def debug_dump(self, msg): """ log a message """ self._debug_out.write(msg + '\n') self._debug_out.flush() def cmd(self, msg): """ do one ecasound command """ eci = self.eci self.debug_dump(msg) output = eci.command(msg) output = output or eci.last_string() error = eci.last_error() if output: self.debug_dump(output) if error: self.debug_dump(error) raise EcaError, error return output def process_info(parsed, prefix, trackrange=None): print "found %d tracks" % len(parsed) # which tracks to extract, and whether to write any. mintrack, maxtrack, dry = _parse_trackrange(trackrange) if dry: print "Doing a dry run, no output." parsed = parsed[mintrack:maxtrack] # do setup. e = EcaEditor() if not dry: pass # do the tracks. count = mintrack for info in parsed: count += 1 new_id = '%s_%02d.wav' % (prefix, count) desired_in = info['file'] debug_info = (new_id, frames_to_time(info['start']), frames_to_time(info['length'])) print "Writing %s, starting at %s, length %s" % debug_info if dry: print "Dry run, no output." continue # Do the ecasound commands. Note that the order is # very important here. # XXX memory usage goes up as we add output files?? # set up the audio input e.cmd("cs-add splitter_chainsetup") #XXX e.cmd("c-add main_chain") e.cmd("ai-add %s" % desired_in) # set up the audio output e.cmd("ao-add %s" % new_id) # set the duration... must not be connected. e.cmd("cs-set-length-samples %d" % info['length']) # fast-forward... must not be connected. e.cmd("ai-select %s" % desired_in) e.cmd("ai-set-position-samples %d" % info['start']) # we have input and output and positions, ok to connect and run. e.cmd("cs-connect") _dump("running...") #_dump(cmd("dump-cs-status")) e.cmd("run") # Done processing. # we're going to re-use the cs, but it remembers its old position # which is no longer relevant. this must be done while connected. # XXX doesn't work. Use the workaround of deleting the chain instead. #e.cmd("cs-get-position-samples") #e.cmd("cs-set-position-samples 0") #e.cmd("cs-get-position-samples") # clean up. e.cmd("cs-disconnect") e.cmd("ai-select %s" % desired_in) e.cmd("ai-remove") e.cmd("ao-select %s" % new_id) e.cmd("ao-remove") e.cmd("c-select main_chain") e.cmd("c-remove") # # NOTE: must remove the chain after each run, # # or else the output length for all but first file are wrong. # # Bug or feature? # clean up e.cmd("cs-select splitter_chainsetup") e.cmd("cs-remove") #_dump(cmd("dump-cs-status")) def _dump(msg): if 1: print msg def usage(): print __doc__ sys.exit(1) if __name__ == '__main__': try: path = sys.argv[1] prefix = sys.argv[2] except IndexError: usage() try: sys.argv[3] trackrange = sys.argv[3] except IndexError: trackrange = None path = os.path.normpath(path) path = os.path.realpath(path) toc = open(path) parsed = parse_toc(toc) # need to set the cwd to the parent directory of the tocfile. d = os.path.dirname(path) try: os.chdir(d) except: print 'XXX\n', d, '\n', path raise process_info(parsed, prefix, trackrange)