Skip to content

Commit

Permalink
feat: Add support for Sox's compression(bitrate) argument
Browse files Browse the repository at this point in the history
Adds support for bitrate as parameter to output arguments. It is useful converting files to MP3 format. Requires a new version of soundfile(0.11.0) that supports MP3 formats alongside upgraded libsndfile version to 1.1.0. This is for the `test_bitrate_valid` test.
  • Loading branch information
adinhodovic committed Oct 17, 2022
1 parent 0a428b8 commit 9e4b981
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 4 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
python -m pip install --upgrade pip
python -m pip install flake8 pytest pytest-cov
python -m pip install coveralls
python -m pip install pysoundfile
python -m pip install soundfile
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
python -m pip install -e .
mkdir ${{ github.workspace }}/coverage
Expand All @@ -46,7 +46,7 @@ jobs:
- name: Test with pytest
run: |
pytest tests/
- name: Run Coveralls
run: |
coverage run -m pytest tests/ > ${{ github.workspace }}/coverage/lcov.info
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
'pytest',
'pytest-cov',
'pytest-pep8',
'pysoundfile >= 0.9.0',
'soundfile >= 0.11.0',
],
'docs': [
'sphinx==1.2.3', # autodoc was broken in 1.3.1
Expand Down
22 changes: 21 additions & 1 deletion sox/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ def _validate_output_format(self, output_format):
bits = output_format.get('bits')
channels = output_format.get('channels')
encoding = output_format.get('encoding')
bitrate = output_format.get('bitrate')
comments = output_format.get('comments')
append_comments = output_format.get('append_comments', True)

Expand All @@ -343,6 +344,9 @@ def _validate_output_format(self, output_format):
if channels is not None and channels <= 0:
raise ValueError('channels must be a positive number')

if not isinstance(bitrate, float) and bitrate is not None:
raise ValueError('bitrate must be an float or None')

if encoding not in ENCODING_VALS + [None]:
raise ValueError(
'Invalid encoding. Must be one of {}'.format(ENCODING_VALS)
Expand All @@ -364,6 +368,7 @@ def _output_format_args(self, output_format):
bits = output_format.get('bits')
channels = output_format.get('channels')
encoding = output_format.get('encoding')
bitrate = output_format.get('bitrate')
comments = output_format.get('comments')
append_comments = output_format.get('append_comments', True)

Expand All @@ -384,6 +389,9 @@ def _output_format_args(self, output_format):
if encoding is not None:
output_format_args.extend(['-e', '{}'.format(encoding)])

if bitrate is not None:
output_format_args.extend(['-C', '{}'.format(bitrate)])

if comments is not None:
if append_comments:
output_format_args.extend(['--add-comment', comments])
Expand All @@ -398,6 +406,7 @@ def set_output_format(self,
bits: Optional[int] = None,
channels: Optional[int] = None,
encoding: Optional[EncodingValue] = None,
bitrate: Optional[float] = None,
comments: Optional[str] = None,
append_comments: bool = True):
'''Sets output file format arguments. These arguments will overwrite
Expand Down Expand Up @@ -456,6 +465,8 @@ def set_output_format(self,
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.
bitrate : float, default=None
Desired bitrate. Uses Sox's -C (compression) argument.
comments : str or None, default=None
If not None, the string is added as a comment in the header of the
output audio file. If None, no comments are added.
Expand All @@ -469,6 +480,7 @@ def set_output_format(self,
'bits': bits,
'channels': channels,
'encoding': encoding,
'bitrate': bitrate,
'comments': comments,
'append_comments': append_comments
}
Expand Down Expand Up @@ -1479,7 +1491,8 @@ def contrast(self, amount=75):
def convert(self,
samplerate: Optional[float] = None,
n_channels: Optional[int] = None,
bitdepth: Optional[int] = None):
bitdepth: Optional[int] = None,
bitrate: Optional[float] = None):
'''Converts output audio to the specified format.
Parameters
Expand All @@ -1490,6 +1503,8 @@ def convert(self,
Desired number of channels. If None, defaults to the same as input.
bitdepth : int, default=None
Desired bitdepth. If None, defaults to the same as input.
bitrate : float, default=None
Desired bitrate. Uses Sox's -C (compression) argument.
See Also
--------
Expand All @@ -1513,6 +1528,11 @@ def convert(self,
if not is_number(samplerate) or samplerate <= 0:
raise ValueError("samplerate must be a positive number.")
self.rate(samplerate)
if bitrate is not None:
if not isinstance(bitrate, float) or bitrate <= 0:
raise ValueError("bitrate must be a positive float.")
self.output_format["bitrate"] = bitrate

return self

def dcshift(self, shift: float = 0.0):
Expand Down
Binary file added tests/data/output.mp3
Binary file not shown.
60 changes: 60 additions & 0 deletions tests/test_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def relpath(f):
INPUT_FILE4 = relpath('data/input4.wav')
OUTPUT_FILE = relpath('data/output.wav')
OUTPUT_FILE_ALT = relpath('data/output_alt.wav')
OUTPUT_FILE_MP3 = relpath('data/output.mp3')
NOISE_PROF_FILE = relpath('data/noise.prof')


Expand Down Expand Up @@ -371,6 +372,35 @@ def test_encoding_invalid(self):
self.tfm.set_input_format(encoding='16-bit-signed-integer')
with self.assertRaises(ValueError):
self.tfm._input_format_args({'encoding': '16-bit-signed-integer'})
def test_bitrate(self):
self.tfm.set_output_format(bitrate=320.0)
actual = self.tfm.output_format
expected = {
'file_type': None,
'rate': None,
'bits': None,
'channels': None,
'encoding': None,
'bitrate': 320.0,
'comments': None,
'append_comments': True
}
self.assertEqual(expected, actual)

actual_args = self.tfm._output_format_args(self.tfm.output_format)
expected_args = ['-C', '320.0']
self.assertEqual(expected_args, actual_args)

actual_result = self.tfm.build(INPUT_FILE, OUTPUT_FILE)
expected_result = True
self.assertEqual(expected_result, actual_result)

def test_bitrate_invalid(self):
with self.assertRaises(ValueError):
self.tfm.set_output_format(bitrate='320.0')
with self.assertRaises(ValueError):
self.tfm._output_format_args({'bitrate': 320})


def test_ignore_length(self):
self.tfm.set_input_format(ignore_length=True)
Expand Down Expand Up @@ -427,6 +457,7 @@ def test_file_type(self):
'bits': None,
'channels': None,
'encoding': None,
'bitrate': None,
'comments': None,
'append_comments': True
}
Expand All @@ -449,6 +480,7 @@ def test_file_type_null_output(self):
'bits': None,
'channels': None,
'encoding': None,
'bitrate': None,
'comments': None,
'append_comments': True
}
Expand Down Expand Up @@ -477,6 +509,7 @@ def test_rate(self):
'bits': None,
'channels': None,
'encoding': None,
'bitrate': None,
'comments': None,
'append_comments': True
}
Expand All @@ -499,6 +532,7 @@ def test_rate_scinotation(self):
'bits': None,
'channels': None,
'encoding': None,
'bitrate': None,
'comments': None,
'append_comments': True
}
Expand Down Expand Up @@ -533,6 +567,7 @@ def test_bits(self):
'bits': 32,
'channels': None,
'encoding': None,
'bitrate': None,
'comments': None,
'append_comments': True
}
Expand Down Expand Up @@ -567,6 +602,7 @@ def test_channels(self):
'bits': None,
'channels': 2,
'encoding': None,
'bitrate': None,
'comments': None,
'append_comments': True
}
Expand Down Expand Up @@ -601,6 +637,7 @@ def test_encoding(self):
'bits': None,
'channels': None,
'encoding': 'signed-integer',
'bitrate': None,
'comments': None,
'append_comments': True
}
Expand Down Expand Up @@ -629,6 +666,7 @@ def test_comments(self):
'bits': None,
'channels': None,
'encoding': None,
'bitrate': None,
'comments': 'asdf',
'append_comments': True
}
Expand Down Expand Up @@ -657,6 +695,7 @@ def test_append_comments(self):
'bits': None,
'channels': None,
'encoding': None,
'bitrate': None,
'comments': 'asdf',
'append_comments': False
}
Expand Down Expand Up @@ -1748,6 +1787,27 @@ def test_bitdepth_invalid(self):
with self.assertRaises(ValueError):
tfm.convert(bitdepth=17)

def test_bitrate_valid(self):
tfm = new_transformer()
tfm.convert(bitrate=320.0)

actual = tfm.output_format
expected = {'bitrate': 320.0}
self.assertEqual(expected, actual)

actual_res = tfm.build(INPUT_FILE, OUTPUT_FILE_MP3)
expected_res = True
self.assertEqual(expected_res, actual_res)

tfm.set_output_format(file_type='mp3', bitrate=320.0)
tfm_assert_array_to_file_output(
INPUT_FILE, OUTPUT_FILE_MP3, tfm, skip_array_tests=True)

def test_bitrate_invalid(self):
tfm = new_transformer()
with self.assertRaises(ValueError):
tfm.convert(bitrate=0)


class TestTransformerDcshift(unittest.TestCase):

Expand Down

0 comments on commit 9e4b981

Please sign in to comment.