beetbox / audioread

cross-library (GStreamer + Core Audio + MAD + FFmpeg) audio decoding for Python
MIT License
481 stars 108 forks source link

Backend autodetection is inefficient #81

Closed ssssam closed 5 years ago

ssssam commented 5 years ago

I'm using Beets with the 'chroma' plugin to import a large music collection, and I've been having performance issues.

One issue I spotted is that audioread is trying every backend, each time it tries to read a file. The process on my machine appears to be:

  1. try to read the file as a .wav
  2. look for coreaudio, which involves the 'ctypes' module invoking gcc -lAudioToolbox
  3. set up a GStreamer pipeline, only to discover that it can't read the duration (https://github.com/beetbox/audioread/issues/31)
  4. read the file with libmad, which hits some other error
  5. finally, spawn ffmpeg and read the data from that.

This is burning a lot of CPU cycles unnecessarily. I haven't measured it scientifically, but from watching the output of pstree -a -t there seems to be a lot of time spent on steps 2 and 3.

ssssam commented 5 years ago

I've worked around the problem locally by adding an AUDIOREAD_BACKEND env var which I can set to 'ffmpeg' to avoid steps 1-4. This is making my Beets experience a lot more enjoyable. However, it's a bit of an ugly workaround. I've opened this issue to see if someone can think of a cleaner way of avoiding the above issue.

sampsyo commented 5 years ago

look for coreaudio, which involves the 'ctypes' module invoking gcc -lAudioToolbox

That's alarming! I didn't know ctypes could invoke the C compiler. :flushed:

Anyway, it would definitely be nice to allow some (optional) visibility into and control over the backends that are used. In spirit, the main thing that would be useful would be to create an API that lets clients use the library in two stages: one to check which backends are "available" on the current platform (and perhaps to prioritize among the ones that are available), and a second one to actually process a file. This would let beets, for example, eliminate the Core Audio backend on non-Mac platforms once in a given invocation rather than trying it many times. It could also enable a beets config option to put ffmpeg first in the priority list.

Here's how this could work: the API could expose an AudioReader class that does the first step of the work. When we construct an AudioReader, we choose the available backends and the order they should be tried in. Invoked with no parameters, as in AudioReader(), the reader should contain all of the available backends in a default order. Then, there's a method on this class called audioread.audio_read that tries all the backends indicated by the object.

Then, our current main API, the audioread.audio_open function, would become something like this:

def audio_open(filename):
    reader = AudioReader()
    return reader.audio_open(filename)

That is, it would create a new reader object every time and try all the backends every time—exactly like the current behavior.

How does that sound?

ssssam commented 5 years ago

Sounds good to me. If I get time I will do a patch for audioread and a corresponding patch for beets.

ssssam commented 5 years ago

The good news is, I implemented this change and I saw a 5x speed up in time taken to read MP3 files with the new API.

The bad news is that it seems very hard to use this new API from Beets. The 'acoustid' Python library, which consumes 'audioread', doesn't have a way to store an AudioReader instance (other than a module-level variable, which would have to be initialized during import acoustid). The Beets 'chroma' plugin does have a place to store an AudioReader instance, but it doesn't have a hard dependency on the 'audioread' module. So to use the new feature, we'd need to add code that detects if the library is available or not, and code which detects if the AudioReader object is available in this version of the library.

To work around this issue, I have instead added a global _backends variable in the audioread module, which is set the first time audio_open() is called. This means Beets can benefit from the fix without having solve the problems mentioned above.

ghost commented 5 years ago

Hello all, In Windows 10 I am getting this:

y, sr = librosa.load(librosa.util.example_audio_file(), duration=5.0) Traceback (most recent call last): File "", line 1, in File "C:\Users\Yelje\AppData\Local\Programs\Python\Python36\lib\site-packages\librosa\core\audio.py", line 112, in load with audioread.audio_open(os.path.realpath(path)) as input_file: File "C:\Users\Yelje\AppData\Local\Programs\Python\Python36\lib\site-packages\audioread__init__.py", line 116, in audio_open raise NoBackendError() audioread.NoBackendError