Exemple #1
0
def test_line_colors(appear: Appearance, data):
    """Test channel-specific line color overrides"""
    cfg = get_renderer_config(appear)
    lcfg = LayoutConfig(orientation=ORIENTATION)
    datas = [data] * NPLOTS

    # Move line color (appear.fg.color) from renderer cfg to individual channel.
    chan = ChannelConfig(wav_path="", line_color=appear.fg.color)
    channels = [chan] * NPLOTS
    cfg.init_line_color = "#888888"
    chan.line_color = appear.fg.color

    r = Renderer(cfg, lcfg, datas, channels, None)
    verify(r, appear, datas)
Exemple #2
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
Exemple #3
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]
Exemple #4
0
    def insertRows(self, row: int, count: int, parent=QModelIndex()) -> bool:
        if not (count >= 1 and 0 <= row <= len(self.channels)):
            return False

        self.beginInsertRows(parent, row, row + count - 1)
        self.channels[row:row] = [ChannelConfig("") for _ in range(count)]
        self.endInsertRows()
        return True
Exemple #5
0
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
Exemple #6
0
    def __init__(
        self,
        cfg: RendererConfig,
        lcfg: "LayoutConfig",
        dummy_datas: List[np.ndarray],
        channel_cfgs: Optional[List["ChannelConfig"]],
        channels: List["Channel"],
    ):
        self.cfg = cfg
        self.lcfg = lcfg

        self.w = cfg.divided_width
        self.h = cfg.divided_height

        # Maps a continuous variable from 0 to 1 (representing one octave) to a color.
        self.pitch_cmap = gen_circular_cmap(cfg.pitch_colors)

        self.nplots = len(dummy_datas)

        if self.nplots > 0:
            assert len(dummy_datas[0].shape) == 2, dummy_datas[0].shape
        self.wave_nsamps = [data.shape[0] for data in dummy_datas]
        self.wave_nchans = [data.shape[1] for data in dummy_datas]

        if channel_cfgs is None:
            channel_cfgs = [ChannelConfig("") for _ in range(self.nplots)]

        if len(channel_cfgs) != self.nplots:
            raise ValueError(
                f"cannot assign {len(channel_cfgs)} colors to {self.nplots} plots"
            )

        self._line_params = [
            LineParam(
                color=coalesce(ccfg.line_color, cfg.global_line_color),
                color_by_pitch=coalesce(ccfg.color_by_pitch,
                                        cfg.global_color_by_pitch),
            ) for ccfg in channel_cfgs
        ]

        # Load channel strides.
        if channels is not None:
            if len(channels) != self.nplots:
                raise ValueError(
                    f"cannot assign {len(channels)} channels to {self.nplots} plots"
                )
            self.render_strides = [
                channel.render_stride for channel in channels
            ]
        else:
            self.render_strides = [1] * self.nplots
Exemple #7
0
def test_default_colors(appear: Appearance, data):
    """Test the default background/foreground colors."""
    cfg = get_renderer_config(appear)
    lcfg = LayoutConfig(orientation=ORIENTATION)
    datas = [data] * NPLOTS

    r = Renderer(cfg, lcfg, datas, None, None)
    verify(r, appear, datas)

    # Ensure default ChannelConfig(line_color=None) does not override line color
    chan = ChannelConfig(wav_path="")
    channels = [chan] * NPLOTS
    r = Renderer(cfg, lcfg, datas, channels, None)
    verify(r, appear, datas)
Exemple #8
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()
Exemple #9
0
def test_line_colors(bg_str, fg_str, grid_str, data):
    """ Test channel-specific line color overrides """
    cfg = RendererConfig(
        WIDTH,
        HEIGHT,
        bg_color=bg_str,
        init_line_color="#888888",
        grid_color=grid_str,
        stereo_grid_opacity=OPACITY,
        line_width=2.0,
        antialiasing=False,
    )
    lcfg = LayoutConfig()

    chan = ChannelConfig(wav_path="", line_color=fg_str)
    channels = [chan] * NPLOTS
    r = MatplotlibRenderer(cfg, lcfg, NPLOTS, channels)
    verify(r, bg_str, fg_str, grid_str, data)
Exemple #10
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)
Exemple #11
0
def test_default_colors(bg_str, fg_str, grid_str, data):
    """ Test the default background/foreground colors. """
    cfg = RendererConfig(
        WIDTH,
        HEIGHT,
        bg_color=bg_str,
        init_line_color=fg_str,
        grid_color=grid_str,
        stereo_grid_opacity=OPACITY,
        line_width=2.0,
        antialiasing=False,
    )
    lcfg = LayoutConfig()

    r = MatplotlibRenderer(cfg, lcfg, NPLOTS, None)
    verify(r, bg_str, fg_str, grid_str, data)

    # Ensure default ChannelConfig(line_color=None) does not override line color
    chan = ChannelConfig(wav_path="")
    channels = [chan] * NPLOTS
    r = MatplotlibRenderer(cfg, lcfg, NPLOTS, channels)
    verify(r, bg_str, fg_str, grid_str, data)
Exemple #12
0
def test_config_channel_integration(
    # Channel
    c_amplification: Optional[float],
    c_trigger_width: int,
    c_render_width: int,
    # Global
    amplification: float,
    trigger_ms: int,
    render_ms: int,
    tsub: int,
    rsub: int,
    default_label: DefaultLabel,
    override_label: bool,
    mocker: MockFixture,
):
    """ (Tautologically) verify:
    - channel.  r_samp (given cfg)
    - channel.t/r_stride (given cfg.*_subsampling/*_width)
    - trigger._tsamp, _stride
    - renderer's method calls(samp, stride)
    - rendered label (channel.label, given cfg, corr_cfg.default_label)
    """

    # region setup test variables
    corrscope.corrscope.PRINT_TIMESTAMP = False  # Cleanup Hypothesis testing logs

    Wave = mocker.patch.object(corrscope.channel, "Wave")
    wave = Wave.return_value

    def get_around(sample: int, return_nsamp: int, stride: int):
        return np.zeros(return_nsamp)

    wave.get_around.side_effect = get_around
    wave.with_flatten.return_value = wave
    wave.nsamp = 10000
    wave.smp_s = 48000

    ccfg = ChannelConfig(
        "tests/sine440.wav",
        trigger_width=c_trigger_width,
        render_width=c_render_width,
        amplification=c_amplification,
        label="label" if override_label else "",
    )

    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,
        )

    # endregion

    cfg = get_cfg()
    channel = Channel(ccfg, cfg)

    # Ensure cfg.width_ms etc. are correct
    assert cfg.trigger_ms == trigger_ms
    assert cfg.render_ms == render_ms

    # Ensure channel.window_samp, trigger_subsampling, render_subsampling are correct.
    def ideal_samp(width_ms, sub):
        width_s = width_ms / 1000
        return pytest.approx(
            round(width_s * channel.trigger_wave.smp_s / sub), rel=1e-6
        )

    ideal_tsamp = ideal_samp(cfg.trigger_ms, tsub)
    ideal_rsamp = ideal_samp(cfg.render_ms, rsub)
    assert channel._render_samp == ideal_rsamp

    assert channel._trigger_stride == tsub * c_trigger_width
    assert channel.render_stride == rsub * c_render_width

    # Ensure amplification override works
    args, kwargs = Wave.call_args
    assert kwargs["amplification"] == coalesce(c_amplification, amplification)

    ## Ensure trigger uses channel.window_samp and _trigger_stride.
    trigger = channel.trigger
    assert trigger._tsamp == ideal_tsamp
    assert trigger._stride == channel._trigger_stride

    ## Ensure corrscope calls render using channel._render_samp and _render_stride.
    corr = CorrScope(cfg, Arguments(cfg_dir=".", outputs=[]))
    renderer = mocker.patch.object(CorrScope, "_load_renderer").return_value
    corr.play()

    # Only Channel.get_render_around() (not NullTrigger) calls wave.get_around().
    (_sample, _return_nsamp, _subsampling), kwargs = wave.get_around.call_args
    assert _return_nsamp == channel._render_samp
    assert _subsampling == channel.render_stride

    # Inspect arguments to renderer.update_main_lines()
    # datas: List[np.ndarray]
    (datas,), kwargs = renderer.update_main_lines.call_args
    render_data = datas[0]
    assert len(render_data) == channel._render_samp

    # Inspect arguments to renderer.add_labels().
    (labels,), kwargs = renderer.add_labels.call_args
    label = labels[0]
    if override_label:
        assert label == "label"
    else:
        if default_label is DefaultLabel.FileName:
            assert label == "sine440"
        elif default_label is DefaultLabel.Number:
            assert label == "1"
        else:
            assert label == ""
Exemple #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)