[pitivi: 4/6] Introduce a content-based auto-aligner.



commit aa684103541d2478d9d31390b181ea461fc5f94f
Author: Benjamin M. Schwartz <bens alum mit edu>
Date:   Sun Jul 24 23:35:30 2011 -0400

    Introduce a content-based auto-aligner.
    
    The auto-aligner shifts clips into the timeline so that their
    contents are synchronized.  It works by analyzing the audio track's
    volume as it changes over time, under the assumption that when there
    are multiple recordings of a single event, they will be loud, or quiet,
    at the same time.

 data/pixmaps/Makefile.am          |    2 +
 data/pixmaps/pitivi-align-24.svg  |  224 +++++++++++++++++++++
 data/pixmaps/pitivi-align.svg     |  228 ++++++++++++++++++++++
 data/ui/alignmentprogress.ui      |   40 ++++
 pitivi/elements/extractionsink.py |   94 +++++++++
 pitivi/timeline/align.py          |  385 +++++++++++++++++++++++++++++++++++++
 pitivi/timeline/alignalgs.py      |  294 ++++++++++++++++++++++++++++
 pitivi/timeline/extract.py        |  230 ++++++++++++++++++++++
 pitivi/timeline/timeline.py       |   16 ++
 pitivi/ui/alignmentprogress.py    |   75 +++++++
 pitivi/ui/mainwindow.py           |    2 +
 pitivi/ui/timeline.py             |   26 +++
 pitivi/utils.py                   |   14 ++
 13 files changed, 1630 insertions(+), 0 deletions(-)
---
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/data/ui/alignmentprogress.ui b/data/ui/alignmentprogress.ui
new file mode 100644
index 0000000..034ec6b
--- /dev/null
+++ b/data/ui/alignmentprogress.ui
@@ -0,0 +1,40 @@
+<?xml version="1.0"?>
+<interface>
+  <requires lib="gtk+" version="2.16"/>
+  <!-- interface-naming-policy toplevel-contextual -->
+  <object class="GtkWindow" id="align-progress">
+    <property name="border_width">12</property>
+    <property name="title" translatable="yes">Auto-Alignment Starting</property>
+    <property name="resizable">False</property>
+    <property name="modal">True</property>
+    <property name="window_position">center-on-parent</property>
+    <property name="deletable">False</property>
+    <child>
+      <object class="GtkVBox" id="vbox1">
+        <property name="visible">True</property>
+        <child>
+          <object class="GtkLabel" id="label5">
+            <property name="visible">True</property>
+            <property name="xalign">0</property>
+            <property name="ypad">12</property>
+            <property name="label" translatable="yes">&lt;b&gt;&lt;big&gt;Performing Auto-Alignment&lt;/big&gt;&lt;/b&gt;</property>
+            <property name="use_markup">True</property>
+          </object>
+          <packing>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkProgressBar" id="progressbar">
+            <property name="visible">True</property>
+            <property name="show_text">True</property>
+            <property name="text" translatable="yes">Estimating...</property>
+          </object>
+          <packing>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/pitivi/elements/extractionsink.py b/pitivi/elements/extractionsink.py
new file mode 100644
index 0000000..701bc60
--- /dev/null
+++ b/pitivi/elements/extractionsink.py
@@ -0,0 +1,94 @@
+# PiTiVi , Non-linear video editor
+#
+#       pitivi/elements/extractionsink.py
+#
+# Copyright (c) 2005, Edward Hervey <bilboed bilboed com>
+# Copyright (c) 2011, Benjamin M. Schwartz <bens alum mit edu>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+
+"""
+Extract audio samples without storing the whole waveform in memory
+"""
+
+import array
+import gobject
+gobject.threads_init()
+import gst
+from pitivi.utils import native_endianness, call_false
+
+
+class ExtractionSink(gst.BaseSink):
+
+    """
+    Passes audio data directly to a provided L{Extractee}
+    """
+
+    caps = gst.Caps(
+        "audio/x-raw-float, width=(int) 32, "
+        "endianness = (int) %s, "
+        "channels = (int) 1,"
+        "rate = (int) [1, 96000]"
+        % native_endianness)
+
+    __gsttemplates__ = (
+        gst.PadTemplate(
+            "sink",
+            gst.PAD_SINK,
+            gst.PAD_ALWAYS,
+            caps),)
+
+    def __init__(self):
+        gst.BaseSink.__init__(self)
+        self.props.sync = False
+        self.rate = 0
+        self.channels = 0
+        self.reset()
+        self._cb = None
+
+    def set_extractee(self, extractee):
+        self.extractee = extractee
+
+    def set_stopped_cb(self, cb):
+        self._cb = cb
+
+    def reset(self):
+        self.samples = array.array('f')
+        self.extractee = None
+
+    def do_set_caps(self, caps):
+        if not caps[0].get_name() == "audio/x-raw-float":
+            return False
+        self.rate = caps[0]["rate"]
+        self.channels = caps[0]["channels"]
+        return True
+
+    def do_render(self, buf):
+        if self.extractee is not None:
+            self.extractee.receive(array.array('f', buf.data))
+        return gst.FLOW_OK
+
+    def do_preroll(self, buf):
+        return gst.FLOW_OK
+
+    def do_event(self, ev):
+        self.info("Got event of type %s" % ev.type)
+        if ev.type == gst.EVENT_EOS:
+            if self._cb:
+                gobject.idle_add(call_false, self._cb)
+        return gst.FLOW_OK
+
+gobject.type_register(ExtractionSink)
diff --git a/pitivi/timeline/align.py b/pitivi/timeline/align.py
new file mode 100644
index 0000000..fdd19bd
--- /dev/null
+++ b/pitivi/timeline/align.py
@@ -0,0 +1,385 @@
+# PiTiVi , Non-linear video editor
+#
+#       timeline/align.py
+#
+# Copyright (c) 2011, Benjamin M. Schwartz <bens alum mit edu>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+
+"""
+Classes for automatic alignment of L{TimelineObject}s
+"""
+
+import array
+import time
+import numpy
+
+import gobject
+import gst
+from pitivi.utils import beautify_ETA, call_false
+from pitivi.timeline.extract import Extractee, RandomAccessAudioExtractor
+from pitivi.stream import AudioStream
+from pitivi.log.loggable import Loggable
+from pitivi.timeline.alignalgs import rigidalign
+
+
+def getAudioTrack(timeline_object):
+    """Helper function for getting an audio track from a TimelineObject
+
+    @param timeline_object: The TimelineObject from which to locate an
+        audio track
+    @type timeline_object: L{TimelineObject}
+    @returns: An audio track from timeline_object, or None if
+        timeline_object has no audio track
+    @rtype: audio L{TrackObject} or L{NoneType}
+
+    """
+    for track in timeline_object.track_objects:
+        if track.stream_type == AudioStream:
+            return track
+    return None
+
+
+class ProgressMeter:
+
+    """Abstract interface representing a progress meter."""
+
+    def addWatcher(self, function):
+        """ Add a progress watching callback function.  This callback will
+        always be called from the main thread.
+
+        @param function: a function to call with progress updates.
+        @type function: callable(fractional_progress, time_remaining_text).
+            fractional_progress is a float normalized to [0,1].
+            time_remaining_text is a localized text string indicating the
+            estimated time remaining.
+        """
+        raise NotImplementedError
+
+
+class ProgressAggregator(ProgressMeter):
+
+    """A ProgressMeter that aggregates progress reports.
+
+    Reports from multiple sources are combined into a unified progress
+    report.
+
+    """
+
+    def __init__(self):
+        # _targets is a list giving the size of each task.
+        self._targets = []
+        # _portions is a list of the same length as _targets, indicating
+        # the portion of each task that as been completed (initially 0).
+        self._portions = []
+        self._start = time.time()
+        self._watchers = []
+
+    def getPortionCB(self, target):
+        """Prepare a new input for the Aggregator.
+
+        Given a target size
+        (in arbitrary units, but should be consistent across all calls on
+        a single ProgressAggregator object), it returns a callback that
+        can be used to update progress on this portion of the task.
+
+        @param target: the total task size for this portion
+        @type target: number
+        @returns: a callback that can be used to inform the Aggregator of
+            subsequent updates to this portion
+        @rtype: function(x), where x should be a number indicating the
+            absolute amount of this subtask that has been completed.
+
+        """
+        i = len(self._targets)
+        self._targets.append(target)
+        self._portions.append(0)
+
+        def cb(thusfar):
+            self._portions[i] = thusfar
+            gobject.idle_add(self._callForward)
+        return cb
+
+    def addWatcher(self, function):
+        self._watchers.append(function)
+
+    def _callForward(self):
+        # This function always returns False so that it may be safely
+        # invoked via gobject.idle_add(). Use of idle_add() is necessary
+        # to ensure that watchers are always called from the main thread,
+        # even if progress updates are received from other threads.
+        total_target = sum(self._targets)
+        total_completed = sum(self._portions)
+        if total_target == 0:
+            return False
+        frac = min(1.0, float(total_completed) / total_target)
+        now = time.time()
+        remaining = (now - self._start) * (1 - frac) / frac
+        for function in self._watchers:
+            function(frac, beautify_ETA(int(remaining * gst.SECOND)))
+        return False
+
+
+class EnvelopeExtractee(Extractee, Loggable):
+
+    """Class that computes the envelope of a 1-D signal (audio).
+
+    The envelope is defined as the sum of the absolute value of the signal
+    over each block.  This class computes the envelope incrementally,
+    so that the entire signal does not ever need to be stored.
+
+    """
+
+    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._blocks = numpy.zeros((0,), dtype=numpy.float32)
+        self._empty = array.array('f', [])
+        # self._samples buffers up to self._threshold samples, before
+        # their envelope is computed and store in self._blocks, in order
+        # to amortize some of the function call overheads.
+        self._samples = array.array('f', [])
+        self._threshold = 2000 * blocksize
+        self._progress_watchers = []
+
+    def receive(self, a):
+        self._samples.extend(a)
+        if len(self._samples) < self._threshold:
+            return
+        else:
+            self._process_samples()
+
+    def addWatcher(self, w):
+        """
+        Add a function to call with progress updates.
+
+        @param w: callback function
+        @type w: function(# of samples received so far)
+
+        """
+        self._progress_watchers.append(w)
+
+    def _process_samples(self):
+        excess = len(self._samples) % self._blocksize
+        if excess != 0:
+            samples_to_process = self._samples[:-excess]
+            self._samples = self._samples[-excess:]
+        else:
+            samples_to_process = self._samples
+            self._samples = array.array('f', [])
+        self.debug("Adding %s samples to %s blocks",
+                   len(samples_to_process), len(self._blocks))
+        newblocks = len(samples_to_process) // self._blocksize
+        samples_abs = numpy.abs(
+                samples_to_process).reshape((newblocks, self._blocksize))
+        self._blocks.resize((len(self._blocks) + newblocks,))
+        # This numpy.sum() call relies on samples_abs being a
+        # floating-point type. If samples_abs.dtype is int16
+        # then the sum may overflow.
+        self._blocks[-newblocks:] = numpy.sum(samples_abs, 1)
+        for w in self._progress_watchers:
+            w(self._blocksize * len(self._blocks) + excess)
+
+    def finalize(self):
+        self._process_samples()  # absorb any remaining buffered samples
+        self._cb(self._blocks, *self._cbargs)
+
+
+class AutoAligner(Loggable):
+
+    """
+    Class for aligning a set of L{TimelineObject}s automatically.
+
+    The alignment is based on their contents, so that the shifted tracks
+    are synchronized.  The current implementation only analyzes audio
+    data, so timeline objects without an audio track cannot be aligned.
+
+    """
+
+    BLOCKRATE = 25
+    """
+    @ivar BLOCKRATE: The number of amplitude blocks per second.
+
+    The AutoAligner works by computing the "amplitude envelope" of each
+    audio stream.  We define an amplitude envelope as the absolute value
+    of the audio samples, downsampled to a low samplerate.  This
+    samplerate, in Hz, is given by BLOCKRATE.  (It is given this name
+    because the downsampling filter is implemented by very simple
+    averaging over blocks, i.e. a box filter.)  25 Hz appears to be a
+    good choice because it evenly divides all common audio samplerates
+    (e.g. 11025 and 8000). Lower blockrate requires less CPU time but
+    produces less accurate alignment.  Higher blockrate is the reverse
+    (and also cannot evenly divide all samplerates).
+
+    """
+
+    def __init__(self, timeline_objects, callback):
+        """
+        @param timeline_objects: an iterable of L{TimelineObject}s.
+            In this implementation, only L{TimelineObject}s with at least one
+            audio track will be aligned.
+        @type timeline_objects: 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._timeline_objects maps each object to its envelope.  The values
+        # are initially None prior to envelope extraction.
+        self._timeline_objects = dict.fromkeys(timeline_objects)
+        self._callback = callback
+        # stack of (Track, Extractee) pairs waiting to be processed
+        # When start() is called, the stack will be populated, and then
+        # processed sequentially.  Only one item from the stack will be
+        # actively in process at a time.
+        self._extraction_stack = []
+
+    @staticmethod
+    def canAlign(timeline_objects):
+        """
+        Can an AutoAligner align these objects?
+
+        Determine whether a group of timeline objects can all
+        be aligned together by an AutoAligner.
+
+        @param timeline_objects: a group of timeline objects
+        @type timeline_objects: iterable(L{TimelineObject})
+        @returns: True iff the objects can aligned.
+        @rtype: L{bool}
+
+        """
+        return all(getAudioTrack(t) is not None for t in timeline_objects)
+
+    def _extractNextEnvelope(self):
+        audiotrack, extractee = self._extraction_stack.pop()
+        r = RandomAccessAudioExtractor(audiotrack.factory,
+                                       audiotrack.stream)
+        r.extract(extractee, audiotrack.in_point,
+                  audiotrack.out_point - audiotrack.in_point)
+        return False
+
+    def _envelopeCb(self, array, timeline_object):
+        self.debug("Receiving envelope for %s", timeline_object)
+        self._timeline_objects[timeline_object] = array
+        if self._extraction_stack:
+            self._extractNextEnvelope()
+        else:  # This was the last envelope
+            self._performShifts()
+            self._callback()
+
+    def start(self):
+        """
+        Initiate the auto-alignment process.
+
+        @returns: a L{ProgressMeter} indicating the progress of the
+            alignment
+        @rtype: L{ProgressMeter}
+
+        """
+        progress_aggregator = ProgressAggregator()
+        pairs = []  # (TimelineObject, {audio}TrackObject) pairs
+        for timeline_object in self._timeline_objects.keys():
+            audiotrack = getAudioTrack(timeline_object)
+            if audiotrack is not None:
+                pairs.append((timeline_object, audiotrack))
+            else:  # forget any TimelineObject without an audio track
+                self._timeline_objects.pop(timeline_object)
+        if len(pairs) >= 2:
+            for timeline_object, audiotrack in pairs:
+                # blocksize is the number of samples per block
+                blocksize = audiotrack.stream.rate // self.BLOCKRATE
+                extractee = EnvelopeExtractee(blocksize, self._envelopeCb,
+                                              timeline_object)
+                # numsamples is the total number of samples in the track,
+                # which is used by progress_aggregator to determine
+                # the percent completion.
+                numsamples = ((audiotrack.duration / gst.SECOND) *
+                              audiotrack.stream.rate)
+                extractee.addWatcher(
+                        progress_aggregator.getPortionCB(numsamples))
+                self._extraction_stack.append((audiotrack, extractee))
+            # After we return, start the extraction cycle.
+            # This gobject.idle_add call should not be necessary;
+            # we should be able to invoke _extractNextEnvelope directly
+            # here.  However, there is some as-yet-unexplained
+            # race condition between the Python GIL, GTK UI updates,
+            # GLib mainloop, and pygst multithreading, resulting in
+            # occasional deadlocks during autoalignment.
+            # This call to idle_add() reportedly eliminates the deadlock.
+            # No one knows why.
+            gobject.idle_add(self._extractNextEnvelope)
+        else:  # We can't do anything without at least two audio tracks
+            # After we return, call the callback function (once)
+            gobject.idle_add(call_false, self._callback)
+        return progress_aggregator
+
+    def _chooseReference(self):
+        """
+        Chooses the timeline object to use as a reference.
+
+        This function currently selects the one with lowest priority,
+        i.e. appears highest in the GUI.  The behavior of this function
+        affects user interaction, because the user may want to
+        determine which object moves and which stays put.
+
+        @returns: the timeline object with lowest priority.
+        @rtype: L{TimelineObject}
+
+        """
+        def priority(timeline_object):
+            return timeline_object.priority
+        return min(self._timeline_objects.iterkeys(), key=priority)
+
+    def _performShifts(self):
+        self.debug("performing shifts")
+        reference = self._chooseReference()
+        # By using pop(), this line also removes the reference
+        # TimelineObject and its envelope from further consideration,
+        # saving some CPU time in rigidalign.
+        reference_envelope = self._timeline_objects.pop(reference)
+        # We call list() because we need a reliable ordering of the pairs
+        # (In python 3, dict.items() returns an unordered dictview)
+        pairs = list(self._timeline_objects.items())
+        envelopes = [p[1] for p in pairs]
+        offsets = rigidalign(reference_envelope, envelopes)
+        for (movable, envelope), offset in zip(pairs, offsets):
+            # tshift is the offset rescaled to units of nanoseconds
+            tshift = int((offset * gst.SECOND) / self.BLOCKRATE)
+            self.debug("Shifting %s to %i ns from %i",
+                       movable, tshift, reference.start)
+            newstart = reference.start + tshift
+            if newstart >= 0:
+                movable.start = newstart
+            else:
+                # Timeline objects always must have a positive start point, so
+                # if alignment would move an object to start at negative time,
+                # we instead make it start at zero and chop off the required
+                # amount at the beginning.
+                movable.start = 0
+                movable.in_point = movable.in_point - newstart
+                movable.duration += newstart
diff --git a/pitivi/timeline/alignalgs.py b/pitivi/timeline/alignalgs.py
new file mode 100644
index 0000000..918ce7c
--- /dev/null
+++ b/pitivi/timeline/alignalgs.py
@@ -0,0 +1,294 @@
+# PiTiVi , Non-linear video editor
+#
+#       timeline/alignalgs.py
+#
+# Copyright (c) 2011, Benjamin M. Schwartz <bens alum mit edu>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+
+"""
+Algorithms for aligning (i.e. registering, synchronizing) time series
+"""
+
+import numpy
+
+
+def nextpow2(x):
+    a = 1
+    while a < x:
+        a *= 2
+    return a
+
+
+def submax(left, middle, right):
+    """
+    Find the maximum of a quadratic function from three samples.
+
+    Given samples from a quadratic P(x) at x=-1, 0, and 1, find the x
+    that extremizes P.  This is useful for determining the subsample
+    position of the extremum given three samples around the observed
+    extreme.
+
+    @param left: value at x=-1
+    @type left: L{float}
+    @param middle: value at x=0
+    @type middle: L{float}
+    @param right: value at x=1
+    @type right: L{float}
+    @returns: value of x that extremizes the interpolating quadratic
+    @rtype: L{float}
+
+    """
+    L = middle - left   # L and R are both positive if middle is the
+    R = middle - right  # observed max of the integer samples
+    return 0.5 * (R - L) / (R + L)
+    # Derivation: Consider a quadratic q(x) := P(0) - P(x).  Then q(x) has
+    # two roots, one at 0 and one at z, and the extreme is at (0+z)/2
+    # (i.e. at z/2)
+    # q(x) = bx*(x-z) # a may be positive or negative
+    # q(1) = b*(1 - z) = R
+    # q(-1) = b*(1 + z) = L
+    # (1+z)/(1-z) = L/R  (from here it's just algebra to find a)
+    # z + 1 = R/L - (R/L)*z
+    # z*(1+R/L) = R/L - 1
+    # z = (R/L - 1)/(R/L + 1) = (R-L)/(R+L)
+
+
+def rigidalign(reference, targets):
+    """
+    Estimate the relative shift between reference and targets.
+
+    The algorithm works by subtracting the mean, and then locating
+    the maximum of the cross-correlation.  For inputs of length M{N},
+    the running time is M{O(C{len(targets)}*N*log(N))}.
+
+    @param reference: the waveform to regard as fixed
+    @type reference: Sequence(Number)
+    @param targets: the waveforms that should be aligned to reference
+    @type targets: Sequence(Sequence(Number))
+    @returns: The shift necessary to bring each target into alignment
+        with the reference.  The returned shift may not be an integer,
+        indicating that the best alignment would be achieved by a
+        non-integer shift and appropriate interpolation.
+    @rtype: Sequence(Number)
+
+    """
+    # L is the maximum size of a cross-correlation between the
+    # reference and any of the targets.
+    L = len(reference) + max(len(t) for t in targets) - 1
+    # We round up L to the next power of 2 for speed in the FFT.
+    L = nextpow2(L)
+    reference = reference - numpy.mean(reference)
+    fref = numpy.fft.rfft(reference, L).conj()
+    shifts = []
+    for t in targets:
+        t = t - numpy.mean(t)
+        # Compute cross-correlation
+        xcorr = numpy.fft.irfft(fref * numpy.fft.rfft(t, L))
+        # shift maximizes dotproduct(t[shift:],reference)
+        # int() to convert numpy.int32 to python int
+        shift = int(numpy.argmax(xcorr))
+        subsample_shift = submax(xcorr[(shift - 1) % L],
+                                 xcorr[shift],
+                                 xcorr[(shift + 1) % L])
+        shift = shift + subsample_shift
+        # shift is now a float indicating the interpolated maximum
+        if shift >= len(t):  # Negative shifts appear large and positive
+            shift -= L       # This corrects them to be negative
+        shifts.append(-shift)
+        # Sign reversed to move the target instead of the reference
+    return shifts
+
+
+def _findslope(a):
+    # Helper function for affinealign
+    # The provided matrix a contains a bright line whose slope we want to know,
+    # against a noisy background.
+    # The line starts at 0,0.  If the slope is positive, it runs toward the
+    # center of the matrix (i.e. toward (-1,-1))
+    # If the slope is negative, it wraps from 0,0 to 0,-1 and continues toward
+    # the center, (i.e. toward (-1,0)).
+    # The line segment terminates at the midline along the X direction.
+    # We locate the line by simply checking the sum along each possible line
+    # up to the Y-max edge of a.  The caller sets the limit by choosing the
+    # size of a.
+    # The function returns a floating-point slope assuming that the matrix
+    # has "square pixels".
+    Y, X = a.shape
+    X /= 2
+    x_pos = numpy.arange(1, X)
+    x_neg = numpy.arange(2 * X - 1, X, -1)
+    best_end = 0
+    max_sum = 0
+    for end in xrange(Y):
+        y = (x_pos * end) // X
+        s = numpy.sum(a[y, x_pos])
+        if s > max_sum:
+            max_sum = s
+            best_end = end
+        s = numpy.sum(a[y, x_neg])
+        if s > max_sum:
+            max_sum = s
+            best_end = -end
+    return float(best_end) / X
+
+
+def affinealign(reference, targets, max_drift=0.02):
+    """ EXPERIMENTAL FUNCTION.
+
+    Perform an affine registration between a reference and a number of
+    targets.  Designed for aligning the amplitude envelopes of recordings of
+    the same event by different devices.
+
+    NOTE: This method is currently NOT USED by PiTiVi, as it has proven both
+    unnecessary and unusable.  So far every test case has been registered
+    successfully by rigidalign, and until PiTiVi supports time-stretching of
+    audio, the drift calculation cannot actually be used.
+
+    @param reference: the reference signal to which others will be registered
+    @type reference: array(number)
+    @param targets: the signals to register
+    @type targets: ordered iterable(array(number))
+    @param max_drift: the maximum absolute clock drift rate
+                  (i.e. stretch factor) that will be considered during search
+    @type max_drift: positive L{float}
+    @return: (offsets, drifts).  offsets[i] is the point in reference at which
+           targets[i] starts.  drifts[i] is the speed of targets[i] relative to
+           the reference (positive is faster, meaning the target should be
+           slowed down to be in sync with the reference)
+    """
+    L = len(reference) + max(len(t) for t in targets) - 1
+    L2 = nextpow2(L)
+    bsize = int(20. / max_drift)  # NEEDS TUNING
+    num_blocks = nextpow2(1.0 * len(reference) // bsize)  # NEEDS TUNING
+    bspace = (len(reference) - bsize) // num_blocks
+    reference -= numpy.mean(reference)
+
+    # Construct FFT'd reference blocks
+    freference_blocks = numpy.zeros((L2 / 2 + 1, num_blocks),
+                                    dtype=numpy.complex)
+    for i in xrange(num_blocks):
+        s = i * bspace
+        tmp = numpy.zeros((L2,))
+        tmp[s:s + bsize] = reference[s:s + bsize]
+        freference_blocks[:, i] = numpy.fft.rfft(tmp, L2).conj()
+    freference_blocks[:10, :] = 0  # High-pass to ignore slow volume variations
+
+    offsets = []
+    drifts = []
+    for t in targets:
+        t -= numpy.mean(t)
+        ft = numpy.fft.rfft(t, L2)
+        #fxcorr is the FFT'd cross-correlation with the reference blocks
+        fxcorr_blocks = numpy.zeros((L2 / 2 + 1, num_blocks),
+                                    dtype=numpy.complex)
+        for i in xrange(num_blocks):
+            fxcorr_blocks[:, i] = ft * freference_blocks[:, i]
+            fxcorr_blocks[:, i] /= numpy.sqrt(numpy.sum(
+                    fxcorr_blocks[:, i] ** 2))
+        del ft
+        # At this point xcorr_blocks would show a distinct bright line, nearly
+        # orthogonal to time, indicating where each of these blocks found their
+        # peak.  Each point on this line represents the time in t where block i
+        # found its match.  The time-intercept gives the time in b at which the
+        # reference starts, and the slope gives the amount by which the
+        # reference is faster relative to b.
+
+        # The challenge now is to find this line.  Our strategy is to reduce the
+        # search to one dimension by first finding the slope.
+        # The Fourier Transform of a smooth real line in 2D is an orthogonal
+        # line through the origin, with phase that gives its position.
+        # Unfortunately this line is not clearly visible in fxcorr_blocks, so
+        # we discard the phase (by taking the absolute value) and then inverse
+        # transform.  This places the line at the origin, so we can find its
+        # slope.
+
+        # Construct the half-autocorrelation matrix
+        # (A true autocorrelation matrix would be ifft(abs(fft(x))**2), but this
+        # is just ifft(abs(fft(x))).)
+        # Construction is stepwise partly in an attempt to save memory
+        # The width is 2*num_blocks in order to avoid overlapping positive and
+        # negative correlations
+        halfautocorr = numpy.fft.fft(fxcorr_blocks, 2 * num_blocks, 1)
+        halfautocorr = numpy.abs(halfautocorr)
+        halfautocorr = numpy.fft.ifft(halfautocorr, None, 1)
+        halfautocorr = numpy.fft.irfft(halfautocorr, None, 0)
+        # Now it's actually the half-autocorrelation.
+        # Chop out the bit we don't care about
+        halfautocorr = halfautocorr[:bspace * num_blocks * max_drift, :]
+        # Remove the local-correlation peak.
+        halfautocorr[-1:2, -1:2] = 0  # NEEDS TUNING
+        # Normalize each column (appears to be necessary)
+        for i in xrange(2 * num_blocks):
+            halfautocorr[:, i] /= numpy.sqrt(numpy.sum(
+                    halfautocorr[:, i] ** 2))
+        #from matplotlib.pyplot import imshow,show
+        #imshow(halfautocorr,interpolation='nearest',aspect='auto');show()
+        drift = _findslope(halfautocorr) / bspace
+        del halfautocorr
+
+        #inverse transform and shift everything into alignment
+        xcorr_blocks = numpy.fft.irfft(fxcorr_blocks, None, 0)
+        del fxcorr_blocks
+        #TODO: see if phase ramps are worthwhile here
+        for i in xrange(num_blocks):
+            blockcenter = i * bspace + bsize / 2
+            shift = int(blockcenter * drift)
+            if shift > 0:
+                temp = xcorr_blocks[:shift, i].copy()
+                xcorr_blocks[:-shift, i] = xcorr_blocks[shift:, i].copy()
+                xcorr_blocks[-shift:, i] = temp
+            elif shift < 0:
+                temp = xcorr_blocks[shift:, i].copy()
+                xcorr_blocks[-shift:, i] = xcorr_blocks[:shift, i].copy()
+                xcorr_blocks[:-shift, i] = temp
+
+        #from matplotlib.pyplot import imshow,show
+        #imshow(xcorr_blocks,interpolation='nearest',aspect='auto');show()
+
+        # xcorr is the drift-compensated cross-correlation
+        xcorr = numpy.sum(xcorr_blocks, axis=1)
+        del xcorr_blocks
+
+        offset = numpy.argmax(xcorr)
+        #from matplotlib.pyplot import plot,show
+        #plot(xcorr);show()
+        del xcorr
+        if offset >= len(t):
+            offset -= L2
+
+        # now offset is the point in target at which reference starts and
+        # drift is the speed with which the reference drifts relative to the
+        # target.  We reverse these relationships for the caller.
+        slope = 1 + drift
+        offsets.append(-offset / slope)
+        drifts.append(1 / slope - 1)
+    return offsets, drifts
+
+if __name__ == '__main__':
+    # Simple command-line test
+    from sys import argv
+    names = argv[1:]
+    envelopes = [numpy.fromfile(n) for n in names]
+    reference = envelopes[-1]
+    offsets, drifts = affinealign(reference, envelopes, 0.02)
+    print offsets, drifts
+    from matplotlib.pyplot import *
+    clf()
+    for i in xrange(len(envelopes)):
+        t = offsets[i] + (1 + drifts[i]) * numpy.arange(len(envelopes[i]))
+        plot(t, envelopes[i] / numpy.sqrt(numpy.sum(envelopes[i] ** 2)))
+    show()
diff --git a/pitivi/timeline/extract.py b/pitivi/timeline/extract.py
new file mode 100644
index 0000000..da9873b
--- /dev/null
+++ b/pitivi/timeline/extract.py
@@ -0,0 +1,230 @@
+# PiTiVi , Non-linear video editor
+#
+#       timeline/extract.py
+#
+# Copyright (c) 2005, Edward Hervey <bilboed bilboed com>
+# Copyright (c) 2011, Benjamin M. Schwartz <bens alum mit edu>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+# Boston, MA 02110-1301, USA.import gst
+
+"""
+Classes for extracting decoded contents of streams into Python
+
+Code derived from ui/previewer.py.
+"""
+
+import gst
+from collections import deque
+from pitivi.elements.singledecodebin import SingleDecodeBin
+from pitivi.elements.extractionsink import ExtractionSink
+from pitivi.log.loggable import Loggable
+from pitivi.utils import pipeline
+
+
+class Extractee:
+
+    """Abstract base class for receiving 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):
+        """
+        Inform the Extractee that receive() will not be called again.
+
+        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 L{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, extractee, start, duration):
+        """
+        Extract the raw data corresponding to a segment of the stream.
+
+        @param extractee: the L{Extractee} that will receive the raw data
+        @type extractee: 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._queue = deque()
+        RandomAccessExtractor.__init__(self, factory, stream_)
+        self._ready = False
+
+    def _pipelineInit(self, factory, sbin):
+        self.spacing = 0
+
+        self.audioSink = ExtractionSink()
+        self.audioSink.set_stopped_cb(self._finishSegment)
+        # This audiorate element ensures that the extracted raw-data
+        # timeline matches the timestamps used for seeking, even if the
+        # audio source has gaps or other timestamp abnormalities.
+        audiorate = gst.element_factory_make("audiorate")
+        conv = gst.element_factory_make("audioconvert")
+        q = gst.element_factory_make("queue")
+        self.audioPipeline = pipeline({
+            sbin: audiorate,
+            audiorate: conv,
+            conv: q,
+            q: self.audioSink,
+            self.audioSink: None})
+        bus = self.audioPipeline.get_bus()
+        bus.add_signal_watch()
+        bus.connect("message::error", self._busMessageErrorCb)
+        self._donecb_id = bus.connect("message::async-done",
+                                      self._busMessageAsyncDoneCb)
+
+        self.audioPipeline.set_state(gst.STATE_PAUSED)
+        # The audiopipeline.set_state() method does not take effect
+        # immediately, but the extraction process (and in particular
+        # self._startSegment) will not work properly until
+        # self.audioPipeline reaches the desired state (STATE_PAUSED).
+        # To ensure that this is the case, we wait until the ASYNC_DONE
+        # message is received before setting self._ready = True,
+        # which enables extraction to proceed.
+
+    def _busMessageErrorCb(self, bus, message):
+        error, debug = message.parse_error()
+        self.error("Event bus error: %s; %s", error, debug)
+
+        return gst.BUS_PASS
+
+    def _busMessageAsyncDoneCb(self, bus, message):
+        self.debug("Pipeline is ready for seeking")
+        bus.disconnect(self._donecb_id)  # Don't call me again
+        self._ready = True
+        if self._queue:  # Someone called .extract() before we were ready
+            self._run()
+
+    def _startSegment(self, timestamp, duration):
+        self.debug("processing segment with timestamp=%i and duration=%i",
+                   timestamp, duration)
+        res = self.audioPipeline.seek(1.0,
+            gst.FORMAT_TIME,
+            gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_ACCURATE,
+            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):
+        self.audioSink.extractee.finalize()
+        self.audioSink.reset()
+        self._queue.popleft()
+        # If there's more to do, keep running
+        if self._queue:
+            self._run()
+
+    def extract(self, extractee, start, duration):
+        stopped = not self._queue
+        self._queue.append((extractee, start, duration))
+        if stopped and self._ready:
+            self._run()
+        # if self._ready is False, self._run() will be called from
+        # self._busMessageDoneCb().
+
+    def _run(self):
+        # Control flows in a cycle:
+        # _run -> _startSegment -> busMessageSegmentDoneCb -> _finishSegment -> _run
+        # This forms a loop that extracts an entire segment (i.e. satisfies an
+        # extract request) in each cycle. The cycle
+        # runs until the queue of Extractees empties.  If the cycle is not
+        # running, extract() will kick it off again.
+        extractee, start, duration = self._queue[0]
+        self.audioSink.set_extractee(extractee)
+        self._startSegment(start, duration)
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index 5974150..e94214f 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
@@ -1958,6 +1959,21 @@ class Timeline(Signallable, Loggable):
 
         self.selection.setSelection(new_track_objects, SELECT_ADD)
 
+    def alignSelection(self, callback):
+        """
+        Auto-align the selected set of L{TimelineObject}s based on their
+        contents.  Return asynchronously, and call back when finished.
+
+        @param callback: function to call (with no arguments) when finished.
+        @type callback: function
+        @returns: a L{ProgressMeter} indicating the state of the alignment
+            process
+        @rtype: L{ProgressMeter}
+        """
+        auto_aligner = AutoAligner(self.selection.selected, callback)
+        progress_meter = auto_aligner.start()
+        return progress_meter
+
     def deleteSelection(self):
         """
         Removes all the currently selected L{TimelineObject}s from the Timeline.
diff --git a/pitivi/ui/alignmentprogress.py b/pitivi/ui/alignmentprogress.py
new file mode 100644
index 0000000..f746b88
--- /dev/null
+++ b/pitivi/ui/alignmentprogress.py
@@ -0,0 +1,75 @@
+# PiTiVi , Non-linear video editor
+#
+#       ui/alignmentprogress.py
+#
+# Copyright (c) 2010, Brandon Lewis <brandon lewis collabora co uk>
+# Copyright (c) 2011, Benjamin M. Schwartz <bens alum mit edu>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+
+"""
+Basic auto-alignment progress dialog, based on the EncodingProgressDialog
+"""
+
+import os
+from gettext import gettext as _
+
+import gobject
+import gtk
+import gst
+
+import pitivi.configure as configure
+from pitivi.signalinterface import Signallable
+
+
+class AlignmentProgressDialog:
+    """ Dialog indicating the progress of the auto-alignment process.
+        Code derived from L{EncodingProgressDialog}, but greatly simplified
+        (read-only, no buttons)."""
+
+    def __init__(self, app):
+        self.builder = gtk.Builder()
+        self.builder.add_from_file(os.path.join(configure.get_ui_dir(),
+                                   "alignmentprogress.ui"))
+        self.builder.connect_signals(self)
+
+        self.window = self.builder.get_object("align-progress")
+        self.progressbar = self.builder.get_object("progressbar")
+        # Parent this dialog with mainwindow
+        # set_transient_for allows this dialog to properly
+        # minimize together with the mainwindow.  This method is
+        # taken from EncodingProgressDialog.  In both cases, it appears
+        # to work correctly, although there is a known bug for Gnome 3 in
+        # EncodingProgressDialog (bug #652917)
+        self.window.set_transient_for(app.gui)
+
+        # UI widgets
+        # We currently reuse the render icon for this dialog.
+        icon_path = os.path.join(configure.get_pixmap_dir(),
+                                 "pitivi-render-16.png")
+        self.window.set_icon_from_file(icon_path)
+
+        # FIXME: Add a cancel button
+
+    def updatePosition(self, fraction, estimated):
+        self.progressbar.set_fraction(fraction)
+        self.window.set_title(_("%d%% Analyzed") % int(100 * fraction))
+        if estimated:
+            # Translators: This string indicates the estimated time
+            # remaining until the action completes.  The "%s" is an
+            # already-localized human-readable duration description like
+            # "31 seconds".
+            self.progressbar.set_text(_("About %s left") % estimated)
diff --git a/pitivi/ui/mainwindow.py b/pitivi/ui/mainwindow.py
index 460e8c9..891783f 100644
--- a/pitivi/ui/mainwindow.py
+++ b/pitivi/ui/mainwindow.py
@@ -151,6 +151,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",
@@ -160,6 +161,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 f39d997..d54324a 100644
--- a/pitivi/ui/timeline.py
+++ b/pitivi/ui/timeline.py
@@ -43,6 +43,8 @@ from pitivi.utils import Seeker
 from pitivi.ui.filelisterrordialog import FileListErrorDialog
 from pitivi.ui.curve import Curve
 from pitivi.ui.common import SPACING
+from pitivi.ui.alignmentprogress import AlignmentProgressDialog
+from pitivi.timeline.align import AutoAligner
 
 from pitivi.factories.operation import EffectFactory
 
@@ -64,6 +66,7 @@ UNLINK = _("Break links between clips")
 LINK = _("Link together arbitrary clips")
 UNGROUP = _("Ungroup clips")
 GROUP = _("Group clips")
+ALIGN = _("Align clips based on their soundtracks")
 SELECT_BEFORE = ("Select all sources before selected")
 SELECT_AFTER = ("Select all after selected")
 
@@ -87,6 +90,7 @@ ui = '''
                 <menuitem action="UnlinkObj" />
                 <menuitem action="GroupObj" />
                 <menuitem action="UngroupObj" />
+                <menuitem action="AlignObj" />
                 <separator />
                 <menuitem action="Prevframe" />
                 <menuitem action="Nextframe" />
@@ -104,6 +108,7 @@ ui = '''
             <toolitem action="LinkObj" />
             <toolitem action="GroupObj" />
             <toolitem action="UngroupObj" />
+            <toolitem action="AlignObj" />
         </placeholder>
     </toolbar>
     <accelerator action="DeleteObj" />
@@ -326,6 +331,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 = (
@@ -350,6 +357,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")
@@ -713,6 +721,7 @@ class Timeline(gtk.Table, Loggable, Zoomable):
         unlink = False
         group = False
         ungroup = False
+        align = False
         split = False
         keyframe = False
         if timeline.selection:
@@ -720,6 +729,7 @@ class Timeline(gtk.Table, Loggable, Zoomable):
             if len(timeline.selection) > 1:
                 link = True
                 group = True
+                align = AutoAligner.canAlign(timeline.selection)
 
             start = None
             duration = None
@@ -748,6 +758,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)
 
@@ -789,6 +800,21 @@ class Timeline(gtk.Table, Loggable, Zoomable):
         if self.timeline:
             self.timeline.groupSelection()
 
+    def alignSelected(self, unused_action):
+        if self.timeline:
+            progress_dialog = AlignmentProgressDialog(self.app)
+            progress_dialog.window.show()
+            self.app.action_log.begin("align")
+            self.timeline.disableUpdates()
+
+            def alignedCb():  # Called when alignment is complete
+                self.timeline.enableUpdates()
+                self.app.action_log.commit()
+                progress_dialog.window.destroy()
+
+            pmeter = self.timeline.alignSelection(alignedCb)
+            pmeter.addWatcher(progress_dialog.updatePosition)
+
     def split(self, action):
         self.app.action_log.begin("split")
         self.timeline.disableUpdates()
diff --git a/pitivi/utils.py b/pitivi/utils.py
index 7035e04..be1554c 100644
--- a/pitivi/utils.py
+++ b/pitivi/utils.py
@@ -108,6 +108,20 @@ def beautify_ETA(length):
     return ", ".join(parts)
 
 
+def call_false(function, *args, **kwargs):
+    """ Helper function for calling an arbitrary function once in the gobject
+        mainloop.  Any positional or keyword arguments after the function will
+        be provided to the function.
+
+    @param function: the function to call
+    @type function: callable({any args})
+    @returns: False
+    @rtype: bool
+    """
+    function(*args, **kwargs)
+    return False
+
+
 def bin_contains(bin, element):
     """ Returns True if the bin contains the given element, the search is recursive """
     if not isinstance(bin, gst.Bin):



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]