Skip to content

Commit

Permalink
Use split ch I/O when appropriate, Add test for it
Browse files Browse the repository at this point in the history
  • Loading branch information
dofuuz committed Aug 15, 2024
1 parent 9826e88 commit cd64f13
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 51 deletions.
35 changes: 4 additions & 31 deletions src/soxr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,37 +193,10 @@ def resample(x: ArrayLike, in_rate: float, out_rate: float, quality='HQ') -> np.
x = np.asarray(x, dtype=np.float32)

try:
divide_proc = getattr(soxr_ext, f'csoxr_divide_proc_{x.dtype}')
except AttributeError:
raise TypeError(_DTYPE_ERR_STR.format(x.dtype))

q = _quality_to_enum(quality)

x = np.ascontiguousarray(x) # make array C-contiguous

if x.ndim == 1:
y = divide_proc(in_rate, out_rate, x[:, np.newaxis], q)
return np.squeeze(y, axis=1)
elif x.ndim == 2:
num_channels = x.shape[1]
if num_channels < 1 or _CH_LIMIT < num_channels:
raise ValueError(_CH_EXEED_ERR_STR.format(num_channels))

return divide_proc(in_rate, out_rate, x, q)
else:
raise ValueError('Input must be 1-D or 2-D array')


def _resample_split_ch(x: ArrayLike, in_rate: float, out_rate: float, quality='HQ') -> np.ndarray:
"""
Resample data with splited channel memory.
It has a little speed adventage when data is in Fortran order.
"""
if type(x) != np.ndarray:
x = np.asarray(x, dtype=np.float32)

try:
divide_proc = getattr(soxr_ext, f'csoxr_split_ch_{x.dtype}')
if x.strides[0] == x.itemsize: # split channel memory layout
divide_proc = getattr(soxr_ext, f'csoxr_split_ch_{x.dtype}')
else:
divide_proc = getattr(soxr_ext, f'csoxr_divide_proc_{x.dtype}')
except AttributeError:
raise TypeError(_DTYPE_ERR_STR.format(x.dtype))

Expand Down
4 changes: 2 additions & 2 deletions src/soxr_ext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,8 @@ auto csoxr_split_ch(
const size_t olen = ilen * out_rate / in_rate + 1;
const unsigned channels = x.shape(1);

if (x.stride(0) != 1)
throw std::invalid_argument("Data not continuos");
if (ilen != 0 && x.stride(0) != 1)
throw std::invalid_argument("Data not contiguous");

soxr_error_t err = NULL;

Expand Down
6 changes: 6 additions & 0 deletions tests/bench.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@
print(f'soxr resample: {t:f} (sec)')


# soxr split ch I/O:
sig_s = np.asfortranarray(sig)
t = timeit.timeit(lambda: soxr.resample(sig_s, P, Q, quality=QUALITY), number=REPEAT)
print(f'soxr split ch I/O: {t:f} (sec)')


# soxr with clear()
# It becomes faster then soxr.resample() when input length (=LEN) is short
rs = soxr.ResampleStream(P, Q, sig.shape[1], dtype=sig.dtype, quality=QUALITY)
Expand Down
32 changes: 18 additions & 14 deletions tests/bench_soxr_oneshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
soxr.resample() divides input automatically to retain speed.
"""

import time
import timeit

import matplotlib.pyplot as plt
import numpy as np
Expand All @@ -28,27 +28,31 @@
instfreq = np.exp(np.linspace(np.log(offset+100), np.log(offset+23900), 96000*5))-offset
deltaphase = 2*np.pi*instfreq/P
cphase = np.cumsum(deltaphase)
sig = np.sin(cphase)
sig = np.stack([sig, sig, sig, sig], axis=-1, dtype=np.float64)
sig1 = np.sin(cphase)
sig2 = np.cos(cphase)
sig_i = np.stack([sig1, sig2, sig1, sig2], axis=-1, dtype=np.float64) # C memory order (interleaved)
sig_s = np.asarray([sig1, sig2, sig1, sig2], dtype=np.float64).T # Fortran memory order (channel splited)


out_lens = []
in_lens = range(4800, len(sig1), 4800)
time_divide = []
time_oneshot = []
for length in range(4800, len(sig), 4800):
time_split = []
for length in in_lens:
# soxr resample
start_time = time.perf_counter()
y_resample = soxr.resample(sig[:length], P, Q)
time_proc = time.perf_counter() - start_time
time_proc = timeit.timeit(lambda: soxr.resample(sig_i[:length], P, Q), number=2)
time_divide.append(time_proc)

# soxr resample w/ split channel I/O
time_proc = timeit.timeit(lambda: soxr.resample(sig_s[:length], P, Q), number=2)
time_split.append(time_proc)

# soxr oneshot
start_time = time.perf_counter()
y_oneshot = soxr._resample_oneshot(sig[:length], P, Q)
time_proc = time.perf_counter() - start_time
time_proc = timeit.timeit(lambda: soxr._resample_oneshot(sig_i[:length], P, Q), number=2)
time_oneshot.append(time_proc)
out_lens.append(len(y_oneshot))

plt.plot(out_lens, time_oneshot)
plt.plot(out_lens, time_divide)
plt.plot(in_lens, time_divide, label='divide')
plt.plot(in_lens, time_split, label='split ch')
plt.plot(in_lens, time_oneshot, label='oneshot')
plt.legend()
plt.show()
18 changes: 14 additions & 4 deletions tests/test_resample.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,27 @@ def test_bad_dtype(dtype):
@pytest.mark.parametrize('in_rate, out_rate', [(44100, 32000), (32000, 44100)])
@pytest.mark.parametrize('dtype', [np.float32, np.float64])
def test_divide_match(in_rate, out_rate, dtype):
x = np.random.randn(49999).astype(dtype)
x = np.random.randn(25999,2).astype(dtype)

y_oneshot = soxr._resample_oneshot(x, in_rate, out_rate)
y_divide = soxr.resample(x, in_rate, out_rate)
y_split = soxr.resample(np.asfortranarray(x), in_rate, out_rate)

assert np.allclose(y_oneshot, y_divide)
assert np.allclose(y_oneshot, y_split)


@pytest.mark.parametrize('in_rate, out_rate', [(44100, 32000), (32000, 44100)])
@pytest.mark.parametrize('length', [0, 1, 2, 99, 100, 101, 31999, 32000, 32001, 34828, 34829, 34830, 44099, 44100, 44101, 47999, 48000, 48001, 66149, 66150, 266151])
def test_length_match(in_rate, out_rate, length):
x = np.random.randn(length).astype(np.float32)
x = np.random.randn(266151, 2).astype(np.float32)

y_oneshot = soxr._resample_oneshot(x, in_rate, out_rate)
y_divide = soxr.resample(x, in_rate, out_rate)
y_oneshot = soxr._resample_oneshot(x[:length], in_rate, out_rate)
y_divide = soxr.resample(x[:length], in_rate, out_rate)
y_split = soxr.resample(np.asfortranarray(x)[:length], in_rate, out_rate)

assert np.allclose(y_oneshot, y_divide)
assert np.allclose(y_oneshot, y_split)


@pytest.mark.parametrize('channels', [1, 2, 3, 5, 7, 97, 197])
Expand All @@ -65,8 +69,10 @@ def test_channel_match(channels):

y_oneshot = soxr._resample_oneshot(x, 44100, 32000)
y_divide = soxr.resample(x, 44100, 32000)
y_split = soxr.resample(np.asfortranarray(x), 44100, 32000)

assert np.allclose(y_oneshot, y_divide)
assert np.allclose(y_oneshot, y_split)


@pytest.mark.parametrize('in_rate, out_rate', [(44100, 32000), (32000, 44100)])
Expand Down Expand Up @@ -135,8 +141,10 @@ def test_quality_sine(in_rate, out_rate, quality):
y = make_tone(FREQ, out_rate, DURATION)

y_pred = soxr.resample(x, in_rate, out_rate, quality=quality)
y_split = soxr.resample(np.asfortranarray(x), in_rate, out_rate, quality=quality)

assert np.allclose(y[IG:-IG], y_pred[IG:-IG], atol=1e-4)
assert np.allclose(y[IG:-IG], y_split[IG:-IG], atol=1e-4)


@pytest.mark.parametrize('in_rate,out_rate', [(48000, 24000), (32000, 44100)])
Expand All @@ -150,5 +158,7 @@ def test_int_sine(in_rate, out_rate, dtype):
y = (make_tone(FREQ, out_rate, DURATION) * 16384).astype(dtype)

y_pred = soxr.resample(x, in_rate, out_rate)
y_split = soxr.resample(np.asfortranarray(x), in_rate, out_rate)

assert np.allclose(y[IG:-IG], y_pred[IG:-IG], atol=2)
assert np.allclose(y[IG:-IG], y_split[IG:-IG], atol=2)

0 comments on commit cd64f13

Please sign in to comment.