diff --git a/sox/transform.py b/sox/transform.py index fda9103..ea2523e 100644 --- a/sox/transform.py +++ b/sox/transform.py @@ -1528,8 +1528,67 @@ def pad(self, start_duration=0.0, end_duration=0.0): return self - def phaser(self): - raise NotImplementedError + def phaser(self, gain_in=0.8, gain_out=0.74, delay=3, decay=0.4, speed=0.5, + modulation_shape='sinusoidal'): + '''Apply a phasing effect to the audio. + + Parameters + ---------- + gain_in : float, default=0.8 + Input volume between 0 and 1 + gain_out: float, default=0.74 + Output volume between 0 and 1 + delay : float, default=3 + Delay in miliseconds between 0 and 5 + decay : float, default=0.4 + Decay relative to gain_in, between 0.1 and 0.5. + speed : float, default=0.5 + Modulation speed in Hz, between 0.1 and 2 + modulation_shape : str, defaul='sinusoidal' + Modulation shpae. One of 'sinusoidal' or 'triangular' + + See Also + -------- + flanger, tremolo + ''' + if not is_number(gain_in) or gain_in <= 0 or gain_in > 1: + raise ValueError("gain_in must be a number between 0 and 1.") + + if not is_number(gain_out) or gain_out <= 0 or gain_out > 1: + raise ValueError("gain_out must be a number between 0 and 1.") + + if not is_number(delay) or delay <= 0 or delay > 5: + raise ValueError("delay must be a positive number.") + + if not is_number(decay) or decay < 0.1 or decay > 0.5: + raise ValueError("decay must be a number between 0.1 and 0.5.") + + if not is_number(speed) or speed < 0.1 or speed > 2: + raise ValueError("speed must be a positive number.") + + if modulation_shape not in ['sinusoidal', 'triangular']: + raise ValueError( + "modulation_shape must be one of 'sinusoidal', 'triangular'." + ) + + effect_args = [ + 'phaser', + '{}'.format(gain_in), + '{}'.format(gain_out), + '{}'.format(delay), + '{}'.format(decay), + '{}'.format(speed) + ] + + if modulation_shape == 'sinusoidal': + effect_args.append('-s') + elif modulation_shape == 'triangular': + effect_args.append('-t') + + self.effects.extend(effect_args) + self.effects_log.append('phaser') + + return self def pitch(self, n_semitones, quick=False): '''Pitch shift the audio without changing the tempo. diff --git a/tests/test_transform.py b/tests/test_transform.py index 50fe40f..bf35f0d 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -1932,6 +1932,127 @@ def test_end_duration_invalid(self): tfm.pad(end_duration='foo') +class TestTransformerPhaser(unittest.TestCase): + + def test_default(self): + tfm = new_transformer() + tfm.phaser() + + actual_args = tfm.effects + expected_args = ['phaser', '0.8', '0.74', '3', '0.4', '0.5', '-s'] + self.assertEqual(expected_args, actual_args) + + actual_log = tfm.effects_log + expected_log = ['phaser'] + self.assertEqual(expected_log, actual_log) + + actual_res = tfm.build(INPUT_FILE, OUTPUT_FILE) + expected_res = True + self.assertEqual(expected_res, actual_res) + + def test_gain_in_valid(self): + tfm = new_transformer() + tfm.phaser(gain_in=0.5) + + actual_args = tfm.effects + expected_args = ['phaser', '0.5', '0.74', '3', '0.4', '0.5', '-s'] + self.assertEqual(expected_args, actual_args) + + actual_res = tfm.build(INPUT_FILE, OUTPUT_FILE) + expected_res = True + self.assertEqual(expected_res, actual_res) + + def test_gain_in_invalid(self): + tfm = new_transformer() + with self.assertRaises(ValueError): + tfm.phaser(gain_in=0) + + def test_gain_out_valid(self): + tfm = new_transformer() + tfm.phaser(gain_out=1.0) + + actual_args = tfm.effects + expected_args = ['phaser', '0.8', '1.0', '3', '0.4', '0.5', '-s'] + self.assertEqual(expected_args, actual_args) + + actual_res = tfm.build(INPUT_FILE, OUTPUT_FILE) + expected_res = True + self.assertEqual(expected_res, actual_res) + + def test_gain_out_invalid(self): + tfm = new_transformer() + with self.assertRaises(ValueError): + tfm.phaser(gain_out=1.1) + + def test_delay_valid(self): + tfm = new_transformer() + tfm.phaser(delay=5) + + actual_args = tfm.effects + expected_args = ['phaser', '0.8', '0.74', '5', '0.4', '0.5', '-s'] + self.assertEqual(expected_args, actual_args) + + actual_res = tfm.build(INPUT_FILE, OUTPUT_FILE) + expected_res = True + self.assertEqual(expected_res, actual_res) + + def test_delay_invalid(self): + tfm = new_transformer() + with self.assertRaises(ValueError): + tfm.phaser(delay=None) + + def test_decay_valid(self): + tfm = new_transformer() + tfm.phaser(decay=0.1) + + actual_args = tfm.effects + expected_args = ['phaser', '0.8', '0.74', '3', '0.1', '0.5', '-s'] + self.assertEqual(expected_args, actual_args) + + actual_res = tfm.build(INPUT_FILE, OUTPUT_FILE) + expected_res = True + self.assertEqual(expected_res, actual_res) + + def test_decay_invalid(self): + tfm = new_transformer() + with self.assertRaises(ValueError): + tfm.phaser(decay=0.0) + + def test_speed_valid(self): + tfm = new_transformer() + tfm.phaser(speed=2) + + actual_args = tfm.effects + expected_args = ['phaser', '0.8', '0.74', '3', '0.4', '2', '-s'] + self.assertEqual(expected_args, actual_args) + + actual_res = tfm.build(INPUT_FILE, OUTPUT_FILE) + expected_res = True + self.assertEqual(expected_res, actual_res) + + def test_speed_invalid(self): + tfm = new_transformer() + with self.assertRaises(ValueError): + tfm.phaser(speed=-1) + + def test_modulation_shape_valid(self): + tfm = new_transformer() + tfm.phaser(modulation_shape='triangular') + + actual_args = tfm.effects + expected_args = ['phaser', '0.8', '0.74', '3', '0.4', '0.5', '-t'] + self.assertEqual(expected_args, actual_args) + + actual_res = tfm.build(INPUT_FILE, OUTPUT_FILE) + expected_res = True + self.assertEqual(expected_res, actual_res) + + def test_modulation_shape_invalid(self): + tfm = new_transformer() + with self.assertRaises(ValueError): + tfm.phaser(modulation_shape='square') + + class TestTransformerPitch(unittest.TestCase): def test_default(self): @@ -2692,6 +2813,18 @@ def test_factor_valid(self): expected_res = True self.assertEqual(expected_res, actual_res) + def test_factor_valid_extreme(self): + tfm = new_transformer() + tfm.speed(2.5) + + actual_args = tfm.effects + expected_args = ['speed', '2.5'] + self.assertEqual(expected_args, actual_args) + + actual_res = tfm.build(INPUT_FILE, OUTPUT_FILE) + expected_res = True + self.assertEqual(expected_res, actual_res) + def test_factor_invalid(self): tfm = new_transformer() with self.assertRaises(ValueError):