Skip to content

Commit 9e4b981

Browse files
committed
feat: Add support for Sox's compression(bitrate) argument
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.
1 parent 0a428b8 commit 9e4b981

File tree

5 files changed

+84
-4
lines changed

5 files changed

+84
-4
lines changed

.github/workflows/run-tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
python -m pip install --upgrade pip
3232
python -m pip install flake8 pytest pytest-cov
3333
python -m pip install coveralls
34-
python -m pip install pysoundfile
34+
python -m pip install soundfile
3535
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
3636
python -m pip install -e .
3737
mkdir ${{ github.workspace }}/coverage
@@ -46,7 +46,7 @@ jobs:
4646
- name: Test with pytest
4747
run: |
4848
pytest tests/
49-
49+
5050
- name: Run Coveralls
5151
run: |
5252
coverage run -m pytest tests/ > ${{ github.workspace }}/coverage/lcov.info

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
'pytest',
3333
'pytest-cov',
3434
'pytest-pep8',
35-
'pysoundfile >= 0.9.0',
35+
'soundfile >= 0.11.0',
3636
],
3737
'docs': [
3838
'sphinx==1.2.3', # autodoc was broken in 1.3.1

sox/transform.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ def _validate_output_format(self, output_format):
317317
bits = output_format.get('bits')
318318
channels = output_format.get('channels')
319319
encoding = output_format.get('encoding')
320+
bitrate = output_format.get('bitrate')
320321
comments = output_format.get('comments')
321322
append_comments = output_format.get('append_comments', True)
322323

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

347+
if not isinstance(bitrate, float) and bitrate is not None:
348+
raise ValueError('bitrate must be an float or None')
349+
346350
if encoding not in ENCODING_VALS + [None]:
347351
raise ValueError(
348352
'Invalid encoding. Must be one of {}'.format(ENCODING_VALS)
@@ -364,6 +368,7 @@ def _output_format_args(self, output_format):
364368
bits = output_format.get('bits')
365369
channels = output_format.get('channels')
366370
encoding = output_format.get('encoding')
371+
bitrate = output_format.get('bitrate')
367372
comments = output_format.get('comments')
368373
append_comments = output_format.get('append_comments', True)
369374

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

392+
if bitrate is not None:
393+
output_format_args.extend(['-C', '{}'.format(bitrate)])
394+
387395
if comments is not None:
388396
if append_comments:
389397
output_format_args.extend(['--add-comment', comments])
@@ -398,6 +406,7 @@ def set_output_format(self,
398406
bits: Optional[int] = None,
399407
channels: Optional[int] = None,
400408
encoding: Optional[EncodingValue] = None,
409+
bitrate: Optional[float] = None,
401410
comments: Optional[str] = None,
402411
append_comments: bool = True):
403412
'''Sets output file format arguments. These arguments will overwrite
@@ -456,6 +465,8 @@ def set_output_format(self,
456465
associated speech quality. SoX has support for GSM’s
457466
original 13kbps ‘Full Rate’ audio format. It is usually
458467
CPU-intensive to work with GSM audio.
468+
bitrate : float, default=None
469+
Desired bitrate. Uses Sox's -C (compression) argument.
459470
comments : str or None, default=None
460471
If not None, the string is added as a comment in the header of the
461472
output audio file. If None, no comments are added.
@@ -469,6 +480,7 @@ def set_output_format(self,
469480
'bits': bits,
470481
'channels': channels,
471482
'encoding': encoding,
483+
'bitrate': bitrate,
472484
'comments': comments,
473485
'append_comments': append_comments
474486
}
@@ -1479,7 +1491,8 @@ def contrast(self, amount=75):
14791491
def convert(self,
14801492
samplerate: Optional[float] = None,
14811493
n_channels: Optional[int] = None,
1482-
bitdepth: Optional[int] = None):
1494+
bitdepth: Optional[int] = None,
1495+
bitrate: Optional[float] = None):
14831496
'''Converts output audio to the specified format.
14841497
14851498
Parameters
@@ -1490,6 +1503,8 @@ def convert(self,
14901503
Desired number of channels. If None, defaults to the same as input.
14911504
bitdepth : int, default=None
14921505
Desired bitdepth. If None, defaults to the same as input.
1506+
bitrate : float, default=None
1507+
Desired bitrate. Uses Sox's -C (compression) argument.
14931508
14941509
See Also
14951510
--------
@@ -1513,6 +1528,11 @@ def convert(self,
15131528
if not is_number(samplerate) or samplerate <= 0:
15141529
raise ValueError("samplerate must be a positive number.")
15151530
self.rate(samplerate)
1531+
if bitrate is not None:
1532+
if not isinstance(bitrate, float) or bitrate <= 0:
1533+
raise ValueError("bitrate must be a positive float.")
1534+
self.output_format["bitrate"] = bitrate
1535+
15161536
return self
15171537

15181538
def dcshift(self, shift: float = 0.0):

tests/data/output.mp3

392 KB
Binary file not shown.

tests/test_transform.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def relpath(f):
1717
INPUT_FILE4 = relpath('data/input4.wav')
1818
OUTPUT_FILE = relpath('data/output.wav')
1919
OUTPUT_FILE_ALT = relpath('data/output_alt.wav')
20+
OUTPUT_FILE_MP3 = relpath('data/output.mp3')
2021
NOISE_PROF_FILE = relpath('data/noise.prof')
2122

2223

@@ -371,6 +372,35 @@ def test_encoding_invalid(self):
371372
self.tfm.set_input_format(encoding='16-bit-signed-integer')
372373
with self.assertRaises(ValueError):
373374
self.tfm._input_format_args({'encoding': '16-bit-signed-integer'})
375+
def test_bitrate(self):
376+
self.tfm.set_output_format(bitrate=320.0)
377+
actual = self.tfm.output_format
378+
expected = {
379+
'file_type': None,
380+
'rate': None,
381+
'bits': None,
382+
'channels': None,
383+
'encoding': None,
384+
'bitrate': 320.0,
385+
'comments': None,
386+
'append_comments': True
387+
}
388+
self.assertEqual(expected, actual)
389+
390+
actual_args = self.tfm._output_format_args(self.tfm.output_format)
391+
expected_args = ['-C', '320.0']
392+
self.assertEqual(expected_args, actual_args)
393+
394+
actual_result = self.tfm.build(INPUT_FILE, OUTPUT_FILE)
395+
expected_result = True
396+
self.assertEqual(expected_result, actual_result)
397+
398+
def test_bitrate_invalid(self):
399+
with self.assertRaises(ValueError):
400+
self.tfm.set_output_format(bitrate='320.0')
401+
with self.assertRaises(ValueError):
402+
self.tfm._output_format_args({'bitrate': 320})
403+
374404

375405
def test_ignore_length(self):
376406
self.tfm.set_input_format(ignore_length=True)
@@ -427,6 +457,7 @@ def test_file_type(self):
427457
'bits': None,
428458
'channels': None,
429459
'encoding': None,
460+
'bitrate': None,
430461
'comments': None,
431462
'append_comments': True
432463
}
@@ -449,6 +480,7 @@ def test_file_type_null_output(self):
449480
'bits': None,
450481
'channels': None,
451482
'encoding': None,
483+
'bitrate': None,
452484
'comments': None,
453485
'append_comments': True
454486
}
@@ -477,6 +509,7 @@ def test_rate(self):
477509
'bits': None,
478510
'channels': None,
479511
'encoding': None,
512+
'bitrate': None,
480513
'comments': None,
481514
'append_comments': True
482515
}
@@ -499,6 +532,7 @@ def test_rate_scinotation(self):
499532
'bits': None,
500533
'channels': None,
501534
'encoding': None,
535+
'bitrate': None,
502536
'comments': None,
503537
'append_comments': True
504538
}
@@ -533,6 +567,7 @@ def test_bits(self):
533567
'bits': 32,
534568
'channels': None,
535569
'encoding': None,
570+
'bitrate': None,
536571
'comments': None,
537572
'append_comments': True
538573
}
@@ -567,6 +602,7 @@ def test_channels(self):
567602
'bits': None,
568603
'channels': 2,
569604
'encoding': None,
605+
'bitrate': None,
570606
'comments': None,
571607
'append_comments': True
572608
}
@@ -601,6 +637,7 @@ def test_encoding(self):
601637
'bits': None,
602638
'channels': None,
603639
'encoding': 'signed-integer',
640+
'bitrate': None,
604641
'comments': None,
605642
'append_comments': True
606643
}
@@ -629,6 +666,7 @@ def test_comments(self):
629666
'bits': None,
630667
'channels': None,
631668
'encoding': None,
669+
'bitrate': None,
632670
'comments': 'asdf',
633671
'append_comments': True
634672
}
@@ -657,6 +695,7 @@ def test_append_comments(self):
657695
'bits': None,
658696
'channels': None,
659697
'encoding': None,
698+
'bitrate': None,
660699
'comments': 'asdf',
661700
'append_comments': False
662701
}
@@ -1748,6 +1787,27 @@ def test_bitdepth_invalid(self):
17481787
with self.assertRaises(ValueError):
17491788
tfm.convert(bitdepth=17)
17501789

1790+
def test_bitrate_valid(self):
1791+
tfm = new_transformer()
1792+
tfm.convert(bitrate=320.0)
1793+
1794+
actual = tfm.output_format
1795+
expected = {'bitrate': 320.0}
1796+
self.assertEqual(expected, actual)
1797+
1798+
actual_res = tfm.build(INPUT_FILE, OUTPUT_FILE_MP3)
1799+
expected_res = True
1800+
self.assertEqual(expected_res, actual_res)
1801+
1802+
tfm.set_output_format(file_type='mp3', bitrate=320.0)
1803+
tfm_assert_array_to_file_output(
1804+
INPUT_FILE, OUTPUT_FILE_MP3, tfm, skip_array_tests=True)
1805+
1806+
def test_bitrate_invalid(self):
1807+
tfm = new_transformer()
1808+
with self.assertRaises(ValueError):
1809+
tfm.convert(bitrate=0)
1810+
17511811

17521812
class TestTransformerDcshift(unittest.TestCase):
17531813

0 commit comments

Comments
 (0)