Rip out all code that directly uses gstreamer
We can now rip CDs without gstreamer. This is not the most clean attempt, but I have tried to remove most of the code that depends on gstreamer. I hope there is not a lot of code left that depends on code that I have removed - I can at least rip a CD fully.
This commit is contained in:
@@ -26,12 +26,8 @@ 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 +38,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 +78,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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -28,148 +28,15 @@ 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 +93,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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
|
||||
@@ -416,12 +415,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'
|
||||
@@ -491,8 +490,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)
|
||||
|
||||
@@ -502,9 +499,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
|
||||
|
||||
@@ -513,7 +507,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
|
||||
@@ -541,7 +535,6 @@ class Program:
|
||||
self.result.table, start, stop, overread,
|
||||
offset=offset,
|
||||
device=device,
|
||||
profile=profile,
|
||||
taglist=taglist,
|
||||
what=what)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user