Entité mp3 (multi uniquement)

Écrit le 03/07/2003 par madfab
Dernière mise à jour : 30/01/2006

Théorie

Un petit peu de théorie avant de commencer...
Tout d'abord, pour pouvoir faire la lecture des mp3, nous allons utiliser une dll externe : fmod. Vous devez vous la procurer ici : http://www.fmod.org et télécharger les API/SDK, ensuite dézipper , placer :

Le principe de la lecture des mp3 par HL serai assez simple en solo car il suffirait de faire tout le code du coté serveur pour que tout aille bien, mais en multi, il faut que ce soit la dll cliente qui lise le mp3. Pour cela nous aurons besoin de passer par des messages serveur/client. Bon la théorie est finie, c'est partie.
(J'avoue que j'ai été très flémard et pour éviter des erreurs de ma part en adaptant les noms, je les ai gardé avec leurs noms d'origine hlpienne)

Coding coté serveur

Pour arriver à nos fins, nous devons crée une nouvelle classe Chlpsound et nous aurons besoin d'y avoir accès depuis d'autre fichier donc les déclarations de la classe se situeront dans hlp.h et le fichier principal sera hlpsound.cpp.

hlp.h :

class CHlpsound : public CBaseEntity
{
public:
    void Spawn( void );
    void SetVol(int dist);
    void KeyValue( KeyValueData *pkvd );
    void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value );
    void EXPORT Start( void );
    void Stop( void );

    int m_radius; // radius du son
    int szSoundFile; // fichier son
    int vol1; // volume o radius 0
    int Chan; // le channel du son
    int Flag; // flag de lecture d'1 mp3.
    int Mode; // loop ou once
    int Strt; // kan part le mp3 en sec
    int Mod; // play everywhere or not ? that is the question
    bool Playing; // entrain de jouer ou non ?

};
void StopSound( CBasePlayer *pPlayer );
void Init (CBasePlayer *pPlayer); // initialise la lecture des mp3 (appelé qu'une fois )

hlpsound.cpp :

//=========================================================
// hlp mp3 ent
// by madfab (shopsuey) for HLP mod
//
//=========================================================

#include "extdll.h"
#include "util.h"
#include "cbase.h"
#include "player.h"
#include "hlp.h" // <- nos def de classe
#include "gamerules.h"
#include "../engine/shake.h"


CBasePlayer * pMPlayer;
extern int gmsgMp3; // <- le message server/client qui fait toutes les actions
int ChannelMp3; // channel mp3 pour pas que ça tombe sur les même

void CHlpsound::KeyValue( KeyValueData *pkvd ) // récupération des infos du fgd
{
    if (FStrEq(pkvd->szKeyName, "radius"))
    {
        m_radius = atoi(pkvd->szValue);
        pkvd->fHandled = true;
    }
    else if(FStrEq(pkvd->szKeyName, "vol1"))
    {
        vol1 = atoi(pkvd->szValue);
        pkvd->fHandled = true;
    }
    else if(FStrEq(pkvd->szKeyName, "mode"))
    {
        Mode = atoi(pkvd->szValue);
        pkvd->fHandled = true;
    }
    else if(FStrEq(pkvd->szKeyName, "mod"))
    {
        Mod = atoi(pkvd->szValue);
        pkvd->fHandled = true;
    }
    else if(FStrEq(pkvd->szKeyName, "start"))
    {
        Strt = atoi(pkvd->szValue);
        pkvd->fHandled = true;
    }
    else if (FStrEq(pkvd->szKeyName, "roomtype"))
    {
        szSoundFile = ALLOC_STRING(pkvd->szValue);
        pkvd->fHandled = true;
    }

    CBaseEntity::KeyValue( pkvd );
}

LINK_ENTITY_TO_CLASS( hlp_mp3sound, CHlpsound ); // relie le code à l'entité ajouté sur la map


void CHlpsound :: Spawn( void )
{
    if (!g_pGameRules->IsMultiplayer()) // donc c'est du solo et là ça plante
                                        // donc petit msg + on enlève l'entité.
    {
        ALERT(at_console, "Les mp3 ne marchent pas en solo...\n"); //petit msg charmant
        UTIL_Remove( this );// kill l'entité
        return; // au cas ou...
    }

    // verif si le fichier existe
    FILE* fp;
    fp = fopen( (char*)STRING(szSoundFile), "r" );

    if ( !fp )
    {
        ALERT(at_console, "[ERROR] %s n existe pas.\n",(char*)STRING(szSoundFile) );
        UTIL_Remove( this );// kill l'entité
        return;
    }
    else
    {
        ALERT(at_console, "[OK] mp3 : %s\n",(char*)STRING(szSoundFile));
        fclose(fp); // on ferme le fichier kan même
    }

    if (!Mode) // si le mod n'a pas été défini, on impose le "once"
        Mode=1;
    if (!Mod) // etc
        Mod=2;
    if (!Strt) // le start n'a pas été précisé , cela fé crasher HL
              //car on utilise un SetThink apres.... donc on ignore l'entité
    {
    ALERT (at_console,"fatal error start entity time\n");
    return;
    }

    if(Strt!=-1) // si le son n'est pas déclencher par un trigger
    {
        SetThink(Start); // le think
        pev->nextthink = gpGlobals->time + Strt; // lancement du mp3 dans Strt secondes.
    }

    ChannelMp3++;
    Chan=ChannelMp3;
}


void Init(CBasePlayer *pPlayer)
{
    pMPlayer=pPlayer;

    MESSAGE_BEGIN( MSG_ONE, gmsgMp3, NULL, pPlayer->pev );
    WRITE_BYTE (1); //<- veut dire qu'on init
    MESSAGE_END();
}


void CHlpsound :: SetVol(int dist)
{
    int vol = 255-(dist/(m_radius/vol1)); // calcul à la con pour connaître le volume à mettre

    if (dist)
    {
        if (vol<0)
            vol=0;

    MESSAGE_BEGIN( MSG_ONE, gmsgMp3, NULL, pMPlayer->pev );
    WRITE_BYTE (5); // <- veut dire qu'on ajuste le vol
    WRITE_BYTE (Chan); // le chan
    WRITE_BYTE(vol); // le volume
    MESSAGE_END();
    }
}


void CHlpsound :: Start (void)
{
    MESSAGE_BEGIN( MSG_ONE, gmsgMp3, NULL, pMPlayer->pev);
    WRITE_BYTE( 2 ); // start
    WRITE_BYTE( Chan );
    WRITE_STRING((char*)STRING(szSoundFile));
    // options
    WRITE_BYTE( Mode ); // 0= loop 1=once
    if (Mod!=1)
    Mod=2;
    WRITE_BYTE( Mod ); // play everywhere ? 2=non 1=yes
    WRITE_BYTE( vol1 ); // vol
    //---
    MESSAGE_END();

    Playing=1; // le son est joué
}


void CHlpsound :: Stop (void)
{
    MESSAGE_BEGIN( MSG_ONE, gmsgMp3, NULL, pMPlayer->pev );
    WRITE_BYTE (3); // 1=loading 2=play etc
    WRITE_BYTE(Chan); //channel à stopper
    MESSAGE_END();
    Playing=0;
}


void StopSound(CBasePlayer *pPlayer )
{
    MESSAGE_BEGIN( MSG_ONE, gmsgMp3, NULL, pPlayer->pev );
    WRITE_BYTE (4); // 1=loading 2=play etc
    MESSAGE_END();
}


// démarrage avec un trigger...
void CHlpsound::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value )
{
    if (Strt==-1 && !Playing)
        Start();
    else
        Stop();
}

Maintenant il faut que dès que le joueur rejoigne la partie il initialise les mp3.
Cela se passe dans le multiplay_gamerules.cpp :

#include "hlp.h" // pour utiliser notre fonction
[...]

void CHalfLifeMultiplay :: InitHUD( CBasePlayer *pl )
{
    [...]
    Init(pl); // mp3
    [...]
}

Il faut aussi que le son du mp3 si il n'est pas joué pour qu'on l'entende partout est le bon volume. Pour cela on va dans player.cpp, au passage on oublie pas de faire notre include de hlp.h et la déclaration du message server/client.

[...]
#include "hlp.h"
[...]
int gmsgMp3 = 0;
[...]
gmsgMp3 = REG_USER_MSG( "Mp3", -1 ); // -1 car la taille du message dépend.
[...]

void CBasePlayer::PostThink()
{
    [...]

    CBaseEntity* pMp3 = NULL;

    // cherche toutes les entités mp3
    while ( pMp3 = UTIL_FindEntityByClassname( pMp3, "hlp_mp3sound" ))
    {
        // envoi la distance entre le player et l'ent à la fonction qui détermine le volume
        ((CHlpsound*)pMp3)->SetVol(int((pev->origin - pMp3->pev->origin).Length()));
    }

    [...]
}

Voilà la programmation est finie du coté serveur, passons au coté client...

Coding coté client

Maintenant nous devons récupérer dans le client le message server/client et le traité.
Donc il faut déjà dire au client que ce message existe.

hud.h :

//
//-----------------------------------------------------
// env ligne 183
class CHudMp3: public CHudBase
{
public:
    int Init( void );
    int VidInit( void );
    int Draw(float flTime);
    int MsgFunc_Mp3(const char *pszName, int iSize, void *pbuf);

};
.............
// (env ligne 703)
CHudMp3 m_Mp3;

hud.cpp

[...]
GetClientVoiceMgr()->Init(&g_VoiceStatusHelper, (vgui::Panel**)&gViewPort);
m_Spectator.Init();
m_Mp3.Init();
[...]
m_StatusIcons.VidInit();
GetClientVoiceMgr()->VidInit();
m_Spectator.VidInit();
m_Mp3.VidInit();

Notre message est déclaré, nous pouvons programmer le fichier principal.

Mp3.cpp par exemple :

#include "hud.h"
#include "cl_util.h"
#include "parsemsg.h"
#include <string.h>
#include <stdio.h>
#include "fmod.h" // on va les ajouter juste après
#include "fmod_errors.h" // lui aussi
#include "loaddll.h" // et lui aussi


DECLARE_MESSAGE ( m_Mp3, Mp3 ); // on declare


int CHudMp3::Init ( void )
{
    HOOK_MESSAGE ( Mp3 );
    m_iFlags |= HUD_ACTIVE;

    gHUD.AddHudElem ( this );
    return 1;
};


int CHudMp3::VidInit ( void )
{
    return 1;
};


int CHudMp3::MsgFunc_Mp3 ( const char *pszName, int iSize, void *pbuf )
{
    BEGIN_READ ( pbuf, iSize );

    char* song; // chanson
    int mod; // action
    int chan; // channel
    int Flag,Mode; // flag de lecture
    int everywhere; // tout est dans le nom
    int vol;
    mod = READ_BYTE();

    switch ( mod )
    {
        case 1: // loading mp3 -> en théorie exécuté une seule fois
        {
            LoadLibrary( "fmod.dll" );
            FSOUND_SetOutput(FSOUND_OUTPUT_DSOUND);
            FSOUND_SetBufferSize(200);
            FSOUND_SetDriver(0);
            FSOUND_Init(44100, 16, 0);
            break;
        }

        case 2: // demande la lecture d'1 chan
        {
            chan = READ_BYTE();
            song = READ_STRING();
            // équivalent de ALERT mais coté client
            gEngfuncs.Con_DPrintf( "Playing %s on channel %i\n",song,chan );
            Mode = READ_BYTE();
            everywhere = READ_BYTE();
            vol = READ_BYTE(); // vol

            Flag = FSOUND_NORMAL; // commun pour toutes les lectures.

            if (Mode==0) // loop
                Flag |= FSOUND_LOOP_NORMAL;


            stream = FSOUND_Stream_OpenFile(song, Flag ,0);

            FSOUND_Stream_Play(chan, stream);

            if (everywhere==1)
                FSOUND_SetVolume(chan,vol);
            else
                FSOUND_SetVolume(chan,0); // on met à zéro pour l'instant.

            break;
        }

        case 3: // stop un chan
        {
            chan = READ_BYTE();
            FSOUND_StopSound(chan);
            break;
        }

        case 4: // stop tous les sons
        {
            FSOUND_Close();
            break;
        }

        case 5: // set vol
        {
            chan = READ_BYTE();
            vol = READ_BYTE();
            FSOUND_SetVolume(chan,vol);
            break;
        }
    }

    return 1;
}

int CHudMp3::Draw ( float flTime )
{
    return 1;
}

J'ai du créer (modif en fait) un include pour pouvoir charger des dlls (fonction LoadLibrary())

loaddll.h

#ifndef EXTDLL_H
#define EXTDLL_H

//
// Global header file for extension DLLs
//

// Allow "DEBUG" in addition to default "_DEBUG"
#ifdef _DEBUG
#define DEBUG 1
#endif

// Silence certain warning
s #pragma warning(disable : 4244) // int or float down-conversion
#pragma warning(disable : 4305) // int or float data truncation
#pragma warning(disable : 4201) // nameless struct/union
#pragma warning(disable : 4514) // unreferenced inline function removed
#pragma warning(disable : 4100) // unreferenced formal parameter

// Prevent tons of unused windows definitions
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define NOWINRES
#define NOSERVICE
#define NOMCX
#define NOIME
#include "windows.h"
#else // _WIN32
#define false 0
#define true (!false)
typedef unsigned long ULONG;
typedef unsigned char BYTE;
typedef int bool;
#define MAX_PATH PATH_MAX
#include <limits.h>
#include <stdarg.h>
#ifndef min
#define min(a,b) (((a) < (b)) ? (a) : (b))
#endif
#ifndef max
#define max(a,b) (((a) > (b)) ? (a) : (b))
#define _vsnprintf(a,b,c,d) vsnprintf(a,b,c,d)
#endif
#endif //_WIN32

// Misc C-runtime library headers
#include "stdio.h"
#include "stdlib.h"
#include "math.h"

// Header file containing definition of globalvars_t and entvars_t
typedef int func_t; //
typedef int string_t; // from engine's pr_comp.h;
typedef float vec_t; // needed before including progdefs.h

// Vector class
//#include "vector.h"

// Defining it as a (bogus) struct helps en force type-checking
#define vec3_t Vector

// Shared engine/DLL constants
#include "const.h"
#include "progdefs.h"
#include "edict.h"

// Shared header describing protocol between engine and DLLs
#include "eiface.h"

// Shared header between the client DLL and the game DLLs
#include "cdll_dll.h"

#endif //EXTDLL_H

Surtout ne pas oublier de rajouter au projet client le fmod.h et fmod_errors.h. Au passage une petite modification du fmod.h

fmod.h (à la fin) :

[...]
FSOUND_STREAM *stream;
#ifdef __cplusplus
[...]

La partie code est presque finie, il faut juste modifier quelques settings du projet de la dll cliente.
Allez dans le menu Project, et choisissez settings, dans l'onglet GENERAL, vérifiez que Not using MFC est bien sélectionné. Allez maintenant dans l'onglet LINK, rajoutez fmodvc.lib juste avant le /nologo, en faisant bien attention à les espacer devant et derrière.

Le fgd correspondant

À rajouter au fgd de votre mod :

//
// hlp_mp3sound
//
//
@PointClass base(Targetname) = hlp_mp3sound : "Mp3 sound"
[
    radius(integer) : "Radius" : 600
    mod(choices) : "Play everywhere":0=
    [
        0: "no"
        1: "yes"
    ]
    mode(choices) : "Mode" : 0 =
    [
        0 : "Loop"
        1 : "Once"
    ]
    start(integer): "Start in x sec (-1 for trigger)" : 0

    vol1(integer) : "Vol at radius 0 (0-255)" : 255
    roomtype(sound) : "MP3"
]

Attention, pour le champ du mp3, imaginez que la racine du répertoire est le hl.exe (racine half-life) donc l'accès à un mp3 sera par exemple hl-parpaing\sound\parpaing.mp3.