예제 #1
0
            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())
예제 #2
0
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]
예제 #3
0
    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)
예제 #4
0
    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)
예제 #5
0
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()
예제 #6
0
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?
예제 #7
0
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()
예제 #8
0
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()
예제 #9
0
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()
예제 #10
0
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
예제 #11
0
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
예제 #12
0
    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()
예제 #13
0
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)
예제 #14
0
 def __init__(self):
     self.renderer = corr._load_renderer()
     self.output = FFplayOutputConfig()(no_audio)