Esempio n. 1
0
def color2hex(color: Any) -> str:
    try:
        return matplotlib.colors.to_hex(color, keep_alpha=False)
    except ValueError:
        raise CorrError(f"invalid color {color}")
    except Exception as e:
        raise CorrError(f"doubly invalid color {color}, raises {e} (report bug!)")
Esempio n. 2
0
    def __attrs_post_init__(self):
        if self.edge_direction not in [-1, 1]:
            raise CorrError(f"{obj_name(self)}.edge_direction must be {{-1, 1}}")

        if self.post_trigger:
            self.post_trigger.parent = self
            if self.post_radius is None:
                name = obj_name(self)
                raise CorrError(
                    f"Cannot supply {name}.post_trigger without supplying {name}.post_radius"
                )
Esempio n. 3
0
    def __init__(self, *args, **kwargs):
        Trigger.__init__(self, *args, **kwargs)

        if self._stride != 1:
            raise CorrError(
                f"{obj_name(self)} with stride != 1 is not allowed "
                f"(supplied {self._stride})")

        if self.post:
            raise CorrError(
                f"Passing {obj_name(self)} a post_trigger is not allowed "
                f"({obj_name(self.post)})")
Esempio n. 4
0
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        if self._stride != 1:
            raise CorrError(
                f"{obj_name(self)} with stride != 1 is not allowed "
                f"(supplied {self._stride})")
Esempio n. 5
0
    def __init__(self, cfg: Config, arg: Arguments):
        """cfg is mutated!
        Recording config is triggered if any FFmpegOutputConfig is found.
        Preview mode is triggered if all outputs are FFplay or others.
        """
        self.cfg = cfg
        self.arg = arg
        self.has_played = False

        # TODO test progress and is_aborted
        # TODO benchmark_mode/not_benchmarking == code duplication.
        benchmark_mode = self.cfg.benchmark_mode
        not_benchmarking = not benchmark_mode

        if not_benchmarking or benchmark_mode == BenchmarkMode.OUTPUT:
            self.output_cfgs = arg.outputs
        else:
            self.output_cfgs = []  # type: List[IOutputConfig]

        if len(self.cfg.channels) == 0:
            raise CorrError("Config.channels is empty")

        # Check for ffmpeg video recording, then mutate cfg.
        is_record = False
        for output in self.output_cfgs:
            if isinstance(output, outputs_.FFmpegOutputConfig):
                is_record = True
                break
        if is_record:
            self.cfg.before_record()
        else:
            self.cfg.before_preview()
Esempio n. 6
0
def calc_flatten_matrix(flatten: FlattenOrStr,
                        stereo_nchan: int) -> np.ndarray:
    """Raises CorrError on invalid input.

    If flatten is Flatten.Stereo, returns shape=(nchan,nchan) identity matrix.
    - (N,nchan) @ (nchan,nchan) = (N,nchan).

    Otherwise, returns shape=(nchan) flattening matrix.
    - (N,nchan) @ (nchan) = (N)

    https://docs.scipy.org/doc/numpy/reference/generated/numpy.matmul.html#numpy.matmul
    '''
    If the second argument is 1-D,
    it is promoted to a matrix by appending a 1 to its dimensions.
    After matrix multiplication the appended 1 is removed."
    '''
    """

    if flatten is Flatten.Stereo:
        # 2D identity (results in 2-dim data)
        flatten_matrix = np.eye(stereo_nchan, dtype=FLOAT)

    # 1D (results in 1-dim data)
    elif flatten is Flatten.SumAvg:
        flatten_matrix = np.ones(stereo_nchan, dtype=FLOAT) / stereo_nchan

    elif flatten is Flatten.DiffAvg:
        flatten_matrix = calc_flatten_matrix(str(flatten), stereo_nchan)
        flatten_matrix = rightpad(flatten_matrix, stereo_nchan, 0)

    else:
        words = flatten.replace(",", " ").split()
        try:
            flatten_matrix = np.array([FLOAT(word) for word in words])
        except ValueError as e:
            raise CorrError("Invalid stereo flattening matrix") from e

        flatten_abs_sum = np.sum(np.abs(flatten_matrix))
        if flatten_abs_sum == 0:
            raise CorrError(
                "Stereo flattening matrix must have nonzero elements")

        flatten_matrix /= flatten_abs_sum

    assert flatten_matrix.dtype == FLOAT, flatten_matrix.dtype
    return flatten_matrix
Esempio n. 7
0
 def set(self: "ConfigModel", val: int):
     if val > 0:
         setattr(self.cfg.layout, altered, val)
         setattr(self.cfg.layout, unaltered, None)
         self.update_all_bound("layout__" + unaltered)
     elif val == 0:
         setattr(self.cfg.layout, altered, None)
     else:
         raise CorrError(f"invalid input: {altered} < 0, should never happen")
Esempio n. 8
0
    def __init__(self, cfg: ChannelConfig, corr_cfg: "Config"):
        self.cfg = cfg

        # Create a Wave object.
        wave = Wave(
            abspath(cfg.wav_path),
            amplification=coalesce(cfg.amplification, corr_cfg.amplification),
        )

        # Flatten wave stereo for trigger and render.
        tflat = coalesce(cfg.trigger_stereo, corr_cfg.trigger_stereo)
        rflat = coalesce(cfg.render_stereo, corr_cfg.render_stereo)

        self.trigger_wave = wave.with_flatten(tflat, return_channels=False)
        self.render_wave = wave.with_flatten(rflat, return_channels=True)

        # `subsampling` increases `stride` and decreases `nsamp`.
        # `width` increases `stride` without changing `nsamp`.
        tsub = corr_cfg.trigger_subsampling
        tw = cfg.trigger_width

        rsub = corr_cfg.render_subsampling
        rw = cfg.render_width

        # nsamp = orig / subsampling
        # stride = subsampling * width
        def calculate_nsamp(width_ms, sub):
            width_s = width_ms / 1000
            return round(width_s * wave.smp_s / sub)

        trigger_samp = calculate_nsamp(corr_cfg.trigger_ms, tsub)
        self.render_samp = calculate_nsamp(corr_cfg.render_ms, rsub)

        self.trigger_stride = tsub * tw
        self.render_stride = rsub * rw

        # Create a Trigger object.
        if isinstance(cfg.trigger, ITriggerConfig):
            tcfg = cfg.trigger
        elif isinstance(
                cfg.trigger,
            (CommentedMap, dict)):  # CommentedMap may/not be subclass of dict.
            tcfg = attr.evolve(corr_cfg.trigger, **cfg.trigger)
        elif cfg.trigger is None:
            tcfg = corr_cfg.trigger
        else:
            raise CorrError(
                f"invalid per-channel trigger {cfg.trigger}, type={type(cfg.trigger)}, "
                f"must be (*)TriggerConfig, dict, or None")

        self.trigger = tcfg(
            wave=self.trigger_wave,
            tsamp=trigger_samp,
            stride=self.trigger_stride,
            fps=corr_cfg.fps,
        )
Esempio n. 9
0
    def __attrs_post_init__(self) -> None:
        if not self.nrows:
            self.nrows = None
        if not self.ncols:
            self.ncols = None

        if self.nrows and self.ncols:
            raise CorrError("cannot manually assign both nrows and ncols")

        if not self.nrows and not self.ncols:
            self.ncols = 1
Esempio n. 10
0
    def flatten(self, flatten: Flatten) -> None:
        # Reject invalid modes (including Mono).
        if flatten not in Flatten.modes:  # type: ignore
            # Flatten.Mono not in Flatten.modes.
            raise CorrError(
                f"Wave {self.wave_path} has invalid flatten mode {flatten} "
                f"not in {Flatten.modes}")

        # If self.is_mono, converts all non-Stereo modes to Mono.
        self._flatten = flatten
        if self.is_mono and flatten != Flatten.Stereo:
            self._flatten = Flatten.Mono
Esempio n. 11
0
    def __init__(
        self,
        wave_path: str,
        amplification: float = 1.0,
        flatten: Flatten = Flatten.SumAvg,
    ):
        self.wave_path = wave_path
        self.amplification = amplification
        self.smp_s, self.data = wavfile.read(wave_path, mmap=True)

        assert self.data.ndim in [1, 2]
        self.is_mono = self.data.ndim == 1
        self.flatten = flatten
        self.return_channels = False

        # Cast self.data to stereo (nsamp, nchan)
        if self.is_mono:
            self.data.shape = (-1, 1)

        self.nsamp, stereo_nchan = self.data.shape
        if stereo_nchan > 2:
            warnings.warn(
                f"File {wave_path} has {stereo_nchan} channels, "
                f"only first 2 will be used",
                CorrWarning,
            )

        dtype = self.data.dtype

        # Calculate scaling factor.
        def is_type(parent: type) -> bool:
            return np.issubdtype(dtype, parent)

        # Numpy types: https://docs.scipy.org/doc/numpy/reference/arrays.scalars.html
        if is_type(np.integer):
            max_int = np.iinfo(dtype).max + 1
            assert max_int & (max_int - 1) == 0  # power of 2

            if is_type(np.unsignedinteger):
                self.center = max_int // 2
                self.max_val = max_int // 2

            elif is_type(np.signedinteger):
                self.center = 0
                self.max_val = max_int

        elif is_type(np.floating):
            self.center = 0
            self.max_val = 1

        else:
            raise CorrError(f"unexpected wavfile dtype {dtype}")
Esempio n. 12
0
 def _load_channels(self) -> None:
     with pushd(self.arg.cfg_dir):
         # Tell user if master audio path is invalid.
         # (Otherwise, only ffmpeg uses the value of master_audio)
         # Windows likes to raise OSError when path contains *, but we don't care.
         if self.cfg.master_audio and not Path(self.cfg.master_audio).exists():
             raise CorrError(
                 f'File not found: master_audio="{self.cfg.master_audio}"'
             )
         self.channels = [Channel(ccfg, self.cfg) for ccfg in self.cfg.channels]
         self.trigger_waves = [channel.trigger_wave for channel in self.channels]
         self.render_waves = [channel.render_wave for channel in self.channels]
         self.triggers = [channel.trigger for channel in self.channels]
         self.nchan = len(self.channels)
Esempio n. 13
0
    def flatten(self, flatten: FlattenOrStr) -> None:
        # Reject invalid modes (including Mono).
        if flatten in _rejected_modes:
            # Flatten.Mono not in Flatten.modes.
            raise CorrError(
                f"Wave {self.wave_path} has invalid flatten mode {flatten} "
                f"not a numeric string, nor in {Flatten.modes}")

        # If self.is_mono, converts all non-Stereo modes to Mono.
        self._flatten = flatten
        if self.is_mono and flatten != Flatten.Stereo:
            self._flatten = Flatten.Mono

        self.flatten_matrix = calc_flatten_matrix(self._flatten,
                                                  self.stereo_nchan)
Esempio n. 14
0
    def get_trigger(self, index: int, cache: "PerFrameCache") -> int:
        N = self._buffer_nsamp

        # Get data
        data = self._wave.get_around(index, N, self._stride)
        data -= cache.mean
        normalize_buffer(data)
        data *= self._data_window

        # Window data
        if cache.period is None:
            raise CorrError(
                "Missing 'cache.period', try stacking CorrelationTrigger "
                "before LocalPostTrigger")

        # To avoid sign errors, see comment in CorrelationTrigger.get_trigger().
        corr = signal.correlate(data, self._windowed_step)
        assert len(corr) == 2 * N - 1
        mid = N - 1

        # If we're near a falling edge, don't try to make drastic changes.
        if corr[mid] < 0:
            # Give up early.
            return index

        # Don't punish negative results too much.
        # (probably useless. if corr[mid] >= 0,
        # all other negative entries will never be optimal.)
        # np.abs(corr, out=corr)

        # Subtract cost function
        cost = self._cost_norm / cache.period
        corr -= cost

        # Find optimal offset (within ±N/4)
        mid = N - 1
        radius = round(N / 4)

        left = mid - radius
        right = mid + radius + 1

        corr = corr[left:right]
        mid = mid - left

        peak_offset = np.argmax(corr) - mid  # type: int
        trigger = index + (self._stride * peak_offset)

        return trigger
Esempio n. 15
0
    def render_resolution(self, value: str):
        error = CorrError(f"invalid resolution {value}, must be WxH")

        for sep in "x*,":
            width_height = value.split(sep)
            if len(width_height) == 2:
                break
        else:
            raise error

        render = self.cfg.render
        width, height = width_height
        try:
            render.width = int(width)
            render.height = int(height)
        except ValueError:
            raise error
Esempio n. 16
0
    def __init__(self, channels: List[ChannelConfig]):
        """ Mutates `channels` and `line_color` for convenience. """
        super().__init__()
        self.channels = channels

        line_color = "line_color"

        for cfg in self.channels:
            t = cfg.trigger
            if isinstance(t, MainTriggerConfig):
                if not isinstance(t, CorrelationTriggerConfig):
                    raise CorrError(f"Loading per-channel {obj_name(t)} not supported")
                trigger_dict = attr.asdict(t)
            else:
                trigger_dict = dict(t or {})

            if line_color in trigger_dict:
                trigger_dict[line_color] = color2hex(trigger_dict[line_color])

            cfg.trigger = trigger_dict
Esempio n. 17
0
def validate_param(self, key: str, begin: float, end: float) -> None:
    value = getattr(self, key)
    if not begin <= value <= end:
        raise CorrError(
            f"Invalid {key}={value} (should be within [{begin}, {end}])")
Esempio n. 18
0
    def __init__(self, cfg: ChannelConfig, corr_cfg: "Config", channel_idx: int = 0):
        """channel_idx counts from 0."""
        self.cfg = cfg

        self.label = cfg.label
        if not self.label:
            if corr_cfg.default_label is DefaultLabel.FileName:
                self.label = Path(cfg.wav_path).stem
            elif corr_cfg.default_label is DefaultLabel.Number:
                self.label = str(channel_idx + 1)

        # Create a Wave object.
        wave = Wave(
            abspath(cfg.wav_path),
            amplification=coalesce(cfg.amplification, corr_cfg.amplification),
        )

        # Flatten wave stereo for trigger and render.
        tflat = coalesce(cfg.trigger_stereo, corr_cfg.trigger_stereo)
        rflat = coalesce(cfg.render_stereo, corr_cfg.render_stereo)

        self.trigger_wave = wave.with_flatten(tflat, return_channels=False)
        self.render_wave = wave.with_flatten(rflat, return_channels=True)

        # `subsampling` increases `stride` and decreases `nsamp`.
        # `width` increases `stride` without changing `nsamp`.
        tsub = corr_cfg.trigger_subsampling
        tw = cfg.trigger_width

        rsub = corr_cfg.render_subsampling
        rw = cfg.render_width

        # nsamp = orig / subsampling
        # stride = subsampling * width
        def calculate_nsamp(width_ms, sub):
            width_s = width_ms / 1000
            return round(width_s * wave.smp_s / sub)

        trigger_samp = calculate_nsamp(corr_cfg.trigger_ms, tsub)
        self._render_samp = calculate_nsamp(corr_cfg.render_ms, rsub)

        self._trigger_stride = tsub * tw
        self.render_stride = rsub * rw

        # Create a Trigger object.
        if isinstance(cfg.trigger, MainTriggerConfig):
            tcfg = cfg.trigger

        elif isinstance(
            cfg.trigger, (CommentedMap, dict)
        ):  # CommentedMap may/not be subclass of dict.
            tcfg = evolve_compat(corr_cfg.trigger, **cfg.trigger)

        elif cfg.trigger is None:
            tcfg = corr_cfg.trigger

        else:
            raise CorrError(
                f"invalid per-channel trigger {cfg.trigger}, type={type(cfg.trigger)}, "
                f"must be (*)TriggerConfig, dict, or None"
            )

        self.trigger = tcfg(
            wave=self.trigger_wave,
            tsamp=trigger_samp,
            stride=self._trigger_stride,
            fps=corr_cfg.fps,
            wave_idx=channel_idx,
        )