Merge pull request #130 from MerlijnWajer/remove-gstreamer

Rip out all code that uses gstreamer
This commit is contained in:
JoeLametta
2017-04-23 23:36:28 +02:00
committed by GitHub
20 changed files with 36 additions and 1736 deletions

View File

@@ -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

View File

@@ -32,7 +32,7 @@ gobject.threads_init()
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 @@ Log files will log the path to tracks relative to this directory.
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 @@ Log files will log the path to tracks relative to this directory.
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 @@ Log files will log the path to tracks relative to this directory.
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 @@ Log files will log the path to tracks relative to this directory.
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 @@ Log files will log the path to tracks relative to this directory.
### 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 @@ Log files will log the path to tracks relative to this directory.
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 @@ Log files will log the path to tracks relative to this directory.
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)

View File

@@ -153,14 +153,6 @@ class Encode(BaseCommand):
# 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 @@ class Encode(BaseCommand):
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 @@ class Encode(BaseCommand):
try:
toPath = unicode(self.options.output)
except IndexError:
toPath = fromPath + '.' + profile.extension
toPath = fromPath + '.flac'
runner = task.SyncRunner()
@@ -191,35 +182,16 @@ class Encode(BaseCommand):
runner.run(encodetask)
sys.stdout.write('Peak level: %r\n' % encodetask.peak)
# 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)
sys.stdout.write('Peak level: %r\n' % peaktask.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'))
checksumtask = checksum.MaxSampleTask(fromPath)
runner.run(checksumtask)
sys.stdout.write('%s\n' % arg)
sys.stdout.write('Biggest absolute sample: %04x\n' %
checksumtask.checksum)
class Tag(BaseCommand):
summary = "run a tag reading task"
description = summary
@@ -325,7 +297,6 @@ class Debug(BaseCommand):
subcommands = {
'checksum': Checksum,
'encode': Encode,
'maxsample': MaxSample,
'tag': Tag,
'musicbrainzngs': MusicBrainzNGS,
'resultcache': ResultCache,

View File

@@ -25,6 +25,7 @@ import sys
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 @@ Retags the image from the given .cue files with tags obtained from MusicBrainz.
)
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()

View File

@@ -32,6 +32,7 @@ from morituri.command.basecommand import BaseCommand
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 @@ CD in the AccurateRip database."""
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 @@ CD in the AccurateRip database."""
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,

View File

@@ -20,18 +20,10 @@
# You should have received a copy of the GNU General Public License
# along with morituri. If not, see <http://www.gnu.org/licenses/>.
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 @@ logger = logging.getLogger(__name__)
# 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 @@ class FastAccurateRipChecksumTask(etask.Task):
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 "<AccurateRipCheckSumTask of track %d in %r>" % (
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("<I", buf[-4:])
checksum += common.SAMPLES_PER_FRAME * 5 * values[0]
checksum &= 0xFFFFFFFF
return checksum
# on last track, skip last 5 CD frames
if self._trackNumber == self._trackCount:
discFrameLength = self._sampleLength / common.SAMPLES_PER_FRAME
if self._discFrameCounter > 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

View File

@@ -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

View File

@@ -20,156 +20,17 @@
# You should have received a copy of the GNU General Public License
# along with morituri. If not, see <http://www.gnu.org/licenses/>.
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 @@ class TaggingTask(task.Task):
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)

View File

@@ -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 <http://www.gnu.org/licenses/>.
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<version>[\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)

View File

@@ -30,9 +30,10 @@ import sys
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 @@ class Program:
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 @@ class Program:
@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 @@ class Program:
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'
@@ -417,12 +416,12 @@ class Program:
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'
@@ -493,8 +492,6 @@ class Program:
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)
@@ -504,9 +501,6 @@ class Program:
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
@@ -515,7 +509,7 @@ class Program:
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
@@ -523,8 +517,6 @@ class Program:
@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()
@@ -543,7 +535,6 @@ class Program:
self.result.table, start, stop, overread,
offset=offset,
device=device,
profile=profile,
taglist=taglist,
what=what)

View File

@@ -6,7 +6,7 @@ import signal
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.

View File

@@ -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 <http://www.gnu.org/licenses/>.
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 '<GstException: GError %r, debug %r>' % (
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

View File

@@ -26,7 +26,9 @@ Wrap on-disk CD images based on the .cue file.
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 @@ class AccurateRipChecksumTask(task.MultiSeparateTask):
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:

View File

@@ -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 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
@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 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
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 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
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))

View File

@@ -56,17 +56,6 @@ class MorituriLogger(result.Logger):
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))

View File

@@ -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):

View File

@@ -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')

View File

@@ -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)

View File

@@ -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.'))

View File

@@ -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"))