diff --git a/pyproject.toml b/pyproject.toml index 6a9ee93..8d21403 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,12 @@ [tool.poetry] name = "pytsmod" version = "0.2.0" -description = "" +description = "An open-source Python library for audio time-scale modification." authors = ["Sangeon Yong "] +license = "GPL-3.0" +readme = "README.md" + [tool.poetry.dependencies] python = "^3.6" numpy = "^1.16.0" @@ -15,8 +18,8 @@ librosa = "^0.8" pytest = "^5.2" flake8 = "^3.8.3" -# [tool.poetry.scripts] -# tsmod = 'pytsmod.console:run' +[tool.poetry.scripts] +tsmod = 'pytsmod.console:run' [build-system] requires = ["poetry>=0.12"] diff --git a/pytsmod/console.py b/pytsmod/console.py new file mode 100755 index 0000000..1dfc84c --- /dev/null +++ b/pytsmod/console.py @@ -0,0 +1,100 @@ +import sys +sys.path.append('./') + +from pytsmod import ola, wsola +from pytsmod import phase_vocoder as pv +from pytsmod import phase_vocoder_int as pv_int +import argparse +import soundfile as sf + + +def run(): + parser = argparse.ArgumentParser(description='Processing time-scale modification for given audio file.') + parser.add_argument('algorithm', nargs='?', choices=['ola', 'wsola', 'pv', 'pv_int', 'hp']) + # parser.add_argument('--help', action='store_true') + + args, sub_args = parser.parse_known_args() + + if args.algorithm == 'ola': + parser = argparse.ArgumentParser() + parser.add_argument('input_file', type=str) + parser.add_argument('output_file', type=str) + parser.add_argument('alpha', type=float) + parser.add_argument('--win_type', '-wt', default='hann', type=str) + parser.add_argument('--win_size', '-ws', default=1024, type=int) + parser.add_argument('--syn_hop_size', '-sh', default=512, type=int) + + params = parser.parse_args(sub_args) + + x, sr = sf.read(params.input_file) + + y = ola(x, params.alpha, win_type=params.win_type, + win_size=params.win_size, syn_hop_size=params.syn_hop_size) + elif args.algorithm == 'wsola': + parser = argparse.ArgumentParser() + parser.add_argument('input_file', type=str) + parser.add_argument('output_file', type=str) + parser.add_argument('alpha', type=float) + parser.add_argument('--win_type', '-wt', default='hann', type=str) + parser.add_argument('--win_size', '-ws', default=1024, type=int) + parser.add_argument('--syn_hop_size', '-sh', default=512, type=int) + parser.add_argument('--tolerance', '-t', default=512, type=int) + + params = parser.parse_args(sub_args) + + x, sr = sf.read(params.input_file) + + y = wsola(x, params.alpha, win_type=params.win_type, + win_size=params.win_size, syn_hop_size=params.syn_hop_size, + tolerance=params.tolerance) + elif args.algorithm == 'pv': + parser = argparse.ArgumentParser() + parser.add_argument('input_file', type=str) + parser.add_argument('output_file', type=str) + parser.add_argument('alpha', type=float) + parser.add_argument('--win_type', '-wt', default='sin', type=str) + parser.add_argument('--win_size', '-ws', default=2048, type=int) + parser.add_argument('--syn_hop_size', '-sh', default=512, type=int) + parser.add_argument('--zero_pad', '-z', default=0, type=int) + parser.add_argument('--restore_energy', '-e', action='store_true') + parser.add_argument('--fft_shift', '-fs', action='store_true') + parser.add_argument('--phase_lock', '-pl', action='store_true') + + params = parser.parse_args(sub_args) + + x, sr = sf.read(params.input_file) + + y = pv(x, params.alpha, win_type=params.win_type, + win_size=params.win_size, syn_hop_size=params.syn_hop_size, + zero_pad=params.zero_pad, restore_energy=params.restore_energy, + fft_shift=params.fft_shift, phase_lock=params.phase_lock) + elif args.algorithm == 'pv_int': + parser = argparse.ArgumentParser() + parser.add_argument('input_file', type=str) + parser.add_argument('output_file', type=str) + parser.add_argument('alpha', type=int) + parser.add_argument('--win_type', '-wt', default='hann', type=str) + parser.add_argument('--win_size', '-ws', default=2048, type=int) + parser.add_argument('--syn_hop_size', '-sh', default=512, type=int) + parser.add_argument('--zero_pad', '-z', default=None, type=int) + parser.add_argument('--restore_energy', '-e', action='store_true') + parser.add_argument('--fft_shift', '-fs', action='store_true') + + params = parser.parse_args(sub_args) + print(params.zero_pad) + + x, sr = sf.read(params.input_file) + + y = pv_int(x, params.alpha, win_type=params.win_type, + win_size=params.win_size, syn_hop_size=params.syn_hop_size, + zero_pad=params.zero_pad, + restore_energy=params.restore_energy, + fft_shift=params.fft_shift) + # elif args.algorithm == 'hp': + # pass + + sf.write(params.output_file, y, sr) + + +if __name__ == '__main__': + run() diff --git a/pytsmod/pvtsm.py b/pytsmod/pvtsm.py index a1bb4b7..fa60949 100755 --- a/pytsmod/pvtsm.py +++ b/pytsmod/pvtsm.py @@ -172,7 +172,7 @@ def phase_vocoder_int(x, s, win_type='hann', win_size=2048, syn_hop_size=512, y[c, :] = y_chan - return y + return y.squeeze() def _find_peaks(spec): diff --git a/tests/test_console.py b/tests/test_console.py new file mode 100755 index 0000000..5e8f3f4 --- /dev/null +++ b/tests/test_console.py @@ -0,0 +1,155 @@ +import pytest +from pytsmod import ola, wsola +from pytsmod import phase_vocoder as pv +from pytsmod import phase_vocoder_int as pv_int +import soundfile as sf +import numpy as np +import os +from subprocess import call + + +@pytest.mark.parametrize('algorithm', ['ola', 'wsola', 'pv', 'pv_int']) +def test_console_default_params(algorithm): + test_file = 'tests/data/castanetsviolin.wav' + alpha = 2 + x, sr = sf.read(test_file) + y = globals()[algorithm](x, alpha) + + cmd = ['python', 'pytsmod/console.py', algorithm, + test_file, 'temp_cli.wav', str(alpha)] + if algorithm == 'pv_int': + cmd.append('-fs') + call(cmd) + + sf.write('temp.wav', y, sr) + y_, _ = sf.read('temp.wav') + + y_cli, _ = sf.read('temp_cli.wav') + + os.remove('temp.wav') + os.remove('temp_cli.wav') + + assert np.allclose(y_, y_cli) + + +@pytest.mark.parametrize('alpha', [1.25]) +@pytest.mark.parametrize('win_type', ['sin']) +@pytest.mark.parametrize('win_size', [512]) +@pytest.mark.parametrize('syn_hop_size', [256]) +def test_console_ola(alpha, win_type, win_size, syn_hop_size): + test_file = 'tests/data/castanetsviolin.wav' + x, sr = sf.read(test_file) + y = ola(x, alpha, win_type=win_type, win_size=win_size, + syn_hop_size=syn_hop_size) + + cmd = ['python', 'pytsmod/console.py', 'ola', + test_file, 'temp_cli.wav', str(alpha), + '-wt', win_type, '-ws', str(win_size), + '-sh', str(syn_hop_size)] + call(cmd) + + sf.write('temp.wav', y, sr) + y_, _ = sf.read('temp.wav') + + y_cli, _ = sf.read('temp_cli.wav') + + os.remove('temp.wav') + os.remove('temp_cli.wav') + + assert np.allclose(y_, y_cli) + + +@pytest.mark.parametrize('alpha', [1.25]) +@pytest.mark.parametrize('win_type', ['sin']) +@pytest.mark.parametrize('win_size', [512]) +@pytest.mark.parametrize('syn_hop_size', [256]) +@pytest.mark.parametrize('tolerance', [256]) +def test_console_wsola(alpha, win_type, win_size, syn_hop_size, tolerance): + test_file = 'tests/data/castanetsviolin.wav' + x, sr = sf.read(test_file) + y = wsola(x, alpha, win_type=win_type, win_size=win_size, + syn_hop_size=syn_hop_size, tolerance=tolerance) + + cmd = ['python', 'pytsmod/console.py', 'wsola', + test_file, 'temp_cli.wav', str(alpha), + '-wt', win_type, '-ws', str(win_size), + '-sh', str(syn_hop_size), '-t', str(tolerance)] + call(cmd) + + sf.write('temp.wav', y, sr) + y_, _ = sf.read('temp.wav') + + y_cli, _ = sf.read('temp_cli.wav') + + os.remove('temp.wav') + os.remove('temp_cli.wav') + + assert np.allclose(y_, y_cli) + + +@pytest.mark.parametrize('alpha', [1.25]) +@pytest.mark.parametrize('win_type', ['hann']) +@pytest.mark.parametrize('win_size', [1024]) +@pytest.mark.parametrize('syn_hop_size', [256]) +@pytest.mark.parametrize('zero_pad', [256]) +@pytest.mark.parametrize('restore_energy', [True]) +@pytest.mark.parametrize('fft_shift', [True]) +@pytest.mark.parametrize('phase_lock', [True]) +def test_console_pv(alpha, win_type, win_size, syn_hop_size, zero_pad, + restore_energy, fft_shift, phase_lock): + test_file = 'tests/data/castanetsviolin.wav' + x, sr = sf.read(test_file) + y = pv(x, alpha, win_type=win_type, win_size=win_size, + syn_hop_size=syn_hop_size, zero_pad=zero_pad, + restore_energy=restore_energy, fft_shift=fft_shift, + phase_lock=phase_lock) + + cmd = ['python', 'pytsmod/console.py', 'pv', + test_file, 'temp_cli.wav', str(alpha), + '-wt', win_type, '-ws', str(win_size), + '-sh', str(syn_hop_size), '-z', str(zero_pad), + '-e', '-fs', '-pl'] + call(cmd) + + sf.write('temp.wav', y, sr) + y_, _ = sf.read('temp.wav') + + y_cli, _ = sf.read('temp_cli.wav') + + os.remove('temp.wav') + os.remove('temp_cli.wav') + + assert np.allclose(y_, y_cli) + + +@pytest.mark.parametrize('alpha', [2]) +@pytest.mark.parametrize('win_type', ['sin']) +@pytest.mark.parametrize('win_size', [1024]) +@pytest.mark.parametrize('syn_hop_size', [256]) +@pytest.mark.parametrize('zero_pad', [256]) +@pytest.mark.parametrize('restore_energy', [True]) +@pytest.mark.parametrize('fft_shift', [False]) +def test_console_pv_int(alpha, win_type, win_size, syn_hop_size, zero_pad, + restore_energy, fft_shift): + test_file = 'tests/data/castanetsviolin.wav' + x, sr = sf.read(test_file) + y = pv(x, alpha, win_type=win_type, win_size=win_size, + syn_hop_size=syn_hop_size, zero_pad=zero_pad, + restore_energy=restore_energy, fft_shift=fft_shift) + + cmd = ['python', 'pytsmod/console.py', 'pv', + test_file, 'temp_cli.wav', str(alpha), + '-wt', win_type, '-ws', str(win_size), + '-sh', str(syn_hop_size), '-z', str(zero_pad), + '-e'] + call(cmd) + + sf.write('temp.wav', y, sr) + y_, _ = sf.read('temp.wav') + + y_cli, _ = sf.read('temp_cli.wav') + + os.remove('temp.wav') + os.remove('temp_cli.wav') + + assert np.allclose(y_, y_cli)