Skip to content

Commit

Permalink
unit test update: convert to unit tests, add negative examples.
Browse files Browse the repository at this point in the history
  • Loading branch information
yiitozer committed Apr 24, 2024
1 parent 40d37bc commit 45d5b04
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 116 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test_conda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ jobs:
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
- name: Test with unittest
run: |
pytest
python tests
8 changes: 4 additions & 4 deletions .github/workflows/test_pip.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ on:
push:
branches:
- main
- unit_tests
- revision
pull_request:
branches:
- main
- unit_tests
- revision

jobs:
build:
Expand Down Expand Up @@ -50,6 +50,6 @@ jobs:
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
- name: Test with unittest
run: |
pytest
python tests
6 changes: 6 additions & 0 deletions libsoni/core/f0.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ def sonify_f0(time_f0: np.ndarray,
f0_sonification: np.ndarray (np.float32 / np.float64) [shape=(M, )]
Sonified f0-trajectory.
"""
if time_f0.ndim != 2:
raise IndexError('time_f0 must be a numpy array of size [N, 2]')
if time_f0.shape[1] != 2:
raise IndexError('time_f0 must be a numpy array of size [N, 2]')


if gains is not None:
assert len(gains) == time_f0.shape[0], 'Array for confidence must have same length as time_f0.'
else:
Expand Down
4 changes: 2 additions & 2 deletions libsoni/core/methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,8 +397,8 @@ def generate_tone_instantaneous_phase(frequency_vector: np.ndarray,
partials_amplitudes = np.ones(len(partials)) if partials_amplitudes is None else partials_amplitudes
partials_phase_offsets = np.zeros(len(partials)) if partials_phase_offsets is None else partials_phase_offsets

assert len(partials) == len(partials_amplitudes) == len(partials_phase_offsets), \
'Partials, Partials_amplitudes and Partials_phase_offsets must be of equal length.'
if not (len(partials) == len(partials_amplitudes) == len(partials_phase_offsets)):
raise ValueError('Partials, Partials_amplitudes and Partials_phase_offsets must be of equal length.')

generated_tone = np.zeros_like(frequency_vector)

Expand Down
12 changes: 11 additions & 1 deletion libsoni/util/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def plot_sonify_novelty_beats(fn_wav, fn_ann, title=''):

def format_df(df: pd.DataFrame) -> pd.DataFrame:
df = df.copy().rename(columns=str.lower)

check_df_schema(df)
if 'duration' not in df.columns:
try:
df['duration'] = df['end'] - df['start']
Expand Down Expand Up @@ -385,3 +385,13 @@ def visualize_pianoroll(pianoroll_df: pd.DataFrame,
plt.tight_layout()

return fig, ax


def check_df_schema(df: pd.DataFrame):
try:
columns_bool = (df.columns == ['start', 'duration', 'pitch', 'velocity', 'label']).all() and \
len(df.columns) == 5
if not columns_bool:
raise ValueError("Columns of the dataframe must be ['start', 'duration', 'pitch', 'velocity', 'label'].")
except:
raise ValueError("Columns of the dataframe must be ['start', 'duration', 'pitch', 'velocity', 'label'].")
11 changes: 11 additions & 0 deletions tests/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import unittest


if __name__ == '__main__':
root_dir = './'
loader = unittest.TestLoader()
testSuite = loader.discover(start_dir='tests',
pattern="test_*.py")

runner = unittest.TextTestRunner(verbosity=2)
runner.run(testSuite)
Binary file removed tests/data/f0_None_0.wav
Binary file not shown.
Binary file removed tests/data/f0_None_1.wav
Binary file not shown.
80 changes: 53 additions & 27 deletions tests/test_f0.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,58 @@
import numpy as np
import soundfile as sf
from unittest import TestCase


from libsoni.core import f0

Fs = 22050
C_MAJOR_SCALE = [261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25, 0.0]
DURATIONS = [None, 3.0, 5.0]
PARTIALS = [np.array([1]), np.array([1, 2, 3])]
PARTIALS_AMPLITUDES = [np.array([1]), np.array([1, 0.5, 0.25])]


def test_f0():
time_positions = np.arange(0.2, len(C_MAJOR_SCALE) * 0.5, 0.5)
time_f0 = np.column_stack((time_positions, C_MAJOR_SCALE))

for duration in DURATIONS:
for par_idx, partials in enumerate(PARTIALS):
if duration is None:
duration_in_samples = None
else:
duration_in_samples = int(duration * Fs)

y = f0.sonify_f0(time_f0=time_f0,
partials=partials,
partials_amplitudes=PARTIALS_AMPLITUDES[par_idx],
sonification_duration=duration_in_samples,
fs=Fs)

ref, _ = sf.read(f'tests/data/f0_{duration_in_samples}_{par_idx}.wav')
assert len(y) == len(ref), 'Length of the generated sonification does not match with the reference!'
assert np.allclose(y, ref, atol=1e-4, rtol=1e-5)

class TestF0(TestCase):
def setUp(self) -> None:
c_major_scale = [261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25, 0.0]
time_positions = np.arange(0.2, len(c_major_scale) * 0.5, 0.5)
self.fs = 22050
self.durations = [int(3.0*self.fs), int(5.0*self.fs)]
self.partials = [np.array([1]), np.array([1, 2, 3])]
self.partials_amplitudes = [np.array([1]), np.array([1, 0.5, 0.25])]
self.time_f0 = np.column_stack((time_positions, c_major_scale))

def test_input_types(self) -> None:
[self.assertIsInstance(duration, int) for duration in self.durations]
[self.assertIsInstance(partials, np.ndarray) for partials in self.partials]
[self.assertIsInstance(partials_amplitude, np.ndarray) for partials_amplitude in self.partials_amplitudes]
self.assertIsInstance(self.fs, int)
self.assertIsInstance(self.time_f0, np.ndarray)

def test_input_shape(self) -> None:
with self.assertRaises(IndexError) as context:
_ = f0.sonify_f0(time_f0=np.zeros(1))
self.assertEqual(str(context.exception), 'time_f0 must be a numpy array of size [N, 2]')

with self.assertRaises(IndexError) as context:
_ = f0.sonify_f0(time_f0=np.zeros((3, 3)))
self.assertEqual(str(context.exception), 'time_f0 must be a numpy array of size [N, 2]')

def test_invalid_partial_sizes(self):
with self.assertRaises(ValueError) as context:
_ = f0.sonify_f0(time_f0=self.time_f0,
partials=self.partials[0],
partials_amplitudes=self.partials_amplitudes[1],
sonification_duration=self.durations[0],
fs=self.fs)

self.assertEqual(str(context.exception), 'Partials, Partials_amplitudes and Partials_phase_offsets must be '
'of equal length.')

def test_sonification(self) -> None:
for duration in self.durations:
for par_idx, partials in enumerate(self.partials):
y = f0.sonify_f0(time_f0=self.time_f0,
partials=self.partials[par_idx],
partials_amplitudes=self.partials_amplitudes[par_idx],
sonification_duration=duration,
fs=self.fs)

ref, _ = sf.read(f'tests/data/f0_{duration}_{par_idx}.wav')
self.assertEqual(len(y), len(ref), msg='Length of the generated sonification '
'does not match with the reference!')
assert np.allclose(y, ref, atol=1e-4, rtol=1e-5)
104 changes: 57 additions & 47 deletions tests/test_methods.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,63 @@
import numpy as np
import soundfile as sf
from unittest import TestCase

from libsoni.core.methods import generate_click, generate_shepard_tone, generate_sinusoid
from libsoni.core.methods import generate_click, generate_shepard_tone, generate_sinusoid,\
generate_tone_instantaneous_phase
from libsoni.util.utils import pitch_to_frequency

DURATIONS = [0.2, 0.5, 1.0]
FADE = [0.05, 0.1]
PITCHES = [60, 69]
Fs = 22050


def test_click():
for duration in DURATIONS:
for pitch in PITCHES:
y = generate_click(pitch=pitch,
click_fading_duration=duration)

ref, _ = sf.read(f'tests/data/click_{pitch}_{(int(Fs * duration))}.wav')

assert len(y) == len(ref), 'Length of the generated sonification does not match with the reference!'
assert np.allclose(y, ref, atol=1e-4, rtol=1e-5)


def test_sinusoid():
for duration in DURATIONS:
for pitch in PITCHES:
for fading_duration in FADE:
freq = pitch_to_frequency(pitch=pitch)
y = generate_sinusoid(frequency=freq,
duration=duration,
fading_duration=fading_duration)
dur_samples = int(Fs * duration)
fade_samples = int(Fs * fading_duration)
ref, _ = sf.read(f'tests/data/sin_{pitch}_{dur_samples}_{fade_samples}.wav')
assert len(y) == len(ref), 'Length of the generated sonification does not match with the reference!'
assert np.allclose(y, ref, atol=1e-4, rtol=1e-5)


def test_shepard_tone():
for duration in DURATIONS:
for pitch in PITCHES:
for fading_duration in FADE:
pitch_class = pitch % 12
y = generate_shepard_tone(pitch_class=pitch_class,
duration=duration,
fading_duration=fading_duration)
dur_samples = int(Fs * duration)
fade_samples = int(Fs * fading_duration)
ref, _ = sf.read(f'tests/data/shepard_{pitch_class}_{dur_samples}_{fade_samples}.wav')
assert len(y) == len(ref), 'Length of the generated sonification does not match with the reference!'
assert np.allclose(y, ref, atol=1e-4, rtol=1e-5)

class TestMethods(TestCase):
def setUp(self) -> None:
self.frequency_vector = np.array([261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25, 0.0])
self.pitches = [60, 69]
self.fade_vals = [0.05, 0.1]
self.fs = 22050
self.durations = [int(0.2*self.fs), int(0.5*self.fs), int(1.0*self.fs)]
self.partials = [np.array([1]), np.array([1, 2, 3])]
self.partials_amplitudes = [np.array([1]), np.array([1, 0.5, 0.25])]

def test_input_types(self) -> None:
[self.assertIsInstance(duration, int) for duration in self.durations]
[self.assertIsInstance(freq, float) for freq in self.frequency_vector]
[self.assertIsInstance(partials, np.ndarray) for partials in self.partials]
[self.assertIsInstance(partials_amplitude, np.ndarray) for partials_amplitude in self.partials_amplitudes]
self.assertIsInstance(self.fs, int)

def test_invalid_partial_sizes(self) -> None:
with self.assertRaises(ValueError) as context:
_ = generate_tone_instantaneous_phase(self.frequency_vector,
partials=self.partials[0],
partials_amplitudes=self.partials_amplitudes[1],
fs=self.fs)

self.assertEqual(str(context.exception), 'Partials, Partials_amplitudes and Partials_phase_offsets must be '
'of equal length.')

def test_click(self) -> None:
for duration in self.durations:
for pitch in self.pitches:
for fade_val in self.fade_vals:
freq = pitch_to_frequency(pitch=pitch)
y = generate_sinusoid(frequency=freq,
duration=duration/self.fs,
fading_duration=fade_val)
fade_samples = int(self.fs * fade_val)
ref, _ = sf.read(f'tests/data/sin_{pitch}_{duration}_{fade_samples}.wav')
self.assertEqual(len(y), len(ref), msg='Length of the generated sonification '
'does not match with the reference!')
assert np.allclose(y, ref, atol=1e-4, rtol=1e-5)

def test_shepard_tone(self) -> None:
for duration in self.durations:
for pitch in self.pitches:
for fade_val in self.fade_vals:
pitch_class = pitch % 12
y = generate_shepard_tone(pitch_class=pitch_class,
duration=duration/self.fs,
fading_duration=fade_val)
fade_samples = int(self.fs * fade_val)
ref, _ = sf.read(f'tests/data/shepard_{pitch_class}_{duration}_{fade_samples}.wav')
self.assertEqual(len(y), len(ref), msg='Length of the generated sonification '
'does not match with the reference!')
assert np.allclose(y, ref, atol=1e-4, rtol=1e-5)
Loading

0 comments on commit 45d5b04

Please sign in to comment.