def test_frontend_overrides_backend(mocker: "pytest_mock.MockFixture"): """ class Renderer inherits from (RendererFrontend, backend). RendererFrontend.get_frame() is a wrapper around backend.get_frame() and should override it (RendererFrontend should come first in MRO). Make sure RendererFrontend methods overshadow backend methods. """ # If RendererFrontend.get_frame() override is removed, delete this entire test. frontend_get_frame = mocker.spy(RendererFrontend, "get_frame") backend_get_frame = mocker.spy(AbstractMatplotlibRenderer, "get_frame") corr_cfg = template_config() chan_cfg = ChannelConfig("tests/sine440.wav") channel = Channel(chan_cfg, corr_cfg, channel_idx=0) data = channel.get_render_around(0) renderer = Renderer(corr_cfg.render, corr_cfg.layout, [data], [chan_cfg], [channel]) renderer.update_main_lines([data]) renderer.get_frame() assert frontend_get_frame.call_count == 1 assert backend_get_frame.call_count == 1
def test_renderer_knows_stride(mocker: "pytest_mock.MockFixture", integration: bool): """ If Renderer draws both "main line" and "custom mono lines" at once, each line must have its x-coordinates multiplied by the stride. Renderer uses "main line stride = 1" by default, but this results in the main line appearing too narrow compared to debug lines. Make sure CorrScope.play() gives Renderer the correct values. """ # Stub out FFplay output. mocker.patch.object(FFplayOutputConfig, "cls") subsampling = 2 width_mul = 3 chan_cfg = ChannelConfig("tests/sine440.wav", render_width=width_mul) corr_cfg = template_config( render_subsampling=subsampling, channels=[chan_cfg], end_time=0 ) if integration: corr = CorrScope(corr_cfg, Arguments(".", [FFplayOutputConfig()])) corr.play() assert corr.renderer.render_strides == [subsampling * width_mul] else: channel = Channel(chan_cfg, corr_cfg, channel_idx=0) data = channel.get_render_around(0) renderer = Renderer( corr_cfg.render, corr_cfg.layout, [data], [chan_cfg], [channel] ) assert renderer.render_strides == [subsampling * width_mul]
def sine440_config(): cfg = template_config( channels=[ChannelConfig("tests/sine440.wav")], master_audio="tests/sine440.wav", end_time=0.5, # Reduce test duration render=render_cfg, ) return cfg
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, )
def test_stereo_render_integration(mocker: "pytest_mock.MockFixture"): """Ensure corrscope plays/renders in stereo, without crashing.""" # Stub out FFplay output. mocker.patch.object(FFplayOutputConfig, "cls") # Render in stereo. cfg = template_config( channels=[ChannelConfig("tests/stereo in-phase.wav")], render_stereo=Flatten.Stereo, end_time=0.5, # Reduce test duration render=RendererConfig(WIDTH, HEIGHT), ) # Make sure it doesn't crash. corr = CorrScope(cfg, Arguments(".", [FFplayOutputConfig()])) corr.play()
def load_cfg_from_path(self, cfg_path: Path): # Bind GUI to dummy config, in case loading cfg_path raises Exception. if self.model is None: self.load_cfg(template_config(), None) assert cfg_path.is_file() self.pref.file_dir = str(cfg_path.parent.resolve()) # Raises YAML structural exceptions cfg = yaml.load(cfg_path) try: # Raises color getter exceptions self.load_cfg(cfg, cfg_path) except Exception as e: # FIXME if error halfway, clear "file path" and load empty model. TracebackDialog(self).showMessage(format_stack_trace(e)) return
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 on_action_new(self): if not self.should_close_document(self.tr("New Project")): return cfg = template_config() self.load_cfg(cfg, None)
if not shutil.which("ffmpeg"): pytestmark = pytest.mark.xfail( reason="Missing ffmpeg, ignoring failed output tests", raises=FileNotFoundError, # includes MissingFFmpegError strict=False, ) class DummyException(Exception): pass NULL_FFMPEG_OUTPUT = FFmpegOutputConfig(None, "-f null") render_cfg = RendererConfig(WIDTH, HEIGHT) CFG = template_config(render=render_cfg) def sine440_config(): cfg = template_config( channels=[ChannelConfig("tests/sine440.wav")], master_audio="tests/sine440.wav", end_time=0.5, # Reduce test duration render=render_cfg, ) return cfg ## Begin tests # Calls MatplotlibRenderer, FFmpegOutput, FFmpeg. def test_render_output():
def test_gui_init(): app = qw.QApplication([]) cfg = template_config() gui.MainWindow(cfg)
def main( files: Tuple[str], # cfg audio: Optional[str], # gui write: bool, play: bool, render: bool, profile: bool, ): """Intelligent oscilloscope visualizer for .wav files. FILES can be one or more .wav files (or wildcards), one folder, or one .yaml config. """ # GUI: # corrscope # corrscope file.yaml # corrscope wildcard/wav/folder ... [--options] # # CLI: # corrscope wildcard/wav/folder ... [--options] --write-cfg file.yaml [--play] # corrscope wildcard/wav/folder ... --play # corrscope file.yaml --play # corrscope file.yaml --write-yaml # # - You can specify as many wildcards or wav files as you want. # - You can only supply one folder, with no files/wildcards. show_gui = not any([write, play, render]) # Gather data for cfg: Config object. CfgOrPath = Union[Config, Path] cfg_or_path: Union[Config, Path, None] = None cfg_dir: Optional[str] = None wav_list: List[Path] = [] for name in files: path = Path(name) # Windows likes to raise OSError when path contains * try: is_dir = path.is_dir() except OSError: is_dir = False if is_dir: # Add a directory. if len(files) > 1: # Warning is technically optional, since wav_prefix has been removed. raise click.ClickException( f'Cannot supply multiple arguments when providing folder {path}' ) matches = sorted(path.glob('*.wav')) wav_list += matches break elif path.suffix in YAML_EXTS: # Load a YAML file to cfg, and skip template_config(). if len(files) > 1: raise click.ClickException( f'Cannot supply multiple arguments when providing config {path}' ) cfg_or_path = path cfg_dir = str(path.parent) break else: # Load one or more wav files. matches = sorted(Path().glob(name)) if not matches: matches = [path] if not path.exists(): raise click.ClickException( f'Supplied nonexistent file or wildcard: {path}') wav_list += matches if not cfg_or_path: # cfg and cfg_dir are always initialized together. channels = [ChannelConfig(str(wav_path)) for wav_path in wav_list] cfg_or_path = template_config( master_audio=audio, # fps=default, channels=channels, # width_ms...trigger=default, # amplification...render=default, ) cfg_dir = '.' assert cfg_or_path is not None assert cfg_dir is not None if show_gui: def command(): from corrscope import gui return gui.gui_main(cast(CfgOrPath, cfg_or_path)) if profile: import cProfile # Pycharm can't load CProfile files with dots in the name. profile_dump_name = get_profile_dump_name('gui') cProfile.runctx('command()', globals(), locals(), profile_dump_name) else: command() else: if not files: raise click.UsageError('Must specify files or folders to play') if isinstance(cfg_or_path, Config): cfg = cfg_or_path cfg_path = None elif isinstance(cfg_or_path, Path): cfg = yaml.load(cfg_or_path) cfg_path = cfg_or_path else: assert False, cfg_or_path if write: # `corrscope file.yaml -w` should write back to same path. write_path = _get_file_name(cfg_path, cfg, ext=YAML_NAME) yaml.dump(cfg, Path(write_path)) outputs = [] # type: List[IOutputConfig] if play: outputs.append(FFplayOutputConfig()) if render: video_path = _get_file_name(cfg_path, cfg, ext=VIDEO_NAME) outputs.append(cfg.get_ffmpeg_cfg(video_path)) if outputs: arg = Arguments(cfg_dir=cfg_dir, outputs=outputs) command = lambda: CorrScope(cfg, arg).play() if profile: import cProfile # Pycharm can't load CProfile files with dots in the name. first_song_name = Path(files[0]).name.split('.')[0] profile_dump_name = get_profile_dump_name(first_song_name) cProfile.runctx('command()', globals(), locals(), profile_dump_name) else: try: command() except MissingFFmpegError as e: # Tell user how to install ffmpeg (__str__). print(e, file=sys.stderr)
def __init__(self, cfg_or_path: Union[Config, Path]): super().__init__() # Load settings. prefs_error = None try: self.pref = gp.load_prefs() if not isinstance(self.pref, gp.GlobalPrefs): raise TypeError( f"prefs.yaml contains wrong type {type(self.pref)}") except Exception as e: prefs_error = e self.pref = gp.GlobalPrefs() # Load UI. self.setupUi(self) # sets windowTitle # Bind UI buttons, etc. Functions block main thread, avoiding race conditions. self.master_audio_browse.clicked.connect(self.on_master_audio_browse) self.channelUp.add_shortcut(self.channelsGroup, "ctrl+shift+up") self.channelDown.add_shortcut(self.channelsGroup, "ctrl+shift+down") self.channelUp.clicked.connect(self.channel_view.on_channel_up) self.channelDown.clicked.connect(self.channel_view.on_channel_down) self.channelAdd.clicked.connect(self.on_channel_add) self.channelDelete.clicked.connect(self.on_channel_delete) # Bind actions. self.action_separate_render_dir.setChecked( self.pref.separate_render_dir) self.action_separate_render_dir.toggled.connect( self.on_separate_render_dir_toggled) self.action_open_config_dir.triggered.connect(self.on_open_config_dir) self.actionNew.triggered.connect(self.on_action_new) self.actionOpen.triggered.connect(self.on_action_open) self.actionSave.triggered.connect(self.on_action_save) self.actionSaveAs.triggered.connect(self.on_action_save_as) self.actionPreview.triggered.connect(self.on_action_preview) self.actionRender.triggered.connect(self.on_action_render) self.actionWebsite.triggered.connect(self.on_action_website) self.actionHelp.triggered.connect(self.on_action_help) self.actionExit.triggered.connect(qw.QApplication.closeAllWindows) # Initialize CorrScope-thread attribute. self.corr_thread: Optional[CorrThread] = None # Setup UI. self.model = ConfigModel(template_config()) self.model.edited.connect(self.on_model_edited) # Calls self.on_gui_edited() whenever GUI widgets change. map_gui(self, self.model) self.model.update_widget["render_stereo"].append( self.on_render_stereo_changed) # Bind config to UI. if isinstance(cfg_or_path, Config): self.load_cfg(cfg_or_path, None) save_dir = self.compute_save_dir(self.cfg) if save_dir: self.pref.file_dir = save_dir elif isinstance(cfg_or_path, Path): self.load_cfg_from_path(cfg_or_path) else: raise TypeError( f"argument cfg={cfg_or_path} has invalid type {obj_name(cfg_or_path)}" ) self.show() if prefs_error is not None: TracebackDialog(self).showMessage( "Warning: failed to load global preferences, resetting to default.\n" + format_stack_trace(prefs_error))