source: trunk/morituri/common/encode.py @ 438

Revision 438, 16.4 KB checked in by thomas, 2 years ago (diff)
  • morituri/program/cdparanoia.py:
  • morituri/common/encode.py:
  • morituri/common/program.py:
  • morituri/rip/cd.py: Add action and what args to describe task better.
Line 
1# -*- Mode: Python; test-case-name: morituri.test.test_common_encode -*-
2# vi:si:et:sw=4:sts=4:ts=4
3
4# Morituri - for those about to RIP
5
6# Copyright (C) 2009 Thomas Vander Stichele
7
8# This file is part of morituri.
9#
10# morituri is free software: you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation, either version 3 of the License, or
13# (at your option) any later version.
14#
15# morituri is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with morituri.  If not, see <http://www.gnu.org/licenses/>.
22
23import math
24import os
25import shutil
26import tempfile
27
28from morituri.common import common, task
29
30class Profile(object):
31    name = None
32    extension = None
33    pipeline = None
34    losless = None
35
36    def test(self):
37        """
38        Test if this profile will work.
39        Can check for elements, ...
40        """
41        pass
42
43class FlacProfile(Profile):
44    name = 'flac'
45    extension = 'flac'
46    pipeline = 'flacenc name=tagger quality=8'
47    lossless = True
48
49    # FIXME: we should do something better than just printing ERRORS
50    def test(self):
51
52        # here to avoid import gst eating our options
53        import gst
54
55        plugin = gst.registry_get_default().find_plugin('flac')
56        if not plugin:
57            print 'ERROR: cannot find flac plugin'
58            return False
59
60        versionTuple = tuple([int(x) for x in plugin.get_version().split('.')])
61        if len(versionTuple) < 4:
62            versionTuple = versionTuple + (0, )
63        if versionTuple > (0, 10, 9, 0) and versionTuple <= (0, 10, 15, 0):
64            print 'ERROR: flacenc between 0.10.9 and 0.10.15 has a bug'
65            return False
66
67        return True
68
69class AlacProfile(Profile):
70    name = 'alac'
71    extension = 'alac'
72    pipeline = 'ffenc_alac name=tagger'
73    lossless = True
74
75# FIXME: wavenc does not have merge_tags
76class WavProfile(Profile):
77    name = 'wav'
78    extension = 'wav'
79    pipeline = 'wavenc'
80    lossless = True
81
82class WavpackProfile(Profile):
83    name = 'wavpack'
84    extension = 'wv'
85    pipeline = 'wavpackenc bitrate=0 name=tagger'
86    lossless = True
87
88class MP3Profile(Profile):
89    name = 'mp3'
90    extension = 'mp3'
91    pipeline = 'lame name=tagger quality=0 ! id3v2mux'
92    lossless = False
93
94class MP3VBRProfile(Profile):
95    name = 'mp3vbr'
96    extension = 'mp3'
97    pipeline = 'lame name=tagger quality=0 vbr=new vbr-mean-bitrate=192 ! id3v2mux'
98    lossless = False
99
100
101class VorbisProfile(Profile):
102    name = 'vorbis'
103    extension = 'oga'
104    pipeline = 'audioconvert ! vorbisenc name=tagger ! oggmux'
105    lossless = False
106
107
108PROFILES = {
109    'wav':     WavProfile,
110    'flac':    FlacProfile,
111    'alac':    AlacProfile,
112    'wavpack': WavpackProfile,
113}
114
115LOSSY_PROFILES = {
116    'mp3':     MP3Profile,
117    'mp3vbr':  MP3VBRProfile,
118    'vorbis':  VorbisProfile,
119}
120
121ALL_PROFILES = PROFILES.copy()
122ALL_PROFILES.update(LOSSY_PROFILES)
123
124class EncodeTask(task.Task):
125    """
126    I am a task that encodes a .wav file.
127    I set tags too.
128    I also calculate the peak level of the track.
129
130    @param peak: the peak volume, from 0.0 to 1.0.  This is the sqrt of the
131                 peak power.
132    @type  peak: float
133    """
134
135    logCategory = 'EncodeTask'
136
137    description = 'Encoding'
138    peak = None
139
140    def __init__(self, inpath, outpath, profile, taglist=None, what="track"):
141        """
142        @param profile: encoding profile
143        @type  profile: L{Profile}
144        """
145        assert type(inpath) is unicode, "inpath %r is not unicode" % inpath
146        assert type(outpath) is unicode, \
147            "outpath %r is not unicode" % outpath
148       
149        self._inpath = inpath
150        self._outpath = outpath
151        self._taglist = taglist
152
153        self._level = None
154        self._peakdB = None
155        self._profile = profile
156
157        self.description = "Encoding %s" % what
158        self._profile.test()
159
160    def start(self, runner):
161        task.Task.start(self, runner)
162
163        # here to avoid import gst eating our options
164        import gst
165
166        desc = '''
167            filesrc location="%s" !
168            decodebin name=decoder !
169            audio/x-raw-int,width=16,depth=16,channels=2 !
170            level name=level !
171            %s !
172            filesink location="%s" name=sink''' % (
173                common.quoteParse(self._inpath).encode('utf-8'),
174                self._profile.pipeline,
175                common.quoteParse(self._outpath).encode('utf-8'))
176
177        self.debug('creating pipeline: %r', desc)
178        self._pipeline = gst.parse_launch(desc)
179
180        tagger = self._pipeline.get_by_name('tagger')
181
182        # set tags
183        if tagger and self._taglist:
184            # FIXME: under which conditions do we not have merge_tags ?
185            # See for example comment saying wavenc did not have it.
186            try:
187                tagger.merge_tags(self._taglist, gst.TAG_MERGE_APPEND)
188            except AttributeError, e:
189                self.warning('Could not merge tags: %r',
190                    log.getExceptionMessage(e))
191
192        self.debug('pausing pipeline')
193        self._pipeline.set_state(gst.STATE_PAUSED)
194        self._pipeline.get_state()
195        self.debug('paused pipeline')
196
197        # get length
198        self.debug('query duration')
199        try:
200            length, qformat = tagger.query_duration(gst.FORMAT_DEFAULT)
201        except gst.QueryError, e:
202            self.setException(e)
203            self.stop()
204            return
205
206        # wavparse 0.10.14 returns in bytes
207        if qformat == gst.FORMAT_BYTES:
208            self.debug('query returned in BYTES format')
209            length /= 4
210        self.debug('total length: %r', length)
211        self._length = length
212
213        # add eos handling
214        bus = self._pipeline.get_bus()
215        bus.add_signal_watch()
216        bus.connect('message::eos', self._message_eos_cb)
217
218        # set up level callbacks
219        bus.connect('message::element', self._message_element_cb)
220        self._level = self._pipeline.get_by_name('level')
221        # add a probe so we can track progress
222        # we connect to level because this gives us offset in samples
223        srcpad = self._level.get_static_pad('src')
224        srcpad.add_buffer_probe(self._probe_handler)
225
226        self.debug('scheduling setting to play')
227        # since set_state returns non-False, adding it as timeout_add
228        # will repeatedly call it, and block the main loop; so
229        #   gobject.timeout_add(0L, self._pipeline.set_state, gst.STATE_PLAYING)
230        # would not work.
231
232        def play():
233            self._pipeline.set_state(gst.STATE_PLAYING)
234            return False
235        self.runner.schedule(0, play)
236
237        #self._pipeline.set_state(gst.STATE_PLAYING)
238        self.debug('scheduled setting to play')
239
240    def _probe_handler(self, pad, buffer):
241        # update progress based on buffer offset (expected to be in samples)
242        # versus length in samples
243        # marshal to main thread
244        self.runner.schedule(0, self.setProgress,
245            float(buffer.offset) / self._length)
246
247        # don't drop the buffer
248        return True
249
250    def _message_eos_cb(self, bus, message):
251        self.debug('eos, scheduling stop')
252        self.runner.schedule(0, self.stop)
253
254    def _message_element_cb(self, bus, message):
255        if message.src != self._level:
256            return
257
258        s = message.structure
259        if s.get_name() != 'level':
260            return
261
262
263        if self._peakdB is None:
264            self._peakdB = s['peak'][0]
265
266        for p in s['peak']:
267            if self._peakdB < p:
268                self.log('higher peakdB found, now %r', self._peakdB)
269                self._peakdB = p
270
271    def stop(self):
272        # here to avoid import gst eating our options
273        import gst
274
275        self.debug('stopping')
276        self.debug('setting state to NULL')
277        self._pipeline.set_state(gst.STATE_NULL)
278        self.debug('set state to NULL')
279        # FIXME: maybe this should move lower ? If used by BaseMultiTask,
280        # this starts the next task without showing us the peakdB
281        task.Task.stop(self)
282
283        if self._peakdB is not None:
284            self.debug('peakdB %r', self._peakdB)
285            self.peak = math.sqrt(math.pow(10, self._peakdB / 10.0))
286        else:
287            self.warning('No peak found, something went wrong!')
288
289class TagReadTask(task.Task):
290    """
291    I am a task that reads tags.
292
293    @ivar  taglist: the tag list read from the file.
294    @type  taglist: L{gst.TagList}
295    """
296
297    logCategory = 'TagReadTask'
298
299    description = 'Reading tags'
300
301    taglist = None
302
303    def __init__(self, path):
304        """
305        """
306        assert type(path) is unicode, "path %r is not unicode" % path
307       
308        self._path = path
309
310    def start(self, runner):
311        task.Task.start(self, runner)
312
313        # here to avoid import gst eating our options
314        import gst
315
316        self._pipeline = gst.parse_launch('''
317            filesrc location="%s" !
318            decodebin name=decoder !
319            fakesink''' % (
320                common.quoteParse(self._path).encode('utf-8')))
321
322        self.debug('pausing pipeline')
323        self._pipeline.set_state(gst.STATE_PAUSED)
324        self._pipeline.get_state()
325        self.debug('paused pipeline')
326
327        # add eos handling
328        bus = self._pipeline.get_bus()
329        bus.add_signal_watch()
330        bus.connect('message::eos', self._message_eos_cb)
331
332        # set up tag callbacks
333        bus.connect('message::tag', self._message_tag_cb)
334
335        self.debug('scheduling setting to play')
336        # since set_state returns non-False, adding it as timeout_add
337        # will repeatedly call it, and block the main loop; so
338        #   gobject.timeout_add(0L, self._pipeline.set_state, gst.STATE_PLAYING)
339        # would not work.
340
341        def play():
342            self._pipeline.set_state(gst.STATE_PLAYING)
343            return False
344        self.runner.schedule(0, play)
345
346        #self._pipeline.set_state(gst.STATE_PLAYING)
347        self.debug('scheduled setting to play')
348
349    def _message_eos_cb(self, bus, message):
350        self.debug('eos, scheduling stop')
351        self.runner.schedule(0, self.stop)
352
353    def _message_tag_cb(self, bus, message):
354        taglist = message.parse_tag()
355        self.taglist = taglist
356
357    def stop(self):
358        # here to avoid import gst eating our options
359        import gst
360
361        self.debug('stopping')
362        self.debug('setting state to NULL')
363        self._pipeline.set_state(gst.STATE_NULL)
364        self.debug('set state to NULL')
365        task.Task.stop(self)
366
367class TagWriteTask(task.Task):
368    """
369    I am a task that retags an encoded file.
370    """
371
372    logCategory = 'TagWriteTask'
373
374    description = 'Writing tags'
375
376    def __init__(self, inpath, outpath, taglist=None):
377        """
378        """
379        assert type(inpath) is unicode, "inpath %r is not unicode" % inpath
380        assert type(outpath) is unicode, "outpath %r is not unicode" % outpath
381       
382        self._inpath = inpath
383        self._outpath = outpath
384        self._taglist = taglist
385
386    def start(self, runner):
387        task.Task.start(self, runner)
388
389        # here to avoid import gst eating our options
390        import gst
391
392        self._pipeline = gst.parse_launch('''
393            filesrc location="%s" !
394            flactag name=tagger !
395            filesink location="%s"''' % (
396                common.quoteParse(self._inpath).encode('utf-8'),
397                common.quoteParse(self._outpath).encode('utf-8')))
398
399        # set tags
400        tagger = self._pipeline.get_by_name('tagger')
401        if self._taglist:
402            tagger.merge_tags(self._taglist, gst.TAG_MERGE_APPEND)
403
404        self.debug('pausing pipeline')
405        self._pipeline.set_state(gst.STATE_PAUSED)
406        self._pipeline.get_state()
407        self.debug('paused pipeline')
408
409        # add eos handling
410        bus = self._pipeline.get_bus()
411        bus.add_signal_watch()
412        bus.connect('message::eos', self._message_eos_cb)
413
414        self.debug('scheduling setting to play')
415        # since set_state returns non-False, adding it as timeout_add
416        # will repeatedly call it, and block the main loop; so
417        #   gobject.timeout_add(0L, self._pipeline.set_state, gst.STATE_PLAYING)
418        # would not work.
419
420        def play():
421            self._pipeline.set_state(gst.STATE_PLAYING)
422            return False
423        self.runner.schedule(0, play)
424
425        #self._pipeline.set_state(gst.STATE_PLAYING)
426        self.debug('scheduled setting to play')
427
428    def _message_eos_cb(self, bus, message):
429        self.debug('eos, scheduling stop')
430        self.runner.schedule(0, self.stop)
431
432    def stop(self):
433        # here to avoid import gst eating our options
434        import gst
435
436        self.debug('stopping')
437        self.debug('setting state to NULL')
438        self._pipeline.set_state(gst.STATE_NULL)
439        self.debug('set state to NULL')
440        task.Task.stop(self)
441
442class SafeRetagTask(task.MultiSeparateTask):
443    """
444    I am a task that retags an encoded file safely in place.
445    First of all, if the new tags are the same as the old ones, it doesn't
446    do anything.
447    If the tags are not the same, then the file gets retagged, but only
448    if the decodes of the original and retagged file checksum the same.
449
450    @ivar changed: True if the tags have changed (and hence an output file is
451                   generated)
452    """
453
454    logCategory = 'SafeRetagTask'
455
456    description = 'Retagging'
457
458    changed = False
459
460    def __init__(self, path, taglist=None):
461        """
462        """
463        assert type(path) is unicode, "path %r is not unicode" % path
464
465        task.MultiSeparateTask.__init__(self)
466       
467        self._path = path
468        self._taglist = taglist.copy()
469
470        self.tasks = [TagReadTask(path), ]
471
472    def stopped(self, taskk):
473        from morituri.common import checksum
474
475        if not taskk.exception:
476            # Check if the tags are different or not
477            if taskk == self.tasks[0]:
478                taglist = taskk.taglist.copy()
479                if common.tagListEquals(taglist, self._taglist):
480                    self.debug('tags are already fine: %r',
481                        common.tagListToDict(taglist))
482                else:
483                    # need to retag
484                    self.debug('tags need to be rewritten')
485                    self.debug('Current tags: %r, new tags: %r',
486                        common.tagListToDict(taglist),
487                        common.tagListToDict(self._taglist))
488                    assert common.tagListToDict(taglist) != common.tagListToDict(self._taglist)
489                    self.tasks.append(checksum.CRC32Task(self._path))
490                    self._fd, self._tmppath = tempfile.mkstemp(
491                        dir=os.path.dirname(self._path), suffix=u'.morituri')
492                    self.tasks.append(TagWriteTask(self._path,
493                        self._tmppath, self._taglist))
494                    self.tasks.append(checksum.CRC32Task(self._tmppath))
495                    self.tasks.append(TagReadTask(self._tmppath))
496            elif len(self.tasks) > 1 and taskk == self.tasks[4]:
497                if common.tagListEquals(self.tasks[4].taglist, self._taglist):
498                    self.debug('tags written successfully')
499                    c1 = self.tasks[1].checksum
500                    c2 = self.tasks[3].checksum
501                    self.debug('comparing checksums %08x and %08x' % (c1, c2))
502                    if c1 == c2:
503                        # data is fine, so we can now move
504                        # but first, copy original mode to our temporary file
505                        shutil.copymode(self._path, self._tmppath)
506                        self.debug('moving temporary file to %r' % self._path)
507                        os.rename(self._tmppath, self._path)
508                        self.changed = True
509                    else:
510                        # FIXME: don't raise TypeError
511                        e = TypeError("Checksums failed")
512                        self.setAndRaiseException(e)
513                else:
514                    self.debug('failed to update tags, only have %r',
515                        common.tagListToDict(self.tasks[4].taglist))
516                    os.unlink(self._tmppath)
517                    e = TypeError("Tags not written")
518                    self.setAndRaiseException(e)
519                   
520        task.MultiSeparateTask.stopped(self, taskk)
521
522     
Note: See TracBrowser for help on using the repository browser.