Exemple #1
0
def test_trigger_stride_edges(cfg: CorrelationTriggerConfig):
    wave = Wave("tests/sine440.wav")
    # period = 48000 / 440 = 109.(09)*

    stride = 4
    trigger = cfg(wave, tsamp=100, stride=stride, fps=FPS)
    # real window_samp = window_samp*stride
    # period = 109

    trigger.get_trigger(0, PerFrameCache())
    trigger.get_trigger(-1000, PerFrameCache())
    trigger.get_trigger(50000, PerFrameCache())
Exemple #2
0
def test_trigger_out_of_bounds(trigger_cfg):
    """Ensure out-of-bounds triggering with stride does not crash.
    (why does stride matter? IDK.)"""
    wave = Wave("tests/sine440.wav")
    # period = 48000 / 440 = 109.(09)*

    stride = 4
    trigger = trigger_cfg(wave, tsamp=100, stride=stride, fps=FPS)
    # real window_samp = window_samp*stride
    # period = 109

    trigger.get_trigger(0, PerFrameCache())
    trigger.get_trigger(-1000, PerFrameCache())
    trigger.get_trigger(50000, PerFrameCache())
Exemple #3
0
def test_post_stride(post_trigger):
    """
    Test that stride is respected when post_trigger is disabled,
    and ignored when post_trigger is enabled.
    """
    cfg = trigger_template(post_trigger=post_trigger)

    wave = Wave("tests/sine440.wav")
    iters = 5
    x0 = 24000
    stride = 4
    trigger = cfg(wave, tsamp=100, stride=stride, fps=FPS)

    cache = PerFrameCache()
    for i in range(1, iters):
        offset = trigger.get_trigger(x0, cache)

        if not cfg.post_trigger:
            assert (offset - x0) % stride == 0, f"iteration {i}"
            assert abs(offset - x0) < 10, f"iteration {i}"

        else:
            # If assertion fails, remove it.
            assert (offset - x0) % stride != 0, f"iteration {i}"
            assert abs(offset - x0) <= 2, f"iteration {i}"
Exemple #4
0
def test_trigger(cfg: CorrelationTriggerConfig):
    wave = Wave("tests/impulse24000.wav")

    iters = 5
    plot = False
    x0 = 24000
    x = x0 - 500
    trigger: CorrelationTrigger = cfg(wave, 4000, stride=1, fps=FPS)

    if plot:
        BIG = 0.95
        SMALL = 0.05
        fig, axes = plt.subplots(iters,
                                 gridspec_kw=dict(
                                     top=BIG,
                                     right=BIG,
                                     bottom=SMALL,
                                     left=SMALL))  # type: Figure, Axes
        fig.tight_layout()
    else:
        axes = range(iters)

    for i, ax in enumerate(axes):
        if i:
            offset = trigger.get_trigger(x, PerFrameCache())
            print(offset)
            assert offset == x0
        if plot:
            ax.plot(trigger._buffer, label=str(i))
            ax.grid()

    if plot:
        plt.show()
Exemple #5
0
    def trigger(pos):
        # We have to generate a new trigger object each time, because
        # CorrelationTrigger.get_trigger() never goes backwards, which violates the
        # stride quantization we're testing for in the "if not cfg.post_trigger" branch.

        trigger = cfg(wave, tsamp=150 // stride, stride=stride, fps=FPS)
        cache = PerFrameCache()
        return trigger.get_trigger(pos, cache).result
Exemple #6
0
def test_mean_subtraction(trigger_cfg, mocker: "pytest_mock.MockFixture"):
    """
    Ensure that trigger subtracts mean properly in all configurations.
    -   Due to a regression, mean was not subtracted when sign_strength = 0.
        This caused get_period() to malfunction.
    """
    wave = Wave("tests/step2400.wav")

    get_period = mocker.spy(triggers, "get_period")
    trigger = trigger_cfg(wave, tsamp=100, stride=1, fps=FPS)
    cache = PerFrameCache()
    trigger.get_trigger(2600, cache)  # step2400.wav

    (data, *args), kwargs = get_period.call_args
    assert isinstance(data, np.ndarray)
    assert abs(np.mean(data)) < 0.01
Exemple #7
0
def test_trigger(trigger_cfg, is_odd: bool, post_trigger):
    """Ensures that trigger can locate
    the first positive sample of a -+ step exactly,
    without off-by-1 errors.

    See CorrelationTrigger and Wave.get_around() docstrings.
    """
    wave = Wave("tests/step2400.wav")
    trigger_cfg = attr.evolve(trigger_cfg, post_trigger=post_trigger)

    iters = 5
    plot = False
    x0 = 2400
    x = x0 - 50
    trigger: CorrelationTrigger = trigger_cfg(wave,
                                              400 + int(is_odd),
                                              stride=1,
                                              fps=FPS)

    if plot:
        BIG = 0.95
        SMALL = 0.05
        fig, axes = plt.subplots(iters,
                                 gridspec_kw=dict(
                                     top=BIG,
                                     right=BIG,
                                     bottom=SMALL,
                                     left=SMALL))  # type: Figure, Axes
        fig.tight_layout()
    else:
        axes = range(iters)

    for i, ax in enumerate(axes):
        if i:
            offset = trigger.get_trigger(x, PerFrameCache())
            assert offset == x0, offset
        if plot:
            ax.plot(trigger._buffer, label=str(i))
            ax.grid()

    if plot:
        plt.show()
Exemple #8
0
def test_post_trigger_stride(post_cfg: CorrelationTriggerConfig):
    cfg = post_cfg

    wave = Wave("tests/sine440.wav")
    iters = 5
    x0 = 24000
    stride = 4
    trigger = cfg(wave, tsamp=100, stride=stride, fps=FPS)

    cache = PerFrameCache()
    for i in range(1, iters):
        offset = trigger.get_trigger(x0, cache)

        if not cfg.post:
            assert (offset - x0) % stride == 0, f"iteration {i}"
            assert abs(offset - x0) < 10, f"iteration {i}"

        else:
            # If assertion fails, remove it.
            assert (offset - x0) % stride != 0, f"iteration {i}"
            assert abs(offset - x0) <= 2, f"iteration {i}"
Exemple #9
0
def test_post_trigger_radius():
    """
    Ensure ZeroCrossingTrigger has no off-by-1 errors when locating edges,
    and slides at a fixed rate if no edge is found.
    """
    wave = Wave("tests/step2400.wav")
    center = 2400
    radius = 5

    cfg = ZeroCrossingTriggerConfig()
    post = cfg(wave, radius, 1, FPS)

    cache = PerFrameCache(mean=0)

    for offset in range(-radius, radius + 1):
        assert post.get_trigger(center + offset, cache) == center, offset

    for offset in [radius + 1, radius + 2, 100]:
        assert post.get_trigger(center - offset,
                                cache) == center - offset + radius
        assert post.get_trigger(center + offset,
                                cache) == center + offset - radius
Exemple #10
0
def test_trigger_stride(cfg: CorrelationTriggerConfig):
    wave = Wave("tests/sine440.wav")
    # period = 48000 / 440 = 109.(09)*

    iters = 5
    x0 = 24000
    stride = 4
    trigger = cfg(wave, tsamp=100, stride=stride, fps=FPS)
    # real window_samp = window_samp*stride
    # period = 109

    cache = PerFrameCache()

    for i in range(1, iters):
        offset = trigger.get_trigger(x0, cache)

        # Debugging CorrelationTrigger.get_trigger:
        # from matplotlib import pyplot as plt
        # plt.plot(data)
        # plt.plot(prev_buffer)
        # plt.plot(corr)

        # When i=0, the data has 3 peaks, the rightmost taller than the center. The
        # *tips* of the outer peaks are truncated between `left` and `right`.
        # After truncation, corr[mid+1] is almost identical to corr[mid], for
        # reasons I don't understand (mid+1 > mid because dithering?).
        if not cfg.use_edge_trigger:
            assert (offset - x0) % stride == 0, f"iteration {i}"
            assert abs(offset - x0) < 10, f"iteration {i}"

        # The edge trigger activates at x0+1=24001. Likely related: it triggers
        # when moving from <=0 to >0. This is a necessary evil, in order to
        # recognize 0-to-positive edges while testing tests/impulse24000.wav .

        else:
            # If assertion fails, remove it.
            assert (offset - x0) % stride != 0, f"iteration {i}"
            assert abs(offset - x0) <= 2, f"iteration {i}"
Exemple #11
0
def test_trigger_direction(post_trigger, double_negate):
    """
    Right now, MainTrigger is responsible for negating wave.amplification
    if edge_direction == -1.
    And triggers should not actually access edge_direction.
    """

    index = 2400
    wave = Wave("tests/step2400.wav")

    if double_negate:
        wave.amplification = -1
        cfg = trigger_template(post_trigger=post_trigger, edge_direction=-1)
    else:
        cfg = trigger_template(post_trigger=post_trigger)

    trigger = cfg(wave, 100, 1, FPS)
    cfg.edge_direction = None
    assert trigger._wave.amplification == 1

    cache = PerFrameCache()
    for dx in [-10, 10, 0]:
        assert trigger.get_trigger(index + dx, cache) == index
Exemple #12
0
    def play(self) -> None:
        if self.has_played:
            raise ValueError("Cannot call CorrScope.play() more than once")
        self.has_played = True

        self._load_channels()
        # Calculate number of frames (TODO master file?)
        fps = self.cfg.fps

        begin_frame = round(fps * self.cfg.begin_time)

        end_time = coalesce(
            self.cfg.end_time, max(wave.get_s() for wave in self.render_waves)
        )
        end_frame = fps * end_time
        end_frame = int(end_frame) + 1

        self.arg.on_begin(self.cfg.begin_time, end_time)

        renderer = self._load_renderer()
        self.renderer = renderer  # only used for unit tests

        renderer.add_labels([channel.label for channel in self.channels])

        # For debugging only
        # for trigger in self.triggers:
        #     trigger.set_renderer(renderer)

        if PRINT_TIMESTAMP:
            begin = time.perf_counter()

        benchmark_mode = self.cfg.benchmark_mode
        not_benchmarking = not benchmark_mode

        with self._load_outputs():
            prev = -1

            # When subsampling FPS, render frames from the future to alleviate lag.
            # subfps=1, ahead=0.
            # subfps=2, ahead=1.
            render_subfps = self.cfg.render_subfps
            ahead = render_subfps // 2

            # For each frame, render each wave
            for frame in range(begin_frame, end_frame):
                if self.arg.is_aborted():
                    # Used for FPS calculation
                    end_frame = frame

                    for output in self.outputs:
                        output.terminate()
                    break

                time_seconds = frame / fps
                should_render = (frame - begin_frame) % render_subfps == ahead

                rounded = int(time_seconds)
                if PRINT_TIMESTAMP and rounded != prev:
                    self.arg.progress(rounded)
                    prev = rounded

                render_inputs = []
                trigger_samples = []
                # Get render-data from each wave.
                for render_wave, channel in zip(self.render_waves, self.channels):
                    sample = round(render_wave.smp_s * time_seconds)

                    # Get trigger.
                    if not_benchmarking or benchmark_mode == BenchmarkMode.TRIGGER:
                        cache = PerFrameCache()

                        result = channel.trigger.get_trigger(sample, cache)
                        trigger_sample = result.result
                        freq_estimate = result.freq_estimate

                    else:
                        trigger_sample = sample
                        freq_estimate = 0

                    # Get render data.
                    if should_render:
                        trigger_samples.append(trigger_sample)
                        data = channel.get_render_around(trigger_sample)
                        render_inputs.append(RenderInput(data, freq_estimate))

                if not should_render:
                    continue

                if not_benchmarking or benchmark_mode >= BenchmarkMode.RENDER:
                    # Render frame
                    renderer.update_main_lines(render_inputs, trigger_samples)
                    frame_data = renderer.get_frame()

                    if not_benchmarking or benchmark_mode == BenchmarkMode.OUTPUT:
                        # Output frame
                        aborted = False
                        for output in self.outputs:
                            if output.write_frame(frame_data) is outputs_.Stop:
                                aborted = True
                                break
                        if aborted:
                            # Outputting frame happens after most computation finished.
                            end_frame = frame + 1
                            break

        if PRINT_TIMESTAMP:
            # noinspection PyUnboundLocalVariable
            dtime_sec = time.perf_counter() - begin
            dframe = end_frame - begin_frame

            frame_per_sec = dframe / dtime_sec
            try:
                msec_per_frame = 1000 * dtime_sec / dframe
            except ZeroDivisionError:
                msec_per_frame = float("inf")

            print(f"{frame_per_sec:.1f} FPS, {msec_per_frame:.2f} ms/frame")
Exemple #13
0
    def play(self) -> None:
        if self.has_played:
            raise ValueError("Cannot call CorrScope.play() more than once")
        self.has_played = True

        self._load_channels()
        # Calculate number of frames (TODO master file?)
        fps = self.cfg.fps

        begin_frame = round(fps * self.cfg.begin_time)

        end_time = coalesce(self.cfg.end_time, self.render_waves[0].get_s())
        end_frame = fps * end_time
        end_frame = int(end_frame) + 1

        self.arg.on_begin(self.cfg.begin_time, end_time)

        renderer = self._load_renderer()
        self.renderer = renderer  # only used for unit tests

        # region show_internals
        # Display buffers, for debugging purposes.
        internals = self.cfg.show_internals
        extra_outputs = SimpleNamespace()
        if internals:
            from corrscope.outputs import FFplayOutputConfig
            import attr

            no_audio = attr.evolve(self.cfg, master_audio="")

            corr = self

            class RenderOutput:
                def __init__(self):
                    self.renderer = corr._load_renderer()
                    self.output = FFplayOutputConfig()(no_audio)

                def render_frame(self, datas):
                    self.renderer.render_frame(datas)
                    self.output.write_frame(self.renderer.get_frame())

        extra_outputs.window = None
        if "window" in internals:
            extra_outputs.window = RenderOutput()

        extra_outputs.buffer = None
        if "buffer" in internals:
            extra_outputs.buffer = RenderOutput()
        # endregion

        if PRINT_TIMESTAMP:
            begin = time.perf_counter()

        benchmark_mode = self.cfg.benchmark_mode
        not_benchmarking = not benchmark_mode

        with self._load_outputs():
            prev = -1

            # When subsampling FPS, render frames from the future to alleviate lag.
            # subfps=1, ahead=0.
            # subfps=2, ahead=1.
            render_subfps = self.cfg.render_subfps
            ahead = render_subfps // 2

            # For each frame, render each wave
            for frame in range(begin_frame, end_frame):
                if self.arg.is_aborted():
                    # Used for FPS calculation
                    end_frame = frame

                    for output in self.outputs:
                        output.terminate()
                    break

                time_seconds = frame / fps
                should_render = (frame - begin_frame) % render_subfps == ahead

                rounded = int(time_seconds)
                if PRINT_TIMESTAMP and rounded != prev:
                    self.arg.progress(rounded)
                    prev = rounded

                render_datas = []
                # Get render-data from each wave.
                for render_wave, channel in zip(self.render_waves, self.channels):
                    sample = round(render_wave.smp_s * time_seconds)

                    # Get trigger.
                    if not_benchmarking or benchmark_mode == BenchmarkMode.TRIGGER:
                        cache = PerFrameCache()
                        trigger_sample = channel.trigger.get_trigger(sample, cache)
                    else:
                        trigger_sample = sample

                    # Get render data.
                    if should_render:
                        render_datas.append(
                            render_wave.get_around(
                                trigger_sample,
                                channel.render_samp,
                                channel.render_stride,
                            )
                        )

                if not should_render:
                    continue

                # region Display buffers, for debugging purposes.
                if extra_outputs.window:
                    triggers = cast(List[CorrelationTrigger], self.triggers)
                    extra_outputs.window.render_frame(
                        [trigger._prev_window for trigger in triggers]
                    )

                if extra_outputs.buffer:
                    triggers = cast(List[CorrelationTrigger], self.triggers)
                    extra_outputs.buffer.render_frame(
                        [trigger._buffer for trigger in triggers]
                    )
                # endregion

                if not_benchmarking or benchmark_mode >= BenchmarkMode.RENDER:
                    # Render frame
                    renderer.render_frame(render_datas)
                    frame_data = renderer.get_frame()

                    if not_benchmarking or benchmark_mode == BenchmarkMode.OUTPUT:
                        # Output frame
                        aborted = False
                        for output in self.outputs:
                            if output.write_frame(frame_data) is outputs_.Stop:
                                aborted = True
                                break
                        if aborted:
                            # Outputting frame happens after most computation finished.
                            end_frame = frame + 1
                            break

            if self.raise_on_teardown:
                raise self.raise_on_teardown

        if PRINT_TIMESTAMP:
            # noinspection PyUnboundLocalVariable
            dtime = time.perf_counter() - begin
            render_fps = (end_frame - begin_frame) / dtime
            print(f"{render_fps:.1f} FPS, {1000 / render_fps:.2f} ms")