Exemplo n.º 1
0
    def __init__(self, cfg: ChannelConfig, corr_cfg: "Config"):
        self.cfg = cfg

        # Create a Wave object.
        wave = Wave(
            abspath(cfg.wav_path),
            amplification=coalesce(cfg.amplification, corr_cfg.amplification),
        )

        # Flatten wave stereo for trigger and render.
        tflat = coalesce(cfg.trigger_stereo, corr_cfg.trigger_stereo)
        rflat = coalesce(cfg.render_stereo, corr_cfg.render_stereo)

        self.trigger_wave = wave.with_flatten(tflat, return_channels=False)
        self.render_wave = wave.with_flatten(rflat, return_channels=True)

        # `subsampling` increases `stride` and decreases `nsamp`.
        # `width` increases `stride` without changing `nsamp`.
        tsub = corr_cfg.trigger_subsampling
        tw = cfg.trigger_width

        rsub = corr_cfg.render_subsampling
        rw = cfg.render_width

        # nsamp = orig / subsampling
        # stride = subsampling * width
        def calculate_nsamp(width_ms, sub):
            width_s = width_ms / 1000
            return round(width_s * wave.smp_s / sub)

        trigger_samp = calculate_nsamp(corr_cfg.trigger_ms, tsub)
        self.render_samp = calculate_nsamp(corr_cfg.render_ms, rsub)

        self.trigger_stride = tsub * tw
        self.render_stride = rsub * rw

        # Create a Trigger object.
        if isinstance(cfg.trigger, ITriggerConfig):
            tcfg = cfg.trigger
        elif isinstance(
                cfg.trigger,
            (CommentedMap, dict)):  # CommentedMap may/not be subclass of dict.
            tcfg = attr.evolve(corr_cfg.trigger, **cfg.trigger)
        elif cfg.trigger is None:
            tcfg = corr_cfg.trigger
        else:
            raise CorrError(
                f"invalid per-channel trigger {cfg.trigger}, type={type(cfg.trigger)}, "
                f"must be (*)TriggerConfig, dict, or None")

        self.trigger = tcfg(
            wave=self.trigger_wave,
            tsamp=trigger_samp,
            stride=self.trigger_stride,
            fps=corr_cfg.fps,
        )
Exemplo n.º 2
0
def test_wave_subsampling_off_by_1(is_odd: bool):
    """When calling wave.get_around(x, N), ensure that result[N//2] == wave[x]."""
    wave = Wave(prefix + "s16-impulse1000.wav")
    for stride in range(1, 5 + 1):
        N = 500 + is_odd
        halfN = N // 2
        result = wave.get_around(1000, N, stride)

        assert result[halfN] > 0.5, stride

        result[halfN] = 0
        np.testing.assert_almost_equal(result, 0, decimal=3)
Exemplo n.º 3
0
def test_wave_subsampling():
    wave = Wave("tests/sine440.wav")
    # period = 48000 / 440 = 109.(09)*

    wave.get_around(1000, return_nsamp=501, stride=4)
    # len([:region_len:subsampling]) == ceil(region_len / subsampling)
    # If region_len % subsampling != 0, len() != region_len // subsampling.

    stride = 4
    region = 100  # diameter = region * stride
    for i in [-1000, 50000]:
        data = wave.get_around(i, region, stride)
        assert (data == 0).all()
Exemplo n.º 4
0
def test_stereo_flatten_modes(
    flatten: Flatten,
    return_channels: bool,
    path: str,
    nchan: int,
    peaks: Sequence[float],
):
    """Ensures all Flatten modes are handled properly
    for stereo and mono signals."""

    # return_channels=False <-> triggering.
    # flatten=stereo -> rendering.
    # These conditions do not currently coexist.
    # if not return_channels and flatten == Flatten.Stereo:
    #     return

    assert nchan == len(peaks)
    wave = Wave(path)

    if flatten not in Flatten.modes:
        with pytest.raises(CorrError):
            wave.with_flatten(flatten, return_channels)
        return
    else:
        wave = wave.with_flatten(flatten, return_channels)

    nsamp = wave.nsamp
    data = wave[:]

    # wave.data == 2-D array of shape (nsamp, nchan)
    if flatten == Flatten.Stereo:
        assert data.shape == (nsamp, nchan)
        for chan_data, peak in zip(data.T, peaks):
            assert_full_scale(chan_data, peak)
    else:
        if return_channels:
            assert data.shape == (nsamp, 1)
        else:
            assert data.shape == (nsamp, )

        # If DiffAvg and in-phase, L-R=0.
        if flatten == Flatten.DiffAvg:
            if len(peaks) >= 2 and peaks[0] == peaks[1]:
                np.testing.assert_equal(data, 0)
            else:
                pass
        # If SumAvg, check average.
        else:
            assert flatten == Flatten.SumAvg
            assert_full_scale(data, np.mean(peaks))
Exemplo n.º 5
0
def test_post_stride(post_trigger):
    """
    Test that stride is respected when post_trigger is disabled,
    and ignored when post_trigger is enabled.
    """
    cfg = trigger_template(post_trigger=post_trigger)

    wave = Wave("tests/sine440.wav")
    iters = 5
    x0 = 24000
    stride = 4
    trigger = cfg(wave, tsamp=100, stride=stride, fps=FPS)

    cache = PerFrameCache()
    for i in range(1, iters):
        offset = trigger.get_trigger(x0, cache)

        if not cfg.post_trigger:
            assert (offset - x0) % stride == 0, f"iteration {i}"
            assert abs(offset - x0) < 10, f"iteration {i}"

        else:
            # If assertion fails, remove it.
            assert (offset - x0) % stride != 0, f"iteration {i}"
            assert abs(offset - x0) <= 2, f"iteration {i}"
Exemplo n.º 6
0
def test_stereo_merge():
    """Test indexing Wave by slices *or* ints. Flatten using default SumAvg mode."""

    # Contains a full-scale sine wave in left channel, and silence in right.
    # λ=100, nsamp=2000
    wave = Wave(prefix + "stereo-sine-left-2000.wav")
    period = 100
    nsamp = 2000

    # [-1, 1) from [-32768..32768)
    int16_step = (1 - -1) / (2**16)
    assert int16_step == 2**-15

    # Check wave indexing dimensions.
    assert wave[0].shape == ()
    assert wave[:].shape == (nsamp, )

    # Check stereo merging.
    assert_allclose(wave[0], 0)
    assert_allclose(wave[period], 0)
    assert_allclose(wave[period // 4], 0.5, atol=int16_step)

    def check_bound(obj):
        amax = np.amax(obj)
        assert amax.shape == ()

        assert_allclose(amax, 0.5, atol=int16_step)
        assert_allclose(np.amin(obj), -0.5, atol=int16_step)

    check_bound(wave[:])
Exemplo n.º 7
0
def test_stereo_doesnt_overflow():
    """Ensure loud stereo tracks do not overflow."""
    wave = Wave("tests/stereo in-phase.wav")

    samp = 100
    stride = 1
    data = wave.get_around(wave.nsamp // 2, samp, stride)
    expect(np.amax(data) > 0.99)
    expect(np.amin(data) < -0.99)

    # In the absence of overflow, sine waves have no large jumps.
    # In the presence of overflow, stereo sum will jump between INT_MAX and INT_MIN.
    # np.mean and rescaling converts to 0.499... and -0.5, which is nearly 1.
    expect(np.amax(np.abs(np.diff(data))) < 0.5)

    assert_expectations()
Exemplo n.º 8
0
def test_post_stride(post_trigger):
    """
    Test that stride is respected when post_trigger is disabled,
    and ignored when post_trigger is enabled.
    """
    cfg = trigger_template(post_trigger=post_trigger, post_radius=10)

    wave = Wave("tests/sine440.wav")
    iters = 5
    x0 = 24000
    stride = 4

    def trigger(pos):
        # We have to generate a new trigger object each time, because
        # CorrelationTrigger.get_trigger() never goes backwards, which violates the
        # stride quantization we're testing for in the "if not cfg.post_trigger" branch.

        trigger = cfg(wave, tsamp=150 // stride, stride=stride, fps=FPS)
        cache = PerFrameCache()
        return trigger.get_trigger(pos, cache).result

    init_offset = trigger(x0)

    for i in range(1, iters):
        offset = trigger(x0 + i)

        if not cfg.post_trigger:
            assert (offset -
                    init_offset) % stride == i % stride, f"iteration {i}"
            assert offset == pytest.approx(x0, abs=9), f"iteration {i}"

        else:
            assert offset == pytest.approx(init_offset,
                                           abs=1), f"iteration {i}"
            assert offset == pytest.approx(x0, abs=1), f"iteration {i}"
Exemplo n.º 9
0
def test_trigger(cfg: CorrelationTriggerConfig):
    wave = Wave("tests/impulse24000.wav")

    iters = 5
    plot = False
    x0 = 24000
    x = x0 - 500
    trigger: CorrelationTrigger = cfg(wave, 4000, stride=1, fps=FPS)

    if plot:
        BIG = 0.95
        SMALL = 0.05
        fig, axes = plt.subplots(iters,
                                 gridspec_kw=dict(
                                     top=BIG,
                                     right=BIG,
                                     bottom=SMALL,
                                     left=SMALL))  # type: Figure, Axes
        fig.tight_layout()
    else:
        axes = range(iters)

    for i, ax in enumerate(axes):
        if i:
            offset = trigger.get_trigger(x, PerFrameCache())
            print(offset)
            assert offset == x0
        if plot:
            ax.plot(trigger._buffer, label=str(i))
            ax.grid()

    if plot:
        plt.show()
Exemplo n.º 10
0
def test_header_larger_than_filesize():
    """According to Zeinok, VortexTracker 2.5 produces slightly corrupted WAV files
    whose RIFF header metadata indicates a filesize larger than the actual filesize.

    Most programs read the audio chunk fine.
    Scipy normally rejects such files, raises ValueError("Unexpected end of file.")
    My version instead accepts such files (but warns WavFileWarning).
    """
    with pytest.warns(WavFileWarning):
        wave = Wave("tests/header larger than filesize.wav")
        assert wave
Exemplo n.º 11
0
def test_trigger_stride_edges(cfg: CorrelationTriggerConfig):
    wave = Wave("tests/sine440.wav")
    # period = 48000 / 440 = 109.(09)*

    stride = 4
    trigger = cfg(wave, tsamp=100, stride=stride, fps=FPS)
    # real window_samp = window_samp*stride
    # period = 109

    trigger.get_trigger(0, PerFrameCache())
    trigger.get_trigger(-1000, PerFrameCache())
    trigger.get_trigger(50000, PerFrameCache())
Exemplo n.º 12
0
def test_trigger_out_of_bounds(trigger_cfg):
    """Ensure out-of-bounds triggering with stride does not crash.
    (why does stride matter? IDK.)"""
    wave = Wave("tests/sine440.wav")
    # period = 48000 / 440 = 109.(09)*

    stride = 4
    trigger = trigger_cfg(wave, tsamp=100, stride=stride, fps=FPS)
    # real window_samp = window_samp*stride
    # period = 109

    trigger.get_trigger(0, PerFrameCache())
    trigger.get_trigger(-1000, PerFrameCache())
    trigger.get_trigger(50000, PerFrameCache())
Exemplo n.º 13
0
def test_trigger_direction(post_trigger, double_negate):
    """
    Right now, MainTrigger is responsible for negating wave.amplification
    if edge_direction == -1.
    And triggers should not actually access edge_direction.
    """

    index = 2400
    wave = Wave("tests/step2400.wav")

    if double_negate:
        wave.amplification = -1
        cfg = trigger_template(post_trigger=post_trigger, edge_direction=-1)
    else:
        cfg = trigger_template(post_trigger=post_trigger)

    trigger = cfg(wave, 100, 1, FPS)
    cfg.edge_direction = None
    assert trigger._wave.amplification == 1

    cache = PerFrameCache()
    for dx in [-10, 10, 0]:
        assert trigger.get_trigger(index + dx, cache) == index
Exemplo n.º 14
0
def test_wave(wave_path):
    with warnings.catch_warnings(record=True) as w:
        # Cause all warnings to always be triggered.
        warnings.simplefilter("always")

        wave = Wave(prefix + wave_path)
        data = wave[:]

        # Audacity dithers <=16-bit WAV files upon export, creating a few bits of noise.
        # As a result, amin(data) <= 0.
        assert -0.01 < np.amin(data) <= 0
        assert 0.99 < np.amax(data) <= 1

        # check for FutureWarning (raised when determining wavfile type)
        warns = [o for o in w if issubclass(o.category, FutureWarning)]
        assert not [str(w) for w in warns]
Exemplo n.º 15
0
def test_mean_subtraction(trigger_cfg, mocker: "pytest_mock.MockFixture"):
    """
    Ensure that trigger subtracts mean properly in all configurations.
    -   Due to a regression, mean was not subtracted when sign_strength = 0.
        This caused get_period() to malfunction.
    """
    wave = Wave("tests/step2400.wav")

    get_period = mocker.spy(triggers, "get_period")
    trigger = trigger_cfg(wave, tsamp=100, stride=1, fps=FPS)
    cache = PerFrameCache()
    trigger.get_trigger(2600, cache)  # step2400.wav

    (data, *args), kwargs = get_period.call_args
    assert isinstance(data, np.ndarray)
    assert abs(np.mean(data)) < 0.01
Exemplo n.º 16
0
def test_trigger(trigger_cfg, is_odd: bool, post_trigger):
    """Ensures that trigger can locate
    the first positive sample of a -+ step exactly,
    without off-by-1 errors.

    See CorrelationTrigger and Wave.get_around() docstrings.
    """
    wave = Wave("tests/step2400.wav")
    trigger_cfg = attr.evolve(trigger_cfg, post_trigger=post_trigger)

    iters = 5
    plot = False
    x0 = 2400
    x = x0 - 50
    trigger: CorrelationTrigger = trigger_cfg(wave,
                                              400 + int(is_odd),
                                              stride=1,
                                              fps=FPS)

    if plot:
        BIG = 0.95
        SMALL = 0.05
        fig, axes = plt.subplots(iters,
                                 gridspec_kw=dict(
                                     top=BIG,
                                     right=BIG,
                                     bottom=SMALL,
                                     left=SMALL))  # type: Figure, Axes
        fig.tight_layout()
    else:
        axes = range(iters)

    for i, ax in enumerate(axes):
        if i:
            offset = trigger.get_trigger(x, PerFrameCache())
            assert offset == x0, offset
        if plot:
            ax.plot(trigger._buffer, label=str(i))
            ax.grid()

    if plot:
        plt.show()
Exemplo n.º 17
0
def test_post_trigger_stride(post_cfg: CorrelationTriggerConfig):
    cfg = post_cfg

    wave = Wave("tests/sine440.wav")
    iters = 5
    x0 = 24000
    stride = 4
    trigger = cfg(wave, tsamp=100, stride=stride, fps=FPS)

    cache = PerFrameCache()
    for i in range(1, iters):
        offset = trigger.get_trigger(x0, cache)

        if not cfg.post:
            assert (offset - x0) % stride == 0, f"iteration {i}"
            assert abs(offset - x0) < 10, f"iteration {i}"

        else:
            # If assertion fails, remove it.
            assert (offset - x0) % stride != 0, f"iteration {i}"
            assert abs(offset - x0) <= 2, f"iteration {i}"
Exemplo n.º 18
0
def test_when_does_trigger_recalc_window():
    cfg = trigger_template(recalc_semitones=1.0)
    wave = Wave("tests/sine440.wav")
    trigger: CorrelationTrigger = cfg(wave, tsamp=1000, stride=1, fps=FPS)

    for x in [0, 1, 1000]:
        assert trigger._is_window_invalid(x), x

    trigger._prev_period = 100

    for x in [0, 99, 101]:
        assert not trigger._is_window_invalid(x), x
    for x in [80, 120]:
        assert trigger._is_window_invalid(x), x

    trigger._prev_period = 0

    x = 0
    assert not trigger._is_window_invalid(x), x
    for x in [1, 100]:
        assert trigger._is_window_invalid(x), x
Exemplo n.º 19
0
def test_post_trigger_radius():
    """
    Ensure ZeroCrossingTrigger has no off-by-1 errors when locating edges,
    and slides at a fixed rate if no edge is found.
    """
    wave = Wave("tests/step2400.wav")
    center = 2400
    radius = 5

    cfg = ZeroCrossingTriggerConfig()
    post = cfg(wave, radius, 1, FPS)

    cache = PerFrameCache(mean=0)

    for offset in range(-radius, radius + 1):
        assert post.get_trigger(center + offset, cache) == center, offset

    for offset in [radius + 1, radius + 2, 100]:
        assert post.get_trigger(center - offset,
                                cache) == center - offset + radius
        assert post.get_trigger(center + offset,
                                cache) == center + offset - radius
Exemplo n.º 20
0
def test_trigger_stride(cfg: CorrelationTriggerConfig):
    wave = Wave("tests/sine440.wav")
    # period = 48000 / 440 = 109.(09)*

    iters = 5
    x0 = 24000
    stride = 4
    trigger = cfg(wave, tsamp=100, stride=stride, fps=FPS)
    # real window_samp = window_samp*stride
    # period = 109

    cache = PerFrameCache()

    for i in range(1, iters):
        offset = trigger.get_trigger(x0, cache)

        # Debugging CorrelationTrigger.get_trigger:
        # from matplotlib import pyplot as plt
        # plt.plot(data)
        # plt.plot(prev_buffer)
        # plt.plot(corr)

        # When i=0, the data has 3 peaks, the rightmost taller than the center. The
        # *tips* of the outer peaks are truncated between `left` and `right`.
        # After truncation, corr[mid+1] is almost identical to corr[mid], for
        # reasons I don't understand (mid+1 > mid because dithering?).
        if not cfg.use_edge_trigger:
            assert (offset - x0) % stride == 0, f"iteration {i}"
            assert abs(offset - x0) < 10, f"iteration {i}"

        # The edge trigger activates at x0+1=24001. Likely related: it triggers
        # when moving from <=0 to >0. This is a necessary evil, in order to
        # recognize 0-to-positive edges while testing tests/impulse24000.wav .

        else:
            # If assertion fails, remove it.
            assert (offset - x0) % stride != 0, f"iteration {i}"
            assert abs(offset - x0) <= 2, f"iteration {i}"
Exemplo n.º 21
0
def test_incomplete_wav_chunk():
    """Tests that WAV files exported from foobar2000 can be loaded properly,
    without a `ValueError: Incomplete wav chunk.` exception being raised."""

    wave = Wave(prefix + "incomplete-wav-chunk.wav")
    data = wave[:]
Exemplo n.º 22
0
    def __init__(self, cfg: ChannelConfig, corr_cfg: "Config", channel_idx: int = 0):
        """channel_idx counts from 0."""
        self.cfg = cfg

        self.label = cfg.label
        if not self.label:
            if corr_cfg.default_label is DefaultLabel.FileName:
                self.label = Path(cfg.wav_path).stem
            elif corr_cfg.default_label is DefaultLabel.Number:
                self.label = str(channel_idx + 1)

        # Create a Wave object.
        wave = Wave(
            abspath(cfg.wav_path),
            amplification=coalesce(cfg.amplification, corr_cfg.amplification),
        )

        # Flatten wave stereo for trigger and render.
        tflat = coalesce(cfg.trigger_stereo, corr_cfg.trigger_stereo)
        rflat = coalesce(cfg.render_stereo, corr_cfg.render_stereo)

        self.trigger_wave = wave.with_flatten(tflat, return_channels=False)
        self.render_wave = wave.with_flatten(rflat, return_channels=True)

        # `subsampling` increases `stride` and decreases `nsamp`.
        # `width` increases `stride` without changing `nsamp`.
        tsub = corr_cfg.trigger_subsampling
        tw = cfg.trigger_width

        rsub = corr_cfg.render_subsampling
        rw = cfg.render_width

        # nsamp = orig / subsampling
        # stride = subsampling * width
        def calculate_nsamp(width_ms, sub):
            width_s = width_ms / 1000
            return round(width_s * wave.smp_s / sub)

        trigger_samp = calculate_nsamp(corr_cfg.trigger_ms, tsub)
        self._render_samp = calculate_nsamp(corr_cfg.render_ms, rsub)

        self._trigger_stride = tsub * tw
        self.render_stride = rsub * rw

        # Create a Trigger object.
        if isinstance(cfg.trigger, MainTriggerConfig):
            tcfg = cfg.trigger

        elif isinstance(
            cfg.trigger, (CommentedMap, dict)
        ):  # CommentedMap may/not be subclass of dict.
            tcfg = evolve_compat(corr_cfg.trigger, **cfg.trigger)

        elif cfg.trigger is None:
            tcfg = corr_cfg.trigger

        else:
            raise CorrError(
                f"invalid per-channel trigger {cfg.trigger}, type={type(cfg.trigger)}, "
                f"must be (*)TriggerConfig, dict, or None"
            )

        self.trigger = tcfg(
            wave=self.trigger_wave,
            tsamp=trigger_samp,
            stride=self._trigger_stride,
            fps=corr_cfg.fps,
            wave_idx=channel_idx,
        )
Exemplo n.º 23
0
def test_stereo_mmap():
    wave = Wave(prefix + "stereo-sine-left-2000.wav")
    assert isinstance(wave.data, np.memmap)