bastibe / python-soundfile

SoundFile is an audio library based on libsndfile, CFFI, and NumPy
BSD 3-Clause "New" or "Revised" License
701 stars 108 forks source link

A new release for soundfile #325

Open bastibe opened 2 years ago

bastibe commented 2 years ago

This is a continuation of the discussion in #310, where I'm working on packaging a new version of soundfile.

It appears that the packaging problem is more complicated than anticipated. Our existing build system will probably need to be replaced, which will take some time.

If anyone would like to help me package soundfile, I'd be grateful!

The problem is, we need to package four wheels for

  1. Windows 32 Bit
  2. Windows 64 Bit
  3. macOS Intel
  4. macOS Arm

each containing their own libsndfile{.dll,.dylib}, and making sure that the right version gets installed on the right platform.

We have a build script in place for building packages for 1-3, but I don't yet know how to build wheels specifically for macOS Arm, and it seems that we should convert our build system to use PEP 517 (pyproject.toml/build et al).

I only had an hour or so today to work on this, so there wasn't much progress. I'll keep you updated.

bastibe commented 2 years ago

I opened an issue at libsndfile to investigate the latter issue: https://github.com/libsndfile/libsndfile/issues/837

(Or is this a soundfile-problem? It doesn't seem to originate from the Python code, though)

adrian-stepien commented 2 years ago

Trying renaming to .mpeg gives a similar error:

---------------------------------------------------------------------------
LibsndfileError                           Traceback (most recent call last)
Input In [4], in <cell line: 1>()
----> 1 sf.read("sine.mpeg")

File ~/miniforge/lib/python3.9/site-packages/soundfile.py:282, in read(file, frames, start, stop, dtype, always_2d, fill_value, out, samplerate, channels, format, subtype, endian, closefd)
    196 def read(file, frames=-1, start=0, stop=None, dtype='float64', always_2d=False,
    197          fill_value=None, out=None, samplerate=None, channels=None,
    198          format=None, subtype=None, endian=None, closefd=True):
    199     """Provide audio data from a sound file as NumPy array.
    200
    201     By default, the whole file is read from the beginning, but the
   (...)
    280
    281     """
--> 282     with SoundFile(file, 'r', samplerate, channels,
    283                    subtype, endian, format, closefd) as f:
    284         frames = f._prepare_read(start, stop, frames)
    285         data = f.read(frames, dtype, always_2d, fill_value, out)

File ~/miniforge/lib/python3.9/site-packages/soundfile.py:655, in SoundFile.__init__(self, file, mode, samplerate, channels, subtype, endian, format, closefd)
    652 self._mode = mode
    653 self._info = _create_info_struct(file, mode, samplerate, channels,
    654                                  format, subtype, endian)
--> 655 self._file = self._open(file, mode_int, closefd)
    656 if set(mode).issuperset('r+') and self.seekable():
    657     # Move write position to 0 (like in Python file objects)
    658     self.seek(0)

File ~/miniforge/lib/python3.9/site-packages/soundfile.py:1213, in SoundFile._open(self, file, mode_int, closefd)
   1210 if file_ptr == _ffi.NULL:
   1211     # get the actual error code
   1212     err = _snd.sf_error(file_ptr)
-> 1213     raise LibsndfileError(err, prefix="Error opening {0!r}: ".format(self.name))
   1214 if mode_int == _snd.SFM_WRITE:
   1215     # Due to a bug in libsndfile version <= 1.0.25, frames != 0
   1216     # when opening a named pipe in SFM_WRITE mode.
   1217     # See http://github.com/erikd/libsndfile/issues/77.
   1218     self._info.frames = 0

LibsndfileError: Error opening 'sine.mpeg': Format not recognised.

Forcing the format (format='MPEG', subtype='MPEG_LAYER_III') does not work either

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [14], in <cell line: 1>()
----> 1 sf.read("sine.mp3", format="MPEG", subtype="MPEG_LAYER_III")

File ~/miniforge/lib/python3.9/site-packages/soundfile.py:282, in read(file, frames, start, stop, dtype, always_2d, fill_value, out, samplerate, channels, format, subtype, endian, closefd)
    196 def read(file, frames=-1, start=0, stop=None, dtype='float64', always_2d=False,
    197          fill_value=None, out=None, samplerate=None, channels=None,
    198          format=None, subtype=None, endian=None, closefd=True):
    199     """Provide audio data from a sound file as NumPy array.
    200
    201     By default, the whole file is read from the beginning, but the
   (...)
    280
    281     """
--> 282     with SoundFile(file, 'r', samplerate, channels,
    283                    subtype, endian, format, closefd) as f:
    284         frames = f._prepare_read(start, stop, frames)
    285         data = f.read(frames, dtype, always_2d, fill_value, out)

File ~/miniforge/lib/python3.9/site-packages/soundfile.py:653, in SoundFile.__init__(self, file, mode, samplerate, channels, subtype, endian, format, closefd)
    651 mode_int = _check_mode(mode)
    652 self._mode = mode
--> 653 self._info = _create_info_struct(file, mode, samplerate, channels,
    654                                  format, subtype, endian)
    655 self._file = self._open(file, mode_int, closefd)
    656 if set(mode).issuperset('r+') and self.seekable():
    657     # Move write position to 0 (like in Python file objects)

File ~/miniforge/lib/python3.9/site-packages/soundfile.py:1480, in _create_info_struct(file, mode, samplerate, channels, format, subtype, endian)
   1477 else:
   1478     if any(arg is not None for arg in (
   1479             samplerate, channels, original_format, subtype, endian)):
-> 1480         raise TypeError("Not allowed for existing files (except 'RAW'): "
   1481                         "samplerate, channels, format, subtype, endian")
   1482 return info

TypeError: Not allowed for existing files (except 'RAW'): samplerate, channels, format, subtype, endian

I created a new environment to play with. Looks like a correct library is being loaded:

>>> sf._snd
<Lib object for '[...]/miniforge/envs/test/lib/python3.9/site-packages/_soundfile_data/libsndfile_arm64.dylib'>

Version is also correct:

>>> sf.__libsndfile_version__
'1.1.0'
adrian-stepien commented 2 years ago

@bastibe Some more debugging. I discovered that the standard version of libsndfile installed on M1 mac via brew does not have mp3 support enabled.

I edited the formula (brew edit libsndfile) by adding depends_on "mpg123". Upon re-building (brew reinstall --build-from-source libsndfile) I was finally able to use the in-built command (sndfile-info sine.mp3) which gave me the correct info. Before I would get something along the lines of "This version was built without mp3 support".

========================================
File : sine.mp3
Length : 80256
MPEG-1/2 Audio
----------------------------------------
  MPEG version   : MPEG 1.0
  layer          : 3
  rate           : 48000
  mode           : mono
  mode ext       : 0
  framesize      : 192
  crc            : 0
  copyright flag : 0
  private flag   : 0
  original flag  : 1
  emphasis       : 0
  bitrate mode   : constant
  bitrate        : 64 kbps

----------------------------------------
Sample Rate : 48000
Frames      : 481536
Channels    : 1
Format      : 0x00230082
Sections    : 1
Seekable    : TRUE
Duration    : 00:00:10.032
Signal Max  : 31688.3 (-0.29 dB)

Next, I removed the the libsndfile_arm64.dylib which was bundled with pysoundfile, in order to force pysoundfile to look for homebrew version of libsndfile - it got it.

sf._snd
<Lib object for '/opt/homebrew/lib/libsndfile.dylib'>

And now, finally, it reads the mp3 file and MPEG*_ appears in the list of available subtypes.

sf.read("/Users/as2/sine.mp3")

(array([ 0.00000000e+00,  2.81360957e-09,  3.50615470e-09, ...,
         4.67424543e-05,  2.53865019e-05, -9.59571480e-07]),
 48000)

I am just wondering whether the bundled libsndfile was compiled with mp3 support?

bastibe commented 2 years ago

I am just wondering whether the bundled libsndfile was compiled with mp3 support?

I actually don't know, and have no way of checking: It was provided by another user, as I don't have an M1 mac myself.

Thus, I'll have to ask for help once again: If you have an M1 mac, could you run the version of mac_build.sh from the 1.1.0 branch of libsndfile-binaries to produce an M1 build of libsndfile with MP3 support?

BobCN2017 commented 2 years ago

could you build a new version for linux platform? thank you very much.

The none-any wheel should work on Linux. But you'll have to provide your own libsndfile with MP3 support.

I have installed libsndfile 1.1.0 and the none-any wheel in the centos server. I used the code to check:

sf.__libsndfile_version__

it is 1.1.0. used bellow:

        audio = BytesIO(request.body)
        audio.name = request.headers.get("filename")
        y, r = sf.read(data)

read the audio. when It read a wav, It worked fine. But if read a mp3 audio. have follow error:

    y, r = sf.read(data)
  File "/opt/rh/rh-python36/root/usr/lib/python3.6/site-packages/soundfile.py", line 283, in read
    subtype, endian, format, closefd) as f:
  File "/opt/rh/rh-python36/root/usr/lib/python3.6/site-packages/soundfile.py", line 655, in __init__
    self._file = self._open(file, mode_int, closefd)
  File "/opt/rh/rh-python36/root/usr/lib/python3.6/site-packages/soundfile.py", line 1213, in _open
    raise LibsndfileError(err, prefix="Error opening {0!r}: ".format(self.name))
soundfile.LibsndfileError: Error opening <_io.BytesIO object at 0x7fc592257a40>: File contains data in an unimplemented format.

Could you give some help, Thank you very much!

faroit commented 2 years ago

@BobCN2017 this is known. Mp3 support isn't compiled in right now.

@bastibe Maybe it's better to remove the beta before we confuse more people. I will try more things this weekend

zh794390558 commented 2 years ago

Follow this.

bastibe commented 2 years ago

Could anyone else try to compile a version of libsndfile with MP3 support on an M1 Mac? Without your help, I won't be able to ship a soundfile wheel for M1.

faroit commented 2 years ago

@bastibe https://github.com/bastibe/libsndfile-binaries/pull/13

bastibe commented 2 years ago

Thank you, @faroit, for providing updated binaries for M1!

There's a new beta release at https://github.com/bastibe/python-soundfile/releases/tag/0.11.0b5 for you to try!

This time, .mp3 files should open correctly by default (no more shenanigans necessary such as renaming to .mpeg, or supplying a format and subtype manually), and we should have MP3 support on M1!

I'd be grateful if you could, once again, test the wheels and see if they finally work on all platforms.

faroit commented 2 years ago

@bastibe success 💪

Python 3.9.13 | packaged by conda-forge | (main, May 27 2022, 17:01:00)
[Clang 13.0.1 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import soundfile as sf
>>> sf.info("testcase.mp3")
testcase.mp3
samplerate: 44100 Hz
channels: 2
duration: 25000 samples
format: MPEG-1/2 Audio [MP3]
subtype: MPEG Layer III [MPEG_LAYER_III]
>>> sf.read("testcase.mp3")
(array([[-0.02847968, -0.02057419],
       [-0.02475205, -0.02332359],
       [ 0.00175894, -0.01752098],
       ...,
       [-0.04847291, -0.07572973],
       [-0.07998063, -0.0545641 ],
       [-0.06100846, -0.04042743]]), 44100)
>>> sf.
sf.LibsndfileError(        sf.SEEK_SET                sf.SoundFileRuntimeError(  sf.blocks(                 sf.info(
sf.SEEK_CUR                sf.SoundFile(              sf.available_formats(      sf.check_format(           sf.read(
sf.SEEK_END                sf.SoundFileError(         sf.available_subtypes(     sf.default_subtype(        sf.write(
>>> sf.__version__
'0.11.0'
bemoody commented 2 years ago

Thanks for your work and I'm looking forward to the new release!

Just a quick note: it looks like the 0.11.0b5 release sets the name of the package in setup.py to soundfile (whereas it was SoundFile in 0.10.3.)

I'm not certain, but it looks as though changing the capitalization of the package name might cause some headaches for other tools, such as poetry - would you please consider changing it back?

bastibe commented 2 years ago

I'm not certain, but it looks as though changing the capitalization of the package name might cause some headaches for other tools, such as poetry - would you please consider changing it back?

Yes, we changed the name from the nonstandard "SoundFile" to a more common "python-soundfile", and the package name from "SoundFile" to "soundfile".

Is this actually causing problems, or is this just a worry at this point? As far as I know, pip does not care about capitalization.

CarlGao4 commented 2 years ago

Currently I can't help with testing M1. But Wheel for macOS x86_64 can't read MP3 files. Wheel for Windows x64 is OK.

igorminds commented 2 years ago

Hi @bastibe. Any updates on this? We are looking forward to use this new version. Thanks.

bastibe commented 2 years ago

It seems that the macOS x64 wheel somehow links to some system-installed /usr/local/opt/lame/lib/libmp3lame.0.dylib, instead of our own libmp3lame.la.

I've tried a fix in libsndfile-binaries#14, but apparently that didn't work as intended and still links against /usr/local/opt/lame. Does anyone understand what's going on here?

As far as I'm aware that's the last blocker for the release.

conorsleithsonos commented 2 years ago

I can help test on either Darwin arch if that's useful

bastibe commented 2 years ago

Thank you for the offer!

The build issue on macOS/Intel is still blocking, and I haven't had time to work on it. Currently, libsndfile-binaries@11 links against /usr/local/opt/lame instead of the self-compiled liblame from build_mac.sh.

If anyone wants to try to fix this issue in the build script, or contribute a self-contained libsndfile with full MP3-support for macOS/Intel, we could get this ready for release.

hiccup7 commented 2 years ago

This issue may be related and one to watch: https://github.com/libsndfile/libsndfile/issues/852

bastibe commented 2 years ago

Thanks to the outstanding work of @joetoddsonos in libsndfile-binaries@15, we should now have working macOS/Intel binaries with statically-linked MP3 support.

As far as I know, this was the last blocking problem for the 0.11 release 🤞.

Once again, I ask for your help, and try out the new wheels of python-soundfile-0.11.0b6. If these finally work on all supported platforms, we can publish the next release of soundfile with MP3 support!

Thank you all so much for your help and patience! This release wouldn't have been possible without your invaluable help!

bastibe commented 1 year ago

I just uploaded the wheels to Pypi, finally! We should have working MP3 support in soundfile, now!

thakurudit commented 1 year ago

@bastibe success 💪

Python 3.9.13 | packaged by conda-forge | (main, May 27 2022, 17:01:00)
[Clang 13.0.1 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import soundfile as sf
>>> sf.info("testcase.mp3")
testcase.mp3
samplerate: 44100 Hz
channels: 2
duration: 25000 samples
format: MPEG-1/2 Audio [MP3]
subtype: MPEG Layer III [MPEG_LAYER_III]
>>> sf.read("testcase.mp3")
(array([[-0.02847968, -0.02057419],
       [-0.02475205, -0.02332359],
       [ 0.00175894, -0.01752098],
       ...,
       [-0.04847291, -0.07572973],
       [-0.07998063, -0.0545641 ],
       [-0.06100846, -0.04042743]]), 44100)
>>> sf.
sf.LibsndfileError(        sf.SEEK_SET                sf.SoundFileRuntimeError(  sf.blocks(                 sf.info(
sf.SEEK_CUR                sf.SoundFile(              sf.available_formats(      sf.check_format(           sf.read(
sf.SEEK_END                sf.SoundFileError(         sf.available_subtypes(     sf.default_subtype(        sf.write(
>>> sf.__version__
'0.11.0'

It's still failing for me :( Status: Fixed (working on python 3.85)

Python 3.10.4 [conda env.] MacOS M1 Using latest pull from master branch

Case 1

import io
import soundfile as sf
from urllib.request import urlopen

url = "https://dl.espressif.com/dl/audio/ff-16b-1c-16000hz.mp3"
data, samplerate = sf.read(io.BytesIO(urlopen(url).read()))

Output

Traceback (most recent call last):
  File "/Users/apple/PycharmProjects/Outplay/Test/api.py", line 8, in <module>
    data, samplerate = sf.read(io.BytesIO(urlopen(url).read()))
  File "/Users/apple/PycharmProjects/Outplay/Test/python_soundfile/soundfile.py", line 282, in read
    with SoundFile(file, 'r', samplerate, channels,
  File "/Users/apple/PycharmProjects/Outplay/Test/python_soundfile/soundfile.py", line 655, in __init__
    self._file = self._open(file, mode_int, closefd)
  File "/Users/apple/PycharmProjects/Outplay/Test/python_soundfile/soundfile.py", line 1213, in _open
    raise LibsndfileError(err, prefix="Error opening {0!r}: ".format(self.name))
python_soundfile.soundfile.LibsndfileError: Error opening <_io.BytesIO object at 0x102ff4680>: Format not recognised.

Case 2

import soundfile as sf
sf.read("/Users/apple/PycharmProjects/Outplay/Test/test.mp3")

Output

Traceback (most recent call last):
  File "/Users/apple/PycharmProjects/Outplay/Test/api.py", line 13, in <module>
    sf.read("/Users/apple/PycharmProjects/Outplay/Test/test.mp3")
  File "/Users/apple/PycharmProjects/Outplay/Test/python_soundfile/soundfile.py", line 282, in read
    with SoundFile(file, 'r', samplerate, channels,
  File "/Users/apple/PycharmProjects/Outplay/Test/python_soundfile/soundfile.py", line 655, in __init__
    self._file = self._open(file, mode_int, closefd)
  File "/Users/apple/PycharmProjects/Outplay/Test/python_soundfile/soundfile.py", line 1213, in _open
    raise LibsndfileError(err, prefix="Error opening {0!r}: ".format(self.name))
python_soundfile.soundfile.LibsndfileError: Error opening '/Users/apple/PycharmProjects/Outplay/Test/test.mp3': Format not recognised.

CC: @bastibe

bastibe commented 1 year ago

How did you install soundfile? You will need both soundfile 0.11.0, and a version of libsndfile that supports MP3. The wheels from Pypi do contain such a libsndfile, but your system's libraries or conda's might not.

thakurudit commented 1 year ago

How did you install soundfile? You will need both soundfile 0.11.0, and a version of libsndfile that supports MP3. The wheels from Pypi do contain such a libsndfile, but your system's libraries or conda's might not.

soundfile version: 0.11.0 libsndfile version: 1.1.0_1

I tried 3 ways to install:-

It's working for python 3.85 :). I can settle with this.

bastibe commented 1 year ago

Please manually download soundfile-0.11.0-py2.py3-none-macosx_10_9_arm64.macosx_11_0_arm64.whl from https://pypi.org/project/soundfile/#files. Then install the wheel using python -m pip install $filename.whl (use python -m pip instead of pip to make sure you're installing into the correct interpreter). Within that python, it should then work.

Otherwise, check if soundfile is picking up your system's libsndfile instead of the wheel-provided one. Library load order is a bit of a fickle beast sometimes.

bmcfee commented 1 year ago

Thanks @bastibe & co for this update! I was looking at opening a PR to update the conda-forge recipe, and for that, it would be most helpful if there was also a source dist (tar.gz) on pypi. Would it be possible to include that in a post-release?

bastibe commented 1 year ago

Dear @bmcfee, thank you for alerting me to that. I simply forgot to upload the source dist. It's available now.

bmcfee commented 1 year ago

Excellent, thank you!

iwo9 commented 1 year ago

Please manually download soundfile-0.11.0-py2.py3-none-macosx_10_9_arm64.macosx_11_0_arm64.whl from https://pypi.org/project/soundfile/#files. Then install the wheel using python -m pip install $filename.whl (use python -m pip instead of pip to make sure you're installing into the correct interpreter). Within that python, it should then work.

Otherwise, check if soundfile is picking up your system's libsndfile instead of the wheel-provided one. Library load order is a bit of a fickle beast sometimes.

I've tried everything else and still can't load mp3 files. I've downloaded a version of libsndfile that supports mp3 but how do I check if soundfile is picking up the right libsndfile version and not a system one? Update: I've checked the libsndfile version using sf.__libsndfile_version__ - it's 1.1.0, which has mp3 support, still not loading image

Was using soundfile as the audio backend for torchaudio, but even when I use soundfile.read it gives the same error

bastibe commented 1 year ago

System error almost always means your file name is incorrect. It seems you are missing the backslash after C:.

iwo9 commented 1 year ago

System error almost always means your file name is incorrect. It seems you are missing the backslash after C:.

Omg thanks, didn't see that. torchaudio.load is working now, but it's returning an empty tensor as waveform when it's called on mp3 files, although working fine with flac files. Any idea why that might be happening? image

bastibe commented 1 year ago

Try opening the file with straight soundfile instead of going through torch. Who knows what torch is doing in the background.

asennoussi commented 1 year ago

I'm facing the same issue: Here is my code:

# iterate over the XML files in the folder
    for xml_file in os.listdir(folder_path):
        if xml_file.endswith(".xml"):
            # parse the XML file
            tree = ET.parse(os.path.join(folder_path,xml_file))
            root = tree.getroot()

            # get the filename of the wav file
            filename = './wav/'+root.find('./head/recording').attrib['filename']
            audio_file = AudioSegment.from_file(f'{filename}.wav')

            # iterate over the segments
            for segment in root.findall('./body/segments/segment'):

                start = float(segment.attrib['starttime']) * 1000  # convert to milliseconds
                end = float(segment.attrib['endtime']) * 1000  # convert to milliseconds
                snippet = audio_file[start:end]
                snippet.export(f'./snippets/{segment.attrib["id"]}.mp3', format='mp3')

                # extract the text from the element tags
                text = ' '.join([element.text for element in segment.findall('./element')])

                # create a unique filename
                new_filename = f'./snippets/{segment.attrib["id"]}.mp3'

                audio, sr = sf.read(new_filename)

This works perfectly on OSX but not on ubuntu 22.04, any idea why?

SpotlightKid commented 1 year ago

@asennoussi Ubuntu 22.04 still packages libsndfile version 1.0.31, which, AFAIK, does not have MP3 support yet.

Also note that, due to errors in the CMake build files, the pen-ultimate release of libsndfile (1.1.0) does not have MP3 support in most Linux distribution packages either, even though this version does support MP3 in theory, but due to said errors it will not be compiled in. The latest version (1.2.0) fixes that, but this still has to arrive in Ubuntu.

asennoussi commented 1 year ago

Makes sense? I just used pydub's AudioSegment.from_file method and then

# get the raw audio data as a one-dimensional array
data = audio.get_array_of_samples()
# get the sample rate
sample_rate = audio.frame_rate

Instead. I hope this helps out someone else.

bastibe commented 1 year ago

I have compiled a preliminary binary wheel for Ubuntu recently, that I'll try to release for testing soon. This should allow MP3 support on Ubuntu if installed with pip, and if no system-libsndfile is present.

bastibe commented 1 year ago

Please check out https://github.com/bastibe/python-soundfile/pull/364 for a beta-release of a binary wheel for Linux.

luohao123 commented 8 months ago

@bastibe hello, I am so fraisated keep got this error:

soundfile.py", line 1226, in _init_virtual_io def vio_get_filelen(user_data): MemoryError: Cannot allocate write+execute memory for ffi.callback(). You might be running on a system that prevents this. For more information, see https://cffi.readthedocs.io/en/latest/using.html#callbacks

I installed from pypi, tried your M1 arm wheel, all fails.

Why? I also tried unsintall previous just keep error

mirodil-ml commented 4 months ago

I am still getting the same error even with version 0.12.1. Is there a solution to this issue?