Corey-M/NAudio.Lame

Please enable NAudio.Lame to be used with Linux

Closed this issue · 8 comments

One of the advantages of Net Core applications is the ability to run on Linux and Mac in addition to Windows. I only see the shared libraries included for Windows, which makes me wonder if I can still use NAudio.Lame in a Linux container.

Running things in the cloud is a lot cheaper with Linux, and incorporating the conversion routines within a container would help my application be much more easily deployed.

If it is not possible to include the shared libraries for Mac and Linux, then at least have the library loader look for the libraries in the standard install place would be helpful. I.e. I can build an image with lame and its libraries installed, as long as NAudio.Lame simply uses the installed libraries.

Answer: no you can't. This is very Windows specific, it would be nice for the loader to attempt to load libmp3lame from the normal library loading mechanism for the host OS. If I can ensure the libraries are installed, then I can use them.

Tested this out in a container which had lame installed:

System.DllNotFoundException: Unable to load shared library 'libmp3lame.64.dll' or one of its dependencies. In order to help diagnose loading problems, consider setting the LD_DEBUG environment variable: Error loading shared library liblibmp3lame.64.dll: No such file or directory
   at LameDLLWrap.NativeMethods.lame_init()
   at LameDLLWrap.LibMp3Lame..ctor()
   at NAudio.Lame.LameMP3FileWriter..ctor(Stream outStream, WaveFormat format, LAMEPreset quality, ID3TagData id3)
   at NAudio.Lame.LameMP3FileWriter..ctor(String outFileName, WaveFormat format, LAMEPreset quality, ID3TagData id3)
   at Calvary.AudioProcessor.Processing.ProcessJob.CreateWriter(WaveFormat format, ID3TagData tags, Quality quality, String relativePath)

If you do include libraries for Linux and Mac as well as Windows, please make sure they are statically linked so we don't have issues with Alpine vs. Debian vs. CentOS images.

Hi @bloritsch ,

Unfortunately I'm not in any position to make this work on multiple platforms. I do not have MacOS or Linux machines to develop on, and I really don't want to learn a brace of platforms just so I can make this happen.

This is very Windows specific, it would be nice for the loader to attempt to load libmp3lame from the normal library loading mechanism for the host OS.

I use P/Invoke to bind to the native library. This is the standard implementation and will use the normal path search to find the DLL to load, so it might work on a Windows machine if the DLLs were moved to a folder that is on the Path.

If you have a look at LameDLLWrap\NativeMethods.cs lines 58-62 you'll see that the DLL name is determined by the build profile. Currently the target type is either X64 or default (x86). In principle you could compile Mac and Linux versions as AnyCPU with libname set to whatever it expects on the specific platform - libmp3lame.dynlib and libmp3lame.so I suppose. Then replace all 191 DLLImport attributes with platform-specific ones, add the new LameDLLWrap versions to the project's resources and change the resource loader code to identify the platform and bit width.

None of which I'm likely to tackle because I only develop on Windows.

If someone wants to fork this and get it working cross-platform then they are more than welcome to do so. The Windows libraries are version 3.100, no guarantees the thing will work with other versions.

If my guess is right, the only thing that needs to be done is to load the correct version of the the Lame lib. For Linux it would be loaded from /usr/lib/libmp3lame0.so and for Mac I'm not sure where it would be installed. Perhaps the same.

I'm OK with having to install the Lame DLL myself, if the loader is smart enough to adapt to load it by known names.

This will take some effort and I have no way to test whether it will work or not.

Here's what is needed:

Oh, and the current package includes the DLLs for 32- and 64-bit Windows. That would be a nuisance for non-Windows users.

None of this is exactly hard to achieve, but I have no desire to support multiple platforms. This is just something I mess around with, not an official or really supported package. If you want to fork this project and give it a go I'd be happy to accept a pull request for it.

If my guess is right, the only thing that needs to be done is to load the correct version of the the Lame lib. For Linux it would be loaded from /usr/lib/libmp3lame0.so and for Mac I'm not sure where it would be installed. Perhaps the same.

I'm OK with having to install the Lame DLL myself, if the loader is smart enough to adapt to load it by known names.

Hi @bloritsch
I have the same issue which is encode wav to mp3 while running on Linux container. Can you please help to share with me some guidelines to archive. I am really appreciated your help. Thank you so much!

Just in case this is helpful to someone, I had to tackle this limitation, and the way I managed to get it working on both Windows and an Alpine Linux container with Lame installed using "apk add lame", was by first upgrading the package from .NET Standard to .NET Core, and then using the new NativeLibrary.SetDllImportResolver method (which was added in .NET Core 3.0) like this:

internal static class NativeMethods
{
    static NativeMethods()
    {
        NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, ImportResolver);
    }

    private static IntPtr ImportResolver(string libraryName, System.Reflection.Assembly assembly, DllImportSearchPath? searchPath)
    {
        var libHandle = IntPtr.Zero;

        if (libraryName != libname)
        {
            return libHandle;
        }

        switch (Environment.OSVersion.Platform)
        {
            case PlatformID.Win32NT when Environment.Is64BitProcess:
                NativeLibrary.TryLoad(libname + ".64.dll", assembly, searchPath, out libHandle);
                break;
            case PlatformID.Win32NT:
                NativeLibrary.TryLoad(libname + ".32.dll", assembly, searchPath, out libHandle);
                break;
            case PlatformID.Unix:
                NativeLibrary.TryLoad(libname + ".so.0", assembly, searchPath, out libHandle);
                break;
        }

        return libHandle;
    }

    const string libname = @"libmp3lame";
    
    ...

    [DllImport(libname, CallingConvention = CallingConvention.Cdecl)]
    internal static extern IntPtr lame_init();

The system was smart enough to look into the /usr/lib/ folder, but not smart enough to try adding the .so.0 on its own - it tries to find an .so file and then gives up, but the .0 was required here.

I also made some other changes, such as moving the LameDLLWrap code into the main library, removing the Fody.Costura and ResourceAssemblyLoader stuff, replacing the X64 conditionals with System.UIntPtr. As far as I can tell it's working, and I'm left with a single .NET Core assembly that works as long as the SetDllImportResolver method can locate Lame.