Click here to Skip to main content
15,880,405 members
Articles / Programming Languages / C#
Article

Using Winamp input plugins for tagging audio files

Rate me:
Please Sign up or sign in to vote.
4.74/5 (9 votes)
21 Jun 2007CPOL5 min read 68.7K   456   30   12
C# Interop with dynamic loaded plug-ins and function pointers

Screenshot - WinampMetadata.jpg

Introduction

Several years ago, I struggled with reliably reading the tags of audio files in diverse formats and versions. No existing library managed to cope with the variety of emerging MP3 successors in my vast collection. At last, I settled on using Winamp input plug-ins. The basic idea was to restrict myself to the most basic fields of audio tags, yet manage these reliably for all future tag formats.

I wrote a C++ wrapper DLL based on CDex source that relayed calls from VB6 clients and never used anything else. On the occasion of converting a client application to .NET, I tried a pure C# solution... and failed miserably. Recently, my second attempt amid several access violations proved successful. Regardless of my initial aim, you can probably use the presented class to play/decode audio with Winamp plug-ins. It might also be of interest if you are absolutely clueless about doing Interop with C-style function pointers.

Documentation, info and inspiration:

  • Winamp SDK
  • How to add meta-information support to your Winamp input plug-in
  • Using WinAmp in plug-ins from VB
  • CDex SourceForge project

    Background (C++)

    Winamp input plug-ins provide a reference to a 38-member structure, In_Module, by calling into the exported method, WINAMPGETINMODULE2:

    C++
    __declspec(dllexport) In_Module*    (*WINAMPGETINMODULE2)( void );

    After some information fields at the beginning, the In_Module structure consists of function pointers of static plug-in functions. Finally, pointers of Winamp visualisation functions:

    C++
    typedef struct 
    {
        int version;        // module type (IN_VER == 0x100)
        char *description;  // description of module
        HWND hMainWindow;   // Winamp's main window (filled in by Winamp)
        HINSTANCE hDllInstance; // DLL instance (filled in by Winamp)
        char *FileExtensions;   // "mp3,mp2,mpg"
        //  ...  
        void (*Config)(HWND hwndParent); // configuration dialog
        void (*About)(HWND hwndParent);  // about dialog
        void (*Init)();     // called at program init
        void (*Quit)();     // called at program quit
        //  ...  
        Out_Module *outMod; //  filled in by Winamp
    } 
    In_Module;

    Winamp loads the plug-in and calls WINAMPGETINMODULE2. The plug-in allocates a static instance of In_Module. It then fills the struct with preliminary description and extension fields, plus all of its function pointers. Finally, it returns a reference. Winamp sets the hMainWindow and hDllInstance fields, plus all of its function pointers. It then calls the plug-in's Init function. The plug-in initializes based upon its user configuration and updates the description and extensions fields. Until Winamp invokes the plug-in's Quit method and unloads the plug-in, both can now communicate as peers by calling their mutual functions.

    Loading a plug-in using C# Interop

    In order to load a plug-in from managed C#, we convert the above types into their respective managed counterparts. The unmanaged function pointers are replaced by delegate types with an appropriate signature, plus a MarshalAs attribute. I chose to nest the 9 needed delegates inside the class and to name them after their return/parameter types, i.e. VoidIntPtrdelegate. The fields of function pointers, supposed to be filled in by Winamp, have the IntPtr data type and can be left empty here.

    C#
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Size = 152)]
    internal class In_Module
    {
        public delegate void VoidIntPtrdelegate(IntPtr hwndParent);
        public delegate void VoidVoiddelegate();
        // ...
        public int version;
        public string description;
        public IntPtr hMainWindow;
        public IntPtr hDllInstance;
        public string FileExtensions;
        // ...
        [MarshalAs(UnmanagedType.FunctionPtr)]
        public VoidIntPtrdelegate Config;
        [MarshalAs(UnmanagedType.FunctionPtr)]
        public VoidIntPtrdelegate About;
        [MarshalAs(UnmanagedType.FunctionPtr)]
        public VoidVoiddelegate Init;
        [MarshalAs(UnmanagedType.FunctionPtr)]
        public VoidVoiddelegate Quit;
        // ...
        public IntPtr outMod;
    } 

    As In_Module contains non-blittable types -- i.e. strings and delegates -- it cannot be pinned. Furthermore, we must marshal data repeatedly between the managed/unmanaged copies. Thus, we declare a delegate for WINAMPGETINMODULE2, which returns a pointer to the unmanaged struct instead of marshalling directly to the managed class.

    C#
    [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
    internal delegate IntPtr GetInModulePtr();

    Sanity omitted:

    C#
    IntPtr hDll = Win32.LoadLibrary("in_mp3.dll");
    IntPtr pFunc = Win32.GetProcAddress(hDll, "WinampGetInModule2");
    GetInModulePtr delFunc = (GetInModulePtr)
        Marshal.GetDelegateForFunctionPointer(pFunc, typeof(GetInModulePtr));
    IntPtr pModule = delFunc();
    
    In_Module inModule = 
        (In_Module)Marshal.PtrToStructure(pModule, typeof(In_Module));
    
    inModule.hMainWindow = this.hWnd;
    inModule.hDllInstance = hDll;
    
    Marshal.StructureToPtr(inModule, pModule, false);
    
    inModule.Init();
    
    inModule = (In_Module)Marshal.PtrToStructure(pModule, typeof(In_Module));
    
    // show plug-in's configuration dialog
    inModule.Config(this.hWnd);
    Debug.Print(inModule.FileExtensions);
    
    inModule.Quit();
    Win32.FreeLibrary(hDll);

    The repeated copying of data is plainly inelegant, but comes with negligible performance cost.

    Unicode (updated)

    Beginning with Winamp 5.10, Nullsoft began implementing Unicode capabilities. The Winamp SDK 5.32 Beta defines a Unicode In_Module in the in2.h header. Only few plug-in functions use Unicode parameters, mostly file paths. The description and FileExtensions fields still use ANSI strings. It is therefore possible here to work with a common ANSI In_Module if ambiguous functions use the IntPtr data type. The corresponding delegates are now converted with GetDelegateForFunctionPointer as needed, rather than by attributing the fields.

    ExtendedFileInfo details

    Winamp SDK defines functions dealing with tags -- called ExtendedFileInfo -- in the wa_ipc.h header: IPC_GET_EXTENDED_FILE_INFO, IPC_SET_EXTENDED_FILE_INFO, IPC_WRITE_EXTENDED_FILE_INFO. These apply to Winamp itself. The corresponding plug-in methods, however, are not documented. Uncovered by Michael Facquet (see article):

    C++
    typedef int (*WINAMPGETEXTENDEDFILEINFO) (
        char* filename,char* metadata,char* ret,int retlen);
    typedef int (*WINAMPSETEXTENDEDFILEINFO) (
        char* filename,char* metadata,char* value);
    typedef int (*WINAMPWRITEEXTENDEDFILEINFO)();

    Newer plug-ins, such as Winamp 5.10+, export Unicode versions of these functions. The metadata parameter (always ANSI) identifies a particular field of the info as string, i.e. "artist," "bitrate." Setting info caches the data in the plug-in. Finally, it must be written to the file. As an example, we'll set the title field using a previously loaded plug-in:

    C#
    [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)]
    private delegate int WinampSetExtendedFileInfo(
        string filename, string metadata, string value);
    
    IntPtr pFunc = Win32.GetProcAddress(hDll, "WinampSetExtendedFileInfo");
    
    WinampSetExtendedFileInfo delFunc = (WinampSetExtendedFileInfo)
        Marshal.GetDelegateForFunctionPointer(
        pFunc, typeof(WinampSetExtendedFileInfo));
    
    delFunc(filename, "title", "Whip the Llama's Ass");

    WinampMetadata class

    The WinampMetadata class encapsulates the needed functionality. Its public interface looks like this:

    C#
    public class WinampMetadata : IDisposable
    {
        public WinampMetadata()
    
        public int PluginCount { get; }
        public string Extensions { get; }
    
        public bool InitPlugins(DirectoryInfo dir, IntPtr hWndOwner)
        public void ClosePlugins()
    
        public bool InitFileInfo(FileInfo file)
        public bool GetFileInfo(MetaDataType dataType, out string value)
        public bool SetFileInfo(MetaDataType dataType, string value)
        public bool WriteFileInfo()
    
        public bool GetFileInfoBatch(out MetaData metaData)
        public bool GetFileInfoBatch2(out MetaData2 metaData)
        public bool SetFileInfoBatch(MetaData metaData)
        
        public void InfoBox(IntPtr hWnd)
        public void ConfigPlugin(IntPtr hWnd)
        public void AboutPlugin(IntPtr hWnd)
    
        public void Dispose()
    }

    InitPlugins enumerates all input plug-ins (in_*.dll) that export the ExtendedFileInfo methods. It assembles a list of plug-in names, the respective file extensions they support and whether they use Unicode. All plug-ins are unloaded immediately. InitFileInfo ensures that an adequate plug-in is loaded for the specified file, either by reusing the last one or by searching in the internal list and loading the corresponding plug-in. The current plug-in is only unloaded if another one is needed or if the class is disposed. Individual tag fields are specified by an enum MetaDataType, rather than strings:

    C#
    public enum MetaDataType
    {
        Artist = 0,   
        Album,
        Track,
        Title,
        Year,
        Comment,
        Genre,
        Length,       // length of song in milliseconds
        Bitrate,      // in kbps if vbr, returns avg bitrate
        SampleRate,   // sampling frequency
        Stereo,       // 1 if stereo, 0 otherwise
        VBR           // if is vbr number of frames, else 0
    }

    In most cases, you will want to work with complete information. The XXXBatch methods utilize two structures, representing the writable and read-only parts. They allow getting/setting data in one operation.

    C#
    public struct MetaData
    {
        public string Artist;
        public string Album;
        public string Track;
        public string Title;
        public string Year;
        public string Comment;
        public string Genre;
        public static MetaData Empty = new MetaData();
    }
    
    public struct MetaData2
    {
        public string Length;
        public string Bitrate;
        public string SampleRate;
        public string Stereo;
        public string VBR;
    }

    The InfoBox method shows the built-in tag editor dialog, which may offer more functionality than Winamp's metadata implementation.

    Using the code

    The download contains an usercontrol that uses a WinampMetadata class as an engine to manage the tags of a single file. Note how the file's listbox lists only the supported files, returned by the Extensions property. The owner-drawn listbox itself may be a valuable addition to your toolbox, as it employs Carl Daniel's FileSystemEnumerator.

    Points of interest

    The older ANSI plug-ins were self-contained. The newer versions depend on common (nscrt.dll) and individual (libFLAC, libmp4v2) libraries. Some of these libraries reside in Winamp's installation folder and must be copied to the folder of the executing assembly. This complicates the testing of multiple plug-in versions, as the correct library must be in the EXE folder. You can download previous Winamp installers here, to try older plug-in versions.

    History

    • 8 June, 2007 -- Original version posted
    • 21 June, 2007 -- Version 1.0
      • Code and article revised for Unicode support.
      • Quit function is not called anymore on unloading, as all Unicode plug-ins crash here. No leakage observed.
      • Improved resizing of usercontrol in demo.
      • Code tested OK with v2.91, all ANSI
      • Code tested OK with v5.09, all ANSI
      • Code tested OK with v5.35, all Unicode
      • Code failed for v5.23 (changed ExtendedFileInfo Unicode typedefs ???), crashed with in_wm and in_mp4.
  • License

    This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


    Written By
    Germany Germany
    This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

    Comments and Discussions

     
    Questionso can you teach me how to use the winamp plug-in to decode the audio source by C#? Pin
    SoftDweller Parco24-Jun-07 20:33
    SoftDweller Parco24-Jun-07 20:33 
    GeneralGot all working except in_wm.dll Pin
    LM Studio19-Jun-07 3:58
    LM Studio19-Jun-07 3:58 
    GeneralSome alternatives... Pin
    taras_b17-Jun-07 14:28
    taras_b17-Jun-07 14:28 
    GeneralRe: Some alternatives... Pin
    LM Studio18-Jun-07 2:41
    LM Studio18-Jun-07 2:41 
    GeneralRe: Some alternatives... Pin
    taras_b18-Jun-07 12:57
    taras_b18-Jun-07 12:57 
    GeneralRe: Some alternatives... Pin
    LM Studio18-Jun-07 17:40
    LM Studio18-Jun-07 17:40 
    NewsNullsoft's latest plugins fail to load [modified] Pin
    OrlandoCurioso12-Jun-07 4:24
    OrlandoCurioso12-Jun-07 4:24 
    GeneralRe: Nullsoft's latest plugins fail to load Pin
    LM Studio12-Jun-07 9:48
    LM Studio12-Jun-07 9:48 
    GeneralRe: Nullsoft's latest plugins fail to load Pin
    LM Studio12-Jun-07 10:28
    LM Studio12-Jun-07 10:28 
    GeneralRe: Nullsoft's latest plugins fail to load Pin
    OrlandoCurioso12-Jun-07 11:42
    OrlandoCurioso12-Jun-07 11:42 
    GeneralRe: Nullsoft's latest plugins fail to load Pin
    LM Studio12-Jun-07 18:42
    LM Studio12-Jun-07 18:42 
    GeneralNice Pin
    LM Studio11-Jun-07 4:44
    LM Studio11-Jun-07 4:44 

    General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

    Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.