diff --git a/sox/combine.py b/sox/combine.py index 815c7b6..63062b7 100644 --- a/sox/combine.py +++ b/sox/combine.py @@ -11,8 +11,12 @@ from . import file_info from . import core +from .core import ENCODING_VALS +from .core import is_number from .core import sox from .core import SoxError +from .core import SoxiError +from .core import VALID_FORMATS from .transform import Transformer @@ -71,10 +75,13 @@ def build(self, input_filepath_list, output_filepath, combine_type, _validate_volumes(input_volumes) input_format_list = _build_input_format_list( - input_filepath_list, input_volumes + input_filepath_list, input_volumes, self.input_format ) - _validate_file_formats(input_filepath_list, combine_type) + try: + _validate_file_formats(input_filepath_list, combine_type) + except SoxiError: + logging.warning("unable to validate file formats.") args = [] args.extend(self.globals) @@ -104,6 +111,165 @@ def build(self, input_filepath_list, output_filepath, combine_type, logging.info("[SoX] {}".format(out)) return True + def set_input_format(self, file_type=None, rate=None, bits=None, + channels=None, encoding=None, ignore_length=None): + '''Sets input file format arguments. This is primarily useful when + dealing with audio files without a file extension. Overwrites any + previously set input file arguments. + + If this function is not explicity called the input format is inferred + from the file extension or the file's header. + + Parameters + ---------- + file_type : list of str or None, default=None + The file type of the input audio file. Should be the same as what + the file extension would be, for ex. 'mp3' or 'wav'. + rate : list of float or None, default=None + The sample rate of the input audio file. If None the sample rate + is inferred. + bits : list of int or None, default=None + The number of bits per sample. If None, the number of bits per + sample is inferred. + channels : list of int or None, default=None + The number of channels in the audio file. If None the number of + channels is inferred. + encoding : list of str or None, default=None + The audio encoding type. Sometimes needed with file-types that + support more than one encoding type. One of: + * signed-integer : PCM data stored as signed (‘two’s + complement’) integers. Commonly used with a 16 or 24−bit + encoding size. A value of 0 represents minimum signal + power. + * unsigned-integer : PCM data stored as unsigned integers. + Commonly used with an 8-bit encoding size. A value of 0 + represents maximum signal power. + * floating-point : PCM data stored as IEEE 753 single precision + (32-bit) or double precision (64-bit) floating-point + (‘real’) numbers. A value of 0 represents minimum signal + power. + * a-law : International telephony standard for logarithmic + encoding to 8 bits per sample. It has a precision + equivalent to roughly 13-bit PCM and is sometimes encoded + with reversed bit-ordering. + * u-law : North American telephony standard for logarithmic + encoding to 8 bits per sample. A.k.a. μ-law. It has a + precision equivalent to roughly 14-bit PCM and is sometimes + encoded with reversed bit-ordering. + * oki-adpcm : OKI (a.k.a. VOX, Dialogic, or Intel) 4-bit ADPCM; + it has a precision equivalent to roughly 12-bit PCM. ADPCM + is a form of audio compression that has a good compromise + between audio quality and encoding/decoding speed. + * ima-adpcm : IMA (a.k.a. DVI) 4-bit ADPCM; it has a precision + equivalent to roughly 13-bit PCM. + * ms-adpcm : Microsoft 4-bit ADPCM; it has a precision + equivalent to roughly 14-bit PCM. + * gsm-full-rate : GSM is currently used for the vast majority + of the world’s digital wireless telephone calls. It + utilises several audio formats with different bit-rates and + associated speech quality. SoX has support for GSM’s + original 13kbps ‘Full Rate’ audio format. It is usually + CPU-intensive to work with GSM audio. + ignore_length : list of bool or None, default=None + If True, overrides an (incorrect) audio length given in an audio + file’s header. If this option is given then SoX will keep reading + audio until it reaches the end of the input file. + + ''' + if file_type is not None and not isinstance(file_type, list): + raise ValueError("file_type must be a list or None.") + + if file_type is not None: + if not all([f in VALID_FORMATS for f in file_type]): + raise ValueError( + 'file_type elements ' + 'must be one of {}'.format(VALID_FORMATS) + ) + else: + file_type = [] + + if rate is not None and not isinstance(rate, list): + raise ValueError("rate must be a list or None.") + + if rate is not None: + if not all([is_number(r) and r > 0 for r in rate]): + raise ValueError('rate elements must be positive floats.') + else: + rate = [] + + if bits is not None and not isinstance(bits, list): + raise ValueError("bits must be a list or None.") + + if bits is not None: + if not all([isinstance(b, int) and b > 0 for b in bits]): + raise ValueError('bit elements must be positive ints.') + else: + bits = [] + + if channels is not None and not isinstance(channels, list): + raise ValueError("channels must be a list or None.") + + if channels is not None: + if not all([isinstance(c, int) and c > 0 for c in channels]): + raise ValueError('channel elements must be positive ints.') + else: + channels = [] + + if encoding is not None and not isinstance(encoding, list): + raise ValueError("encoding must be a list or None.") + + if encoding is not None: + if not all([e in ENCODING_VALS for e in encoding]): + raise ValueError( + 'elements of encoding must ' + 'be one of {}'.format(ENCODING_VALS) + ) + else: + encoding = [] + + if ignore_length is not None and not isinstance(ignore_length, list): + raise ValueError("ignore_length must be a list or None.") + + if ignore_length is not None: + if not all([isinstance(l, bool) for l in ignore_length]): + raise ValueError("ignore_length elements must be booleans.") + else: + ignore_length = [] + + max_input_arg_len = max([ + len(file_type), len(rate), len(bits), len(channels), + len(encoding), len(ignore_length) + ]) + + input_format = [] + for _ in range(max_input_arg_len): + input_format.append([]) + + for i, f in enumerate(file_type): + input_format[i].extend(['-t', '{}'.format(f)]) + + for i, r in enumerate(rate): + input_format[i].extend(['-r', '{}'.format(r)]) + + for i, b in enumerate(bits): + input_format[i].extend(['-b', '{}'.format(b)]) + + for i, c in enumerate(channels): + input_format[i].extend(['-c', '{}'.format(c)]) + + for i, e in enumerate(encoding): + input_format[i].extend(['-e', '{}'.format(e)]) + + for i, l in enumerate(ignore_length): + if l is True: + input_format[i].append('--ignore-length') + + self.input_format = input_format + return self + + def splice(self): + raise NotImplementedError + def _validate_file_formats(input_filepath_list, combine_type): '''Validate that combine method can be performed with given files. @@ -144,19 +310,30 @@ def _validate_num_channels(input_filepath_list, combine_type): ) -def _build_input_format_list(input_filepath_list, input_volumes): +def _build_input_format_list(input_filepath_list, input_volumes=None, + input_format=None): '''Set input formats given input_volumes. Parameters ---------- + input_filepath_list : list of str + List of input files input_volumes : list of float, default=None List of volumes to be applied upon combining input files. Volumes are applied to the input files in order. If None, input files will be combined at their original volumes. + input_format : list of lists, default=None + List of input formats to be applied to each input file. Formatting + arguments are applied to the input files in order. + If None, the input formats will be inferred from the file header. ''' n_inputs = len(input_filepath_list) input_format_list = [] + for _ in range(n_inputs): + input_format_list.append([]) + + # Adjust length of input_volumes list if input_volumes is None: vols = [1] * n_inputs else: @@ -178,8 +355,32 @@ def _build_input_format_list(input_filepath_list, input_volumes): else: vols = [v for v in input_volumes] - for vol in vols: - input_format_list.append(['-v', '{}'.format(vol)]) + # Adjust length of input_format list + if input_format is None: + fmts = [[] for _ in range(n_inputs)] + else: + n_fmts = len(input_format) + if n_fmts < n_inputs: + logging.warning( + 'Input formats were only specified for %s out of %s files.' + 'The last %s files will remain unformatted.', + n_fmts, n_inputs, n_inputs - n_fmts + ) + fmts = [f for f in input_format] + fmts.extend([[] for _ in range(n_inputs - n_fmts)]) + elif n_fmts > n_inputs: + logging.warning( + '%s Input formats were specified but only %s input files exist' + '. The last %s formats will be ignored.', + n_fmts, n_inputs, n_fmts - n_inputs + ) + fmts = input_format[:n_inputs] + else: + fmts = [f for f in input_format] + + for i, (vol, fmt) in enumerate(zip(vols, fmts)): + input_format_list[i].extend(['-v', '{}'.format(vol)]) + input_format_list[i].extend(fmt) return input_format_list diff --git a/sox/core.py b/sox/core.py index 3bed356..90560af 100644 --- a/sox/core.py +++ b/sox/core.py @@ -7,6 +7,11 @@ SOXI_ARGS = ['b', 'c', 'a', 'D', 'e', 't', 's', 'r'] +ENCODING_VALS = [ + 'signed-integer', 'unsigned-integer', 'floating-point', 'a-law', 'u-law', + 'oki-adpcm', 'ima-adpcm', 'ms-adpcm', 'gsm-full-rate' +] + def enquote_filepath(fpath): """Wrap a filepath in double-quotes to protect difficult characters. diff --git a/sox/file_info.py b/sox/file_info.py index 9b0b8c1..11e647b 100644 --- a/sox/file_info.py +++ b/sox/file_info.py @@ -217,7 +217,7 @@ def validate_input_file(input_filepath): ext = file_extension(input_filepath) if ext not in VALID_FORMATS: logging.info("Valid formats: %s", " ".join(VALID_FORMATS)) - raise SoxError( + logging.warning( "This install of SoX cannot process .{} files.".format(ext) ) @@ -270,7 +270,7 @@ def validate_output_file(output_filepath): ext = file_extension(output_filepath) if ext not in VALID_FORMATS: logging.info("Valid formats: %s", " ".join(VALID_FORMATS)) - raise SoxError( + logging.warning( "This install of SoX cannot process .{} files.".format(ext) ) diff --git a/sox/transform.py b/sox/transform.py index 9d12b93..60ffea2 100644 --- a/sox/transform.py +++ b/sox/transform.py @@ -9,6 +9,7 @@ import logging import random +from .core import ENCODING_VALS from .core import is_number from .core import play from .core import sox @@ -20,10 +21,6 @@ logging.basicConfig(level=logging.DEBUG) VERBOSITY_VALS = [0, 1, 2, 3, 4] -ENCODING_VALS = [ - 'signed-integer', 'unsigned-integer', 'floating-point', 'a-law', 'u-law', - 'oki-adpcm', 'ima-adpcm', 'ms-adpcm', 'gsm-full-rate' -] class Transformer(object): @@ -1217,7 +1214,6 @@ def flanger(self, delay=0, depth=2, regen=0, width=71, speed=0.5, self.effects_log.append('flanger') return self - raise NotImplementedError def gain(self, gain_db=0.0, normalize=True, limiter=False, balance=None): '''Apply amplification or attenuation to the audio signal. @@ -1810,10 +1806,27 @@ def silence(self, location=0, silence_threshold=0.1, def sinc(self): raise NotImplementedError - def speed(self): - raise NotImplementedError + def speed(self, factor): + '''Adjust the audio speed (pitch and tempo together). + Technically, the speed effect only changes the sample rate information, + leaving the samples themselves untouched. The rate effect is invoked + automatically to resample to the output sample rate, using its default + quality/speed. For higher quality or higher speed resampling, in + addition to the speed effect, specify the rate effect with the desired + quality option. + + Parameters + ---------- + factor : float + The ratio of the new tempo to the old tempo. + For ex. 1.1 speeds up the tempo by 10%; 0.9 slows it down by 10%. + Note - this argument is the inverse of what is passed to the sox + stretch effect for consistency with tempo. - def splice(self): + See Also + -------- + tempo, pitch, rate + ''' raise NotImplementedError def swap(self): @@ -1833,8 +1846,55 @@ def swap(self): return self - def stretch(self): - raise NotImplementedError + def stretch(self, factor, window=20): + '''Change the audio duration (but not its pitch). + **Unless factor is close to 1, use the tempo effect instead.** + + This effect is broadly equivalent to the tempo effect with search set + to zero, so in general, its results are comparatively poor; it is + retained as it can sometimes out-perform tempo for small factors. + + Parameters + ---------- + factor : float + The ratio of the new tempo to the old tempo. + For ex. 1.1 speeds up the tempo by 10%; 0.9 slows it down by 10%. + Note - this argument is the inverse of what is passed to the sox + stretch effect for consistency with tempo. + window : float, default=20 + Window size in miliseconds + + See Also + -------- + tempo, speed, pitch + + ''' + if not is_number(factor) or factor <= 0: + raise ValueError("factor must be a positive number") + + if factor < 0.5 or factor > 2: + logging.warning( + "Using an extreme time stretching factor. " + "Quality of results will be poor" + ) + + if abs(factor - 1.0) > 0.1: + logging.warning( + "For this stretch factor, " + "the tempo effect has better performance." + ) + + if not is_number(window) or window <= 0: + raise ValueError( + "window must be a positive number." + ) + + effect_args = ['stretch', '{}'.format(factor), '{}'.format(window)] + + self.effects.extend(effect_args) + self.effects_log.append('stretch') + + return self def tempo(self, factor, audio_type=None, quick=False): '''Time stretch audio without changing pitch. @@ -1871,6 +1931,12 @@ def tempo(self, factor, audio_type=None, quick=False): "Quality of results will be poor" ) + if abs(factor - 1.0) <= 0.1: + logging.warning( + "For this stretch factor, " + "the stretch effect has better performance." + ) + if audio_type not in [None, 'm', 's', 'l']: raise ValueError( "audio_type must be one of None, 'm', 's', or 'l'." diff --git a/tests/test_combine.py b/tests/test_combine.py index 4372f02..36985bf 100644 --- a/tests/test_combine.py +++ b/tests/test_combine.py @@ -52,6 +52,14 @@ def test_build(self): ) self.assertEqual(expected_result, actual_result) + def test_build_with_vols(self): + expected_result = True + actual_result = self.cbn.build( + [INPUT_WAV, INPUT_WAV], OUTPUT_FILE, 'mix', + input_volumes=[0.5, 2] + ) + self.assertEqual(expected_result, actual_result) + def test_failed_build(self): cbn = new_combiner() with self.assertRaises(SoxError): @@ -97,6 +105,187 @@ def test_multiply(self): self.assertEqual(expected, actual) +class TestSetInputFormat(unittest.TestCase): + + def test_none(self): + cbn = new_combiner() + cbn.set_input_format() + expected = [] + actual = cbn.input_format + self.assertEqual(expected, actual) + + def test_file_type(self): + cbn = new_combiner() + cbn.set_input_format(file_type=['wav', 'aiff']) + expected = [['-t', 'wav'], ['-t', 'aiff']] + actual = cbn.input_format + self.assertEqual(expected, actual) + + def test_invalid_file_type(self): + cbn = new_combiner() + with self.assertRaises(ValueError): + cbn.set_input_format(file_type='wav') + + def test_invalid_file_type_val(self): + cbn = new_combiner() + with self.assertRaises(ValueError): + cbn.set_input_format(file_type=['xyz', 'wav']) + + def test_rate(self): + cbn = new_combiner() + cbn.set_input_format(rate=[2000, 44100, 22050]) + expected = [['-r', '2000'], ['-r', '44100'], ['-r', '22050']] + actual = cbn.input_format + self.assertEqual(expected, actual) + + def test_invalid_rate(self): + cbn = new_combiner() + with self.assertRaises(ValueError): + cbn.set_input_format(rate=2000) + + def test_invalid_rate_val(self): + cbn = new_combiner() + with self.assertRaises(ValueError): + cbn.set_input_format(rate=[-2, 'a']) + + def test_bits(self): + cbn = new_combiner() + cbn.set_input_format(bits=[16]) + expected = [['-b', '16']] + actual = cbn.input_format + self.assertEqual(expected, actual) + + def test_invalid_bits(self): + cbn = new_combiner() + with self.assertRaises(ValueError): + cbn.set_input_format(bits=32) + + def test_invalid_bits_val(self): + cbn = new_combiner() + with self.assertRaises(ValueError): + cbn.set_input_format(bits=[0]) + + def test_channels(self): + cbn = new_combiner() + cbn.set_input_format(channels=[1, 2, 3]) + expected = [['-c', '1'], ['-c', '2'], ['-c', '3']] + actual = cbn.input_format + self.assertEqual(expected, actual) + + def test_invalid_channels(self): + cbn = new_combiner() + with self.assertRaises(ValueError): + cbn.set_input_format(channels='x') + + def test_invalid_channels_val(self): + cbn = new_combiner() + with self.assertRaises(ValueError): + cbn.set_input_format(channels=[1.5, 2, 3]) + + def test_encoding(self): + cbn = new_combiner() + cbn.set_input_format(encoding=['floating-point', 'oki-adpcm']) + expected = [['-e', 'floating-point'], ['-e', 'oki-adpcm']] + actual = cbn.input_format + self.assertEqual(expected, actual) + + def test_invalid_encoding(self): + cbn = new_combiner() + with self.assertRaises(ValueError): + cbn.set_input_format(encoding='wav') + + def test_invalid_encoding_val(self): + cbn = new_combiner() + with self.assertRaises(ValueError): + cbn.set_input_format(encoding=['xyz', 'wav']) + + def test_ignore_length(self): + cbn = new_combiner() + cbn.set_input_format(ignore_length=[True, False, True]) + expected = [['--ignore-length'], [], ['--ignore-length']] + actual = cbn.input_format + self.assertEqual(expected, actual) + + def test_invalid_ignore_length(self): + cbn = new_combiner() + with self.assertRaises(ValueError): + cbn.set_input_format(ignore_length=1) + + def test_invalid_ignore_length_val(self): + cbn = new_combiner() + with self.assertRaises(ValueError): + cbn.set_input_format(ignore_length=[False, True, 3]) + + def test_multiple_same_len(self): + cbn = new_combiner() + cbn.set_input_format(rate=[44100, 2000], bits=[32, 8]) + expected = [['-r', '44100', '-b', '32'], ['-r', '2000', '-b', '8']] + actual = cbn.input_format + self.assertEqual(expected, actual) + + def test_multiple_different_len(self): + cbn = new_combiner() + cbn.set_input_format(rate=[44100, 2000], bits=[32, 8, 16]) + expected = [ + ['-r', '44100', '-b', '32'], + ['-r', '2000', '-b', '8'], + ['-b', '16'] + ] + actual = cbn.input_format + self.assertEqual(expected, actual) + + def test_build_same_len(self): + cbn = new_combiner() + cbn.set_input_format(rate=[44100, 44100], channels=[1, 1]) + actual = cbn.build([INPUT_WAV, INPUT_WAV], OUTPUT_FILE, 'mix') + expected = True + self.assertEqual(expected, actual) + + def test_build_same_len_vol(self): + cbn = new_combiner() + cbn.set_input_format(rate=[44100, 44100], channels=[1, 1]) + actual = cbn.build( + [INPUT_WAV, INPUT_WAV], OUTPUT_FILE, 'mix', input_volumes=[1, 2] + ) + expected = True + self.assertEqual(expected, actual) + + def test_build_greater_len(self): + cbn = new_combiner() + cbn.set_input_format(rate=[44100, 44100, 44100], channels=[1, 1]) + actual = cbn.build([INPUT_WAV, INPUT_WAV], OUTPUT_FILE, 'mix') + expected = True + self.assertEqual(expected, actual) + + def test_build_greater_len_vol(self): + cbn = new_combiner() + cbn.set_input_format(rate=[44100, 44100, 44100], channels=[1, 1]) + actual = cbn.build( + [INPUT_WAV, INPUT_WAV], OUTPUT_FILE, 'mix', input_volumes=[1, 2] + ) + expected = True + self.assertEqual(expected, actual) + + def test_build_lesser_len(self): + cbn = new_combiner() + cbn.set_input_format(rate=[44100, 44100], channels=[1, 1]) + actual = cbn.build( + [INPUT_WAV, INPUT_WAV, INPUT_WAV], OUTPUT_FILE, 'mix' + ) + expected = True + self.assertEqual(expected, actual) + + def test_build_lesser_len_vol(self): + cbn = new_combiner() + cbn.set_input_format(rate=[44100, 44100], channels=[1, 1]) + actual = cbn.build( + [INPUT_WAV, INPUT_WAV, INPUT_WAV], OUTPUT_FILE, 'mix', + input_volumes=[1, 2] + ) + expected = True + self.assertEqual(expected, actual) + + class TestValidateFileFormats(unittest.TestCase): def test_different_samplerates(self): @@ -139,31 +328,56 @@ class TestBuildInputFormatList(unittest.TestCase): def test_none(self): expected = [['-v', '1'], ['-v', '1']] actual = combine._build_input_format_list( - [INPUT_WAV, INPUT_WAV], None + [INPUT_WAV, INPUT_WAV], None, None ) self.assertEqual(expected, actual) - def test_equal_num(self): + def test_equal_num_vol(self): expected = [['-v', '0.5'], ['-v', '1.1']] actual = combine._build_input_format_list( - [INPUT_WAV, INPUT_WAV], [0.5, 1.1] + [INPUT_WAV, INPUT_WAV], [0.5, 1.1], None ) self.assertEqual(expected, actual) - def test_greater_num(self): + def test_greater_num_vol(self): actual = combine._build_input_format_list( - [INPUT_WAV, INPUT_WAV], [0.5, 1.1, 3] + [INPUT_WAV, INPUT_WAV], [0.5, 1.1, 3], None ) expected = [['-v', '0.5'], ['-v', '1.1']] self.assertEqual(expected, actual) - def test_lesser_num(self): + def test_lesser_num_vol(self): actual = combine._build_input_format_list( - [INPUT_WAV, INPUT_WAV, INPUT_WAV], [0.5, 1.1] + [INPUT_WAV, INPUT_WAV, INPUT_WAV], [0.5, 1.1], None ) expected = [['-v', '0.5'], ['-v', '1.1'], ['-v', '1']] self.assertEqual(expected, actual) + def test_equal_num_fmt(self): + expected = [['-v', '1', '-t', 'wav'], ['-v', '1', '-t', 'aiff']] + actual = combine._build_input_format_list( + [INPUT_WAV, INPUT_WAV], None, [['-t', 'wav'], ['-t', 'aiff']] + ) + self.assertEqual(expected, actual) + + def test_greater_num_fmt(self): + actual = combine._build_input_format_list( + [INPUT_WAV, INPUT_WAV], None, + [['-t', 'wav'], ['-t', 'aiff'], ['-t', 'wav']] + ) + expected = [['-v', '1', '-t', 'wav'], ['-v', '1', '-t', 'aiff']] + self.assertEqual(expected, actual) + + def test_lesser_num_fmt(self): + actual = combine._build_input_format_list( + [INPUT_WAV, INPUT_WAV, INPUT_WAV], None, + [['-t', 'wav'], ['-t', 'aiff']] + ) + expected = [ + ['-v', '1', '-t', 'wav'], ['-v', '1', '-t', 'aiff'], ['-v', '1'] + ] + self.assertEqual(expected, actual) + class TestBuildInputArgs(unittest.TestCase): diff --git a/tests/test_file_info.py b/tests/test_file_info.py index 405fc4a..cca028f 100644 --- a/tests/test_file_info.py +++ b/tests/test_file_info.py @@ -224,7 +224,7 @@ def test_dictionary(self): 'duration': 10.0, 'num_samples': 441000, 'encoding': 'Signed Integer PCM', - 'silent': False + 'silent': False } self.assertEqual(expected, actual) @@ -246,8 +246,9 @@ def test_nonexistent(self): file_info.validate_input_file('data/asdfasdfasdf.wav') def test_invalid_format(self): - with self.assertRaises(SoxError): - file_info.validate_input_file(INPUT_FILE_INVALID) + actual = file_info.validate_input_file(INPUT_FILE_INVALID) + expected = None + self.assertEqual(expected, actual) class TestValidateInputFileList(unittest.TestCase): @@ -276,10 +277,11 @@ def test_nonexistent(self): ) def test_invalid_format(self): - with self.assertRaises(SoxError): - file_info.validate_input_file_list( - [INPUT_FILE_INVALID, INPUT_FILE] - ) + actual = file_info.validate_input_file_list( + [INPUT_FILE_INVALID, INPUT_FILE] + ) + expected = None + self.assertEqual(expected, actual) class TestValidateOutputFile(unittest.TestCase): @@ -294,11 +296,9 @@ def test_not_writeable(self): file_info.validate_output_file('notafolder/output.wav') def test_invalid_format(self): - with self.assertRaises(SoxError): - file_info.validate_output_file('output.xyz') - - with self.assertRaises(SoxError): - file_info.validate_output_file('./output.xyz') + actual = file_info.validate_output_file('output.xyz') + expected = None + self.assertEqual(expected, actual) def test_file_exists(self): actual = file_info.validate_output_file(INPUT_FILE) diff --git a/tests/test_transform.py b/tests/test_transform.py index 8edf3ad..bf5b480 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -386,6 +386,11 @@ def test_invalid(self): with self.assertRaises(IOError): self.tfm.build('blah/asdf.wav', OUTPUT_FILE) + def test_failed_sox(self): + self.tfm.effects = ['channels', '-1'] + with self.assertRaises(SoxError): + self.tfm.build(INPUT_FILE, OUTPUT_FILE) + class TestTransformerPreview(unittest.TestCase): def setUp(self): @@ -2349,6 +2354,42 @@ def test_buffer_around_silence_invalid(self): tfm.silence(buffer_around_silence=0) +class TestTransformerSpeed(unittest.TestCase): + + def test_default(self): + tfm = new_transformer() + tfm.speed(1.5) + + actual_args = tfm.effects + expected_args = ['speed', '1.5'] + self.assertEqual(expected_args, actual_args) + + actual_log = tfm.effects_log + expected_log = ['speed'] + 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_factor_valid(self): + tfm = new_transformer() + tfm.speed(0.7) + + actual_args = tfm.effects + expected_args = ['speed', '0.7'] + 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): + tfm.speed(-1) + + class TestTransformerSwap(unittest.TestCase): def test_default(self): @@ -2368,6 +2409,59 @@ def test_default(self): self.assertEqual(expected_res, actual_res) +class TestTransformerStretch(unittest.TestCase): + + def test_default(self): + tfm = new_transformer() + tfm.stretch(1.1) + + actual_args = tfm.effects + expected_args = ['stretch', '1.1', '20'] + self.assertEqual(expected_args, actual_args) + + actual_log = tfm.effects_log + expected_log = ['stretch'] + 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_factor_valid(self): + tfm = new_transformer() + tfm.stretch(0.7) + + actual_args = tfm.effects + expected_args = ['stretch', '0.7', '20'] + 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): + tfm.stretch(-1) + + def test_window_valid(self): + tfm = new_transformer() + tfm.stretch(0.99, window=10) + + actual_args = tfm.effects + expected_args = ['stretch', '0.99', '10'] + 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_window_invalid(self): + tfm = new_transformer() + with self.assertRaises(ValueError): + tfm.stretch(0.99, window=0) + + class TestTransformerTempo(unittest.TestCase): def test_default(self):