diff --git a/ChangeLog b/ChangeLog index 4f924fb..e9010ee 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,14 @@ +2009-06-01 Thomas Vander Stichele + + * morituri/common/encode.py: + * morituri/program/cdparanoia.py: + Add encoding profiles, kept simple for now as a class and + subclasses. Use them to encode. Calculate peak level while + encoding, compared to EAC and replaygain's value. + * morituri/rip/cd.py: + Use the encoding profiles, ripping with the right extension. + Add a --profile parameter for it. + 2009-05-31 Thomas Vander Stichele * morituri/rip/cd.py: diff --git a/morituri/common/encode.py b/morituri/common/encode.py index d8e0723..d2b6164 100644 --- a/morituri/common/encode.py +++ b/morituri/common/encode.py @@ -21,6 +21,7 @@ # along with morituri. If not, see . import os +import math import struct import zlib @@ -31,33 +32,79 @@ from morituri.common import common, task from morituri.common import log log.init() +class Profile(object): + name = None + extension = None + pipeline = None + +class FlacProfile(Profile): + name = 'flac' + extension = 'flac' + pipeline = 'flacenc name=muxer quality=8' + +class AlacProfile(Profile): + name = 'alac' + extension = 'alac' + pipeline = 'ffenc_alac name=muxer' + +class WavProfile(Profile): + name = 'wav' + extension = 'wav' + pipeline = 'wavenc name=muxer' + +class WavpackProfile(Profile): + name = 'wavpack' + extension = 'wv' + pipeline = 'wavpackenc bitrate=0 name=muxer' + + +PROFILES = { + 'wav': WavProfile, + 'flac': FlacProfile, + 'alac': AlacProfile, + 'wavpack': WavpackProfile, +} + class EncodeTask(task.Task): """ I am a task that encodes a .wav file. I set tags too. + I also calculate the peak level of the track. + + @param peak: the peak power, from 0.0 to 1.0. To get the peak volume, + square root this value. + @type peak: float """ description = 'Encoding' + peak = None - def __init__(self, inpath, outpath, taglist=None): + def __init__(self, inpath, outpath, profile, taglist=None): """ """ self._inpath = inpath self._outpath = outpath self._taglist = taglist + self._level = None + self._peakdB = None + self._profile = PROFILES[profile] + def start(self, runner): task.Task.start(self, runner) self._pipeline = gst.parse_launch(''' filesrc location="%s" ! - decodebin name=decoder ! audio/x-raw-int ! - flacenc name=muxer ! - filesink location="%s" name=sink''' % (self._inpath, self._outpath)) + decodebin name=decoder ! + audio/x-raw-int,width=16,depth=16,channels=2 ! + level name=level ! + %s ! + filesink location="%s" name=sink''' % (self._inpath, + self._profile.pipeline, self._outpath)) + muxer = self._pipeline.get_by_name('muxer') # set tags if self._taglist: - muxer = self._pipeline.get_by_name('muxer') muxer.merge_tags(self._taglist, gst.TAG_MERGE_APPEND) self.debug('pausing pipeline') @@ -79,12 +126,16 @@ class EncodeTask(task.Task): # add a probe so we can track progress sinkpad = muxer.get_pad('sink') srcpad = sinkpad.get_peer() - srcpad.add_buffer_probe(self._probe_handler, False) + srcpad.add_buffer_probe(self._probe_handler) # add eos handling bus = self._pipeline.get_bus() bus.add_signal_watch() - bus.connect('message::eos', self._eos_cb) + bus.connect('message::eos', self._message_eos_cb) + + # set up level callbacks + bus.connect('message::element', self._message_element_cb) + self._level = self._pipeline.get_by_name('level') self.debug('scheduling setting to play') # since set_state returns non-False, adding it as timeout_add @@ -100,19 +151,40 @@ class EncodeTask(task.Task): #self._pipeline.set_state(gst.STATE_PLAYING) self.debug('scheduled setting to play') - def _probe_handler(self, pad, buffer, ret): + def _probe_handler(self, pad, buffer): # marshal to main thread self.runner.schedule(0, self.setProgress, float(buffer.offset) / self._length) + + # don't drop the buffer return True - def _eos_cb(self, bus, message): + def _message_eos_cb(self, bus, message): self.debug('eos, scheduling stop') self.runner.schedule(0, self.stop) + def _message_element_cb(self, bus, message): + if message.src != self._level: + return + + s = message.structure + if s.get_name() != 'level': + return + + + if self._peakdB is None: + self._peakdB = s['peak'][0] + + for p in s['peak']: + if self._peakdB < p: + self._peakdB = p + def stop(self): self.debug('stopping') self.debug('setting state to NULL') self._pipeline.set_state(gst.STATE_NULL) self.debug('set state to NULL') task.Task.stop(self) + + + self.peak = math.pow(10, self._peakdB / 10.0) diff --git a/morituri/program/cdparanoia.py b/morituri/program/cdparanoia.py index 6221c8e..479a52e 100644 --- a/morituri/program/cdparanoia.py +++ b/morituri/program/cdparanoia.py @@ -27,7 +27,7 @@ import shutil import subprocess import tempfile -from morituri.common import task, log, common, checksum +from morituri.common import task, log, common, checksum, encode from morituri.extern import asyncsub class FileSizeError(Exception): @@ -228,22 +228,25 @@ class ReadVerifyTrackTask(task.MultiSeparateTask): @ivar checksum: the checksum of the track; set if they match. @ivar testchecksum: the test checksum of the track. @ivar copychecksum: the copy checksum of the track. + @ivar peak: the peak level of the track """ - def __init__(self, path, table, start, stop, offset=0, device=None): + def __init__(self, path, table, start, stop, offset=0, device=None, profile=None): """ - @param path: where to store the ripped track - @type path: str - @param table: table of contents of CD - @type table: L{table.Table} - @param start: first frame to rip - @type start: int - @param stop: last frame to rip (inclusive) - @type stop: int - @param offset: read offset, in samples - @type offset: int - @param device: the device to rip from - @type device: str + @param path: where to store the ripped track + @type path: str + @param table: table of contents of CD + @type table: L{table.Table} + @param start: first frame to rip + @type start: int + @param stop: last frame to rip (inclusive) + @type stop: int + @param offset: read offset, in samples + @type offset: int + @param device: the device to rip from + @type device: str + @param profile: the encoding profile + @type profile: str """ task.MultiSeparateTask.__init__(self) @@ -263,10 +266,20 @@ class ReadVerifyTrackTask(task.MultiSeparateTask): self.tasks.append(t) self.tasks.append(checksum.CRC32Task(tmppath)) + # FIXME: clean this up + fd, tmpoutpath = tempfile.mkstemp(suffix='.morituri.flac') + os.close(fd) + self._tmppath = tmpoutpath + self.tasks.append(encode.EncodeTask(tmppath, tmpoutpath, profile)) + # make sure our encoding is accurate + self.tasks.append(checksum.CRC32Task(tmpoutpath)) + self.checksum = None def stop(self): if not self.exception: + self.peak = self.tasks[4].peak + self.testchecksum = c1 = self.tasks[1].checksum self.copychecksum = c2 = self.tasks[3].checksum if c1 == c2: @@ -275,6 +288,8 @@ class ReadVerifyTrackTask(task.MultiSeparateTask): else: self.error('read and verify failed') + if self.tasks[5].checksum != self.checksum: + self.error('Encoding failed, checksum does not match') try: shutil.move(self._tmppath, self.path) self.checksum = checksum diff --git a/morituri/rip/cd.py b/morituri/rip/cd.py index f6ab023..80d78ce 100644 --- a/morituri/rip/cd.py +++ b/morituri/rip/cd.py @@ -22,11 +22,13 @@ import os import sys +import math import gobject gobject.threads_init() -from morituri.common import logcommand, task, checksum, common, accurip, drive +from morituri.common import logcommand, task, checksum, common, accurip +from morituri.common import drive, encode from morituri.image import image, cue, table from morituri.program import cdrdao, cdparanoia @@ -199,6 +201,12 @@ class Rip(logcommand.LogCommand): action="store", dest="disc_template", help="template for disc file naming (default %s)" % default, default=default) + default = 'flac' + self.parser.add_option('', '--profile', + action="store", dest="profile", + help="profile for encoding (default '%s', choices '%s')" % ( + default, "', '".join(encode.PROFILES.keys())), + default=default) def do(self, args): @@ -248,6 +256,8 @@ class Rip(logcommand.LogCommand): itable.getAccurateRipURL(), ittoc.getAccurateRipURL()) outdir = self.options.output_directory or os.getcwd() + profile = encode.PROFILES[self.options.profile] + extension = profile.extension # check for hidden track one audio htoapath = None @@ -264,7 +274,7 @@ class Rip(logcommand.LogCommand): print 'Found Hidden Track One Audio from frame %d to %d' % (start, stop) # rip it - htoapath = getPath(outdir, self.options.track_template, metadata, 0) + '.wav' + htoapath = getPath(outdir, self.options.track_template, metadata, 0) + '.' + extension dirname = os.path.dirname(htoapath) if not os.path.exists(dirname): os.makedirs(dirname) @@ -275,12 +285,17 @@ class Rip(logcommand.LogCommand): t = cdparanoia.ReadVerifyTrackTask(htoapath, ittoc, start, stop - 1, offset=int(self.options.offset), - device=self.parentCommand.options.device) + device=self.parentCommand.options.device, + profile=self.options.profile) function(runner, t) + if t.checksum is not None: print 'Checksums match for track %d' % 0 else: print 'ERROR: checksums did not match for track %d' % 0 + print 'Peak level: %.2f %%' % (math.sqrt(t.peak) * 100.0, ) + if t.peak == 0.0: + print 'HTOA is completely silent' # overlay this rip onto the Table itable.setFile(1, 0, htoapath, htoalength, 0) @@ -292,7 +307,7 @@ class Rip(logcommand.LogCommand): track.indexes[1].relative = 0 continue - path = getPath(outdir, self.options.track_template, metadata, i + 1) + '.wav' + path = getPath(outdir, self.options.track_template, metadata, i + 1) + '.' + extension dirname = os.path.dirname(path) if not os.path.exists(dirname): os.makedirs(dirname) @@ -304,13 +319,15 @@ class Rip(logcommand.LogCommand): ittoc.getTrackStart(i + 1), ittoc.getTrackEnd(i + 1), offset=int(self.options.offset), - device=self.parentCommand.options.device) + device=self.parentCommand.options.device, + profile=self.options.profile) t.description = 'Reading Track %d' % (i + 1) function(runner, t) if t.checksum: print 'Checksums match for track %d' % (i + 1) else: print 'ERROR: checksums did not match for track %d' % (i + 1) + print 'Peak level: %.2f %%' % (math.sqrt(t.peak) * 100.0, ) # overlay this rip onto the Table itable.setFile(i + 1, 1, path, ittoc.getTrackLength(i + 1), i + 1) @@ -340,7 +357,7 @@ class Rip(logcommand.LogCommand): handle.write('%s\n' % os.path.basename(htoapath)) for i, track in enumerate(itable.tracks): - path = getPath(outdir, self.options.track_template, metadata, i) + '.wav' + path = getPath(outdir, self.options.track_template, metadata, i) + '.' + extension handle.write('#EXTINF:%d,%s\n' % ( itable.getTrackLength(i + 1) / common.FRAMES_PER_SECOND, os.path.basename(path)))