Commit 1c21cb56 authored by valentin.emiya's avatar valentin.emiya

Waveform.fs can now be of type float

parent cc3eb783
Pipeline #605 failed with stage
in 1 minute
......@@ -503,9 +503,19 @@
"source": [
"Note that:\n",
"* sampling frequency: only a restricted set of sampling frequencies are allowed for input/output (see set of supported frequencies `waveform.VALID_IO_FS`)\n",
"* *dtype*: float/int data types are conserved when exporting a *Waveform*, since the .wav format allows many data types. However, many audio players only read .wav files coded with int16 values so you may not be able to listen to your exported sound with your favorite player. In that case, you may convert the data type of your *Waveform* using the optional *dtype* argument of method *to_wavfile*.\n",
"* mask: the mask is lost when exporting to a .wav file.\n"
"* mask: the mask is lost when exporting to a .wav file.\n",
"* sampling frequency: sampling frequencies may be arbitrary ``float`` or ``int`` values; however, only a restricted set of sampling frequencies are allowed for input/output (see set of supported frequencies ``madarrays.waveform.VALID_IO_FS` below).\n"
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from madarrays.waveform import VALID_IO_FS\n",
......@@ -236,6 +236,7 @@ class TestWaveform:
def test_resample(self):
# Common integer values
for fs in FS:
# Mono
w = Waveform(self.x_mono, fs=self.fs)
......@@ -262,16 +263,34 @@ class TestWaveform:
np.arange(np.floor(fs * self.length / self.fs)) / fs)
# Floating values with ratios that are exact rationals
for (old_fs, new_fs) in [(1, 1.5), (0.5, 3), (100.1, 200.2)]:
w = Waveform(self.x_mono, fs=old_fs)
assert w.fs == new_fs
assert w.length == int(np.floor(new_fs * self.length / old_fs))
# Floating values with ratios that are not well approximated
# by rationals
old_fs = np.sqrt(2)
new_fs = np.pi
with pytest.warns(UserWarning):
w = Waveform(self.x_mono, fs=old_fs)
np.testing.assert_almost_equal(w.fs, new_fs)
w.length, int(np.floor(new_fs * self.length / old_fs)))
# Negative frequency sampling
with pytest.raises(
match='`fs` should be a positive integer \(given: -\d+\)'):
match='`fs` should be a positive number \(given: -\d+\)'):
# Frequency sampling equal to 0
with pytest.raises(
match='`fs` should be a positive integer \(given: 0\)'):
match='`fs` should be a positive number \(given: 0\)'):
# Masked data
......@@ -47,6 +47,7 @@
import warnings
from fractions import Fraction
import numpy as np
import resampy
import simpleaudio as sa
......@@ -133,7 +134,7 @@ class Waveform(MadArray):
data : nd-array [N] or [N, 2]
Audio samples, as a N-length vector for a mono signal or a
[N, 2]-shape array for a stereo signal
fs : int, optional
fs : int or float, optional
Sampling frequency of the original signal, in Hz. If float, truncated.
If None and :paramref:`data` is a Waveform, use `data.fs`, otherwise it
is set to 1.
......@@ -169,7 +170,7 @@ class Waveform(MadArray):
raise ValueError('`data` should be either mono or stereo.')
# add the new attribute to the created instance
obj.fs = int(fs)
obj.fs = fs
return obj
......@@ -206,7 +207,7 @@ class Waveform(MadArray):
def fs(self):
"""Frequency sampling of the audio signal.
"""Frequency sampling of the audio signal (int or float).
The signal is not resampled when the sampling frequency is modified.
......@@ -222,7 +223,7 @@ class Waveform(MadArray):
if fs <= 0:
errmsg = 'fs is not strictly positive (given: {})'
raise ValueError(errmsg.format(fs))
self._fs = int(fs)
self._fs = fs
def rms(self):
......@@ -290,29 +291,60 @@ class Waveform(MadArray):
Can be only performed on a waveform without missing data.
Note that if the current or the new sampling frequencies are not
integers, the new sampling frequency may be different from the
desired value since the resampling method only allows input and
output frequencies of type ``int``. In this case, a warning is
fs : int
fs : int or float
New sampling frequency.
If `fs` is not a positive integer.
If `fs` is not a positive number.
If `self` has missing samples.
assert np.issubdtype(type(fs), np.integer) or np.issubdtype(type(fs),
if fs <= 0:
errmsg = '`fs` should be a positive integer (given: {})'
errmsg = '`fs` should be a positive number (given: {})'
raise ValueError(errmsg.format(fs))
if np.issubdtype(type(fs), np.float) or np.issubdtype(type(self.fs),
# Find a good rational number to approximate the ratio between
# sampling frequencies
fs_ratio = Fraction(fs/self.fs)
fs_ratio = fs_ratio.limit_denominator(10000)
# Sampling frequencies used for the resampling (need to be int)
resample_new_fs = fs_ratio.numerator
resample_old_fs = fs_ratio.denominator
# Adjust new sampling frequency
new_fs = fs_ratio * self.fs
if new_fs != fs:
warnings.warn('New sampling frequency adjusted to {} instead '
'of {} in order to use ``int`` values in '
'``resample``.'.format(fs_ratio * self.fs, fs))
fs = new_fs
resample_new_fs = fs
resample_old_fs = self.fs
if self.is_masked():
errmsg = 'Waveform has missing entries.'
raise ValueError(errmsg)
if fs != self.fs:
if resample_new_fs != resample_old_fs:
x = self.to_np_array()
y = resampy.resample(x, self.fs, fs, axis=0)
y = resampy.resample(x, resample_old_fs, resample_new_fs, axis=0)
self.resize(y.shape, refcheck=False)
self[:] = y
......@@ -411,8 +443,8 @@ class Waveform(MadArray):
If the signal is complex.
If the sampling frequency is not supported (see set of supported
frequencies `waveform.VALID_IO_FS`).
If the sampling frequency is not an integer from the set of
supported frequencies ``madarrays.waveform.VALID_IO_FS``).
If dtype is not supported by the current implementation.
......@@ -420,7 +452,7 @@ class Waveform(MadArray):
if int(self.fs) not in VALID_IO_FS:
if self.fs not in VALID_IO_FS:
errmsg = '`fs` is not a valid sampling frequency (given: {}).'
raise ValueError(errmsg.format(self.fs))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment