Source code for aeneas.exacttiming

#!/usr/bin/env python
# coding=utf-8

# aeneas is a Python/C library and a set of tools
# to automagically synchronize audio and text (aka forced alignment)
#
# Copyright (C) 2012-2013, Alberto Pettarin (www.albertopettarin.it)
# Copyright (C) 2013-2015, ReadBeyond Srl   (www.readbeyond.it)
# Copyright (C) 2015-2017, Alberto Pettarin (www.albertopettarin.it)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
This module contains the following classes:

* :class:`~aeneas.exacttiming.TimeValue`,
  a numeric type to represent time values with arbitrary precision.
* :class:`~aeneas.exacttiming.TimeInterval`,
  representing a time interval, that is,
  a pair ``(begin, end)`` of time points.

.. versionadded:: 1.5.0
"""

from __future__ import absolute_import
from __future__ import print_function
from decimal import Decimal
from decimal import InvalidOperation
import math
import sys


PY2 = (sys.version_info[0] == 2)


[docs]class TimeValue(Decimal): """ A numeric type to represent time values with arbitrary precision. """ TAG = u"TimeValue" def __repr__(self): return super(TimeValue, self).__repr__().replace("Decimal", "TimeValue") @property def is_integer(self): """ Return ``True`` if this time value represents an integer. :rtype: bool """ return self == int(self)
[docs] def geq_multiple(self, other): """ Return the next multiple of this time value, greater than or equal to ``other``. If ``other`` is zero, return this time value. :rtype: :class:`~aeneas.exacttiming.TimeValue` """ if other == TimeValue("0.000"): return self return int(math.ceil(other / self)) * self
# NOTE overriding so that the result # is still an instance of TimeValue def __add__(self, other, context=None): if PY2: return TimeValue(Decimal.__add__(self, other, context)) return TimeValue(Decimal.__add__(self, other)) def __div__(self, other, context=None): if PY2: return TimeValue(Decimal.__div__(self, other, context)) return TimeValue(Decimal.__div__(self, other)) def __floordiv__(self, other, context=None): if PY2: return TimeValue(Decimal.__floordiv__(self, other, context)) return TimeValue(Decimal.__floordiv__(self, other)) def __mod__(self, other, context=None): if PY2: return TimeValue(Decimal.__mod__(self, other, context)) return TimeValue(Decimal.__mod__(self, other)) def __mul__(self, other, context=None): if PY2: return TimeValue(Decimal.__mul__(self, other, context)) return TimeValue(Decimal.__mul__(self, other)) def __radd__(self, other, context=None): if PY2: return TimeValue(Decimal.__radd__(self, other, context)) return TimeValue(Decimal.__radd__(self, other)) def __rdiv__(self, other, context=None): if PY2: return TimeValue(Decimal.__rdiv__(self, other, context)) return TimeValue(Decimal.__rdiv__(self, other)) def __rfloordiv__(self, other, context=None): if PY2: return TimeValue(Decimal.__rfloordiv__(self, other, context)) return TimeValue(Decimal.__rfloordiv__(self, other)) def __rmod__(self, other, context=None): if PY2: return TimeValue(Decimal.__rmod__(self, other, context)) return TimeValue(Decimal.__rmod__(self, other)) def __rmul__(self, other, context=None): if PY2: return TimeValue(Decimal.__rmul__(self, other, context)) return TimeValue(Decimal.__rmul__(self, other)) def __rsub__(self, other, context=None): if PY2: return TimeValue(Decimal.__rsub__(self, other, context)) return TimeValue(Decimal.__rsub__(self, other)) def __rtruediv__(self, other, context=None): if PY2: return TimeValue(Decimal.__rtruediv__(self, other, context)) return TimeValue(Decimal.__rtruediv__(self, other)) def __sub__(self, other, context=None): if PY2: return TimeValue(Decimal.__sub__(self, other, context)) return TimeValue(Decimal.__sub__(self, other)) def __truediv__(self, other, context=None): if PY2: return TimeValue(Decimal.__truediv__(self, other, context)) return TimeValue(Decimal.__truediv__(self, other))
[docs]class TimeInterval(object): """ A type representing a time interval, that is, a pair ``(begin, end)`` of time points. This class has some convenience methods for calculating the length of interval, whether a given time point belongs to it, etc. .. versionadded:: 1.7.0 :param begin: the begin time :type begin: :class:`~aeneas.exacttiming.TimeValue` :param end: the end time :type end: :class:`~aeneas.exacttiming.TimeValue` :raises TypeError: if ``begin`` or ``end`` are not instances of :class:`~aeneas.exacttiming.TimeValue` :raises ValueError: if ``begin`` is negative or if ``begin`` is bigger than ``end`` """ # Relative positions of two intervals # XX_Y or XX_Z or XX_WV # X = P (point, i.e., zero-length interval) or I (non-zero-length interval) # Y = L (less), C (coincide), G (greater) # Z = L (less), B (begin), I (inside), E (end), G (greater) # WV = each W and V takes value in L, C, G, B, I, E as above # # TABLE 1 | # self: * # other: | # PP_L * | # PP_C * # PP_G | * # | # # # TABLE 2 | # self: * # other: | # PI_LL ***** | # PI_LC ***** # PI_LG ***** # PI_CG ***** # PI_GG | ***** # | # # # TABLE 3 | | # self: ***** # other: | | # IP_L * | | # IP_B * | # IP_I | * | # IP_E | * # IP_G | | * # | | # # # TABLE 4 | | # self: ***** # other: | | # II_LL ***** | | # II_LB ******** | # II_LI ********** | # II_LE ************ # II_LG ************** # | | # # # TABLE 5 | | # self: ***** # other: | | # II_BI *** | # II_BE ***** # II_BG ******* # | | # # # TABLE 6 | | # self: ***** # other: | | # II_II |***| # II_IE | *** # II_IG | *** # | | # # # TABLE 7 | | # self: ***** # other: | | # II_EG | *** # | | # # # TABLE 8 | | # self: ***** # other: | | # II_GG | | *** # | | # # RELATIVE_POSITION_PP_L = 0 RELATIVE_POSITION_PP_C = 1 RELATIVE_POSITION_PP_G = 2 RELATIVE_POSITION_PI_LL = 3 RELATIVE_POSITION_PI_LC = 4 RELATIVE_POSITION_PI_LG = 5 RELATIVE_POSITION_PI_CG = 6 RELATIVE_POSITION_PI_GG = 7 RELATIVE_POSITION_IP_L = 8 RELATIVE_POSITION_IP_B = 9 RELATIVE_POSITION_IP_I = 10 RELATIVE_POSITION_IP_E = 11 RELATIVE_POSITION_IP_G = 12 RELATIVE_POSITION_II_LL = 13 RELATIVE_POSITION_II_LB = 14 RELATIVE_POSITION_II_LI = 15 RELATIVE_POSITION_II_LE = 16 RELATIVE_POSITION_II_LG = 17 RELATIVE_POSITION_II_BI = 18 RELATIVE_POSITION_II_BE = 19 RELATIVE_POSITION_II_BG = 20 RELATIVE_POSITION_II_II = 21 RELATIVE_POSITION_II_IE = 22 RELATIVE_POSITION_II_IG = 23 RELATIVE_POSITION_II_EG = 24 RELATIVE_POSITION_II_GG = 25 INVERSE_RELATIVE_POSITION = { RELATIVE_POSITION_PP_L: RELATIVE_POSITION_PP_G, RELATIVE_POSITION_PP_C: RELATIVE_POSITION_PP_C, RELATIVE_POSITION_PP_G: RELATIVE_POSITION_PP_L, RELATIVE_POSITION_PI_LL: RELATIVE_POSITION_IP_G, RELATIVE_POSITION_PI_LC: RELATIVE_POSITION_IP_E, RELATIVE_POSITION_PI_LG: RELATIVE_POSITION_IP_I, RELATIVE_POSITION_PI_CG: RELATIVE_POSITION_IP_B, RELATIVE_POSITION_PI_GG: RELATIVE_POSITION_IP_L, RELATIVE_POSITION_IP_L: RELATIVE_POSITION_PI_GG, RELATIVE_POSITION_IP_B: RELATIVE_POSITION_PI_CG, RELATIVE_POSITION_IP_I: RELATIVE_POSITION_PI_LG, RELATIVE_POSITION_IP_E: RELATIVE_POSITION_PI_LC, RELATIVE_POSITION_IP_G: RELATIVE_POSITION_PI_LL, RELATIVE_POSITION_II_LL: RELATIVE_POSITION_II_GG, RELATIVE_POSITION_II_LB: RELATIVE_POSITION_II_EG, RELATIVE_POSITION_II_LI: RELATIVE_POSITION_II_IG, RELATIVE_POSITION_II_LE: RELATIVE_POSITION_II_IE, RELATIVE_POSITION_II_LG: RELATIVE_POSITION_II_II, RELATIVE_POSITION_II_BI: RELATIVE_POSITION_II_BG, RELATIVE_POSITION_II_BE: RELATIVE_POSITION_II_BE, RELATIVE_POSITION_II_BG: RELATIVE_POSITION_II_BI, RELATIVE_POSITION_II_II: RELATIVE_POSITION_II_LG, RELATIVE_POSITION_II_IE: RELATIVE_POSITION_II_LE, RELATIVE_POSITION_II_IG: RELATIVE_POSITION_II_LI, RELATIVE_POSITION_II_EG: RELATIVE_POSITION_II_LB, RELATIVE_POSITION_II_GG: RELATIVE_POSITION_II_LL, } TAG = u"TimeInterval" def __init__(self, begin, end): if not isinstance(begin, TimeValue): raise TypeError(u"begin is not an instance of TimeValue") if not isinstance(end, TimeValue): raise TypeError(u"end is not an instance of TimeValue") if begin < 0: raise ValueError(u"begin is negative") if begin > end: raise ValueError(u"begin is bigger than end") self.begin = begin self.end = end def __eq__(self, other): if not isinstance(other, TimeInterval): return False return (self.begin, self.end) == (other.begin, other.end) def __ne__(self, other): return not (self == other) def __gt__(self, other): if not isinstance(other, TimeInterval): return False return (self.begin, self.end) > (other.begin, other.end) def __lt__(self, other): if not isinstance(other, TimeInterval): return False return (self.begin, self.end) < (other.begin, other.end) def __ge__(self, other): return (self > other) or (self == other) def __le__(self, other): return (self < other) or (self == other) def __repr__(self): return u"[%s, %s]" % (self.begin, self.end) @property def length(self): """ Return the length of this interval, that is, the difference between its end and begin values. :rtype: :class:`~aeneas.exacttiming.TimeValue` """ return self.end - self.begin @property def has_zero_length(self): """ Returns ``True`` if this interval has zero length, that is, if its begin and end values coincide. :rtype: bool """ return self.end == self.begin
[docs] def starts_at(self, time_point): """ Returns ``True`` if this interval starts at the given time point. :param time_point: the time point to test :type time_point: :class:`~aeneas.exacttiming.TimeValue` :raises TypeError: if ``time_point`` is not an instance of ``TimeValue`` :rtype: bool """ if not isinstance(time_point, TimeValue): raise TypeError(u"time_point is not an instance of TimeValue") return self.begin == time_point
[docs] def ends_at(self, time_point): """ Returns ``True`` if this interval ends at the given time point. :param time_point: the time point to test :type time_point: :class:`~aeneas.exacttiming.TimeValue` :raises TypeError: if ``time_point`` is not an instance of ``TimeValue`` :rtype: bool """ if not isinstance(time_point, TimeValue): raise TypeError(u"time_point is not an instance of TimeValue") return self.end == time_point
[docs] def percent_value(self, percent): """ Returns the time value at ``percent`` of this interval. :param percent: the percent :type percent: :class:`~aeneas.exacttiming.Decimal` :raises TypeError: if ``time_point`` is not an instance of ``TimeValue`` :rtype: :class:`~aeneas.exacttiming.TimeValue` """ if not isinstance(percent, Decimal): raise TypeError(u"percent is not an instance of Decimal") percent = Decimal(max(min(percent, 100), 0) / 100) return self.begin + self.length * percent
[docs] def offset(self, offset, allow_negative=False, min_begin_value=None, max_end_value=None): """ Move this interval by the given shift ``offset``. The begin and end time points of the translated interval are ensured to be non-negative (i.e., they are maxed with ``0.000``), unless ``allow_negative`` is set to ``True``. :param offset: the shift to be applied :type offset: :class:`~aeneas.exacttiming.TimeValue` :param allow_negative: if ``True``, allow the translated interval to have negative extrema :type allow_negative: bool :param min_begin_value: if not ``None``, specify the minimum value for the begin of the translated interval :type min_begin_value: :class:`~aeneas.exacttiming.TimeValue` :param max_begin_value: if not ``None``, specify the maximum value for the end of the translated interval :type max_begin_value: :class:`~aeneas.exacttiming.TimeValue` :raises TypeError: if ``offset`` is not an instance of ``TimeValue`` :rtype: :class:`~aeneas.exacttiming.TimeInterval` """ if not isinstance(offset, TimeValue): raise TypeError(u"offset is not an instance of TimeValue") self.begin += offset self.end += offset if not allow_negative: self.begin = max(self.begin, TimeValue("0.000")) self.end = max(self.end, TimeValue("0.000")) if (min_begin_value is not None) and (max_end_value is not None): self.begin = min(max(self.begin, min_begin_value), max_end_value) self.end = min(self.end, max_end_value) return self
[docs] def contains(self, time_point): """ Returns ``True`` if this interval contains the given time point. :param time_point: the time point to test :type time_point: :class:`~aeneas.exacttiming.TimeValue` :rtype: bool """ if not isinstance(time_point, TimeValue): raise TypeError(u"time_point is not an instance of TimeValue") return (self.begin <= time_point) and (time_point <= self.end)
[docs] def inner_contains(self, time_point): """ Returns ``True`` if this interval contains the given time point, excluding its extrema (begin and end). :param time_point: the time point to test :type time_point: :class:`~aeneas.exacttiming.TimeValue` :rtype: bool """ if not isinstance(time_point, TimeValue): raise TypeError(u"time_point is not an instance of TimeValue") return (self.begin < time_point) and (time_point < self.end)
[docs] def relative_position_of(self, other): """ Return the position of the given other time interval, relative to this time interval, as a ``RELATIVE_POSITION_*`` constant. :param other: the other interval :type other: :class:`~aeneas.exacttiming.TimeInterval` :rtype: int """ if not isinstance(other, TimeInterval): raise TypeError(u"other is not an instance of TimeInterval") if self.has_zero_length: if other.has_zero_length: # TABLE 1 if other.begin < self.begin: return self.RELATIVE_POSITION_PP_L elif other.begin == self.begin: return self.RELATIVE_POSITION_PP_C else: # other.begin > self.begin return self.RELATIVE_POSITION_PP_G else: # TABLE 2 if other.end < self.begin: return self.RELATIVE_POSITION_PI_LL elif other.end == self.begin: return self.RELATIVE_POSITION_PI_LC elif other.begin < self.begin: return self.RELATIVE_POSITION_PI_LG elif other.begin == self.begin: return self.RELATIVE_POSITION_PI_CG else: # other.begin > self.begin return self.RELATIVE_POSITION_PI_GG else: if other.has_zero_length: # TABLE 3 if other.begin < self.begin: return self.RELATIVE_POSITION_IP_L elif other.begin == self.begin: return self.RELATIVE_POSITION_IP_B elif other.begin < self.end: return self.RELATIVE_POSITION_IP_I elif other.begin == self.end: return self.RELATIVE_POSITION_IP_E else: # other.begin > self.end return self.RELATIVE_POSITION_IP_G else: if other.begin < self.begin: # TABLE 4 if other.end < self.begin: return self.RELATIVE_POSITION_II_LL elif other.end == self.begin: return self.RELATIVE_POSITION_II_LB elif other.end < self.end: return self.RELATIVE_POSITION_II_LI elif other.end == self.end: return self.RELATIVE_POSITION_II_LE else: # other.end > self.end return self.RELATIVE_POSITION_II_LG elif other.begin == self.begin: # TABLE 5 if other.end < self.end: return self.RELATIVE_POSITION_II_BI elif other.end == self.end: return self.RELATIVE_POSITION_II_BE else: # other.end > self.end return self.RELATIVE_POSITION_II_BG elif other.begin < self.end: # TABLE 6 if other.end < self.end: return self.RELATIVE_POSITION_II_II elif other.end == self.end: return self.RELATIVE_POSITION_II_IE else: # other.end > self.end return self.RELATIVE_POSITION_II_IG elif other.begin == self.end: # TABLE 7 return self.RELATIVE_POSITION_II_EG else: # other.begin > self.end # TABLE 8 return self.RELATIVE_POSITION_II_GG
[docs] def relative_position_wrt(self, other): """ Return the position of this interval, relative to the given other time interval, as a ``RELATIVE_POSITION_*`` constant. :param other: the other interval :type other: :class:`~aeneas.exacttiming.TimeInterval` :rtype: int """ return self.INVERSE_RELATIVE_POSITION[self.relative_position_of(other)]
[docs] def intersection(self, other): """ Return the intersection between this time interval and the given time interval, or ``None`` if the two intervals do not overlap. :rtype: :class:`~aeneas.exacttiming.TimeInterval` or ``NoneType`` """ relative_position = self.relative_position_of(other) if relative_position in [ self.RELATIVE_POSITION_PP_C, self.RELATIVE_POSITION_PI_LC, self.RELATIVE_POSITION_PI_LG, self.RELATIVE_POSITION_PI_CG, self.RELATIVE_POSITION_IP_B, self.RELATIVE_POSITION_II_LB, ]: return TimeInterval(begin=self.begin, end=self.begin) if relative_position in [ self.RELATIVE_POSITION_IP_E, self.RELATIVE_POSITION_II_EG, ]: return TimeInterval(begin=self.end, end=self.end) if relative_position in [ self.RELATIVE_POSITION_II_BI, self.RELATIVE_POSITION_II_BE, self.RELATIVE_POSITION_II_II, self.RELATIVE_POSITION_II_IE, ]: return TimeInterval(begin=other.begin, end=other.end) if relative_position in [ self.RELATIVE_POSITION_IP_I, self.RELATIVE_POSITION_II_LI, self.RELATIVE_POSITION_II_LE, self.RELATIVE_POSITION_II_LG, self.RELATIVE_POSITION_II_BG, self.RELATIVE_POSITION_II_IG, ]: begin = max(self.begin, other.begin) end = min(self.end, other.end) return TimeInterval(begin=begin, end=end) return None
[docs] def overlaps(self, other): """ Return ``True`` if the given time interval overlaps this time interval (possibly only at an extremum). :param other: the other interval :type other: :class:`~aeneas.exacttiming.TimeInterval` :rtype: bool """ return self.intersection(other) is not None
[docs] def is_non_zero_before_non_zero(self, other): """ Return ``True`` if this time interval ends when the given other time interval begins, and both have non zero length. :param other: the other interval :type other: :class:`~aeneas.exacttiming.TimeInterval` :raises TypeError: if ``other`` is not an instance of ``TimeInterval`` :rtype: bool """ return self.is_adjacent_before(other) and (not self.has_zero_length) and (not other.has_zero_length)
[docs] def is_non_zero_after_non_zero(self, other): """ Return ``True`` if this time interval begins when the given other time interval ends, and both have non zero length. :param other: the other interval :type other: :class:`~aeneas.exacttiming.TimeInterval` :raises TypeError: if ``other`` is not an instance of ``TimeInterval`` :rtype: bool """ return other.is_non_zero_before_non_zero(self)
[docs] def is_adjacent_before(self, other): """ Return ``True`` if this time interval ends when the given other time interval begins. :param other: the other interval :type other: :class:`~aeneas.exacttiming.TimeInterval` :raises TypeError: if ``other`` is not an instance of ``TimeInterval`` :rtype: bool """ if not isinstance(other, TimeInterval): raise TypeError(u"other is not an instance of TimeInterval") return (self.end == other.begin)
[docs] def is_adjacent_after(self, other): """ Return ``True`` if this time interval begins when the given other time interval ends. :param other: the other interval :type other: :class:`~aeneas.exacttiming.TimeInterval` :raises TypeError: if ``other`` is not an instance of ``TimeInterval`` :rtype: bool """ return other.is_adjacent_before(self)
def shadow(self, quantity): if quantity <= 0: raise ValueError(u"quantity is not positive") begin = max(self.begin - quantity, TimeValue("0.000")) end = self.end + quantity return TimeInterval(begin=begin, end=end) def shrink(self, quantity, from_begin=True): if quantity <= 0: raise ValueError(u"quantity is not positive") if quantity > self.length: raise ValueError(u"quantity is greater than length") if from_begin: self.begin = self.end - self.length + quantity else: self.end = self.begin + self.length - quantity def enlarge(self, quantity, from_begin=True): if quantity <= 0: raise ValueError(u"quantity is not positive") if from_begin: self.begin -= quantity else: self.end += quantity def move_end_at(self, point): if point < self.begin: raise ValueError(u"point is before begin") length = self.length self.end = point self.begin = self.end - length def move_begin_at(self, point): if point > self.end: raise ValueError(u"point is after end") length = self.length self.begin = point self.end = self.begin + length