Repository: rabitt/pysox

Issue:
Is `file_info.bitrate` correct?
Hello

This function seems to be returning the bit depth (aka bits per sample) not the bit rate.

`soxi -b` is the bit depth.

`soxi -B` is the bitrate.

Happy to submit a pull request with a fix and a second function for the bit depth.

Cheers.

Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/18160988/badge)](https://coveralls.io/builds/18160988)

Coverage increased (+0.005%) to 98.759% when pulling **b74ac52f02d9cbf04a5e7c1c297039d823060ec7 on lostanlen:patch-1** into **f5671ebe3e5d83277833a0a768a422e9e14a4b0d on rabitt:master**.

lostanlen: Had an offline conversation with `rabitt` and she asked me to convert the `bitrate` field into an Python integer, expressed in bits per second. I'm going to write a simple-minded string parser in `file_info.bitrate` to strip the `k` suffix
rabitt: @lostanlen thanks for catching this, and for the PR! Before I merge, could you please squash your commits down to one?
lostanlen: Tests are passing, this is ready for CR.
This PR have important breaking changes, so it would be best to schedule if for `v1.4`
lostanlen: I implemented the fix to issue #78
Tests are passing. Coverage decreases a little bit because:
1. I don't know how to produce a "zero bit depth" audio file
2. I don't know how to produce an audio file with less than 1kbps bit rate

Some help for 1 and/or 2 would be greatly appreciated.

@rabitt This is ready for a new round of CR 
rabitt: > Tests are passing. Coverage decreases a little bit because:
> 
> 1. I don't know how to produce a "zero bit depth" audio file
>2.  I don't know how to produce an audio file with less than 1kbps bit rate
>
> Some help for 1 and/or 2 would be greatly appreciated.

Just spent some time trying to create "valid" (files SoX knows how to read) files with these stats, and so far I haven't found a way to do it either. The obvious degenerate cases (e.g. a truly empty file with an audio file extension) can't be read in the first place and creates a higher level SoX error. And an 0-length audio file with a header readable by SoX has bit depth information that is nonzero. 

If anyone else has ideas how to hit these cases, help would be welcome! Otherwise, I'd say we ignore these new uncovered lines and merge despite the decreased coverage.
lostanlen: Rebased. Commits squashed into two (one for issue #68, one for issue #78). I might have goofed up with the source tree; do tell me in case I did
rabitt: @lostanlen I don't see the rebased commits - just the original set. Did you `--force` push?

Patch:
diff --git a/README.md b/README.md
index 3325a5b..7fe349d 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@ Python wrapper around sox. [Read the Docs here](http://pysox.readthedocs.org).
 
 [![PyPI version](https://badge.fury.io/py/sox.svg)](https://badge.fury.io/py/sox)
 [![Documentation Status](https://readthedocs.org/projects/resampy/badge/?version=latest)](http://pysox.readthedocs.io/en/latest/?badge=latest)
-[![GitHub license](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://raw.githubusercontent.com/rabitt/pysox/master/LICENSE.md)
+[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/rabitt/pysox/master/LICENSE.md)
 [![PyPI](https://img.shields.io/pypi/pyversions/Django.svg?maxAge=2592000)]()
 
 [![Build Status](https://travis-ci.org/rabitt/pysox.svg?branch=master)](https://travis-ci.org/rabitt/pysox)
@@ -67,7 +67,7 @@ nosetests .
 
 ```python
 import sox
-# create transformer
+# create trasnformer
 tfm = sox.Transformer()
 # trim the audio between 5 and 10.5 seconds.
 tfm.trim(5, 10.5)
diff --git a/setup.py b/setup.py
index f6ed177..8fc4c8b 100644
--- a/setup.py
+++ b/setup.py
@@ -18,7 +18,7 @@ if __name__ == "__main__":
         package_data={'sox': []},
         long_description="""Python wrapper around SoX.""",
         keywords='audio effects SoX',
-        license='BSD-3-Clause',
+        license='MIT',
         install_requires=[
         ],
         extras_require={
diff --git a/sox/core.py b/sox/core.py
index 118cd4a..178fbb9 100644
--- a/sox/core.py
+++ b/sox/core.py
@@ -6,7 +6,7 @@ from subprocess import CalledProcessError
 
 from . import NO_SOX
 
-SOXI_ARGS = ['b', 'c', 'a', 'D', 'e', 't', 's', 'r']
+SOXI_ARGS = ['B', 'b', 'c', 'a', 'D', 'e', 't', 's', 'r']
 
 ENCODING_VALS = [
     'signed-integer', 'unsigned-integer', 'floating-point', 'a-law', 'u-law',
diff --git a/sox/file_info.py b/sox/file_info.py
index 9077f1a..d4a373d 100644
--- a/sox/file_info.py
+++ b/sox/file_info.py
@@ -10,9 +10,9 @@ from .core import sox
 from .core import enquote_filepath
 
 
-def bitrate(input_filepath):
+def bitdepth(input_filepath):
     '''
-    Number of bits per sample (0 if not applicable).
+    Number of bits per sample, or None if not applicable.
 
     Parameters
     ----------
@@ -21,17 +21,50 @@ def bitrate(input_filepath):
 
     Returns
     -------
-    bitrate : int
-        number of bits per sample
-        returns 0 if not applicable
+    bitdepth : int or None
+        Number of bits per sample.
+        Returns None if not applicable.
     '''
+    
     validate_input_file(input_filepath)
     output = soxi(input_filepath, 'b')
     if output == '0':
-        logger.warning("Bitrate unavailable for %s", input_filepath)
+        logger.warning("Bit depth unavailable for %s", input_filepath)
+        return None
     return int(output)
 
 
+def bitrate(input_filepath):
+    '''
+    Bit rate averaged over the whole file.
+    Expressed in bytes per second (bps), or None if not applicable.
+
+    Parameters
+    ----------
+    input_filepath : str
+        Path to audio file.
+
+    Returns
+    -------
+    bitrate : float or None
+        Bit rate, expressed in bytes per second.
+        Returns None if not applicable.
+    '''
+    
+    validate_input_file(input_filepath)
+    output = soxi(input_filepath, 'B')
+    # The characters below stand for kilo, Mega, Giga, etc.
+    greek_prefixes = '\0kMGTPEZY'
+    if output == "0":
+        logger.warning("Bit rate unavailable for %s", input_filepath)
+        return None
+    elif output[-1] in greek_prefixes:
+        multiplier = 1000.0**(greek_prefixes.index(output[-1]))
+        return float(output[:-1])*multiplier
+    else:
+        return float(output[:-1])
+
+
 def channels(input_filepath):
     '''
     Show number of channels.
@@ -73,7 +106,7 @@ def comments(input_filepath):
 
 def duration(input_filepath):
     '''
-    Show duration in seconds (0 if unavailable).
+    Show duration in seconds, or None if not available.
 
     Parameters
     ----------
@@ -82,15 +115,15 @@ def duration(input_filepath):
 
     Returns
     -------
-    duration : float
+    duration : float or None
         Duration of audio file in seconds.
-        If unavailable or empty, returns 0.
+        If unavailable or empty, returns None.
     '''
     validate_input_file(input_filepath)
     output = soxi(input_filepath, 'D')
-    if output == '0':
+    if float(output) == 0.0:
         logger.warning("Duration unavailable for %s", input_filepath)
-
+        return None
     return float(output)
 
 
@@ -134,7 +167,7 @@ def file_type(input_filepath):
 
 def num_samples(input_filepath):
     '''
-    Show number of samples (0 if unavailable).
+    Show number of samples, or None if unavailable.
 
     Parameters
     ----------
@@ -143,14 +176,15 @@ def num_samples(input_filepath):
 
     Returns
     -------
-    n_samples : int
+    n_samples : int or None
         total number of samples in audio file.
-        Returns 0 if empty or unavailable
+        Returns None if empty or unavailable.
     '''
     validate_input_file(input_filepath)
     output = soxi(input_filepath, 's')
     if output == '0':
         logger.warning("Number of samples unavailable for %s", input_filepath)
+        return None
     return int(output)
 
 
@@ -313,6 +347,7 @@ def info(filepath):
         Dictionary of file information. Fields are:
             * channels
             * sample_rate
+            * bitdepth
             * bitrate
             * duration
             * num_samples
@@ -322,6 +357,7 @@ def info(filepath):
     info_dictionary = {
         'channels': channels(filepath),
         'sample_rate': sample_rate(filepath),
+        'bitdepth': bitdepth(filepath),
         'bitrate': bitrate(filepath),
         'duration': duration(filepath),
         'num_samples': num_samples(filepath),
diff --git a/sox/transform.py b/sox/transform.py
index df4609e..3dd9a1c 100644
--- a/sox/transform.py
+++ b/sox/transform.py
@@ -2682,7 +2682,7 @@ class Transformer(object):
         Returns
         -------
         power_spectrum : list
-            List of frequency (Hz), amplitude pairs.
+            List of frequency (Hz), amplitdue pairs.
 
         See Also
         --------
@@ -2725,7 +2725,7 @@ class Transformer(object):
         Returns
         -------
         stats_dict : dict
-            List of frequency (Hz), amplitude pairs.
+            List of frequency (Hz), amplitdue pairs.
 
         See Also
         --------
diff --git a/sox/version.py b/sox/version.py
index ad0d588..22a6515 100644
--- a/sox/version.py
+++ b/sox/version.py
@@ -3,4 +3,4 @@
 """Version info"""
 
 short_version = '1.3'
-version = '1.3.5'
+version = '1.3.4'


Test patch:
diff --git a/tests/test_file_info.py b/tests/test_file_info.py
index cca028f..d960434 100644
--- a/tests/test_file_info.py
+++ b/tests/test_file_info.py
@@ -22,19 +22,40 @@ class TestBitrate(unittest.TestCase):
 
     def test_wav(self):
         actual = file_info.bitrate(INPUT_FILE)
-        expected = 16
+        expected = 706000.0
         self.assertEqual(expected, actual)
 
     def test_aiff(self):
         actual = file_info.bitrate(INPUT_FILE2)
-        expected = 32
+        expected = 768000.0
         self.assertEqual(expected, actual)
 
     def test_empty(self):
         actual = file_info.bitrate(EMPTY_FILE)
+        expected = None
+
+class TestBitdepth(unittest.TestCase):
+
+    def test_wav(self):
+        actual = file_info.bitdepth(INPUT_FILE)
+        expected = 16
+        self.assertEqual(expected, actual)
+
+    def test_aiff(self):
+        actual = file_info.bitdepth(INPUT_FILE2)
+        expected = 32
+        self.assertEqual(expected, actual)
+
+    def test_empty(self):
+        actual = file_info.bitdepth(INPUT_FILE)
         expected = 16
         self.assertEqual(expected, actual)
 
+    def test_aiff(self):
+        actual = file_info.bitdepth(INPUT_FILE2)
+        expected = 32
+        self.assertEqual(expected, actual)
+
 
 class TestChannels(unittest.TestCase):
 
@@ -91,7 +112,7 @@ class TestDuration(unittest.TestCase):
 
     def test_empty(self):
         actual = file_info.duration(EMPTY_FILE)
-        expected = 0
+        expected = None
         self.assertEqual(expected, actual)
 
 
@@ -145,7 +166,7 @@ class TestNumSamples(unittest.TestCase):
 
     def test_empty(self):
         actual = file_info.num_samples(EMPTY_FILE)
-        expected = 0
+        expected = None
         self.assertEqual(expected, actual)
 
 
@@ -220,7 +241,8 @@ class TestInfo(unittest.TestCase):
         expected = {
             'channels': 1,
             'sample_rate': 44100.0,
-            'bitrate': 16,
+            'bitdepth': 16,
+            'bitrate': 706000.0,
             'duration': 10.0,
             'num_samples': 441000,
             'encoding': 'Signed Integer PCM',