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

Simple C++ DirectShow MP3 Player with .NET Wrapper

Rate me:
Please Sign up or sign in to vote.
4.77/5 (22 votes)
6 May 2024Ms-PL4 min read 91.3K   4.3K   50   10
No frills C++ MP3/WMA DirectShow player class

Demo Application Screenshot

Introduction

The DirectShow MP3 player class featured in this article, is part of a bigger C++ MFC Karaoke subtitle project until I decided to scrap the source code and rewrite in C# WPF to take advantage of WPF builtin animation. The MP3 source code is available on Codeplex for quite some time and the number of downloads (323) has exceeded my other popular project (317). This reflects that in the Windows C++ world, there is no good simple MP3 player class. The alternatives are usually either the outdated Media Control Interface (MCI) or monolithic commercial libraries (with many features) which is an overkill if the programmer just need to simply play a MP3 file.

Source Code

If you need to just play MP3s in your application (for example, play a short MP3 during the application splash screen), Mp3 class is a no frills C++ MP3/WMA DirectShow player class, for such simple needs. The original code is from Flipcode's contributor, Alan Kemp. The original code need a bit of tweaking to include the necessary header files and import libraries to get it to compile in Visual Studio 2010. Since this class relies on DirectShow, you need to download the Windows SDK to build it. If you are using Visual Studio 2010, it actually comes with a subset of the Windows SDK which includes the DirectShow lbraries, so you can build this class without downloading anything. You have to call COM's CoInitialize to initialize COM's runtime before calling the Load on mp3 file. And you have to also call CoUninitialize at the end of your application, after the Cleanup is called. The header file, Mp3.h is listed below.

C++
#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
// Windows Header Files:
#include <windows.h>
#include <mmsystem.h>
#include <strmif.h>
#include <control.h>

#pragma comment(lib, "strmiids.lib")

class Mp3
{
public:
    Mp3();
    ~Mp3();

    bool Load(LPCWSTR filename);
    void Cleanup();

    bool Play();
    bool Pause();
    bool Stop();
	
    // Poll this function with msTimeout = 0, so that it return immediately.
    // If the mp3 finished playing, WaitForCompletion will return true;
    bool WaitForCompletion(long msTimeout, long* EvCode);

    // -10000 is lowest volume and 0 is highest volume, positive value > 0 will fail
    bool SetVolume(long vol);
	
    // -10000 is lowest volume and 0 is highest volume
    long GetVolume();
	
    // Returns the duration in 1/10 millionth of a second,
    // meaning 10,000,000 == 1 second
    // You have to divide the result by 10,000,000 
    // to get the duration in seconds.
    __int64 GetDuration();
	
    // Returns the current playing position
    // in 1/10 millionth of a second,
    // meaning 10,000,000 == 1 second
    // You have to divide the result by 10,000,000 
    // to get the duration in seconds.
    __int64 GetCurrentPosition();

    // Seek to position with pCurrent and pStop
    // bAbsolutePositioning specifies absolute or relative positioning.
    // If pCurrent and pStop have the same value, the player will seek to the position
    // and stop playing. Note: Even if pCurrent and pStop have the same value,
    // avoid putting the same pointer into both of them, meaning put different
    // pointers with the same dereferenced value.
    bool SetPositions(__int64* pCurrent, __int64* pStop, bool bAbsolutePositioning);

private:
    IGraphBuilder *  pigb;
    IMediaControl *  pimc;
    IMediaEventEx *  pimex;
    IBasicAudio * piba;
    IMediaSeeking * pims;
    bool    ready;
    // Duration of the MP3.
    __int64 duration;

};

The original class only has the play, pause and stop functionality. Note: after calling Pause, you have to call Play to resume playing. Since I have a need to loop my music, so I need to know when my MP3 ended, I added the method, WaitForCompletion for me to poll periodically whether the playing has ended, to replay it again. Since the original code always played at full volume, I have also added a method, GetVolume to get volume and another method, SetVolume to adjust volume. Note: -10000 is the minimum volume and 0 is the maximum volume. And if you set any positive volume greater than 0, you will receive an error. You can call GetDuration and GetCurrentPosition to get the duration of the MP3 and the current playing (time) position of the MP3 respectively. These 2 methods return units of 10th millionth of a second(1/10,000,000 of a second): you have to divide by 10,000,000 to get the duration in seconds. The reason I did not return the duration in seconds, is because I found that second unit is too coarse grained to do seeking. The source code implementation of Mp3.cpp is listed below.

C++
#include "Mp3.h"
#include <uuids.h>

Mp3::Mp3()
{
    pigb = NULL;
    pimc = NULL;
    pimex = NULL;
    piba = NULL;
    pims = NULL;
    ready = false;
    duration = 0;
}

Mp3::~Mp3()
{
    Cleanup();
}

void Mp3::Cleanup()
{
    if (pimc)
        pimc->Stop();

    if(pigb)
    {
        pigb->Release();
        pigb = NULL;
    }

    if(pimc)
    {
        pimc->Release();
        pimc = NULL;
    }

    if(pimex)
    {
        pimex->Release();
        pimex = NULL;
    }

    if(piba)
    {
        piba->Release();
        piba = NULL;
    }

    if(pims)
    {
        pims->Release();
        pims = NULL;
    }
    ready = false;
}

bool Mp3::Load(LPCWSTR szFile)
{
    Cleanup();
    ready = false;
    if (SUCCEEDED(CoCreateInstance( CLSID_FilterGraph,
        NULL,
        CLSCTX_INPROC_SERVER,
        IID_IGraphBuilder,
        (void **)&this->pigb)))
    {
        pigb->QueryInterface(IID_IMediaControl, (void **)&pimc);
        pigb->QueryInterface(IID_IMediaEventEx, (void **)&pimex);
        pigb->QueryInterface(IID_IBasicAudio, (void**)&piba);
        pigb->QueryInterface(IID_IMediaSeeking, (void**)&pims);

        HRESULT hr = pigb->RenderFile(szFile, NULL);
        if (SUCCEEDED(hr))
        {
            ready = true;
            if(pims)
            {
                pims->SetTimeFormat(&TIME_FORMAT_MEDIA_TIME);
                pims->GetDuration(&duration); // returns 10,000,000 for a second.
                duration = duration;
            }
        }
    }
    return ready;
}

bool Mp3::Play()
{
    if (ready&&pimc)
    {
        HRESULT hr = pimc->Run();
        return SUCCEEDED(hr);
    }
    return false;
}

bool Mp3::Pause()
{
    if (ready&&pimc)
    {
        HRESULT hr = pimc->Pause();
        return SUCCEEDED(hr);
    }
    return false;
}

bool Mp3::Stop()
{
    if (ready&&pimc)
    {
        HRESULT hr = pimc->Stop();
        return SUCCEEDED(hr);
    }
    return false;
}

bool Mp3::WaitForCompletion(long msTimeout, long* EvCode)
{
    if (ready&&pimex)
    {
        HRESULT hr = pimex->WaitForCompletion(msTimeout, EvCode);
        return *EvCode > 0;
    }

    return false;
}

bool Mp3::SetVolume(long vol)
{
    if (ready&&piba)
    {
        HRESULT hr = piba->put_Volume(vol);
        return SUCCEEDED(hr);
    }
    return false;
}

long Mp3::GetVolume()
{
    if (ready&&piba)
    {
        long vol = -1;
        HRESULT hr = piba->get_Volume(&vol);

        if(SUCCEEDED(hr))
            return vol;
    }

    return -1;
}

__int64 Mp3::GetDuration()
{
    return duration;
}

__int64 Mp3::GetCurrentPosition()
{
    if (ready&&pims)
    {
        __int64 curpos = -1;
        HRESULT hr = pims->GetCurrentPosition(&curpos);

        if(SUCCEEDED(hr))
            return curpos;
    }

    return -1;
}

bool Mp3::SetPositions(__int64* pCurrent, __int64* pStop, bool bAbsolutePositioning)
{
    if (ready&&pims)
    {
        DWORD flags = 0;
        if(bAbsolutePositioning)
            flags = AM_SEEKING_AbsolutePositioning | AM_SEEKING_SeekToKeyFrame;
        else
            flags = AM_SEEKING_RelativePositioning | AM_SEEKING_SeekToKeyFrame;

        HRESULT hr = pims->SetPositions(pCurrent, flags, pStop, flags);

        if(SUCCEEDED(hr))
            return true;
    }

    return false;
}

Below is an example of how the programmer would use the Mp3 class in a console application. Note: Play is an non-blocking call.

C++
#include "Mp3.h"

void main()
{
    // Initialize COM
    ::CoInitialize(NULL);

    std::wcout<<L"Enter the MP3 path: ";
    std::wstring path;
    getline(wcin, path);

    std::wcout<<path<<std::endl;

    Mp3 mp3;

    int status = 0;
    if(mp3.Load(path.c_str()))
    {
        status = SV_LOADED;
    }
    else // Error
    {
        // ...
    }

    if(mp3.Play())
    {
        status = SV_PLAYING;
    }
    else // Error
    {
        // ...
    }

    // ... after some time

    if(mp3.Stop())
    {
        status = SV_STOPPED;
    }
    else // Error
    {
        // ...
    }

    // Uninitialize COM
    ::CoUninitialize();
}

The source code includes a static library project and the DLL project and a demo project, PlayMp3, which plays MP3 with a helper class, CLibMP3DLL, to load the LibMP3DLL.dll at runtime. Usage of CLibMP3DLL is similar to Mp3 class, with additional LoadDLL and UnloadDLL methods to load/unload dll. Below is the header file of CLibMP3DLL.

C++
class CLibMP3DLL
{
public:
    CLibMP3DLL(void);
    ~CLibMP3DLL(void);

    bool LoadDLL(LPCWSTR dll);
    void UnloadDLL();

    bool Load(LPCWSTR filename);
    bool Cleanup();

    bool Play();
    bool Pause();
    bool Stop();
    bool WaitForCompletion(long msTimeout, long* EvCode);

    bool SetVolume(long vol);
    long GetVolume();

    __int64 GetDuration();
    __int64 GetCurrentPosition();

    bool SetPositions(__int64* pCurrent, __int64* pStop, bool bAbsolutePositioning);

private:
    HMODULE m_Mod;
};

Note: the source code download also include a C++/CLI wrapper to enable the Mp3 class to be used in .NET application. Note: If you build the C++ library in x86 (32 bits) or x64 (64 bits) platform, the .NET application has to be in the same platform, not 'Any CPU' platform. Note: You have to install the Advanced Audio Coding (AAC) codec in order to play AAC files (*.aac). 

Conclusion

Though I may have added a few methods to the Mp3 class, it took me quite a bit effort to get them run correctly. I hope to pass these time-savings to other developers who simply wants to play a MP3 file, minus the hassle. Source code is hosted at Github.

History

  • 2024-05-07 : Update the article title to reflect there is a C++/CLI wrapper to use this MP3 class in .NET application.
  • 2023-03-11 : v0.7.3 added #pragma once to the Mp3 header
  • 2012-04-26 : Initial Release

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
Software Developer (Senior)
Singapore Singapore
Shao Voon is from Singapore. His interest lies primarily in computer graphics, software optimization, concurrency, security, and Agile methodologies.

In recent years, he shifted focus to software safety research. His hobby is writing a free C++ DirectX photo slideshow application which can be viewed here.

Comments and Discussions

 
QuestionCOM/C++ best practices, and better way to expose to WPF/.NET Pin
Stacy Dudovitz9-May-24 8:32
professionalStacy Dudovitz9-May-24 8:32 
AnswerRe: COM/C++ best practices, and better way to expose to WPF/.NET Pin
Shao Voon Wong12-May-24 13:25
mvaShao Voon Wong12-May-24 13:25 
GeneralRe: COM/C++ best practices, and better way to expose to WPF/.NET Pin
Stacy Dudovitz12-May-24 21:20
professionalStacy Dudovitz12-May-24 21:20 
I need to address a couple of things you said in your reply.

Let me start with CComPtr. I'm thinking there is a misunderstanding how this is used in C++. I think some sample code I pulled from a working project might help. This project is a full fledged .NET WPF project that is a mix of C#, C++ and C++/CLI. This code is used to control some industrial hardware, and provide the software and GUI to operate the machine.

In this example, I would like to use a DataSet object, which is a part of WinForms. In as much as this is a WPF application, I would prefer not to drag in the WinForms assemblies into the main application. I would also like to access, and share this object both in C++ and C#. Here's how it's done.

Lets start with some code. This is the header file contents to define the DataSet COM object implemented using ATL:
//============================================================================
// CDataSetObj
//============================================================================

class ATL_NO_VTABLE CDataSetObj : 
    public CComObjectRootEx<CComMultiThreadModel>,
    public CComCoClass<CDataSetObj, &CLSID_DataSetObj>,
    public ISupportErrorInfo,
    public IDataSetObj,
    public CErrorHandler<IDataSetObj>
{
public:
    CDataSetObj();
    ~CDataSetObj();

    HRESULT FinalConstruct();
    void FinalRelease(); 

    DECLARE_REGISTRY_RESOURCEID(IDR_DATASETOBJ)
    DECLARE_NOT_AGGREGATABLE(CDataSetObj)
    DECLARE_PROTECT_FINAL_CONSTRUCT()

    BEGIN_COM_MAP(CDataSetObj)
        COM_INTERFACE_ENTRY(IDataSetObj)
        COM_INTERFACE_ENTRY(ISupportErrorInfo)
    END_COM_MAP()

    // ISupportsErrorInfo
    STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);

    // IDataSetObj
    STDMETHOD(put_Handle)(/[in]/ ULARGE_INTEGER handle);
    STDMETHOD(get_Handle)(/[out,ref,retval]/ ULARGE_INTEGER * handle);

    STDMETHOD(Create)(/[in,ref]/ BSTR name);

    STDMETHOD(AddTable)(/[in]/ IDataTableObj * table);
    STDMETHOD(GetTable)(/[in,ref]/ BSTR tableName, /[out]/ IDataTableObj ** table);

    // table iteration support
    STDMETHOD(get_EnumTableObjs)(/[out]/ IEnumTableObjs ** ppenum);
    STDMETHOD(get_Count)(/[out,ref]/ long * pcount);

    // constraints
    STDMETHOD(AddRelation)(/[in]/ IDataRelationObj * relation);

    // Class data
private:
    gcroot<DataSet * > * m_dataSet;
    MANAGED_HANDLE m_handle;
}

Now lets look at the Create method:
STDMETHODIMP CDataSetObj::Create(/[in,ref]/ BSTR name)
{
    HRESULT hr = S_OK;

    try
    {
        //::MessageBox(NULL, L"test", L"test", MB_OK);

        // create managed helper classes
        m_dataSet = new(static_cast<gcroot<DataSet * > * >(::CoTaskMemAlloc(sizeof (gcroot<DataSet* >)))) gcroot<DataSet * >;
        m_handle.LowPart = static_cast<DWORD >(reinterpret_cast<DWORD_PTR >(m_dataSet));
        m_handle.HighPart = ::GetProcessId(GetCurrentProcess());

        // create new DataSet object
        *m_dataSet = new DataSet(new System::String(name));
    }

    catch   (HRESULT badhr)
    {
        hr = badhr;
    }
    catch (System::Exception * e)
    {
        const wchar_t __pin * msg = PtrToStringChars(e->Message);
        std::wstringstream t;
        t << L"Create(): Exception! Line: " << __LINE __ << std::endl << msg << std::endl;
        msg = NULL;
        LogError(L"DataSetObj", t.str().c_str(), FALSE);
        hr = E_UNEXPECTED;
    }
    catch   (...)
    {
        hr = E_UNEXPECTED;
        m_errlineno =   __LINE __;
    }

    return hr;
}

Apologies for the weird spacing, I'm fighting the MarkDown formatting.

How to use this? Here is the code snippet below for both C++/CLI and C#:
try
 {
     // we need to be multi threaded since we will be talking to other MTA threads
     FAILED_THROW(::CoInitializeEx(0, COINIT_MULTITHREADED));

     // initialize error handler
     FAILED_THROW(err.ErrorHandlerInit());

     //[C#]  DataSet ds = new DataSet("SomeSet");
     CComPtr<IDataSetObj > ds;
     FAILED_THROW(ds.CoCreateInstance(CLSID_DataSetObj));
     FAILED_THROW(ds->Create(_bstr_t(L"SomeSet")));

     //[C#]  DataTable someTable = new DataTable("SomeTable");
     CComPtr<IDataTableObj > someTable;
     FAILED_THROW(someTable.CoCreateInstance(CLSID_DataTableObj));
     FAILED_THROW(someTable->Create(_bstr_t(L"Some Table")));

     //[C#]  ds.Tables.Add(someTable);
     FAILED_THROW(ds->AddTable(someTable));

     //[C#]  DataTableCollection dtc = ds.Tables;
     //[C#]  IEnumerator en = dtc.GetEnumerator();
     //[C#]  while (en.MoveNext())
     //[C#]  {
     //[C#]      DataTable dt = (DataTable)en.Current;
     //[C#]      Trace.WriteLine(dt.TableName);
     //[C#]  }
     CComPtr<IEnumTableObjs> eto;
     FAILED_THROW(ds->get_EnumTableObjs(&eto));
     long count = 0;
     FAILED_THROW(ds->get_Count(&count));
     for (long i = 0; i < count; i++)
     {
         CComPtr<IDataTableObj> dto;
         HRESULT localhr = eto->Next(1, &dto, NULL);
         if (FAILED(localhr))
         {
             FAILED_THROW(localhr);
         }
         else
         {
         }
         CComBSTR name;
         FAILED_THROW(dto->get_TableName(&name));
         Trace::WriteLine(new System::String(name));
     }

     //[C#]  StringBuilder sb = new StringBuilder();
     //[C#]  for (int i = 0; i < dcnames.Length; i++)
     //[C#]  {
     //[C#]      DataColumn dc = new DataColumn();
     //[C#]      dc.ColumnName = dcnames[i];
     //[C#]      sb.AppendFormat("System.{0}", dctypes[i]);
     //[C#]      dc.DataType = Type.GetType(sb.ToString());
     //[C#]      sb.Length = 0;
     //[C#]      someTable.Columns.Add(dc);
     //[C#]  }
     for (long i = 0; i < 5; i++)
     {
         CComPtr<IDataColumnObj> dc;
         FAILED_THROW(dc.CoCreateInstance(CLSID_DataColumnObj));
         FAILED_THROW(dc->Create(NULL));
         FAILED_THROW(dc->put_ColumnName(_bstr_t(dcnames[i])));
         s << L"System." << dctypes[i];
         FAILED_THROW(dc->put_DataType(_bstr_t(s.str().c_str())));
         FAILED_THROW(someTable->AddColumn(dc));
         s.str(L"");
     }

     //[C#]  en = someTable.Columns.GetEnumerator();
     //[C#]  while (en.MoveNext())
     //[C#]  {
     //[C#]      DataColumn dc = (DataColumn)en.Current;
     //[C#]      sb.AppendFormat("Name: {0}  Type: {1}", dc.ColumnName, dc.DataType);
     //[C#]      Trace.WriteLine(sb.ToString());
     //[C#]      sb.Length = 0;
     //[C#]  }
     CComPtr<IEnumColumnObjs> eco;
     FAILED_THROW(someTable->get_EnumColumnObjs(&eco));
     count = 0;
     FAILED_THROW(someTable->get_CountColumns(&count));
     for (long i = 0; i < count; i++)
     {
         CComPtr<IDataColumnObj> dco;
         HRESULT localhr = eco->Next(1, &dco, NULL);
         if (FAILED(localhr))
         {
             FAILED_THROW(localhr);
         }
         else
         {
         }
         CComBSTR name;
         FAILED_THROW(dco->get_ColumnName(&name));
         CComBSTR type;
         FAILED_THROW(dco->get_DataType(&type));
         s << L"Name: " << (const wchar_t *)name << L"  Type: " << (const wchar_t *)type << std::endl;
         ::OutputDebugString(s.str().c_str());
         s.str(L"");
     }

     //[C#]  DataRow row = someTable.NewRow();
     //[C#]  row["X"] = 1.1;
     //[C#]  row["Y"] = 2.2;
     //[C#]  row["Z"] = 3.3;
     //[C#]  row["DCRA"] = 4;
     //[C#]  row["Mask"] = 5;
     //[C#]  someTable.Rows.Add(row);
     //[C#]  someTable.AcceptChanges();
     CComPtr<IDataRowObj> row;
     FAILED_THROW(row.CoCreateInstance(CLSID_DataRowObj));
     FAILED_THROW(row->Create(someTable));
     FAILED_THROW(row->SetItem(_bstr_t(L"X"), _variant_t(1.1)));
     FAILED_THROW(row->SetItem(_bstr_t(L"Y"), _variant_t(2.2)));
     FAILED_THROW(row->SetItem(_bstr_t(L"Z"), _variant_t(3.3)));
     FAILED_THROW(row->SetItem(_bstr_t(L"DCRA"), _variant_t((long)4)));
     FAILED_THROW(row->SetItem(_bstr_t(L"Mask"), _variant_t((long)5)));
     FAILED_THROW(someTable->AddRow(row));
     FAILED_THROW(someTable->AcceptChanges());
     row = NULL;

     <snip...>

All of the DataSet objects are individual COM objects that I wrapped. I think you get the basic gist of it...

The RCWs and CCWs don't require a great deal of knowledge to use. In fact, here's what you basically do:
  • Register your COM object(s) in the registry with regsvr.exe.
  • Reference the same said assembl(y)(ies) in your .NET project
Then use the code similar to the way that the commented C# code demonstrates.

Hopefully that helps.
QuestionA few feature requests ... Pin
Youn-seong Cho18-May-23 20:38
Youn-seong Cho18-May-23 20:38 
QuestionMCI - outdated? Pin
graphic equaliser25-Sep-12 4:34
graphic equaliser25-Sep-12 4:34 
AnswerRe: MCI - outdated? Pin
dinh van nguyen10-Jul-13 21:36
dinh van nguyen10-Jul-13 21:36 
GeneralMy vote of 1 Pin
hoseinhero12-Jun-12 5:20
hoseinhero12-Jun-12 5:20 
GeneralRe: My vote of 1 Pin
GµårÐïåñ13-Mar-23 9:42
professionalGµårÐïåñ13-Mar-23 9:42 
GeneralNice Pin
Rahul Rajat Singh27-Apr-12 0:37
professionalRahul Rajat Singh27-Apr-12 0:37 
GeneralRe: Nice Pin
Shao Voon Wong1-May-12 16:26
mvaShao Voon Wong1-May-12 16:26 

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.