Ejemplo n.º 1
0
    def on_action_render(self):
        """ Get file name. Then show a progress dialog while rendering to file. """
        error_msg = "Cannot render to file, another play/render is active"
        if self.corr_thread is not None:
            qw.QMessageBox.critical(self, "Error", error_msg)
            return

        video_path = self.file_stem + cli.VIDEO_NAME
        filters = ["MP4 files (*.mp4)", "All files (*)"]

        # Points to either `file_dir` or `render_dir`.
        ref = self.pref.render_dir_ref

        path = get_save_file_path(ref, self, "Render to Video", video_path,
                                  filters, cli.VIDEO_NAME)
        if path:
            name = str(path)
            dlg = CorrProgressDialog(self, "Rendering video")

            outputs = [FFmpegOutputConfig(name)]
            self.play_thread(outputs, dlg)
Ejemplo n.º 2
0
def test_load_yaml_another_dir(mocker, Popen):
    """ YAML file located in `another/dir` should resolve `master_audio`, `channels[].
    wav_path`, and video `path` from `another/dir`. """

    subdir = "tests"
    wav = "sine440.wav"
    mp4 = "sine440.mp4"
    with pushd(subdir):
        arg_str = f"{wav} -a {wav}"
        cfg, outpath = yaml_sink(mocker, arg_str)  # type: Config, Path

    cfg.begin_time = 100  # To skip all actual rendering

    # Log execution of CorrScope().play()
    Wave = mocker.spy(corrscope.channel, "Wave")

    # Issue: this test does not use cli.main() to compute output path.
    # Possible solution: Call cli.main() via Click runner.
    output = FFmpegOutputConfig(cli._get_file_name(None, cfg, cli.VIDEO_NAME))
    corr = CorrScope(cfg, Arguments(subdir, [output]))
    corr.play()

    # Compute absolute paths
    wav_abs = abspath(f"{subdir}/{wav}")
    mp4_abs = abspath(f"{subdir}/{mp4}")

    # Test `wave_path`
    args, kwargs = Wave.call_args
    (wave_path, ) = args
    assert wave_path == wav_abs

    # Test output `master_audio` and video `path`
    args, kwargs = Popen.call_args
    argv = args[0]
    assert argv[-1] == mp4_abs
    assert f"-i {wav_abs}" in " ".join(argv)
Ejemplo n.º 3
0
        assert popen.stdin != popen.stdout

        popen.stdin.write.side_effect = exc
        popen.wait.return_value = 0
        return popen

    Popen = mocker.patch.object(subprocess, "Popen", autospec=True)
    Popen.side_effect = popen_factory
    return Popen


class DummyException(Exception):
    pass


NULL_FFMPEG_OUTPUT = FFmpegOutputConfig(None, "-f null")

render_cfg = RendererConfig(WIDTH, HEIGHT)
CFG = default_config(render=render_cfg)


def sine440_config():
    cfg = default_config(
        channels=[ChannelConfig("tests/sine440.wav")],
        master_audio="tests/sine440.wav",
        end_time=0.5,  # Reduce test duration
        render=render_cfg,
    )
    return cfg

Ejemplo n.º 4
0
class Config(
    KeywordAttrs,
    always_dump="""
    begin_time end_time
    render_subfps trigger_subsampling render_subsampling
    trigger_stereo render_stereo
    """,
):
    """Default values indicate optional attributes."""

    master_audio: Optional[str]
    begin_time: float = with_units("s", default=0)
    end_time: Optional[float] = None

    fps: int

    trigger_ms: int = with_units("ms")
    render_ms: int = with_units("ms")

    # Performance
    trigger_subsampling: int = 1
    render_subsampling: int = 1

    # Performance (skipped when recording to video)
    render_subfps: int = 2
    render_fps = property(lambda self: Fraction(self.fps, self.render_subfps))
    # FFmpeg accepts FPS as a fraction. (decimals may work, but are inaccurate.)

    # Both before_* functions should be idempotent, AKA calling twice does no harm.
    def before_preview(self) -> None:
        """Called *once* before preview. Does nothing."""
        self.render.before_preview()

    def before_record(self) -> None:
        """Called *once* before recording video. Force high-quality rendering."""
        self.render_subfps = 1
        self.trigger_subsampling = 1
        self.render_subsampling = 1
        self.render.before_record()

    # End Performance
    amplification: float

    # Stereo config
    trigger_stereo: FlattenOrStr = Flatten.SumAvg
    render_stereo: FlattenOrStr = Flatten.SumAvg

    trigger: CorrelationTriggerConfig  # Can be overriden per Wave

    # Multiplies by trigger_width, render_width. Can override trigger.
    channels: List[ChannelConfig]
    default_label: DefaultLabel = DefaultLabel.NoLabel

    layout: LayoutConfig
    render: RendererConfig
    ffmpeg_cli: FFmpegOutputConfig = attr.ib(factory=lambda: FFmpegOutputConfig(None))

    def get_ffmpeg_cfg(self, video_path: str) -> FFmpegOutputConfig:
        return attr.evolve(self.ffmpeg_cli, path=os.path.abspath(video_path))

    benchmark_mode: BenchmarkMode = attr.ib(
        BenchmarkMode.NONE, converter=BenchmarkMode.by_name
    )
Ejemplo n.º 5
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)