Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for Sox's compression(bitrate) argument #153

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install sox
sudo apt-get install libsox-fmt-mp3
python -m pip install --upgrade pip
python -m pip install flake8 pytest pytest-cov
python -m pip install coveralls
Expand All @@ -47,7 +48,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
22 changes: 21 additions & 1 deletion sox/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,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 @@ -341,6 +342,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 a positive float or None')

if encoding not in ENCODING_VALS + [None]:
raise ValueError(
f'Invalid encoding. Must be one of {ENCODING_VALS}'
Expand All @@ -362,6 +366,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 @@ -382,6 +387,9 @@ def _output_format_args(self, output_format):
if encoding is not None:
output_format_args.extend(['-e', f'{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 @@ -396,6 +404,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 @@ -454,6 +463,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 @@ -467,6 +478,7 @@ def set_output_format(self,
'bits': bits,
'channels': channels,
'encoding': encoding,
'bitrate': bitrate,
'comments': comments,
'append_comments': append_comments
}
Expand Down Expand Up @@ -1477,7 +1489,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 @@ -1488,6 +1501,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 @@ -1511,6 +1526,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