> Hardware |
PrograZine issue #10 - http://www.citeweb.net/discase/10/sbdsp.htm | Edito | Sommaire | Contribution | Contacts |
Programmation de la SoundBlaster (DSP)
par Plouf - plouf@win.be http://www.citeweb.net/pmplayer |
Initié | DOS | Pascal/ASM |
Ce texte s'adresse à des personnes ayant déjà une connaissance des IRQ et des INT, j'écrirai en parallèle un tutoriel concernant ces "matières" pour les autres :) Les codes exemples que je fournirai sont en pascal, parce que tout mon moteur son est programmé dedans, ça sera donc plus facile pour moi... Dans ce texte, je me limite au son 8bit-mono. Je mes en fin de textes toutes les références permettant d'approfondir pour aller plus haut. Le programme fourni (normalement) avec ce texte est une ébauche de player .Wav, qui ne prend également que les wavs 8bit-mono. Je l'ai testé avec un wav de 9mo (long, hein? :)) mais pas avec un tout petit
Comment le son est-il stocké dans un fichier? Et bien c'est vraiment tout bête : pour le 8bit-mono, chaque octet contient en fait la hauteur de l'onde. Par exemple
Le son stéréo est fort semblable, sauf qu'un octet sur deux est pour le canal gauche, et l'autre est pour le canal droit. Donc chaque octet va par paire, et jouer a 22050hz signifiera jouer 22050 paires par secondes, et donc 44100octets par secondes. Même résonement pour le 16bit, et pour le 16bit stéréo, ce qui fait qu'à la vitesse de 48000hz, en 16bit stéréo, la carte joue 192000 octets par seconde. Considérable, surtout pour le programme qui doit les lui donner :)
Comment fonctionne une sb? Bah c'est facile, en fait le DSP (digital sound processor) lit une suite d'octets et la joue sur les speakers, à nous de lui fournir cette liste. Bien sûr, il est impossible de tout lui envoyer d'un coup, donc il doit être capable de nous signaler qu'il a fini la suite et qu'il en attend une autre. Le dsp fonctionne par paquets d'octets, donc par buffers : je lui envoie un buffer, il le joue, et il me rappelle quand il a fini pour que je lui renvoie un autre buffer, etc. La meilleure façon de nous le signaler, sans pour autant que le programme doivent sans arrêt l'interroger, est de générer une IRQ en fin de buffer. A nous de patcher l'int correspondant à cette irq... et de savoir quoi y faire! On pourrait croire que le DSP possède une mémoire interne dans laquelle nous allons écrire. En réalité il n'en est rien, c'est l'inverse, c'est nous qui allons dire à la carte où aller chercher les données dans notre zone mémoire à nous, donc en mémoire conventionnelle. Bien sur, la carte n'a pas accès directement à cette mémoire, et c'est la qu'un pote va venir nous aider : le DMA (direct access memory). C'est lui qui sert d'intermédiaire entre la mémoire et la carte. Donc on peut résumer comme ceci :
Pour cela on a besoin de trois renseignements importants : le port E/S de la carte, son canal DMA et l'IRQ qu'elle utilise, d'où l'éternelle questions des programmes sous dos : "port,irq,dma?". Normalement cette question n'est pas obligatoire car la variable d'environnement BLASTER contient ces infos sous la syntaxe suivante :
Bien sûr on peut aussi faire de l'autodetection mais c'est un processus long et périlleux que je n'aborderai pas ici.
Imaginons à présent ce qui se passe quand la carte à fini de jouer et que par conséquent elle nous appelle via IRQ. Nous prenons la main, mettons à jour la zone mémoire que nous avons fourni au DMA, et... et rien ne se passe, plus de son, plus un bruit, la carte s'est arrêté. Et c'est normal, on lui a juste dit "joue ceci", et pas "joue ceci est recommence". Donc nous reprogrammons la carte (et le dma par la même occasion), et tout repart, ouf. Ben non, pas ouf du tout, car entre le moment où la carte nous a appelé, et celui où elle repart, il y a un temps non négligeable (à l'échelle du cpu s'entend), ce qui provoque un superbe "tick" dans les speakers, l'idéal. Il faudrait lui expliquer qu'elle doit repartir tout de suite, donc de s'auto-réinitialiser à chaque fin de buffer, pour qu'elle boucle une fois qu'elle arrive à la fin. Et bien croyez-le ou non mais ça existe :) Et c'est juste ce qu'il nous faut... ...ou presque, car rien à faire, elle s'est quand même arrêté un tout petit laps de temps, pendant le remplissage du buffer. Certes le "tick" est bien moins fort mais toujours là, et est d'autant plus fort que le remplissage est long (et donc que l'ordi est lent). En réalité, il est temps de préciser les choses : l'autoinitialize, c'est la carte qui se relance a la fin du buffer, et c'est le dma qui boucle l'offset du buffer. La carte n'a rien à voir avec l'offset, elle se contente juste de demander au dma "c'est quoi la suite?". La solution est donc simple, il faut signaler a la carte un buffer deux fois plus petit que celui que nous signalons au dma. Ainsi, une fois arrivé à la moitié du buffer, la carte croit qu'elle est arrivé à terme et nous appelle via l'IRQ, pendant qu'elle continue sur sa lancée. Le dma, lui, n'est pas arrivé à la fin du buffer, et continue dans la seconde moitié, pendant que nous, dans l'irq, nous mettons à jour la première moitié. Et une fois que la carte aura a nouveau fini son buffer (donc la seconde moitié) et que par conséquent elle nous appelle, le DMA va cycler (car cette fois-ci on est vraiment à la fin), nous laissant le champ libre pour mettre à jour la seconde moitié, et ainsi de suite... Si vous n'avez pas compris (pasque ça n'est pas facile), relisez autant de fois qu'il le faudra car c'est capital :) Schémas moche en ascii qui va peut-être vous aider :
Donc il faut avoir deux pointers, un vers la première moitié, l'autre vers la seconde. Dans l'irq, la première fois, on écrira dans le premier (qui vient juste d'être fini, le second débute), la seconde fois dans le second (le premier repart), la troisième fois dans le premier, etc. En fait il suffit d'avoir deux pointers que l'on SWAP à chaque appel irq.
Ici la question se pose : que lui raconter? Et comment? Le dma, comme tous les périphériques, s'adresse via les ports E/S. Voici les trois ports qui vont nous servir. Rassurez-vous, rien de dur, j'explique après :)
Envoyer un 0 sur ce port reset le pointeur du canal. Ensuite pour parler aux canaux eux-mêmes, bah ya des autres ports, ici l'ennui c'est que en fonction du canal, ca change. Voici le tableau
Bon, pas trop paniqué? :) Alors évidemment la question c'est : "mais qu'est-ce qu'il faut faire avec tout ca?" C'est facile, voici les étapes à suivre, la recette donc :
Pfou! La le DMA est prêt à servir, reste à programmer la carte :)
Pour communiquer avec le DSP, il faut aussi passer par des ports E/S. Le fameux $220 fait croire qu'il n'y en a qu'un, en réalité port=$220 veut dire que le DSP s'adresse en utilisant les port de $220 à $22f, donc il y a 16ports (pas tous utilisés). Voici la liste : (x=2 -> port 220 x=3 -> port 230 etc.)
Première chose à faire : faire un reset du DSP, la suite des commandes est :
En pratique, vous devriez limiter le temps d'attente maximal au point c, car si la carte n'est pas installée ou si le port e/s est incorrect, la réponse ne viendra jamais. Quand une init son plante, c'est juste que le programmeur n'a pas prévu ce bête cas... Ne me faites pas une routine instable, par pitié :)) Au point a, attendre 3microsecondes est mission impossible, mais comme rien n'empêche d'attendre plus, ne vous en privez pas. Perso j'ai mis Delay(100) Une fois le DSP fin prêt, vous pouvez commencer à communiquer avec lui. Pour lui envoyer un octet, vous devez attendre que le bit 7 du status soit 1, et ensuite lui envoyer cet octet sur le write command. Raisonnement inverse pour la lecture, ce qui nous donne au final :
Où BasePort est le port de la carte son, donc par exemple $220. Pour parler avec le dsp, on doit d'abord lui dire ce qu'on lui veut : lui envoyer une commande, pour ensuite envoyer les valeurs, ou bien lire les valeurs, en fonction de la commande. Pour lancer le son, nous devons lui donner 4 commandes
Le reste n'est qu'un détail Vous devez créer un buffer, et deux pointers, le premiers points vers le début du buffer, le second vers le milieu
Avant de commencer à jouer, il faudrait remplir ces buffers. Je vous conseille de créer une routine Lire (ou lecture, ou n'importe quoi) qui ferait les choses suivantes : Remplir TailleBuffer octets pointés par Of1 Echanger Of1 et Of2 Heu... ben c'est tout :) Juste au début, pour remplir les deux buffers, contentez-vous d'appeler deux fois de suite Lire.
Activez ensuite l'IRQ, et patchez-la vers une routine qui fera :
Pour couper le son, il suffit de faire un reset, de stopper le son (commande $d0) et de couper les haut-parleurs (commande $d3). N'oubliez pas de libérer l'irq, bien sur, et de libérer vos buffers, etc...
Des erreurs? Oh oui, ca j'en ai sûrement dit des erreurs (j'espère mineures) dans ce texte. Si vous avez des questions, des doutes, des klougs (heu non, pas des klougs), n'hésitez pas a me mailler : plouf@skynet.be
En général
|
Cet article est la propriété de Plouf. La copie et la diffusion sont libres sauf dans un but lucratif sans accord explicite de l'auteur. |