> 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
>Préambule : le son digital

    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
  For I:=0 To 255 Do Wav[I]:=Trunc(Sin(I/255*2*Pi)*127)+128;
l
une fois envoyé à la carte lui fera jouer une belle sinusoidale, donc un son (à peu près) pur. Bien sur, tout dépendra à quelle vitesse la carte le jouera. 22050hz signifie que la carte jouera 22050octets en une seconde, et que pour avoir un buffer qui "tiendra" une seconde, il faut créer un buffer de 22050octets.

    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 :)
>Un peu de bla-bla pour avoir les idées claires

    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 :
  • Il faut commencer par se présenter à la carte, lui expliquer ce qu'on attend d'elle, bref l'initialiser. Pour cela il faut commencer par la trouver, et donc savoir via quels ports d'E/S il faut lui parler.
  • Ensuite il faut dire au DMA qu'on va avoir besoin de ses services et également l'initialiser (le tout est de rester cohérent entre ce que l'on demande à la carte et ce que l'on demande au DMA). Comme il y a plusieurs canaux DMA (pour que plusieurs periphs puissent fonctionner en même temps), il faut savoir sur lequel la carte est branchée.
  • Et puis brancher l'IRQ et patcher l'INT correspondante via laquelle la carte nous appellera. Il faut donc connaître cette IRQ.
  • Enfin donner l'ordre final à la carte : GO!


    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 :
  BLASTER=A220 I5 D1 [...]
  A pour le port (valeur en hexa, attention!)
  I pour l'irq (en décimal cette fois, donc I10 est permis)
  D pour le dma (on se fiche de savoir si c'est en décimal ou en hexa
       vu que ca va de 0 à 3 :))
  Le reste de la ligne (si reste il y a) ne nous intéresse pas.
l

    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.
>Single cycle VS auto-initialize

    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 :
------------------------------------------------------
|    Première moitié     |     Seconde moitié        |
|                        |                           |
| > > > > > > > > > >appel IRQ> > > > > > > > > >appel IRQ
|                        |                           |
| > > > > > > > > > > > >|> > > > > > > > > > >cyclage du DMA
|                        |                           |
------------------------------------------------------
\------------------------/
 taille donnée à la carte= demi buffer
\----------------------------------------------------/
  taille donnée au DMA = buffer
l

    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.
>Bon, et concrètement?
>Initialiser le DMA

    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 :)
>DMA Mask Register : $a
        bit 7 - 3 = 0  réservé
            bit 2 = 0  ferme
                  = 1  ouvre
        bits 1 - 0 = [0..3] pour s'adresser au canal [0..3]
l
Avant de programmer un canal DMA, il faut l'ouvrir, c'est avec ca qu'on le fait.
>DMA Mode Register : $b
        bit 7 - 6 = 00 Demand mode
                  = 01 Signal mode
                  = 10 Block mode
                  = 11 Cascade mode
        bit 5 - 4 = 01 réservé (*)
        bit 3 - 2 = 00 Vérification
                  = 01 Ecriture
                  = 10 Lecture
       bits 1 - 0 = [0..] pour s'adresser au canal [0..3]
      Explique au dma ce qu'on lui veut, à la fin...
      (*) C'est ici que ca foire, ma documentation me dit 00, mais manifestement
          ca ne fonctionne pas, il faut mettre 01, a vous de voir...
l
>DMA clear byte ptr : $c

     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
      Canal    Page    Adresse  Taille
        0       87h       0       1
        1       83h       2       3
        2       81h       4       5
        3       82h       6       7
l

     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 :
  • Ouvrir la discussion en précisant un canal, donc Port[$a]:=Channel OR 4;
  • Réinitialiser le pointeur de ce canal Port[$c]:=0;
  • Lui expliquer ce qu'on attend de lui On le configure en Signal Mode, Lecteur (le signal mode je sais pas c'est quoi) $58=01011000, donc signal, réservé, lecture Port[$b]:=$58 Or Channel;
  • Lui donner l'adresse du buffer dans lequel il va piocher L'adresse est l'adresse FLAT, donc 16*segment+offset sur 20bits Les 16premiers bits se donnent sur le port Adresse, les 4derniers sur Page
                Adr:=Seg*16+Ofs;
                Port[Adresse]:=Adr AND 255;
                Port[Adresse]:=Adr SHR 8;
                Port[Page]:=Adr SHR 16;
    
    l
  • Lui donner la longueur du buffer, dans le port Taille Si TailleBuffer est la taille du demi-buffer : ATTENTION le DMA veut la taille-1 et pas la taille (c'est bête mais c'est comme ca)
                Port[Taille]:=(TailleBuffer*2-1) AND 255;
                Port[Taille]:=((TailleBuffer*2-1) SHR 8;
    
    l
  • Fermer la discussion sur le canal Port[$a]:=Channel OR 0;


    Pfou! La le DMA est prêt à servir, reste à programmer la carte :)
>Initialiser 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.)
       Reset : $2x6 (Write Only)
       Read Data : $2xa (Real Only)
       Write Command/Data : $2xc (Write)  \ double emploi, pas de fautes de
       Write-Buffer Status : $2xc (Read)  / frappes dans le numéro du port :)
       Read-Buffer Status : $2xe (Read Only)
l

     Première chose à faire : faire un reset du DSP, la suite des commandes est :
  • Ecrire un 1 au port Reset et attendre 3microsecondes (!)
  • Ecrire un 0 au port Reset
  • Attendre l'octet $aa du port Read Data. Vous devez attendre que le bit 7 du Read Status soit 1 avant de lire le Read Data.


     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 :
      Procedure WriteDsp(Oct:Byte);
      Begin
        Repeat Until (Port[BasePort+12] AND 128)=0;
        Port[BasePort+12]:=Oct;
      End;
      Function ReadDsp:Byte;
      Begin
        Repeat Until (Port[BasePort+14] AND 128)=1;
        ReadDsp:=Port[BasePort+10];
      End;
      Function ResetDsp:Boolean;
        Var Try:Word;
      Begin
        Port[BasePort+6]:=1;
        Delay(100);
        Port[BasePort+6]:=0;
        Try:=65535;
        Repeat Dec(Try) Until ((Port[BasePort+$e] SHR 7=1) AND (Port[BasePort+$a]=$aa)) Or (Try=0);
        If Try>0 Then ResetDsp:=True Else ResetDsp:=False;
      End;
l

     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
  • Brancher les speakers (commande $d1) Forcément... WriteDsp($d1);
  • Lui donner la fréquence pour le son (commande $40) Monsieur est un précieux, monsieur n'aime pas les valeurs brutes, il demande TimeConstant, qui se calcule comme suit : TimeConst:=Round(65536-((256000000/Mode)/Freq)) SHR 8; Mode=1 ou 2 pour mono/stéréo WriteDsp($40); WriteDsp(TimeConst);
  • Lui donner la taille des buffers (commande $48) C'est ici qu'on se rappelle qu'il faut lui donner la demi-taille du buffer total, qui est égal à TailleBuffer (on avait envoyé TailleBuffer*2 au DMA)
              WriteDsp($48);
              WriteDsp((TailleBuffer) AND 255);
              WriteDsp(((TailleBuffer) SHR 8);
    	
    l
  • Jouer (commande $90) Sans commentaire. La commande $90 est en fait l'ordre de jouer en output autoinitialize high-speed Pour jouer a des fréquences de plus de 22050hz, le DSP doit être mis en mode high speed, ce qui entraîne que plus aucune commande DSP ne répondra. La seule manière de reprendre le contrôle est de faire un reset du DSP

>Le programme autour

     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
  Var Buffer:Array[1..TailleBuffer*2] Of Byte;
  (...)
  Of1:=@Buffer;
  Of2:=Ptr(Seg(Buffer),Ofs(Buffer)+TailleBuffer);
l

    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.
lNote : après deux inversions, Of1 repointera a nouveau vers le début, ce qui est bien ce que l'on veut, lors du premier appel, ca sera bien la première partie qu'il faudra mettre à jour.

    Activez ensuite l'IRQ, et patchez-la vers une routine qui fera :
  • Répondre au DSP (il veut être sur, sinon il continue pas, le bougre) Ca consiste à aller lire un octet sur le read status (la valeur on s'en fiche) iBid:=Port[BasePort+14];
  • Appeler Lire
  • Répondre au contrôleur irq, comme pour toutes les irq

>Arrêter le massacre

    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...
>FeedBack

    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
>Références
http://nwfs1.rz.fh-hannover.de/~heineman/isasound.htm
   fichier sbhwpg.pdf
http://www.qzx.com/pc-gpe/
   et en particulier dans le site :
   http://www.qzx.com/pc-gpe/sbdsp.txt
   http://www.qzx.com/pc-gpe/wav.txt
   http://www.qzx.com/pc-gpe/dma_vla.txt
l

    En général
  http://www.ctyme.com/rbrown.htm
  http://webster.cs.ucr.edu/Page_asm/ArtOfAsm.html
l
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.