Click here to Skip to main content
13,768,153 members
Click here to Skip to main content
Add your own
alternative version

Stats

19.1K views
1.2K downloads
12 bookmarked
Posted 20 Jul 2015
Licenced CPOL

Windows MCI Player in C++ CLI

, 31 Jan 2018
Rate this:
Please Sign up or sign in to vote.
A music player powered by MCI & Window Forms

Download source code

Download Velvet Player

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.

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.

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.

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:

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).

License

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

Share

About the Author

Jerome Vonk
Systems Engineer
Brazil Brazil
C/C++ developer for Windows and Linux. Cryptography enthusiast.

You may also be interested in...

Comments and Discussions

 
QuestionOnly support MP3 files??? Pin
YDLU5-Feb-18 14:31
memberYDLU5-Feb-18 14:31 
GeneralMy vote of 5 Pin
Sharjith20-Nov-17 20:10
professionalSharjith20-Nov-17 20:10 
QuestionVery interesting Pin
fginez5-Aug-15 15:52
memberfginez5-Aug-15 15:52 
GeneralTexture Pin
Member 1185227223-Jul-15 11:48
memberMember 1185227223-Jul-15 11:48 
GeneralRe: Texture Pin
Jerome Vonk24-Jul-15 4:23
memberJerome Vonk24-Jul-15 4:23 

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.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01-2016 | 2.8.181116.1 | Last Updated 31 Jan 2018
Article Copyright 2015 by Jerome Vonk
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid