1. Introduction

Nous savons que notre console favorite offre cinq moyens distincts pour produire du son : quatre canaux et un mode particulier intitulé Direct Sound. A l'instar du premier canal, le deuxième produit des ondes carrées de durée de cycle aléatoire, employant une fonction enveloppe. Toutefois, celui-ci ne possède pas de modificateur de fréquence. Son utilisation ne nécessite donc que de changer les registres employés. Le listing 1 propose un exemple de son émit par le canal 2.

 
Sélectionnez

/* Listing 1 */
REG_SOUNDCNT_X = OPERATE_SOUND_ALL;
REG_SOUNDCNT_L = SOUND2_L_OUTPUT | SOUND2_R_OUTPUT |
  SOUNDLEVEL_L(7) | SOUNDLEVEL_R(7);
REG_SOUNDCNT_H = OUTPUT_RATIO_FULL;
  
REG_SOUND2CNT_L = SOUNDDUTY50 | SOUND2ENVDEC |
  SOUND2ENVINIT(12);
REG_SOUND2CNT_H = 0x0400 | SOUND2INIT | SOUND2ONCE;

Penchons-nous à présent sur le mode Direct Sound, bien plus intéressant pour le programmeur.

2. Le mode Direct Sound

La console de Nintendo offre la possibilité de jouer des sons au format PCM, d'une fréquence quelconque, contenant des samples de 8 bits en mono ou stéréo. Pour ce faire, elle dispose de deux convertisseurs digital/analogique (DAC) nommés Direct Sound A et B. La lecture de données sonores en stéréo implique leur utilisation simultanée, un par voie. En mono seul l'un d'entre eux sera requis.

Pour charger les samples, le matériel lit les données se trouvant aux adresses 0x40000A0 et 0x40000A4. Ces adresses désignent des files de type FIFO, intitulée A et B. Leur présence explique la capacité de la GBA à lire du son en stéréo puisqu'à chaque DAC peut se voir associé un FIFO particulier. La fréquence de lecture des sons se contrôle par l'intermédiaire des compteurs des timers 0 et 1. De ce fait, chaque voie en mode stéréo peut se voir jouée à une fréquence distincte. Enfin, sachez qu'il existe deux méthodes d'utilisation du mode Direct Sound : par DMA et par interruption. Nous préférerons la première solution, bien plus efficace, car entièrement exécutée par la machine.

La mise en place du mode Direct Sound nécessite l'utilisation du registre REG_SOUNDCNT_H situé à l'adresse 0x4000082. Les deux premiers bits de ce registre ne nous sont pas inconnus puisque nous nous en sommes déjà servis pour paramétrer le niveau de sortie du canal 1. Les bits deux et trois déterminent le niveau des DAC A et B. Quand ils prennent la valeur 1, le niveau est à 100%, sinon à 50%. Ensuite, les bits 4 à 7 n'ont aucune utilité. Les sorties sonores droite et gauche du Direct Sound A s'activent quand la valeur 1 se trouve attribuée aux bits 8 et 9. Le bit 10 quant à lui désigne le timer lié au canal A : 1 pour le timer 1 et 0 pour le timer 0. Enfin l'attribution de la valeur 1 au bit 11 remet à zéro le contenu du FIFO A. Les bits restants, de 12 à 15, fonctionnent exactement de la même manière que les quatre précédents mais s'appliquent au DAC B. L'ensemble de ces informations nous permet à présent d'activer le chipset sonore de manière adéquate :

 
Sélectionnez

REG_SOUNDCNT_X = OPERATE_SOUND_ALL;
REG_SOUNDCNT_L = SOUNDLEVEL_L(7) | SOUNDLEVEL_R(7);
REG_SOUNDCNT_H = DSOUND_A_FIFO | DSOUND_A_FULL |
  DSOUND_A_L_OUTPUT | DSOUND_A_R_OUTPUT;

Dans le cas présent, nous ne ferons appel qu'au seul DAC A, lié au timer 0. L'étape suivante de la lecture du son consiste à préparer le DMA pour le transfert des données dans le FIFO A. Auparavant, nous devons avoir incorporé notre fichier sonore dans la ROM du jeu. En nous munissant d'un fichier d'extension .wav encodé en PCM 16 Khz 8 bits mono, nous pouvons faire appel à l'utilitaire objcopy de GCC pour le transformer en fichier ELF :

 
Sélectionnez

objcopy -I binary -O elf32-little sound.wav sound.o

Ensuite, le Makefile doit être modifié en conséquence pour lier ce nouvel objet à l'exécutable final. Après conversion, un nouveau tableau de données 32 bits est accessible : _binary_sound_wav_start[]. La déclaration suivante donne l'adresse de notre fichier sonore :

 
Sélectionnez

extern const u32 _binary_sound_wav_start[];

Nous avons alors l'adresse source de la copie DMA. Vous aurez compris que nous souhaitons transférer les données de cette adresse vers le FIFO A. Pour ce faire, le DMA nécessite un paramétrage particulier. Chaque transfert traitera 32 bits, l'adresse source sera ensuite incrémentée tandis que l'adresse de destination restera fixe. En outre, nous devons activer le mode de répétition et demander le départ du transfert à chaque requête FIFO. Cette dernière option s'applique aux bits 12 et 13 du registre REG_DMA1CNT. Nous avions appris que la valeur 11 se révélait interdite. Il existe une exception lorsque l'adresse de destination est celle d'un FIFO. De plus, nous devons impérativement choisir le mode répétition, paramétré par le bit 9. Enfin, notez que Direct Sound ne peut employer que les canaux DMA 1 et 2. La préparation du transfert requière la rédaction de ces quelques lignes :

 
Sélectionnez

REG_DMA1SAD = (u32) _binary_sound_wav_start;
REG_DMA1DAD = 0x40000A0;
REG_DMA1CNT = DMA_ENABLE | DMA_TIMEING_FIFO | DMA_32 |
  DMA_REPEAT | DMA_SOURCE_INCREMENT | DMA_DEST_FIXED;

A présent, le développeur n'a plus qu'à activer le timer 0 pour exécuter la lecture du son. Avant de modifier le registre REG_TM0CNT à cet effet, le DAC doit connaître la fréquence de lecture du son. D'habitude, les registres REG_TM*D accueillent un compteur incrémenté à l'écoulement de chaque intervalle de temps d'une durée précisée par REG_TM*CNT. Dans le cas des timers 0 et 1, ce compteur correspond en réalité à cette fameuse fréquence. Cette valeur doit être renseignée par le programmeur lui-même et provient de la formule suivante :

 
Sélectionnez

REG_TM0D = 0xFFFF - round(2^24 / fréquence)

Dans notre exemple, le calcul sera : 0xFFFF - round(2^24 / 16 000) = 0xFBE8. Jouons donc le son :

 
Sélectionnez

REG_TM0D = PLAY_16KHZ;
REG_TM0CNT = TIMER_ENABLE;

3. Interruptions

La méthode exposée ci-dessus propose les meilleures performances. Malheureusement, lorsqu'un programme fait appel aux interruptions matérielles des problèmes peuvent apparaître. Si d'aventure une interruption survient au beau milieu d'une opération de transfert DMA, celle-ci ne se verra traitée qu'à la fin de celle-là. Ceci s'avère particulièrement dans le cadre de jeu multi-joueurs dont les communications fonctionnent suivant ce principe. De ce fait, si les données reçues ne sont pas lues assez vite, d'autres pourront venir les écraser. Une solution couramment employée pour circonvenir à cet inconvénient consiste à implémenter le mode Direct Sound par interruption. Dans ce cas, le timer contenant la fréquence de lecture doit être paramétré pour générer des interruptions. C'est alors le gestionnaire d'interruption qui chargera les données dans le FIFO adéquat. La valeur 1 dans le registre REG_IME active les interruptions. Celles que nous désirons recevoir se trouvent définies par le registre REG_IE (par exemple REG_IE = IRQ_TM0, référez-vous au fichier main.h). Enfin, les interruptions en cours sont spécifiées dans le registre REG_IF. Reportez-vous au listing 2 pour un exemple d'interruption lors de l'appui sur une touche.

 
Sélectionnez

/* Listing 2 */
void interruptProcess(void) __attribute__ ((section(".iwram")));
void interruptProcess()
{
  if (REG_IF & IRQ_KEY)
    // on a appuyé sur une touche

  // on efface les interruptions
  REG_IF |= REG_IF;
}
// ...
REG_IE = IRQ_KEY;
REG_IME = 1;

4. Téléchargements

L'exemple de cet article est disponible en téléchargement.

Version PDF de l'article (22 Ko).

Liens :