Beispiel #1
0
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
Beispiel #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]
Beispiel #3
0
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
Beispiel #4
0
 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,
     )
Beispiel #5
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()
Beispiel #6
0
    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
Beispiel #7
0
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)
Beispiel #8
0
 def on_action_new(self):
     if not self.should_close_document(self.tr("New Project")):
         return
     cfg = template_config()
     self.load_cfg(cfg, None)
Beispiel #9
0
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():
Beispiel #10
0
def test_gui_init():
    app = qw.QApplication([])
    cfg = template_config()
    gui.MainWindow(cfg)
Beispiel #11
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 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)
Beispiel #12
0
    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))