The attached patch implements a "working" autoaligner. It works well on my one test case. I encourage you to try it out and see if it works for you. When you select a number of timeline objects, it aligns them to the one with highest priority. It works by looking at audio tracks, so timeline objects without audio will be skipped. (Video with an associated audio track should work.)
The patch is definitely not ready to deploy, especially because it still contains a workaround for a race condition using time.sleep(). I'd fix it, but it's time for me to sleep. Notes: The process is much slower than I expected, but I have no idea how to profile pitivi to find the bottleneck. I also have no idea how to provide the user with a progress indicator. I call timeline.disableUpdates() but this doesn't seem to have any user-visible effect. --Ben
diff --git a/data/pixmaps/Makefile.am b/data/pixmaps/Makefile.am index bfb9a12..e3283c3 100644 --- a/data/pixmaps/Makefile.am +++ b/data/pixmaps/Makefile.am @@ -19,6 +19,8 @@ pixmap_DATA = \ pitivi-split.svg \ pitivi-ungroup-24.svg \ pitivi-ungroup.svg \ + pitivi-align-24.svg \ + pitivi-align.svg \ pitivi-unlink-24.svg \ pitivi-unlink.svg \ pitivi-video.png \ diff --git a/data/pixmaps/pitivi-align-24.svg b/data/pixmaps/pitivi-align-24.svg new file mode 100644 index 0000000..1e30764 --- /dev/null +++ b/data/pixmaps/pitivi-align-24.svg @@ -0,0 +1,224 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="24" + height="24" + id="svg2" + sodipodi:version="0.32" + inkscape:version="0.47 r22583" + sodipodi:modified="true" + version="1.0" + sodipodi:docname="pitivi-group-24.svg" + inkscape:output_extension="org.inkscape.output.svg.inkscape"> + <defs + id="defs4"> + <linearGradient + id="linearGradient3174"> + <stop + style="stop-color:#73d216;stop-opacity:1;" + offset="0" + id="stop3176" /> + <stop + style="stop-color:#73d216;stop-opacity:0.51127821;" + offset="1" + id="stop3178" /> + </linearGradient> + <linearGradient + id="linearGradient3166"> + <stop + style="stop-color:#3465a4;stop-opacity:1;" + offset="0" + id="stop3168" /> + <stop + style="stop-color:#3465a4;stop-opacity:0.52549022;" + offset="1" + id="stop3170" /> + </linearGradient> + <inkscape:perspective + sodipodi:type="inkscape:persp3d" + inkscape:vp_x="0 : 24 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="48 : 24 : 1" + inkscape:persp3d-origin="24 : 16 : 1" + id="perspective7" /> + <linearGradient + gradientTransform="matrix(1.004639,0,0,1,-1.037685,4.7681e-2)" + gradientUnits="userSpaceOnUse" + y2="40.231434" + x2="34.744495" + y1="10.445395" + x1="17.498823" + id="linearGradient5315" + xlink:href="#linearGradient5113" + inkscape:collect="always" /> + <radialGradient + r="8.0625" + fy="19.03125" + fx="11.25" + cy="19.03125" + cx="11.25" + gradientTransform="matrix(1,0,0,0.282946,0,13.64644)" + gradientUnits="userSpaceOnUse" + id="radialGradient4354" + xlink:href="#linearGradient5105" + inkscape:collect="always" /> + <linearGradient + id="linearGradient5113" + inkscape:collect="always"> + <stop + id="stop5115" + offset="0" + style="stop-color:white;stop-opacity:1;" /> + <stop + id="stop5117" + offset="1" + style="stop-color:white;stop-opacity:0;" /> + </linearGradient> + <linearGradient + id="linearGradient5105" + inkscape:collect="always"> + <stop + id="stop5107" + offset="0" + style="stop-color:black;stop-opacity:1;" /> + <stop + id="stop5109" + offset="1" + style="stop-color:black;stop-opacity:0;" /> + </linearGradient> + <inkscape:perspective + id="perspective3181" + inkscape:persp3d-origin="24 : 16 : 1" + inkscape:vp_z="48 : 24 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_x="0 : 24 : 1" + sodipodi:type="inkscape:persp3d" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient5113" + id="linearGradient3199" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0,0.2905223,-0.2891808,0,18.630492,26.384583)" + x1="17.498823" + y1="10.445395" + x2="34.744495" + y2="40.231434" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient5105" + id="radialGradient3204" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,0.282946,0,13.64644)" + cx="11.25" + cy="19.03125" + fx="11.25" + fy="19.03125" + r="8.0625" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3166" + id="linearGradient3172" + x1="1" + y1="8.4946384" + x2="23" + y2="8.4946384" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3174" + id="linearGradient3180" + x1="1" + y1="15.450444" + x2="23.0625" + y2="15.450444" + gradientUnits="userSpaceOnUse" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="32" + inkscape:cx="20.915634" + inkscape:cy="11.335195" + inkscape:document-units="px" + inkscape:current-layer="layer1" + width="48px" + height="48px" + borderlayer="true" + inkscape:showpageshadow="false" + showgrid="true" + inkscape:window-width="1440" + inkscape:window-height="872" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1"> + <inkscape:grid + type="xygrid" + id="grid2380" + visible="true" + enabled="true" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Calque 1" + inkscape:groupmode="layer" + id="layer1"> + <rect + style="opacity:1;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#fce94f;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + id="rect3231" + width="22.974112" + height="14.856694" + x="0.5" + y="4.625" /> + <rect + style="opacity:1;fill:url(#linearGradient3172);fill-opacity:1;fill-rule:evenodd;stroke:#204a87;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + id="rect2382" + width="21" + height="5.9375" + x="1.5" + y="5.5258884" /> + <rect + style="opacity:1;fill:url(#linearGradient3180);fill-opacity:1;fill-rule:evenodd;stroke:#4e9a06;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + id="rect3162" + width="21.0625" + height="6.0625" + x="1.5" + y="12.419194" /> + <path + style="fill:#555753;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:1.00000048;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible" + d="M 7.0865102,22.702 C 8.91839,21.110925 12.75026,17.519849 14.58214,15.928774 c -1.83188,-1.5831 -5.66375,-5.1662 -7.4956298,-6.7492997 0,0.7586018 0,2.4547037 0,3.2133057 -1.4455,0 -2.14101,0 -3.58651,0 0,1.644005 0,5.475511 0,7.119517 1.4455,0 2.14101,0 3.58651,0 0,0.771567 0,2.418135 0,3.189703 z" + id="path4348" + sodipodi:nodetypes="cccccccc" /> + <use + x="0" + y="0" + xlink:href="#path4348" + id="use3209" + transform="matrix(-0.99999496,0,0,-1,24.082133,23.979474)" + width="22" + height="22" /> + </g> +</svg> diff --git a/data/pixmaps/pitivi-align.svg b/data/pixmaps/pitivi-align.svg new file mode 100644 index 0000000..3ebe1a5 --- /dev/null +++ b/data/pixmaps/pitivi-align.svg @@ -0,0 +1,228 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="22" + height="22" + id="svg2" + sodipodi:version="0.32" + inkscape:version="0.47 r22583" + sodipodi:modified="true" + version="1.0" + sodipodi:docname="pitivi-group.svg" + inkscape:output_extension="org.inkscape.output.svg.inkscape"> + <defs + id="defs4"> + <linearGradient + id="linearGradient3175"> + <stop + style="stop-color:#73d216;stop-opacity:1;" + offset="0" + id="stop3177" /> + <stop + style="stop-color:#73d216;stop-opacity:0.51127821;" + offset="1" + id="stop3179" /> + </linearGradient> + <linearGradient + id="linearGradient3167"> + <stop + style="stop-color:#3465a4;stop-opacity:1;" + offset="0" + id="stop3169" /> + <stop + style="stop-color:#3465a4;stop-opacity:0.51127821;" + offset="1" + id="stop3171" /> + </linearGradient> + <inkscape:perspective + sodipodi:type="inkscape:persp3d" + inkscape:vp_x="0 : 24 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="48 : 24 : 1" + inkscape:persp3d-origin="24 : 16 : 1" + id="perspective7" /> + <linearGradient + gradientTransform="matrix(1.004639,0,0,1,-1.037685,4.7681e-2)" + gradientUnits="userSpaceOnUse" + y2="40.231434" + x2="34.744495" + y1="10.445395" + x1="17.498823" + id="linearGradient5315" + xlink:href="#linearGradient5113" + inkscape:collect="always" /> + <radialGradient + r="8.0625" + fy="19.03125" + fx="11.25" + cy="19.03125" + cx="11.25" + gradientTransform="matrix(1,0,0,0.282946,0,13.64644)" + gradientUnits="userSpaceOnUse" + id="radialGradient4354" + xlink:href="#linearGradient5105" + inkscape:collect="always" /> + <linearGradient + id="linearGradient5113" + inkscape:collect="always"> + <stop + id="stop5115" + offset="0" + style="stop-color:white;stop-opacity:1;" /> + <stop + id="stop5117" + offset="1" + style="stop-color:white;stop-opacity:0;" /> + </linearGradient> + <linearGradient + id="linearGradient5105" + inkscape:collect="always"> + <stop + id="stop5107" + offset="0" + style="stop-color:black;stop-opacity:1;" /> + <stop + id="stop5109" + offset="1" + style="stop-color:black;stop-opacity:0;" /> + </linearGradient> + <inkscape:perspective + id="perspective3181" + inkscape:persp3d-origin="24 : 16 : 1" + inkscape:vp_z="48 : 24 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_x="0 : 24 : 1" + sodipodi:type="inkscape:persp3d" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient5113" + id="linearGradient3199" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0,0.2905223,-0.2891808,0,18.630492,26.384583)" + x1="17.498823" + y1="10.445395" + x2="34.744495" + y2="40.231434" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient5105" + id="radialGradient3204" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,0.282946,0,13.64644)" + cx="11.25" + cy="19.03125" + fx="11.25" + fy="19.03125" + r="8.0625" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3167" + id="linearGradient3173" + x1="1" + y1="8" + x2="21.0625" + y2="8" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3175" + id="linearGradient3181" + x1="1.0625" + y1="14.0625" + x2="21.0625" + y2="14.0625" + gradientUnits="userSpaceOnUse" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="32" + inkscape:cx="6.1600473" + inkscape:cy="11.838985" + inkscape:document-units="px" + inkscape:current-layer="layer1" + width="48px" + height="48px" + borderlayer="true" + inkscape:showpageshadow="false" + showgrid="true" + inkscape:window-width="1276" + inkscape:window-height="867" + inkscape:window-x="57" + inkscape:window-y="0" + inkscape:snap-grids="false" + inkscape:snap-to-guides="false" + inkscape:window-maximized="0"> + <inkscape:grid + type="xygrid" + id="grid2380" + visible="true" + enabled="true" + empspacing="5" + snapvisiblegridlinesonly="true" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Calque 1" + inkscape:groupmode="layer" + id="layer1"> + <rect + style="fill:none;stroke:#fce94f;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + id="rect3231" + width="20.96875" + height="12.9375" + x="0.53125" + y="4.53125" /> + <rect + style="fill:url(#linearGradient3173);fill-opacity:1;fill-rule:evenodd;stroke:#204a87;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + id="rect2382" + width="19.03125" + height="5.0625" + x="1.53125" + y="5.5" /> + <rect + style="fill:url(#linearGradient3181);fill-opacity:1;fill-rule:evenodd;stroke:#4e9a06;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + id="rect3162" + width="19" + height="4.9375" + x="1.53125" + y="11.5625" /> + <path + style="fill:#555753;fill-opacity:1;fill-rule:nonzero;stroke:#2e3436;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible" + d="m 6.510526,18.485817 5.714383,-4.491976 -5.683133,-4.40555 0,1.900806 -2.992759,0 0,5.057017 2.961509,-0.03125 0,1.970953 z" + id="path4348" + sodipodi:nodetypes="cccccccc" /> + <use + x="0" + y="0" + xlink:href="#path4348" + id="use3209" + transform="matrix(-1,0,0,-1,22.023926,22.042858)" + width="22" + height="22" /> + </g> +</svg> diff --git a/pitivi/timeline/align.py b/pitivi/timeline/align.py new file mode 100644 index 0000000..6bbe794 --- /dev/null +++ b/pitivi/timeline/align.py @@ -0,0 +1,131 @@ +import numpy +from pitivi.timeline.extract import Extractee, RandomAccessAudioExtractor +from pitivi.stream import AudioStream +from pitivi.log.loggable import Loggable + +def nextpow2(n): + i = 1 + while i < n: + i *= 2 + return i + +class EnvelopeExtractee(Extractee, Loggable): + """ Class that computes the envelope of a 1-D signal + (presumably audio). The envelope is computed incrementally, + so that the entire signal does not ever need to be stored. + + The envelope is defined as the sum of the absolute value of the signal + over a block.""" + + def __init__(self, blocksize, callback, *cbargs): + """ + @param blocksize: the number of samples in a block + @type blocksize: L{int} + @param callback: a function to call when the extraction is complete. + The function's first argument will be a numpy array representing + the envelope, and any later argument to this function will be passed + as subsequent arguments to callback. + """ + Loggable.__init__(self) + self._blocksize = blocksize + self._cb = callback + self._cbargs = cbargs + self._chunks = [] + self._leftover = numpy.zeros((0,)) + + def receive(self, a): + if len(self._leftover) > 0: + a = numpy.concatenate((self._leftover, a)) + lol = len(a) % self._blocksize + if lol > 0: + self._leftover = a[-lol:] + a = a[:-lol] + else: + self._leftover = numpy.zeros((0,)) + a = numpy.abs(a).reshape((len(a)//self._blocksize, self._blocksize)) + a = numpy.sum(a,1) + self._chunks.append(a) + self.debug("Chunk %i has size %i" % (len(self._chunks), len(a))) + + def finalize(self): + self.debug("Finalizing %i chunks" % len(self._chunks)) + a = numpy.concatenate(self._chunks) + self._cb(a, *self._cbargs) + +class AutoAligner(Loggable): + """ Class for aligning a set of L{TimelineObject}s automatically based on + their contents. """ + BLOCKRATE = 25 + + @staticmethod + def _getAudioTrack(to): + for track in to.track_objects: + if track.stream_type == AudioStream: + return track + return None + + def __init__(self, tobjects, callback): + """ + @param tobjects: an iterable of L{TimelineObject}s. + In this implementation, only L{TimelineObject}s with at least one + audio track will be aligned. + @type tobjects: iter(L{TimelineObject}) + @param callback: A function to call when alignment is complete. No + arguments will be provided. + @type callback: function + """ + Loggable.__init__(self) + self._tos = dict.fromkeys(tobjects) + #values are implicitly None. The values will be replaced by the + #envelopes as envelopes are computed + self._callback = callback + + def _envelopeCb(self, array, to): + self.debug("Receiving envelope for %s" % to) + self._tos[to] = array + if not None in self._tos.itervalues(): + self._performShifts() + + def start(self): + for to in self._tos.iterkeys(): + a = self._getAudioTrack(to) + if a is None: + self._tos.remove(to) + continue + blocksize = a.stream.rate//self.BLOCKRATE + e = EnvelopeExtractee(blocksize, self._envelopeCb, to) + r = RandomAccessAudioExtractor(a.factory, a.stream) + r.extract(e, a.in_point, a.out_point - a.in_point) + + def _chooseTemplate(self): + def priority(to): return to.priority + return min(self._tos.iterkeys(), key=priority) + + def _performShifts(self): + self.debug("performing shifts") + template = self._chooseTemplate() + tenv = self._tos.pop(template) + #L is the maximum size of a cross-correlation + L = len(tenv) + max(len(e) for e in self._tos.itervalues()) - 1 + L = nextpow2(L) + tenv -= numpy.mean(tenv) + tenv = numpy.fft.rfft(tenv, L).conj() + for movable, menv in self._tos.iteritems(): + # Estimates the relative shift between movable and template + # by locating the maximum of the cross-correlation of their + # (mean-subtracted) envelopes. + menv -= numpy.mean(menv) + xcorr = numpy.fft.irfft(tenv*numpy.fft.rfft(menv, L)) + p = numpy.argmax(xcorr) + if p > L - len(menv): + p -= L + tshift = (int(p) * int(1e9))//self.BLOCKRATE + self.debug("Shifting %s to %i ns from %i" + % (movable, tshift, template.start)) + newstart = template.start - tshift + if newstart >= 0: + movable.start = newstart + else: + movable.start = 0 + movable.in_point = movable.in_point - newstart + self._callback() diff --git a/pitivi/timeline/extract.py b/pitivi/timeline/extract.py new file mode 100644 index 0000000..dade21f --- /dev/null +++ b/pitivi/timeline/extract.py @@ -0,0 +1,150 @@ +import gst +from pitivi.elements.singledecodebin import SingleDecodeBin +from pitivi.elements.arraysink import ArraySink +from pitivi.log.loggable import Loggable +import pitivi.utils as utils + +class Extractee: + """ Abstract base class for objects that receive raw data from an + L{Extractor}.""" + + def receive(self, array): + """ Receive a chunk of data from an Extractor. + + @param array: The chunk of data as an array + @type array: any kind of numeric array + """ + raise NotImplementedError + + def finalize(self): + """ Indicates that the extraction is complete, so the Extractee should + process the data it has received. """ + raise NotImplementedError + +class Extractor(Loggable): + """ Abstract base class for extraction of raw data from a stream. + Closely modeled on Previewer. """ + + def __init__(self, factory, stream_): + """ Create a new Extractor. + + @param factory: the factory with which to decode the stream + @type factory: L{ObjectFactory} + @param stream_: the stream to decode + @type stream_: L{Stream} + """ + Loggable.__init__(self) + self.debug("Initialized with %s %s" % (factory, stream_)) + + def extract(self, e, start, duration): + """ Extract the raw data corresponding to a segment of the stream. + + @param e: the L{Extractee} that will receive the raw data + @type e: L{Extractee} + @param start: The point in the stream at which the segment starts (nanoseconds) + @type start: L{long} + @param duration: The duration of the segment (nanoseconds) + @type duration: L{long}""" + raise NotImplementedError + +class RandomAccessExtractor(Extractor): + """ Abstract class for L{Extractor}s of random access streams, closely + inspired by L{RandomAccessPreviewer}.""" + def __init__(self, factory, stream_): + Extractor.__init__(self, factory, stream_) + # FIXME: + # why doesn't this work? + # bin = factory.makeBin(stream_) + uri = factory.uri + caps = stream_.caps + bin = SingleDecodeBin(uri=uri, caps=caps, stream=stream_) + + self._pipelineInit(factory, bin) + + def _pipelineInit(self, factory, bin): + """Create the pipeline for the preview process. Subclasses should + override this method and create a pipeline, connecting to callbacks to + the appropriate signals, and prerolling the pipeline if necessary.""" + raise NotImplementedError + +class RandomAccessAudioExtractor(RandomAccessExtractor): + """L{Extractor} for random access audio streams, closely + inspired by L{RandomAccessAudioPreviewer}.""" + + def __init__(self, factory, stream_): + self.tdur = 30 * gst.SECOND + self._queue = [] + RandomAccessExtractor.__init__(self, factory, stream_) + + def _pipelineInit(self, factory, sbin): + self.spacing = 0 + + self.audioSink = ArraySink() + conv = gst.element_factory_make("audioconvert") + self.audioPipeline = utils.pipeline({ + sbin : conv, + conv : self.audioSink, + self.audioSink : None}) + bus = self.audioPipeline.get_bus() + bus.add_signal_watch() + bus.connect("message::segment-done", self._busMessageSegmentDoneCb) + bus.connect("message::error", self._busMessageErrorCb) + + self._audio_cur = None + self.audioPipeline.set_state(gst.STATE_PAUSED) + import time; time.sleep(5) + + def _busMessageSegmentDoneCb(self, bus, message): + self.debug("segment done") + self._finishSegment() + + def _busMessageErrorCb(self, bus, message): + error, debug = message.parse_error() + print "Event bus error:", str(error), str(debug) + + return gst.BUS_PASS + + def _startSegment(self, timestamp, duration): + self.debug("processing segment with timestamp=%i and duration=%i" % (timestamp, duration)) + self._audio_cur = timestamp, duration + res = self.audioPipeline.seek(1.0, + gst.FORMAT_TIME, + gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_ACCURATE | gst.SEEK_FLAG_SEGMENT, + gst.SEEK_TYPE_SET, timestamp, + gst.SEEK_TYPE_SET, timestamp + duration) + if not res: + self.warning("seek failed %s", timestamp) + self.audioPipeline.set_state(gst.STATE_PLAYING) + + return res + + def _finishSegment(self): + samples = self.audioSink.samples + e, start, duration = self._queue[0] + e.receive(samples) + self.audioSink.reset() + start += self.tdur + duration -= self.tdur + if duration > 0: + self._queue[0] = (e, start, duration) + else: + self._queue.pop(0) + e.finalize() + if len(self._queue) > 0: + self._run() + + def extract(self, e, start, duration): + stopped = len(self._queue) == 0 + self._queue.append((e, start, duration)) + if stopped: + self._run() + + def _run(self): + # Control flows in a cycle: + # _run -> _startSegment -> busMessageSegmentDoneCb -> _finishSegment -> _run + # This forms a loop that extracts one block in each cycle. The cycle + # runs until the queue of Extractees empties. If the cycle is not + # running, extract() will kick it off again. + e, start, duration = self._queue[0] + self._startSegment(start, min(duration, self.tdur)) + diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py index 5bd21c5..6fbec2c 100644 --- a/pitivi/timeline/timeline.py +++ b/pitivi/timeline/timeline.py @@ -32,6 +32,7 @@ from pitivi.utils import start_insort_right, infinity, getPreviousObject, \ getNextObject from pitivi.timeline.gap import Gap, SmallestGapsFinder, invalid_gap from pitivi.stream import VideoStream +from pitivi.timeline.align import AutoAligner # Selection modes SELECT = 0 @@ -1940,6 +1941,11 @@ class Timeline(Signallable, Loggable): self.selection.setSelection(new_track_objects, SELECT_ADD) + def alignSelection(self): + a = AutoAligner(self.selection.selected, self.enableUpdates) + self.disableUpdates() + a.start() + def deleteSelection(self): """ Removes all the currently selected L{TimelineObject}s from the Timeline. diff --git a/pitivi/ui/mainwindow.py b/pitivi/ui/mainwindow.py index 72446a8..7970f7b 100644 --- a/pitivi/ui/mainwindow.py +++ b/pitivi/ui/mainwindow.py @@ -148,6 +148,7 @@ def create_stock_icons(): ('pitivi-ungroup', _('Ungroup'), 0, 0, 'pitivi'), # Translators: This is an action, the title of a button ('pitivi-group', _('Group'), 0, 0, 'pitivi'), + ('pitivi-align', _('Align'), 0, 0, 'pitivi'), ]) pixmaps = { "pitivi-render" : "pitivi-render-24.png", @@ -157,6 +158,7 @@ def create_stock_icons(): "pitivi-link" : "pitivi-relink-24.svg", "pitivi-ungroup" : "pitivi-ungroup-24.svg", "pitivi-group" : "pitivi-group-24.svg", + "pitivi-align" : "pitivi-align-24.svg", } factory = gtk.IconFactory() pmdir = get_pixmap_dir() diff --git a/pitivi/ui/timeline.py b/pitivi/ui/timeline.py index c75ccf0..2484be6 100644 --- a/pitivi/ui/timeline.py +++ b/pitivi/ui/timeline.py @@ -64,6 +64,7 @@ UNLINK = _("Break links between clips") LINK = _("Link together arbitrary clips") UNGROUP = _("Ungroup clips") GROUP = _("Group clips") +ALIGN = _("Align clips based on content") SELECT_BEFORE = ("Select all sources before selected") SELECT_AFTER = ("Select all after selected") @@ -87,6 +88,7 @@ ui = ''' <menuitem action="UnlinkObj" /> <menuitem action="GroupObj" /> <menuitem action="UngroupObj" /> + <menuitem action="AlignObj" /> <separator /> <menuitem action="Prevframe" /> <menuitem action="Nextframe" /> @@ -104,6 +106,7 @@ ui = ''' <toolitem action="LinkObj" /> <toolitem action="GroupObj" /> <toolitem action="UngroupObj" /> + <toolitem action="AlignObj" /> </placeholder> </toolbar> <accelerator action="DeleteObj" /> @@ -325,6 +328,8 @@ class Timeline(gtk.Table, Loggable, Zoomable): self.ungroupSelected), ("GroupObj", "pitivi-group", None, "<Control>G", GROUP, self.groupSelected), + ("AlignObj", "pitivi-align", None, "<Shift><Control>A", ALIGN, + self.alignSelected), ) self.playhead_actions = ( @@ -349,6 +354,7 @@ class Timeline(gtk.Table, Loggable, Zoomable): self.unlink_action = actiongroup.get_action("UnlinkObj") self.group_action = actiongroup.get_action("GroupObj") self.ungroup_action = actiongroup.get_action("UngroupObj") + self.align_action = actiongroup.get_action("AlignObj") self.delete_action = actiongroup.get_action("DeleteObj") self.split_action = actiongroup.get_action("Split") self.keyframe_action = actiongroup.get_action("Keyframe") @@ -716,6 +722,7 @@ class Timeline(gtk.Table, Loggable, Zoomable): unlink = False group = False ungroup = False + align = False split = False keyframe = False if timeline.selection: @@ -723,6 +730,7 @@ class Timeline(gtk.Table, Loggable, Zoomable): if len(timeline.selection) > 1: link = True group = True + align = True start = None duration = None @@ -751,6 +759,7 @@ class Timeline(gtk.Table, Loggable, Zoomable): self.unlink_action.set_sensitive(unlink) self.group_action.set_sensitive(group) self.ungroup_action.set_sensitive(ungroup) + self.align_action.set_sensitive(align) self.split_action.set_sensitive(split) self.keyframe_action.set_sensitive(keyframe) @@ -794,6 +803,10 @@ class Timeline(gtk.Table, Loggable, Zoomable): if self.timeline: self.timeline.groupSelection() + def alignSelected(self, unused_action): + if self.timeline: + self.timeline.alignSelection() + def split(self, action): self.app.action_log.begin("split") self.timeline.disableUpdates()
signature.asc
Description: OpenPGP digital signature
------------------------------------------------------------------------------ EditLive Enterprise is the world's most technically advanced content authoring tool. Experience the power of Track Changes, Inline Image Editing and ensure content is compliant with Accessibility Checking. http://p.sf.net/sfu/ephox-dev2dev
_______________________________________________ Pitivi-pitivi mailing list Pitivi-pitivi@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/pitivi-pitivi