diff --git a/ChangeLog b/ChangeLog index fb74e38..c00d809 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,16 @@ +2009-10-17 Thomas Vander Stichele + + * morituri/image/image.py: + Add ImageEncodeTask to encode a disk image to a different profile + and directory. + * morituri/common/encode.py: + Add lossy encoding profiles for mp3 and vorbis. + Rename muxer to tagger since that's what we use it for. + Do progress probe after level to make sure we get samples for + offsets. + * morituri/rip/image.py: + Add rip image encode command. + 2009-10-17 Thomas Vander Stichele * morituri/test/José González.toc (added): diff --git a/morituri/common/encode.py b/morituri/common/encode.py index f5ddc17..f380325 100644 --- a/morituri/common/encode.py +++ b/morituri/common/encode.py @@ -33,6 +33,7 @@ class Profile(object): name = None extension = None pipeline = None + losless = None def test(self): """ @@ -44,7 +45,8 @@ class Profile(object): class FlacProfile(Profile): name = 'flac' extension = 'flac' - pipeline = 'flacenc name=muxer quality=8' + pipeline = 'flacenc name=tagger quality=8' + lossless = True # FIXME: we should do something better than just printing ERRORS def test(self): @@ -65,26 +67,49 @@ class FlacProfile(Profile): class AlacProfile(Profile): name = 'alac' extension = 'alac' - pipeline = 'ffenc_alac name=muxer' + pipeline = 'ffenc_alac name=tagger' + lossless = True class WavProfile(Profile): name = 'wav' extension = 'wav' - pipeline = 'wavenc name=muxer' + pipeline = 'wavenc name=tagger' + lossless = True class WavpackProfile(Profile): name = 'wavpack' extension = 'wv' - pipeline = 'wavpackenc bitrate=0 name=muxer' + pipeline = 'wavpackenc bitrate=0 name=tagger' + lossless = True + +class MP3Profile(Profile): + name = 'mp3' + extension = 'mp3' + pipeline = 'lame name=tagger quality=0 ! id3v2mux' + lossless = False + +class VorbisProfile(Profile): + name = 'vorbis' + extension = 'oga' + pipeline = 'audioconvert ! vorbisenc name=tagger ! oggmux' + lossless = False PROFILES = { - 'wav': WavProfile, - 'flac': FlacProfile, - 'alac': AlacProfile, + 'wav': WavProfile, + 'flac': FlacProfile, + 'alac': AlacProfile, 'wavpack': WavpackProfile, } +LOSSY_PROFILES = { + 'mp3': MP3Profile, + 'vorbis': VorbisProfile, +} + +ALL_PROFILES = PROFILES.copy() +ALL_PROFILES.update(LOSSY_PROFILES) + class EncodeTask(task.Task): """ I am a task that encodes a .wav file. @@ -125,11 +150,11 @@ class EncodeTask(task.Task): filesink location="%s" name=sink''' % (self._inpath, self._profile.pipeline, self._outpath)) - muxer = self._pipeline.get_by_name('muxer') + tagger = self._pipeline.get_by_name('tagger') # set tags if self._taglist: - muxer.merge_tags(self._taglist, gst.TAG_MERGE_APPEND) + tagger.merge_tags(self._taglist, gst.TAG_MERGE_APPEND) self.debug('pausing pipeline') self._pipeline.set_state(gst.STATE_PAUSED) @@ -138,7 +163,7 @@ class EncodeTask(task.Task): # get length self.debug('query duration') - length, qformat = muxer.query_duration(gst.FORMAT_DEFAULT) + length, qformat = tagger.query_duration(gst.FORMAT_DEFAULT) # wavparse 0.10.14 returns in bytes if qformat == gst.FORMAT_BYTES: self.debug('query returned in BYTES format') @@ -146,11 +171,6 @@ class EncodeTask(task.Task): self.debug('total length: %r', length) self._length = length - # add a probe so we can track progress - sinkpad = muxer.get_pad('sink') - srcpad = sinkpad.get_peer() - srcpad.add_buffer_probe(self._probe_handler) - # add eos handling bus = self._pipeline.get_bus() bus.add_signal_watch() @@ -159,6 +179,10 @@ class EncodeTask(task.Task): # set up level callbacks bus.connect('message::element', self._message_element_cb) self._level = self._pipeline.get_by_name('level') + # add a probe so we can track progress + # we connect to level because this gives us offset in samples + srcpad = self._level.get_static_pad('src') + srcpad.add_buffer_probe(self._probe_handler) self.debug('scheduling setting to play') # since set_state returns non-False, adding it as timeout_add @@ -175,6 +199,8 @@ class EncodeTask(task.Task): self.debug('scheduled setting to play') def _probe_handler(self, pad, buffer): + # update progress based on buffer offset (expected to be in samples) + # versus length in samples # marshal to main thread self.runner.schedule(0, self.setProgress, float(buffer.offset) / self._length) diff --git a/morituri/image/image.py b/morituri/image/image.py index 199d585..0c29006 100644 --- a/morituri/image/image.py +++ b/morituri/image/image.py @@ -24,9 +24,11 @@ Wrap on-disk CD images based on the .cue file. """ +import os + import gst -from morituri.common import task, checksum, log, common +from morituri.common import task, checksum, log, common, encode from morituri.image import cue, table class Image(object, log.Loggable): @@ -215,3 +217,45 @@ class ImageVerifyTask(task.MultiSeparateTask): self.lengths[trackIndex] = end - index.relative task.MultiSeparateTask.stop(self) + +class ImageEncodeTask(task.MultiSeparateTask): + """ + I encode a disk image to a different format. + """ + + description = "Encoding tracks" + + def __init__(self, image, profile, outdir): + task.MultiSeparateTask.__init__(self) + + self._image = image + self._profile = profile + cue = image.cue + self._tasks = [] + self.lengths = {} + + def add(index): + path = image.getRealPath(index.path) + assert type(path) is unicode, "%r is not unicode" % path + self.debug('schedule encode of %r', path) + root, ext = os.path.splitext(os.path.basename(path)) + outpath = os.path.join(outdir, root + '.' + profile.extension) + self.debug('schedule encode to %r', outpath) + taskk = encode.EncodeTask(path, os.path.join(outdir, + root + '.' + profile.extension), profile) + self.addTask(taskk) + + try: + htoa = cue.table.tracks[0].indexes[0] + self.debug('encoding htoa track') + add(htoa) + except IndexError: + self.debug('no htoa track') + pass + + for trackIndex, track in enumerate(cue.table.tracks): + self.debug('encoding track %d', trackIndex + 1) + index = track.indexes[1] + add(index) + + diff --git a/morituri/rip/image.py b/morituri/rip/image.py index 39ac17e..d8c0f43 100644 --- a/morituri/rip/image.py +++ b/morituri/rip/image.py @@ -20,12 +20,76 @@ # You should have received a copy of the GNU General Public License # along with morituri. If not, see . +import os + from morituri.common import logcommand, task, checksum, accurip, program +from morituri.common import encode from morituri.image import image, cue from morituri.result import result from morituri.program import cdrdao, cdparanoia +class Encode(logcommand.LogCommand): + summary = "encode image" + + def addOptions(self): + # FIXME: get from config + self.parser.add_option('-O', '--output-directory', + action="store", dest="output_directory", + help="output directory (defaults to current directory)") + + default = 'vorbis' + self.parser.add_option('', '--profile', + action="store", dest="profile", + help="profile for encoding (default '%s', choices '%s')" % ( + default, "', '".join(encode.ALL_PROFILES.keys())), + default=default) + + + def do(self, args): + prog = program.Program() + prog.outdir = (self.options.output_directory or os.getcwd()) + prog.outdir = prog.outdir.decode('utf-8') + profile = encode.ALL_PROFILES[self.options.profile]() + + runner = task.SyncRunner() + + for arg in args: + arg = unicode(arg) + indir = os.path.dirname(arg) + cueImage = image.Image(arg) + cueImage.setup(runner) + # FIXME: find a decent way to get an album-specific outdir + root, ext = os.path.splitext(os.path.basename(indir)) + outdir = os.path.join(prog.outdir, root) + try: + os.makedirs(outdir) + except: + # FIXME: handle other exceptions than OSError Errno 17 + pass + # FIXME: handle this nicer + assert outdir != indir + + taskk = image.ImageEncodeTask(cueImage, profile, outdir) + runner.run(taskk) + + # FIXME: translate .m3u file if it exists + root, ext = os.path.splitext(arg) + m3upath = root + '.m3u' + if os.path.exists(m3upath): + self.debug('translating .m3u file') + inm3u = open(m3upath) + outm3u = open(os.path.join(outdir, os.path.basename(m3upath)), + 'w') + for line in inm3u.readlines(): + root, ext = os.path.splitext(line) + if ext: + # newline is swallowed by splitext here + outm3u.write('%s.%s\n' % (root, profile.extension)) + else: + outm3u.write('%s' % root) + outm3u.close() + class Verify(logcommand.LogCommand): summary = "verify image" @@ -35,6 +99,7 @@ class Verify(logcommand.LogCommand): cache = accurip.AccuCache() for arg in args: + arg = unicode(arg) cueImage = image.Image(arg) cueImage.setup(runner) @@ -56,4 +121,4 @@ class Verify(logcommand.LogCommand): class Image(logcommand.LogCommand): summary = "handle images" - subCommandClasses = [Verify, ] + subCommandClasses = [Encode, Verify, ]