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())
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())
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}"
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()
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
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
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()
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}"
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
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}"
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
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")
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")