From 38f5db5fd2c6a729a89461e0d7ad4dab50f89278 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Tue, 19 Apr 2016 14:09:33 -0400 Subject: [PATCH 01/12] updating docs --- resampy/filters.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resampy/filters.py b/resampy/filters.py index eb88a56..24f084d 100644 --- a/resampy/filters.py +++ b/resampy/filters.py @@ -91,6 +91,10 @@ def get_filter(name_or_function, **kwargs): If a string, and it matches the name of a pre-computed filter, the corresponding filter is retrieved, and kwargs is ignored. + Valid pre-computed filter names are: + - 'kaiser_fast' + - 'kaiser_best' + Returns ------- half_window : np.ndarray From 7dc0868ed39df291bc2bd0b782223516e641cd17 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Tue, 19 Apr 2016 14:13:24 -0400 Subject: [PATCH 02/12] updated doc config --- docs/api.rst | 4 ++++ docs/conf.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 148ef51..63a0511 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -4,3 +4,7 @@ .. automodule:: core :members: + +.. automodule:: filters + :members: + diff --git a/docs/conf.py b/docs/conf.py index 1ca0efe..ba07555 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -59,7 +59,7 @@ class Mock(MagicMock): @classmethod def __getattr__(cls, name): - return Mock() + return Mock() MOCK_MODULES = ['numpy', 'scipy', 'scipy.signal'] sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) From ebc0e94cd71f68e35cfe3f584d3ff91dd16a2584 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Tue, 19 Apr 2016 14:16:19 -0400 Subject: [PATCH 03/12] updated coveragerc --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.coveragerc b/.coveragerc index 7200479..a6c55c9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,3 +3,5 @@ plugins = Cython.Coverage omit = resampy/setup.py resampy/__check_build/* +[report] +show_missing = True From dd47fb8d4f2e47e62e8fb5973ea961c8386657a7 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Tue, 19 Apr 2016 14:28:44 -0400 Subject: [PATCH 04/12] fixing import for doc building --- resampy/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resampy/core.py b/resampy/core.py index 4582711..aa99749 100644 --- a/resampy/core.py +++ b/resampy/core.py @@ -4,7 +4,7 @@ import numpy as np -from . import filters +from .filters import get_filter from .resample import resample_f __all__ = ['resample'] @@ -58,7 +58,7 @@ def resample(x, sr_orig, sr_new, axis=-1, filter='sinc_window', **kwargs): y = np.zeros(shape, dtype=x.dtype) - interp_win, precision = filters.get_filter(filter, **kwargs) + interp_win, precision = get_filter(filter, **kwargs) if sample_ratio < 1: interp_win *= sample_ratio From 3e1017b80c1b831e740cc3d5957fb510700c11cc Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Tue, 19 Apr 2016 14:43:32 -0400 Subject: [PATCH 05/12] fixed doc build --- docs/api.rst | 8 ++++++-- docs/conf.py | 13 ++++--------- docs/index.rst | 4 +++- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 63a0511..4004e37 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2,9 +2,13 @@ .. module:: resampy -.. automodule:: core +Core functionality +------------------ +.. automodule:: resampy.core :members: -.. automodule:: filters +Filters +------- +.. automodule:: resampy.filters :members: diff --git a/docs/conf.py b/docs/conf.py index ba07555..7378844 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../resampy')) +sys.path.insert(0, os.path.abspath('../')) # -- General configuration ------------------------------------------------ @@ -55,14 +55,9 @@ project = u'resampy' copyright = u'2016, Brian McFee' -from mock import MagicMock -class Mock(MagicMock): - @classmethod - def __getattr__(cls, name): - return Mock() - -MOCK_MODULES = ['numpy', 'scipy', 'scipy.signal'] -sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) +import mock +MOCK_MODULES = ['numpy', 'scipy', 'scipy.signal', 'resampy.resample'] +sys.modules.update((mod_name, mock.Mock()) for mod_name in MOCK_MODULES) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/index.rst b/docs/index.rst index c50d68a..d5af23c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,9 +3,11 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. +Examples +-------- + API Reference ============= - .. toctree:: :maxdepth: 2 From 0c7fa7b543aa1212b35a196c38199857fd4a420f Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Tue, 19 Apr 2016 15:08:09 -0400 Subject: [PATCH 06/12] updating docstrings --- docs/conf.py | 1 - resampy/core.py | 30 ++++++++++++++++++++++++++++++ resampy/filters.py | 16 +++++++++++++--- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 7378844..15d4a24 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -280,4 +280,3 @@ intersphinx_mapping = {'python': ('http://docs.python.org/', None), 'numpy': ('http://docs.scipy.org/doc/numpy/', None), 'scipy': ('http://docs.scipy.org/doc/scipy/reference/', None)} - diff --git a/resampy/core.py b/resampy/core.py index aa99749..4bbfac7 100644 --- a/resampy/core.py +++ b/resampy/core.py @@ -42,6 +42,36 @@ def resample(x, sr_orig, sr_new, axis=-1, filter='sinc_window', **kwargs): ------ ValueError if `sr_orig` or `sr_new` is not positive + + Examples + -------- + >>> # Generate a sine wave at 440 Hz for 5 seconds + >>> sr_orig = 44100.0 + >>> x = np.sin(2 * np.pi * 440.0 / sr_orig * np.arange(5 * sr_orig)) + >>> x + array([ 0. , 0.063, ..., -0.125, -0.063]) + >>> # Resample to 22050 with default parameters + >>> resampy.resample(x, sr_orig, 22050) + array([ 0.011, 0.123, ..., -0.193, -0.103]) + >>> # Resample using the fast (low-quality) filter + >>> resampy.resample(x, sr_orig, 22050, filter='kaiser_fast') + array([ 0.013, 0.121, ..., -0.189, -0.102]) + >>> # Resample using a high-quality filter + >>> resampy.resample(x, sr_orig, 22050, filter='kaiser_best') + array([ 0.011, 0.123, ..., -0.193, -0.103]) + >>> # Resample using a Hann-windowed sinc filter + >>> resampy.resample(x, sr_orig, 22050, window=scipy.signal.hann) + array([ 0.011, 0.123, ..., -0.193, -0.103]) + + >>> # Generate stereo data + >>> x_right = np.sin(2 * np.pi * 880.0 / sr_orig * np.arange(len(x)))]) + >>> x_stereo = np.stack([x, x_right]) + >>> x_stereo.shape + (2, 220500) + >>> # Resample along the time axis (1) + >>> y_stereo = resampy.resample(x, sr_orig, 22050, axis=1) + >>> y_stereo.shape + (2, 110250) ''' if sr_orig <= 0: diff --git a/resampy/filters.py b/resampy/filters.py index 24f084d..fc85be0 100644 --- a/resampy/filters.py +++ b/resampy/filters.py @@ -47,6 +47,16 @@ def sinc_window(num_zeros=64, precision=9, window=None, rolloff=0.945): if `num_zeros < 1`, `precision < 1`, or `rolloff` is outside the range `(0, 1]`. + Examples + -------- + >>> # A filter with 10 zero-crossings, 32 samples per crossing, and a + ... # Hann window for tapering. + >>> halfwin, prec = resampy.filters.sinc_window(num_zeros=10, precision=5, + ... window=scipy.signal.hann) + >>> halfwin + array([ 9.450e-01, 9.436e-01, ..., -7.455e-07, -0.000e+00]) + >>> prec + 32 ''' if window is None: @@ -86,14 +96,14 @@ def get_filter(name_or_function, **kwargs): If a function, returns `name_or_function(**kwargs)`. If a string, and it matches the name of one of the defined - filter functions, the corresponding function is called with **kwargs. + filter functions, the corresponding function is called with `**kwargs`. If a string, and it matches the name of a pre-computed filter, the corresponding filter is retrieved, and kwargs is ignored. Valid pre-computed filter names are: - - 'kaiser_fast' - - 'kaiser_best' + - 'kaiser_fast' + - 'kaiser_best' Returns ------- From 45d49572f3b25ce09d31eee82b405a623364e327 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Tue, 19 Apr 2016 15:29:22 -0400 Subject: [PATCH 07/12] Adding example documentation --- docs/example.rst | 93 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 4 +++ 2 files changed, 97 insertions(+) create mode 100644 docs/example.rst diff --git a/docs/example.rst b/docs/example.rst new file mode 100644 index 0000000..58558ae --- /dev/null +++ b/docs/example.rst @@ -0,0 +1,93 @@ +.. _examples: + +Monophonic resampling +===================== + +The following code block demonstrates how to resample an audio signal. + +We use `librosa `_ for loading the audio, +but this is purely for ease of demonstration. ``resampy`` does not depend on ``librosa``. + +.. code-block:: python + :linenos: + + import librosa + import resampy + + # Load in librosa's example audio file at its native sampling rate + x, sr_orig = librosa.load(librosa.util.example_audio_file(), sr=None) + + # x is now a 1-d numpy array, with `sr_orig` audio samples per second + + # We can resample this to any sampling rate we like, say 16000 Hz + y_low = resampy.resample(x, sr_orig, 16000) + + # That's it! + + +Stereo and multi-dimensional data +================================= + +The previous example operates on monophonic signals, but resampy also supports stereo +resampling, as demonstrated below. + +.. code-block:: python + :linenos: + + import librosa + import resampy + + # Load in librosa's example audio file at its native sampling rate. + # This time, also disable the stereo->mono downmixing + x, sr_orig = librosa.load(librosa.util.example_audio_file(), sr=None, mono=False) + + # x is now a 2-d numpy array, with `sr_orig` audio samples per second + # The first dimension of x indexes the channels, the second dimension indexes + # samples. + # x[0] is the left channel, x[1] is the right channel. + + # We can again resample. By default, resample assumes the last index is time. + y_low = resampy.resample(x, sr_orig, 16000) + + # To be more explicit, provide a target axis + y_low = resampy.resample(x, sr_orig, 16000, axis=1) + + +The next block illustrates resampling along an arbitrary dimension. + +.. code-block:: python + :linenos: + + import numpy as np + import resampy + + # Generate 4-dimensional white noise. The third axis (axis=2) will index time. + sr_orig = 22050 + x = np.random.randn(10, 3, sr_orig * 5, 2) + + # x is now a 10-by-3-(5*22050)-by-2 tensor of data. + + # We can resample along the time axis as follows + y_low = resampy.resample(x, sr_orig, 11025, axis=2) + + +Advanced filtering +================== +resampy allows you to control the design of the filters used in resampling operations. + +.. code-block:: python + :linenos: + + import numpy as np + import scipy.signal + import librosa + import resampy + + # Load in some audio + x, sr_orig = librosa.load(librosa.util.example_audio_file(), sr=None, mono=False) + + # Resample to 22050Hz using a Hann-windowed sinc-filter + y = resampy.resample(x, sr_orig, sr_new, window=scipy.signal.hann) + + # Or a shorter sinc-filter than the default (num_zeros=64) + y = resampy.resample(x, sr_orig, sr_new, num_zeros=32) diff --git a/docs/index.rst b/docs/index.rst index d5af23c..09f3cc4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,6 +5,10 @@ Examples -------- +.. toctree:: + :maxdepth: 3 + + example API Reference ============= From 2d6f3c17d45619c0b7e6db6ddf78f223ad5729a2 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Tue, 19 Apr 2016 15:34:51 -0400 Subject: [PATCH 08/12] switched default to kaiser_best --- docs/example.rst | 14 +++++++++++--- resampy/core.py | 7 +++++-- tests/test_core.py | 10 +++++----- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/docs/example.rst b/docs/example.rst index 58558ae..253f0e2 100644 --- a/docs/example.rst +++ b/docs/example.rst @@ -65,11 +65,12 @@ The next block illustrates resampling along an arbitrary dimension. sr_orig = 22050 x = np.random.randn(10, 3, sr_orig * 5, 2) - # x is now a 10-by-3-(5*22050)-by-2 tensor of data. + # x is now a 10-by-3-by-(5*22050)-by-2 tensor of data. # We can resample along the time axis as follows y_low = resampy.resample(x, sr_orig, 11025, axis=2) + # y_low is now a 10-by-3-(5*11025)-by-2 tensor of data Advanced filtering ================== @@ -87,7 +88,14 @@ resampy allows you to control the design of the filters used in resampling opera x, sr_orig = librosa.load(librosa.util.example_audio_file(), sr=None, mono=False) # Resample to 22050Hz using a Hann-windowed sinc-filter - y = resampy.resample(x, sr_orig, sr_new, window=scipy.signal.hann) + y = resampy.resample(x, sr_orig, sr_new, filter='sinc_window', window=scipy.signal.hann) # Or a shorter sinc-filter than the default (num_zeros=64) - y = resampy.resample(x, sr_orig, sr_new, num_zeros=32) + y = resampy.resample(x, sr_orig, sr_new, filter='sinc_window', num_zeros=32) + + # Or use the pre-built high-quality filter + y = resampy.resample(x, sr_orig, sr_new, filter='kaiser_best') + + # Or use the pre-built fast filter + y = resampy.resample(x, sr_orig, sr_new, filter='kaiser_fast') + diff --git a/resampy/core.py b/resampy/core.py index 4bbfac7..5234e17 100644 --- a/resampy/core.py +++ b/resampy/core.py @@ -10,7 +10,7 @@ __all__ = ['resample'] -def resample(x, sr_orig, sr_new, axis=-1, filter='sinc_window', **kwargs): +def resample(x, sr_orig, sr_new, axis=-1, filter='kaiser_best', **kwargs): '''Resample a signal x from sr_orig to sr_new along a given axis. Parameters @@ -30,6 +30,8 @@ def resample(x, sr_orig, sr_new, axis=-1, filter='sinc_window', **kwargs): filter : optional, str or callable The resampling filter to use. + By default, uses the `kaiser_best` (pre-computed filter). + kwargs additional keyword arguments provided to the specified filter @@ -60,7 +62,8 @@ def resample(x, sr_orig, sr_new, axis=-1, filter='sinc_window', **kwargs): >>> resampy.resample(x, sr_orig, 22050, filter='kaiser_best') array([ 0.011, 0.123, ..., -0.193, -0.103]) >>> # Resample using a Hann-windowed sinc filter - >>> resampy.resample(x, sr_orig, 22050, window=scipy.signal.hann) + >>> resampy.resample(x, sr_orig, 22050, filter='sinc_window', + ... window=scipy.signal.hann) array([ 0.011, 0.123, ..., -0.193, -0.103]) >>> # Generate stereo data diff --git a/tests/test_core.py b/tests/test_core.py index f53a773..faffab0 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -41,7 +41,7 @@ def test_bad_rolloff(): def __test(rolloff): x = np.zeros(100) - resampy.resample(x, 100, 50, rolloff=rolloff) + resampy.resample(x, 100, 50, filter='sinc_window', rolloff=rolloff) yield __test, -1 yield __test, 1.5 @@ -51,14 +51,14 @@ def __test(rolloff): def test_bad_precision(): x = np.zeros(100) - resampy.resample(x, 100, 50, precision=-1) + resampy.resample(x, 100, 50, filter='sinc_window', precision=-1) @raises(ValueError) def test_bad_num_zeros(): x = np.zeros(100) - resampy.resample(x, 100, 50, num_zeros=0) + resampy.resample(x, 100, 50, filter='sinc_window', num_zeros=0) def test_dtype(): @@ -78,7 +78,7 @@ def test_bad_window(): x = np.zeros(100) - resampy.resample(x, 100, 200, window=np.ones(50)) + resampy.resample(x, 100, 200, filter='sinc_window', window=np.ones(50)) def test_good_window(): @@ -87,6 +87,6 @@ def test_good_window(): sr_new = 200 x = np.random.randn(500) - y = resampy.resample(x, sr_orig, sr_new, window=scipy.signal.blackman) + y = resampy.resample(x, sr_orig, sr_new, filter='sinc_window', window=scipy.signal.blackman) eq_(len(y), 2 * len(x)) From f1d4ab28fefb53fd4a84d388c294ef5f03fe0d16 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Tue, 19 Apr 2016 17:40:37 -0400 Subject: [PATCH 09/12] updated documentation and requirements --- docs/index.rst | 22 ++++++++++++++++++++++ requirements.txt | 1 + 2 files changed, 23 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 09f3cc4..cdc37da 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,6 +3,28 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. +Introduction +------------ +`resampy` is a python module for efficient time-series resampling. It is based on the +band-limited sinc interpolation method for sampling rate conversion as described by +[1]_. + +.. [1] Smith, Julius O. Digital Audio Resampling Home Page + Center for Computer Research in Music and Acoustics (CCRMA), + Stanford University, 2015-02-23. + Web published at ``_. + +`resampy` supports multi-dimensional resampling on numpy arrays, and is well-suited to +audio applications. For long-duration signals --- e.g., minutes at a high-quality +sampling rate --- `resampy` will be considerably faster +than `scipy.signal.resample` and have little perceivable difference in audio quality. + +Its dependencies are `numpy `_, `scipy +`_, and `Cython `_. + + +For a quick introduction to using `resampy`, please refer to the `Examples`_ section. + Examples -------- .. toctree:: diff --git a/requirements.txt b/requirements.txt index 8c60f2b..0b5043f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ numpy>=1.10 scipy>=0.13 six>=1.3 +Cython>=0.21 From 34a06042388855e743a000afea13b882689f88fe Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Tue, 19 Apr 2016 17:42:52 -0400 Subject: [PATCH 10/12] fixed a typesetting error --- docs/example.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/example.rst b/docs/example.rst index 253f0e2..dbef70e 100644 --- a/docs/example.rst +++ b/docs/example.rst @@ -6,7 +6,7 @@ Monophonic resampling The following code block demonstrates how to resample an audio signal. We use `librosa `_ for loading the audio, -but this is purely for ease of demonstration. ``resampy`` does not depend on ``librosa``. +but this is purely for ease of demonstration. `resampy` does not depend on `librosa`. .. code-block:: python :linenos: From ea139410a6c85a8a89262a5faafabf64e5972b4e Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Tue, 19 Apr 2016 17:55:19 -0400 Subject: [PATCH 11/12] more doc updates --- resampy/filters.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/resampy/filters.py b/resampy/filters.py index fc85be0..a072f2a 100644 --- a/resampy/filters.py +++ b/resampy/filters.py @@ -1,6 +1,30 @@ #!/usr/bin/env python # -*- encoding: utf-8 -*- -'''Filter construction and loading''' +'''Filter construction and loading. +-------------------------------- + +`resampy` provides two pre-computed resampling filters which are tuned for either +high-quality or fast calculation: + + - `kaiser_best` : 64 zero-crossings, a Kaiser window with beta=14.769656459379492, + and a roll-off frequency of Nyquist * 0.9475937167399596. + + - `kaiser_fast` : 16 zero-crossings, a Kaiser window with beta=8.555504641634386, + and a roll-off frequency of Nyquist * 0.85. + +These filters can be used by calling `resample` as follows: + >>> resampy.resample(x, sr_orig, sr_new, filter='kaiser_best') # High-quality + >>> resampy.resample(x, sr_orig, sr_new, filter='kaiser_fast') # Fast calculation + + +It is also possible to construct custom filters as follows: + + >>> resampy.resample(x, sr_orig, sr_new, filter='sinc_window', + ... **kwargs) + +where ``**kwargs`` are additional parameters to `resampy.filters.sinc_window`_. + +''' import scipy.signal import numpy as np @@ -57,6 +81,11 @@ def sinc_window(num_zeros=64, precision=9, window=None, rolloff=0.945): array([ 9.450e-01, 9.436e-01, ..., -7.455e-07, -0.000e+00]) >>> prec 32 + + >>> # Or using sinc-window filter construction directly in resample + >>> y = resampy.resample(x, sr_orig, sr_new, filter='sinc_window', + ... num_zeros=10, precision=5, + ... window=scipy.signal.hann) ''' if window is None: From 8158479bcd8c0899c66152a5b9d6fe9fa6034938 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Tue, 19 Apr 2016 17:55:46 -0400 Subject: [PATCH 12/12] version update --- resampy/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resampy/version.py b/resampy/version.py index be42a99..aeba0e5 100644 --- a/resampy/version.py +++ b/resampy/version.py @@ -2,5 +2,5 @@ # -*- coding: utf-8 -*- """Version info""" -short_version = '0.0' -version = '0.0.0' +short_version = '0.1' +version = '0.1.0pre'