Apprenez à programmer en C !

Apprenez à programmer en C !

Mis à jour le mardi 29 juillet 2014

Ce chapitre de travaux pratiques va vous proposer de manipuler la SDL et FMOD simultanément. Cette fois, nous n'allons pas travailler sur un jeu. Certes, la SDL est tout particulièrement adaptée à cela, mais on peut l'utiliser dans d'autres domaines. Ce chapitre va justement vous prouver qu'elle peut servir à autre chose.

Nous allons réaliser ici une visualisation du spectre sonore en SDL. Cela consiste à afficher la composition du son que l'on joue, par exemple une musique. On retrouve cela dans de nombreux lecteurs audio. C'est amusant et ce n'est pas si compliqué que ça en a l'air !

Ce chapitre va nous permettre de travailler autour de notions que nous avons découvertes récemment :

  • la gestion du temps ;

  • la bibliothèque FMOD.

Nous découvrirons en outre comment modifier une surface pixel par pixel.

La figure suivante vous donne un aperçu du programme que nous allons créer dans ce chapitre.

Visualisation du spectre sonore

C'est le genre de visualisation qu'on peut retrouver dans des lecteurs audio tels que Winamp, Windows Media Player ou encore AmaroK.
Et pour ne rien gâcher, comme je vous l'ai dit ce n'est pas bien difficile à faire. D'ailleurs, contrairement au TP Mario Sokoban, cette fois c'est vous qui allez travailler. Ça vous fera un très bon exercice.

Les consignes

Les consignes sont simples. Suivez-les pas à pas dans l'ordre, et vous n'aurez pas d'ennuis.

1/ Lire un MP3

Pour commencer, vous devez créer un programme qui lit un fichier MP3. Vous n'avez qu'à reprendre la chanson « Home » du groupe Hype que nous avons utilisée dans le chapitre sur FMOD pour illustrer le fonctionnement de la lecture d'une musique.

Si vous avez bien suivi le chapitre sur FMOD, il ne vous faudra pas plus de quelques minutes pour arriver à le faire. Je vous conseille au passage de placer le MP3 dans le dossier de votre projet.

2/ Récupérer les données spectrales du son

Pour comprendre comment la visualisation spectrale fonctionne, il est indispensable que je vous explique un peu comment cela fonctionne à l'intérieur (pas dans le détail non plus, sinon ça va se transformer en cours de maths).

Un son peut être découpé en fréquences. Certaines fréquences sont basses, d'autres moyennes, et d'autres hautes.
Ce que nous allons faire dans notre visualisation, c'est afficher la quantité de chacune de ces fréquences sous forme de barres. Plus la barre est grande, plus la fréquence est utilisée (figure suivante).

La visualisation des fréquences d'un son

Sur la gauche de la fenêtre, nous faisons donc apparaître les basses fréquences, et sur la droite les hautes fréquences.

Mais comment récupérer les quantités de chaque fréquence ?

FMOD nous mâche le travail. On peut faire appel à la fonction FMOD_Channel_GetSpectrum. Son prototype est le suivant :

FMOD_RESULT FMOD_Channel_GetSpectrum(
  FMOD_CHANNEL *  channel,
  float *  spectrumarray, 
  int  numvalues, 
  int  channeloffset, 
  FMOD_DSP_FFT_WINDOW  windowtype
);

Et voici ses paramètres :

  • Le canal sur lequel la musique est jouée. Donc a priori il faut récupérer un pointeur vers ce canal.

  • Un tableau de float. Il faut que ce tableau soit déjà alloué, statiquement ou dynamiquement, pour permettre à FMOD de le remplir correctement.

  • La taille du tableau. Cette taille doit obligatoirement être une puissance de 2, par exemple 512.

  • Ce paramètre sert à définir à quelle sortie on s'intéresse. Par exemple si vous êtes en stéréo, 0 veut dire gauche, et 1 veut dire droite.

  • Ce paramètre est un peu plus complexe, et ne nous intéresse pas vraiment dans ce cours. On se contentera de lui donner la valeur FMOD_DSP_FFT_WINDOW_RECT.

En clair, on déclare notre tableau de float :

float spectre[512];

Ensuite, lorsque la musique est en train d'être jouée, on demande à FMOD de remplir le tableau du spectre en faisant par exemple :

FMOD_Channel_GetSpectrum(canal, spectre, 512, 0,  FMOD_DSP_FFT_WINDOW_RECT);

On peut ensuite parcourir ce tableau pour obtenir les valeurs de chacune des fréquences :

spectre[0] // Fréquence la plus basse (à gauche)
spectre[1]
spectre[2]
...
spectre[509]
spectre[510]
spectre[511] // Fréquence la plus haute (à droite)

Chaque fréquence est un nombre décimal compris entre 0 (rien) et 1 (maximum). Votre travail va consister à afficher une barre plus ou moins grande en fonction de la valeur que contient chaque case du tableau.

Par exemple, si la valeur est 0.5, vous devrez tracer une barre dont la hauteur correspondra à la moitié de la fenêtre.
Si la valeur est 1, elle devra faire toute la hauteur de la fenêtre.

Généralement, les valeurs sont assez faibles (plutôt proches de 0 que de 1). Je recommande de multiplier par 20 toutes les valeurs pour mieux voir le spectre.
Attention : si vous faites ça, vérifiez que vous ne dépassez pas 1 (arrondissez à 1 s'il le faut). Si vous vous retrouvez avec des valeurs supérieures à 1, vous risquez d'avoir des problèmes pour tracer les barres verticales par la suite !

Mais les barres doivent bouger au fur et à mesure du temps non ? Comme le son change tout le temps, il faut mettre à jour le graphique. Comment faire ?

Bonne question. En effet, le tableau de 512 float que vous renvoie FMOD change toutes les 25 ms (pour être à jour par rapport au son actuel). Il va donc falloir dans votre code que vous relisiez le tableau de 512 floats (en refaisant appel à FMOD_Channel_GetSpectrum toutes les 25 ms), puis que vous mettiez à jour votre graphique en barres.

Relisez le chapitre sur la gestion du temps en SDL pour vous rappeler comment faire. Vous avez le choix entre une solution à base de GetTicks ou à base de callbacks. Faites ce qui vous paraît le plus facile.

4/ Réaliser le dégradé

Dans un premier temps, vous pouvez réaliser des barres de couleur unie. Vous pourrez donc créer des surfaces. Il devra y avoir 512 surfaces : une pour chaque barre. Chaque surface fera donc 1 pixel de large et la hauteur des surfaces variera en fonction de l'intensité de chaque fréquence.

Toutefois, je vous propose ensuite d'effectuer une amélioration : la barre doit tendre vers le rouge lorsque le son devient de plus en plus intense. En clair, la barre doit être verte en bas et rouge en haut.

Mais… une surface ne peut avoir qu'une seule couleur si on utilise SDL_FillRect(). On ne peut pas faire de dégradé !

En effet. On pourrait certes créer des surfaces de 1 pixel de large et 1 pixel de haut pour chaque couleur du dégradé, mais ça ferait vraiment beaucoup de surfaces à gérer et ce ne serait pas très optimisé !

Comment fait-on pour dessiner pixel par pixel ?
Je ne vous l'ai pas appris auparavant, car cette technique ne méritait pas un chapitre entier. Vous allez voir en effet que ce n'est pas bien compliqué.

En fait, la SDL ne propose aucune fonction pour dessiner pixel par pixel. Mais on a le droit de l'écrire nous-mêmes.

Pour ce faire, il faut suivre ces étapes méthodiquement dans l'ordre :

  1. Faites appel à la fonction SDL_LockSurfacepour annoncer à la SDL que vous allez modifier la surface manuellement. Cela « bloque » la surface pour la SDL et vous êtes le seul à y avoir accès tant que la surface est bloquée.

  2. Ici, je vous conseille de ne travailler qu'avec une seule surface : l'écran. Si vous voulez dessiner un pixel à un endroit précis de l'écran, vous devrez donc bloquer la surface ecran :

    SDL_LockSurface(ecran);
    
  3. Vous pouvez ensuite modifier le contenu de chaque pixel de la surface. Comme la SDL ne propose aucune fonction pour faire ça, il va falloir l'écrire nous-même dans notre programme.

  4. Cette fonction, je vous la donne. Je la tire de la documentation de la SDL. Elle est un peu compliquée car elle travaille sur la surface directement et gère toutes les profondeurs de couleurs (bits par pixel) possibles. Pas besoin de la retenir ou de la comprendre, copiez-la simplement dans votre programme pour pouvoir l'utiliser :

    void setPixel(SDL_Surface *surface, int x, int y, Uint32 pixel)
    {
        int bpp = surface->format->BytesPerPixel;
    
        Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp;
    
        switch(bpp) {
        case 1:
            *p = pixel;
            break;
    
        case 2:
            *(Uint16 *)p = pixel;
            break;
    
        case 3:
            if(SDL_BYTEORDER == SDL_BIG_ENDIAN) {
                p[0] = (pixel >> 16) & 0xff;
                p[1] = (pixel >> 8) & 0xff;
                p[2] = pixel & 0xff;
            } else {
                p[0] = pixel & 0xff;
                p[1] = (pixel >> 8) & 0xff;
                p[2] = (pixel >> 16) & 0xff;
            }
            break;
    
        case 4:
            *(Uint32 *)p = pixel;
            break;
        }
    }
    

    Elle est simple à utiliser. Envoyez les paramètres suivants :

  • le pointeur vers la surface à modifier (cette surface doit préalablement avoir été bloquée avec SDL_LockSurface) ;

  • la position en abscisse du pixel à modifier dans la surface (x) ;

  • la position en ordonnée du pixel à modifier dans la surface (y) ;

  • la nouvelle couleur à donner à ce pixel. Cette couleur doit être au format Uint32 ; vous pouvez donc la générer à l'aide de la fonction SDL_MapRGB() que vous connaissez bien maintenant.

  • Enfin, lorsque vous avez fini de travailler sur la surface, il ne faut pas oublier de la débloquer en appelant SDL_UnlockSurface.

  • SDL_UnlockSurface(ecran);
    
    Code résumé d'exemple

    Si on résume, vous allez voir que c'est tout simple.
    Ce code dessine un pixel rouge au milieu de la surface ecran (donc au milieu de la fenêtre).

    SDL_LockSurface(ecran); /* On bloque la surface */
    setPixel(ecran, ecran->w / 2, ecran->h / 2, SDL_MapRGB(ecran->format, 255, 0, 0)); /* On dessine un pixel rouge au milieu de l'écran */
    SDL_UnlockSurface(ecran); /* On débloque la surface*/
    

    Avec cette base vous devriez pouvoir réaliser des dégradés du vert au rouge (un indice : il faut utiliser des boucles :p ).

    La solution

    Alors, comment avez-vous trouvé le sujet ? Il n'est pas bien difficile à appréhender, il faut juste faire quelques calculs, surtout pour la réalisation du dégradé. C'est du niveau de tout le monde, il faut juste réfléchir un petit peu.

    Certains mettent plus de temps que d'autres pour trouver la solution. Si vous avez du mal, ce n'est pas bien grave. Ce qui compte, c'est de finir par y arriver. Quel que soit le projet dans lequel vous vous lancerez, vous aurez forcément des moments où il ne suffit pas de savoir programmer ; il faut aussi être logique et bien réfléchir.

    Je vous donne le code complet ci-dessous. Il est suffisamment commenté.

    #include <stdlib.h>
    #include <stdio.h>
    #include <SDL/SDL.h>
    #include <fmodex/fmod.h>
    
    #define LARGEUR_FENETRE         512 /* DOIT rester à 512 impérativement car il y a 512 barres (correspondant aux 512 floats) */
    #define HAUTEUR_FENETRE         400 /* Vous pouvez la faire varier celle-là par contre */
    #define RATIO                   (HAUTEUR_FENETRE / 255.0)
    #define DELAI_RAFRAICHISSEMENT  25 /* Temps en ms entre chaque mise à jour du graphe. 25 ms est la valeur minimale. */
    #define TAILLE_SPECTRE          512
    
    void setPixel(SDL_Surface *surface, int x, int y, Uint32 pixel);
    
    int main(int argc, char *argv[])
    {
        SDL_Surface *ecran = NULL;
        SDL_Event event;
        int continuer = 1, hauteurBarre = 0, tempsActuel = 0, tempsPrecedent = 0, i = 0, j = 0;
        float spectre[TAILLE_SPECTRE];
    
        /* Initialisation de FMOD
           ----------------------
    
           On charge FMOD, la musique on lance la lecture de la musique.
           
        */
    
        FMOD_SYSTEM *system;
        FMOD_SOUND *musique;
        FMOD_CHANNEL *canal;
        FMOD_RESULT resultat;
    
    
        FMOD_System_Create(&system);
        FMOD_System_Init(system, 1, FMOD_INIT_NORMAL, NULL);
        
        /* On ouvre la musique */
        resultat = FMOD_System_CreateSound(system, "hype_home.mp3", FMOD_SOFTWARE | FMOD_2D | FMOD_CREATESTREAM, 0, &musique);
    
        /* On vérifie si elle a bien été ouverte (IMPORTANT) */
        if (resultat != FMOD_OK)
        {
            fprintf(stderr, "Impossible de lire le fichier mp3\n");
            exit(EXIT_FAILURE);
        }
    
        /* On joue la musique */
        FMOD_System_PlaySound(system, FMOD_CHANNEL_FREE, musique, 0, NULL);
        
        /* On récupère le pointeur du canal */
        FMOD_System_GetChannel(system, 0, &canal);
    
        /* Initialisation de la SDL
           ------------------------
    
           On charge la SDL, on ouvre la fenêtre et on écrit dans sa barre de titre
           On récupère au passage un pointeur vers la surface ecran
           Qui sera la seule surface utilisée dans ce programme */
    
        SDL_Init(SDL_INIT_VIDEO);
        ecran = SDL_SetVideoMode(LARGEUR_FENETRE, HAUTEUR_FENETRE, 32, SDL_SWSURFACE | SDL_DOUBLEBUF);
        SDL_WM_SetCaption("Visualisation spectrale du son", NULL);
    
        /* Boucle principale */
    
        while (continuer)
        {
            SDL_PollEvent(&event); // On doit utiliser PollEvent car il ne faut pas attendre d'évènement de l'utilisateur pour mettre à jour la fenêtre
            switch(event.type)
            {
            case SDL_QUIT:
                continuer = 0;
                break;
            }
    
            /* On efface l'écran à chaque fois avant de dessiner le graphe (fond noir) */
            SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 0, 0, 0));
    
            /* Gestion du temps
               -----------------
               On compare le temps actuel par rapport au temps précédent (dernier passage dans la boucle)
               Si ça fait moins de 25 ms (DELAI_RAFRAICHISSEMENT)
               Alors on attend le temps qu'il faut pour qu'au moins 25 ms se soient écoulées.
               On met ensuite à jour tempsPrecedent avec le nouveau temps */
    
            tempsActuel = SDL_GetTicks();
            if (tempsActuel - tempsPrecedent < DELAI_RAFRAICHISSEMENT)
            {
                SDL_Delay(DELAI_RAFRAICHISSEMENT - (tempsActuel - tempsPrecedent));
            }
            tempsPrecedent = SDL_GetTicks();
    
            /* Dessin du spectre sonore
               ------------------------
    
               C'est la partie la plus intéressante. Il faut réfléchir un peu à la façon de dessiner pour y arriver, mais c'est tout à fait faisable (la preuve).
    
               On remplit le tableau de 512 floats via FMOD_Channel_GetSpectrum()
               On travaille ensuite pixel par pixel sur la surface ecran pour dessiner les barres.
               On fait une première boucle pour parcourir la fenêtre en largeur.
               La seconde boucle parcourt la fenêtre en hauteur pour dessiner chaque barre.
            */
            
            /* On remplit le tableau de 512 floats. J'ai choisi de m'intéresser à la sortie gauche */
            FMOD_Channel_GetSpectrum(canal, spectre, TAILLE_SPECTRE, 0,  FMOD_DSP_FFT_WINDOW_RECT);
    
            SDL_LockSurface(ecran); /* On bloque la surface ecran car on va directement modifier ses pixels */
    
            /* BOUCLE 1 : on parcourt la fenêtre en largeur (pour chaque barre verticale) */
            for (i = 0 ; i < LARGEUR_FENETRE ; i++)
            {
                /* On calcule la hauteur de la barre verticale qu'on va dessiner.
                   spectre[i] nous renvoie un nombre entre 0 et 1 qu'on multiplie par 20 pour zoomer afin de voir un peu mieux (comme je vous avais dit).
                   On multiplie ensuite par HAUTEUR_FENETRE pour que la barre soit agrandie par rapport à la taille de la fenêtre. */
                hauteurBarre = spectre[i] * 20 * HAUTEUR_FENETRE;
    
                /* On vérifie que la barre ne dépasse pas la hauteur de la fenêtre
                   Si tel est le cas on coupe la barre au niveau de la hauteur de la fenêtre. */
                if (hauteurBarre > HAUTEUR_FENETRE)
                    hauteurBarre = HAUTEUR_FENETRE;
    
                /* BOUCLE 2 : on parcourt en hauteur la barre verticale pour la dessiner */
                for (j = HAUTEUR_FENETRE - hauteurBarre ; j < HAUTEUR_FENETRE ; j++)
                {
                    /* On dessine chaque pixel de la barre à la bonne couleur.
                       On fait simplement varier le rouge et le vert, chacun dans un sens différent.
    
                       j ne varie pas entre 0 et 255 mais entre 0 et HAUTEUR_FENETRE.
                       Si on veut l'adapter proportionnellement à la hauteur de la fenêtre, il suffit de faire le calcul j / RATIO, où RATIO vaut (HAUTEUR_FENETRE / 255.0).
                       J'ai dû réfléchir 2-3 minutes pour trouver le bon calcul à faire, mais c'est du niveau de tout le monde. Il suffit de réfléchir un tout petit peu */
                    setPixel(ecran, i, j, SDL_MapRGB(ecran->format, 255 - (j / RATIO), j / RATIO, 0));
                }
            }
    
            SDL_UnlockSurface(ecran); /* On a fini de travailler sur l'écran, on débloque la surface */
    
            SDL_Flip(ecran);
        }
    
        /* Le programme se termine.
           On libère la musique de la mémoire
           et on ferme FMOD et SDL */
        
        FMOD_Sound_Release(musique);
        FMOD_System_Close(system);
        FMOD_System_Release(system);
    
        SDL_Quit();
    
        return EXIT_SUCCESS;
    }
    
    /* La fonction setPixel permet de dessiner pixel par pixel dans une surface */
    void setPixel(SDL_Surface *surface, int x, int y, Uint32 pixel)
    {
        int bpp = surface->format->BytesPerPixel;
    
        Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp;
    
        switch(bpp) {
        case 1:
            *p = pixel;
            break;
    
        case 2:
            *(Uint16 *)p = pixel;
            break;
    
        case 3:
            if(SDL_BYTEORDER == SDL_BIG_ENDIAN) {
                p[0] = (pixel >> 16) & 0xff;
                p[1] = (pixel >> 8) & 0xff;
                p[2] = pixel & 0xff;
            } else {
                p[0] = pixel & 0xff;
                p[1] = (pixel >> 8) & 0xff;
                p[2] = (pixel >> 16) & 0xff;
            }
            break;
    
        case 4:
            *(Uint32 *)p = pixel;
            break;
        }
    }
    

    Vous devriez obtenir un résultat correspondant à la figure suivante.

    Visualisation spectrale d'un son

    Bien entendu, il vaut mieux une animation pour apprécier ce résultat. C'est donc ce que je vous propose de visualiser.

    Voir l'animation "Visualisation spectrale du son" (4,3 Mo)

    Notez que la compression a réduit la qualité du son et le nombre d'images par seconde.
    Le mieux est encore de télécharger le programme complet (avec son code source) pour tester chez soi. Vous pourrez ainsi apprécier le programme dans les meilleures conditions. :)

    Télécharger le projet Code::Blocks complet + l'exécutable Windows (335 Ko)

    Idées d'amélioration

    Il est toujours possible d'améliorer un programme. Ici, j'ai par exemple des tonnes d'idées d'extensions qui pourraient aboutir à la création d'un véritable petit lecteur MP3.

    • Il serait bien qu'on puisse choisir le MP3 qu'on veut lire. Il faudrait par exemple lister tous les .mp3 présents dans le dossier du programme. Nous n'avons pas vu comment faire ça, mais vous pouvez le découvrir par vous-mêmes. Un indice : utilisez la librairie dirent (il faudra inclure dirent.h). À vous de chercher des informations sur le web pour savoir comment l'utiliser.

    • Si votre programme était capable de lire et gérer les playlists, ça serait encore mieux. Il existe plusieurs formats de playlist, le plus connu est le format M3U.

    • Vous pourriez afficher le nom du MP3 en cours de lecture dans la fenêtre (il faudra utiliser SDL_ttf).

    • Vous pourriez afficher un indicateur pour qu'on sache où en est la lecture du morceau, comme cela se fait sur la plupart des lecteurs MP3.

    • Vous pourriez aussi proposer de modifier le volume de lecture.

    • etc.

    Bref, il y a beaucoup à faire. Vous avez la possibilité de créer de beaux lecteurs, il ne tient plus qu'à vous de les coder !

    Exemple de certificat de réussite
    Exemple de certificat de réussite