* morituri/common/encode.py:

* morituri/program/cdparanoia.py:
	  Add encoding profiles, kept simple for now as a class and
	  subclasses.  Use them to encode.  Calculate peak level while
	  encoding, compared to EAC and replaygain's value.
	* morituri/rip/cd.py:
	  Use the encoding profiles, ripping with the right extension.
	  Add a --profile parameter for it.
This commit is contained in:
Thomas Vander Stichele
2009-05-31 23:04:58 +00:00
parent b3c2949066
commit f33c50cead
4 changed files with 144 additions and 29 deletions

View File

@@ -1,3 +1,14 @@
2009-06-01 Thomas Vander Stichele <thomas at apestaart dot org>
* morituri/common/encode.py:
* morituri/program/cdparanoia.py:
Add encoding profiles, kept simple for now as a class and
subclasses. Use them to encode. Calculate peak level while
encoding, compared to EAC and replaygain's value.
* morituri/rip/cd.py:
Use the encoding profiles, ripping with the right extension.
Add a --profile parameter for it.
2009-05-31 Thomas Vander Stichele <thomas at apestaart dot org>
* morituri/rip/cd.py:

View File

@@ -21,6 +21,7 @@
# along with morituri. If not, see <http://www.gnu.org/licenses/>.
import os
import math
import struct
import zlib
@@ -31,33 +32,79 @@ from morituri.common import common, task
from morituri.common import log
log.init()
class Profile(object):
name = None
extension = None
pipeline = None
class FlacProfile(Profile):
name = 'flac'
extension = 'flac'
pipeline = 'flacenc name=muxer quality=8'
class AlacProfile(Profile):
name = 'alac'
extension = 'alac'
pipeline = 'ffenc_alac name=muxer'
class WavProfile(Profile):
name = 'wav'
extension = 'wav'
pipeline = 'wavenc name=muxer'
class WavpackProfile(Profile):
name = 'wavpack'
extension = 'wv'
pipeline = 'wavpackenc bitrate=0 name=muxer'
PROFILES = {
'wav': WavProfile,
'flac': FlacProfile,
'alac': AlacProfile,
'wavpack': WavpackProfile,
}
class EncodeTask(task.Task):
"""
I am a task that encodes a .wav file.
I set tags too.
I also calculate the peak level of the track.
@param peak: the peak power, from 0.0 to 1.0. To get the peak volume,
square root this value.
@type peak: float
"""
description = 'Encoding'
peak = None
def __init__(self, inpath, outpath, taglist=None):
def __init__(self, inpath, outpath, profile, taglist=None):
"""
"""
self._inpath = inpath
self._outpath = outpath
self._taglist = taglist
self._level = None
self._peakdB = None
self._profile = PROFILES[profile]
def start(self, runner):
task.Task.start(self, runner)
self._pipeline = gst.parse_launch('''
filesrc location="%s" !
decodebin name=decoder ! audio/x-raw-int !
flacenc name=muxer !
filesink location="%s" name=sink''' % (self._inpath, self._outpath))
decodebin name=decoder !
audio/x-raw-int,width=16,depth=16,channels=2 !
level name=level !
%s !
filesink location="%s" name=sink''' % (self._inpath,
self._profile.pipeline, self._outpath))
muxer = self._pipeline.get_by_name('muxer')
# set tags
if self._taglist:
muxer = self._pipeline.get_by_name('muxer')
muxer.merge_tags(self._taglist, gst.TAG_MERGE_APPEND)
self.debug('pausing pipeline')
@@ -79,12 +126,16 @@ class EncodeTask(task.Task):
# add a probe so we can track progress
sinkpad = muxer.get_pad('sink')
srcpad = sinkpad.get_peer()
srcpad.add_buffer_probe(self._probe_handler, False)
srcpad.add_buffer_probe(self._probe_handler)
# add eos handling
bus = self._pipeline.get_bus()
bus.add_signal_watch()
bus.connect('message::eos', self._eos_cb)
bus.connect('message::eos', self._message_eos_cb)
# set up level callbacks
bus.connect('message::element', self._message_element_cb)
self._level = self._pipeline.get_by_name('level')
self.debug('scheduling setting to play')
# since set_state returns non-False, adding it as timeout_add
@@ -100,19 +151,40 @@ class EncodeTask(task.Task):
#self._pipeline.set_state(gst.STATE_PLAYING)
self.debug('scheduled setting to play')
def _probe_handler(self, pad, buffer, ret):
def _probe_handler(self, pad, buffer):
# marshal to main thread
self.runner.schedule(0, self.setProgress,
float(buffer.offset) / self._length)
# don't drop the buffer
return True
def _eos_cb(self, bus, message):
def _message_eos_cb(self, bus, message):
self.debug('eos, scheduling stop')
self.runner.schedule(0, self.stop)
def _message_element_cb(self, bus, message):
if message.src != self._level:
return
s = message.structure
if s.get_name() != 'level':
return
if self._peakdB is None:
self._peakdB = s['peak'][0]
for p in s['peak']:
if self._peakdB < p:
self._peakdB = p
def stop(self):
self.debug('stopping')
self.debug('setting state to NULL')
self._pipeline.set_state(gst.STATE_NULL)
self.debug('set state to NULL')
task.Task.stop(self)
self.peak = math.pow(10, self._peakdB / 10.0)

View File

@@ -27,7 +27,7 @@ import shutil
import subprocess
import tempfile
from morituri.common import task, log, common, checksum
from morituri.common import task, log, common, checksum, encode
from morituri.extern import asyncsub
class FileSizeError(Exception):
@@ -228,22 +228,25 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
@ivar checksum: the checksum of the track; set if they match.
@ivar testchecksum: the test checksum of the track.
@ivar copychecksum: the copy checksum of the track.
@ivar peak: the peak level of the track
"""
def __init__(self, path, table, start, stop, offset=0, device=None):
def __init__(self, path, table, start, stop, offset=0, device=None, profile=None):
"""
@param path: where to store the ripped track
@type path: str
@param table: table of contents of CD
@type table: L{table.Table}
@param start: first frame to rip
@type start: int
@param stop: last frame to rip (inclusive)
@type stop: int
@param offset: read offset, in samples
@type offset: int
@param device: the device to rip from
@type device: str
@param path: where to store the ripped track
@type path: str
@param table: table of contents of CD
@type table: L{table.Table}
@param start: first frame to rip
@type start: int
@param stop: last frame to rip (inclusive)
@type stop: int
@param offset: read offset, in samples
@type offset: int
@param device: the device to rip from
@type device: str
@param profile: the encoding profile
@type profile: str
"""
task.MultiSeparateTask.__init__(self)
@@ -263,10 +266,20 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
self.tasks.append(t)
self.tasks.append(checksum.CRC32Task(tmppath))
# FIXME: clean this up
fd, tmpoutpath = tempfile.mkstemp(suffix='.morituri.flac')
os.close(fd)
self._tmppath = tmpoutpath
self.tasks.append(encode.EncodeTask(tmppath, tmpoutpath, profile))
# make sure our encoding is accurate
self.tasks.append(checksum.CRC32Task(tmpoutpath))
self.checksum = None
def stop(self):
if not self.exception:
self.peak = self.tasks[4].peak
self.testchecksum = c1 = self.tasks[1].checksum
self.copychecksum = c2 = self.tasks[3].checksum
if c1 == c2:
@@ -275,6 +288,8 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
else:
self.error('read and verify failed')
if self.tasks[5].checksum != self.checksum:
self.error('Encoding failed, checksum does not match')
try:
shutil.move(self._tmppath, self.path)
self.checksum = checksum

View File

@@ -22,11 +22,13 @@
import os
import sys
import math
import gobject
gobject.threads_init()
from morituri.common import logcommand, task, checksum, common, accurip, drive
from morituri.common import logcommand, task, checksum, common, accurip
from morituri.common import drive, encode
from morituri.image import image, cue, table
from morituri.program import cdrdao, cdparanoia
@@ -199,6 +201,12 @@ class Rip(logcommand.LogCommand):
action="store", dest="disc_template",
help="template for disc file naming (default %s)" % default,
default=default)
default = 'flac'
self.parser.add_option('', '--profile',
action="store", dest="profile",
help="profile for encoding (default '%s', choices '%s')" % (
default, "', '".join(encode.PROFILES.keys())),
default=default)
def do(self, args):
@@ -248,6 +256,8 @@ class Rip(logcommand.LogCommand):
itable.getAccurateRipURL(), ittoc.getAccurateRipURL())
outdir = self.options.output_directory or os.getcwd()
profile = encode.PROFILES[self.options.profile]
extension = profile.extension
# check for hidden track one audio
htoapath = None
@@ -264,7 +274,7 @@ class Rip(logcommand.LogCommand):
print 'Found Hidden Track One Audio from frame %d to %d' % (start, stop)
# rip it
htoapath = getPath(outdir, self.options.track_template, metadata, 0) + '.wav'
htoapath = getPath(outdir, self.options.track_template, metadata, 0) + '.' + extension
dirname = os.path.dirname(htoapath)
if not os.path.exists(dirname):
os.makedirs(dirname)
@@ -275,12 +285,17 @@ class Rip(logcommand.LogCommand):
t = cdparanoia.ReadVerifyTrackTask(htoapath, ittoc,
start, stop - 1,
offset=int(self.options.offset),
device=self.parentCommand.options.device)
device=self.parentCommand.options.device,
profile=self.options.profile)
function(runner, t)
if t.checksum is not None:
print 'Checksums match for track %d' % 0
else:
print 'ERROR: checksums did not match for track %d' % 0
print 'Peak level: %.2f %%' % (math.sqrt(t.peak) * 100.0, )
if t.peak == 0.0:
print 'HTOA is completely silent'
# overlay this rip onto the Table
itable.setFile(1, 0, htoapath, htoalength, 0)
@@ -292,7 +307,7 @@ class Rip(logcommand.LogCommand):
track.indexes[1].relative = 0
continue
path = getPath(outdir, self.options.track_template, metadata, i + 1) + '.wav'
path = getPath(outdir, self.options.track_template, metadata, i + 1) + '.' + extension
dirname = os.path.dirname(path)
if not os.path.exists(dirname):
os.makedirs(dirname)
@@ -304,13 +319,15 @@ class Rip(logcommand.LogCommand):
ittoc.getTrackStart(i + 1),
ittoc.getTrackEnd(i + 1),
offset=int(self.options.offset),
device=self.parentCommand.options.device)
device=self.parentCommand.options.device,
profile=self.options.profile)
t.description = 'Reading Track %d' % (i + 1)
function(runner, t)
if t.checksum:
print 'Checksums match for track %d' % (i + 1)
else:
print 'ERROR: checksums did not match for track %d' % (i + 1)
print 'Peak level: %.2f %%' % (math.sqrt(t.peak) * 100.0, )
# overlay this rip onto the Table
itable.setFile(i + 1, 1, path, ittoc.getTrackLength(i + 1), i + 1)
@@ -340,7 +357,7 @@ class Rip(logcommand.LogCommand):
handle.write('%s\n' % os.path.basename(htoapath))
for i, track in enumerate(itable.tracks):
path = getPath(outdir, self.options.track_template, metadata, i) + '.wav'
path = getPath(outdir, self.options.track_template, metadata, i) + '.' + extension
handle.write('#EXTINF:%d,%s\n' % (
itable.getTrackLength(i + 1) / common.FRAMES_PER_SECOND,
os.path.basename(path)))