Repository: librosa/librosa

Issue:
Option to force audioread to use ffmpeg
When loading an mp3, librosa calls audioread. On my system audioread then calls gstreamer instead of ffmpeg, but gstreamer does a poor job decoding the file. It decodes much better with ffmpeg. When I call audioread.available_backends(), the output is:

```
[<class 'audioread.rawread.RawAudioFile'>, <class 'audioread.gstdec.GstAudioFile'>, <class 'audioread.ffdec.FFmpegAudioFile'>]
```
So I'd like an option that forces audioread to prefer ffmpeg. Or even better, always just force it to use ffmpeg if it is available. The audioread audio_open method has a backends argument you can use for that. Or a change could be made in audioread so it checks for ffmpeg before checking for gstreamer. That's probably the simplest solution.

Option to force audioread to use ffmpeg
When loading an mp3, librosa calls audioread. On my system audioread then calls gstreamer instead of ffmpeg, but gstreamer does a poor job decoding the file. It decodes much better with ffmpeg. When I call audioread.available_backends(), the output is:

```
[<class 'audioread.rawread.RawAudioFile'>, <class 'audioread.gstdec.GstAudioFile'>, <class 'audioread.ffdec.FFmpegAudioFile'>]
```
So I'd like an option that forces audioread to prefer ffmpeg. Or even better, always just force it to use ffmpeg if it is available. The audioread audio_open method has a backends argument you can use for that. Or a change could be made in audioread so it checks for ffmpeg before checking for gstreamer. That's probably the simplest solution.



Discussion:
Interesting.  I'm sympathetic to the issue, but also keeping in mind that the eventual plan is to deprecate and remove audioread entirely.  My preference is to avoid building in new API that will be removed in the near future unless it's absolutely necessary.

In the specific case of mp3 support, this will be obviated by the next libsndfile release, which includes mp3.

How much of the `load` functionality are you using here?  It might make more sense in your case, for the time being at least, to  use audioread directly with a forced backend and then resample / downmix manually.
I've been able to work around it with some "manual" code along the lines you suggested, but it would be nice to have a neater solution, and to save other people from running into this in future. The quick change I suggested to audioread would avoid any API changes.
> The quick change I suggested to audioread would avoid any API changes.

It would necessitate expanding our API to support it as a pass-through, followed by deprecating the new API when we remove audioread in the future.

A similar discussion arose from #1349 , where the requested API extension was for soundfile.  In that case, we opted against adding parameters and in favor of accepting pre-constructed SoundFile objects instead of filenames.  Most of this decision came from not wanting to add API that only works on one backend but not the other, but it also spares us from having to maintain API parity with backend functionality.  I wonder if a solution like that could work for your problem?  That is, if we made it possible for you to initialize an audioread object however you see fit, and then we read from that instead of constructing a new one?
Looking more into this, it's a bit annoying that audioread objects have no abstract base class, ~~so we'd have to either detect each type separately, or do some kind of duck-typing/hasattr nonsense.  This is certainly doable, but it won't be a particularly elegant solution.~~ we could do:
```python
isinstance(path, tuple(audioread.available_backends()))
```
still inelegant, but serviceable.
Sorry, I don't understand how the isinstance statement above fits in.
The idea is that you could do something like:

```python
>>> ar_obj = audioread.audio_open(path, [whatever audioread options you like])
>>> y, sr = librosa.load(ar_obj, [librosa options])
```
We would detect `path` as an audioread object, using the aforementioned check, and then bypass creating a new audioread object.
Thanks - makes sense!
Interesting.  I'm sympathetic to the issue, but also keeping in mind that the eventual plan is to deprecate and remove audioread entirely.  My preference is to avoid building in new API that will be removed in the near future unless it's absolutely necessary.

In the specific case of mp3 support, this will be obviated by the next libsndfile release, which includes mp3.

How much of the `load` functionality are you using here?  It might make more sense in your case, for the time being at least, to  use audioread directly with a forced backend and then resample / downmix manually.
I've been able to work around it with some "manual" code along the lines you suggested, but it would be nice to have a neater solution, and to save other people from running into this in future. The quick change I suggested to audioread would avoid any API changes.
> The quick change I suggested to audioread would avoid any API changes.

It would necessitate expanding our API to support it as a pass-through, followed by deprecating the new API when we remove audioread in the future.

A similar discussion arose from #1349 , where the requested API extension was for soundfile.  In that case, we opted against adding parameters and in favor of accepting pre-constructed SoundFile objects instead of filenames.  Most of this decision came from not wanting to add API that only works on one backend but not the other, but it also spares us from having to maintain API parity with backend functionality.  I wonder if a solution like that could work for your problem?  That is, if we made it possible for you to initialize an audioread object however you see fit, and then we read from that instead of constructing a new one?
Looking more into this, it's a bit annoying that audioread objects have no abstract base class, ~~so we'd have to either detect each type separately, or do some kind of duck-typing/hasattr nonsense.  This is certainly doable, but it won't be a particularly elegant solution.~~ we could do:
```python
isinstance(path, tuple(audioread.available_backends()))
```
still inelegant, but serviceable.
Sorry, I don't understand how the isinstance statement above fits in.
The idea is that you could do something like:

```python
>>> ar_obj = audioread.audio_open(path, [whatever audioread options you like])
>>> y, sr = librosa.load(ar_obj, [librosa options])
```
We would detect `path` as an audioread object, using the aforementioned check, and then bypass creating a new audioread object.
Thanks - makes sense!

Resolving patch:
diff --git a/librosa/core/audio.py b/librosa/core/audio.py
--- a/librosa/core/audio.py
+++ b/librosa/core/audio.py
@@ -64,7 +64,7 @@ def load(
 
     Parameters
     ----------
-    path : string, int, pathlib.Path, soundfile.SoundFile or file-like object
+    path : string, int, pathlib.Path, soundfile.SoundFile, audioread object, or file-like object
         path to the input file.
 
         Any codec supported by `soundfile` or `audioread` will work.
@@ -75,8 +75,9 @@ def load(
         If the codec is supported by `soundfile`, then `path` can also be
         an open file descriptor (int) or an existing `soundfile.SoundFile` object.
 
-        On the contrary, if the codec is not supported by `soundfile`
-        (for example, MP3), then `path` must be a file path (string or `pathlib.Path`).
+        Pre-constructed audioread decoders are also supported here, see the example
+        below.  This can be used, for example, to force a specific decoder rather
+        than relying upon audioread to select one for you.
 
     sr : number > 0 [scalar]
         target sampling rate
@@ -143,37 +144,32 @@ def load(
     >>> sr
     22050
 
-    """
+    >>> # Load using an already open SoundFile object
+    >>> import soundfile
+    >>> sfo = soundfile.SoundFile(librosa.ex('brahms'))
+    >>> y, sr = librosa.load(sfo)
 
-    try:
-        if isinstance(path, sf.SoundFile):
-            # If the user passed an existing soundfile object,
-            # we can use it directly
-            context = path
-        else:
-            # Otherwise, create the soundfile object
-            context = sf.SoundFile(path)
-
-        with context as sf_desc:
-            sr_native = sf_desc.samplerate
-            if offset:
-                # Seek to the start of the target read
-                sf_desc.seek(int(offset * sr_native))
-            if duration is not None:
-                frame_duration = int(duration * sr_native)
-            else:
-                frame_duration = -1
+    >>> # Load using an already open audioread object
+    >>> import audioread.ffdec  # Use ffmpeg decoder
+    >>> aro = audioread.ffdec.FFmpegAudioFile(librosa.ex('brahms'))
+    >>> y, sr = librosa.load(aro)
+    """
 
-            # Load the target number of frames, and transpose to match librosa form
-            y = sf_desc.read(frames=frame_duration, dtype=dtype, always_2d=False).T
+    if isinstance(path, tuple(audioread.available_backends())):
+        # Force the audioread loader if we have a reader object already
+        y, sr_native = __audioread_load(path, offset, duration, dtype)
+    else:
+        # Otherwise try soundfile first, and then fall back if necessary
+        try:
+            y, sr_native = __soundfile_load(path, offset, duration, dtype)
 
-    except RuntimeError as exc:
-        # If soundfile failed, try audioread instead
-        if isinstance(path, (str, pathlib.PurePath)):
-            warnings.warn("PySoundFile failed. Trying audioread instead.", stacklevel=2)
-            y, sr_native = __audioread_load(path, offset, duration, dtype)
-        else:
-            raise (exc)
+        except RuntimeError as exc:
+            # If soundfile failed, try audioread instead
+            if isinstance(path, (str, pathlib.PurePath)):
+                warnings.warn("PySoundFile failed. Trying audioread instead.", stacklevel=2)
+                y, sr_native = __audioread_load(path, offset, duration, dtype)
+            else:
+                raise exc
 
     # Final cleanup for dtype and contiguity
     if mono:
@@ -188,6 +184,32 @@ def load(
     return y, sr
 
 
+def __soundfile_load(path, offset, duration, dtype):
+    """Load an audio buffer using soundfile."""
+    if isinstance(path, sf.SoundFile):
+        # If the user passed an existing soundfile object,
+        # we can use it directly
+        context = path
+    else:
+        # Otherwise, create the soundfile object
+        context = sf.SoundFile(path)
+
+    with context as sf_desc:
+        sr_native = sf_desc.samplerate
+        if offset:
+            # Seek to the start of the target read
+            sf_desc.seek(int(offset * sr_native))
+        if duration is not None:
+            frame_duration = int(duration * sr_native)
+        else:
+            frame_duration = -1
+
+        # Load the target number of frames, and transpose to match librosa form
+        y = sf_desc.read(frames=frame_duration, dtype=dtype, always_2d=False).T
+
+    return y, sr_native
+
+
 def __audioread_load(path, offset, duration, dtype):
     """Load an audio buffer using audioread.
 
@@ -195,7 +217,15 @@ def __audioread_load(path, offset, duration, dtype):
     """
 
     y = []
-    with audioread.audio_open(path) as input_file:
+
+    if isinstance(path, tuple(audioread.available_backends())):
+        # If we have an audioread object already, don't bother opening
+        reader = path
+    else:
+        # If the input was not an audioread object, try to open it
+        reader = audioread.audio_open(path)
+
+    with reader as input_file:
         sr_native = input_file.samplerate
         n_channels = input_file.channels
 


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -14,6 +14,7 @@
     pass
 
 import soundfile
+import audioread.rawread
 import librosa
 import glob
 import numpy as np
@@ -69,6 +70,20 @@ def test_load_soundfile():
     assert np.isclose(sr, sr2)
 
 
+def test_load_audioread():
+    fname = os.path.join("tests", "data", "test1_44100.wav")
+    
+    # Load using an existing audioread object
+    reader = audioread.rawread.RawAudioFile(fname)
+    y, sr = librosa.load(reader, sr=None)
+
+    # Load using sndfile
+    y2, sr2 = librosa.load(fname, sr=None)
+
+    assert np.allclose(y, y2)
+    assert np.isclose(sr, sr2)
+
+
 @pytest.mark.parametrize("res_type", ["kaiser_fast", "kaiser_best", "scipy"])
 def test_load_resample(res_type):