[pitivi/ges: 191/287] autoaligner: Move all code related to autoaligner to autoaligner.py
- From: Jean-FranÃois Fortin Tam <jfft src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi/ges: 191/287] autoaligner: Move all code related to autoaligner to autoaligner.py
- Date: Thu, 15 Mar 2012 16:41:53 +0000 (UTC)
commit b7552e7788b97c9363eff0ee748664317f046ae6
Author: Thibault Saunier <thibault saunier collabora com>
Date: Wed Jan 11 09:19:30 2012 -0300
autoaligner: Move all code related to autoaligner to autoaligner.py
pitivi/Makefile.am | 1 +
pitivi/autoaligner.py | 708 ++++++++++++++++++++++++++++++++++++++++
pitivi/timeline/timeline.py | 2 +-
pitivi/ui/Makefile.am | 1 -
pitivi/ui/alignmentprogress.py | 75 -----
pitivi/utils/Makefile.am | 2 -
pitivi/utils/align.py | 390 ----------------------
pitivi/utils/alignalgs.py | 297 -----------------
pitivi/utils/extract.py | 6 +-
9 files changed, 714 insertions(+), 768 deletions(-)
---
diff --git a/pitivi/Makefile.am b/pitivi/Makefile.am
index b78f02e..c737b38 100644
--- a/pitivi/Makefile.am
+++ b/pitivi/Makefile.am
@@ -9,6 +9,7 @@ pitividir = $(libdir)/pitivi/python/pitivi
pitivi_PYTHON = \
__init__.py \
application.py \
+ autoaligner.py \
check.py \
configure.py \
effects.py \
diff --git a/pitivi/autoaligner.py b/pitivi/autoaligner.py
new file mode 100644
index 0000000..2f759bd
--- /dev/null
+++ b/pitivi/autoaligner.py
@@ -0,0 +1,708 @@
+# 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.
+
+# TODO reimplement after GES port
+"""
+Classes for automatic alignment of L{TimelineObject}s
+"""
+
+import gobject
+import gst
+import array
+import time
+import gtk
+import os
+
+
+try:
+ import numpy
+except ImportError:
+ numpy = None
+
+from gettext import gettext as _
+
+import pitivi.configure as configure
+
+from pitivi.utils.ui import beautify_ETA
+from pitivi.utils.misc import call_false
+from pitivi.utils.extract import Extractee
+from pitivi.utils.loggable import Loggable
+
+
+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
+
+
+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}
+
+ """
+ # numpy is a "soft dependency". If you're running without numpy,
+ # this False return value is your only warning not to
+ # use the AutoAligner, which will crash immediately.
+ 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
+
+
+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)
+
+
+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/timeline.py b/pitivi/timeline/timeline.py
index b010640..0d9f6e0 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -35,6 +35,7 @@ from gettext import gettext as _
from pitivi.check import soft_deps
from pitivi.effects import AUDIO_EFFECT, VIDEO_EFFECT
+from pitivi.autoaligner import AlignmentProgressDialog
from pitivi.settings import GlobalSettings
@@ -44,7 +45,6 @@ from pitivi.utils.timeline import Controller, MoveContext, SELECT, Zoomable
from pitivi.ui.depsmanager import DepsManager
from pitivi.ui.filelisterrordialog import FileListErrorDialog
-from pitivi.ui.alignmentprogress import AlignmentProgressDialog
from pitivi.ui.prefs import PreferencesDialog
from pitivi.utils.receiver import receiver, handler
diff --git a/pitivi/ui/Makefile.am b/pitivi/ui/Makefile.am
index a25aee6..cb34030 100644
--- a/pitivi/ui/Makefile.am
+++ b/pitivi/ui/Makefile.am
@@ -2,7 +2,6 @@ uidir = $(libdir)/pitivi/python/pitivi/ui
ui_PYTHON = \
__init__.py \
- alignmentprogress.py \
depsmanager.py \
filelisterrordialog.py \
mainwindow.py \
diff --git a/pitivi/utils/Makefile.am b/pitivi/utils/Makefile.am
index 7e09451..11c2d40 100644
--- a/pitivi/utils/Makefile.am
+++ b/pitivi/utils/Makefile.am
@@ -2,8 +2,6 @@ utilsdir = $(libdir)/pitivi/python/pitivi/utils
utils_PYTHON = \
__init__.py \
- align.py \
- alignalgs.py \
extract.py \
timeline.py \
loggable.py \
diff --git a/pitivi/utils/extract.py b/pitivi/utils/extract.py
index 9467b54..6c02c59 100644
--- a/pitivi/utils/extract.py
+++ b/pitivi/utils/extract.py
@@ -26,10 +26,12 @@ Classes for extracting decoded contents of streams into Python
Code derived from ui/previewer.py.
"""
+# FIXME reimplement after GES port
+
import gst
from collections import deque
-from pitivi.elements.singledecodebin import SingleDecodeBin
-from pitivi.elements.extractionsink import ExtractionSink
+#from pitivi.elements.singledecodebin import SingleDecodeBin
+#from pitivi.elements.extractionsink import ExtractionSink
from pitivi.utils.loggable import Loggable
from pitivi.utils.misc import pipeline
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]