jouer des MP3
(niveau : facile)


Table des matières

Introduction
Interface de l'application
Classe CPlayerMP3
Utilisation de CPlayerMP3
Annexe : code de CPlayerMP3


Introduction

Le but de cet article n'est pas de programmer un décodeur MPEG, mais plus modestement d' utiliser DirectShow (l'un des composants de DirectX) pour jouer des fichiers MP3 sous Windows. J'ai écrit pour cela une petite application MFC, qui démontre l'utilisation de la classe CPlayerMP3 qui gère les appels à DirectShow.


ce n'est qu'une image, inutile de cliquer dessus ! ;)

Vous pouvez bien sûr télécharger l'ensemble du projet : sources de CPlayerMP3, de l'application MFC, et exe (95 Ko).
(le SDK de DirectX est nécessaire pour recompiler)


Interface de l'application

L'utilisation de l'application (cf image ci-dessus) est triviale : vous devez tout d'abord choisir un fichier MP3 à l'aide du bouton "Load", puis vous pouvez le jouer, faire une pause, stopper, revenir au début, et régler le volume (0 à 100). Vous pouvez à tout moment charger un autre fichier, sans faire "stop". Quelques remarques :

- l'application refuse de charger le fichier si DirectSound est utilisé par un autre programme ; elle ne peut en effet alors pas l'initialiser pour ses propres besoins. Ceci se produit par exemple lorsque Winamp est lancé, et de la même façon Winamp refusera de jouer si l'application MFC est en train de le faire.

- le point précédent ne s'applique pas quand il s'agit du même programme : vous pouvez lancer plusieurs instances de l'application MFC simultanément, les musiques qu'elles jouent seront mixées entre elles.

- je suis tombé par hasard sur des fichiers .mp3 que l'application refuse de charger : ils sont en fait au format MPEG 1 layer 1, DirectShow ne semble accepter "que" le MPEG 1 layer 3, qui constitue la majorité des fichiers musicaux que l'on trouve sur Internet.

- il n'y a pas une très grosse différence entre "Pause" et "Stop" : par exemple Stop ne revient pas automatiquement en début de fichier. C'est à l'application de le faire (en appelant Rewind) si c'est ce comportement qui est souhaité, j'ai pour ma part préféré refléter le fonctionnement de DirectShow au plus près.


Classe CPlayerMP3

Il n'y a pas grand chose à dire au sujet de cette classe, qui est extrêmement simple. Le code complet se trouve en annexe, pour ceux qui veulent y jeter un oeil sans télécharger. En ce qui concerne les appels à DirectShow, il faut noter que :

- les différentes fonctionnalités implémentées dans CPlayerMP3 utilisent plusieurs interfaces COM : IMediaControl principalement, IMediaPosition pour Rewind, IBasicAudio pour le volume, IMediaEvent pour détecter la fin de la musique. Il en existe bien d'autres (voir la documentation du SDK DirectX) mais ce sont les principales pour le sujet qui nous intéresse, avec IMediaSeeking.

- chaque fois qu'un pointeur sur une interface a été demandé par QueryInterface, et lorsqu'il a fini d'être utilisé, ne pas oublier d'appeler sa méthode Release.

- dans CPlayerMP3::hrRewind j'appelle put_CurrentPosition(0) pour revenir au début du fichier ; cette fonction permet de se placer n'importe où dans la musique et pas uniquement au départ, vous pouvez l'utiliser pour coder une "avance rapide". La documentation conseille toutefois de plutôt utiliser IMediaSeeking pour ce genre d'opération.

- le volume doit être donné entre 0 (maximum) et -10000 (silence) car il s'agit d'une atténuation en dB (décibels) [plus exactement en centièmes de décibels, donc entre 0 et -100dB]. De ce fait l'échelle n'est pas linéaire mais logarithmique, c'est à dire que le volume sonore est divisé par 2 chaque fois que l'atténuation est multipliée par 10. C'est un peu compliqué à utiliser, ne vous inquiétez pas : l'application contient une conversion (empirique) qui s'occupe de ça.

- il y a plusieurs façons de détecter que la musique est terminée (pour passer à la suivante, ou dans mon programme pour la stopper et "relever" le bouton Play). J'ai choisi la plus simple, en allant vérifier de temps en temps si l'événement adéquat (EC_COMPLETE) s'est produit ou non. Une autre méthode consisterait par exemple à demander à DirectShow d'appeler un callback, c'est faisable mais je n'ai pas regardé comment en détail.


Utilisation de CPlayerMP3

L'application MFC, que j'ai gardée la plus simple possible, utilise la classe précédente de la façon suivante :

- dans OnInitDialog, elle appelle CoInitialize(NULL) [nécessaire pour utiliser COM et DirectX], crée un objet CPlayerMP3, initialise l'état des boutons, et crée un timer (appelé une fois par seconde pour tester la fin de la musique - NB: ce test pourrait également être placé dans le OnIdle d'une application MDI).

- à l'inverse, la fermeture de la boîte de dialogue détruit le timer (dans OnDestroy car la fenêtre doit encore exister), puis le player, et appelle CoUninitialize().

- la fonction CPlayMP3Dlg::OnLoad charge un nouveau fichier et vérifie sa validité. Les méthodes gérant les autres boutons de l'interface font de simples appels à l'objet CPlayerMP3.

- la gestion du volume dans OnDeltaposSpinvolume peut vous intéresser : une petite formule convertit la valeur linéaire entre 0 et 100 en une valeur logarithmique attendue par DirectShow, le but étant qu'à 25% du volume maximal on entende à peu près 4 fois moins la musique qu'à 100%, et ainsi de suite.

C'est tout ! A vous d'implémenter une jolie interface :)


Annexe : code de CPlayerMP3

PlayerMP3.h
PlayerMP3.inl
PlayerMP3.cpp

PlayerMP3.h
/**********************************************************************************************************************/
/*                                                MP3 PLAYER                                                          */
/* 14/12/00, M : creation of the file                                                                                 */
/*               uses DirectShow (DX7)                                                                                */
/**********************************************************************************************************************/

#if !defined(AFX_PLAYERMP3_H__5E8A1F22_D1D0_11D4_9849_00A0CC2C5341__INCLUDED_)
#define AFX_PLAYERMP3_H__5E8A1F22_D1D0_11D4_9849_00A0CC2C5341__INCLUDED_

#if _MSC_VER >= 1000
#pragma once
#endif

/************************************************ INCLUDE *************************************************************/

#include  <strmif.h>                                        // DirectShow
#include  <string>                                          // STL string

/************************************************ CLASSES *************************************************************/

/**********************************************************************************************************************/
/*                                                CPlayerMP3                                                          */
/**********************************************************************************************************************/

class     CPlayerMP3
  {
// public enums

  public:

    typedef enum 
    {
          _UNINITIALIZED_               = 0,
          _STOPPED_,
          _PAUSED_,
          _PLAYING_,
    } PLAYER_STATE;

    typedef enum
    {
          _OK_                          = 0,
          _MP3ERR_CANTPLAY_             = 0x80000000,
          _MP3ERR_CANTPAUSE_,
          _MP3ERR_CANTREWIND_,
          _MP3ERR_CANTSTOP_,
          _MP3ERR_NOVOLUME_,
    } MP3_ERROR;

// public methods

  public:

    // constructors & destructor

    CPlayerMP3                          (void);
   ~CPlayerMP3                          (void);

    HRESULT         hrLoad              (const TCHAR* lpszFile);

    // state

    bool            boCanPlay           (void) const;
    bool            boCanPause          (void) const;
    bool            boCanStop           (void) const;

    bool            boIsInitialized     (void) const;
    bool            boIsStopped         (void) const;
    bool            boIsPaused          (void) const;
    bool            boIsPlaying         (void) const;
    bool            boCompleted         (void) const;

    const TCHAR*    lpszGetFile         (void) const;

    // play

    HRESULT         hrPlay              (void);
    HRESULT         hrPause             (void);
    HRESULT         hrRewind            (void);
    HRESULT         hrStop              (void);
    HRESULT         hrSetVolume         (long lVolume);     // 0=full, -10000=silence, logarithmic

// protected methods

  protected:

    void            vInit               (void);
    void            vClean              (void);
    HRESULT         hrCreateFilterGraph (void);

// protected members

  protected:

    IGraphBuilder*  m_lpGraph;
    std::string     m_strFile;
    DWORD           m_dwState;
  };

/************************************************ INLINE **************************************************************/

#include  "PlayerMP3.inl"

/**********************************************************************************************************************/

#endif // !defined(AFX_PLAYERMP3_H__5E8A1F22_D1D0_11D4_9849_00A0CC2C5341__INCLUDED_)

PlayerMP3.inl
/**********************************************************************************************************************/
/*                                                MP3 PLAYER                                                          */
/* 14/12/00, M : creation of the file                                                                                 */
/*               uses DirectShow (DX7)                                                                                */
/**********************************************************************************************************************/

#ifndef   _PLAYERMP3_INL_
#define   _PLAYERMP3_INL_

/**********************************************************************************************************************/
/*                                                STATE                                                               */
/**********************************************************************************************************************/

inline bool CPlayerMP3::boCanPlay() const
  {
  return((m_dwState == _STOPPED_) || (m_dwState == _PAUSED_));
  }

inline bool CPlayerMP3::boCanStop() const
  {
  return((m_dwState == _PLAYING_) || (m_dwState == _PAUSED_));
  }

inline bool CPlayerMP3::boCanPause() const
  {
  return((m_dwState == _PLAYING_) || (m_dwState == _STOPPED_));
  }

//

inline bool CPlayerMP3::boIsInitialized() const
  {
  return (m_dwState != _UNINITIALIZED_);
  }

inline bool CPlayerMP3::boIsStopped() const
  {
  return (m_dwState == _STOPPED_);
  }

inline bool CPlayerMP3::boIsPaused() const
  {
  return (m_dwState == _PAUSED_);
  }

inline bool CPlayerMP3::boIsPlaying() const
  {
  return (m_dwState == _PLAYING_);
  }

//

inline const TCHAR* CPlayerMP3::lpszGetFile() const
  {
  return m_strFile.c_str();
  }

/**********************************************************************************************************************/

#endif

PlayerMP3.cpp
/**********************************************************************************************************************/
/*                                                MP3 PLAYER                                                          */
/* 14/12/00, M : creation of the file                                                                                 */
/*               uses DirectShow (DX7)                                                                                */
/**********************************************************************************************************************/

/************************************************ INCLUDE *************************************************************/

#include  "stdafx.h"
#include  "PlayerMP3.h"

#include  <control.h>                                       // IMediaControl
#include  <uuids.h>
#include  <evcode.h>                                        // EC_COMPLETE

/**********************************************************************************************************************/
/*                                                CONSTRUCTORS / DESTRUCTOR                                           */
/**********************************************************************************************************************/

CPlayerMP3::CPlayerMP3()
  {
  vInit();
  }

//

CPlayerMP3::~CPlayerMP3()
  {
  vClean();
  }

/************************************************ vInit ***************************************************************/
// initialize members & resources
// 14/12/00, M
// in :
// out:
/**********************************************************************************************************************/

void CPlayerMP3::vInit()
  {
  m_lpGraph = NULL;
  m_dwState = _UNINITIALIZED_;
  }

/************************************************ vClean **************************************************************/
// release memory & resources
// 14/12/00, M
// in :
// out:
/**********************************************************************************************************************/

void CPlayerMP3::vClean()
  {
  if(m_lpGraph) m_lpGraph->Release();
  m_strFile = "";

  vInit();
  }

/************************************************ hrCreateFilterGraph *************************************************/
// initialize DShow object
// 14/12/00, M
// in :
// out: _OK_ or error ID
/**********************************************************************************************************************/

HRESULT CPlayerMP3::hrCreateFilterGraph()
  {
  if(m_lpGraph) return true;

  HRESULT hrErr = CoCreateInstance(CLSID_FilterGraph,
                                   NULL,
                                   CLSCTX_INPROC_SERVER,
                                   IID_IGraphBuilder,
                                   (void**)&m_lpGraph);
  if(FAILED(hrErr))
    {
    m_lpGraph = NULL;
    return hrErr;
    }

  return _OK_;
  }

/************************************************ hrLoad **************************************************************/
// load a file with graph object
// 14/12/00, M
// in :
// out: _OK_ or error ID
/**********************************************************************************************************************/

HRESULT CPlayerMP3::hrLoad(const TCHAR* lpszFile)
  {
  vClean();

  HRESULT hrErr = hrCreateFilterGraph();
  if(FAILED(hrErr)) return hrErr;

  WCHAR wszFile[MAX_PATH];
  MultiByteToWideChar(CP_ACP,0,lpszFile,-1,wszFile,MAX_PATH);

  hrErr = m_lpGraph->RenderFile(wszFile,NULL);
  if(FAILED(hrErr)) return hrErr;

  m_strFile = lpszFile;
  m_dwState = _STOPPED_;
  return _OK_;
}

/**********************************************************************************************************************/
/*                                                PLAY                                                                */
/**********************************************************************************************************************/

/************************************************ hrPlay **************************************************************/
// start playing
// 14/12/00, M
// in :
// out: _OK_ or error ID
/**********************************************************************************************************************/

HRESULT CPlayerMP3::hrPlay()
  {
  if(!boCanPlay()) return _MP3ERR_CANTPLAY_;

  IMediaControl* pMC;
  HRESULT hrErr = m_lpGraph->QueryInterface(IID_IMediaControl,(void**)&pMC);
  if(FAILED(hrErr)) return hrErr;

  hrErr = pMC->Run();
  pMC->Release();
  if(FAILED(hrErr)) return hrErr;

  m_dwState = _PLAYING_;
  return _OK_;
  }

/************************************************ hrPause *************************************************************/
// pause replay
// 14/12/00, M
// in :
// out: _OK_ or error ID
/**********************************************************************************************************************/

HRESULT CPlayerMP3::hrPause()
  {
  if(!boCanPause()) return _MP3ERR_CANTPAUSE_;

  IMediaControl* pMC;
  HRESULT hrErr = m_lpGraph->QueryInterface(IID_IMediaControl,(void**)&pMC);
  if(FAILED(hrErr)) return hrErr;

  hrErr = pMC->Pause();
  pMC->Release();
  if(FAILED(hrErr)) return hrErr;

  m_dwState = _PAUSED_;
  return _OK_;
  }

/************************************************ hrRewind ************************************************************/
// rewind to the beginning
// 14/12/00, M
// in :
// out: _OK_ or error ID
/**********************************************************************************************************************/

HRESULT CPlayerMP3::hrRewind()
  {
  if(!boIsInitialized()) return _MP3ERR_CANTREWIND_;
  hrStop();

  IMediaPosition* pMP;
  HRESULT hrErr = m_lpGraph->QueryInterface(IID_IMediaPosition,(void**)&pMP);
  if(FAILED(hrErr)) return hrErr;
  
  hrErr = pMP->put_CurrentPosition(0);
  pMP->Release();
  if(FAILED(hrErr)) return hrErr;

  return _OK_;
  }

/************************************************ hrStop **************************************************************/
// stop playing
// 14/12/00, M
// in :
// out: _OK_ or error ID
/**********************************************************************************************************************/

HRESULT CPlayerMP3::hrStop()
  {
  if(!boCanStop()) return _MP3ERR_CANTSTOP_;

  IMediaControl* pMC;
  HRESULT hrErr = m_lpGraph->QueryInterface(IID_IMediaControl,(void**)&pMC);
  if(FAILED(hrErr)) return hrErr;

  hrErr = pMC->Stop();
  pMC->Release();
  if(FAILED(hrErr)) return hrErr;

  m_dwState = _STOPPED_;
  return _OK_;
  }

/************************************************ hrSetVolume *********************************************************/
// change volume
// 27/02/02, M
// in : volume (dB*100)
// out: _OK_ or error ID
/**********************************************************************************************************************/

HRESULT CPlayerMP3::hrSetVolume(long lVolume)
  {
  if(!boIsInitialized()) return _MP3ERR_NOVOLUME_;

  IBasicAudio* pBA;
  HRESULT hrErr = m_lpGraph->QueryInterface(IID_IBasicAudio,(void**)&pBA);
  if(FAILED(hrErr)) return hrErr;

  if(lVolume >      0) lVolume = 0;
  if(lVolume < -10000) lVolume = -10000;
  hrErr = pBA->put_Volume(lVolume);
  pBA->Release();
  if(FAILED(hrErr)) return hrErr;

  return _OK_;
  }

/************************************************ boCompleted *********************************************************/
// check if end of stream is reached
// 28/02/02, M
// in :
// out: end reached ?
/**********************************************************************************************************************/

bool CPlayerMP3::boCompleted() const
  {
  if(!boIsPlaying()) return false;

  IMediaEvent* pME;
  HRESULT hrErr = m_lpGraph->QueryInterface(IID_IMediaEvent,(void**)&pME);
  if(FAILED(hrErr)) return false;

  long lEvent;
  hrErr = pME->WaitForCompletion(0,&lEvent);
  pME->Release();
  if(FAILED(hrErr)) return false;

  return(lEvent == EC_COMPLETE);
  }

haut de la page