Windows MCI Player in C++ CLI – DEVELOPPARADISE
31/01/2018

Windows MCI Player in C++ CLI

Download source code

Download Velvet Player

Windows MCI Player in C++ CLI

Introduction

This is a simple music player that uses the Media Control Interface (MCI) to play songs. MCI is an easy-to-use and powerful interface. I did not found good examples of C++/CLI implementations of MCI, so I decided to write this article. However, if you find interesting ideas here, it should be easy to port it to C#.

Background

The documentation for MCI can be found at MDSN.

There are several good articles with C# implementations of MCI, like Simple MCI Player and Media Player With MCI.

This project also makes use of the id3lib, an open-source library for reading/writing ID3 tags.

The Environment

The IDE used is Visual Studio 2015, but should be compatible with other editions. The project type is Windows Forms.

It is very important to understand the configurations properties of the project you are working. They can be set by right-clicking your project and choosing properties, or hitting ALT+F7.

This project uses Common Language Runtime Support (/clr) (because we are going to use native code as well), Multi-Byte Character Set and no Pre-compiled headers:

The targeted .NET framework version is 4.5.2, which is now the default for Visual Studio 2015. The project started using .NET 2.0 for maximum compatibility, but after a couple years I decided to use the newer versions of the framework. It is still possible to target previous versions by editing the .vcxproj file with an external editor like Notepad++. Here is the line you should look at:

<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>

Importing Libaries

I like to implicit link the external libraries for two main reasons: a) it’s easier, in the sense that you’ll have to write less (or none at all) code to import the libraries, and b) Windows warns you, when you run the program, if a required DLL is not found in your computer.

Importing MCI functions

This is the easy part. Since MCI is already a part of the windows API, you just need to do two things:
1) Include “windows.h”, so the compiler will know the prototype of the MCI functions you are going to use;
2) Tell the linker where to search for this functions by adding winmm.lib to Additional Dependencies, in your Project Properties (ALT+F7)->Linker->Input.
 

Importing id3lib functions

First, we need to download the Windows binaries(.dll and .lib) and the source code (just because we need the headers) from the id3lib’s download page. I placed the header in a folder named ‘inc’ and the object file library in a folder named ‘lib’.

Now, we must tell the compiler and the linker where they should look to find the definitions and the body of the functions of the library. This is done by going to Project Properties (ALT+F7)->C/C++ ->General and adding ../inc;../inc/id3 to Additional Include Directories.

After that, we need to add ../lib to Additional Include Directories in Project Properties (ALT+F7)->Linker->Input. And, of course, add id3lib.lib as dependence, like we did for winmm.lib
 

Dependency Walker

This is what Windows shows when you inspect the released executable with Dependency Walker. Both WINMM.DLL and ID3LIB.DLL are listed as dependencies. In the picture, it is also possible to see which functions from WINMM.DLL are used by our player: mciGetErrorString and mciSendString.

Windows MCI Player in C++ CLI

WINMM.DLL is part of Windows and you don’t have to worry about it. But make sure you place the id3lib in C:/Windows/System32 or in the same folder as the application. Otherwise, Windows will show you a message like that: “The program can’t start because id3lib.dll is missing from your computer”.

 

Using the code

There are three important classes in these project that are worth talking about.

First of all, there is the MainForm (main.h), a class which implements all graphical elements for the main window of VelvetPlayer. It also contains some logic for managing the playlist items, reading information (ID3 tags) from the MP3 files, shuffling and saving the session for future launches.

MCI_interface ( MCI_interface.c, .h) is a native C++ class for interfacing with MCI. It has a modular design so it can be used in other projects, there is nothing in it that is specific to the VelvetPlayer application.

Edit_ID3 (Edit_ID3.h) is a form that allows the user to edit some information stored in the MP3 file as tags. If the song information stored in the file, like the artist or the album, is incomplete or inaccurate, the user can modify it. It will also show the album art, if it is available as a tag.

Project files structure:

The project is organized into four folders:
build: output directory for both Release and Debug configurations.
inc: directory for additional third-party includes.
lib: directory for additional third-party-dependencies.
source: everything else, such as source files (.h, .cpp), project files (.sln, .vcxproj) and non-compiled resources.

The MCI Interface Class

This is quite simple and was the start of project. After it, I got creative and started adding cool functionalities that I think a music player shoud have.

Class definition
class CMCI_interface { public:    CMCI_interface(char* szAlias = NULL);    ~CMCI_interface();     DWORD Open(char* szFile, bool bCanSeek);    DWORD Close();    DWORD Play();    DWORD Pause();    DWORD Resume();    DWORD Stop();    DWORD SetVolume(int iVolume);    char* GetCurrentSong();    DWORD GetSongLength(int* iMinutes, int* iSeconds);    DWORD GetSongLength(int* iMiliSeconds);    DWORD GetCurrentPosition(int* iMinutes, int* iSeconds);    DWORD GetCurrentPosition(int* iMiliSeconds);    DWORD isPlaying();    DWORD Seek(int iMiliSeconds);  public:    int m_iCurrentSongMinLen;    int m_iCurrentSongSecLen;  private:    char m_szAlias[30];    char m_szCurrentSong[220];    bool m_bCanSeek; };
Class constructor

You can construct an MCI_interface object with no parameters or an optional alias.

MCI = new CMCI_interface("velvet");
CMCI_interface::CMCI_interface(char* szAlias) : m_bCanSeek(false),   m_iCurrentSongMinLen(0),   m_iCurrentSongSecLen(0) {    if ( NULL != szAlias && strlen(szAlias) < 30 )    {       strcpy_s(m_szAlias, sizeof(m_szAlias), szAlias);    }    else    {       sprintf_s(m_szAlias, sizeof(m_szAlias), "MCI_App");    } }  CMCI_interface::~CMCI_interface() {  }
Pulic functions

The Open function receives a c-stryle string with the path for a song and also a boolean indicating if this song can be seeakable (more on that later, when we’ll see that MCI handles badly VBR-encoded MP3s).

This function makes three calls to mcisendstring:

  • String command open will initialize the device.
  • After calling set time format to milliseconds, all commands that use position values will assume milliseconds.
  • Finally, set seek exactly on makes seek always move to the frame specified.
DWORD CMCI_interface::Open(char* szFile, bool bCanSeek) {    DWORD dwRet = 0;    char szCmd[MCI_BUFFER_LEN];     // Open    sprintf_s( szCmd, sizeof(szCmd), "open /"%s/" alias %s", szFile, m_szAlias );    mciSendString( szCmd, NULL, 0, NULL );    if ( dwRet != 0)    {       return dwRet;    }     // Set time format    sprintf_s( szCmd, sizeof(szCmd), "set %s time format to milliseconds", m_szAlias );    mciSendString( szCmd, NULL, 0, NULL );    if ( dwRet != 0)    {       return dwRet;    }     // Set seek    sprintf_s( szCmd, sizeof(szCmd), "set %s seek exactly on", m_szAlias );    mciSendString( szCmd, NULL, 0, NULL );    if ( dwRet != 0)    {       return dwRet;    }     // Set as current song    strcpy_s(m_szCurrentSong, sizeof(m_szCurrentSong), szFile);     // In these variable, store if the user can seek through  the song    m_bCanSeek = bCanSeek;     // Call GetSongLength to update class variables (m_iCurrentSongMinLen and m_iCurrentSongSecLen)    GetSongLength(NULL);     return 0; }

 

Other commands, like close, play, pause, resume, stop, setaudio volume, are pretty straightforward. Many of them don’t have parameters. The SetVolume function expecets integeres in the range of 0 to 1000:

DWORD CMCI_interface::Close(void) {    char szCmd[MCI_BUFFER_LEN];    sprintf_s( szCmd, sizeof(szCmd), "close %s", m_szAlias  );     // Erase current song    strcpy_s(m_szCurrentSong, sizeof(m_szCurrentSong), "/0");     return mciSendString( szCmd, NULL, 0, NULL ); }  DWORD CMCI_interface::Play(void) {    char szCmd[MCI_BUFFER_LEN];    sprintf_s( szCmd, sizeof(szCmd), "play %s", m_szAlias );    return mciSendString( szCmd, NULL, 0, NULL ); }  DWORD CMCI_interface::Pause(void) {    char szCmd[MCI_BUFFER_LEN];    sprintf_s( szCmd, sizeof(szCmd), "pause %s", m_szAlias );    return mciSendString( szCmd, NULL, 0, NULL ); }  DWORD CMCI_interface::Resume(void) {    char szCmd[MCI_BUFFER_LEN];    sprintf_s( szCmd, sizeof(szCmd), "resume %s", m_szAlias );    return mciSendString( szCmd, NULL, 0, NULL ); }  DWORD CMCI_interface::Stop(void) {    char szCmd[MCI_BUFFER_LEN];    sprintf_s( szCmd, sizeof(szCmd), "stop %s", m_szAlias );    return mciSendString( szCmd, NULL, 0, NULL ); }  DWORD CMCI_interface::SetVolume(int iVolume) {    char szCmd[MCI_BUFFER_LEN];    sprintf_s( szCmd, sizeof(szCmd), "setaudio %s volume to %u", m_szAlias, iVolume);    return mciSendString( szCmd, NULL, 0, NULL ); }

 

The status mode string command returns “playing” when a song is currently playing:

DWORD CMCI_interface::isPlaying() {    DWORD dwRet = 0;    char szCmd[MCI_BUFFER_LEN]      = {0};    char szResponse[MCI_BUFFER_LEN] = {0};    sprintf_s( szCmd, sizeof(szCmd), "status %s mode", m_szAlias );    dwRet =  mciSendString( szCmd, szResponse, sizeof(szResponse), NULL );     if ( 0 == memcmp(szResponse, "playing", 7) )    {       return 1;    }    else    {       return 0;    } }

 

As said before, commands that use position values will assume milliseconds, so we have two polymorphic functions for retrieving the current position of the song:

DWORD CMCI_interface::GetCurrentPosition(int* iMinutes, int* iSeconds) {    DWORD dwRet = 0;    char szCmd[MCI_BUFFER_LEN]      = {0};    char szResponse[MCI_BUFFER_LEN] = {0};    sprintf_s( szCmd, sizeof(szCmd), "status %s position", m_szAlias );     dwRet = mciSendString( szCmd, szResponse, sizeof(szResponse), NULL );      if ( 0 == dwRet )    {       *iSeconds = (iMiliSeconds / 1000) % 60;       *iMinutes = (iMiliSeconds / 1000) / 60;    }    int iMiliSeconds = atoi(szResponse);     return dwRet; }  DWORD CMCI_interface::GetCurrentPosition(int* iMiliSeconds) {    DWORD dwRet = 0;    char szCmd[MCI_BUFFER_LEN]      = {0};    char szResponse[MCI_BUFFER_LEN] = {0};    sprintf_s( szCmd, sizeof(szCmd), "status %s position", m_szAlias );     dwRet =  mciSendString( szCmd, szResponse, sizeof(szResponse), NULL );    *iMiliSeconds = atoi(szResponse);     return dwRet; }

 

The Seek function has two scenarios. The user might try to seek a song that is already playing or start another song from a particular position:

DWORD CMCI_interface::Seek(int iMiliSeconds) {    char szCmd[MCI_BUFFER_LEN] = {0};     if ( false == m_bCanSeek )    {       return NO_SEEKING_ALLOWED;    }     if ( 1 == isPlaying() )    {       sprintf_s( szCmd, sizeof(szCmd), "play %s from %d", m_szAlias, iMiliSeconds);    }    else    {       sprintf_s( szCmd, sizeof(szCmd), "seek %s to %d", m_szAlias, iMiliSeconds);    }     return mciSendString( szCmd, NULL, 0, NULL ); }

 

The GetSongLength function is the tricky one. After a while, I discovered that that MCI will return an incorrect length for the songs encoded with variable bit rate. But this is not the only problem: the seeking for a VBR-encoded song will be horribly inaccurate as well (that’s the reason why Seek function checks to see if the user is allowed to seek through a particular song).

Instead, we are going to use id3lib’s GetMp3HeaderInfo function, which correctly retrieves the length, and we’ll use MCI only in the cases of tracks withoud the ID3 tag (which is weird, but might happen).

DWORD CMCI_interface::GetSongLength(int* iSeconds) {    // ***************************************************************************************************************    // This MCI function may return the wrong length if the mp3 encoded with VBR (variable bit rate)    // See http://forums.codeguru.com/showthread.php?456663-mciSendString%28-quot-status-ALIAS-length-quot-%29-returns-wrong-value    // and https://en.wikipedia.org/wiki/Variable_bitrate    // ***************************************************************************************************************     // ***************************************************************************************************************     if ( strlen(m_szCurrentSong) == 0 )    {       return 2;    }     // Instead, we are going to use id3lib    DWORD dwRet = 0;    int iSecondsAux = 0;     ID3_Tag myTag(m_szCurrentSong);    const Mp3_Headerinfo* mp3info;    if ((mp3info = myTag.GetMp3HeaderInfo()) != NULL)    {        // If we have the information from ID3 lib, use it       if ( 0 != mp3info->time )       {          iSecondsAux = mp3info->time;       }       else       {          // Error with the ID3 tag, so let's use MCI          char szCmd[MCI_BUFFER_LEN]      = {0};          char szResponse[MCI_BUFFER_LEN] = {0};          sprintf_s( szCmd, sizeof(szCmd), "status %s length", m_szAlias);          dwRet = mciSendString( szCmd, szResponse, sizeof(szResponse), NULL );           int iMiliSeconds = atoi(szResponse);          iSecondsAux      = (iMiliSeconds / 1000) + 1; // add 1, no harm done in rounding up       }        m_iCurrentSongMinLen = iSecondsAux / 60;       m_iCurrentSongSecLen = iSecondsAux % 60;        if ( NULL != iSeconds )       {          *iSeconds = iSecondsAux;       }    }    else    {       dwRet = 1;    }     return dwRet; }

The Main Form

Implements the graphical interface and some logic for playing, shuffling songs and reading ID3 tags. This is a screenshot of the original GUI, which has been redesigned in November, 2017.

Windows MCI Player in C++ CLI

Graphical Interface

The key element in the VelvetPlayer graphical interface is a ListView, that we will be referring to as “playlist” from now on.

The form also contains:
– A menustrip, which holds menus and items;
– 5 buttons for playback ( play, pause, stop, previous, next);
– A text box (SongPosition TextBox) that displays the current position of the song in minutes and seconds;
– A trackbar (SongProgress TrackBar) that displays the progress of the song and can be scrolled;
– Another trackbar for setting the volume of the player.
 

Tips for converting between C++/CLI and C

Many times we are going to need to convert C-style string to managed String^ and vice-versa (I try to avoid using std::string while writing code in C++/CLI in order not to make an even bigger mess). This happens a lot when we use libraries written in C, like the id3lib.

It is straightforward to convert a c-style string into a managed String:

char* cString = "I am a c-style string"; String^ strExample = gcnew String(cString);

The other way around is not so obvious. First, make sure to declare use of the following namespace:

using namespace System::Runtime::InteropServices;

Then, to convert from a managed String to a c-style string:

String^ strExample = "A managed string"; char* cString = (char*)(void*)Marshal::StringToHGlobalAnsi( strExample );
Adding Songs

There are two ways to add songs to the playlist.

Via an OpenFileDialog, the user can browse folders and select multiple MP3 songs at once.

private: System::Void openToolStripMenuItem2_Click(System::Object^  sender, System::EventArgs^  e) {    if (openFileDialog->ShowDialog() == System::Windows::Forms::DialogResult::OK)    {       // How many items were in the list before the user clicked open?       int iBefore = this->playlist->Items->Count;        // Add songs to the playlist       for each (String^ file in openFileDialog->FileNames)       {          AddSongToPlaylist(file);       }        // How many are in the playlist now?       int iAfter = this->playlist->Items->Count;        // Focus on the first item added at this time (or the first item at all if nothings was there before)       // Since the items index begins at position 0 and the count starts on 1, we don't need to add 1       // Eg.: if there were 3 items(indexes 0,1,2), the next item will be at index 3       this->playlist->Items[iBefore]->Selected = true;       this->playlist->Items[iBefore]->Focused  = true;        // Inform user       SetStatusLabel( (iAfter - iBefore).ToString() + " songs added");        // Create a new shuffle list       ShuffleSongs();        // Now that we added some files, resize the colums       ResizePlaylistColumns();        // Save session       SaveSession();    }    else    {       SetStatusLabel("");    } }

Alternatively, the user can select a folder via a FolderBrowserDialog. Next thing, he will be prompted if the player should also look inside sub-folders recursively (this can be slow, there’s a MessageBox warning the user of so).

private: System::Void openFolderToolStripMenuItem_Click(System::Object^  sender, System::EventArgs^  e) {    if (folderBrowserDialog->ShowDialog() == System::Windows::Forms::DialogResult::OK)    {       // How many items were in the list before the user browsed a folder?       int iBefore = this->playlist->Items->Count;        // Prompt the user if shoud look inside sub-directories       if ( System::Windows::Forms::DialogResult::Yes == MessageBox::Show( "Include sub-folders? This may take a few minutes", "Recursive?", MessageBoxButtons::YesNo, MessageBoxIcon::Question) )       {          // Add all MP3 files on the selected folder and its sub-directories          AddFolderToPlaylist(folderBrowserDialog->SelectedPath, true);       }       else       {          // Add all MP3 files on the selected folder          AddFolderToPlaylist(folderBrowserDialog->SelectedPath, false);       }        // How many are in the playlist now?       int iAfter = this->playlist->Items->Count;        // If there was a change, focus on the first new item       if ( iAfter > iBefore )       {          this->playlist->Items[iBefore]->Selected = true;          this->playlist->Items[iBefore]->Focused  = true;           // Inform user          SetStatusLabel( (iAfter - iBefore).ToString() + " songs added");           // Create a new shuffle list          ShuffleSongs();           // Now that we added some files, resize the colums          ResizePlaylistColumns();           // Save session          SaveSession();       }       else       {          SetStatusLabel("");       }    } }  private: System::Void AddFolderToPlaylist(String^ szFolder, bool bRecursive) {    try    {       // Process the list of files found in the directory.       array<String^>^fileEntries = Directory::GetFiles( szFolder );       IEnumerator^ files = fileEntries->GetEnumerator();       while ( files->MoveNext() )       {          String^ fileName = safe_cast<String^>(files->Current);           // Only add if it is an MP3-encoded song          if (Path::GetExtension(fileName) == ".mp3")          {             AddSongToPlaylist(fileName);          }       }        if ( bRecursive )       {          // Recurse into subdirectories of this directory.          array<String^>^subdirectoryEntries = Directory::GetDirectories( szFolder );          IEnumerator^ dirs = subdirectoryEntries->GetEnumerator();          while ( dirs->MoveNext() )          {             String^ subdirectory = safe_cast<String^>(dirs->Current);             AddFolderToPlaylist( subdirectory, bRecursive );          }       }    }    catch(...)    {       // Bad permission?    } }

Both approches make use of the AddSongToPlaylist function, which creates a new item for the playlist, uses id3lib to read information from the song and adds that information as sub-items of the playlist, like Artist, Song title and Album.

private: System::Void AddSongToPlaylist(String^ szSongToAdd) {    ListViewItem^ item = gcnew ListViewItem(szSongToAdd, 0 );    item->Text         = Path::GetDirectoryName(szSongToAdd);    item->SubItems->Add( Path::GetFileName(szSongToAdd) );     // Read the id3 tag    char* szMP3 = (char*)(void*)Marshal::StringToHGlobalAnsi( szSongToAdd );    ID3_Tag myTag(szMP3);     // Artist    ID3_Frame* frameArtist = myTag.Find(ID3FID_LEADARTIST);    if (NULL != frameArtist)    {       char szArtist[128] = {0};       frameArtist->GetField(ID3FN_TEXT)->SetEncoding(ID3TE_ISO8859_1);       frameArtist->GetField(ID3FN_TEXT)->Get(szArtist, sizeof(szArtist));       item->SubItems->Add( gcnew String(szArtist) );    }    else    {       item->SubItems->Add( "Unknown" );    }     // Song    ID3_Frame* frameTitle = myTag.Find(ID3FID_TITLE);    if (NULL != frameTitle)    {       char szSong[128] = {0};       frameTitle->GetField(ID3FN_TEXT)->SetEncoding(ID3TE_ISO8859_1);       frameTitle->GetField(ID3FN_TEXT)->Get(szSong, sizeof(szSong));       item->SubItems->Add( gcnew String(szSong) );    }    else    {       item->SubItems->Add( "Unknown" );    }     // Album    ID3_Frame* frameAlbum = myTag.Find(ID3FID_ALBUM);    if (NULL != frameAlbum)    {       char szAlbum[128] = {0};       frameAlbum->GetField(ID3FN_TEXT)->SetEncoding(ID3TE_ISO8859_1);       frameAlbum->GetField(ID3FN_TEXT)->Get(szAlbum, sizeof(szAlbum));       item->SubItems->Add( gcnew String(szAlbum) );    }    else    {       item->SubItems->Add( "Unknown" );    }     //Length    const Mp3_Headerinfo* mp3info;    if ((mp3info = myTag.GetMp3HeaderInfo()) != NULL)    {       item->SubItems->Add( String::Format("{0:00}", mp3info->time / 60)                            + ":"                            + String::Format("{0:00}", mp3info->time % 60) );    }    else    {       item->SubItems->Add( "Unknown" );    }     this->playlist->Items->Add(item);     // We do not need to call ResizePlaylistColumns() here, the caller is responsible for that }

 

Reading MP3 Tags

Let’s explain the use of id3lib in more detail. First of all, you need to declare an ID3_Tag variable:

ID3_Tag myTag("path-to-mp3-song");

Another option is to declare the ID3_Tag and link it to a file in two steps:

ID3_Tag myTag; if ( myTag.Link("path-to-mp3-song") <= 0) {    return; }

After that, we search for the frame that we want:

ID3_Frame* frameArtist = myTag.Find(ID3FID_LEADARTIST); if (NULL != frameArtist) {    //... }

Finally, we get the field. Fields can represent text, numbers or binary data. Because we know the field type we want, we can access it directly like this:

ID3_Field* myField = frameArtist->GetField(ID3FN_TEXT); if ( NULL != myField ) {    char szArtist[128] = {0};    myField->SetEncoding(ID3TE_ISO8859_1);    myField->Get(szArtist, 128); }

It is important to set the encoding! I was getting strange information for some songs before I set it like above.

It’s also possible to access the fields in a slightly shorter way:

ID3_Frame* frameArtist = myTag.Find(ID3FID_LEADARTIST); if (NULL != frameArtist) {    frameArtist->GetField(ID3FN_TEXT)->SetEncoding(ID3TE_ISO8859_1);    frameArtist->GetField(ID3FN_TEXT)->Get(szArtist, sizeof(szArtist)); }
Shuffling

My first idea was to just pick a random number (from 1 to n, where n is the number of songs) and play the correspondent song in the playlist. However, there are too caveats with this approach. First, System::Random is not a great pseudo-random-number-generator(PRNG) and second, nothing would prevent a song on the list from being played twice or more before all other songs on the playlist got played once.

After reading this article, I decided to use the modern Fisher-Yates shuffle algorithm, which can be written in a couple lines:

// Pick up random numbers private: static Random^ rnd = gcnew Random();

The songs are stored in a List<T> Class object.

delete L_Songs; L_Songs = gcnew System::Collections::Generic::List<int>();  for( int i = 0; i < playlist->Items->Count; i++ ) {    L_Songs->Add(i); }  // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Shuffle the List usign the modern Fisher–Yates shuffle algorithm // https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - for( int i = 0; i < playlist->Items->Count - 2; i++ ) {    int j = rnd->Next(i, playlist->Items->Count);    int aux = L_Songs[i];     L_Songs[i] = L_Songs[j];    L_Songs[j] = aux; }

We end up with a shuffled list of n elements to play through if the user enabled the shuffle mode. Just remember, we should create a new list every time a song is added or removed from the playlist.

Playing, pausing, next

There are three states in which the player can be:

typedef enum EState_tag {    STOPPED = 0,    PAUSED,    PLAYING } EState;

The paused state differs from the stopped state in that it holds the information about the song that is playing. The song can be either resumed, if the user presses the pause button again, or started from the beggining, if the user hits the play button.
 

The play function needs to set a bunch of stuff besides calling MCI to start playing. It must check if it is a VBR-encoded MP3, set the volume (held by the Volume Trackbar) and set the length of the song, both in the SongProgress trackbar and in the SongPosition TextBox

private: System::Void play(char* szFile) {    bool bCanSeek = true;    ID3_Tag myTag(szFile);    const Mp3_Headerinfo* mp3info;    if ( ( mp3info = myTag.GetMp3HeaderInfo() ) == NULL )    {       SetStatusLabel("There was a problem getting MP3 header info for selected song");       return;    }     // If the song was encoded using variable bit rate (VBR), we cannot seek through the song because MCI will get confused    long lVBR = mp3info->vbr_bitrate;    if ( 0 != lVBR )    {       bCanSeek = false;    }     // First, we open    DWORD dwRet = MCI->Open(szFile, bCanSeek);    if ( 0 != dwRet )    {       char buffer[1024] = {0};       mciGetErrorString(dwRet, (LPSTR)buffer, sizeof(buffer));       SetStatusLabel("Error MCI->Open (" + dwRet.ToString() + "): " + gcnew String(buffer));       return;    }     // Then, we play    dwRet = MCI->Play();    if ( 0 != dwRet )    {       char buffer[1024] = {0};       mciGetErrorString(dwRet, (LPSTR)buffer, sizeof(buffer));       SetStatusLabel("Error MCI->Play (" + dwRet.ToString() + "): " + gcnew String(buffer));       return;    }    else    {       m_State = PLAYING;       timer->Enabled = true;    }     // Set volume    dwRet = MCI->SetVolume(trbVolume->Value);    if ( 0 != dwRet )    {       SetStatusLabel("Error MCI->SetVolume (" + dwRet.ToString() + ")");       return;    }     // Set trackbar    int iSeconds = 0;    dwRet = MCI->GetSongLength(&iSeconds);    if ( 0 != dwRet )    {       SetStatusLabel("Error MCI->GetSongLength (" + dwRet.ToString() + ")");       return;    }    else    {       // Since GetCurrentPosition returns in milliseconds, we must do the same here       trbSong->Maximum = iSeconds*1000;    }     // Set song length    this->tbPosition->Text = "00:00 / " + String::Format("{0:00}", MCI->m_iCurrentSongMinLen) + ":" + String::Format("{0:00}", MCI->m_iCurrentSongSecLen);     // Update status label    SetStatusLabel("Playing: " + Path::GetFileName(gcnew String(szFile)) );  }

 

The stop and pause functions are way simpler:

private: System::Void resume() {    // Resume    DWORD dwRet = MCI->Resume();    if ( 0 != dwRet )    {       char buffer[1024] = {0};       mciGetErrorString(dwRet, (LPSTR)buffer, sizeof(buffer));       SetStatusLabel("Error MCI->Open (" + dwRet.ToString() + "): " + gcnew String(buffer));       return;    }    else    {       m_State = PLAYING;    }     // Update status label    SetStatusLabel("Resuming: " + Path::GetFileName( gcnew String( MCI->GetCurrentSong() ) ) ); }  private: System::Void stop() {    // First, we stop    DWORD dwRet = MCI->Stop();    if ( 0 != dwRet)    {       // There might be no song playing, so we don't set the status label here       return;    }     // Then, we close    dwRet = MCI->Close();    if ( 0 != dwRet )    {       // There might be no song opened, so we don't set the status label here       return;    }     // Set trackbar to zero    trbSong->Value = 0;     // Set track length to zero    this->tbPosition->Text = L"00:00 / 00:00";     m_State = STOPPED;     SetStatusLabel(""); }

 

The next and previous funtions must consider two scenarios:
When the shuffle is on, they must select the next (or previous) item on the shuffled list. When in non-shuffle mode, they select the next (or previous) item on the playlist.

In the case we reach the the last (or the first) position in either of the lists, we simply start over as it were a “circular” list.

private: System::Void next() {    try    {       int iCurrentSongIndex  = this->playlist->FocusedItem->Index;       int iCount             = this->playlist->Items->Count;        if ( false == g_bShuffle )       {          // Select next song on the playlist          if (iCurrentSongIndex + 1 < iCount )          {             this->playlist->Items[iCurrentSongIndex+1]->Selected = true;             this->playlist->Items[iCurrentSongIndex+1]->Focused = true;          }          else          {             this->playlist->Items[0]->Selected = true;             this->playlist->Items[0]->Focused = true;          }       }       else       {          // Get the index of the current song on the shuffle list          int iToPlay = L_Songs->IndexOf(iCurrentSongIndex);          if ( iToPlay < 0)          {             // Some error happened.             SetStatusLabel("Error shuffling");             this->playlist->Items[0]->Selected = true;             this->playlist->Items[0]->Focused = true;          }          else          {             // Selected next song on the shuffle list             iToPlay++;             if ( iToPlay >= L_Songs->Count )             {                // It was the last song of the shuffle list, so start again                iToPlay = 0;             }             this->playlist->Items[ L_Songs[iToPlay] ]->Selected = true;             this->playlist->Items[ L_Songs[iToPlay] ]->Focused = true;          }       }        // Play selected song       char szFile[256];       sprintf_s( szFile, sizeof(szFile), "%s//%s", this->playlist->SelectedItems[0]->SubItems[0]->Text,                                                    this->playlist->SelectedItems[0]->SubItems[1]->Text);       play(szFile);    }    catch(...)    {       SetStatusLabel("No song selected on playlist.");    } } private: System::Void previous() {    try    {       int iCurrentSongIndex = this->playlist->FocusedItem->Index;       int iCount            = this->playlist->Items->Count;        if ( false == g_bShuffle )       {          // No shuffling here, select previous song on the playlist          if (iCurrentSongIndex > 0)          {             this->playlist->Items[iCurrentSongIndex-1]->Selected = true;             this->playlist->Items[iCurrentSongIndex-1]->Focused = true;          }          else          {             this->playlist->Items[iCount-1]->Selected = true;             this->playlist->Items[iCount-1]->Focused = true;          }       }       else       {          // Get the index of the current song on the shuffle list          int iToPlay = L_Songs->IndexOf(iCurrentSongIndex);          if ( iToPlay < 0)          {             // Some error happened.             SetStatusLabel("Error shuffling");             this->playlist->Items[0]->Selected = true;             this->playlist->Items[0]->Focused = true;          }          else          {             // Selected next song on the shuffle list             iToPlay--;             if ( iToPlay < 0)             {                // It was the last song of the shuffle list, so start again                iToPlay = L_Songs->Count - 1;             }             this->playlist->Items[ L_Songs[iToPlay] ]->Selected = true;             this->playlist->Items[ L_Songs[iToPlay] ]->Focused = true;          }       }        // Play selected song       char szFile[256];       sprintf_s( szFile, sizeof(szFile), "%s//%s", this->playlist->SelectedItems[0]->SubItems[0]->Text,                                                    this->playlist->SelectedItems[0]->SubItems[1]->Text);       play(szFile);    }    catch(...)    {       SetStatusLabel("No song selected on playlist.");    } }

 

There is also a Timer object, which raises events with interval of 200ms. It has mainly two tasks:
– Update the SongTime TextBox and SongProgress Trackbar;
– Detectet if the song had reached the end; if so, go to next one.

private: System::Void timer_Tick(System::Object^  sender, System::EventArgs^  e) {    int iMinutes = 0;    int iSeconds = 0;    int iCurrentMiliSecond = 0;     if ( m_State == PLAYING )    {       // Still playing the song?       if ( MCI->isPlaying() )       {          // Still playing, so update trackbar and textbox          DWORD dwRet = MCI->GetCurrentPosition(&iMinutes, &iSeconds);          if ( 0 != dwRet )          {             return;          }          else          {             this->tbPosition->Text = String::Format("{0:00}", iMinutes) + ":" + String::Format("{0:00}", iSeconds) + " / " + String::Format("{0:00}", MCI->m_iCurrentSongMinLen) + ":" + String::Format("{0:00}", MCI->m_iCurrentSongSecLen) ;          }           dwRet = MCI->GetCurrentPosition(&iCurrentMiliSecond);          if ( 0 != dwRet )          {             return;          }          else          {             trbSong->Value = iCurrentMiliSecond;          }       }       else       {          // This song has finished, go to the next          stop();          next();           // Sleep a bit, so that MCI can start playing before we check again          System::Threading::Thread::Sleep(5);       }    } }

 

Saving user session in disk

It’s a good idea to remember the user’s preferences for the next launch, right? A simple way to do that is writing to a text file (velvetplayer.dat, created in the same directory where the application is run) in the following format
– Path to all songs that are in the playlist, one by line;
– A sequence of “##########” indicating the end of songs;
– Either “true” or “false” indicating the shuffle state;
– Another “true” or “false” indicating if the auto-size columns feature is turned on;
– 32-bit ARGB value representing the foreground color of the playlist items;
– A string representation of the font family, size and style used in the playlist.

SaveSession must be called every time any of the configurations is changed.

#define SESSION_FILE "velvetplayer.dat"
private: System::Void SaveSession() {    // Use .NET StreamWriter class to write to the file    try    {       StreamWriter^ sw = gcnew StreamWriter(SESSION_FILE);       int iCount = this->playlist->Items->Count;        for( int i = 0; i < iCount; i++)       {          String^ str;          try          {             str = gcnew String(this->playlist->Items[i]->SubItems[0]->Text ) + "//"                +  gcnew String(this->playlist->Items[i]->SubItems[1]->Text );              // Add this song to the file             sw->WriteLine(str);          }          catch(...)          {             SetStatusLabel("Something went wrong with item: " + iCount.ToString());             return;          }       }        // The separator       sw->WriteLine("##########");        // Shuffle On of Off       g_bShuffle? sw->WriteLine("true"): sw->WriteLine("false");        // Auto-size columns On or Off       g_bAutosize? sw->WriteLine("true"): sw->WriteLine("false");        // The color       sw->WriteLine(this->playlist->ForeColor.ToArgb());        // Font family and size       String^ toFile = TypeDescriptor::GetConverter( System::Drawing::Font::typeid )->ConvertToString( this->playlist->Font );       sw->WriteLine(toFile);        // Close the streamwriter       sw->Close();    }    catch(Exception^ e)    {       SetStatusLabel("Problem saving session file: " + e->ToString());    } }

 

The RestoreSession function is called only once, inside the form constructor:

public ref class Main : public System::Windows::Forms::Form {    public:       Main(void)       {          InitializeComponent();          MCI = new CMCI_interface("velvet");          RestoreSession();       }    // [...] }

 

It parses the session file in the same order described above. The trickiest part is the font, because first you have to declare a System::ComponentModel::TypeConverter^ object and then convert from String^ to Drawing::Font^.

private: System::Void RestoreSession() {    try    {       StreamReader^ sr = File::OpenText(SESSION_FILE);        // Add the songs       String^ strSong;       while ((strSong = sr->ReadLine()) != nullptr)       {          if ( File::Exists(strSong) && Path::GetExtension(strSong) == ".mp3")          {             AddSongToPlaylist(strSong);          }          else if ( strSong->Contains("###") )          {             // End of songs             break;          }       }        // Shuffle On of Off       String^ strTemp;       if ( (strTemp = sr->ReadLine()) != nullptr)       {          g_bShuffle = Convert::ToBoolean(strTemp);       }        if ( (strTemp = sr->ReadLine()) != nullptr)       {          g_bAutosize = Convert::ToBoolean(strTemp);       }        // Set the foreground color of the playlist;       String^ strForeColor;       if ( (strForeColor = sr->ReadLine()) != nullptr)       {          Color color = Color::FromArgb( Convert::ToInt32(strForeColor) );          this->playlist->ForeColor = color;       }        // Set the font of the playlist       String^ strFont;       if ( (strFont = sr->ReadLine()) != nullptr)       {          System::ComponentModel::TypeConverter^ converter = TypeDescriptor::GetConverter( Drawing::Font::typeid );          Drawing::Font^ font = dynamic_cast<Drawing::Font^>(converter->ConvertFromString(strFont));          this->playlist->Font = font;       }        sr->Close();        // Set shuffling according to saved session       if ( g_bShuffle )       {          shuffleToolStripMenuItem->Checked = true;          ShuffleSongs();       }       else       {          shuffleToolStripMenuItem->Checked = false;       }        // Set auto-sizing according to saved session       if ( g_bAutosize )       {          autosizeColumnsToolStripMenuItem->Checked = true;          ResizePlaylistColumns();       }       else       {          autosizeColumnsToolStripMenuItem->Checked = false;       }    }    catch(FileNotFoundException^ e)    {       e; // prevent the warning    }    catch(Exception^ e)    {       SetStatusLabel( e->ToString() );    } }

 

Edit ID3 Form

Lets the user edit information for a particular song and displays the album art, when it is available.

Windows MCI Player in C++ CLI

Editing information for a song

After being constructed, the Edit ID3 Form must receive the existing information for the song, so it can show in the Textboxes:

System::Void setFields(char* szArtist, char* szSong, char* szAlbum, bool bArt ) {    tbArtist->Text = gcnew String(szArtist);    tbSong->Text   = gcnew String(szSong);    tbAlbum->Text  = gcnew String(szAlbum);     if ( bArt )    {       this->pictureBox->BackgroundImage = Image::FromFile(L"tmp_albumart.jpg");       this->pictureBox->BackgroundImageLayout = System::Windows::Forms::ImageLayout::Stretch;       Application::DoEvents();    } }

 

Since the Textboxes are private members of the Edit ID3 Form, we also need a function to get their values:

System::Void getFields(char* szArtist, char* szSong, char* szAlbum ) {    char* szAuxArtist = (char*)(void*)Marshal::StringToHGlobalAnsi(tbArtist->Text );    strcpy_s(szArtist, 128, szAuxArtist);     char* szAuxSong   = (char*)(void*)Marshal::StringToHGlobalAnsi( tbSong->Text );    strcpy_s(szSong, 128, szAuxSong);     char* szAuxAlbum  = (char*)(void*)Marshal::StringToHGlobalAnsi( tbAlbum->Text );    strcpy_s(szAlbum, 128, szAuxAlbum); }

 

This is the code, belonging to the Main Form, that constructs the Edit ID3 Form and updates the information both in the file and in the playlist:

private: System::Void iD3TagToolStripMenuItem_Click(System::Object^  sender, System::EventArgs^  e) {    // String to edit in tag    char szArtist[128];    char szSong[128];    char szAlbum[128];     // Read the id3 tag    char szMP3[MAX_FILE_LEN] = {0};    try    {       sprintf_s( szMP3, sizeof(szMP3), "%s//%s", this->playlist->SelectedItems[0]->SubItems[0]->Text,                                                  this->playlist->SelectedItems[0]->SubItems[1]->Text);    }    catch(...)    {       SetStatusLabel("No song selected on playlist.");       return;    }     ID3_Tag myTag(szMP3);    if ( myTag.GetMp3HeaderInfo() == NULL )    {       SetStatusLabel("There was a problem getting MP3 header info for selected song");       return;    }     // Artist    ID3_Frame* frameArtist = myTag.Find(ID3FID_LEADARTIST);    if (NULL != frameArtist)    {       frameArtist->GetField(ID3FN_TEXT)->SetEncoding(ID3TE_ISO8859_1);       frameArtist->GetField(ID3FN_TEXT)->Get(szArtist, sizeof(szArtist));    }    else    {       sprintf_s(szArtist, sizeof(szArtist), "Unknown");    }     // Song    ID3_Frame* frameTitle = myTag.Find(ID3FID_TITLE);    if (NULL != frameTitle)    {       frameTitle->GetField(ID3FN_TEXT)->SetEncoding(ID3TE_ISO8859_1);       frameTitle->GetField(ID3FN_TEXT)->Get(szSong, sizeof(szSong));    }    else    {       sprintf_s(szSong, sizeof(szSong), "Unknown");    }     // Album    ID3_Frame* frameAlbum = myTag.Find(ID3FID_ALBUM);    if (NULL != frameAlbum)    {       frameAlbum->GetField(ID3FN_TEXT)->SetEncoding(ID3TE_ISO8859_1);       frameAlbum->GetField(ID3FN_TEXT)->Get(szAlbum, sizeof(szAlbum));    }    else    {       sprintf_s(szAlbum, sizeof(szAlbum), "Unknown");    }     // Album art    ID3_Frame* frameAlbumArt = myTag.Find(ID3FID_PICTURE);    bool bArt = false;    if ( NULL != frameAlbumArt && frameAlbumArt->Contains(ID3FN_DATA))    {       frameAlbumArt->Field(ID3FN_DATA).ToFile("tmp_albumart.jpg");       bArt = true;    }     // Construct the Edit_ID3 Form inside this context, so that    // its destructor will be called immediately after we finish updating    {       // Set the fields with the information that is already in the tag       Edit_ID3 editID3_Form;       editID3_Form.setFields(szArtist, szSong, szAlbum, bArt);        // Show the form: if the user presses OK, we update the tag       if ( editID3_Form.ShowDialog() == System::Windows::Forms::DialogResult::OK)       {          editID3_Form.getFields(szArtist, szSong, szAlbum);           if (NULL != frameArtist)          {             // The frame already exists, just update it             frameArtist->GetField(ID3FN_TEXT)->Set(szArtist);          }          else          {             // The ID3FID_LEADARTIST frame doesn't exist, let's create a new one             ID3_Frame frame;             frame.SetID(ID3FID_LEADARTIST);             frame.GetField(ID3FN_TEXT)->Set(szArtist);             myTag.AddFrame(frame);          }           if (NULL != frameTitle )          {             // The frame already exists, just update it             frameTitle->GetField(ID3FN_TEXT)->Set(szSong);          }          else          {             // The ID3FID_TITLE frame doesn't exist, let's create a new one             ID3_Frame frame;             frame.SetID(ID3FID_TITLE);             frame.GetField(ID3FN_TEXT)->Set(szSong);             myTag.AddFrame(frame);          }           if (NULL != frameAlbum )          {             // The frame already exists, just update it             frameAlbum->GetField(ID3FN_TEXT)->Set(szAlbum);          }          else          {             // The ID3FID_ALBUM frame doesn't exist, let's create a new one             ID3_Frame frame;             frame.SetID(ID3FID_ALBUM);             frame.GetField(ID3FN_TEXT)->Set(szAlbum);             myTag.AddFrame(frame);          }           // We are finished, update the tag          myTag.Update();           // Edit in the playlist as well.          this->playlist->SelectedItems[0]->SubItems[2]->Text = gcnew String(szArtist);          this->playlist->SelectedItems[0]->SubItems[3]->Text = gcnew String(szSong);          this->playlist->SelectedItems[0]->SubItems[4]->Text = gcnew String(szAlbum);           SetStatusLabel("Updated");       }    }     // Since the form has been destroyed, we can delete the .jpg    if ( File::Exists(gcnew String("tmp_albumart.jpg") ) )    {       try       {          File::Delete(gcnew String("tmp_albumart.jpg"));       }       catch(Exception^ e)       {          e;          //SetStatusLabel("Unable to delete album art");       }    } }

Using the player

Let’s list the things you can do with the player:

Windows MCI Player in C++ CLI

Keyboard shortcuts:

Open File Alt + O
Open Folder Alt + F
Exit Alt + F4
Edit ID3 tag Alt + E
Remove selected song from playlist Alt + R
Toggle shuffle Alt + S
Remove all songs from playlist Alt + A
Show Path Column (Only accessible by shortcut) Alt + 1
Show Filename Column (Only accessible by shortcut) Alt + 2

Changing the font and colors of the playlist

It is possible to change the foreground color, the font and size of the playlist by navigating to Playlist->Font and colors. Your preferences will be saved and restored the next time the player is launched. If you messed up and want to revert back to original, click Playlist->Font and colors->Reset do default.

Seeking

Seeking is allowed for songs that weren’t encoded with VBR. If you try to seek a a VBR-encoded song, you will see the following message in the status bar: “Seeking not allowed for this song”

Resizing columns and the form

By default, the columns of the playlist will auto-resize themselves to have its size based on the longer item . This will occur each time a song is added to or deleted from the playlist

If you find it boring, simply disable it by unchecking this item at the menu Playlist->Auto-size columns.

Known issues

The player currently supports only MP3 songs. The possibility of supporting other formats must be evaluated.

MCI has a problem with VBR-encoded songs. It may retrieve the wrong length of the song. If you let it play without seeking, there will be no problem, but if you try to seek in the song, MCI will get lost.

On average, it tooks about 30ms to process a song’s information and add it to the playlist. Therefore, loading the information for the whole playlist can take some time. For example, if during initialization the saved session had a hundred songs, it would take more than 3 seconds to load the playlist and the user can get impatient.

History

On November 2017, updated the article with newer souce code (small bugs fixed), new screenshots (changed color of the GUI) and new version of executable (1.2).