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())
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 on_action_preview(self): """ Launch CorrScope and ffplay. """ error_msg = "Cannot play, another play/render is active" if self.corr_thread is not None: qw.QMessageBox.critical(self, "Error", error_msg) return outputs = [FFplayOutputConfig()] self.play_thread(outputs, dlg=None)
def on_action_preview(self): """Launch CorrScope and ffplay.""" if self.corr_thread is not None: error_msg = self.tr("Cannot preview, another {} is active").format( self.preview_or_render) qw.QMessageBox.critical(self, "Error", error_msg) return outputs = [FFplayOutputConfig()] self.play_thread(outputs, PreviewOrRender.preview, dlg=None)
def test_corr_terminate_works(): """ Ensure that ffmpeg/ffplay terminate quickly after Python exceptions, when `popen.terminate()` is called. """ cfg = sine440_config() corr = CorrScope(cfg, Arguments(".", [FFplayOutputConfig()])) corr.raise_on_teardown = DummyException with pytest.raises(DummyException): # Raises `subprocess.TimeoutExpired` if popen.terminate() doesn't work. corr.play()
def test_close_output(Popen): """ FFplayOutput unit test: Ensure ffmpeg and ffplay are terminated when Python exceptions occur. """ ffplay_cfg = FFplayOutputConfig() output: FFplayOutput with ffplay_cfg(CFG) as output: pass output._pipeline[0].stdin.close.assert_called() for popen in output._pipeline: popen.wait.assert_called() # Does wait() need to be called?
def test_terminate_ffplay(Popen): """ FFplayOutput unit test: Ensure ffmpeg and ffplay are terminated when Python exceptions occur. """ ffplay_cfg = FFplayOutputConfig() try: output: FFplayOutput with ffplay_cfg(CFG) as output: raise DummyException except DummyException: for popen in output._pipeline: popen.terminate.assert_called()
def test_corr_terminate_ffplay(Popen, mocker: "pytest_mock.MockFixture"): """ Integration test: Ensure corrscope calls terminate() on ffmpeg and ffplay when Python exceptions occur. """ cfg = sine440_config() corr = CorrScope(cfg, Arguments(".", [FFplayOutputConfig()])) render_frame = mocker.patch.object(MatplotlibRenderer, "render_frame") render_frame.side_effect = DummyException() with pytest.raises(DummyException): corr.play() assert len(corr.outputs) == 1 output: FFplayOutput = corr.outputs[0] for popen in output._pipeline: popen.terminate.assert_called()
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 test_closing_ffplay_stops_main(Popen, errno_id): """ Closing FFplay should make FFplayOutput.write_frame() return Stop to main loop. """ # Create mocks. exc = OSError(errno_id, "Simulated ffplay-closed error") if errno_id == errno.EPIPE: assert type(exc) == BrokenPipeError Popen.set_exception(exc) assert Popen.side_effect # Launch corrscope with FFplayOutputConfig()(CFG) as output: # Writing to Popen instance raises exc. ret = output.write_frame(b"") # Ensure FFplayOutput catches OSError. # Also ensure it returns Stop after exception. assert ret is Stop, ret
def test_closing_ffplay_stops_main(mocker: "pytest_mock.MockFixture", errno_id): """ Closing FFplay should make FFplayOutput.write_frame() return Stop to main loop. """ # Create mocks. exc = OSError(errno_id, "Simulated ffplay-closed error") if errno_id == errno.EPIPE: assert type(exc) == BrokenPipeError # Yo Mock, I herd you like not working properly, # so I put a test in your test so I can test your mocks while I test my code. Popen = exception_Popen(mocker, exc) assert Popen is subprocess.Popen assert Popen.side_effect # Launch corrscope with FFplayOutputConfig()(CFG) as output: ret = output.write_frame(b"") # Ensure FFplayOutput catches OSError. # Also ensure it returns Stop after exception. assert ret is Stop, ret
cfg.render.height = 108 cfg.render.res_divisor = 1.5 return cfg def previews_records(mocker): """Returns 2 lists of method MagicMock.""" configs = (Config, RendererConfig) previews = [mocker.spy(cls, "before_preview") for cls in configs] records = [mocker.spy(cls, "before_record") for cls in configs] return previews, records NO_FFMPEG = [[], [FFplayOutputConfig()]] @pytest.mark.usefixtures("Popen" ) # Prevents FFplayOutput from launching processes. @pytest.mark.parametrize("outputs", NO_FFMPEG) def test_preview_performance(Popen, mocker: "pytest_mock.MockFixture", outputs): """ Ensure performance optimizations enabled if all outputs are FFplay or others. """ get_frame = mocker.spy(MatplotlibRenderer, "get_frame") previews, records = previews_records(mocker) cfg = cfg_192x108() corr = CorrScope(cfg, Arguments(".", outputs)) corr.play()
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 default_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 = default_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: write_path = get_path(audio, YAML_NAME) yaml.dump(cfg, write_path) outputs = [] # type: List[IOutputConfig] if play: outputs.append(FFplayOutputConfig()) if render: video_path = get_path(cfg_path or audio, VIDEO_NAME) outputs.append(FFmpegOutputConfig(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): self.renderer = corr._load_renderer() self.output = FFplayOutputConfig()(no_audio)