P
'
t
i
t
e
C
h
a
t
t
e
 
spacer~ NORMAL IS JUST A SETTING ON THE DRYER Articles | Connexion
 
~Sauvegardes et son pour la GBA

Précédent  
  GBA  
  Suivant
 Présentation

Jusqu'à présent, les articles présentés dans ces colonnes se focalisaient principalement sur les fonctions graphiques de la GameBoy Advance. Maintenant que nous savons correctement les programmer, appliquons-nous à mettre en oeuvre le son et les sauvegardes.
 Sommaire


 Introduction

A l'heure actuelle, de nombreux jeux laissent au joueur le loisir de sauvegarder leur partie à tout moment et de la reprendre ensuite à l'endroit exact où il l'avait laissée. Toutefois, certains développeurs optent pour d'obscurs systèmes de mots de passe. Lorsque vous aurez appris à manipuler les sauvegardes de la GameBoy Advance, vous comprendrez qu'il ne s'agit que d'un artifice destiné à augmenter artificiellement la durée de vie des jeux. Sauvegarder des données dans une cartouche s'avère en effet d'une déconcertante facilitée.


 Le mappage mémoire de la machine

Avant de nous atteler à notre tâche, rappelons la structure de la mémoire de la console. Celle-ci se décompose en deux grandes parties appelées respectivement la mémoire interne et la mémoire du Game Pak (nom officiel des cartouches).

La première contient la ROM du système, occupant 16 kilo-octets, la RAM externe de travail pour le processeur, d'un poids de 256 Kilo-octets, la RAM interne du processeur, qui pèse 32 ko, les différents registres, la palette de couleurs s'étendant sur 1 ko, la VRAM, d'une taille de 96 ko et enfin la mémoire OAM d'un Kilo-octet. Cette dernière, ainsi que la VRAM, sont employés pour le stockage des sprites et des images.

La seconde mémoire, celle du jeu, peut adresser 3 blocs de ROM et un bloc de RAM. Les premiers occupent toujours 4 Méga-octets. C'est pourquoi si vous avez la curiosité d'aspirer le contenu d'une cartouche commerciale, vous obtiendrez toujours une image binaire de 4, 8 ou 16 Méga-octets. Enfin nous en arrivons à la section de mémoire qui nous intéresse, la Game Pak RAM.

Occupant toutes les adresses comprises entre 0xE000000h et 0xE00FFFFh, celle-ci met à disposition du programmeur 64 Kilo-octets libres de toute contrainte. Le contenu de cette RAM est sauvegardé sur la cartouche par une pile.


 Manipulation de la Game Pak RAM

Le principe de l'ensemble des manipulations relatives à la Game Pak RAM (également intitulée SRAM) consiste à lire et écrire des octets à partir de l'adresse de départ de cette mémoire spéciale. Nous devons donc simplement la définir puis écrire quelques fonctions relativement simples :

#define SRAM 0xE000000
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
Pour écrire un octet dans la SRAM nous pouvons à présent simplement rédiger la ligne de code suivant :

*(u8 *) SRAM = 0xCA;
       
      
JextCopier dans Jext
Vous constaterez que la déclaration de l'adresse de la mémoire de sauvegarde ne définit pas un pointeur ainsi que nous l'avions fait pour l'écran :

#define REG_DISPCNT *(u32*) 0x4000000
       
      
JextCopier dans Jext
En effet, nous souhaitons pouvoir lire et écrire des données à une position donnée à partir de l'adresse 0xE000000h, comme dans l'exemple suivant :

*(u8 *) (SRAM + 1) = 0xFE;
*(u8 *) (SRAM + 2) = 0xBA;
*(u8 *) (SRAM + 3) = 0xBE;
       
      
JextCopier dans Jext
A l'aide de quatre instructions, nous avons pu sauvegarder la valeur 0xCAFEBABEh. L'opération de lecture permettant de récupérer nos données ne demande que peu d'efforts :

u8 magic = *(u8 *) (SRAM + 2);
       
      
JextCopier dans Jext
La ligne de code précédente assigne la valeur 0xBAh à la variable nommée "magic". En conclusion, lire et écrire un octet dans la zone de sauvegarde ne représente aucun problème que ce soit. Les choses deviennent plus compliquées dès lors que nous désirons opérer sur des entiers (type de données finalement très couramment employé dans nos programmes). Le listing 1 présente la fonction de sauvegarde d'une variable de ce type.

/* Listing 1 */
void saveByte(u16 offset, u8 value)
{
  *(u8 *) (SRAM + offset) = value;
}

void saveInt(u16 offset, int value)
{
  u8 b1 = (u8) ((value) & 0xFF);
  u8 b2 = (u8) ((value >>  8) & 0xFF);
  u8 b3 = (u8) ((value >> 16) & 0xFF);
  u8 b4 = (u8) ((value >> 24));

  saveByte(offset + 0, b1);
  saveByte(offset + 1, b2);
  saveByte(offset + 2, b3);
  saveByte(offset + 3, b4);
}
       
      
JextCopier dans Jext
La fonction saveInt() parvient à son but en décomposant l'entier en quatre segments de 8 bits. Souvenons-nous en effet que le type "int" de la GameBoy Advance occupe 32 bits en mémoire, nous pouvons donc le découper en 4 octets successifs. Une fois le découpage achevé, la fonction invoque l'une de ses soeurs, saveByte(), en lui demandant de sauvegarder les quatre octets obtenus les uns à la suite des autres en jouant sur le décalage de l'adresse. Nous constatons ici l'utilité du positionnement évoqué précédemment.

Bien évidemment, nous devons être en mesure de récupérer les entiers enregistrés de la sorte. Le listing 2 présente un extrait de l'exemple du CD-Rom destiné à cela. De nouveau, notre code source repose sur des opérations relatives aux octets. Après avoir lu 4 octets consécutifs, notre fonction les combine en une unique séquence de 32 bits pour former un entier. Nous pouvons imaginer réaliser de la même manière des fonctions capables de lire et écrire des float (32 bits), des double (64 bits), des long (32 bits) ou des short (16 bits). Vous ne devriez avoir aucun mal à écrire ces dernières par vous-mêmes en vous basant sur les listings de cet article. Le code source présent sur le CD-Rom du magazine présente également des fonctions de traitement des chaînes de caractères.

/* Listing 2 */
u8 loadByte(u16 offset)
{
  return *(u8 *) (SRAM + offset);
}

int loadInt(u16 offset)
{
  u8 b1 = loadByte(offset + 0);
  u8 b2 = loadByte(offset + 1);
  u8 b3 = loadByte(offset + 2);
  u8 b4 = loadByte(offset + 3);
  return (int) ((b1 & 0xFF) | ((b2 << 8) & 0xFF00) | ((b3 << 16) & 0xFF0000) | (b4 << 24));
}
       
      
JextCopier dans Jext
Toutefois, leur cas ne nécessite aucune explication puisque chaque caractère d'une chaîne occupe exactement un octet. Leur implémentation se révèle ainsi particulièrement triviale.


 Utilisation des fonctions de sauvegarde

A l'exemple développé les mois précédents, nous avons ce mois-ci adjoint une fonctionnalité de reprise du jeu après extinction de la console. Lorsque le joueur presse la touche Start de sa machine, le jeu enregistre divers paramètres, exactement la position du sprite, de la carte et l'angle de rotation du tout. Pendant l'initialisation, notre programme charge ses informations en mémoire pour replacer le joueur dans sa situation passée.

Le processus de sauvegarde de l'état du jeu se voit présenté au sein du listing 3. Les valeurs employées en tant que premiers arguments correspondent à des constantes définies au début du fichier main.cpp. Celles-ci définissent la position, en octets, dans la Game Pak RAM où le logiciel doit écrire les états. N'oubliez pas qu'un entier occupe 4 octets. C'est pourquoi lorsque nous écrivons l'angle de rotation en huitième position, nous plaçons l'abscisse de la carte en douzième position, l'angle étant représenté par une variable de type "int". La première ligne de la fonction saveState() en interpellera peut-être plus d'un. Pourquoi diable sauvegardons-nous la chaîne "wing" ? La réponse réside dans le listing 4 qui présente la fonction inverse, destinée à restaurer l'état du programme.

/* Listing 3 */
void saveState()
{
  saveString(OFFSET_IS_SAVE, "wing");
  saveByte(OFFSET_XWING_X, (u8) xwing->x);
  saveByte(OFFSET_XWING_Y, (u8) xwing->y);
  saveInt(OFFSET_XWING_ANGLE, xwing->angle);
  saveInt(OFFSET_BACKGROUND_X, bg2->xScroll);
  saveInt(OFFSET_BACKGROUND_Y, bg2->yScroll);
}
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
/* Listing 4 */
void loadState()
{
  char* magic = new char[5];
  loadString(OFFSET_IS_SAVE, 4, magic);

  if (strcmp(magic, "wing") == 0)
  {
    xwing->x = (int) loadByte(OFFSET_XWING_X);
    xwing->y = (int) loadByte(OFFSET_XWING_Y);

    xwing->angle = loadInt(OFFSET_XWING_ANGLE);

    bg2->xScroll = loadInt(OFFSET_BACKGROUND_X);
    bg2->yScroll = loadInt(OFFSET_BACKGROUND_Y);
  }
  delete[] magic;
}
       
      
JextCopier dans Jext
Pour éviter tout état incohérent, nous devons nous assurer que notre programme n'essayera pas de charger des valeurs depuis la SRAM tandis qu'aucune sauvegarde n'a encore été réalisée. C'est pourquoi l'exemple présenté ici enregistre la chaîne "wing" sur les 4 premiers octets de la mémoire de sauvegarde.

Quand nous invoquons la fonction de chargement, nous vérifions la présence de cette chaîne. Si le test réussit, nous savons qu'une sauvegarde a déjà été réalisée et nous pouvons opérer le charment des états sans crainte. Soyez très attentifs au cours de vos développements si vous modifiez les positions des enregistrements. Pensez dans ce cas à effacer les fichiers créés par les émulateurs ou à implémenter une fonction d'effacement de la SRAM.

Vous pouvez aisément vous retrouver avec des valeurs incorrectes puisque le contenu de cette mémoire ne se voit supprimée que lors du retrait de la pile de la cartouche.


 Les capacités sonores de la GBA

Notre console préférée offre d'impressionnantes capacités sonores. En plus des 4 canaux sonores hérités de la GameBoy Color, le programmeur peut employer deux DAC, ou convertisseurs digitaux/analogiques, 8 bits. Ces derniers permettent par exemple de jouer facilement des voix ou des musiques au format PCM par l'entremise du mode Direct Sound. Une amélioration apportée par cette machine concerne le troisième canal sonore qui permet de jouer des échantillons de sons. Le chargement d'un nouvel échantillon bénéficie à présent d'un système de prévention de la distorsion sonore. Enfin, le développeur peut à loisir manipuler les sorties droite et gauche du système puisque ce dernier offre un son stéréo.

Notre initiation au GBA Sound System portera sur la manipulation du premier canal sonore. Comme à l'accoutumée, la rédaction du code source sera précédée par une étude des registres requis. Ce canal produit des ondes carrées de durée de cycle aléatoire (le "duty"), avec modificateur de fréquence (le "sweep") et employant une fonction enveloppe. Le sweep permet de réaliser des accroissements ou des réductions de la fréquence durant la lecture du son. La quantité et la vitesse des altérations sont contrôlables par la programmation.

Sur les trois registres correspondant au premier canal sonore, REG_SOUND1CNT_L a pour rôle de contrôler le sweep. Le second registre, REG_SOUND1CNT_H permet de manipuler le duty et l'enveloppe. Pour finir, REG_SOUND1CNT_X contrôle la fréquence du son.

En outre, nous allons devoir modifier trois registres supplémentaires, relatifs à l'ensemble du système sonore. REG_SOUNDCNT_L contrôle le volume sonore et les effets de stéréo tandis que REG_SOUNDCNT_H s'occupe du mode Direct Sound et du niveau de sortie des canaux principaux. Enfin, REG_SOUNDCNT_X active ou non la lecture de ces derniers.


 Les registres communs

Ce dernier registre se révèle extrêmement simple. Les quatre premiers bits correspondent aux quatre canaux; quand un bit prend la valeur 1, le canal est activé. Le huitième bit se trouve être l'unique autre bit employé parmi les 16 composant ce registre. S'il contient 1 tous les canaux deviennent actifs.

Le registre de contrôle du volume sonore et de la stéréo ne représente pas de grande difficulté non plus. Les trois premiers bits définissent le volume de la sortie droite. Les bits 4 à 6 modifient celui de la sortie gauche. Les 8 bits de poids fort activent les sorties droite et gauche pour les différents canaux : les quatre premiers bits autorisent ou non la voie de droite de ceux-ci et les quatre derniers bits font de même pour la celle de gauche.

Enfin, seuls 2 bits de REG_SOUNDCNT_H s'avèrent relatifs à nos canaux. Les valeurs 00, 01 et 10 placent le niveau de ces derniers à 25%, 50% ou 100%. Le code source suivant initialise le GBASS en tous les canaux avec un niveau de 100% et les voies stéréo au volume maximal :

REG_SOUNDCNT_X = OPERATE_SOUND_ALL;
REG_SOUNDCNT_L = SOUND1_L_OUTPUT | SOUND1_R_OUTPUT | SOUNDLEVEL_L(7) | SOUNDLEVEL_R(7);
REG_SOUNDCNT_H = OUTPUT_RATIO_FULL;
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
Les macros et définitions employées dans ces quelques lignes se trouvent dans le fichier sound.h de l'exemple du CD-Rom.


 Le canal 1

Le modificateur de fréquence repose sur un intervalle de décalage particulier. Chaque fois que cet intervalle de temps s'écoule, la fréquence du son se trouve augmentée ou diminuée. Le contrôle du sweep se voit assuré par les 7 bits de poids faible du registre REG_SOUND1CNT_L. Les trois premiers bits définissent le taux de modification de la fréquence (valeur comprise entre 1 et 7). Le suivant spécifie l'opération d'augmentation (valeur 0) ou de diminution (valeur 1). Quant aux trois derniers, ils accueillent l'intervalle de changement. Les différentes valeurs se trouvent exposées dans le tableau 1. Le temps est calculé de la sorte : (valeur décimale des trois bits) / 128 KHz. La formule exacte employée pour la modification de la fréquence se trouve décrite par le schéma 1.

Tableau 1
ValeurIntervalle (ms)
000Aucun
0017.8
01015.6
01123.4
10031.3
10139.1
11046.9
11154.7



Schéma 1. Calcul de la nouvelle fréquence en fonction du sweep.

La fonction d'enveloppe autorise la réalisation d'effets de fondus sonores. Sa résolution de 4 bits lui permet de générer 16 niveaux d'amplitude différents. Il s'agit du niveau initial de l'enveloppe paramétré par les bits 12 à 15. Le délai entre deux changements d'amplitude se trouve spécifié dans les bits 8 à 10 de REG_SOUND1CNT_H. La durée d'un délai se calcule grâce à la formule T = valeur du registre * 1 / 64. En positionnant le bit 11 à 1, nous demandons un changement croissant au lieu de décroissant. Les bits 6 et 7 déterminent le temps pendant lequel le signal est actif au regarde d'une période complète. Les valeurs possibles sont 11, 10, 01 et 00 pour respectivement 7 5% de la période, 50%, 25% ou 12.5%. Vous pouvez également définir la durée du son par l'entremise des bits 0 à 5. La durée se détermine de la sorte : durée = (64 - valeur des bits) * 1 / 256.



Schéma 2. La fonction enveloppe atténue le son.

Rien ne pourrait se produire si nous ne faisions appel à une fréquence de base. En modifiant les bits 0 à 10 de REG_SOUND1CNT_X, nous la choisissons selon la formule qui suit : fréquence (Hz) = 4194394 / (32 * (2048 - valeur des bits)). La fréquence se révèle au final comprise entre 64 Hz et 131.1 KHz. Dans ce même registre, quand le bit 15 prend la valeur 1 la console lance la lecture du son. Le dernier bit utilisé par la machine dans ce registre correspond à la méthode de lecture. Si le bit 14 prend la valeur 1, le son se trouve joué pour la durée précisée dans le registre REG_SOUND1CNT_H. Dans le cas contraire, il est joué en continu.



Schéma 3. Effets du sweep sur la courbe sonore en fonction du temps.

Voici un exemple concret de son dans un jeu GameBoy Advance :

REG_SOUND1CNT_L = SOUND1SWEEPSHIFT(6) | SOUND1SWEEPTIME(5) | SOUND1SWEEPINC;
REG_SOUND1CNT_H = SOUNDDUTY50 | SOUND1ENVDEC | SOUND1ENVINIT(12);
REG_SOUND1CNT_X = 0x0400 | SOUND1INIT;
       
      
JextCopier dans Jext | Jext | Plugin Codegeek


Schéma 4. Influence du pourcentage de duty sur la courbe sonore.

Les fois prochaines, nous découvrirons les canaux restants ainsi que le mode Direct Sound. D'ici là, vous pouvez vous occuper en essayant les diverses combinaisons des paramètres du canal sonore 1.


 Téléchargements

Nous vous invitons à télécharger l'exemple de ce cours.



par Romain Guy
romain.guy@jext.org
http://www.jext.org
Dernière mise à jour : 14/10/2006


Précédent  
  GBA  
  Suivant

 
#ProgX©2005 Mathieu GINOD - Romain GUY - Erik LOUISE