— Rédigé pour le Quasar Net par OffseT.
D'aucuns avaient sans doute déjà renoncé à l'idée de voir enfin un jour l'article sur les SID-voices, pourtant, il est bien là comme en témoigne le filet de bave qui coule de la bouche du petit blond à lunettes.
Oui, mais ne soyons pas si impatients de commencer. Car même si ce type de son est bien connu de la plupart des gens de la scène, n'en oublions pas pour autant les profanes ! Et avançons pas à pas dans cette caverne sombre et humide1).
Or donc, un SID-voice c'est quoi ?
De tout temps, le C64 a été jalousé pour les étonnantes sonorités de ses somptueuses musiques de jeux et de démos2). Eh oui, alors que la plupart des machines 8 bits de l'époque se voyaient affublées d'un générateur de son primitif, le C64 disposait du processeur SID ! Et ce chipset audio conçu spécialement pour la machine est un bijou disposant de capacités bien supérieures à tout ce que pourra jamais produire un AY.
Oui, car sur CPC nous ne faisons pas mieux que les Oric et autres ZX Spectrum côté chipset son, tout comme eux nous avons un AY (ou pour certains la variante YM qui ne vaut guère mieux). Ce misérable chipset ne dispose que d'une seule forme d'onde : le carré (fort heureusement, en polyphonie et avec un contrôle relativement fin du volume).
En quoi cela est complètement ridicule comparé au SID du C64 ? À peu près en tout à vrai dire. Le SID dispose également de trois canaux indépendants, mais il offre le choix de la forme d'onde (carré, triangle, dents de scie ou bruit blanc), le contrôle du rapport cyclique de ces formes élémentaires et l'application de filtres ! Ne cherchez pas, on ne peut pas lutter.
Si historiquement cette supériorité du processeur SID par rapport au AY n'a que peu révolté les possesseurs de CPC, il en était tout autrement des possesseurs d'Atari ST. Eh oui, cet ordinateur 16/32 bits qui espérait rivaliser avec l'Amiga3) (sic) était lui aussi doté d'un YM… imaginez la honte.
Le C64, cette petite machine 8 bits était capable de produire des sons outrageusement variés et des basses dignes de Cliff Burton alors que le ST, tout comme le CPC, devait se contenter des « bips » classiques dévolus aux AY. Pathétique. Certes, le YM, tout comme notre brave AY, permet de créer des sons un peu différents en y ajoutant du bruit (pour faire des percussions) ou en utilisant les enveloppes hard à haute fréquence (pour faire ce que l'on appelle des basses hard). Toutefois, c'était clairement insuffisant pour oser regarder droit dans les yeux un C64 en train de faire vibrer les haut-parleurs de la sono des parents.
Les codeurs ST se sont donc mis au travail et ont inventé ce que tout le monde appelle désormais les sons SID (ou SID-voices en anglais). L'idée était de moduler le triste signal carré du YM par une enveloppe de volume très rapide afin de le transformer en modifiant son rapport cyclique. On est bien loin de la palette de sons d'un vrai processeur SID, mais le simple fait de pouvoir faire cracher au YM ces petits sons cristallins fit le tour de la scène ; et bien vite, tous les musiciens ST se mirent à « faire du SID ».
C'est donc cet effet sonore que nous allons étudier afin de comprendre comment le reproduire sur CPC avec notre bon vieil AY.
Comme je m'en doutais, le petit blond à lunettes veut faire du SID mais n'a rien compris au concept. Commençons donc par les bases.
Si l'on fait abstraction des basses hard et du générateur bruit, notre AY est uniquement capable de produire des sons carrés. Comme expliqué en détail dans l'article sur le PSG, on en contrôle fort heureusement la fréquence et le volume grâce aux registres 0-1, 2-3, 4-5 (tonalité sur les canaux A, B et C) et 8, 9 et 10 (volume sur ces mêmes trois canaux). Mais on aura beau faire des portamentos ou des vibratos (variations de la tonalité dans le temps), des arpeggios (changement rapide de la note afin de simuler un accord) ou des tremolos (changement du volume au cours du temps), il n'en demeure pas moins que nos sons sont désespérément carrés. Difficile dans ces conditions de produire une musique aux sonorités variés où le thème, l'accompagnement et la basse donneraient l'impression d'être joués par trois instruments différents.
Dans sa palette sonore, le C64 (ou plus exactement son processeur SID) a lui aussi les sons carrés. Oui mais voilà, s'il peut tout comme nous contrôler la fréquence et le volume, il peut surtout choisir le rapport cyclique. Pour ceux qui, comme le petit blond à lunettes, ont séché les cours de physique au lycée, le rapport cyclique d'un son carré représente le ratio entre la durée de sa partie haute et sa période. Sur AY ce rapport est fixe ; il est de 50%. Les sons sont rigoureusement carrés, la demi-période basse dure exactement le même temps que la demi-période haute.
Le petit blond à lunettes commence à décrocher et demande un exemple. Le voici :
Dans ce premier cas, soit 50%; notre signal est parfaitement carré.
Ici on voit bien que l'état haut dure plus longtemps que la partie basse, nous avons un rapport cyclique de l'ordre de soit 70%.
Mais qu'est-ce qu'il nous embête avec le rapport cyclique d'un son ? Eh bien même si visuellement il n'y paraît pas grand chose, au niveau acoustique cette petite différence change à peu près tout. En effet, modifier le rapport cyclique d'un son agit tout simplement sur son timbre. Bon, je vois que certains sont déjà en train de chercher les tarifs postaux de 1983 ; je suis désolé, mais je ne vais pas développer tout ça ici puisque Zik l'a déjà fait là.
Un son SID, au sens du AY, est donc tout simplement un signal carré dont on va modifier le rapport cyclique… à la main ! Eh oui, comme notre AY ne permet pas de le contrôler directement, nous allons agir dessus en reprogrammant très rapidement (en « temps-réel » comme on dit dans le milieu) le registre de volume pendant qu'un son se joue. On dirait qu'une étincelle d'intelligence vient de briller dans les yeux du petit blond à lunettes ! Il vient de comprendre l'idée. En faisant varier le volume entre 0 et la valeur souhaitée pour le son, nous allons tout simplement hacher notre signal carré maussade pour le métamorphoser en un magnifique rectangle au son cristallin : nous avons un son SID !
Petit schéma :
On voit que le son de base est filtré par le son de hachage afin d'obtenir un nouveau son. Le son de hachage devra généralement être à la même fréquence que celle du son de base, mais pourra légèrement osciller autour de celle-ci afin d'ajouter des modulations au son final. Enfin, lorsque le déphasage des sons de base et de hachage évolue, le son final verra son rapport cyclique évoluer. Ce déphasage étant de fait permanent4), nous avons notre son sid.
À noter que plutôt que d'agir sur le registre de volume, on peut aussi utiliser le registre 7 (le mixeur) pour ouvrir et fermer le canal devant jouer du SID à la-dite fréquence. Le résultat sera sensiblement le même, mais c'est généralement moins souple, surtout si l'on veut produire plusieurs SID-voices à partir des AY-lists du CPC+.
Vous l'aurez compris, cette technique est assez lourde puisqu'il va falloir reprogrammer le AY très rapidement et différemment selon la fréquence du son à produire. Bonjour l'angoisse.
Sur Atari ST la chose se fait assez simplement à l'aide de leurs deux timers. Sous interruption, ils vont reprogrammer les registres de volume des deux canaux qu'ils souhaitent transformer en SID-voices. Et par la même occasion vous venez de comprendre pourquoi les musiques ST sont généralement limitées à deux SID-voices… ce n'est pas une contrainte de leur YM, mais simplement une limitation de leurs timers qui ne sont qu'au nombre de deux (bien sûr, en programmant plus finement on peut facilement passer outre cette limitation).
Sur CPC, d'une part nous n'avons pas de timer programmable, et surtout, le AY n'est programmable qu'au travers du PPI via un nombre de OUT affolant. On voit de suite poindre le problème de performance. Bien sûr, rien n'empêche de faire des sons SID façon « split-raster ». Mais on voit tout de suite la limite du procédé ; ceci explique pourquoi personne n'a jamais sorti de musique qui utilise vraiment des SID-voices sur CPC5) (même si les initiés ont déjà vu des previews jouant des sons SID parfaits en même temps que des rasters par exemple). L'autre gros soucis, c'est que l'on ne pourra raisonnablement produire qu'un seul SID-voice à la fois. Imaginez l'enfer pour synchroniser la reprogrammation de deux SID-voices à des fréquences différentes, le tout en temps-réel. Non merci. Bien sûr, on peut tout arrondir à 64 micro-secondes près et poser des boucles synchronisées avec la vidéo, mais c'est tellement couteux que ça ne vaut pas vraiment le coup au final.
Sur CPC+ les choses sont notablement différentes, car nous avons les DMA audio. Nous pouvons les utiliser de deux manières différentes. Soit comme des timers (comme sur Atari ST) qui vont déclencher nos routines de reprogrammation du AY (via la ribambelle de OUT sur le PPI) ; soit comme des copper-lists (comme sur Amiga) qui vont directement reprogrammer les registres du AY.
L'avantage de cette technique est la précision. Même si les DMA audio ne sont précis qu'à 64 micro-secondes près, il est ensuite aisé de faire l'ajustement à la micro-seconde près dans la routine d'interruption elle-même. On peut donc contrôler très finement le rapport cyclique et avoir des sons SID quasi-parfaits (par rapport à l'Atari ST).
Mais bien sûr, la précision a un coût. D'abord, toutes ces interruptions, ça prend du temps, beaucoup de temps. Et plus le son sera aigu, plus les interruptions seront nombreuses ; le Z80 sera très rapidement saturé. Ensuite, toujours à cause de la vitesse de notre brave Z80, il sera impossible d'avoir plus d'un SID-voice à la fois. Le traitement des interruptions ne sera pas assez rapide.
Cette méthode permet de rejouer à la perfection les musiques ST n'utilisant qu'un seul SID-voice. Elle fut utilisée, par exemple, dans Larsen (pour une musique de Scavenger) et dans la compo de Kris de l'Amstrad Expo 2009 (avec une musique de Mad Max).
Ici, l'idée est de créer des AY-lists qui vont directement reprogrammer les registres sur AY. Nous sommes limités par la précision des DMA, soit 64 micro-secondes et il faudra bien gérer les arrondis si on ne veut pas faire pleurer les musiciens6). En revanche, ceux-ci pourront allègrement avoir trois SID-voices simultanément sans grand soucis.
À partir de là, nous avons deux façons de procéder.
On peut simplement faire une boucle DMA qui va alternativement changer le volume. Notre AY-list ressemblerait à ceci :
REPEAT &FFF PAUSE n LOAD R8,0 PAUSE n-1 LOAD R8,vol LOOP
Il suffit ensuite de modifier n et vol à chaque frame en fonction de la fréquence et du volume du son. C'est très peu couteux en temps processeur (la génération de l'AY-list est très rapide) et les calculs des SID voices ne dépendent plus de la fréquence des sons joués. Mais -eh oui, il y a toujours un mais- nous ne sommes pas très précis et jouer des sons SID aigus par ce biais est tout simplement impossible. Ceci étant, si n est bien calculé, il n'en demeure pas moins qu'il s'agit d'un excellent compromis, surtout dans le cas de musiques composées sur CPC plutôt que converties. Le Soundtracker DMA de Zik utilise d'ailleurs cette méthode (mais en programmant le registre 7 plutôt que les registres de volume).
Alors là, c'est du lourd. Plutôt que d'avoir une AY-list qui boucle et que l'on modifie à la volée, nous allons calculer une AY-list complète frame après frame. C'est couteux en temps processeur -tout de même pas autant que par interruption-, mais c'est presque aussi précis que par interruption. En réalité, seuls les puristes remarqueront une légère différence de timbre entre un SID-voice à « DMA déroulé » et un SID-voice par interruption.
Au final, nos AY-lists sont ici très simples :
LOAD R8,0 PAUSE n1 LOAD R8,vol PAUSE n1 LOAD R8,0 PAUSE n2 LOAD R8,vol PAUSE n2 LOAD R8,0 PAUSE n3 LOAD R8,vol PAUSE n3 ...
Comme les players sont généralement à la frame, cette liste devra simplement durer une frame. On voit tout de suite qu'il faudra poker une multitude de valeurs : toutes les pauses (n1, n2, n3, etc.) et aussi le volume (vol, autant de fois que nécessaire). De plus, on comprend bien que plus un son sera aigu, plus il faudra poker de valeurs (l'AY-list sera plus longue !).
En pratique, cette technique n'a qu'une seule utilité : permettre de rejouer fidèlement des musiques SID composées sur ST en limitant la charge processeur par rapport aux interruptions, et en autorisant les musiques ayant deux voire trois SID-voices. C'est ce type de player que j'avais codé pour Iron et qu'il a intégré dans la très controversée Killmax.
Face à l'insistance du petit blond à lunettes, je vais vous livrer un petit programme d'exemple qui permet de jouer des musiques SID. Ce player est une variante de celui utilisé dans Killmax et est conçu de façon très modulaire (et peu optimale) afin d'être facile à lire et à comprendre. Il fonctionne exclusivement sur CPC+ en utilisant des AY-lists DMA déroulées comme décrit précédemment pour jouer les sons SID.
Les musiques utilisées par le player sont dans un format dédié (que j'ai simplement appelé AY+). Il est assez similaire au format YM5/6 mais a été adapté pour le CPC. Les flux de registres sont compressés indépendamment à l'aide de PuCrunch et stockés de façon plus optimale.
Ces flux contiennent donc les valeurs des pseudo-registres du PSG au fil des frames. Certains sont directement des registres du AY, d'autres sont des registres composites qui seront décodés avant d'être utilisés. En outre, ce format ne se limite pas à la gestion des “SID-voices” mais permet de stocker les informations d'autres effets spéciaux comme les “sinus-SID”, les “synch-buzzer” et les “digidrums” qui sont également utilisés sur ST (le player ci-après ne les gère toutefois pas).
Les flux compressés sont stockés dans l'ordre dans le fichier qui est sauvé en RAW sans aucune forme de header (un fichier de configuration décrit ci-après joue le rôle de header).
Voici donc la description des données extraites de chaque flux :
Flux | Codage | Présence | Description | |||||||
---|---|---|---|---|---|---|---|---|---|---|
0 | HardA | SidA | DrumA | SinA | BuzzA | D10 | D9 | D8 | Toujours | D8-D10 : bits de poids fort de la période sur le canal A (registre 0) |
1 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | Toujours | D7-D0 : bits de poids faible de la période sur le canal A (registre 0) |
2 | HardB | SidB | DrumB | SinB | BuzzB | D10 | D9 | D8 | Toujours | D8-D10 : bits de poids fort de la période sur le canal B (registre 1) |
3 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | Toujours | D7-D0 : bits de poids faible de la période sur le canal B (registre 1) |
4 | HardC | SidC | DrumC | SinC | BuzzC | D10 | D9 | D8 | Toujours | D8-D10 : bits de poids fort de la période sur le canal C (registre 2) |
5 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | Toujours | D0-D7 : bits de poids faible de la période sur le canal C (registre 2) |
6 | D4 | D3 | D2 | D1 | D0 | Toujours | D0-D4 : niveau de bruit (registre 6) | |||
7 | D5 | D4 | D3 | D2 | D1 | D0 | Toujours | D0-D5 : mixeur (registre 7) | ||
8 | D3 | D2 | D1 | D0 | Toujours | D0-D3 : Volume sur le canal A (registre 8) | ||||
9 | D3 | D2 | D1 | D0 | Toujours | D0-D3 : Volume sur le canal B (registre 9) | ||||
10 | D3 | D2 | D1 | D0 | Toujours | D0-D3 : Volume sur le canal C (registre 10) | ||||
11 | H2 | H1 | H0 | D4 | D3 | D2 | D1 | D0 | Si HardEnv7) | D0-D4 : bits de poids fort de la période de l'enveloppe hard (registre 11), H0-H2 : forme de l'enveloppe hard8) |
12 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | Si HardEnv9) | D7-D0 : bits de poids faible de la période de l'enveloppe hard (registre 12) |
13 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | Si FX A10) | D7-D6 : bits de poids fort de la période11) de l'effet spécial sur le canal A |
14 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | Si FX A12) | D7-D6 : bits de poids faible de la période13) de l'effet spécial sur le canal A |
15 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | Si FX B14) | D7-D6 : bits de poids fort de la période15) de l'effet spécial sur le canal B |
16 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | Si FX B16) | D7-D6 : bits de poids faible de la période17) de l'effet spécial sur le canal B |
17 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | Si FX C18) | D7-D6 : bits de poids fort de la période19) de l'effet spécial sur le canal C |
18 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | Si FX C20) | D7-D6 : bits de poids faible de la période21) de l'effet spécial sur le canal C |
Codage de l'enveloppe hard (flux 11) :
H0-H2 | Registre 13 |
---|---|
0 | 0, 1, 2, 3 ou 9 |
1 | 4, 5, 6, 7 ou 15 |
2 | 8 |
3 | 12 |
4 | 10 |
5 | 14 |
6 | 11 |
7 | 13 |
Tous les flux ne sont pas forcément présents dans chaque fichier AY+. Par exemple une musique qui n'utilise pas les enveloppes hard aura un fichier dans lequel le flux des registres 11, 12 et 13 sera absent. L'absence ou la présence des flux est déterminée à partir d'un fichier de configuration du player qui est fourni avec chaque fichier de données.
Ces fichiers contiennent les informations sur la taille de chaque flux (ce qui permet de les localiser dans le fichier AY+) ainsi que sur les effets sonores utilisés par la musique (ce qui permet d'identifier les flux éventuellement absents et au player de s'assembler différemment selon le cas).
Au final, il s'agit simplement d'un bout de code assembleur destiné à être inclus dans le player avant d'être assemblé (sous Maxam 1.5).
Voici un exemple :
; ; Configuration de la musique ; let SIDOnA=0 let SIDOnB=1 let SIDOnC=1 let HardEnv=1 ; ; Table contenant la taille des streams ; StreamsSize dw 1112,1761, 143, 693, 147,1882, 200, 253 dw 229, 247, 567, 206,1635+24+24, 468, 679, 990,1748
La bibliothèque de décompression (qui permet de sortir les registres des flux compressés pour chaque frame) que je vous fournis ici a été développée à partir de PuCrunch dont l'implémentation a été modifiée pour pouvoir gérer du streaming avec un dictionnaire de 256 octets. Elle n'est pas performante et aucun effort d'optimisation n'a été effectué dessus ; c'est de toute manière un peu hors sujet au regard du sujet de cet article. Si vous voulez en savoir plus sur PuCrunch, je vous invite à allez jeter un coup d'œil ici22). Pour le reste, libre à vous d'optimiser cette bibliothèque de streaming, ou mieux, d'implémenter la votre car il est possible de faire beaucoup plus performant tant en terme de compression que de temps machine utilisé pour la décompression.
Ceci étant, si vous désirez créer des fichiers AY+ par vous même, voici la ligne de commande qu'il vous faudra utiliser pour compresser chacun des flux afin que le décompresseur utilisé ici puisse fonctionner correctement :
pucrunch -c0 -d -r256 <fichier d'entrée contenant les données brutes> <fichier de sortie compressé>
Ensuite, concaténez simplement les fichiers compressées de chaque flux en un seul gros fichier et créez un fichier de configuration correspondant (avec la taille de chaque flux compressé et les autres informations pour le player).
Comme vous l'aurez compris, le player n'est pas générique mais s'assemble conditionnellement en fonction du fichier de configuration qui vient avec chaque musique au format AY+. Le tout est prévu pour s'assembler avec Maxam 1.5. Tout comme pour la bibliothèque de décompression, le code n'est absolument pas optimisé ce qui lui permet de rester relativement facile à comprendre (si vous vous essayez à l'exercice d'optimisation, vous devriez sans problème parvenir à diviser le temps machine nécessaire par deux).
Le principe de fonctionnement est assez élémentaire. Une fois les flux décompressés, le player va donc simplement prendre un registre de chaque puis reprogrammer le PSG et calculer les AY-lists pour les SID pour la frame en cours. La partie qui nous intéresse ici le plus est bien évidemment celle qui décode et gère les pseudo-registres des effets spéciaux pour les “SID-voices”.
Les toutes premières lignes permettent de configurer la musique avec :
READ
,Le petit blond à lunettes est circonspect concernant ce dernier point. Je m'explique. Le player est en effet en temps constant avec un système de buffer dynamique qui se remplit à l'avance pendant les phases où jouer la musique prend moins de temps. Ainsi, il commence à s'exécuter à la première ligne et fera le maximum de travail jusqu'à la ligne indiquée avant d'être interrompu jusqu'à la prochaine frame. Ce prebuffer a une taille de 256 octets par flux.
Lorsque la musique se joue, des rasters permettent de visualiser le temps machine pris par chaque partie du code dans la limite du temps maximum configuré (player en jaune, le gestionnaire de flux et le générateur d'AY-lists en rouge, le décompresseur en magenta). Le fond d'écran change également de couleur et tend plus ou moins vers le rouge selon la marge disponible dans le buffer de prebuffering (si c'est noir tout va bien, et plus c'est rouge vif, plus le buffer risque d'être vidé trop tôt). Si le décompresseur a pris trop de retard par rapport au player (c'est-à-dire s'il n'a pas réussi à alimenter le buffer assez rapidement au fil de frames) alors un octet à &FF
est poké en &C000
(en haut à gauche de l'écran). Si le générateur d'AY-lists n'a pas pu terminer son travail dans cette même limite de temps, alors le border clignote. Dans ces deux cas il faut augmenter le numéro de ligne maximum d'exécution pour le player !
Enfin, je ne vais donc pas vous en détailler tout le fonctionnement ici, il est de toute façon largement commenté.
Amusez-vous bien !
Télécharger le listing au format Maxam 1.5
; AY+ DMA Player ; OffseT/Futurs' pour Iron ; Mai 2007 - Octobre 2007 ; -> Version initiale ; Mai 2009 ; -> Ajout d'une bufferisation dynamique ; -> Maitrise du temps CPU max du stream Org &9000 Nolist Let entry=$ jp start read"cave.cfg" ; Nom de la musique Bank Equ &c4 ; Bank de la musique StreamsStart Equ &4000 ; Adresse de la musique Buffers Equ &d000 ; Adresse des n buffers de &100 octets ; n dépend de la musique, voir le fichier .cfg ; min=13 et max=17 (19 en théorie mais bon...) BreakLine Equ 80 ; Numéro de ligne limite pour le player ; ################################################################### ; Ici commencent les inits du programme principal ; ################################################################### Start ld a,tableint/256 ld i,a ld a,tableint mod 256 or 1 ld hl,&6805 ld bc,&7fb8 ld ix,&6c02 ld e,&a0 di im 2 out (c),c ld (hl),a ld (ix+0),0 ld (ix+4),0 ld (ix+8),0 out (c),e ei exx ex af,af' push af push bc push de push hl ld bc,&7f00+bank out (c),c ld c,&8e out (c),c call initplayer ; ################################################################### ; Ici commence le programme principal ; ################################################################### ; Boucle principale Loop ld bc,&f500 Synch in a,(c) rra jp nc,synch ld a,(alertbuffer) ; Récupération du flag d'erreur buffer ld (&c000),a ld a,(prebufferingcount+1) ; Colorisation en fonction du buffer ld c,a ld a,prebuffers sub c ld bc,&7fb8 out (c),c sla a:sla a:sla a:sla a ld (&6400),a xor a ld (&6401),a ld bc,&7fa0 out (c),c ld bc,&7f10 out (c),c ld bc,&7fb8 ; On place une synchro pour appeler "runplayer" out (c),c ld a,1 ld (&6800),a ld bc,&7fa0 out (c),c halt ld bc,&7f00+74 out (c),c call runplayer ; Doit etre stable sur la frame ! ld bc,&7f00+76 out (c),c call getframeplayer ld bc,&7f00+79 out (c),c ld a,breakline ; Numéro de ligne à ne pas dépasser call getbuffersplayer ld bc,&7f00+84 out (c),c ; Test clavier ld bc,&f40e out (c),c ld bc,&f6c0 out (c),c xor a out (c),a ld bc,&f792 out (c),c ld bc,&f648 out (c),c ld b,&f4 in a,(c) ld bc,&f782 out (c),c ld bc,&f600 out (c),c rra rra rra jr nc,broken EndLoop jp loop ; ################################################################### ; Ici commence la fin du programme principal ; ################################################################### Broken di ld bc,&7fb8 out (c),c ld a,&f0 ld (&6c0f),a xor a ld (&6800),a ld bc,&7fa0 out (c),c ei ld bc,&7fc0 out (c),c pop hl pop de pop bc pop af exx ex af,af' call &bca7 im 1 ret ; ################################################################### ; A partir d'ici, on ne touche plus à rien c'est le player ; ################################################################### ; ; Configuration du player ; let AutoSIDA=0 ; Calcul SID automatique (ne pas utiliser le timer ST) let AutoSIDB=0 let AutoSIDC=0 let SyncSID=0 ; Synchroniser l'attaque du SID avec le AY (expérimental) ; ; Flag d'alerte, doit toujours valoir zéro ; AlertBuffer db 0 ; ; Initialisations globales ; StreamsCount Equ 11+hardenv+hardenv+sidona+sidona+sidonb+sidonb+sidonc+sidonc PreBuffers Equ 256/streamscount-1 ; ; Initialisation et prébufferisation des streams ; InitPlayer ld a,prebuffers ld (prebufferingcount+1),a xor a ld (alertbuffer),a ld (currentstreamcount+1),a ld (framebufferpointer+1),a ld hl,streamscontextadr ld (currentstreamcontextadr+2),hl ld hl,goprebuffering ld (pcstack),hl ld hl,regstack ld (workstack+1),hl ld hl,regsoldvalues ld de,regsoldvalues+1 ld (hl),&ff ld bc,31 ldir IF sidona xor a ld (whichdmalista+1),a ld hl,primarysidlistapredelay ld (dmalista+1),hl ld hl,primarysidlista ld de,primarysidlista+4 ld bc,312-4 ldir ld hl,secondarysidlista ld de,secondarysidlista+4 ld bc,312-4 ldir ENDIF IF sidonb xor a ld (whichdmalistb+1),a ld hl,primarysidlistbpredelay ld (dmalistb+1),hl ld hl,primarysidlistb ld de,primarysidlistb+4 ld bc,312-4 ldir ld hl,secondarysidlistb ld de,secondarysidlistb+4 ld bc,312-4 ldir ENDIF if sidonc xor a ld (whichdmalistc+1),a ld hl,primarysidlistcpredelay ld (dmalistc+1),hl ld hl,primarysidlistc ld de,primarysidlistc+4 ld bc,312-4 ldir ld hl,secondarysidlistc ld de,secondarysidlistc+4 ld bc,312-4 ldir ENDIF ld de,buffers ld hl,streamsstart ld ix,streamscontextadr ld iy,streamssize ld b,streamscount LoopInitStreams push bc push hl push de call initstream pop hl ld bc,bufferssize add hl,bc ex de,hl pop hl ld b,(iy+1) ld c,(iy+0) add hl,bc inc iy inc iy ld bc,streamscontextsize add ix,bc pop bc djnz loopinitstreams call getframeplayer ret ; ; Mise à jour des buffers ; ds 8 RegStack dw 0,0,0,0,0,0 PCStack dw goprebuffering ; A = ligne max jusqu'à laquelle travailler GetBuffersPlayer di ld bc,&7fb8 out (c),c ld (&6800),a ld bc,&7fa0 out (c),c ld hl,stopbufferinginterrupt ld (intdma0),hl ld (intrast),hl ld a,(prebufferingcount+1) dec a ; Soucis si a était à zéro ! (buffer vide) cp &ff jr nz,goonbuffersok ld (alertbuffer),a GoOnBuffersOK ld (prebufferingcount+1),a ld (restoresp+1),sp WorkStack ld sp,regstack pop af pop bc pop de pop hl pop ix pop iy ei ret StopBufferingInterrupt push iy push ix push hl push de push bc push af ld hl,rasterinterrupt ld (intdma0),hl ld (intrast),hl ld (workstack+1),sp RestoreSP ld sp,0 ei ret GoPreBuffering ei nop di PreBufferingCount ld a,prebuffers cp prebuffers jr z,goprebuffering ei ld a,(prebufferingcount+1) inc a ld (prebufferingcount+1),a CurrentStreamContextAdr ld ix,streamscontextadr call getnextbytes CurrentStreamCount ld a,0 inc a cp streamscount jr nz,updatetonextstream ResetToFirstStream xor a ld ix,streamscontextadr-streamscontextsize UpdateToNextStream ld (currentstreamcount+1),a ld bc,streamscontextsize add ix,bc ld (currentstreamcontextadr+2),ix jr goprebuffering ; ; Récupération des données du framedata courant dans les buffers ; GetFramePlayer ld hl,framedata ld d,buffers/256 FrameBufferPointer ld e,0 ld b,streamscount LoopGetFrameData ld a,(de) ld (hl),a inc hl inc d djnz loopgetframedata ; Mise à jour du pointeur de buffer ld a,(framebufferpointer+1) inc a ld (framebufferpointer+1),a ; Construction de la table des regsvalues à partir du framebuffer ld ix,framedata ld de,regsvalues ld bc,&8007 ld a,(ix+f_R0): ld (de),a:inc e ; R0 ld a,(ix+f_R1):and c: ld (de),a:inc e ; R1 ld a,(ix+f_R2): ld (de),a:inc e ; R2 ld a,(ix+f_R3):and c: ld (de),a:inc e ; R3 ld a,(ix+f_R4): ld (de),a:inc e ; R4 ld a,(ix+f_R5):and c: ld (de),a:inc e ; R5 ld a,(ix+f_R6): ld (de),a:inc e ; R6 ld a,(ix+f_R7): ld (de),a:inc e ; R7 ld a,(ix+f_R1):and b srl a:srl a:srl a:ld h,a ld a,(ix+f_R8):or h: ld (de),a:inc e ; R8 ld a,(ix+f_R3):and b srl a:srl a:srl a:ld h,a ld a,(ix+f_R9):or h: ld (de),a:inc e ; R9 ld a,(ix+f_R5):and b srl a:srl a:srl a:ld h,a ld a,(ix+f_R10):or h: ld (de),a:inc e ; R10 IF hardenv ld a,(ix+f_R11): ld (de),a:inc e ; R11 ld a,(ix+f_R12) ld h,a:and c: ld (de),a:inc e ; R12 ld a,h:and &f8 srl a:srl a:srl a:srl a:srl a ld c,a:ld b,0 ld hl,tablereg13:add hl,bc ld a,(hl): ld (de),a:inc e ; R13 ld c,&7 ENDIF ld a,(ix+f_R1):and &70: ld (de),a:inc e ; FXA IF sidona ld hl,putfxa_vol ; Désactive volume A si SID A bit 6,a jr nz,skipr8 ld hl,putpsg2 SkipR8 ld (poker8+1),hl IF autosida ld a,(ix+f_R1):and c:ld h,a:ld l,(ix+f_R0) ld a,7:and l:ld b,a srl h:rr l:srl h:rr l:srl h:rr l ld a,l: ld (de),a:inc e ; FreqFXA High ld a,b sla a:sla a:sla a: ld (de),a:inc e ; FreqFXA Low ELSE ld a,(ix+f_FXA_H) ld (de),a:inc e ; FreqFXA High ld a,(ix+f_FXA_L) ld (de),a:inc e ; FreqFXA Low ENDIF ENDIF ld a,(ix+f_R3):and &70: ld (de),a:inc e ; FXB IF sidonb ld hl,putfxb_vol ; désactive volume B si SID B bit 6,a jr nz,skipr9 ld hl,putpsg2 SkipR9 ld (poker9+1),hl IF autosidb ld a,(ix+f_R3):and c:ld h,a:ld l,(ix+f_R2) ld a,7:and l:ld b,a srl h:rr l:srl h:rr l:srl h:rr l ld a,l: ld (de),a:inc e ; FreqFXB High ld a,b sla a:sla a:sla a: ld (de),a:inc e ; FreqFXB Low ELSE ld a,(ix+f_FXB_H) ld (de),a:inc e ; FreqFXB High ld a,(ix+f_FXB_L) ld (de),a:inc e ; FreqFXB Low ENDIF ENDIF ld a,(ix+f_R5):and &70: ld (de),a:inc e ; FXC IF sidonc ld hl,putfxc_vol ; désactive volume C si SID C bit 6,a jr nz,skipr10 ld hl,putpsg2 SkipR10 ld (poker10+1),hl IF autosidc ld a,(ix+f_R5):and c:ld h,a:ld l,(ix+f_R4) ld a,7:and l:ld b,a srl h:rr l:srl h:rr l:srl h:rr l ld a,l: ld (de),a:inc e ; FreqFXC High ld a,b sla a:sla a:sla a: ld (de),a:inc e ; FreqFXC Low ELSE ld a,(ix+f_FXC_H) ld (de),a:inc e ; FreqFXC High ld a,(ix+f_FXC_L) ld (de),a:inc e ; FrefFXC Low ENDIF ENDIF IF sidona ; appel de la routine de préparation de l'ay list sid A ld a,(regsvalues+FXA) bit 6,a ; Check SID call nz,createdmalista ENDIF IF sidonb ; appel de la routine de préparation de l'ay list sid B ld a,(regsvalues+FXB) bit 6,a ; Check SID call nz,createdmalistb ENDIF IF sidonc ; appel de la routine de préparation de l'ay list sid C ld a,(regsvalues+FXC) bit 6,a ; Check SID call nz,createdmalistc ENDIF ret ; ; Programmation du PSG et DMA à partir des regsvalues ; RunPlayer ; appel de la routine de lancement de l'ay list sid di IF sidona ld a,(regsvalues+FXA) call putfxa ENDIF IF sidonb ld a,(regsvalues+FXB) call putfxb ENDIF IF sidonc ld a,(regsvalues+FXC) call putfxc ENDIF ei ld de,regsvalues ; DE=regvalues ld h,d ld l,regsoldvalues MOD 256 ; HL=oldregvalues ld a,(de):cp (hl):call nz,putpsg2 ; R0 inc e:inc l ld a,(de):cp (hl):call nz,putpsg2 ; R1 inc e:inc l ld a,(de):cp (hl):call nz,putpsg2 ; R2 inc e:inc l ld a,(de):cp (hl):call nz,putpsg2 ; R3 inc e:inc l ld a,(de):cp (hl):call nz,putpsg2 ; R4 inc e:inc l ld a,(de):cp (hl):call nz,putpsg2 ; R5 inc e:inc l ld a,(de):cp (hl):call nz,putpsg2 ; R6 inc e:inc l ld a,(de):cp (hl):call nz,putpsg2 ; R7 inc e:inc l ld a,(de):cp (hl) ; R8 PokeR8 call nz,putpsg2:inc e:inc l ld a,(de):cp (hl) ; R9 PokeR9 call nz,putpsg2:inc e:inc l ld a,(de):cp (hl) ; R10 PokeR10 call nz,putpsg2:inc e:inc l IF hardenv ld a,(de):cp (hl):call nz,putpsg2 ; R11 inc e:inc l ld a,(de):cp (hl):call nz,putpsg2 ; R12 inc e:inc l ld a,(de):cp (hl):call nz,putpsg2 ; R13 ENDIF ret ; E=Reg ; A=Val ; Sauve A dans (HL) PutPSG2 ld (hl),a ld b,&f4 di out (c),e ld bc,&f6c0 out (c),c ld c,0 out (c),c ld b,&f4 out (c),a ld a,&80 ld b,&f6 out (c),a out (c),c ei ret IF sidona PutFXA_Vol ld (hl),a ret ; E=FXA ; A=Val PutFXA bit 6,a ; Check SID, check reg7 ? jp nz,startdmalista StopFXA ld bc,&7fb8 IF sidonb+sidonc ; On compense en temps constant ds 25 ; uniquement si d'autres DMA viennent ENDIF ; après out (c),c ld a,(&6c0f) ; Stop DMA0 and &fe ld (&6c0f),a ld c,&a0 out (c),c IF sidonb+sidonc ds 22 ENDIF ret ENDIF IF sidonb PutFXB_Vol ld (hl),a ret ; E=FXB ; A=Val PutFXB bit 6,a ; Check SID, check reg7 ? jp nz,startdmalistb StopFXB ld bc,&7fb8 IF sidonc ; On compense en temps constant ds 25 ; uniquement si d'autres DMA ENDIF ; viennent après out (c),c ld a,(&6c0f) ; Stop DMA1 and &fd ld (&6c0f),a ld c,&a0 out (c),c IF sidonc ds 22 ENDIF ret ENDIF IF sidonc PutFXC_Vol ld (hl),a ret ; E=FXC ; A=Val PutFXC bit 6,a ; Check SID, check reg7 ? jp nz,startdmalistc StopFXC ld bc,&7fb8 ; Pas de compensation en temps constant comme pour out (c),c ; A et B car aucun autre DMA ne viendra après ld a,(&6c0f) ; Stop DMA2 and &fb ld (&6c0f),a ld c,&a0 out (c),c ret ENDIF IF sidona ; Routines de creation des l'AYList ; A = volume SID ; DE = frequence SID CreateDMAListA ld a,(regsvalues+FXA_H) ld d,a ; D=nbre ligne periode sid IF syncsid ld a,(regsoldvalues+FXA_H) cp d jr z,nonewfxa_h ld a,d ld (regsoldvalues+FXA_H),a xor a ld (runtimeoffseta+1),a NoNewFXA_H ENDIF ld a,(regsvalues+FXA_L) ld e,a ; E=retenue en nops IF syncsid ld a,(regsoldvalues+FXA_L) cp e jr z,nonewfxa_l ld a,e ld (regsoldvalues+FXA_L),a xor a ld (runtimeoffseta+1),a NoNewFXA_L ENDIF ld a,(regsvalues+R8) ; On recupere le volume courant et ld (pokeswitchvolumea+1),a ; on le met en switch dans la dma loop ld b,a ; B=volume du canal DMAListA ld hl,primarysidlistapredelay ; AYList a utiliser PreDelayA ld a,255 ; On recupere le nombre de lignes qui restaient ld (hl),a ; a attendre de la frame precedente et on le inc hl ; poke dans la phase d'init de l'AYList inc hl inc a ex af,af' SIDVolumeA ; On recupere l'etat du volume SID et ld a,15 ; le poke dans la phase d'init de l'AYList ld (hl),a inc hl inc hl or a ; si le volume courant du canal a change depuis jr z,nonewvola ; la frame precedente on le RAZ ld a,b NoNewVolA ex af,af' ; A'=volume SID courant ; ; On crée l'AYList ; ; IN ; A'=volume SID courant ; ; A=nombre de lignes deja traitee en init ; ; D,E=periode SID en base 64 ; ; (pokeswichvolume+1)=volume canal courant ; ; (dmacarry+1)=retenue periode SID en cours ; OUT ; A=nombre de lignes SID debordant de la frame ; ; A'=volume SID courant MakeDMAListA exx ld h,0 ; HL'=compteur de ligne ld l,a ld b,h ; B'=0 (constante) ld e,56 ; E'=312 modulo 256 (constante) LoopDMA_A exx ld b,d ; B=nombre de lignes a attendre RunTimeOffsetA ld a,0 ; A=retenue du calcul precedent PokeA_1 add a,e ld c,a ; C=retenue globale and &3f ld (runtimeoffseta+1),a ; Est-ce qu'on a plus d'une cp c ; ligne de retenue ? jr z,nodmaoverflowa DMAOverflowA inc b NoDMAOverflowA ; a une ligne moins a attendre ld a,b dec b ld (hl),b ; Pokage de l'unite de pause inc hl ;ld (hl),&10 inc hl ex af,af' PokeSwitchVolumeA ; Calcul du volume SID courant xor 15 ld (hl),a ; Pokage du volume SID ex af,af' inc hl ;ld (hl),8 inc hl exx ld c,a ; Test de fin de frame add hl,bc bit 0,h jr z,loopdma_a ld a,l sub a,e jr c,loopdma_a exx ;ld (hl),&20 ;inc hl ;ld (hl),&40 dec a ; Correction du nombre de lignes ; en overflow (255=pas d'overflow) ld (predelaya+1),a ; On recupere le nombre de ligne qui debordent de ; la frame pour les mettre au debut de la suivante ex af,af' ; On recupere le volume SID courant pour ld (sidvolumea+1),a ; reprendre au bon etat a la frame suivante ret ; ; Routines de lancement des AYLists A ; StartDMAListA ld e,2 ld bc,4 ld hl,(dmalista+1) ; On ne lance l'AYList du debut ld a,(hl) ; que si on avait une pre-attente cp 255 ; sinon si on n'avait pas de pre-attente jr z,startwithnopredelaya ; alors on saute toute la phase d'init ; or a ; sinon si on avait juste une ligne de ; jr nz,startwithpredelaya ; pre-attente on saute uniquement ld c,e ; l'instruction de pause StartWithNoPredelayA add hl,bc StartWithPredelayA ; Ici HL=adresse AYList a lancer ld bc,&7fb8 out (c),c ld (&6c00),hl ; adr DMA0 ld a,(&6c0f) or 1 ld (&6c0f),a ; run DMA0 ld c,&a0 out (c),c ld hl,primarysidlistapredelay ; On switch les AYLists de travail ld de,secondarysidlistapredelay WhichDMAListA ld a,0 xor 1 ld (whichdmalista+1),a jp z,startwithprimarya StartWithSecondary ld (dmalista+1),de ret StartWithPrimaryA nop ld (dmalista+1),hl ret ENDIF IF sidonb ; Routines de creation des AYLists B ; A = volume SID ; DE = frequence SID CreateDMAListB ld a,(regsvalues+FXB_H) ld d,a ; D=nbre ligne periode sid IF syncsid ld a,(regsoldvalues+FXB_H) cp d jr z,nonewfxb_h ld a,d ld (regsoldvalues+FXB_H),a xor a ld (runtimeoffsetb+1),a NoNewFXB_H ENDIF ld a,(regsvalues+FXB_L) ld e,a ; E=retenue en nops IF syncsid ld a,(regsoldvalues+FXB_L) cp e jr z,nonewfxb_l ld a,e ld (regsoldvalues+FXB_L),a xor a ld (runtimeoffsetb+1),a NoNewFXB_L ENDIF ld a,(regsvalues+R9) ; On recupere le volume courant et ld (pokeswitchvolumeb+1),a ; on le met en switch dans la dma loop ld b,a ; B=volume du canal DMAListB ld hl,primarysidlistbpredelay ; AYList a utiliser PreDelayB ld a,255 ; On recupere le nombre de lignes qui restaient ld (hl),a ; a attendre de la frame precedente et on le inc hl ; poke dans la phase d'init de l'AYList inc hl inc a ex af,af' SIDVolumeB ; On recupere l'etat du volume SID et ld a,15 ; le poke dans la phase d'init de l'AYList ld (hl),a inc hl inc hl or a ; si le volume courant du canal a change depuis jr z,nonewvolb ; la frame precedente on le RAZ ld a,b NoNewVolB ex af,af' ; A'=volume SID courant ; ; On crée l'AYList B ; ; IN ; A'=volume SID courant ; ; A=nombre de lignes deja traitee en init ; ; D,E=periode SID en base 64 ; ; (pokeswichvolume+1)=volume canal courant ; ; (runtimeoffsetb+1)=retenue periode SID en cours ; OUT ; A=nombre de lignes SID debordant de la frame ; ; A'=volume SID courant MakeDMAListB exx ld h,0 ; HL'=compteur de ligne ld l,a ld b,h ; B'=0 (constante) ld e,56 ; E'=312 modulo 256 (constante) LoopDMA_B exx ld b,d ; B=nombre de lignes a attendre RunTimeOffsetB ld a,0 ; A=retenue du calcul precedent PokeB_1 add a,e ld c,a ; C=retenue globale and &3f ld (runtimeoffsetb+1),a ; Est-ce qu'on a plus d'une cp c ; ligne de retenue ? jr z,nodmaoverflowb DMAOverflowB inc b NoDMAOverflowB ; a une ligne moins a attendre ld a,b dec b ld (hl),b ; Pokage de l'unite de pause inc hl ld (hl),&10 inc hl ex af,af' PokeSwitchVolumeB ; Calcul du volume SID courant xor 15 ld (hl),a ; Pokage du volume SID ex af,af' inc hl ;ld (hl),9 inc hl exx ld c,a ; Test de fin de frame add hl,bc bit 0,h jr z,loopdma_b ld a,l sub a,e jr c,loopdma_b exx ld (hl),&20 inc hl ld (hl),&40 dec a ; Correction du nombre de lignes ; en overflow (255=pas d'overflow) ld (predelayb+1),a ; On recupere le nombre de ligne qui debordent de ; la frame pour les mettre au debut de la suivante ex af,af' ; On recupere le volume SID courant pour ld (sidvolumeb+1),a ; reprendre au bon etat a la frame suivante ret ; ; Routines de lancement des AYLists B ; StartDMAListB ld e,2 ld bc,4 ld hl,(dmalistb+1) ; On ne lance l'AYList du debut ld a,(hl) ; que si on avait une pre-attente cp 255 ; sinon si on n'avait pas de pre-attente jr z,startwithnopredelayb ; alors on saute toute la phase d'init ; or a ; sinon si on avait juste une ligne de ; jr nz,startwithpredelayb ; pre-attente on saute uniquement ld c,e ; l'instruction de pause StartWithNoPredelayB add hl,bc StartWithPredelayB ; Ici HL=adresse AYList a lancer ld bc,&7fb8 out (c),c ld (&6c04),hl ; adr DMA1 ld a,(&6c0f) or 2 ld (&6c0f),a ; run DMA1 ld c,&a0 out (c),c ld hl,primarysidlistbpredelay ; On switch les AYLists de travail ld de,secondarysidlistbpredelay WhichDMAListB ld a,0 xor 1 ld (whichdmalistb+1),a jp z,startwithprimaryb StartWithSecondaryB ld (dmalistb+1),de ret StartWithPrimaryB nop ld (dmalistb+1),hl ret ENDIF IF sidonc ; Routines de creation des AYLists C ; A = volume SID ; DE = frequence SID CreateDMAListC ld a,(regsvalues+FXC_H) ld d,a ; D=nbre ligne periode sid IF syncsid ld a,(regsoldvalues+FXC_H) cp d jr z,nonewfxc_h ld a,d ld (regsoldvalues+FXC_H),a xor a ld (runtimeoffsetc+1),a NoNewFXC_H ENDIF ld a,(regsvalues+FXC_L) ld e,a ; E=retenue en nops IF syncsid ld a,(regsoldvalues+FXC_L) cp e jr z,nonewfxc_l ld a,e ld (regsoldvalues+FXC_L),a xor a ld (runtimeoffsetc+1),a NoNewFXC_L ENDIF ld a,(regsvalues+R10) ; On recupere le volume courant et ld (pokeswitchvolumec+1),a ; on le met en switch dans la dma loop ld b,a ; B=volume du canal DMAListC ld hl,primarysidlistcpredelay ; AYList a utiliser PreDelayC ld a,255 ; On recupere le nombre de lignes qui restaient ld (hl),a ; a attendre de la frame precedente et on le inc hl ; poke dans la phase d'init de l'AYList inc hl inc a ex af,af' SIDVolumeC ; On recupere l'etat du volume SID et ld a,15 ; le poke dans la phase d'init de l'AYList ld (hl),a inc hl inc hl or a ; si le volume courant du canal a change depuis jr z,nonewvolc ; la frame precedente on le RAZ ld a,b NoNewVolC ex af,af' ; A'=volume SID courant ; ; On crée l'AYList C ; ; IN ; A'=volume SID courant ; ; A=nombre de lignes deja traitee en init ; ; D,E=periode SID en base 64 ; ; (pokeswichvolume+1)=volume canal courant ; ; (runtimeoffsetc+1)=retenue periode SID en cours ; OUT ; A=nombre de lignes SID debordant de la frame ; ; A'=volume SID courant MakeDMAListC exx ld h,0 ; HL'=compteur de ligne ld l,a ld b,h ; B'=0 (constante) ld e,56 ; E'=312 modulo 256 (constante) LoopDMA_C exx ld b,d ; B=nombre de lignes a attendre RunTimeOffsetC ld a,0 ; A=retenue du calcul precedent PokeC_1 add a,e ld c,a ; C=retenue globale and &3f ld (runtimeoffsetc+1),a ; Est-ce qu'on a plus d'une cp c ; ligne de retenue ? jr z,nodmaoverflowc DMAOverflowC inc b NoDMAOverflowC ; a une ligne moins a attendre ld a,b dec b ld (hl),b ; Pokage de l'unite de pause inc hl ;ld (hl),&10 inc hl ex af,af' PokeSwitchVolumeC ; Calcul du volume SID courant xor 15 ld (hl),a ; Pokage du volume SID ex af,af' inc hl ;ld (hl),10 inc hl exx ld c,a ; Test de fin de frame add hl,bc bit 0,h jr z,loopdma_c ld a,l sub a,e jr c,loopdma_c exx ;ld (hl),&20 ;inc hl ;ld (hl),&40 dec a ; Correction du nombre de lignes ; en overflow (255=pas d'overflow) ld (predelayc+1),a ; On recupere le nombre de ligne qui debordent de ; la frame pour les mettre au debut de la suivante ex af,af' ; On recupere le volume SID courant pour ld (sidvolumec+1),a ; reprendre au bon etat a la frame suivante ret ; ; Routines de lancement des AYLists C ; StartDMAListC ld e,2 ld bc,4 ld hl,(dmalistc+1) ; On ne lance l'AYList du debut ld a,(hl) ; que si on avait une pre-attente cp 255 ; sinon si on n'avait pas de pre-attente jr z,startwithnopredelayc ; alors on saute toute la phase d'init ; or a ; sinon si on avait juste une ligne de ; jr nz,startwithpredelayc ; pre-attente on saute uniquement ld c,e ; l'instruction de pause StartWithNoPredelayC add hl,bc StartWithPredelayC ; Ici HL=adresse AYList a lancer ld bc,&7fb8 out (c),c ld (&6c08),hl ; adr DMA2 ld a,(&6c0f) or 4 ld (&6c0f),a ; run DMA2 ld c,&a0 out (c),c ld hl,primarysidlistcpredelay ; On switch les AYLists de travail WhichDMAListC ; Ici pas besoin de temps constant ld a,0 ; comme les routines DMA A et B xor 1 ; car on ne lancera rien après ld (whichdmalistc+1),a jr z,startwithprimaryc StartWithSecondaryC ld hl,secondarysidlistcpredelay StartWithPrimaryC ld (dmalistc+1),hl ret ENDIF IF hardenv TableReg13 db 0,4,8,12,10,14,11,13 ENDIF FrameDATA ds streamscount Let CurrentPC=$ Org 0:Nocode f_R1 db 0:f_R0 db 0 f_R3 db 0:f_R2 db 0 f_R5 db 0:f_R4 db 0 f_R6 db 0:f_R7 db 0 f_R8 db 0:f_R9 db 0:f_R10 db 0 IF hardenv:f_R12 db 0:f_R11 db 0:ENDIF IF sidona:f_FXA_H db 0:f_FXA_L db 0:ENDIF IF sidonb:f_FXB_H db 0:f_FXB_L db 0:ENDIF IF sidonc:f_FXC_H db 0:f_FXC_L db 0:ENDIF Org CurrentPC:Code Read"streamv4.inc" Let CurrentPC=$+1 Org CurrentPC and &fffe RasterInterrupt ei ret DMAInterrupt1 ; Spécifique DMA1 push af push bc ld bc,&7fb8 out (c),c ld a,(&6c0f) or &20 and &25 ; ack&stop DMA1 ld (&6c0f),a ld c,&a0 out (c),c pop bc pop af ei ret DMAInterrupt2 ; Spécifique DMA2 push af push bc ld bc,&7fb8 out (c),c ld a,(&6c0f) or &10 and &13 ; ack&stop DMA2 ld (&6c0f),a ld c,&a0 out (c),c pop bc pop af ei ret Let CurrentPC=$+&100 ; Multiple de &100 car E=numéro de registre Org CurrentPC and &ff00 RegsValues ds 32 RegsOldValues ds 32,&aa Let CurrentPC=$ ; Offset des registres AY Org 0:NoCode R0 db 0:R1 db 0:R2 db 0:R3 db 0:R4 db 0:R5 db 0 R6 db 0:R7 db 0 R8 db 0:R9 db 0:R10 db 0 IF hardenv:R11 db 0:R12 db 0:R13 db 0:ENDIF FXA db 0:IF sidona:FXA_H db 0:FXA_L db 0:ENDIF FXB db 0:IF sidonb:FXB_H db 0:FXB_L db 0:ENDIF FXC db 0:IF sidonc:FXC_H db 0:FXC_L db 0:ENDIF Org CurrentPC:Code Let CurrentPC=$+8 ; Multiple de &8 car lié à IVR Org CurrentPC and &fff8 TableInt IntDMA2 dw dmainterrupt2 IntDMA1 dw dmainterrupt1 IntDMA0 dw rasterinterrupt IntRast dw rasterinterrupt Let CurrentPC=$+1 ; Multiple de 2 car AY List Org CurrentPC and &fffe ; SID AYLists IF sidona PrimarySIDListAPredelay dw &1000 ; Pause NN dw &080F ; LD VolA,NN PrimarySIDListA dw &1000 ; Pause NN dw &080F ; LD VolA,NN ds 312-4 dw &4020 ; STOP SecondarySIDListAPredelay dw &1000 ; Pause NN dw &080F ; LD VolA,NN SecondarySIDListA dw &1000 ; Pause NN dw &080F ; LD VolA,NN ds 312-4 dw &4020 ; STOP ENDIF IF sidonb PrimarySIDListBPredelay dw &1000 ; Pause NN dw &090F ; LD VolB,NN PrimarySIDListB dw &1000 ; Pause NN dw &090F ; LD VolB,NN ds 312-4 dw &4020 ; STOP SecondarySIDListBPredelay dw &1000 ; Pause NN dw &090F ; LD VolB,NN SecondarySIDListB dw &1000 ; Pause NN dw &090F ; LD VolB,NN ds 312-4 dw &4020 ; STOP ENDIF IF sidonc PrimarySIDListCPredelay dw &1000 ; Pause NN dw &0A0F ; LD VolC,NN PrimarySIDListC dw &1000 ; Pause NN dw &0A0F ; LD VolC,NN ds 312-4 dw &4020 ; STOP SecondarySIDListCPredelay dw &1000 ; Pause NN dw &0A0F ; LD VolC,NN SecondarySIDListC dw &1000 ; Pause NN dw &0A0F ; LD VolC,NN ds 312-4 dw &4020 ; STOP ENDIF List EndAdr Nolist Org entry
Télécharger le listing au format Maxam 1.5
; Routine de décompression ; version modifiée du pucrunch ; pour pouvoir g{rer du streaming ; par packets de taille fixe ; OffseT - Mai 2007 BuffersSize Equ &100 BufferSize Equ streamscount ; ****** Unpack pucrunch data ****** ; Entry HL = Source packed data ; DE = Destination for unpacked data ; HL = InPtr ; D = bitstr ; E = X ; BC = temps InitStream ld a,buffersize*prebuffers ld (buffercount),a ld (ix+OutPtr),e ld (ix+OutPtr+1),d ld (ix+StartPtr),l ld (ix+StartPtr+1),h ; Read the file header & setup variables RestartStream ld bc,6 add hl,bc ld a,(hl) ld (ix+escPu),a inc hl inc hl inc hl ld a,(hl) ld (ix+EscBits),a ld a,8 sub (hl) ld (ix+Esc8Bits),a inc hl ld a,(hl) ld (ix+MaxGamma),a ld a,9 sub (hl) ld (ix+Max8Gamma),a inc hl ld a,(hl) ld (ix+Max1Gamma),a add a,a dec a ld (ix+Max2Gamma),a inc hl ld a,(hl) ld (ix+ExtraBits),a inc hl inc hl inc hl ld c,(hl) ; Get lenght of RLE table (max = 31) inc c ; (B=0) ld (ix+tablePu),l ld (ix+tablePu+1),h add hl,bc ; Go to the start of data ld d,&80 jp main newesc ld b,a ld a,(ix+escPu) ld (ix+regy),a ld a,(ix+EscBits) ld e,a ld a,b inc e call getchk ld (ix+escPu),a ld a,(ix+regy) ; Fall through and get the rest of the bits. noesc ld b,a ld a,(ix+Esc8Bits) ld e,a ld a,b inc e call getchk ; Write out the escaped/normal byte ld c,(ix+OutPtr) ld b,(ix+OutPtr+1) ld (bc),a inc c ld (ix+OutPtr),c ld a,(buffercount) dec a ld (buffercount),a jr nz,main ld (ix+returntype),0 ld (ix+saved),d ld (ix+savel),l ld (ix+saveh),h ret ; Fall through and check the escape bits again main ld a,(ix+EscBits) ld e,a xor a ld (ix+regy),a inc e call getchk ; X=2 -> X=0 ld b,a ld a,(ix+escPu) cp b ld a,b jr nz,noesc ; Not the escape code -> get the rest of the byte ; Fall through to packed code call getval ; X=0 -> X=0 ld (ix+lzpos),a ; xstore - save the length for a later time srl a ; cmp #1 ; LEN == 2 ? (A is never 0) jp nz,lz77 ; LEN != 2 -> LZ77 call get1bit ; X=0 -> X=0 srl a ; bit -> C, A = 0 jp nc,lz772 ; A=0 -> LZPOS+1 LZ77, len=2 ; e..e01 call get1bit ; X=0 -> X=0 srl a ; bit -> C, A = 0 jr nc,newesc ; e..e010 New Escape ; e..e011 Short/Long RLE ld a,(ix+regy) ; Y is 1 bigger than MSB loops inc a ld (ix+regy),a call getval ; Y is 1, get len, X=0 -> X=0 ld (ix+lzpos),a ; xstore - Save length LSB ld c,a ld a,(ix+Max1Gamma) ld b,a ld a,c cp b ; ** PARAMETER 63-64 -> C set, 64-64 -> C clear.. jr c,chrcode ; short RLE, get bytecode ; Otherwise it's long RLE longrle ld b,a ld a,(ix+Max8Gamma) ld e,a ; ** PARAMETER 111111xxxxxx ld a,b call getbits ; get 3/2/1 more bits to get a full byte, X=2 -> X=0 ld (ix+lzpos),a ; xstore - Save length LSB call getval ; length MSB, X=0 -> X=0 ld (ix+regy),a ; Y is 1 bigger than MSB loops chrcode call getval ; Byte Code, X=0 -> X=0 ld e,a ld a,(ix+tablepu) add a,e ld c,a ld a,(ix+tablepu+1) adc a,0 ld b,a ld a,e cp 32 ; 31-32 -> C set, 32-32 -> C clear.. ld a,(bc) jr c,less32 ; 1..31 ; Not ranks 1..31, -> 11111 # xxxxx (32..64), get byte.. ld a,e ; get back the value (5 valid bits) ld e,3 call getbits ; get 3 more bits to get a full byte, X=3 -> X=0 less32 ld (ix+savel),l ld (ix+saveh),h ld (ix+saved),d ld b,(ix+lzpos) ; xstore - get length LSB inc b ; adjust for cpx#$ff;bne -> bne ld c,(ix+regy) jr z,rlenodecc dec c RLENoDecC ld d,a ld l,(ix+OutPtr) ld h,(ix+OutPtr+1) RLEBufferManager ; D=fill byte, CB=size, HL=dest pointer xor a cp c jr nz,rlebufferoverflowbig ld a,(buffercount) cp b jr c,rlebufferoverflowsmall RLEBufferOK sub a,b RLEBufferOKLoop ld (hl),d inc l djnz rlebufferokloop ld (ix+OutPtr),l or a jr z,rlebufferokreturn RLEBufferOKGoMain ld (buffercount),a ld d,(ix+saved) ld l,(ix+savel) ld h,(ix+saveh) jp main RLEBufferOKReturn ld (ix+returntype),0 ret RLEBufferOverflowSmall ld c,b ld b,a RLEBufferOverflowSmallLoop ld (hl),d inc l djnz rlebufferoverflowsmallloop ld (ix+OutPtr),l ld b,a ld a,c ld c,b sub a,c ld (ix+data1),a ; remaining size ld (ix+data2),0 ld (ix+returntype),1 ret RLEBufferOverflowBig ld a,(buffercount) ld e,a ld a,b ld b,e sub a,b jr nc,rlebufferoverflowbigno16boverflow RLEBufferOverflowBig16bOverflow dec c RLEBufferOverflowBigNo16bOverflow ld (ix+data1),a ld (ix+data2),c ld b,e RLEBufferOverflowBigLoop ld (hl),d inc l djnz rlebufferoverflowbigloop ld (ix+OutPtr),l ld (ix+returntype),1 ret Exit ld l,(ix+StartPtr) ld h,(ix+StartPtr+1) jp restartstream lz77 call getval ; X=0 -> X=0 ld b,a ld a,(ix+Max2Gamma) cp b ; end of file? jr z,exit ; yes, exit ld a,(ix+ExtraBits) ; ** PARAMETER (more bits to get) ld e,a ld a,b dec a ; subtract 1 (1..126 -> 0..125) inc e call getchk ; clears Carry, X=0 -> X=0 lz772 ld e,8 ; offset MSB (lzlen+1) but always = 0 call getbits ; clears Carry, X=8 -> X=0 ; Note; Already eor;ed in the compressor.. ld b,(ix+lzpos) ; xstore - LZLEN (read before it's overwritten) inc b ; adjust for cpx#$ff;bne -> bne add a,(ix+OutPtr) ; -offset -1 + curpos (C is clear) ld (ix+lzpos),a ; Write decompressed bytes out to RAM ld (ix+saved),d ld (ix+savel),l ld (ix+saveh),h LZBufferManager ; A=src oft, B=lz size ld e,(ix+OutPtr) ; copy X+1 number of chars from LZPOS to OUTPOS ld d,(ix+OutPtr+1) ld l,a ld h,d ld a,(buffercount) cp b ; b > buffercount ? jr c,lzbufferoverflow LZBufferOK ld c,a sub a,b cp c jr z,lzbufferoverflow ; b=0 -> b=256 ld c,a LZBufferOKLoop ld a,(hl) ld (de),a inc l inc e djnz lzbufferokloop ld (ix+outptr),e ld a,c or a jr z,lzbufferokreturn LZBufferOKGoMain ld (buffercount),a ld d,(ix+saved) ld l,(ix+savel) ld h,(ix+saveh) jp main LZBufferOKReturn ld (ix+returntype),0 ret LZBufferOverflow ld c,a ld a,b ld b,c sub a,b ld (ix+data1),a LZBufferOverflowLoop ld a,(hl) ld (de),a inc l inc e djnz lzbufferoverflowloop ld (ix+outptr),e ld (ix+data2),l ld (ix+returntype),2 ret GetNextBytes ld a,buffersize ld (buffercount),a ld a,(ix+returntype) or a jr z,returntype0 cp 1 jr z,returntype1 cp 2 jr z,returntype2 ret ReturnType0 ; Simple return ld d,(ix+saved) ld l,(ix+savel) ld h,(ix+saveh) jp main ReturnType1 ; RLE copy management ld l,(ix+OutPtr) ld h,(ix+OutPtr+1) dec l ld d,(hl) inc l ld b,(ix+data1) ld c,(ix+data2) jp rlebuffermanager ReturnType2 ; LZ77 copy management ld b,(ix+data1) ld a,(ix+data2) jp lzbuffermanager ; getval; Gets a 'static huffman coded' value ; ** Scratches X, returns the value in A ** getval ld a,1 ; X must be 0 when called! ld e,a loop0 sla d jr nz,loop1 ld d,(hl) inc hl rl d ; Shift in C=1 (last bit marker) bitstr initial value = &80 == empty loop1 jr nc,getchk ; got 0-bit inc e ld b,a ; save a ld a,(ix+MaxGamma) cp e ld a,b ; restore a jr nz,loop0 jr getchk ; getbits; Gets X bits from the stream ; ** Scratches X, returns the value in A ** get1bit inc e getbits sla d jr nz,loop3 ld d,(hl) inc hl rl d ; Shift in C=1 (last bit marker) bitstr initial value = &80 == empty loop3 rla getchk dec e jr nz,getbits ret BufferCount db 0 ; Data Index Let CurrentPC=$ Org 0 Nocode escPu db 0 OutPtr dw 0 lzpos db 0 EscBits db 0 Esc8Bits db 0 MaxGamma db 0 Max1Gamma db 0 Max2Gamma db 0 Max8Gamma db 0 ExtraBits db 0 tablePu dw 0 regy db 0 StartPtr dw 0 SaveL db 0 SaveH db 0 SaveD db 0 Data1 db 0 Data2 db 0 ReturnType db 0 StreamsContextSize db 0 Org CurrentPC Code StreamsContextAdr ds streamscount*streamscontextsize
Les musiques d'exemple ci-après ont été converties à partir de fichiers YM 5/6 (également fournis). Le programme de conversion ne tournant pas sur CPC et n'ayant pas d'intérêt au regard de cet article sur les sons SID26), je n'ai pas jugé bon de le publier (de toute manière c'est un horrible mix entre C, AmigaDOS et AREXX dont vous ne sauriez que faire).
En jouant ces fichiers AY+ avec le player que je vous fournis ici, vous constaterez que leur rendu sur CPC+ est quasiment identique à celui de la musique originale sur Atari ST. Toutefois, les sons SID sont légèrement moins purs (à cause de la précision des DMA qui est inférieure à celle des timers du ST, mais on peut passer outre en jouant les SID par interruption plutôt que via des boucles DMA). Enfin, certains sons hard ne sonnent pas aussi juste (le YM du ST est à 2MHz, il est donc deux fois plus précis que le AY à 1MHz du CPC ce qui se ressent sur les sons aigus) ou ne sont pas à la même octave (le convertisseur de YM passe en effet les sons hard à l'octave inférieure s'il estime que ça sonnerait trop faux, toujours à cause de la différence de précision entre le YM à 2Mhz et le AY à 1MHz).
Titre de la musique | Auteur | Fichier AY+ | Fichier YM d'origine |
---|---|---|---|
Lemon Sqeezers Dream | Jess | LEMONSQ.AY+ | Lemon Sqeezers Dream.ym |
Lin Wu's Challenge | Tao | LINWU.AY+ | linwu1.ym |
Reality27) | Big Alec | REALITY.AY+ | Reality.ym |
SID Tune 1 | Mad Max | MAD-MAX1.AY+ | Sid Music #1.ym |
Stay Away | Tao | STAY.AY+ | Stay Away.ym |
Synergy DBA Music Disk (4) | Scavenger | FLIPOINT.AY+ | flipointro.ym |
Synergy DBA Music Disk (5) | Scavenger | SYNER5.AY+ | scaven5.ym |
The Cave | Tao | CAVE.AY+ | cave.ym |
The Delegate | Tao | JACIT.AY+ | JACIT.YM |
Virtual Escape (Equinox) Main Tune | Mad Max | VIRTEST2.AY+ | Virtual Escape Main.ym |
Virtual Escape (Equinox) End Tune | Mad Max | VIRTEST3.AY+ | Vitual Escape Final.ym |
&4000
et ne rentrent pas entièrement dans une bank et déborderont en &8000
.&D000
(en mémoire graphique), libre à vous de les déplacer où vous voulez sur une adresse multiple de &100
en faisant bien attention à leur taille.