diff --git a/README.md b/README.md index 98fb4de3..fddac3ec 100644 --- a/README.md +++ b/README.md @@ -69,10 +69,6 @@ Whipper relies on the following packages in order to run correctly and provide a - [cdparanoia](https://www.xiph.org/paranoia/), for the actual ripping - [cdrdao](http://cdrdao.sourceforge.net/), for session, TOC, pre-gap, and ISRC extraction -- [GStreamer](https://gstreamer.freedesktop.org/) and its python bindings, for encoding (it's going to be removed soon™) - - `gstreamer0.10-base-plugins` (or `gstreamer0.10-plugins-base` depending on Linux distro) >= **0.10.22** for appsink - - `gstreamer0.10-good-plugins` (or `gstreamer0.10-plugins-good`) for wav encoding (it depends on the Linux distro used) - - `python-gst0.10` (GStreamer Python bindings) - [python-musicbrainzngs](https://github.com/alastair/python-musicbrainzngs), for metadata lookup - [python-setuptools](https://pypi.python.org/pypi/setuptools), for installation, plugins support - [python-cddb](http://cddb-py.sourceforge.net/), for showing but not using metadata if disc not available in the MusicBrainz DB diff --git a/morituri/command/cd.py b/morituri/command/cd.py index dbfcced3..38e03e05 100644 --- a/morituri/command/cd.py +++ b/morituri/command/cd.py @@ -32,7 +32,7 @@ from morituri.command.basecommand import BaseCommand from morituri.common import ( - accurip, common, config, drive, gstreamer, program, task + accurip, common, config, drive, program, task ) from morituri.program import cdrdao, cdparanoia, utils from morituri.result import result @@ -317,17 +317,6 @@ def handle_arguments(self): def doCommand(self): - # here to avoid import gst eating our options - from morituri.common import encode - profile = encode.PROFILES['flac']() - self.program.result.profileName = profile.name - self.program.result.profilePipeline = profile.pipeline - elementFactory = profile.pipeline.split(' ')[0] - self.program.result.gstreamerVersion = gstreamer.gstreamerVersion() - self.program.result.gstPythonVersion = gstreamer.gstPythonVersion() - self.program.result.encoderVersion = gstreamer.elementFactoryVersion( - elementFactory) - self.program.setWorkingDirectory(self.options.working_directory) self.program.outdir = self.options.output_directory.decode('utf-8') self.program.result.offset = int(self.options.offset) @@ -339,7 +328,7 @@ def doCommand(self): while True: discName = self.program.getPath(self.program.outdir, self.options.disc_template, self.mbdiscid, 0, - profile=profile, disambiguate=disambiguate) + disambiguate=disambiguate) dirname = os.path.dirname(discName) if os.path.exists(dirname): sys.stdout.write("Output directory %s already exists\n" % @@ -382,8 +371,8 @@ def ripIfNotRipped(number): path = self.program.getPath(self.program.outdir, self.options.track_template, self.mbdiscid, number, - profile=profile, disambiguate=disambiguate) \ - + '.' + profile.extension + disambiguate=disambiguate) \ + + '.' + 'flac' logger.debug('ripIfNotRipped: path %r' % path) trackResult.number = number @@ -429,7 +418,6 @@ def ripIfNotRipped(number): self.program.ripTrack(self.runner, trackResult, offset=int(self.options.offset), device=self.device, - profile=profile, taglist=self.program.getTagList(number), overread=self.options.overread, what='track %d of %d%s' % ( @@ -509,7 +497,7 @@ def ripIfNotRipped(number): ### write disc files discName = self.program.getPath(self.program.outdir, self.options.disc_template, self.mbdiscid, 0, - profile=profile, disambiguate=disambiguate) + disambiguate=disambiguate) dirname = os.path.dirname(discName) if not os.path.exists(dirname): os.makedirs(dirname) @@ -521,7 +509,8 @@ def ripIfNotRipped(number): logger.debug('writing m3u file for %r', discName) m3uPath = u'%s.m3u' % discName handle = open(m3uPath, 'w') - handle.write(u'#EXTM3U\n') + u = u'#EXTM3U\n' + handle.write(u.encode('utf-8')) def writeFile(handle, path, length): targetPath = common.getRelativePath(path, m3uPath) @@ -541,8 +530,7 @@ def writeFile(handle, path, length): path = self.program.getPath(self.program.outdir, self.options.track_template, self.mbdiscid, i + 1, - profile=profile, - disambiguate=disambiguate) + '.' + profile.extension + disambiguate=disambiguate) + '.' + 'flac' writeFile(handle, path, self.itable.getTrackLength(i + 1) / common.FRAMES_PER_SECOND) diff --git a/morituri/command/debug.py b/morituri/command/debug.py index 8271486c..c6310db3 100644 --- a/morituri/command/debug.py +++ b/morituri/command/debug.py @@ -153,14 +153,6 @@ def add_arguments(self): # here to avoid import gst eating our options from morituri.common import encode - default = 'flac' - # slated for deletion as flac will be the only encoder - self.parser.add_argument('--profile', - action="store", - dest="profile", - help="profile for encoding (default '%s', choices '%s')" % ( - default, "', '".join(encode.ALL_PROFILES.keys())), - default=default) self.parser.add_argument('input', action='store', help="audio file to encode") self.parser.add_argument('output', nargs='?', action='store', @@ -168,7 +160,6 @@ def add_arguments(self): def do(self): from morituri.common import encode - profile = encode.ALL_PROFILES[self.options.profile]() try: fromPath = unicode(self.options.input) @@ -180,7 +171,7 @@ def do(self): try: toPath = unicode(self.options.output) except IndexError: - toPath = fromPath + '.' + profile.extension + toPath = fromPath + '.flac' runner = task.SyncRunner() @@ -191,33 +182,14 @@ def do(self): runner.run(encodetask) - sys.stdout.write('Peak level: %r\n' % encodetask.peak) - sys.stdout.write('Encoded to %s\n' % toPath.encode('utf-8')) - - -class MaxSample(BaseCommand): - summary = "run a max sample task" - description = summary - - def add_arguments(self): - self.parser.add_argument('files', nargs='+', action='store', - help="audio files to sample") - - def do(self): - runner = task.SyncRunner() - # here to avoid import gst eating our options - from morituri.common import checksum - - for arg in self.options.files: - fromPath = unicode(arg.decode('utf-8')) + # I think we want this to be + # fromPath, not toPath, since the sox peak task, afaik, works on wave + # files + peaktask = encode.SoxPeakTask(fromPath) + runner.run(peaktask) - checksumtask = checksum.MaxSampleTask(fromPath) - - runner.run(checksumtask) - - sys.stdout.write('%s\n' % arg) - sys.stdout.write('Biggest absolute sample: %04x\n' % - checksumtask.checksum) + sys.stdout.write('Peak level: %r\n' % peaktask.peak) + sys.stdout.write('Encoded to %s\n' % toPath.encode('utf-8')) class Tag(BaseCommand): @@ -325,7 +297,6 @@ class Debug(BaseCommand): subcommands = { 'checksum': Checksum, 'encode': Encode, - 'maxsample': MaxSample, 'tag': Tag, 'musicbrainzngs': MusicBrainzNGS, 'resultcache': ResultCache, diff --git a/morituri/command/image.py b/morituri/command/image.py index 52a22d3b..606ed813 100644 --- a/morituri/command/image.py +++ b/morituri/command/image.py @@ -25,6 +25,7 @@ from morituri.command.basecommand import BaseCommand from morituri.common import accurip, config, program +from morituri.common import encode from morituri.extern.task import task from morituri.image import image from morituri.result import result @@ -59,8 +60,6 @@ def add_arguments(self): ) def do(self): - # here to avoid import gst eating our options - from morituri.common import encode prog = program.Program(config.Config(), stdout=sys.stdout) runner = task.SyncRunner() diff --git a/morituri/command/offset.py b/morituri/command/offset.py index b8a86b2b..065f2ea0 100644 --- a/morituri/command/offset.py +++ b/morituri/command/offset.py @@ -32,6 +32,7 @@ from morituri.common import accurip, common, config, drive, program from morituri.common import task as ctask from morituri.program import cdrdao, cdparanoia, utils +from morituri.common import checksum from morituri.extern.task import task @@ -80,7 +81,6 @@ def handle_arguments(self): logger.debug('Trying with offsets %r', self._offsets) def do(self): - prog = program.Program(config.Config()) runner = ctask.SyncRunner() device = self.options.device @@ -209,8 +209,6 @@ def _arcs(self, runner, table, track, offset): track, offset) runner.run(t) - # here to avoid import gst eating our options - from morituri.common import checksum # TODO MW: Update this to also use the v2 checksum(s) t = checksum.FastAccurateRipChecksumTask(path, trackNumber=track, diff --git a/morituri/common/checksum.py b/morituri/common/checksum.py index 0e89dabc..9ed12a80 100644 --- a/morituri/common/checksum.py +++ b/morituri/common/checksum.py @@ -20,18 +20,10 @@ # You should have received a copy of the GNU General Public License # along with morituri. If not, see . -import os -import struct -import zlib import binascii import wave -import gst -from morituri.common import common, task -from morituri.common import gstreamer as cgstreamer - -from morituri.extern.task import gstreamer from morituri.extern.task import task as etask from morituri.program.arc import accuraterip_checksum @@ -42,238 +34,6 @@ # checksums are not CRC's. a CRC is a specific type of checksum. -class ChecksumTask(gstreamer.GstPipelineTask): - """ - I am a task that calculates a checksum of the decoded audio data. - - @ivar checksum: the resulting checksum - """ - - logCategory = 'ChecksumTask' - - # this object needs a main loop to stop - description = 'Calculating checksum' - - def __init__(self, path, sampleStart=0, sampleLength=-1): - """ - A sample is considered a set of samples for each channel; - ie 16 bit stereo is 4 bytes per sample. - If sampleLength < 0 it is treated as 'unknown' and calculated. - - @type path: unicode - @type sampleStart: int - @param sampleStart: the sample to start at - """ - - # sampleLength can be e.g. -588 when it is -1 * SAMPLES_PER_FRAME - - assert type(path) is unicode, "%r is not unicode" % path - - self.logName = "ChecksumTask 0x%x" % id(self) - - # use repr/%r because path can be unicode - if sampleLength < 0: - logger.debug( - 'Creating checksum task on %r from sample %d until the end', - path, sampleStart) - else: - logger.debug( - 'Creating checksum task on %r from sample %d for %d samples', - path, sampleStart, sampleLength) - - if not os.path.exists(path): - raise IndexError('%r does not exist' % path) - - self._path = path - self._sampleStart = sampleStart - self._sampleLength = sampleLength - self._sampleEnd = None - self._checksum = 0 - self._bytes = 0 # number of bytes received - self._first = None - self._last = None - self._adapter = gst.Adapter() - - self.checksum = None # result - - cgstreamer.removeAudioParsers() - - ### gstreamer.GstPipelineTask implementations - - def getPipelineDesc(self): - return ''' - filesrc location="%s" ! - decodebin name=decode ! audio/x-raw-int ! - appsink name=sink sync=False emit-signals=True - ''' % gstreamer.quoteParse(self._path).encode('utf-8') - - def _getSampleLength(self): - # get length in samples of file - sink = self.pipeline.get_by_name('sink') - - logger.debug('query duration') - try: - length, qformat = sink.query_duration(gst.FORMAT_DEFAULT) - except gst.QueryError, e: - self.setException(e) - return None - - # wavparse 0.10.14 returns in bytes - if qformat == gst.FORMAT_BYTES: - logger.debug('query returned in BYTES format') - length /= 4 - logger.debug('total sample length of file: %r', length) - - return length - - - def paused(self): - sink = self.pipeline.get_by_name('sink') - - length = self._getSampleLength() - if length is None: - return - - if self._sampleLength < 0: - self._sampleLength = length - self._sampleStart - logger.debug('sampleLength is queried as %d samples', - self._sampleLength) - else: - logger.debug('sampleLength is known, and is %d samples' % - self._sampleLength) - - self._sampleEnd = self._sampleStart + self._sampleLength - 1 - logger.debug('sampleEnd is sample %d' % self._sampleEnd) - - logger.debug('event') - - - if self._sampleStart == 0 and self._sampleEnd + 1 == length: - logger.debug('No need to seek, crcing full file') - else: - # the segment end only is respected since -good 0.10.14.1 - event = gst.event_new_seek(1.0, gst.FORMAT_DEFAULT, - gst.SEEK_FLAG_FLUSH, - gst.SEEK_TYPE_SET, self._sampleStart, - gst.SEEK_TYPE_SET, self._sampleEnd + 1) # half-inclusive - logger.debug('CRCing %r from frame %d to frame %d (excluded)' % ( - self._path, - self._sampleStart / common.SAMPLES_PER_FRAME, - (self._sampleEnd + 1) / common.SAMPLES_PER_FRAME)) - # FIXME: sending it with sampleEnd set screws up the seek, we - # don't get # everything for flac; fixed in recent -good - result = sink.send_event(event) - logger.debug('event sent, result %r', result) - if not result: - msg = 'Failed to select samples with GStreamer seek event' - logger.critical(msg) - raise Exception(msg) - sink.connect('new-buffer', self._new_buffer_cb) - sink.connect('eos', self._eos_cb) - - logger.debug('scheduling setting to play') - # since set_state returns non-False, adding it as timeout_add - # will repeatedly call it, and block the main loop; so - # gobject.timeout_add(0L, self.pipeline.set_state, gst.STATE_PLAYING) - # would not work. - - def play(): - self.pipeline.set_state(gst.STATE_PLAYING) - return False - self.schedule(0, play) - - #self.pipeline.set_state(gst.STATE_PLAYING) - logger.debug('scheduled setting to play') - - def stopped(self): - logger.debug('stopped') - if not self._last: - # see http://bugzilla.gnome.org/show_bug.cgi?id=578612 - logger.debug( - 'not a single buffer gotten, setting exception EmptyError') - self.setException(common.EmptyError('not a single buffer gotten')) - return - else: - self._checksum = self._checksum % 2 ** 32 - logger.debug("last buffer's sample offset %r", self._last.offset) - logger.debug("last buffer's sample size %r", len(self._last) / 4) - last = self._last.offset + len(self._last) / 4 - 1 - logger.debug("last sample offset in buffer: %r", last) - logger.debug("requested sample end: %r", self._sampleEnd) - logger.debug("requested sample length: %r", self._sampleLength) - logger.debug("checksum: %08X", self._checksum) - logger.debug("bytes: %d", self._bytes) - if self._sampleEnd != last: - msg = 'did not get all samples, %d of %d missing' % ( - self._sampleEnd - last, self._sampleEnd) - logger.warning(msg) - self.setExceptionAndTraceback(common.MissingFrames(msg)) - return - - self.checksum = self._checksum - - ### subclass methods - - def do_checksum_buffer(self, buf, checksum): - """ - Subclasses should implement this. - - @param buf: a byte buffer containing two 16-bit samples per - channel. - @type buf: C{str} - @param checksum: the checksum so far, as returned by the - previous call. - @type checksum: C{int} - """ - raise NotImplementedError - - ### private methods - - def _new_buffer_cb(self, sink): - buf = sink.emit('pull-buffer') - gst.log('received new buffer at offset %r with length %r' % ( - buf.offset, buf.size)) - if self._first is None: - self._first = buf.offset - logger.debug('first sample is sample offset %r', self._first) - self._last = buf - - assert len(buf) % 4 == 0, "buffer is not a multiple of 4 bytes" - - # FIXME: gst-python 0.10.14.1 doesn't have adapter_peek/_take wrapped - # see http://bugzilla.gnome.org/show_bug.cgi?id=576505 - self._adapter.push(buf) - - while self._adapter.available() >= common.BYTES_PER_FRAME: - # FIXME: in 0.10.14.1, take_buffer leaks a ref - buf = self._adapter.take_buffer(common.BYTES_PER_FRAME) - - self._checksum = self.do_checksum_buffer(buf, self._checksum) - self._bytes += len(buf) - - # update progress - sample = self._first + self._bytes / 4 - samplesDone = sample - self._sampleStart - progress = float(samplesDone) / float((self._sampleLength)) - # marshal to the main thread - self.schedule(0, self.setProgress, progress) - - def _eos_cb(self, sink): - # get the last one; FIXME: why does this not get to us before ? - #self._new_buffer_cb(sink) - logger.debug('eos, scheduling stop') - self.schedule(0, self.stop) - -class CRC32TaskOld(ChecksumTask): - """ - I do a simple CRC32 check. - """ - - description = 'Calculating CRC' - - def do_checksum_buffer(self, buf, checksum): - return zlib.crc32(buf, checksum) - class CRC32Task(etask.Task): # TODO: Support sampleStart, sampleLength later on (should be trivial, just # add change the read part in _crc32 to skip some samples and/or not @@ -314,143 +74,3 @@ def _arc(self): self.checksum = arc self.stop() - - -class AccurateRipChecksumTask(ChecksumTask): - """ - I implement the AccurateRip checksum. - - See http://www.accuraterip.com/ - """ - - description = 'Calculating AccurateRip checksum' - - def __init__(self, path, trackNumber, trackCount, sampleStart=0, - sampleLength=-1): - ChecksumTask.__init__(self, path, sampleStart, sampleLength) - self._trackNumber = trackNumber - self._trackCount = trackCount - self._discFrameCounter = 0 # 1-based - - def __repr__(self): - return "" % ( - self._trackNumber, self._path) - - def do_checksum_buffer(self, buf, checksum): - self._discFrameCounter += 1 - - # on first track ... - if self._trackNumber == 1: - # ... skip first 4 CD frames - if self._discFrameCounter <= 4: - gst.debug('skipping frame %d' % self._discFrameCounter) - return checksum - # ... on 5th frame, only use last value - elif self._discFrameCounter == 5: - values = struct.unpack(" discFrameLength - 5: - logger.debug('skipping frame %d', self._discFrameCounter) - return checksum - - # self._bytes is updated after do_checksum_buffer - factor = self._bytes / 4 + 1 - values = struct.unpack("<%dI" % (len(buf) / 4), buf) - for value in values: - checksum += factor * value - factor += 1 - # offset = self._bytes / 4 + i + 1 - # if offset % common.SAMPLES_PER_FRAME == 0: - # print 'frame %d, ends before %d, last value %08x, CRC %08x' % ( - # offset / common.SAMPLES_PER_FRAME, offset, value, sum) - - checksum &= 0xFFFFFFFF - return checksum - - -class TRMTask(task.GstPipelineTask): - """ - I calculate a MusicBrainz TRM fingerprint. - - @ivar trm: the resulting trm - """ - - trm = None - description = 'Calculating fingerprint' - - def __init__(self, path): - if not os.path.exists(path): - raise IndexError('%s does not exist' % path) - - self.path = path - self._trm = None - self._bus = None - - def getPipelineDesc(self): - return ''' - filesrc location="%s" ! - decodebin ! audioconvert ! audio/x-raw-int ! - trm name=trm ! - appsink name=sink sync=False emit-signals=True''' % self.path - - def parsed(self): - sink = self.pipeline.get_by_name('sink') - sink.connect('new-buffer', self._new_buffer_cb) - - def paused(self): - gst.debug('query duration') - - self._length, qformat = self.pipeline.query_duration(gst.FORMAT_TIME) - gst.debug('total length: %r' % self._length) - gst.debug('scheduling setting to play') - # since set_state returns non-False, adding it as timeout_add - # will repeatedly call it, and block the main loop; so - # gobject.timeout_add(0L, self.pipeline.set_state, gst.STATE_PLAYING) - # would not work. - - - # FIXME: can't move this to base class because it triggers too soon - # in the case of checksum - - def bus_eos_cb(self, bus, message): - gst.debug('eos, scheduling stop') - self.schedule(0, self.stop) - - def bus_tag_cb(self, bus, message): - taglist = message.parse_tag() - if 'musicbrainz-trmid' in taglist.keys(): - self._trm = taglist['musicbrainz-trmid'] - - def _new_buffer_cb(self, sink): - # this is just for counting progress - buf = sink.emit('pull-buffer') - position = buf.timestamp - if buf.duration != gst.CLOCK_TIME_NONE: - position += buf.duration - self.setProgress(float(position) / self._length) - - def stopped(self): - self.trm = self._trm - -class MaxSampleTask(ChecksumTask): - """ - I check for the biggest sample value. - """ - - description = 'Finding highest sample value' - - def do_checksum_buffer(self, buf, checksum): - values = struct.unpack("<%dh" % (len(buf) / 2), buf) - absvalues = [abs(v) for v in values] - m = max(absvalues) - if checksum < m: - checksum = m - - return checksum - diff --git a/morituri/common/common.py b/morituri/common/common.py index d13488c1..641720f1 100644 --- a/morituri/common/common.py +++ b/morituri/common/common.py @@ -135,47 +135,6 @@ def formatTime(seconds, fractional=3): return " ".join(chunks) - -def tagListToDict(tl): - """ - Converts gst.TagList to dict. - Also strips it of tags that are not writable. - """ - import gst - - d = {} - for key in tl.keys(): - if key == gst.TAG_DATE: - date = tl[key] - d[key] = "%4d-%2d-%2d" % (date.year, date.month, date.day) - elif key in [ - gst.TAG_AUDIO_CODEC, - gst.TAG_VIDEO_CODEC, - gst.TAG_MINIMUM_BITRATE, - gst.TAG_BITRATE, - gst.TAG_MAXIMUM_BITRATE, - ]: - pass - else: - d[key] = tl[key] - return d - - -def tagListEquals(tl1, tl2): - d1 = tagListToDict(tl1) - d2 = tagListToDict(tl2) - - return d1 == d2 - - -def tagListDifference(tl1, tl2): - d1 = tagListToDict(tl1) - d2 = tagListToDict(tl2) - return set(d1.keys()) - set(d2.keys()) - - return d1 == d2 - - class MissingDependencyException(Exception): dependency = None diff --git a/morituri/common/encode.py b/morituri/common/encode.py index 9bf2e02f..4d3b69ad 100644 --- a/morituri/common/encode.py +++ b/morituri/common/encode.py @@ -20,156 +20,17 @@ # You should have received a copy of the GNU General Public License # along with morituri. If not, see . -import math -import os -import shutil -import tempfile from mutagen.flac import FLAC -from morituri.common import common -from morituri.common import gstreamer as cgstreamer -from morituri.common import task as ctask +from morituri.extern.task import task -from morituri.extern.task import task, gstreamer from morituri.program import sox from morituri.program import flac import logging logger = logging.getLogger(__name__) -class Profile: - - name = None - extension = None - pipeline = None - losless = None - - def test(self): - """ - Test if this profile will work. - Can check for elements, ... - """ - pass - - -class FlacProfile(Profile): - name = 'flac' - extension = 'flac' - pipeline = 'flacenc name=tagger quality=8' - lossless = True - - # FIXME: we should do something better than just printing ERRORS - - def test(self): - - # here to avoid import gst eating our options - import gst - - plugin = gst.registry_get_default().find_plugin('flac') - if not plugin: - print 'ERROR: cannot find flac plugin' - return False - - versionTuple = tuple([int(x) for x in plugin.get_version().split('.')]) - if len(versionTuple) < 4: - versionTuple = versionTuple + (0, ) - if versionTuple > (0, 10, 9, 0) and versionTuple <= (0, 10, 15, 0): - print 'ERROR: flacenc between 0.10.9 and 0.10.15 has a bug' - return False - - return True - -# FIXME: ffenc_alac does not have merge_tags - - -class AlacProfile(Profile): - name = 'alac' - extension = 'alac' - pipeline = 'ffenc_alac' - lossless = True - -# FIXME: wavenc does not have merge_tags - - -class WavProfile(Profile): - name = 'wav' - extension = 'wav' - pipeline = 'wavenc' - lossless = True - - -class WavpackProfile(Profile): - name = 'wavpack' - extension = 'wv' - pipeline = 'wavpackenc bitrate=0 name=tagger' - lossless = True - - -class _LameProfile(Profile): - extension = 'mp3' - lossless = False - - def test(self): - version = cgstreamer.elementFactoryVersion('lamemp3enc') - logger.debug('lamemp3enc version: %r', version) - if version: - t = tuple([int(s) for s in version.split('.')]) - if t >= (0, 10, 19): - self.pipeline = self._lamemp3enc_pipeline - return True - - version = cgstreamer.elementFactoryVersion('lame') - logger.debug('lame version: %r', version) - if version: - self.pipeline = self._lame_pipeline - return True - - return False - - -class MP3Profile(_LameProfile): - name = 'mp3' - - _lame_pipeline = 'lame name=tagger quality=0 ! id3v2mux' - _lamemp3enc_pipeline = \ - 'lamemp3enc name=tagger target=bitrate cbr=true bitrate=320 ! ' \ - 'xingmux ! id3v2mux' - - -class MP3VBRProfile(_LameProfile): - name = 'mp3vbr' - - _lame_pipeline = 'lame name=tagger ' \ - 'vbr-quality=0 vbr=new vbr-mean-bitrate=192 ! ' \ - 'id3v2mux' - _lamemp3enc_pipeline = 'lamemp3enc name=tagger quality=0 ' \ - '! xingmux ! id3v2mux' - - -class VorbisProfile(Profile): - name = 'vorbis' - extension = 'oga' - pipeline = 'audioconvert ! vorbisenc name=tagger ! oggmux' - lossless = False - - -PROFILES = { - 'wav': WavProfile, - 'flac': FlacProfile, - 'alac': AlacProfile, - 'wavpack': WavpackProfile, -} - -LOSSY_PROFILES = { - 'mp3': MP3Profile, - 'mp3vbr': MP3VBRProfile, - 'vorbis': VorbisProfile, -} - -ALL_PROFILES = PROFILES.copy() -ALL_PROFILES.update(LOSSY_PROFILES) - class SoxPeakTask(task.Task): description = 'Calculating peak level' @@ -226,380 +87,3 @@ def _tag(self): w.save() self.stop() - -class EncodeTask(ctask.GstPipelineTask): - """ - 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 volume, from 0.0 to 1.0. This is the sqrt of the - peak power. - @type peak: float - """ - - logCategory = 'EncodeTask' - - description = 'Encoding' - peak = None - - def __init__(self, inpath, outpath, profile, taglist=None, what="track"): - """ - @param profile: encoding profile - @type profile: L{Profile} - """ - assert type(inpath) is unicode, "inpath %r is not unicode" % inpath - assert type(outpath) is unicode, \ - "outpath %r is not unicode" % outpath - - self._inpath = inpath - self._outpath = outpath - self._taglist = taglist - self._length = 0 # in samples - - self._level = None - self._peakdB = None - self._profile = profile - - self.description = "Encoding %s" % what - self._profile.test() - - cgstreamer.removeAudioParsers() - - def getPipelineDesc(self): - # start with an emit interval of one frame, because we end up setting - # the final interval after paused and after processing some samples - # already, which is too late - interval = int(self.gst.SECOND / 75.0) - return ''' - filesrc location="%s" ! - decodebin name=decoder ! - audio/x-raw-int,width=16,depth=16,channels=2 ! - level name=level interval=%d ! - %s ! identity name=identity ! - filesink location="%s" name=sink''' % ( - gstreamer.quoteParse(self._inpath).encode('utf-8'), - interval, - self._profile.pipeline, - gstreamer.quoteParse(self._outpath).encode('utf-8')) - - def parsed(self): - tagger = self.pipeline.get_by_name('tagger') - - # set tags - if tagger and self._taglist: - # FIXME: under which conditions do we not have merge_tags ? - # See for example comment saying wavenc did not have it. - try: - tagger.merge_tags(self._taglist, self.gst.TAG_MERGE_APPEND) - except AttributeError, e: - logger.warning('Could not merge tags: %r', str(e)) - - def paused(self): - # get length - identity = self.pipeline.get_by_name('identity') - logger.debug('query duration') - try: - length, qformat = identity.query_duration(self.gst.FORMAT_DEFAULT) - except self.gst.QueryError, e: - self.setException(e) - self.stop() - return - - - # wavparse 0.10.14 returns in bytes - if qformat == self.gst.FORMAT_BYTES: - logger.debug('query returned in BYTES format') - length /= 4 - logger.debug('total length: %r', length) - self._length = length - - duration = None - try: - duration, qformat = identity.query_duration(self.gst.FORMAT_TIME) - except self.gst.QueryError, e: - logger.debug('Could not query duration') - self._duration = duration - - # set up level callbacks - # FIXME: publicize bus and reuse it instead of regetting and adding ? - bus = self.pipeline.get_bus() - bus.add_signal_watch() - - bus.connect('message::element', self._message_element_cb) - self._level = self.pipeline.get_by_name('level') - - # set an interval that is smaller than the duration - # FIXME: check level and make sure it emits level up to the last - # sample, even if input is small - interval = self.gst.SECOND - if interval > duration: - interval = duration / 2 - logger.debug('Setting level interval to %s, duration %s', - self.gst.TIME_ARGS(interval), self.gst.TIME_ARGS(duration)) - self._level.set_property('interval', interval) - # 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') - self.gst.debug('adding srcpad buffer probe to %r' % srcpad) - ret = srcpad.add_buffer_probe(self._probe_handler) - self.gst.debug('added srcpad buffer probe to %r: %r' % (srcpad, ret)) - - 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.schedule(0, self.setProgress, - float(buffer.offset) / self._length) - - # don't drop the buffer - return True - - def bus_eos_cb(self, bus, message): - logger.debug('eos, scheduling stop') - self.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: - logger.debug('higher peakdB found, now %r', self._peakdB) - self._peakdB = p - - # FIXME: works around a bug on F-15 where buffer probes don't seem - # to get triggered to update progress - if self._duration is not None: - self.schedule(0, self.setProgress, - float(s['stream-time'] + s['duration']) / self._duration) - - def stopped(self): - if self._peakdB is not None: - logger.debug('peakdB %r', self._peakdB) - self.peak = math.sqrt(math.pow(10, self._peakdB / 10.0)) - return - - logger.warning('No peak found.') - - self.peak = 0.0 - - if self._duration: - logger.warning('GStreamer level element did not send messages.') - # workaround for when the file is too short to have volume ? - if self._length == common.SAMPLES_PER_FRAME: - logger.warning('only one frame of audio, setting peak to 0.0') - self.peak = 0.0 - -class TagReadTask(ctask.GstPipelineTask): - """ - I am a task that reads tags. - - @ivar taglist: the tag list read from the file. - @type taglist: L{gst.TagList} - """ - - logCategory = 'TagReadTask' - - description = 'Reading tags' - - taglist = None - - def __init__(self, path): - """ - """ - assert type(path) is unicode, "path %r is not unicode" % path - - self._path = path - - def getPipelineDesc(self): - return ''' - filesrc location="%s" ! - decodebin name=decoder ! - fakesink''' % ( - gstreamer.quoteParse(self._path).encode('utf-8')) - - def bus_eos_cb(self, bus, message): - logger.debug('eos, scheduling stop') - self.schedule(0, self.stop) - - def bus_tag_cb(self, bus, message): - taglist = message.parse_tag() - logger.debug('tag_cb, %d tags' % len(taglist.keys())) - if not self.taglist: - self.taglist = taglist - else: - import gst - self.taglist = self.taglist.merge(taglist, gst.TAG_MERGE_REPLACE) - - -class TagWriteTask(ctask.LoggableTask): - """ - I am a task that retags an encoded file. - """ - - logCategory = 'TagWriteTask' - - description = 'Writing tags' - - def __init__(self, inpath, outpath, taglist=None): - """ - """ - assert type(inpath) is unicode, "inpath %r is not unicode" % inpath - assert type(outpath) is unicode, "outpath %r is not unicode" % outpath - - self._inpath = inpath - self._outpath = outpath - self._taglist = taglist - - def start(self, runner): - task.Task.start(self, runner) - - # here to avoid import gst eating our options - import gst - - # FIXME: this hardcodes flac; we should be using the correct - # tag element instead - self._pipeline = gst.parse_launch(''' - filesrc location="%s" ! - flactag name=tagger ! - filesink location="%s"''' % ( - gstreamer.quoteParse(self._inpath).encode('utf-8'), - gstreamer.quoteParse(self._outpath).encode('utf-8'))) - - # set tags - tagger = self._pipeline.get_by_name('tagger') - if self._taglist: - tagger.merge_tags(self._taglist, gst.TAG_MERGE_APPEND) - - logger.debug('pausing pipeline') - self._pipeline.set_state(gst.STATE_PAUSED) - self._pipeline.get_state() - logger.debug('paused pipeline') - - # add eos handling - bus = self._pipeline.get_bus() - bus.add_signal_watch() - bus.connect('message::eos', self._message_eos_cb) - - logger.debug('scheduling setting to play') - # since set_state returns non-False, adding it as timeout_add - # will repeatedly call it, and block the main loop; so - # gobject.timeout_add(0L, self._pipeline.set_state, - # gst.STATE_PLAYING) - # would not work. - - def play(): - self._pipeline.set_state(gst.STATE_PLAYING) - return False - self.schedule(0, play) - - #self._pipeline.set_state(gst.STATE_PLAYING) - logger.debug('scheduled setting to play') - - def _message_eos_cb(self, bus, message): - logger.debug('eos, scheduling stop') - self.schedule(0, self.stop) - - def stop(self): - # here to avoid import gst eating our options - import gst - - logger.debug('stopping') - logger.debug('setting state to NULL') - self._pipeline.set_state(gst.STATE_NULL) - logger.debug('set state to NULL') - task.Task.stop(self) - - -class SafeRetagTask(ctask.LoggableMultiSeparateTask): - """ - I am a task that retags an encoded file safely in place. - First of all, if the new tags are the same as the old ones, it doesn't - do anything. - If the tags are not the same, then the file gets retagged, but only - if the decodes of the original and retagged file checksum the same. - - @ivar changed: True if the tags have changed (and hence an output file is - generated) - """ - - logCategory = 'SafeRetagTask' - - description = 'Retagging' - - changed = False - - def __init__(self, path, taglist=None): - """ - """ - assert type(path) is unicode, "path %r is not unicode" % path - - task.MultiSeparateTask.__init__(self) - - self._path = path - self._taglist = taglist.copy() - - self.tasks = [TagReadTask(path), ] - - def stopped(self, taskk): - from morituri.common import checksum - - if not taskk.exception: - # Check if the tags are different or not - if taskk == self.tasks[0]: - taglist = taskk.taglist.copy() - if common.tagListEquals(taglist, self._taglist): - logger.debug('tags are already fine: %r', - common.tagListToDict(taglist)) - else: - # need to retag - logger.debug('tags need to be rewritten') - logger.debug('Current tags: %r, new tags: %r', - common.tagListToDict(taglist), - common.tagListToDict(self._taglist)) - assert common.tagListToDict(taglist) \ - != common.tagListToDict(self._taglist) - self.tasks.append(checksum.CRC32Task(self._path)) - self._fd, self._tmppath = tempfile.mkstemp( - dir=os.path.dirname(self._path), suffix=u'.morituri') - self.tasks.append(TagWriteTask(self._path, - self._tmppath, self._taglist)) - self.tasks.append(checksum.CRC32Task(self._tmppath)) - self.tasks.append(TagReadTask(self._tmppath)) - elif len(self.tasks) > 1 and taskk == self.tasks[4]: - if common.tagListEquals(self.tasks[4].taglist, self._taglist): - logger.debug('tags written successfully') - c1 = self.tasks[1].checksum - c2 = self.tasks[3].checksum - logger.debug('comparing checksums %08x and %08x' % (c1, c2)) - if c1 == c2: - # data is fine, so we can now move - # but first, copy original mode to our temporary file - shutil.copymode(self._path, self._tmppath) - logger.debug('moving temporary file to %r' % self._path) - os.rename(self._tmppath, self._path) - self.changed = True - else: - # FIXME: don't raise TypeError - e = TypeError("Checksums failed") - self.setAndRaiseException(e) - else: - logger.debug('failed to update tags, only have %r', - common.tagListToDict(self.tasks[4].taglist)) - logger.debug('difference: %r', - common.tagListDifference(self.tasks[4].taglist, - self._taglist)) - os.unlink(self._tmppath) - e = TypeError("Tags not written") - self.setAndRaiseException(e) - - task.MultiSeparateTask.stopped(self, taskk) diff --git a/morituri/common/gstreamer.py b/morituri/common/gstreamer.py deleted file mode 100644 index ed1e5c35..00000000 --- a/morituri/common/gstreamer.py +++ /dev/null @@ -1,86 +0,0 @@ -# -*- Mode: Python; test-case-name: morituri.test.test_common_gstreamer -*- -# vi:si:et:sw=4:sts=4:ts=4 - -# Morituri - for those about to RIP - -# Copyright (C) 2009 Thomas Vander Stichele - -# This file is part of morituri. -# -# morituri 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 3 of the License, or -# (at your option) any later version. -# -# morituri 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 morituri. If not, see . - -import re -import commands - -import logging -logger = logging.getLogger(__name__) - -# workaround for issue #64 - - -def removeAudioParsers(): - logger.debug('Removing buggy audioparsers plugin if needed') - - import gst - registry = gst.registry_get_default() - - plugin = registry.find_plugin("audioparsersbad") - if plugin: - # always remove from bad - logger.debug('removing audioparsersbad plugin from registry') - registry.remove_plugin(plugin) - - plugin = registry.find_plugin("audioparsers") - if plugin: - logger.debug('removing audioparsers plugin from %s %s', - plugin.get_source(), plugin.get_version()) - - # the query bug was fixed after 0.10.30 and before 0.10.31 - # the seek bug is still there though - # if plugin.get_source() == 'gst-plugins-good' \ - # and plugin.get_version() > '0.10.30.1': - # return - - registry.remove_plugin(plugin) - -def gstreamerVersion(): - import gst - return _versionify(gst.version()) - -def gstPythonVersion(): - import gst - return _versionify(gst.pygst_version) - -_VERSION_RE = re.compile( - "Version:\s*(?P[\d.]+)") - -def elementFactoryVersion(name): - # surprisingly, there is no python way to get from an element factory - # to its plugin and its version directly; you can only compare - # with required versions - # Let's use gst-inspect-0.10 and wave hands and assume it points to the - # same version that python uses - output = commands.getoutput('gst-inspect-0.10 %s | grep Version' % name) - m = _VERSION_RE.search(output) - if not m: - return None - return m.group('version') - - -def _versionify(tup): - l = list(tup) - if len(l) == 4 and l[3] == 0: - l = l[:3] - v = [str(n) for n in l] - return ".".join(v) diff --git a/morituri/common/program.py b/morituri/common/program.py index 27295073..d1285aab 100644 --- a/morituri/common/program.py +++ b/morituri/common/program.py @@ -30,9 +30,10 @@ import time from morituri.common import common, mbngs, cache, path +from morituri.common import checksum from morituri.program import cdrdao, cdparanoia from morituri.image import image -from morituri.extern.task import task, gstreamer +from morituri.extern.task import task import logging logger = logging.getLogger(__name__) @@ -172,8 +173,7 @@ def getRipResult(self, cddbdiscid): def saveRipResult(self): self._presult.persist() - def getPath(self, outdir, template, mbdiscid, i, profile=None, - disambiguate=False): + def getPath(self, outdir, template, mbdiscid, i, disambiguate=False): """ Based on the template, get a complete path for the given track, minus extension. @@ -185,7 +185,6 @@ def getPath(self, outdir, template, mbdiscid, i, profile=None, @type template: unicode @param i: track number (0 for HTOA, or for disc) @type i: int - @type profile: L{morituri.common.encode.Profile} @rtype: unicode """ @@ -208,7 +207,7 @@ def getPath(self, outdir, template, mbdiscid, i, profile=None, v['R'] = 'Unknown' v['B'] = '' # barcode v['C'] = '' # catalog number - v['x'] = profile and profile.extension or 'unknown' + v['x'] = 'flac' v['X'] = v['x'].upper() v['y'] = '0000' @@ -416,12 +415,12 @@ def getMusicBrainz(self, ittoc, mbdiscid, release=None, country=None, prompt=Fal def getTagList(self, number): """ - Based on the metadata, get a gst.TagList for the given track. + Based on the metadata, get a dict of tags for the given track. @param number: track number (0 for HTOA) @type number: int - @rtype: L{gst.TagList} + @rtype: dict """ trackArtist = u'Unknown Artist' albumArtist = u'Unknown Artist' @@ -491,8 +490,6 @@ def getHTOA(self): return (start, stop) def verifyTrack(self, runner, trackResult): - # here to avoid import gst eating our options - from morituri.common import checksum t = checksum.CRC32Task(trackResult.filename) @@ -502,9 +499,6 @@ def verifyTrack(self, runner, trackResult): if isinstance(e.exception, common.MissingFrames): logger.warning('missing frames for %r' % trackResult.filename) return False - elif isinstance(e.exception, gstreamer.GstException): - logger.warning('GstException %r' % (e.exception, )) - return False else: raise @@ -513,7 +507,7 @@ def verifyTrack(self, runner, trackResult): trackResult.testcrc, t.checksum, ret) return ret - def ripTrack(self, runner, trackResult, offset, device, profile, taglist, + def ripTrack(self, runner, trackResult, offset, device, taglist, overread, what=None): """ Ripping the track may change the track's filename as stored in @@ -521,8 +515,6 @@ def ripTrack(self, runner, trackResult, offset, device, profile, taglist, @param trackResult: the object to store information in. @type trackResult: L{result.TrackResult} - @param number: track number (1-based) - @type number: int """ if trackResult.number == 0: start, stop = self.getHTOA() @@ -541,7 +533,6 @@ def ripTrack(self, runner, trackResult, offset, device, profile, taglist, self.result.table, start, stop, overread, offset=offset, device=device, - profile=profile, taglist=taglist, what=what) diff --git a/morituri/common/task.py b/morituri/common/task.py index f43cf172..24197042 100644 --- a/morituri/common/task.py +++ b/morituri/common/task.py @@ -6,7 +6,7 @@ import subprocess from morituri.extern import asyncsub -from morituri.extern.task import task, gstreamer +from morituri.extern.task import task import logging logger = logging.getLogger(__name__) @@ -24,10 +24,6 @@ class LoggableMultiSeparateTask(task.MultiSeparateTask): pass -class GstPipelineTask(gstreamer.GstPipelineTask): - pass - - class PopenTask(task.Task): """ I am a task that runs a command using Popen. diff --git a/morituri/extern/task/gstreamer.py b/morituri/extern/task/gstreamer.py deleted file mode 100644 index 6cd7f024..00000000 --- a/morituri/extern/task/gstreamer.py +++ /dev/null @@ -1,272 +0,0 @@ -# -*- Mode: Python; test-case-name: test_gstreamer -*- -# vi:si:et:sw=4:sts=4:ts=4 - -# Morituri - for those about to RIP - -# Copyright (C) 2009 Thomas Vander Stichele - -# This file is part of morituri. -# -# morituri 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 3 of the License, or -# (at your option) any later version. -# -# morituri 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 morituri. If not, see . - -import task - -def quoteParse(path): - """ - Quote a path for use in gst.parse_launch. - """ - # Make sure double quotes and backslashes are escaped. See - # morituri.test.test_common_checksum.NormalPathTestCase - - return path.replace('\\', '\\\\').replace('"', '\\"') - - -class GstException(Exception): - def __init__(self, gerror, debug): - self.args = (gerror, debug, ) - self.gerror = gerror - self.debug = debug - - def __repr__(self): - return '' % ( - self.gerror.message, self.debug) - -class GstPipelineTask(task.Task): - """ - I am a base class for tasks that use a GStreamer pipeline. - - I handle errors and raise them appropriately. - - @cvar gst: the GStreamer module, so code does not have to import gst - as a module in code everywhere to avoid option stealing. - @cvar playing: whether the pipeline should be set to playing after - paused. Some pipelines don't need to play for a task - to be done (for example, querying length) - @type playing: bool - @type pipeline: L{gst.Pipeline} - @type bus: L{gst.Bus} - """ - - gst = None - playing = True - pipeline = None - bus = None - - ### task.Task implementations - def start(self, runner): - import gst - self.gst = gst - - task.Task.start(self, runner) - - self.getPipeline() - - self.bus = self.pipeline.get_bus() - # FIXME: remove this - self._bus = self.bus - self.gst.debug('got bus %r' % self.bus) - - # a signal watch calls callbacks from an idle loop - # self.bus.add_signal_watch() - - # sync emission triggers sync-message signals which calls callbacks - # from the thread that signals, but happens immediately - self.bus.enable_sync_message_emission() - self.bus.connect('sync-message::eos', self.bus_eos_cb) - self.bus.connect('sync-message::tag', self.bus_tag_cb) - self.bus.connect('sync-message::error', self.bus_error_cb) - - self.parsed() - - self.debug('setting pipeline to PAUSED') - self.pipeline.set_state(gst.STATE_PAUSED) - self.debug('set pipeline to PAUSED') - # FIXME: this can block - ret = self.pipeline.get_state() - self.debug('got pipeline to PAUSED: %r', ret) - - # GStreamer tasks could already be done in paused, and not - # need playing. - if self.exception: - raise self.exception - - done = self.paused() - - if done: - self.debug('paused() is done') - else: - self.debug('paused() wants more') - self.play() - - def play(self): - # since set_state returns non-False, adding it as timeout_add - # will repeatedly call it, and block the main loop; so - # gobject.timeout_add(0L, self._pipeline.set_state, - # gst.STATE_PLAYING) - # would not work. - def playLater(): - if self.exception: - self.debug('playLater: exception was raised, not playing') - self.stop() - return False - - self.debug('setting pipeline to PLAYING') - self.pipeline.set_state(self.gst.STATE_PLAYING) - self.debug('set pipeline to PLAYING') - return False - - if self.playing: - self.debug('schedule playLater()') - self.schedule(0, playLater) - - def stop(self): - self.debug('stopping') - - - # FIXME: in theory this should help clean up properly, - # but in practice we can still get - # python: /builddir/build/BUILD/Python-2.7/Python/pystate.c:595: PyGILState_Ensure: Assertion `autoInterpreterState' failed. - - self.pipeline.set_state(self.gst.STATE_READY) - self.debug('set pipeline to READY') - # FIXME: this can block - ret = self.pipeline.get_state() - self.debug('got pipeline to READY: %r', ret) - - self.debug('setting state to NULL') - self.pipeline.set_state(self.gst.STATE_NULL) - self.debug('set state to NULL') - self.stopped() - task.Task.stop(self) - - ### subclass optional implementations - def getPipeline(self): - desc = self.getPipelineDesc() - - self.debug('creating pipeline %r', desc) - self.pipeline = self.gst.parse_launch(desc) - - def getPipelineDesc(self): - """ - subclasses should implement this to provide a pipeline description. - - @rtype: str - """ - raise NotImplementedError - - def parsed(self): - """ - Called after parsing/getting the pipeline but before setting it to - paused. - """ - pass - - def paused(self): - """ - Called after pipeline is paused. - - If this returns True, the task is done and - should not continue going to PLAYING. - """ - pass - - def stopped(self): - """ - Called after pipeline is set back to NULL but before chaining up to - stop() - """ - pass - - def bus_eos_cb(self, bus, message): - """ - Called synchronously (ie from messaging thread) on eos message. - - Override me to handle eos - """ - pass - - def bus_tag_cb(self, bus, message): - """ - Called synchronously (ie from messaging thread) on tag message. - - Override me to handle tags. - """ - pass - - def bus_error_cb(self, bus, message): - """ - Called synchronously (ie from messaging thread) on error message. - """ - self.debug('bus_error_cb: bus %r, message %r' % (bus, message)) - if self.exception: - self.debug('bus_error_cb: already got an exception, ignoring') - return - - exc = GstException(*message.parse_error()) - self.setAndRaiseException(exc) - self.debug('error, scheduling stop') - self.schedule(0, self.stop) - - def query_length(self, element): - """ - Query the length of the pipeline in samples, for progress updates. - To be called from paused() - """ - # get duration - self.debug('query duration') - try: - duration, qformat = element.query_duration(self.gst.FORMAT_DEFAULT) - except self.gst.QueryError, e: - # Fall back to time; for example, oggdemux/vorbisdec only supports - # TIME - try: - duration, qformat = element.query_duration(self.gst.FORMAT_TIME) - except self.gst.QueryError, e: - self.setException(e) - # schedule it, otherwise runner can get set to None before - # we're done starting - self.schedule(0, self.stop) - return - - # wavparse 0.10.14 returns in bytes - if qformat == self.gst.FORMAT_BYTES: - self.debug('query returned in BYTES format') - duration /= 4 - - if qformat == self.gst.FORMAT_TIME: - rate = None - self.debug('query returned in TIME format') - # we need sample rate - pads = list(element.pads()) - sink = element.get_by_name('sink') - pads += list(sink.pads()) - - for pad in pads: - caps = pad.get_negotiated_caps() - print caps[0].keys() - if 'rate' in caps[0].keys(): - rate = caps[0]['rate'] - self.debug('Sample rate: %d Hz', rate) - - if not rate: - raise KeyError( - 'Cannot find sample rate, cannot convert to samples') - - duration = int(float(rate) * (float(duration) / self.gst.SECOND)) - - self.debug('total duration: %r', duration) - - return duration - - diff --git a/morituri/image/image.py b/morituri/image/image.py index 42c692c9..d2f2b92c 100644 --- a/morituri/image/image.py +++ b/morituri/image/image.py @@ -26,7 +26,9 @@ import os +from morituri.common import encode from morituri.common import common +from morituri.common import checksum from morituri.image import cue, table from morituri.extern.task import task from morituri.program.soxi import AudioLengthTask @@ -135,8 +137,6 @@ def __init__(self, image): path = image.getRealPath(index.path) - # here to avoid import gst eating our options - from morituri.common import checksum checksumTask = checksum.FastAccurateRipChecksumTask(path, trackNumber=trackIndex + 1, trackCount=len(cue.table.tracks), @@ -221,27 +221,24 @@ class ImageEncodeTask(task.MultiSeparateTask): description = "Encoding tracks" - def __init__(self, image, profile, outdir): + def __init__(self, image, outdir): task.MultiSeparateTask.__init__(self) self._image = image - self._profile = profile cue = image.cue self._tasks = [] self.lengths = {} def add(index): - # here to avoid import gst eating our options - from morituri.common import encode path = image.getRealPath(index.path) assert type(path) is unicode, "%r is not unicode" % path logger.debug('schedule encode of %r', path) root, ext = os.path.splitext(os.path.basename(path)) - outpath = os.path.join(outdir, root + '.' + profile.extension) + outpath = os.path.join(outdir, root + '.' + 'flac') logger.debug('schedule encode to %r', outpath) - taskk = encode.EncodeTaskFlac(path, os.path.join(outdir, - root + '.' + profile.extension)) + taskk = encode.FlacEncodeTask(path, os.path.join(outdir, + root + '.' + 'flac')) self.addTask(taskk) try: diff --git a/morituri/program/cdparanoia.py b/morituri/program/cdparanoia.py index 6daa672f..b2321ade 100644 --- a/morituri/program/cdparanoia.py +++ b/morituri/program/cdparanoia.py @@ -430,7 +430,7 @@ class ReadVerifyTrackTask(task.MultiSeparateTask): _tmppath = None def __init__(self, path, table, start, stop, overread, offset=0, - device=None, profile=None, taglist=None, what="track"): + device=None, taglist=None, what="track"): """ @param path: where to store the ripped track @type path: str @@ -444,10 +444,8 @@ def __init__(self, path, table, start, stop, overread, offset=0, @type offset: int @param device: the device to rip from @type device: str - @param profile: the encoding profile - @type profile: L{encode.Profile} - @param taglist: a list of tags - @param taglist: L{gst.TagList} + @param taglist: a dict of tags + @type taglist: dict """ task.MultiSeparateTask.__init__(self) @@ -461,7 +459,6 @@ def __init__(self, path, table, start, stop, overread, offset=0, os.close(fd) self._tmpwavpath = tmppath - # here to avoid import gst eating our options from morituri.common import checksum self.tasks = [] @@ -487,7 +484,6 @@ def __init__(self, path, table, start, stop, overread, offset=0, self._tmppath = tmpoutpath self.path = path - # here to avoid import gst eating our options from morituri.common import encode self.tasks.append(encode.FlacEncodeTask(tmppath, tmpoutpath)) diff --git a/morituri/result/logger.py b/morituri/result/logger.py index ce89a0af..fbd3505b 100644 --- a/morituri/result/logger.py +++ b/morituri/result/logger.py @@ -56,17 +56,6 @@ def logRip(self, ripResult, epoch): lines.append(" Gap detection: cdrdao %s" % ripResult.cdrdaoVersion) lines.append("") - # Rip encoding settings - lines.append("Encoding phase information:") - lines.append(" Used output format: %s" % ripResult.profileName) - lines.append(" GStreamer:") - lines.append(" Pipeline: %s" % ripResult.profilePipeline) - lines.append(" Version: %s" % ripResult.gstreamerVersion) - lines.append(" Python version: %s" % ripResult.gstPythonVersion) - lines.append(" Encoder plugin version: %s" % - ripResult.encoderVersion) - lines.append("") - # CD metadata lines.append("CD metadata:") lines.append(" Album: %s - %s" % (ripResult.artist, ripResult.title)) diff --git a/morituri/result/result.py b/morituri/result/result.py index 26f72adf..52e5f47a 100644 --- a/morituri/result/result.py +++ b/morituri/result/result.py @@ -108,13 +108,6 @@ class RipResult: cdparanoiaVersion = None cdparanoiaDefeatsCache = None - gstreamerVersion = None - gstPythonVersion = None - encoderVersion = None - - profileName = None - profilePipeline = None - classVersion = 3 def __init__(self): diff --git a/morituri/test/test_common_checksum.py b/morituri/test/test_common_checksum.py deleted file mode 100644 index 32639755..00000000 --- a/morituri/test/test_common_checksum.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- Mode: Python; test-case-name: morituri.test.test_common_checksum -*- -# vi:si:et:sw=4:sts=4:ts=4 - -import os -import tempfile - -import gobject -gobject.threads_init() - -from morituri.common import checksum, task as ctask - -from morituri.extern.task import task, gstreamer - -from morituri.test import common as tcommon - - -def h(i): - return "0x%08x" % i - - -class EmptyTestCase(tcommon.TestCase): - - def testEmpty(self): - # this test makes sure that checksumming empty files doesn't hang - self.runner = ctask.SyncRunner(verbose=False) - fd, path = tempfile.mkstemp(suffix=u'morituri.test.empty') - checksumtask = checksum.ChecksumTask(path) - # FIXME: do we want a specific error for this ? - e = self.assertRaises(task.TaskException, self.runner.run, - checksumtask, verbose=False) - self.failUnless(isinstance(e.exception, gstreamer.GstException)) - os.unlink(path) - - -class PathTestCase(tcommon.TestCase): - - def _testSuffix(self, suffix): - self.runner = ctask.SyncRunner(verbose=False) - fd, path = tempfile.mkstemp(suffix=suffix) - checksumtask = checksum.ChecksumTask(path) - e = self.assertRaises(task.TaskException, self.runner.run, - checksumtask, verbose=False) - self.failUnless(isinstance(e.exception, gstreamer.GstException)) - os.unlink(path) - - -class UnicodePathTestCase(PathTestCase, tcommon.UnicodeTestMixin): - - def testUnicodePath(self): - # this test makes sure we can checksum a unicode path - self._testSuffix(u'morituri.test.B\xeate Noire.empty') - - -class NormalPathTestCase(PathTestCase): - - def testSingleQuote(self): - self._testSuffix(u"morituri.test.Guns 'N Roses") - - def testDoubleQuote(self): - # This test makes sure we can checksum files with double quote in - # their name - self._testSuffix(u'morituri.test.12" edit') - - def testBackSlash(self): - # This test makes sure we can checksum files with a backslash in - # their name - self._testSuffix(u'morituri.test.40 Years Back\\Come') diff --git a/morituri/test/test_common_encode.py b/morituri/test/test_common_encode.py deleted file mode 100644 index 38a8b9ab..00000000 --- a/morituri/test/test_common_encode.py +++ /dev/null @@ -1,146 +0,0 @@ -# -*- Mode: Python; test-case-name: morituri.test.test_common_encode -*- -# vi:si:et:sw=4:sts=4:ts=4 - -import os -import tempfile - -import gobject -gobject.threads_init() - -import gst - -from morituri.common import encode - -from morituri.extern.task import task, gstreamer - -from morituri.test import common - - -class PathTestCase(common.TestCase): - - def _testSuffix(self, suffix): - # because of https://bugzilla.gnome.org/show_bug.cgi?id=688625 - # we first create the file with a 'normal' filename, then rename - self.runner = task.SyncRunner(verbose=False) - fd, path = tempfile.mkstemp() - - cmd = "gst-launch " \ - "audiotestsrc num-buffers=100 samplesperbuffer=1024 ! " \ - "audioconvert ! audio/x-raw-int,width=16,depth=16,channels =2 ! " \ - "wavenc ! " \ - "filesink location=\"%s\" > /dev/null 2>&1" % ( - gstreamer.quoteParse(path).encode('utf-8'), ) - self.debug('Running cmd %r' % cmd) - os.system(cmd) - self.failUnless(os.path.exists(path)) - os.close(fd) - - fd, newpath = tempfile.mkstemp(suffix=suffix) - os.rename(path, newpath) - - encodetask = encode.EncodeTask(newpath, newpath + '.out', - encode.WavProfile()) - self.runner.run(encodetask, verbose=False) - os.close(fd) - os.unlink(newpath) - os.unlink(newpath + '.out') - - -# class UnicodePathTestCase(PathTestCase, common.UnicodeTestMixin): - -# def testUnicodePath(self): -# # this test makes sure we can checksum a unicode path -# self._testSuffix(u'.morituri.test_encode.B\xeate Noire') - - -# class NormalPathTestCase(PathTestCase): - -# def testSingleQuote(self): -# self._testSuffix(u".morituri.test_encode.Guns 'N Roses") - -# def testDoubleQuote(self): -# self._testSuffix(u'.morituri.test_encode.12" edit') - - -class TagReadTestCase(common.TestCase): - - def testRead(self): - path = os.path.join(os.path.dirname(__file__), u'track.flac') - self.runner = task.SyncRunner(verbose=False) - t = encode.TagReadTask(path) - self.runner.run(t) - self.failUnless(t.taglist) - self.assertEquals(t.taglist['audio-codec'], 'FLAC') - self.assertEquals(t.taglist['description'], 'audiotest wave') - - -# class TagWriteTestCase(common.TestCase): - -# def testWrite(self): -# fd, inpath = tempfile.mkstemp(suffix=u'.morituri.tagwrite.flac') - -# # wave is pink-noise because a pure sine is encoded too efficiently -# # by flacenc and triggers not enough frames in parsing -# # FIXME: file a bug for this in GStreamer -# os.system('gst-launch ' -# 'audiotestsrc ' -# 'wave=pink-noise num-buffers=10 samplesperbuffer=588 ! ' -# 'audioconvert ! ' -# 'audio/x-raw-int,channels=2,width=16,height=16,rate=44100 ! ' -# 'flacenc ! filesink location=%s > /dev/null 2>&1' % inpath) -# os.close(fd) - -# fd, outpath = tempfile.mkstemp(suffix=u'.morituri.tagwrite.flac') -# self.runner = task.SyncRunner(verbose=False) -# taglist = gst.TagList() -# taglist[gst.TAG_ARTIST] = 'Artist' -# taglist[gst.TAG_TITLE] = 'Title' - -# t = encode.TagWriteTask(inpath, outpath, taglist) -# self.runner.run(t) - -# t = encode.TagReadTask(outpath) -# self.runner.run(t) -# self.failUnless(t.taglist) -# self.assertEquals(t.taglist['audio-codec'], 'FLAC') -# self.assertEquals(t.taglist['description'], 'audiotest wave') -# self.assertEquals(t.taglist[gst.TAG_ARTIST], 'Artist') -# self.assertEquals(t.taglist[gst.TAG_TITLE], 'Title') - -# os.unlink(inpath) -# os.unlink(outpath) - - -class SafeRetagTestCase(common.TestCase): - - def setUp(self): - self._fd, self._path = tempfile.mkstemp(suffix=u'.morituri.retag.flac') - - os.system('gst-launch ' - 'audiotestsrc ' - 'num-buffers=40 samplesperbuffer=588 wave=pink-noise ! ' - 'audioconvert ! ' - 'audio/x-raw-int,channels=2,width=16,height=16,rate=44100 ! ' - 'flacenc ! filesink location=%s > /dev/null 2>&1' % self._path) - os.close(self._fd) - self.runner = task.SyncRunner(verbose=False) - - def tearDown(self): - os.unlink(self._path) - - # def testNoChange(self): - # taglist = gst.TagList() - # taglist[gst.TAG_DESCRIPTION] = 'audiotest wave' - # taglist[gst.TAG_AUDIO_CODEC] = 'FLAC' - - # t = encode.SafeRetagTask(self._path, taglist) - # self.runner.run(t) - - # def testChange(self): - # taglist = gst.TagList() - # taglist[gst.TAG_DESCRIPTION] = 'audiotest retagged' - # taglist[gst.TAG_AUDIO_CODEC] = 'FLAC' - # taglist[gst.TAG_ARTIST] = 'Artist' - - # t = encode.SafeRetagTask(self._path, taglist) - # self.runner.run(t) diff --git a/morituri/test/test_common_gstreamer.py b/morituri/test/test_common_gstreamer.py deleted file mode 100644 index 94de5c41..00000000 --- a/morituri/test/test_common_gstreamer.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- Mode: Python -*- -# vi:si:et:sw=4:sts=4:ts=4 - -from morituri.common import gstreamer - -from morituri.test import common - - -class VersionTestCase(common.TestCase): - - def testGStreamer(self): - version = gstreamer.gstreamerVersion() - self.failUnless(version.startswith('0.')) - - def testGSTPython(self): - version = gstreamer.gstPythonVersion() - self.failUnless(version.startswith('0.')) - - def testFlacEnc(self): - version = gstreamer.elementFactoryVersion('flacenc') - self.failUnless(version.startswith('0.')) diff --git a/morituri/test/test_image_image.py b/morituri/test/test_image_image.py deleted file mode 100644 index c0221c88..00000000 --- a/morituri/test/test_image_image.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- Mode: Python; test-case-name: morituri.test.test_image_image -*- -# vi:si:et:sw=4:sts=4:ts=4 - -import os -import tempfile - -import gobject -gobject.threads_init() - -import gst - -from morituri.image import image -from morituri.common import common - -from morituri.extern.task import task, gstreamer - -from morituri.test import common as tcommon - - -def h(i): - return "0x%08x" % i - - -class TrackSingleTestCase(tcommon.TestCase): - - def setUp(self): - self.image = image.Image(os.path.join(os.path.dirname(__file__), - u'track-single.cue')) - self.runner = task.SyncRunner(verbose=False) - self.image.setup(self.runner) - - def testAccurateRipChecksum(self): - checksumtask = image.AccurateRipChecksumTask(self.image) - self.runner.run(checksumtask, verbose=False) - - self.assertEquals(len(checksumtask.checksums), 4) -# self.assertEquals(h(checksumtask.checksums[0]), '0x00000000') -# self.assertEquals(h(checksumtask.checksums[1]), '0x793fa868') -# self.assertEquals(h(checksumtask.checksums[2]), '0x8dd37c26') -# self.assertEquals(h(checksumtask.checksums[3]), '0x00000000') - - def testLength(self): - self.assertEquals(self.image.table.getTrackLength(1), 2) - self.assertEquals(self.image.table.getTrackLength(2), 2) - self.assertEquals(self.image.table.getTrackLength(3), 2) - self.assertEquals(self.image.table.getTrackLength(4), 4) - - def testCDDB(self): - self.assertEquals(self.image.table.getCDDBDiscId(), "08000004") - - def testAccurateRip(self): - self.assertEquals(self.image.table.getAccurateRipIds(), ( - "00000016", "0000005b")) - - -class TrackSeparateTestCase(tcommon.TestCase): - - def setUp(self): - self.image = image.Image(os.path.join(os.path.dirname(__file__), - u'track-separate.cue')) - self.runner = task.SyncRunner(verbose=False) - self.image.setup(self.runner) - - def testAccurateRipChecksum(self): - checksumtask = image.AccurateRipChecksumTask(self.image) - self.runner.run(checksumtask, verbose=False) - - self.assertEquals(len(checksumtask.checksums), 4) - self.assertEquals(h(checksumtask.checksums[0]), '0xd60e55e1') - self.assertEquals(h(checksumtask.checksums[1]), '0xd63dc2d2') - self.assertEquals(h(checksumtask.checksums[2]), '0xd63dc2d2') - self.assertEquals(h(checksumtask.checksums[3]), '0x7271db39') - - def testLength(self): - self.assertEquals(self.image.table.getTrackLength(1), 10) - self.assertEquals(self.image.table.getTrackLength(2), 10) - self.assertEquals(self.image.table.getTrackLength(3), 10) - self.assertEquals(self.image.table.getTrackLength(4), 10) - - def testCDDB(self): - self.assertEquals(self.image.table.getCDDBDiscId(), "08000004") - - def testAccurateRip(self): - self.assertEquals(self.image.table.getAccurateRipIds(), ( - "00000064", "00000191"))