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, )
def __init__( self, cfg: RendererConfig, lcfg: "LayoutConfig", dummy_datas: List[np.ndarray], channel_cfgs: Optional[List["ChannelConfig"]], channels: List["Channel"], ): self.cfg = cfg self.lcfg = lcfg self.w = cfg.divided_width self.h = cfg.divided_height # Maps a continuous variable from 0 to 1 (representing one octave) to a color. self.pitch_cmap = gen_circular_cmap(cfg.pitch_colors) self.nplots = len(dummy_datas) if self.nplots > 0: assert len(dummy_datas[0].shape) == 2, dummy_datas[0].shape self.wave_nsamps = [data.shape[0] for data in dummy_datas] self.wave_nchans = [data.shape[1] for data in dummy_datas] if channel_cfgs is None: channel_cfgs = [ChannelConfig("") for _ in range(self.nplots)] if len(channel_cfgs) != self.nplots: raise ValueError( f"cannot assign {len(channel_cfgs)} colors to {self.nplots} plots" ) self._line_params = [ LineParam( color=coalesce(ccfg.line_color, cfg.global_line_color), color_by_pitch=coalesce(ccfg.color_by_pitch, cfg.global_color_by_pitch), ) for ccfg in channel_cfgs ] # Load channel strides. if channels is not None: if len(channels) != self.nplots: raise ValueError( f"cannot assign {len(channels)} channels to {self.nplots} plots" ) self.render_strides = [ channel.render_stride for channel in channels ] else: self.render_strides = [1] * self.nplots
def __init__( self, cfg: RendererConfig, lcfg: "LayoutConfig", nplots: int, channel_cfgs: Optional[List["ChannelConfig"]], ): self.cfg = cfg self.lcfg = lcfg self.nplots = nplots # Load line colors. if channel_cfgs is not None: if len(channel_cfgs) != self.nplots: raise ValueError( f"cannot assign {len(channel_cfgs)} colors to {self.nplots} plots" ) line_colors = [cfg.line_color for cfg in channel_cfgs] else: line_colors = [None] * self.nplots self._line_params = [ LineParam(color=coalesce(color, cfg.init_line_color)) for color in line_colors ]
def __init__( self, cfg: RendererConfig, lcfg: "LayoutConfig", dummy_datas: List[np.ndarray], channel_cfgs: Optional[List["ChannelConfig"]], channels: List["Channel"], ): self.cfg = cfg self.lcfg = lcfg self.w = cfg.divided_width self.h = cfg.divided_height self.nplots = len(dummy_datas) if self.nplots > 0: assert len(dummy_datas[0].shape) == 2, dummy_datas[0].shape self.wave_nsamps = [data.shape[0] for data in dummy_datas] self.wave_nchans = [data.shape[1] for data in dummy_datas] # Load line colors. if channel_cfgs is not None: if len(channel_cfgs) != self.nplots: raise ValueError( f"cannot assign {len(channel_cfgs)} colors to {self.nplots} plots" ) line_colors = [cfg.line_color for cfg in channel_cfgs] else: line_colors = [None] * self.nplots self._line_params = [ LineParam(color=coalesce(color, cfg.init_line_color)) for color in line_colors ] # Load channel strides. if channels is not None: if len(channels) != self.nplots: raise ValueError( f"cannot assign {len(channels)} channels to {self.nplots} plots" ) self.render_strides = [ channel.render_stride for channel in channels ] else: self.render_strides = [1] * self.nplots
def render__label_qfont(self) -> QFont: qfont = QFont() qfont.setStyleHint(QFont.SansSerif) # no-op on X11 font = self.cfg.render.label_font if font.toString: qfont.fromString(font.toString) return qfont # Passing None or "" to QFont(family) results in qfont.family() = "", and # wrong font being selected (Abyssinica SIL, which appears early in the list). family = coalesce(font.family, qfont.defaultFamily()) # Font file selection qfont.setFamily(family) qfont.setBold(font.bold) qfont.setItalic(font.italic) # Font size qfont.setPointSizeF(font.size) return qfont
def test_per_channel_stereo( filename: str, global_stereo: Flatten, chan_stereo: Optional[Flatten] ): """Ensure you can enable/disable stereo on a per-channel basis.""" stereo = coalesce(chan_stereo, global_stereo) # Test render wave. cfg = template_config(render_stereo=global_stereo) ccfg = ChannelConfig("tests/stereo in-phase.wav", render_stereo=chan_stereo) channel = Channel(ccfg, cfg) # Render wave *must* return stereo. assert channel.render_wave[0:1].ndim == 2 data = channel.render_wave.get_around(0, return_nsamp=4, stride=1) assert data.ndim == 2 if "stereo" in filename: assert channel.render_wave._flatten == stereo assert data.shape[1] == (2 if stereo is Flatten.Stereo else 1)
def test_config_channel_integration( # Channel c_amplification: Optional[float], c_trigger_width: int, c_render_width: int, # Global amplification: float, trigger_ms: int, render_ms: int, tsub: int, rsub: int, default_label: DefaultLabel, override_label: bool, mocker: MockFixture, ): """ (Tautologically) verify: - channel. r_samp (given cfg) - channel.t/r_stride (given cfg.*_subsampling/*_width) - trigger._tsamp, _stride - renderer's method calls(samp, stride) - rendered label (channel.label, given cfg, corr_cfg.default_label) """ # region setup test variables corrscope.corrscope.PRINT_TIMESTAMP = False # Cleanup Hypothesis testing logs Wave = mocker.patch.object(corrscope.channel, "Wave") wave = Wave.return_value def get_around(sample: int, return_nsamp: int, stride: int): return np.zeros(return_nsamp) wave.get_around.side_effect = get_around wave.with_flatten.return_value = wave wave.nsamp = 10000 wave.smp_s = 48000 ccfg = ChannelConfig( "tests/sine440.wav", trigger_width=c_trigger_width, render_width=c_render_width, amplification=c_amplification, label="label" if override_label else "", ) def get_cfg(): return template_config( trigger_ms=trigger_ms, render_ms=render_ms, trigger_subsampling=tsub, render_subsampling=rsub, amplification=amplification, channels=[ccfg], default_label=default_label, trigger=NullTriggerConfig(), benchmark_mode=BenchmarkMode.OUTPUT, ) # endregion cfg = get_cfg() channel = Channel(ccfg, cfg) # Ensure cfg.width_ms etc. are correct assert cfg.trigger_ms == trigger_ms assert cfg.render_ms == render_ms # Ensure channel.window_samp, trigger_subsampling, render_subsampling are correct. def ideal_samp(width_ms, sub): width_s = width_ms / 1000 return pytest.approx( round(width_s * channel.trigger_wave.smp_s / sub), rel=1e-6 ) ideal_tsamp = ideal_samp(cfg.trigger_ms, tsub) ideal_rsamp = ideal_samp(cfg.render_ms, rsub) assert channel._render_samp == ideal_rsamp assert channel._trigger_stride == tsub * c_trigger_width assert channel.render_stride == rsub * c_render_width # Ensure amplification override works args, kwargs = Wave.call_args assert kwargs["amplification"] == coalesce(c_amplification, amplification) ## Ensure trigger uses channel.window_samp and _trigger_stride. trigger = channel.trigger assert trigger._tsamp == ideal_tsamp assert trigger._stride == channel._trigger_stride ## Ensure corrscope calls render using channel._render_samp and _render_stride. corr = CorrScope(cfg, Arguments(cfg_dir=".", outputs=[])) renderer = mocker.patch.object(CorrScope, "_load_renderer").return_value corr.play() # Only Channel.get_render_around() (not NullTrigger) calls wave.get_around(). (_sample, _return_nsamp, _subsampling), kwargs = wave.get_around.call_args assert _return_nsamp == channel._render_samp assert _subsampling == channel.render_stride # Inspect arguments to renderer.update_main_lines() # datas: List[np.ndarray] (datas,), kwargs = renderer.update_main_lines.call_args render_data = datas[0] assert len(render_data) == channel._render_samp # Inspect arguments to renderer.add_labels(). (labels,), kwargs = renderer.add_labels.call_args label = labels[0] if override_label: assert label == "label" else: if default_label is DefaultLabel.FileName: assert label == "sine440" elif default_label is DefaultLabel.Number: assert label == "1" else: assert label == ""
def get_label_color(self): return coalesce(self.label_color_override, self.init_line_color)
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, )
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")