2018-07-20 22:29:36 +00:00
|
|
|
import warnings
|
2019-01-25 06:15:57 +00:00
|
|
|
from typing import Sequence
|
2018-07-20 22:29:36 +00:00
|
|
|
|
2018-07-20 08:12:28 +00:00
|
|
|
import numpy as np
|
2018-12-29 23:43:11 +00:00
|
|
|
from numpy.testing import assert_allclose
|
2018-07-20 08:12:28 +00:00
|
|
|
import pytest
|
2018-11-18 06:00:46 +00:00
|
|
|
from delayed_assert import expect, assert_expectations
|
2018-07-20 08:12:28 +00:00
|
|
|
|
2019-01-08 08:08:37 +00:00
|
|
|
from corrscope.config import CorrError
|
2019-01-26 03:27:46 +00:00
|
|
|
from corrscope.utils.scipy.wavfile import WavFileWarning
|
2019-01-08 08:08:37 +00:00
|
|
|
from corrscope.wave import Wave, Flatten
|
2018-07-20 08:12:28 +00:00
|
|
|
|
2019-01-03 08:57:30 +00:00
|
|
|
prefix = "tests/wav-formats/"
|
2018-07-20 08:12:28 +00:00
|
|
|
wave_paths = [
|
|
|
|
# 2000 samples, with a full-scale peak at data[1000].
|
2019-01-03 08:57:30 +00:00
|
|
|
"u8-impulse1000.wav",
|
|
|
|
"s16-impulse1000.wav",
|
|
|
|
"s32-impulse1000.wav",
|
|
|
|
"f32-impulse1000.wav",
|
|
|
|
"f64-impulse1000.wav",
|
2018-07-20 08:12:28 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("wave_path", wave_paths)
|
|
|
|
def test_wave(wave_path):
|
2018-07-20 22:29:36 +00:00
|
|
|
with warnings.catch_warnings(record=True) as w:
|
|
|
|
# Cause all warnings to always be triggered.
|
|
|
|
warnings.simplefilter("always")
|
|
|
|
|
2019-01-08 22:28:42 +00:00
|
|
|
wave = Wave(prefix + wave_path)
|
2018-07-20 22:29:36 +00:00
|
|
|
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
|
2018-07-20 08:12:28 +00:00
|
|
|
|
2018-07-20 22:29:36 +00:00
|
|
|
# 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]
|
2018-07-29 09:07:00 +00:00
|
|
|
|
|
|
|
|
2019-01-08 08:08:37 +00:00
|
|
|
# Stereo tests
|
|
|
|
|
|
|
|
|
2018-12-29 23:43:11 +00:00
|
|
|
def test_stereo_merge():
|
2019-01-08 08:08:37 +00:00
|
|
|
"""Test indexing Wave by slices *or* ints. Flatten using default SumAvg mode."""
|
2018-12-29 23:43:11 +00:00
|
|
|
|
|
|
|
# Contains a full-scale sine wave in left channel, and silence in right.
|
|
|
|
# λ=100, nsamp=2000
|
2019-01-08 22:28:42 +00:00
|
|
|
wave = Wave(prefix + "stereo-sine-left-2000.wav")
|
2018-12-29 23:43:11 +00:00
|
|
|
period = 100
|
|
|
|
nsamp = 2000
|
|
|
|
|
|
|
|
# [-1, 1) from [-32768..32768)
|
|
|
|
int16_step = (1 - -1) / (2 ** 16)
|
2019-01-03 08:57:30 +00:00
|
|
|
assert int16_step == 2 ** -15
|
2018-12-29 23:43:11 +00:00
|
|
|
|
|
|
|
# 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)
|
2019-01-03 08:57:30 +00:00
|
|
|
assert_allclose(wave[period // 4], 0.5, atol=int16_step)
|
2018-12-29 23:43:11 +00:00
|
|
|
|
|
|
|
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[:])
|
|
|
|
|
|
|
|
|
2019-01-25 06:15:57 +00:00
|
|
|
AllFlattens = Flatten.__members__.values()
|
2019-01-08 08:08:37 +00:00
|
|
|
|
|
|
|
|
2019-01-25 06:15:57 +00:00
|
|
|
@pytest.mark.parametrize("flatten", AllFlattens)
|
2019-02-18 10:10:17 +00:00
|
|
|
@pytest.mark.parametrize("return_channels", [False, True])
|
2019-01-25 06:15:57 +00:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
"path,nchan,peaks",
|
|
|
|
[
|
|
|
|
("tests/sine440.wav", 1, [0.5]),
|
|
|
|
("tests/stereo in-phase.wav", 2, [1, 1]),
|
|
|
|
("tests/wav-formats/stereo-sine-left-2000.wav", 2, [1, 0]),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
def test_stereo_flatten_modes(
|
2019-02-18 10:10:17 +00:00
|
|
|
flatten: Flatten,
|
|
|
|
return_channels: bool,
|
|
|
|
path: str,
|
|
|
|
nchan: int,
|
|
|
|
peaks: Sequence[float],
|
2019-01-25 06:15:57 +00:00
|
|
|
):
|
2019-01-08 08:08:37 +00:00
|
|
|
"""Ensures all Flatten modes are handled properly
|
|
|
|
for stereo and mono signals."""
|
2019-02-18 10:10:17 +00:00
|
|
|
|
|
|
|
# return_channels=False <-> triggering.
|
|
|
|
# flatten=stereo -> rendering.
|
|
|
|
# These conditions do not currently coexist.
|
|
|
|
# if not return_channels and flatten == Flatten.Stereo:
|
|
|
|
# return
|
|
|
|
|
2019-01-25 06:15:57 +00:00
|
|
|
assert nchan == len(peaks)
|
|
|
|
wave = Wave(path)
|
2019-01-08 08:08:37 +00:00
|
|
|
|
|
|
|
if flatten not in Flatten.modes:
|
|
|
|
with pytest.raises(CorrError):
|
2019-02-18 10:10:17 +00:00
|
|
|
wave.with_flatten(flatten, return_channels)
|
2019-01-08 08:08:37 +00:00
|
|
|
return
|
|
|
|
else:
|
2019-02-18 10:10:17 +00:00
|
|
|
wave = wave.with_flatten(flatten, return_channels)
|
2019-01-08 08:08:37 +00:00
|
|
|
|
|
|
|
nsamp = wave.nsamp
|
|
|
|
data = wave[:]
|
|
|
|
|
|
|
|
# wave.data == 2-D array of shape (nsamp, nchan)
|
|
|
|
if flatten == Flatten.Stereo:
|
2019-01-25 06:15:57 +00:00
|
|
|
assert data.shape == (nsamp, nchan)
|
|
|
|
for chan_data, peak in zip(data.T, peaks):
|
|
|
|
assert_full_scale(chan_data, peak)
|
2019-01-08 08:08:37 +00:00
|
|
|
else:
|
2019-02-18 10:10:17 +00:00
|
|
|
if return_channels:
|
|
|
|
assert data.shape == (nsamp, 1)
|
|
|
|
else:
|
|
|
|
assert data.shape == (nsamp,)
|
2019-01-25 06:15:57 +00:00
|
|
|
|
|
|
|
# If DiffAvg and in-phase, L-R=0.
|
2019-01-25 06:27:36 +00:00
|
|
|
if flatten == Flatten.DiffAvg:
|
2019-01-25 06:15:57 +00:00
|
|
|
if len(peaks) >= 2 and peaks[0] == peaks[1]:
|
|
|
|
np.testing.assert_equal(data, 0)
|
|
|
|
else:
|
|
|
|
pass
|
|
|
|
# If SumAvg, check average.
|
2019-01-08 08:08:37 +00:00
|
|
|
else:
|
2019-01-25 06:27:36 +00:00
|
|
|
assert flatten == Flatten.SumAvg
|
2019-01-25 06:15:57 +00:00
|
|
|
assert_full_scale(data, np.mean(peaks))
|
|
|
|
|
|
|
|
|
|
|
|
def assert_full_scale(data, peak):
|
|
|
|
peak = abs(peak)
|
|
|
|
assert np.amax(data) == pytest.approx(peak, rel=0.01)
|
|
|
|
assert np.amin(data) == pytest.approx(-peak, rel=0.01)
|
2019-01-08 08:08:37 +00:00
|
|
|
|
|
|
|
|
2018-12-29 23:43:11 +00:00
|
|
|
def test_stereo_mmap():
|
2019-01-08 22:28:42 +00:00
|
|
|
wave = Wave(prefix + "stereo-sine-left-2000.wav")
|
2018-12-29 23:43:11 +00:00
|
|
|
assert isinstance(wave.data, np.memmap)
|
|
|
|
|
|
|
|
|
2019-01-08 08:08:37 +00:00
|
|
|
# Miscellaneous tests
|
|
|
|
|
|
|
|
|
2018-07-29 09:07:00 +00:00
|
|
|
def test_wave_subsampling():
|
2019-01-08 22:28:42 +00:00
|
|
|
wave = Wave("tests/sine440.wav")
|
2018-07-29 09:07:00 +00:00
|
|
|
# period = 48000 / 440 = 109.(09)*
|
|
|
|
|
2019-01-08 05:13:02 +00:00
|
|
|
wave.get_around(1000, return_nsamp=501, stride=4)
|
2018-07-29 09:07:00 +00:00
|
|
|
# len([:region_len:subsampling]) == ceil(region_len / subsampling)
|
|
|
|
# If region_len % subsampling != 0, len() != region_len // subsampling.
|
|
|
|
|
2018-11-17 23:23:24 +00:00
|
|
|
stride = 4
|
2019-01-03 08:57:30 +00:00
|
|
|
region = 100 # diameter = region * stride
|
2018-07-29 09:07:00 +00:00
|
|
|
for i in [-1000, 50000]:
|
2018-11-17 23:23:24 +00:00
|
|
|
data = wave.get_around(i, region, stride)
|
2018-07-29 09:07:00 +00:00
|
|
|
assert (data == 0).all()
|
2018-11-18 05:44:50 +00:00
|
|
|
|
|
|
|
|
|
|
|
def test_stereo_doesnt_overflow():
|
|
|
|
""" Ensure loud stereo tracks do not overflow. """
|
2019-01-08 22:28:42 +00:00
|
|
|
wave = Wave("tests/stereo in-phase.wav")
|
2018-11-18 05:44:50 +00:00
|
|
|
|
|
|
|
samp = 100
|
|
|
|
stride = 1
|
|
|
|
data = wave.get_around(wave.nsamp // 2, samp, stride)
|
2018-11-18 06:00:46 +00:00
|
|
|
expect(np.amax(data) > 0.99)
|
|
|
|
expect(np.amin(data) < -0.99)
|
2018-11-18 05:44:50 +00:00
|
|
|
|
|
|
|
# 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.
|
2018-11-18 06:00:46 +00:00
|
|
|
expect(np.amax(np.abs(np.diff(data))) < 0.5)
|
|
|
|
|
|
|
|
assert_expectations()
|
2019-01-03 08:35:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
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):
|
2019-01-08 22:28:42 +00:00
|
|
|
wave = Wave("tests/header larger than filesize.wav")
|
2019-01-03 08:35:53 +00:00
|
|
|
assert wave
|