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

Attachment: 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

Reply via email to